Skip to content

Commit

Permalink
module: custom --conditions flag option
Browse files Browse the repository at this point in the history
PR-URL: #34637
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Geoffrey Booth <webmaster@geoffreybooth.com>
Reviewed-By: Jan Krems <jan.krems@gmail.com>
  • Loading branch information
guybedford committed Aug 11, 2020
1 parent 420da0c commit 77a515c
Show file tree
Hide file tree
Showing 10 changed files with 93 additions and 38 deletions.
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
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 @@ -491,8 +492,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 @@ -509,6 +514,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 @@ -596,21 +604,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 @@ -619,30 +635,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 @@ -999,8 +1002,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(
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.

0 comments on commit 77a515c

Please sign in to comment.