From 1a458e0bf581737e4962e8d33f0af01672b708e4 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Mon, 8 Mar 2021 00:01:16 -0800 Subject: [PATCH] implement custom conditions for "exports" --- CHANGELOG.md | 2 ++ internal/bundler/bundler_packagejson_test.go | 28 +++++++++++++++++++ .../snapshots/snapshots_packagejson.txt | 6 ++++ internal/config/config.go | 1 + internal/resolver/resolver.go | 3 ++ lib/common.ts | 10 +++++++ lib/types.ts | 1 + pkg/api/api.go | 1 + pkg/api/api_impl.go | 4 +++ pkg/cli/cli_impl.go | 3 ++ 10 files changed, 59 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0a5fe7fd05..929bdcacbe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,8 @@ Note that when you use conditions, _your package may end up in the bundle multiple times!_ This is a subtle issue that can cause bugs due to duplicate copies of your code's state in addition to bloating the resulting bundle. This is commonly known as the [dual package hazard](https://nodejs.org/docs/latest/api/packages.html#packages_dual_package_hazard). The primary way of avoiding this is to put all of your code in the `require` condition and have the `import` condition just be a light wrapper that calls `require` on your package and re-exports the package using ESM syntax. + There is also support for custom conditions with the `--conditions=` flag. The meaning of these is entirely up to package authors. For example, you could imagine a package that requires you to configure `--conditions=test,en-US`. Node has currently only endorsed the `development` and `production` custom conditions for recommended use. + ## 0.8.57 * Fix overlapping chunk names when code splitting is active ([#928](https://github.com/evanw/esbuild/issues/928)) diff --git a/internal/bundler/bundler_packagejson_test.go b/internal/bundler/bundler_packagejson_test.go index b95e7b242cd..33bfc0d7e6d 100644 --- a/internal/bundler/bundler_packagejson_test.go +++ b/internal/bundler/bundler_packagejson_test.go @@ -1663,3 +1663,31 @@ Users/user/project/node_modules/pkg1/package.json: note: The module specifier ". `, }) } + +func TestPackageJsonExportsCustomConditions(t *testing.T) { + packagejson_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/Users/user/project/src/entry.js": ` + import 'pkg1' + `, + "/Users/user/project/node_modules/pkg1/package.json": ` + { + "exports": { + "custom1": "./custom1.js", + "custom2": "./custom2.js", + "default": "./default.js" + } + } + `, + "/Users/user/project/node_modules/pkg1/custom2.js": ` + console.log('SUCCESS') + `, + }, + entryPaths: []string{"/Users/user/project/src/entry.js"}, + options: config.Options{ + Mode: config.ModeBundle, + AbsOutputFile: "/Users/user/project/out.js", + Conditions: []string{"custom2"}, + }, + }) +} diff --git a/internal/bundler/snapshots/snapshots_packagejson.txt b/internal/bundler/snapshots/snapshots_packagejson.txt index 30beefc199a..9b139b29ab8 100644 --- a/internal/bundler/snapshots/snapshots_packagejson.txt +++ b/internal/bundler/snapshots/snapshots_packagejson.txt @@ -430,6 +430,12 @@ TestPackageJsonExportsBrowser // Users/user/project/node_modules/pkg/browser.js console.log("SUCCESS"); +================================================================================ +TestPackageJsonExportsCustomConditions +---------- /Users/user/project/out.js ---------- +// Users/user/project/node_modules/pkg1/custom2.js +console.log("SUCCESS"); + ================================================================================ TestPackageJsonExportsDefaultOverImportAndRequire ---------- /Users/user/project/out.js ---------- diff --git a/internal/config/config.go b/internal/config/config.go index 675ff6c6a4b..2996c453641 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -201,6 +201,7 @@ type Options struct { ExtensionOrder []string MainFields []string + Conditions []string AbsNodePaths []string // The "NODE_PATH" variable from Node.js ExternalModules ExternalModules diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index 864215b2642..c0e7bcafa93 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -198,6 +198,9 @@ func NewResolver(fs fs.FS, log logger.Log, caches *cache.CacheSet, options confi esmConditionsDefault := map[string]bool{} esmConditionsImport := map[string]bool{"import": true} esmConditionsRequire := map[string]bool{"require": true} + for _, condition := range options.Conditions { + esmConditionsDefault[condition] = true + } switch options.Platform { case config.PlatformBrowser: esmConditionsDefault["browser"] = true diff --git a/lib/common.ts b/lib/common.ts index b3464d8ded8..2c40c7728cc 100644 --- a/lib/common.ts +++ b/lib/common.ts @@ -176,6 +176,7 @@ function flagsForBuildOptions( let resolveExtensions = getFlag(options, keys, 'resolveExtensions', mustBeArray); let nodePathsInput = getFlag(options, keys, 'nodePaths', mustBeArray); let mainFields = getFlag(options, keys, 'mainFields', mustBeArray); + let conditions = getFlag(options, keys, 'conditions', mustBeArray); let external = getFlag(options, keys, 'external', mustBeArray); let loader = getFlag(options, keys, 'loader', mustBeObject); let outExtension = getFlag(options, keys, 'outExtension', mustBeObject); @@ -235,6 +236,15 @@ function flagsForBuildOptions( } flags.push(`--main-fields=${values.join(',')}`); } + if (conditions) { + let values: string[] = []; + for (let value of conditions) { + value += ''; + if (value.indexOf(',') >= 0) throw new Error(`Invalid condition: ${value}`); + values.push(value); + } + flags.push(`--conditions=${values.join(',')}`); + } if (external) for (let name of external) flags.push(`--external:${name}`); if (banner) { for (let type in banner) { diff --git a/lib/types.ts b/lib/types.ts index 55bca7f7477..afd1f29a645 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -44,6 +44,7 @@ export interface BuildOptions extends CommonOptions { loader?: { [ext: string]: Loader }; resolveExtensions?: string[]; mainFields?: string[]; + conditions?: string[]; write?: boolean; tsconfig?: string; outExtension?: { [ext: string]: string }; diff --git a/pkg/api/api.go b/pkg/api/api.go index 82b8f1af8ab..d8904f5433e 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -254,6 +254,7 @@ type BuildOptions struct { Format Format External []string MainFields []string + Conditions []string // For the "exports" field in "package.json" Loader map[string]Loader ResolveExtensions []string Tsconfig string diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index 799f9aa498b..3494c85596a 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -737,6 +737,7 @@ func rebuildImpl( ExternalModules: validateExternals(log, realFS, buildOpts.External), TsConfigOverride: validatePath(log, realFS, buildOpts.Tsconfig, "tsconfig path"), MainFields: buildOpts.MainFields, + Conditions: append([]string{}, buildOpts.Conditions...), PublicPath: buildOpts.PublicPath, KeepNames: buildOpts.KeepNames, InjectAbsPaths: make([]string, len(buildOpts.Inject)), @@ -749,6 +750,9 @@ func rebuildImpl( WatchMode: buildOpts.Watch != nil, Plugins: plugins, } + if options.MainFields != nil { + options.MainFields = append([]string{}, options.MainFields...) + } for i, path := range buildOpts.Inject { options.InjectAbsPaths[i] = validatePath(log, realFS, path, "inject path") } diff --git a/pkg/cli/cli_impl.go b/pkg/cli/cli_impl.go index 187fc261cd6..e80af891ad0 100644 --- a/pkg/cli/cli_impl.go +++ b/pkg/cli/cli_impl.go @@ -180,6 +180,9 @@ func parseOptionsImpl(osArgs []string, buildOpts *api.BuildOptions, transformOpt case strings.HasPrefix(arg, "--main-fields=") && buildOpts != nil: buildOpts.MainFields = strings.Split(arg[len("--main-fields="):], ",") + case strings.HasPrefix(arg, "--conditions=") && buildOpts != nil: + buildOpts.Conditions = strings.Split(arg[len("--conditions="):], ",") + case strings.HasPrefix(arg, "--public-path=") && buildOpts != nil: buildOpts.PublicPath = arg[len("--public-path="):]