Esbuild: logform library uses non-string literals in require and fails at runtime, but works with webpack

Created on 21 Oct 2020  路  5Comments  路  Source: evanw/esbuild

The npm library logform does this:

      return require(`./${path}.js`);

The value of path is pretty "static" for a list of local modules.

Running this with esbuild results in this warning:

node_modules/logform/index.js:28:13: warning: This call to "require" will not be bundled because the argument is not a string literal
      return require(`./${path}.js`);
             ~~~~~~~

But at runtime usage of the logform module fails with e.g.

Error: Cannot find module './combine.js'

While I understand that non-string literals are skipped, this actually works fine with webpack (using version 4). So they must have a trick for such a "simple" case that maybe esbuild could replicate.

Most helpful comment

I believe Webpack does this by bundling all files reachable in that directory and all nested directories, which ends up unintentionally including extra stuff in the bundle in this case. I think this is kind of an anti-pattern and I'm not currently planning on including this ability in esbuild. This code in logform can easily be replaced by bare require() calls that are easy for a bundler to analyze.

Webpack has a ton of features both because it's by far the majority bundler and because it's had many years of many contributors adding features. It's not a goal of esbuild to replicate all of Webpack's features, so just because Webpack supports this doesn't necessarily mean esbuild should too, especially if the feature isn't trivial and has negative implications for bundle size.

All 5 comments

I believe Webpack does this by bundling all files reachable in that directory and all nested directories, which ends up unintentionally including extra stuff in the bundle in this case. I think this is kind of an anti-pattern and I'm not currently planning on including this ability in esbuild. This code in logform can easily be replaced by bare require() calls that are easy for a bundler to analyze.

Webpack has a ton of features both because it's by far the majority bundler and because it's had many years of many contributors adding features. It's not a goal of esbuild to replicate all of Webpack's features, so just because Webpack supports this doesn't necessarily mean esbuild should too, especially if the feature isn't trivial and has negative implications for bundle size.

Makes sense. Unfortunately I don't have influence over logform and the code that is using it. Is there some solution? E.g. some config to manually include all the files from e.g. node_modules/logform/*?

You could hop-over the logform/index.js file entirely by creating your own local version of that index file which points to its dependencies. I've made a repo as an example:

https://github.com/heyheyhello/esbuild-logform-example

https://github.com/heyheyhello/esbuild-logform-example/blob/work/index.js#L1-L16

https://github.com/heyheyhello/esbuild-logform-example/blob/work/logform.js#L1-L22

Basically everywhere in your project that you import "logform" now import "./logform" (or setup a non-relative path in your tsconfig.json/jsconfig.json since esbuild will follow that)

Hmm, that seems a bit involved.

Would the inject option work? IIUC you can provide a list of paths there? Or would the resolution not work in the bundled file?

Since esbuild respects paths option in tsconfig.json, you can use a tsconfig.json file to redirect logform to your own file like this:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "logform": [
        "./custom-logform.js"
      ]
    }
  }
}

Where custom-logform.js is logform/index.js modified to avoid dynamic require:


(click to expand custom-logform.js)

'use strict';

/*
 * @api public
 * @property {function} format
 * Both the construction method and set of exposed
 * formats.
 */
const format = exports.format = require('logform/format');

/*
 * @api public
 * @method {function} levels
 * Registers the specified levels with logform.
 */
exports.levels = require('logform/levels');

/*
 * @api private
 * method {function} exposeFormat
 * Exposes a sub-format on the main format object
 * as a lazy-loaded getter.
 */
function exposeFormat(name, requireFormat) {
  Object.defineProperty(format, name, {
    get() {
      return requireFormat();
    },
    configurable: true
  });
}

//
// Setup all transports as lazy-loaded getters.
//
exposeFormat('align', function () { return require('logform/align') });
exposeFormat('errors', function () { return require('logform/errors') });
exposeFormat('cli', function () { return require('logform/cli') });
exposeFormat('combine', function () { return require('logform/combine') });
exposeFormat('colorize', function () { return require('logform/colorize') });
exposeFormat('json', function () { return require('logform/json') });
exposeFormat('label', function () { return require('logform/label') });
exposeFormat('logstash', function () { return require('logform/logstash') });
exposeFormat('metadata', function () { return require('logform/metadata') });
exposeFormat('ms', function () { return require('logform/ms') });
exposeFormat('padLevels', function () { return require('logform/pad-levels') });
exposeFormat('prettyPrint', function () { return require('logform/pretty-print') });
exposeFormat('printf', function () { return require('logform/printf') });
exposeFormat('simple', function () { return require('logform/simple') });
exposeFormat('splat', function () { return require('logform/splat') });
exposeFormat('timestamp', function () { return require('logform/timestamp') });
exposeFormat('uncolorize', function () { return require('logform/uncolorize') });

FWIW there is already an open PR against logform that would resolve this issue: https://github.com/winstonjs/logform/pull/113.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

aelbore picture aelbore  路  3Comments

vforsh picture vforsh  路  3Comments

iamakulov picture iamakulov  路  4Comments

tonyhb picture tonyhb  路  3Comments

mohsen1 picture mohsen1  路  3Comments