Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR for version 0.11.0 #1078

Merged
merged 6 commits into from
Mar 29, 2021
Merged

PR for version 0.11.0 #1078

merged 6 commits into from
Mar 29, 2021

Conversation

evanw
Copy link
Owner

@evanw evanw commented Mar 28, 2021

This is a PR for version 0.11.0, which is a release that contains the following breaking changes:

  • Change how require() and import() of ESM works (fixes Circular dependency issue #667, fixes ESM entry module's exports signature is not preserved when required() by other modules #706)

    Previously if you call require() on an ESM file, or call import() on an ESM file with code splitting disabled, esbuild would convert the ESM file to CommonJS. For example, if you had the following input files:

    // cjs-file.js
    console.log(require('./esm-file.js').foo)
    // esm-file.js
    export let foo = bar()

    The previous bundling behavior would generate something like this:

    var require_esm_file = __commonJS((exports) => {
      __markAsModule(exports);
      __export(exports, {
        foo: () => foo
      });
      var foo = bar();
    });
    console.log(require_esm_file().foo);

    This behavior has been changed and esbuild now generates something like this instead:

    var esm_file_exports = {};
    __export(esm_file_exports, {
      foo: () => foo
    });
    var foo;
    var init_esm_file = __esm(() => {
      foo = bar();
    });
    console.log((init_esm_file(), esm_file_exports).foo);

    The variables have been pulled out of the lazily-initialized closure and are accessible to the rest of the module's scope. Some benefits of this approach:

    • If another file does import {foo} from "./esm-file.js", it will just reference foo directly and will not pay the performance penalty or code size overhead of the dynamic property accesses that come with CommonJS-style exports. So this improves performance and reduces code size in some cases.

    • This fixes a long-standing bug (ESM entry module's exports signature is not preserved when required() by other modules #706) where entry point exports could be broken if the entry point is a target of a require() call and the output format was ESM. This happened because previously calling require() on an entry point converted it to CommonJS, which then meant it only had a single default export, and the exported variables were inside the CommonJS closure and inaccessible to an ESM-style export {} clause. Now calling require() on an entry point only causes it to be lazily-initialized but all exports are still in the module scope and can still be exported using a normal export {} clause.

    • Now that this has been changed, import() of a module with top-level await (support for top level await #253) is now allowed when code splitting is disabled. Previously this didn't work because import() with code splitting disabled was implemented by converting the module to CommonJS and using Promise.resolve().then(() => require()), but converting a module with top-level await to CommonJS is impossible because the CommonJS call signature must be synchronous. Now that this implemented using lazy initialization instead of CommonJS conversion, the closure wrapping the ESM file can now be async and the import() expression can be replaced by a call to the lazy initializer.

    • Adding the ability for ESM files to be lazily-initialized is an important step toward additional future code splitting improvements including: manual chunk names ([Feature] Manual chunks #207), correct import evaluation order (Incorrect import order with code splitting and multiple entry points #399), and correct top-level await evaluation order (support for top level await #253). These features all need to make use of deferred evaluation of ESM code.

    In addition, calling require() on an ESM file now recursively wraps all transitive dependencies of that file instead of just wrapping that ESM file itself. This is an increase in the size of the generated code, but it is important for correctness (Circular dependency issue #667). Calling require() on a module means its evaluation order is determined at run-time, which means the evaluation order of all dependencies must also be determined at run-time. If you don't want the increase in code size, you should use an import statement instead of a require() call.

  • Dynamic imports now use chunk names instead of entry names (fixes --entry-names=[name] errors out for some dynamic imports #1056)

    Previously the output paths of dynamic imports (files imported using the import() syntax) were determined by the --entry-names= setting. However, this can cause problems if you configure the --entry-names= setting to omit both [dir] and [hash] because then two dynamic imports with the same name will cause an output file name collision.

    Now dynamic imports use the --chunk-names= setting instead, which is used for automatically-generated chunks. This setting is effectively required to include [hash] so dynamic import name collisions should now be avoided.

    In addition, dynamic imports no longer affect the automatically-computed default value of outbase. By default outbase is computed to be the lowest common ancestor directory of all entry points. Previously dynamic imports were considered entry points in this calculation so adding a dynamic entry point could unexpectedly affect entry point output file paths. This issue has now been fixed.

  • Allow custom output paths for individual entry points

    By default, esbuild will automatically generate an output path for each entry point by computing the relative path from the outbase directory to the entry point path, and then joining that relative path to the outdir directory. The output path can be customized using outpath, but that only works for a single file. Sometimes you may need custom output paths while using multiple entry points. You can now do this by passing the entry points as a map instead of an array:

    • CLI

      esbuild out1=in1.js out2=in2.js --outdir=out
      
    • JS

      esbuild.build({
        entryPoints: {
          out1: 'in1.js',
          out2: 'in2.js',
        },
        outdir: 'out',
      })
    • Go

      api.Build(api.BuildOptions{
        EntryPointsAdvanced: []api.EntryPoint{{
          OutputPath: "out1",
          InputPath: "in1.js",
        }, {
          OutputPath: "out2",
          InputPath: "in2.js",
        }},
        Outdir: "out",
      })

    This will cause esbuild to generate the files out/out1.js and out/out2.js inside the output directory. These custom output paths are used as input for the --entry-names= path template setting, so you can use something like --entry-names=[dir]/[name]-[hash] to add an automatically-computed hash to each entry point while still using the custom output path.

  • Derive entry point output paths from the original input (fixes onResolve shouldn't change output path for entry points #945)

    Previously esbuild would determine the output path for an entry point by looking at the post-resolved path. For example, running esbuild --bundle react --outdir=out would generate the output path out/index.js because the input path react was resolved to node_modules/react/index.js. With this release, the output path is now determined by looking at the pre-resolved path. For example, running esbuild --bundle react --outdir=out now generates the output path out/react.js. If you need to keep using the output path that esbuild previously generated with the old behavior, you can use the custom output path feature (described above).

  • Use the file namespace for file entry points (fixes onResolve is not fired on entrypoints when namespace equals 'file' #791)

    Plugins that contain an onResolve callback with the file filter don't apply to entry point paths because it's not clear that entry point paths are files. For example, you could potentially bundle an entry point of https://www.example.com/file.js with a HTTP plugin that automatically downloads data from the server at that URL. But this behavior can be unexpected for people writing plugins.

    With this release, esbuild will do a quick check first to see if the entry point path exists on the file system before running plugins. If it exists as a file, the namespace will now be file for that entry point path. This only checks the exact entry point name and doesn't attempt to search for the file, so for example it won't handle cases where you pass a package path as an entry point or where you pass an entry point without an extension. Hopefully this should help improve this situation in the common case where the entry point is an exact path.

@evanw evanw merged commit 20e603a into master Mar 29, 2021
@evanw evanw deleted the 0.11.0-wip branch March 29, 2021 02:34
@zaydek
Copy link

zaydek commented Mar 29, 2021

Thank you! The ability to alias entry names should be really useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment