evanw/esbuild

[Feature] Dynamic import w/ template literals w/ variables

Closed this issue ยท 16 comments

Dynamic import w/ template literals w/ variables problem. Used esbuild@0.0.15.

// x.js
export const a = 1;

// test.js
const name = 'x';
(async () => {
    await import(`./${name}.js`);
})();

Run: esbuild --bundle test.js --outfile=build.js

Expected output:

Wrote to build.js

Actual output:

test.js:3:17: error: The argument to import() must be a string literal
evanw commented

Bundling with esbuild is intended to traverse your whole dependency tree and include all of your dependencies in the bundle. However, it can only do this if the dependency tree is statically analyzable. This is why esbuild requires string literals. Dynamically-computed import paths are not statically analyzable.

Can you say more about your use case? What are you using this to try to do?

Is it possible to extract all the dynamic parts of a template literal expression and handle them like a * wildcard?

An example use case would be dynamically loading translation files which is what I use it for at work. e.g. import(`/translations/${language}`)

@traverse do you have a bundle per language or do you import the language at runtime?

@kilianc the translations files are simple JSON files per language but since we're using webpack it creates bundles and a bundle map for us so that they can be loaded dynamically at runtime. You can find some more info here https://webpack.js.org/api/module-methods/#dynamic-expressions-in-import. Like it states in the documentation though the path can't be fully dynamic.

evanw commented

Since it's relevant to this thread: as of version 0.5.3, dynamic imports with template literals with variables are now a warning, not an error. The dynamic import is passed through to the output code unchanged.

At the moment I don't have any plans to emulate a virtual file system like Webpack does. That feature in Webpack seems too complicated and special-cased to be built into esbuild itself in my opinion, especially with all of the Webpack-specific comment directives.

One option is to wait for esbuild to have plugin support and then write a plugin for this (see #111). Another option is to just rewrite the import expression in your source code if it is intended to be statically-determined and included in the bundle. So instead of this:

let translation = await import(`translations/${language}.json`)

You could just do something like this instead:

let translation = await {
  'en-US': () => import(`translations/en-US.json`),
  'zh-CN': () => import(`translations/zh-CN.json`),
  // ...
}[language]()
evanw commented

I'm going to close this issue "won't fix" as explained in my post above.

moos commented

Unfortunately the suggested work-around doesn't scale for larger (legacy) code bases where a dir substructure must be loaded. Webpack has variable dynamic imports and Parcel has the super useful glob import:

import foo from "/assets/*.png";

(but Parcel has other shortcomings, namely handling externals).

I suppose one could run a glob preprocessor and generate statically linked imports -- but having it in the tool makes it a lot easier. Or as a plugin, when that's available.

@moos I think the plugin API will fit this beautifully. You'll be able to catch the "*" and return an object with anything you like

Not sure if anyone else has come up with a better solution, but I was able to create a plugin that will look for dynamic imports that have a template literal and a variable inside. Transform that variable into a glob i.e. /foo/bar/${baz}.vue -> /foo/bar/**/*.vue. Check the FS using fast-glob for all possible files matching that glob and generate static imports for those files. Which then the statically imported modules would be referenced by the dynamic imports instead.

I was just able to get it working for my pretty large code base and am working on cleaning up the code. Seems like my plugin accomplishes what is talked about in this issue. I can check back in when I get it published if anyone is interested.

@kalvenschraut could you share your plugin?

@AndrewBogdanovTSS See above comment if you have not seen it yet

its not completely clear to me is:

let translation = await import(`translations/${language}.json`)

working at runtime? if i would copy the files in that translations directory myself?

because doing it statically is a nogo for us then we never are able to use esbuild for building because in our current product that would mean 1271 lines of code....
(and that could suddenly be more if 3rd party stuff like angular,uppy,numbro and other components would suddenly add more language or locale files that i then constantly have to track)

currently webpack build does copy all the dynamic files (1271 for our current product) and i can use that dynamic import
i can live with esbuild not copy it for me, i can do that manually somewhere as long as it then works at runtime.....

Here it is

https://github.com/RtVision/esbuild-dynamic-import

i see you make it static imports what does that exactly mean?
they are still lazy loaded when needed right? and only the js/json file is loaded at runtime that i ask for?

No they are still imports at being added at the top of the file which was fine for my use case at the time. In theory I think it would work to make them dynamic imports of the specific file but I haven't tested it. Main thing that esbuild support IIRC is the template literal parse so if the plugin were to enumerate all the options as dynamic imports instead of static ones at the top then hopefully would work.

Vite does something similar internally, they have option to preload the imports or still keep them lazy. Though this is more so when using globs, which is very similar to how these template literals work anyway on the bundler side. They add a method that you call that they then compile down. import.meta.glob.