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

module: development and production exports conditions #32869

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not clear on how this works at all. is this production for "require" or for "import"? what about for "browser" or for "node"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just updated the example to a case where production and browser are used together. These conditions are fully orthogonal if we are still allowed to use that word, so by default it applies for both require and import.

* `"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._

Expand All @@ -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"
}
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 5 additions & 1 deletion lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
27 changes: 27 additions & 0 deletions test/es-module/test-esm-exports-dev-production.mjs
Original file line number Diff line number Diff line change
@@ -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');
}
Original file line number Diff line number Diff line change
@@ -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],
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/node_modules/pkgexports-dev/dev.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/fixtures/node_modules/pkgexports-dev/dev.mjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions test/fixtures/node_modules/pkgexports-dev/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/fixtures/node_modules/pkgexports-dev/prod.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/fixtures/node_modules/pkgexports-dev/prod.mjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions test/fixtures/pkgexports-dev.mjs
Original file line number Diff line number Diff line change
@@ -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);
})();