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: custom --conditions flag option #34637

Closed
wants to merge 10 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: 16 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ $ node --completion-bash > node_bash_completion
$ source node_bash_completion
```

### `-u`, `--conditions=condition`
<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

Enable experimental support for custom conditional exports resolution
GeoffreyBooth marked this conversation as resolved.
Show resolved Hide resolved
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`
<!-- YAML
added: v12.0.0
Expand Down Expand Up @@ -1232,6 +1247,7 @@ node --require "./a.js" --require "./b.js"

Node.js options that are allowed are:
<!-- node-options-node start -->
* `--conditions`, `-u`
* `--diagnostic-dir`
* `--disable-proto`
* `--enable-fips`
Expand Down
15 changes: 15 additions & 0 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 33 additions & 31 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,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.
Expand Down Expand Up @@ -497,8 +498,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 = '';
Expand All @@ -515,6 +520,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;
Expand Down Expand Up @@ -602,21 +610,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);
Expand All @@ -625,30 +641,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);
}
Expand Down Expand Up @@ -1005,8 +1008,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;
Expand Down
10 changes: 4 additions & 6 deletions lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);


Expand Down Expand Up @@ -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(
guybedford marked this conversation as resolved.
Show resolved Hide resolved
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;
Expand Down
5 changes: 5 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,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)",
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class DebugOptions : public Options {
class EnvironmentOptions : public Options {
public:
bool abort_on_uncaught_exception = false;
std::vector<std::string> conditions;
bool enable_source_maps = false;
bool experimental_json_modules = false;
bool experimental_modules = false;
Expand Down
10 changes: 10 additions & 0 deletions test/es-module/test-esm-custom-exports.mjs
Original file line number Diff line number Diff line change
@@ -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');
}));
});
1 change: 1 addition & 0 deletions test/fixtures/node_modules/pkgexports/custom-condition.js

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

5 changes: 4 additions & 1 deletion test/fixtures/node_modules/pkgexports/package.json

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