diff --git a/doc/api/esm.md b/doc/api/esm.md index 3d7882c98d9302..153270bff2b10e 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -358,6 +358,9 @@ Node.js supports the following conditions: * `"node"` - matched for any Node.js environment. Can be a CommonJS or ES module file. _This condition should always come after `"import"` or `"require"`._ +* `"production"` - matched if `process.env.NODE_ENV` is set to `production`. +* `"development"` - matched if `process.env.NODE_ENV` is not set to `production` + so that `"production"` and `"development"` are fully mutually exclusive. * `"default"` - the generic fallback that will always match. Can be a CommonJS or ES module file. _This condition should always come last._ @@ -383,6 +386,10 @@ Conditional exports can also be extended to exports subpaths, for example: "exports": { ".": "./main.js", "./feature": { + "production": { + "browser": "./feature-browser-production.js", + "default": "./feature-production.js" + }, "browser": "./feature-browser.js", "default": "./feature.js" } @@ -391,8 +398,9 @@ Conditional exports can also be extended to exports subpaths, for example: ``` Defines a package where `require('pkg/feature')` and `import 'pkg/feature'` -could provide different implementations between the browser and Node.js, -given third-party tool support for a `"browser"` condition. +could provide different implementations between the browser and Node.js +given third-party tool support for a `"browser"` condition, while also +supporting production and development variants. #### Nested conditions @@ -1472,7 +1480,9 @@ In the following algorithms, all subroutine errors are propagated as errors of these top-level routines unless stated otherwise. _defaultEnv_ is the conditional environment name priority array, -`["node", "import"]`. +`["node", "import", "development"]`. If `process.env.NODE_ENV` is set to +`production` then the `"production"` condition is used in place of +`"development"`. The resolver can throw the following errors: * _Invalid Module Specifier_: Module specifier is an invalid URL, package name diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 96aa4e2ebfc1aa..9bee7d8bacc24b 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -524,6 +524,8 @@ function isArrayIndex(p) { return n >= 0 && n < (2 ** 32) - 1; } +const devCondition = + process.env.NODE_ENV === 'production' ? 'production' : 'development'; function resolveExportsTarget(baseUrl, target, subpath, mappingKey) { if (typeof target === 'string') { let resolvedTarget, resolvedTargetPath; @@ -575,6 +577,14 @@ function resolveExportsTarget(baseUrl, target, subpath, mappingKey) { } for (const p of keys) { switch (p) { + case devCondition: + try { + return resolveExportsTarget(baseUrl, target[p], subpath, + mappingKey); + } catch (e) { + if (e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw e; + } + break; case 'node': case 'require': try { diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index 04c6abe54269f6..b344f4fb2f261d 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -46,7 +46,11 @@ const { ERR_UNSUPPORTED_ESM_URL_SCHEME, } = require('internal/errors').codes; -const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import']); +const DEFAULT_CONDITIONS = ObjectFreeze([ + 'node', + 'import', + process.env.NODE_ENV === 'production' ? 'production' : 'development' +]); const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS); function getConditionsSet(conditions) { diff --git a/test/es-module/test-esm-exports-dev-production.mjs b/test/es-module/test-esm-exports-dev-production.mjs new file mode 100644 index 00000000000000..bc78da074392ee --- /dev/null +++ b/test/es-module/test-esm-exports-dev-production.mjs @@ -0,0 +1,27 @@ +// Flags: --experimental-wasm-modules +import '../common/index.mjs'; +import { path } from '../common/fixtures.mjs'; +import { strictEqual } from 'assert'; +import { spawnSync } from 'child_process'; + +{ + const output = spawnSync(process.execPath, [path('/pkgexports-dev.mjs')], { + env: Object.assign({}, process.env, { + NODE_ENV: 'production' + }) + }); + strictEqual(output.stdout.toString().trim(), 'production'); +} + +{ + const output = spawnSync(process.execPath, [path('/pkgexports-dev.mjs')], { + }); + strictEqual(output.stdout.toString().trim(), 'development'); +} + +{ + const output = spawnSync(process.execPath, [path('/pkgexports-dev.mjs')], { + NODE_ENV: 'any' + }); + strictEqual(output.stdout.toString().trim(), 'development'); +} diff --git a/test/fixtures/es-module-loaders/loader-with-custom-condition.mjs b/test/fixtures/es-module-loaders/loader-with-custom-condition.mjs index 78ffd75e6be27b..9518bde0ebaa05 100644 --- a/test/fixtures/es-module-loaders/loader-with-custom-condition.mjs +++ b/test/fixtures/es-module-loaders/loader-with-custom-condition.mjs @@ -1,8 +1,11 @@ import {ok, deepStrictEqual} from 'assert'; +const env = + process.env.NODE_ENV === 'production' ? 'production' : 'development'; + export async function resolve(specifier, context, defaultResolve) { ok(Array.isArray(context.conditions), 'loader receives conditions array'); - deepStrictEqual([...context.conditions].sort(), ['import', 'node']); + deepStrictEqual([...context.conditions].sort(), [env, 'import', 'node']); return defaultResolve(specifier, { ...context, conditions: ['custom-condition', ...context.conditions], diff --git a/test/fixtures/node_modules/pkgexports-dev/dev.js b/test/fixtures/node_modules/pkgexports-dev/dev.js new file mode 100644 index 00000000000000..41bd0ae0260c0c --- /dev/null +++ b/test/fixtures/node_modules/pkgexports-dev/dev.js @@ -0,0 +1 @@ +module.exports = 'development'; diff --git a/test/fixtures/node_modules/pkgexports-dev/dev.mjs b/test/fixtures/node_modules/pkgexports-dev/dev.mjs new file mode 100644 index 00000000000000..a54d8245409883 --- /dev/null +++ b/test/fixtures/node_modules/pkgexports-dev/dev.mjs @@ -0,0 +1 @@ +export default 'development'; diff --git a/test/fixtures/node_modules/pkgexports-dev/package.json b/test/fixtures/node_modules/pkgexports-dev/package.json new file mode 100644 index 00000000000000..983149f7713eb3 --- /dev/null +++ b/test/fixtures/node_modules/pkgexports-dev/package.json @@ -0,0 +1,12 @@ +{ + "exports": { + "development": { + "require": "./dev.js", + "import": "./dev.mjs" + }, + "production": { + "require": "./prod.js", + "import": "./prod.mjs" + } + } +} diff --git a/test/fixtures/node_modules/pkgexports-dev/prod.js b/test/fixtures/node_modules/pkgexports-dev/prod.js new file mode 100644 index 00000000000000..e1fc2b9b665e96 --- /dev/null +++ b/test/fixtures/node_modules/pkgexports-dev/prod.js @@ -0,0 +1 @@ +module.exports = 'production'; diff --git a/test/fixtures/node_modules/pkgexports-dev/prod.mjs b/test/fixtures/node_modules/pkgexports-dev/prod.mjs new file mode 100644 index 00000000000000..d2cb21188b6873 --- /dev/null +++ b/test/fixtures/node_modules/pkgexports-dev/prod.mjs @@ -0,0 +1 @@ +export default 'production'; diff --git a/test/fixtures/pkgexports-dev.mjs b/test/fixtures/pkgexports-dev.mjs new file mode 100644 index 00000000000000..19185972818897 --- /dev/null +++ b/test/fixtures/pkgexports-dev.mjs @@ -0,0 +1,17 @@ +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; +import { strictEqual } from 'assert'; + +const require = createRequire(fileURLToPath(import.meta.url)); + +const production = process.env.NODE_ENV === 'production'; +const expectValue = production ? 'production' : 'development'; + +strictEqual(require('pkgexports-dev'), expectValue); + +(async () => { + const { default: value } = await import('pkgexports-dev'); + strictEqual(value, expectValue); + + console.log(expectValue); +})();