esbuild icon indicating copy to clipboard operation
esbuild copied to clipboard

Dynamic import not resolved during build

Open Paul-Browne opened this issue 9 months ago • 1 comments

I have this simple client-side SPA router that will dynamically import a script based on the url eg. example.com/foo/bar will fire a request for example.com/js/pages/foo/bar.js

I'm handling 404's in the catch statement like so:

const router = async () => {
    const path = window.location.pathname;
    const resourcePath = `/js/pages${path}.js`.replace("/.js", ".js");
    try {
        (await import(resourcePath)).default({ state: window.state });
    } catch {
        console.error(`Page not found: ${resourcePath}`);
        (await import('/js/pages/404.js')).default({ state: window.state });
    }
}

The problem is that the build is trying to resolve the 404 import at build time. And even thought the file exists, it still doesn't find it...

✘ [ERROR] Could not resolve "/js/pages/404.js"

    src/js/spa_router.js:10:22:
      10 │         (await import('/js/pages/404.js')).default({ state: window.state });
         ╵                       ~~~~~~~~~~~~~~~~~~

Error: Build failed with 1 error:
src/js/spa_router.js:10:22: ERROR: Could not resolve "/js/pages/404.js"

wrapping the 404 import in another try-catch solves the problem and the builder works just fine, but it shouldn't need to be done this way IMO. And I don't understand why it doesn't find the 404.js (yes, very ironic)

const router = async () => {
    const path = window.location.pathname;
    const resourcePath = `/js/pages${path}.js`.replace("/.js", ".js");
    try {
        (await import(resourcePath)).default({ state: window.state });
    } catch {
        console.error(`Page not found: ${resourcePath}`);
        try {
            (await import('/js/pages/404.js')).default({ state: window.state });
        } catch {}
    }
}

This is the build script

await esbuild.build({
    entryPoints: ['src/js/**/*.js'],
    bundle: true,
    minify: true,
    sourcemap: true,
    splitting: true,
    treeShaking: true,
    format: "esm",
    target: "esnext",
    outdir: 'public/js'
})

Paul-Browne avatar May 13 '24 10:05 Paul-Browne

This is because "/js/pages/404.js" is an absolute path. esbuild runs its node-behavior resolver against those paths in import(path) and require(path) and includes (bundles) them in the bundle. There's no such file with the absolute path in your file system.

If it is in a single build (i.e. in one esbuild.build() call), you can reference 404.js with relative path, e.g. "./pages/404.js".

On the other hand, you may mark this path as external through a plugin, roughly look like this:

var custom_external_plugin = {
  name: "custom-external",
  setup({ onResolve }) {
    onResolve({ filter: /404\.js$/ }, args => {
      return { path: args.path, external: true }
    })
  }
}

hyrious avatar May 13 '24 20:05 hyrious