diff --git a/doc/api/cli.md b/doc/api/cli.md index 293b337bcc013c..58ecd7d403467d 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -81,6 +81,21 @@ $ node --completion-bash > node_bash_completion $ source node_bash_completion ``` +### `-u`, `--conditions=condition` + + +> Stability: 1 - Experimental + +Enable experimental support for custom conditional exports resolution +conditions. + +Any number of custom string condition names are permitted. + +The default Node.js conditions of `"node"`, `"default"`, `"import"`, and +`"require"` will always apply as defined. + ### `--cpu-prof` +* `--conditions`, `-u` * `--diagnostic-dir` * `--disable-proto` * `--enable-fips` diff --git a/doc/api/esm.md b/doc/api/esm.md index 268ccff5f99f7c..afa4724dea484b 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -501,6 +501,21 @@ a nested conditional does not have any mapping it will continue checking the remaining conditions of the parent condition. In this way nested conditions behave analogously to nested JavaScript `if` statements. +#### Resolving user conditions + +When running Node.js, custom user conditions can be added with the +`--conditions` or `-u` flag: + +```bash +node --conditions=development main.js +``` + +which would then resolve the `"development"` condition in package imports and +exports, while resolving the existing `"node"`, `"default"`, `"import"`, and +`"require"` conditions as appropriate. + +Any number of custom conditions can be set with repeat flags. + #### Self-referencing a package using its name Within a package, the values defined in the package’s diff --git a/doc/node.1 b/doc/node.1 index 3cd85d3cb0dfb2..8c4a6908383adc 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -78,6 +78,10 @@ Aborting instead of exiting causes a core file to be generated for analysis. .It Fl -completion-bash Print source-able bash completion script for Node.js. . +.It Fl u , Fl -conditions Ar string +Use custom conditional exports conditions +.Ar string +. .It Fl -cpu-prof Start the V8 CPU profiler on start up, and write the CPU profile to disk before exit. If diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 45972ff3b1e305..789b22a5f1784c 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -80,6 +80,7 @@ const manifest = getOptionValue('--experimental-policy') ? require('internal/process/policy').manifest : null; const { compileFunction } = internalBinding('contextify'); +const userConditions = getOptionValue('--conditions'); // Whether any user-provided CJS modules had been loaded (executed). // Used for internal assertions. @@ -472,8 +473,12 @@ function applyExports(basePath, expansion) { if (typeof pkgExports === 'object') { if (ObjectPrototypeHasOwnProperty(pkgExports, mappingKey)) { const mapping = pkgExports[mappingKey]; - return resolveExportsTarget(pathToFileURL(basePath + '/'), mapping, '', - mappingKey); + const resolved = resolveExportsTarget( + pathToFileURL(basePath + '/'), mapping, '', mappingKey); + if (resolved === null || resolved === undefined) + throw new ERR_PACKAGE_PATH_NOT_EXPORTED( + basePath, mappingKey); + return resolved; } let dirMatch = ''; @@ -490,6 +495,9 @@ function applyExports(basePath, expansion) { const subpath = StringPrototypeSlice(mappingKey, dirMatch.length); const resolved = resolveExportsTarget(pathToFileURL(basePath + '/'), mapping, subpath, mappingKey); + if (resolved === null || resolved === undefined) + throw new ERR_PACKAGE_PATH_NOT_EXPORTED( + basePath, mappingKey + subpath); // Extension searching for folder exports only const rc = stat(resolved); if (rc === 0) return resolved; @@ -577,21 +585,29 @@ function resolveExportsTarget(baseUrl, target, subpath, mappingKey) { throw new ERR_INVALID_MODULE_SPECIFIER(mappingKey + subpath, reason); } else if (ArrayIsArray(target)) { if (target.length === 0) - throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - baseUrl.pathname, mappingKey + subpath); + return null; let lastException; for (const targetValue of target) { + let resolved; try { - return resolveExportsTarget(baseUrl, targetValue, subpath, mappingKey); + resolved = resolveExportsTarget(baseUrl, targetValue, subpath, + mappingKey); } catch (e) { lastException = e; - if (e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED' && - e.code !== 'ERR_INVALID_PACKAGE_TARGET') + if (e.code !== 'ERR_INVALID_PACKAGE_TARGET') throw e; } + if (resolved === undefined) + continue; + if (resolved === null) { + lastException = null; + continue; + } + return resolved; } // Throw last fallback error - assert(lastException !== undefined); + if (lastException === undefined || lastException === null) + return lastException; throw lastException; } else if (typeof target === 'object' && target !== null) { const keys = ObjectKeys(target); @@ -600,30 +616,17 @@ function resolveExportsTarget(baseUrl, target, subpath, mappingKey) { 'contain numeric property keys.'); } for (const p of keys) { - switch (p) { - case 'node': - case 'require': - try { - return resolveExportsTarget(baseUrl, target[p], subpath, - mappingKey); - } catch (e) { - if (e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw e; - } - break; - case 'default': - try { - return resolveExportsTarget(baseUrl, target.default, subpath, - mappingKey); - } catch (e) { - if (e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw e; - } + if (cjsConditions.has(p) || p === 'default') { + const resolved = resolveExportsTarget(baseUrl, target[p], subpath, + mappingKey); + if (resolved === undefined) + continue; + return resolved; } } - throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - baseUrl.pathname, mappingKey + subpath); + return undefined; } else if (target === null) { - throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - baseUrl.pathname, mappingKey + subpath); + return null; } throw new ERR_INVALID_PACKAGE_TARGET(baseUrl.pathname, mappingKey, target); } @@ -923,8 +926,7 @@ Module._load = function(request, parent, isMain) { return module.exports; }; -// TODO: Use this set when resolving pkg#exports conditions. -const cjsConditions = new SafeSet(['require', 'node']); +const cjsConditions = new SafeSet(['require', 'node', ...userConditions]); Module._resolveFilename = function(request, parent, isMain, options) { if (NativeModule.canBeRequiredByUsers(request)) { return request; diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index 7ea59f30c6894e..7cf3552948194d 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -51,7 +51,8 @@ const { const { Module: CJSModule } = require('internal/modules/cjs/loader'); const packageJsonReader = require('internal/modules/package_json_reader'); -const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import']); +const userConditions = getOptionValue('--conditions'); +const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import', ...userConditions]); const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS); @@ -359,12 +360,9 @@ function isArrayIndex(key) { function resolvePackageTarget( packageJSONUrl, target, subpath, packageSubpath, base, internal, conditions) { if (typeof target === 'string') { - const resolved = resolvePackageTargetString( + return finalizeResolution(resolvePackageTargetString( target, subpath, packageSubpath, packageJSONUrl, base, internal, - conditions); - if (resolved === null) - return null; - return finalizeResolution(resolved, base); + conditions), base); } else if (ArrayIsArray(target)) { if (target.length === 0) return null; diff --git a/src/node_options.cc b/src/node_options.cc index 53e4c9bb3a315f..790265106be952 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -283,6 +283,11 @@ DebugOptionsParser::DebugOptionsParser() { } EnvironmentOptionsParser::EnvironmentOptionsParser() { + AddOption("--conditions", + "additional user conditions for conditional exports and imports", + &EnvironmentOptions::conditions, + kAllowedInEnvironment); + AddAlias("-u", "--conditions"); AddOption("--diagnostic-dir", "set dir for all output files" " (default: current working directory)", diff --git a/src/node_options.h b/src/node_options.h index 603edb0c405270..1e7ffadf0dafbc 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; + std::vector conditions; bool enable_source_maps = false; bool experimental_json_modules = false; bool experimental_modules = false; diff --git a/test/es-module/test-esm-custom-exports.mjs b/test/es-module/test-esm-custom-exports.mjs new file mode 100644 index 00000000000000..ad81abfdafd861 --- /dev/null +++ b/test/es-module/test-esm-custom-exports.mjs @@ -0,0 +1,10 @@ +// Flags: --conditions=custom-condition -u another +import { mustCall } from '../common/index.mjs'; +import { strictEqual } from 'assert'; +import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs'; +[requireFixture, importFixture].forEach((loadFixture) => { + loadFixture('pkgexports/condition') + .then(mustCall((actual) => { + strictEqual(actual.default, 'from custom condition'); + })); +}); diff --git a/test/fixtures/node_modules/pkgexports/custom-condition.js b/test/fixtures/node_modules/pkgexports/custom-condition.js new file mode 100644 index 00000000000000..63d77460d8d6b7 --- /dev/null +++ b/test/fixtures/node_modules/pkgexports/custom-condition.js @@ -0,0 +1 @@ +module.exports = 'from custom condition'; diff --git a/test/fixtures/node_modules/pkgexports/package.json b/test/fixtures/node_modules/pkgexports/package.json index b99e5c7b79f6a8..71406a407c453d 100644 --- a/test/fixtures/node_modules/pkgexports/package.json +++ b/test/fixtures/node_modules/pkgexports/package.json @@ -21,7 +21,10 @@ "./nofallback2": [null, {}, "builtin:x"], "./nodemodules": "./node_modules/internalpkg/x.js", "./condition": [{ - "custom-condition": "./custom-condition.mjs", + "custom-condition": { + "import": "./custom-condition.mjs", + "require": "./custom-condition.js" + }, "import": "///overridden", "require": { "require": {