Skip to content

Commit d9857fd

Browse files
guybedfordcodebytere
authored andcommitted
module: custom --conditions flag option
PR-URL: #34637 Backport-PR-URL: #35385 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Geoffrey Booth <[email protected]> Reviewed-By: Jan Krems <[email protected]>
1 parent 3ad146d commit d9857fd

File tree

10 files changed

+93
-38
lines changed

10 files changed

+93
-38
lines changed

doc/api/cli.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,21 @@ $ node --completion-bash > node_bash_completion
8181
$ source node_bash_completion
8282
```
8383

84+
### `-u`, `--conditions=condition`
85+
<!-- YAML
86+
added: REPLACEME
87+
-->
88+
89+
> Stability: 1 - Experimental
90+
91+
Enable experimental support for custom conditional exports resolution
92+
conditions.
93+
94+
Any number of custom string condition names are permitted.
95+
96+
The default Node.js conditions of `"node"`, `"default"`, `"import"`, and
97+
`"require"` will always apply as defined.
98+
8499
### `--cpu-prof`
85100
<!-- YAML
86101
added: v12.0.0
@@ -1166,6 +1181,7 @@ node --require "./a.js" --require "./b.js"
11661181

11671182
Node.js options that are allowed are:
11681183
<!-- node-options-node start -->
1184+
* `--conditions`, `-u`
11691185
* `--diagnostic-dir`
11701186
* `--disable-proto`
11711187
* `--enable-fips`

doc/api/esm.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,21 @@ a nested conditional does not have any mapping it will continue checking
501501
the remaining conditions of the parent condition. In this way nested
502502
conditions behave analogously to nested JavaScript `if` statements.
503503

504+
#### Resolving user conditions
505+
506+
When running Node.js, custom user conditions can be added with the
507+
`--conditions` or `-u` flag:
508+
509+
```bash
510+
node --conditions=development main.js
511+
```
512+
513+
which would then resolve the `"development"` condition in package imports and
514+
exports, while resolving the existing `"node"`, `"default"`, `"import"`, and
515+
`"require"` conditions as appropriate.
516+
517+
Any number of custom conditions can be set with repeat flags.
518+
504519
#### Self-referencing a package using its name
505520

506521
Within a package, the values defined in the package’s

doc/node.1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ Aborting instead of exiting causes a core file to be generated for analysis.
7878
.It Fl -completion-bash
7979
Print source-able bash completion script for Node.js.
8080
.
81+
.It Fl u , Fl -conditions Ar string
82+
Use custom conditional exports conditions
83+
.Ar string
84+
.
8185
.It Fl -cpu-prof
8286
Start the V8 CPU profiler on start up, and write the CPU profile to disk
8387
before exit. If

lib/internal/modules/cjs/loader.js

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const manifest = getOptionValue('--experimental-policy') ?
8080
require('internal/process/policy').manifest :
8181
null;
8282
const { compileFunction } = internalBinding('contextify');
83+
const userConditions = getOptionValue('--conditions');
8384

8485
// Whether any user-provided CJS modules had been loaded (executed).
8586
// Used for internal assertions.
@@ -472,8 +473,12 @@ function applyExports(basePath, expansion) {
472473
if (typeof pkgExports === 'object') {
473474
if (ObjectPrototypeHasOwnProperty(pkgExports, mappingKey)) {
474475
const mapping = pkgExports[mappingKey];
475-
return resolveExportsTarget(pathToFileURL(basePath + '/'), mapping, '',
476-
mappingKey);
476+
const resolved = resolveExportsTarget(
477+
pathToFileURL(basePath + '/'), mapping, '', mappingKey);
478+
if (resolved === null || resolved === undefined)
479+
throw new ERR_PACKAGE_PATH_NOT_EXPORTED(
480+
basePath, mappingKey);
481+
return resolved;
477482
}
478483

479484
let dirMatch = '';
@@ -490,6 +495,9 @@ function applyExports(basePath, expansion) {
490495
const subpath = StringPrototypeSlice(mappingKey, dirMatch.length);
491496
const resolved = resolveExportsTarget(pathToFileURL(basePath + '/'),
492497
mapping, subpath, mappingKey);
498+
if (resolved === null || resolved === undefined)
499+
throw new ERR_PACKAGE_PATH_NOT_EXPORTED(
500+
basePath, mappingKey + subpath);
493501
// Extension searching for folder exports only
494502
const rc = stat(resolved);
495503
if (rc === 0) return resolved;
@@ -577,21 +585,29 @@ function resolveExportsTarget(baseUrl, target, subpath, mappingKey) {
577585
throw new ERR_INVALID_MODULE_SPECIFIER(mappingKey + subpath, reason);
578586
} else if (ArrayIsArray(target)) {
579587
if (target.length === 0)
580-
throw new ERR_PACKAGE_PATH_NOT_EXPORTED(
581-
baseUrl.pathname, mappingKey + subpath);
588+
return null;
582589
let lastException;
583590
for (const targetValue of target) {
591+
let resolved;
584592
try {
585-
return resolveExportsTarget(baseUrl, targetValue, subpath, mappingKey);
593+
resolved = resolveExportsTarget(baseUrl, targetValue, subpath,
594+
mappingKey);
586595
} catch (e) {
587596
lastException = e;
588-
if (e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED' &&
589-
e.code !== 'ERR_INVALID_PACKAGE_TARGET')
597+
if (e.code !== 'ERR_INVALID_PACKAGE_TARGET')
590598
throw e;
591599
}
600+
if (resolved === undefined)
601+
continue;
602+
if (resolved === null) {
603+
lastException = null;
604+
continue;
605+
}
606+
return resolved;
592607
}
593608
// Throw last fallback error
594-
assert(lastException !== undefined);
609+
if (lastException === undefined || lastException === null)
610+
return lastException;
595611
throw lastException;
596612
} else if (typeof target === 'object' && target !== null) {
597613
const keys = ObjectKeys(target);
@@ -600,30 +616,17 @@ function resolveExportsTarget(baseUrl, target, subpath, mappingKey) {
600616
'contain numeric property keys.');
601617
}
602618
for (const p of keys) {
603-
switch (p) {
604-
case 'node':
605-
case 'require':
606-
try {
607-
return resolveExportsTarget(baseUrl, target[p], subpath,
608-
mappingKey);
609-
} catch (e) {
610-
if (e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw e;
611-
}
612-
break;
613-
case 'default':
614-
try {
615-
return resolveExportsTarget(baseUrl, target.default, subpath,
616-
mappingKey);
617-
} catch (e) {
618-
if (e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw e;
619-
}
619+
if (cjsConditions.has(p) || p === 'default') {
620+
const resolved = resolveExportsTarget(baseUrl, target[p], subpath,
621+
mappingKey);
622+
if (resolved === undefined)
623+
continue;
624+
return resolved;
620625
}
621626
}
622-
throw new ERR_PACKAGE_PATH_NOT_EXPORTED(
623-
baseUrl.pathname, mappingKey + subpath);
627+
return undefined;
624628
} else if (target === null) {
625-
throw new ERR_PACKAGE_PATH_NOT_EXPORTED(
626-
baseUrl.pathname, mappingKey + subpath);
629+
return null;
627630
}
628631
throw new ERR_INVALID_PACKAGE_TARGET(baseUrl.pathname, mappingKey, target);
629632
}
@@ -923,8 +926,7 @@ Module._load = function(request, parent, isMain) {
923926
return module.exports;
924927
};
925928

926-
// TODO: Use this set when resolving pkg#exports conditions.
927-
const cjsConditions = new SafeSet(['require', 'node']);
929+
const cjsConditions = new SafeSet(['require', 'node', ...userConditions]);
928930
Module._resolveFilename = function(request, parent, isMain, options) {
929931
if (NativeModule.canBeRequiredByUsers(request)) {
930932
return request;

lib/internal/modules/esm/resolve.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ const {
5151
const { Module: CJSModule } = require('internal/modules/cjs/loader');
5252

5353
const packageJsonReader = require('internal/modules/package_json_reader');
54-
const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import']);
54+
const userConditions = getOptionValue('--conditions');
55+
const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import', ...userConditions]);
5556
const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS);
5657

5758

@@ -359,12 +360,9 @@ function isArrayIndex(key) {
359360
function resolvePackageTarget(
360361
packageJSONUrl, target, subpath, packageSubpath, base, internal, conditions) {
361362
if (typeof target === 'string') {
362-
const resolved = resolvePackageTargetString(
363+
return finalizeResolution(resolvePackageTargetString(
363364
target, subpath, packageSubpath, packageJSONUrl, base, internal,
364-
conditions);
365-
if (resolved === null)
366-
return null;
367-
return finalizeResolution(resolved, base);
365+
conditions), base);
368366
} else if (ArrayIsArray(target)) {
369367
if (target.length === 0)
370368
return null;

src/node_options.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,11 @@ DebugOptionsParser::DebugOptionsParser() {
283283
}
284284

285285
EnvironmentOptionsParser::EnvironmentOptionsParser() {
286+
AddOption("--conditions",
287+
"additional user conditions for conditional exports and imports",
288+
&EnvironmentOptions::conditions,
289+
kAllowedInEnvironment);
290+
AddAlias("-u", "--conditions");
286291
AddOption("--diagnostic-dir",
287292
"set dir for all output files"
288293
" (default: current working directory)",

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class DebugOptions : public Options {
100100
class EnvironmentOptions : public Options {
101101
public:
102102
bool abort_on_uncaught_exception = false;
103+
std::vector<std::string> conditions;
103104
bool enable_source_maps = false;
104105
bool experimental_json_modules = false;
105106
bool experimental_modules = false;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Flags: --conditions=custom-condition -u another
2+
import { mustCall } from '../common/index.mjs';
3+
import { strictEqual } from 'assert';
4+
import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs';
5+
[requireFixture, importFixture].forEach((loadFixture) => {
6+
loadFixture('pkgexports/condition')
7+
.then(mustCall((actual) => {
8+
strictEqual(actual.default, 'from custom condition');
9+
}));
10+
});

test/fixtures/node_modules/pkgexports/custom-condition.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/node_modules/pkgexports/package.json

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)