diff --git a/doc/api/cli.md b/doc/api/cli.md index e6310b279f424c..1f9dfa858c7440 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -127,6 +127,15 @@ added: v12.0.0 Specify the file name of the CPU profile generated by `--cpu-prof`. +### `--dev` + + +> Stability: 1 - Experimental + +Enables the `"development"` [conditional export][] in package resolution. + ### `--disable-proto=mode` +* `--dev` * `--disable-proto` * `--enable-fips` * `--enable-source-maps` @@ -1495,3 +1505,4 @@ $ node --max-old-space-size=1536 index.js [jitless]: https://v8.dev/blog/jitless [libuv threadpool documentation]: http://docs.libuv.org/en/latest/threadpool.html [remote code execution]: https://www.owasp.org/index.php/Code_Injection +[conditional export]: esm.html##esm_conditional_exports diff --git a/doc/api/esm.md b/doc/api/esm.md index 5047600f988f66..1882d7fe01808d 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -358,6 +358,8 @@ 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"`._ +* `"development"` - enabled by the `--dev` flag in Node.js, this allows code + paths to be loaded that provide additional debugging information. * `"default"` - the generic fallback that will always match. Can be a CommonJS or ES module file. _This condition should always come last._ @@ -383,6 +385,10 @@ Conditional exports can also be extended to exports subpaths, for example: "exports": { ".": "./main.js", "./feature": { + "development": { + "browser": "./feature-browser-dev.js", + "default": "./feature-dev.js" + }, "browser": "./feature-browser.js", "default": "./feature.js" } @@ -392,7 +398,8 @@ 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. +given third-party tool support for a `"browser"` condition, as well as +loading different code between development and production environments. #### Nested conditions @@ -1471,8 +1478,8 @@ future updates. 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"]`. +_defaultEnv_ is the conditional environment array, `["node", "import"]`, +including the `"development"` condition if the `--dev` flag is set. The resolver can throw the following errors: * _Invalid Module Specifier_: Module specifier is an invalid URL, package name diff --git a/doc/node.1 b/doc/node.1 index ec2642170aee6f..fd02c853e04ff8 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -100,6 +100,9 @@ The default is File name of the V8 CPU profile generated with .Fl -cpu-prof . +.It Fl -dev +Enables the development mode conditional exports resolution. +. .It Fl -disable-proto Ns = Ns Ar mode Disable the `Object.prototype.__proto__` property. If .Ar mode diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index f2715d07cb44fb..daf5a05cf0a991 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -71,6 +71,7 @@ const { loadNativeModule } = require('internal/modules/cjs/helpers'); const { getOptionValue } = require('internal/options'); +const development = getOptionValue('--dev'); const enableSourceMaps = getOptionValue('--enable-source-maps'); const preserveSymlinks = getOptionValue('--preserve-symlinks'); const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); @@ -575,6 +576,16 @@ function resolveExportsTarget(baseUrl, target, subpath, mappingKey) { } for (const p of keys) { switch (p) { + case 'development': + if (development) { + 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 8b8582544494a5..640051620471a8 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -33,6 +33,7 @@ const { sep } = require('path'); const preserveSymlinks = getOptionValue('--preserve-symlinks'); const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); +const development = getOptionValue('--dev'); const typeFlag = getOptionValue('--input-type'); const { URL, pathToFileURL, fileURLToPath } = require('internal/url'); const { @@ -46,7 +47,11 @@ const { ERR_UNSUPPORTED_ESM_URL_SCHEME, } = require('internal/errors').codes; -const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import']); +const DEFAULT_CONDITIONS = ObjectFreeze([ + 'node', + 'import', + ...development ? ['development'] : [] +]); const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS); function getConditionsSet(conditions) { diff --git a/src/node_options.cc b/src/node_options.cc index 3b9142c19e98a8..89cd8c4f6599be 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -271,6 +271,10 @@ DebugOptionsParser::DebugOptionsParser() { } EnvironmentOptionsParser::EnvironmentOptionsParser() { + AddOption("--dev", + "experimental development mode", + &EnvironmentOptions::development, + kAllowedInEnvironment); AddOption("--enable-source-maps", "experimental Source Map V3 support", &EnvironmentOptions::enable_source_maps, diff --git a/src/node_options.h b/src/node_options.h index 539e41e67ac6ee..ac9f2cab6e0ba7 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -100,6 +100,7 @@ class DebugOptions : public Options { class EnvironmentOptions : public Options { public: bool abort_on_uncaught_exception = false; + bool development = false; bool enable_source_maps = false; bool experimental_json_modules = false; bool experimental_modules = false; diff --git a/test/es-module/test-esm-exports-dev.mjs b/test/es-module/test-esm-exports-dev.mjs new file mode 100644 index 00000000000000..3c5967266246a6 --- /dev/null +++ b/test/es-module/test-esm-exports-dev.mjs @@ -0,0 +1,16 @@ +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')]); + console.log(output.stderr.toString()); + strictEqual(output.stdout.toString().trim(), 'production'); +} + +{ + const output = spawnSync(process.execPath, + ['--dev', path('/pkgexports-dev.mjs')]); + 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..f6ec19fb855f90 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,13 @@ import {ok, deepStrictEqual} from 'assert'; +const dev = process.env.NODE_ENV === '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].filter(c => c !== 'development').sort(), + ['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..7f22b2394b0b3f --- /dev/null +++ b/test/fixtures/node_modules/pkgexports-dev/package.json @@ -0,0 +1,10 @@ +{ + "exports": { + "development": { + "require": "./dev.js", + "import": "./dev.mjs" + }, + "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..3d5494d1db4352 --- /dev/null +++ b/test/fixtures/pkgexports-dev.mjs @@ -0,0 +1,12 @@ +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; +import { strictEqual, AssertionError } from 'assert'; + +const require = createRequire(fileURLToPath(import.meta.url)); +const requireVal = require('pkgexports-dev'); + +(async () => { + const { default: importVal } = await import('pkgexports-dev'); + strictEqual(requireVal, importVal); + console.log(importVal); +})();