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 9 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=conditionA,conditionB`
guybedford marked this conversation as resolved.
Show resolved Hide resolved
<!-- 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 custom comma-separated 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
13 changes: 13 additions & 0 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,19 @@ 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 custom conditions

When running Node.js, custom comma-separated conditions can be set with the
guybedford marked this conversation as resolved.
Show resolved Hide resolved
`--conditions` or `-u` flag:

```bash
node --conditions=development main.js
```

which would then resolve the `"development"` condition in exports, along with
the existing `"node"`, `"default"`, `"import"`, and `"require"` conditions as
appropriate.

#### 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
65 changes: 34 additions & 31 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ const manifest = getOptionValue('--experimental-policy') ?
require('internal/process/policy').manifest :
null;
const { compileFunction } = internalBinding('contextify');
const userConditions = getOptionValue('--conditions') &&
StringPrototypeSplit(getOptionValue('--conditions'), ',');

// Whether any user-provided CJS modules had been loaded (executed).
// Used for internal assertions.
Expand Down Expand Up @@ -497,8 +499,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 +521,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 +611,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 +642,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 +1009,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
11 changes: 5 additions & 6 deletions lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ 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') &&
StringPrototypeSplit(getOptionValue('--conditions'), ',');
const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import', ...userConditions]);
const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS);


Expand Down Expand Up @@ -359,12 +361,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::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,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.