Skip to content

Commit

Permalink
module: expose exports conditions to loaders
Browse files Browse the repository at this point in the history
PR-URL: #31303
Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Reviewed-By: Geoffrey Booth <webmaster@geoffreybooth.com>
  • Loading branch information
jkrems authored and targos committed Apr 28, 2020
1 parent 06953df commit b62910c
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 23 deletions.
19 changes: 19 additions & 0 deletions doc/api/esm.md
Expand Up @@ -1056,11 +1056,22 @@ and parent URL. The module specifier is the string in an `import` statement or
`import()` expression, and the parent URL is the URL of the module that imported
this one, or `undefined` if this is the main entry point for the application.
The `conditions` property on the `context` is an array of conditions for
[Conditional Exports][] that apply to this resolution request. They can be used
for looking up conditional mappings elsewhere or to modify the list when calling
the default resolution logic.
The [current set of Node.js default conditions][Conditional Exports] will always
be in the `context.conditions` list passed to the hook. If the hook wants to
ensure Node.js-compatible resolution logic, all items from this default
condition list **must** be passed through to the `defaultResolve` function.
```js
/**
* @param {string} specifier
* @param {object} context
* @param {string} context.parentURL
* @param {string[]} context.conditions
* @param {function} defaultResolve
* @returns {object} response
* @returns {string} response.url
Expand All @@ -1075,6 +1086,14 @@ export async function resolve(specifier, context, defaultResolve) {
new URL(specifier, parentURL).href : new URL(specifier).href
};
}
if (anotherCondition) {
// When calling the defaultResolve, the arguments can be modified. In this
// case it's adding another value for matching conditional exports.
return defaultResolve(specifier, {
...context,
conditions: [...context.conditions, 'another-condition'],
});
}
// Defer to Node.js for all other specifiers.
return defaultResolve(specifier, context, defaultResolve);
}
Expand Down
7 changes: 5 additions & 2 deletions lib/internal/modules/esm/loader.js
Expand Up @@ -19,7 +19,10 @@ const { validateString } = require('internal/validators');
const ModuleMap = require('internal/modules/esm/module_map');
const ModuleJob = require('internal/modules/esm/module_job');

const { defaultResolve } = require('internal/modules/esm/resolve');
const {
defaultResolve,
DEFAULT_CONDITIONS,
} = require('internal/modules/esm/resolve');
const { defaultGetFormat } = require('internal/modules/esm/get_format');
const { defaultGetSource } = require(
'internal/modules/esm/get_source');
Expand Down Expand Up @@ -92,7 +95,7 @@ class Loader {
validateString(parentURL, 'parentURL');

const resolveResponse = await this._resolve(
specifier, { parentURL }, defaultResolve);
specifier, { parentURL, conditions: DEFAULT_CONDITIONS }, defaultResolve);
if (typeof resolveResponse !== 'object') {
throw new ERR_INVALID_RETURN_VALUE(
'object', 'loader resolve', resolveResponse);
Expand Down
91 changes: 70 additions & 21 deletions lib/internal/modules/esm/resolve.js
Expand Up @@ -4,9 +4,11 @@ const {
ArrayIsArray,
JSONParse,
JSONStringify,
ObjectFreeze,
ObjectGetOwnPropertyNames,
ObjectPrototypeHasOwnProperty,
SafeMap,
SafeSet,
StringPrototypeEndsWith,
StringPrototypeIncludes,
StringPrototypeIndexOf,
Expand Down Expand Up @@ -35,6 +37,7 @@ const typeFlag = getOptionValue('--input-type');
const { URL, pathToFileURL, fileURLToPath } = require('internal/url');
const {
ERR_INPUT_TYPE_NOT_ALLOWED,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_MODULE_SPECIFIER,
ERR_INVALID_PACKAGE_CONFIG,
ERR_INVALID_PACKAGE_TARGET,
Expand All @@ -43,6 +46,20 @@ const {
ERR_UNSUPPORTED_ESM_URL_SCHEME,
} = require('internal/errors').codes;

const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import']);
const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS);

function getConditionsSet(conditions) {
if (conditions !== undefined && conditions !== DEFAULT_CONDITIONS) {
if (!ArrayIsArray(conditions)) {
throw new ERR_INVALID_ARG_VALUE('conditions', conditions,
'expected an array');
}
return new SafeSet(conditions);
}
return DEFAULT_CONDITIONS_SET;
}

const realpathCache = new SafeMap();
const packageJSONCache = new SafeMap(); /* string -> PackageConfig */

Expand Down Expand Up @@ -310,14 +327,18 @@ function resolveExportsTargetString(
return subpathResolved;
}

function isArrayIndex(key /* string */) { /* -> boolean */
/**
* @param {string} key
* @returns {boolean}
*/
function isArrayIndex(key) {
const keyNum = +key;
if (`${keyNum}` !== key) return false;
return keyNum >= 0 && keyNum < 0xFFFF_FFFF;
}

function resolveExportsTarget(
packageJSONUrl, target, subpath, packageSubpath, base) {
packageJSONUrl, target, subpath, packageSubpath, base, conditions) {
if (typeof target === 'string') {
const resolved = resolveExportsTargetString(
target, subpath, packageSubpath, packageJSONUrl, base);
Expand All @@ -332,7 +353,8 @@ function resolveExportsTarget(
let resolved;
try {
resolved = resolveExportsTarget(
packageJSONUrl, targetItem, subpath, packageSubpath, base);
packageJSONUrl, targetItem, subpath, packageSubpath, base,
conditions);
} catch (e) {
lastException = e;
if (e.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' ||
Expand All @@ -357,11 +379,12 @@ function resolveExportsTarget(
}
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key === 'node' || key === 'import' || key === 'default') {
if (key === 'default' || conditions.has(key)) {
const conditionalTarget = target[key];
try {
return resolveExportsTarget(
packageJSONUrl, conditionalTarget, subpath, packageSubpath, base);
packageJSONUrl, conditionalTarget, subpath, packageSubpath, base,
conditions);
} catch (e) {
if (e.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') continue;
throw e;
Expand Down Expand Up @@ -397,16 +420,18 @@ function isConditionalExportsMainSugar(exports, packageJSONUrl, base) {
}


function packageMainResolve(packageJSONUrl, packageConfig, base) {
function packageMainResolve(packageJSONUrl, packageConfig, base, conditions) {
if (packageConfig.exists) {
const exports = packageConfig.exports;
if (exports !== undefined) {
if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) {
return resolveExportsTarget(packageJSONUrl, exports, '', '', base);
return resolveExportsTarget(packageJSONUrl, exports, '', '', base,
conditions);
} else if (typeof exports === 'object' && exports !== null) {
const target = exports['.'];
if (target !== undefined)
return resolveExportsTarget(packageJSONUrl, target, '', '', base);
return resolveExportsTarget(packageJSONUrl, target, '', '', base,
conditions);
}

throw new ERR_PACKAGE_PATH_NOT_EXPORTED(packageJSONUrl, '.');
Expand Down Expand Up @@ -434,9 +459,16 @@ function packageMainResolve(packageJSONUrl, packageConfig, base) {
fileURLToPath(new URL('.', packageJSONUrl)), fileURLToPath(base));
}


/**
* @param {URL} packageJSONUrl
* @param {string} packageSubpath
* @param {object} packageConfig
* @param {string} base
* @param {Set<string>} conditions
* @returns {URL}
*/
function packageExportsResolve(
packageJSONUrl, packageSubpath, packageConfig, base) /* -> URL */ {
packageJSONUrl, packageSubpath, packageConfig, base, conditions) {
const exports = packageConfig.exports;
if (exports === undefined ||
isConditionalExportsMainSugar(exports, packageJSONUrl, base)) {
Expand All @@ -447,7 +479,7 @@ function packageExportsResolve(
if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) {
const target = exports[packageSubpath];
const resolved = resolveExportsTarget(
packageJSONUrl, target, '', packageSubpath, base);
packageJSONUrl, target, '', packageSubpath, base, conditions);
return finalizeResolution(resolved, base);
}

Expand All @@ -466,7 +498,7 @@ function packageExportsResolve(
const target = exports[bestMatch];
const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length);
const resolved = resolveExportsTarget(
packageJSONUrl, target, subpath, packageSubpath, base);
packageJSONUrl, target, subpath, packageSubpath, base, conditions);
return finalizeResolution(resolved, base);
}

Expand All @@ -478,7 +510,13 @@ function getPackageType(url) {
return packageConfig.type;
}

function packageResolve(specifier /* string */, base /* URL */) { /* -> URL */
/**
* @param {string} specifier
* @param {URL} base
* @param {Set<string>} conditions
* @returns {URL}
*/
function packageResolve(specifier, base, conditions) {
let separatorIndex = StringPrototypeIndexOf(specifier, '/');
let validPackageName = true;
let isScoped = false;
Expand Down Expand Up @@ -530,10 +568,11 @@ function packageResolve(specifier /* string */, base /* URL */) { /* -> URL */
if (packageSubpath === './') {
return new URL('./', packageJSONUrl);
} else if (packageSubpath === '') {
return packageMainResolve(packageJSONUrl, packageConfig, base);
return packageMainResolve(packageJSONUrl, packageConfig, base,
conditions);
} else {
return packageExportsResolve(
packageJSONUrl, packageSubpath, packageConfig, base);
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
}
}
}
Expand All @@ -559,10 +598,11 @@ function packageResolve(specifier /* string */, base /* URL */) { /* -> URL */
if (packageSubpath === './') {
return new URL('./', packageJSONUrl);
} else if (packageSubpath === '') {
return packageMainResolve(packageJSONUrl, packageConfig, base);
return packageMainResolve(packageJSONUrl, packageConfig, base,
conditions);
} else if (packageConfig.exports !== undefined) {
return packageExportsResolve(
packageJSONUrl, packageSubpath, packageConfig, base);
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
} else {
return finalizeResolution(
new URL(packageSubpath, packageJSONUrl), base);
Expand All @@ -587,7 +627,13 @@ function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) {
return false;
}

function moduleResolve(specifier /* string */, base /* URL */) { /* -> URL */
/**
* @param {string} specifier
* @param {URL} base
* @param {Set<string>} conditions
* @returns {URL}
*/
function moduleResolve(specifier, base, conditions) {
// Order swapped from spec for minor perf gain.
// Ok since relative URLs cannot parse as URLs.
let resolved;
Expand All @@ -597,13 +643,14 @@ function moduleResolve(specifier /* string */, base /* URL */) { /* -> URL */
try {
resolved = new URL(specifier);
} catch {
return packageResolve(specifier, base);
return packageResolve(specifier, base, conditions);
}
}
return finalizeResolution(resolved, base);
}

function defaultResolve(specifier, { parentURL } = {}, defaultResolveUnused) {
function defaultResolve(specifier, context = {}, defaultResolveUnused) {
let { parentURL, conditions } = context;
let parsed;
try {
parsed = new URL(specifier);
Expand Down Expand Up @@ -641,7 +688,8 @@ function defaultResolve(specifier, { parentURL } = {}, defaultResolveUnused) {
throw new ERR_INPUT_TYPE_NOT_ALLOWED();
}

let url = moduleResolve(specifier, new URL(parentURL));
conditions = getConditionsSet(conditions);
let url = moduleResolve(specifier, parentURL, conditions);

if (isMain ? !preserveSymlinksMain : !preserveSymlinks) {
const urlPath = fileURLToPath(url);
Expand All @@ -658,6 +706,7 @@ function defaultResolve(specifier, { parentURL } = {}, defaultResolveUnused) {
}

module.exports = {
DEFAULT_CONDITIONS,
defaultResolve,
getPackageType
};
7 changes: 7 additions & 0 deletions test/es-module/test-esm-loader-custom-condition.mjs
@@ -0,0 +1,7 @@
// Flags: --experimental-modules --experimental-loader ./test/fixtures/es-module-loaders/loader-with-custom-condition.mjs
import '../common/index.mjs';
import assert from 'assert';

import * as ns from '../fixtures/es-modules/conditional-exports.mjs';

assert.deepStrictEqual({ ...ns }, { default: 'from custom condition' });
10 changes: 10 additions & 0 deletions test/fixtures/es-module-loaders/loader-with-custom-condition.mjs
@@ -0,0 +1,10 @@
import {ok, deepStrictEqual} from 'assert';

export async function resolve(specifier, context, defaultResolve) {
ok(Array.isArray(context.conditions), 'loader receives conditions array');
deepStrictEqual([...context.conditions].sort(), ['import', 'node']);
return defaultResolve(specifier, {
...context,
conditions: ['custom-condition', ...context.conditions],
});
}
1 change: 1 addition & 0 deletions test/fixtures/es-modules/conditional-exports.mjs
@@ -0,0 +1 @@
export { default } from 'pkgexports/condition';
1 change: 1 addition & 0 deletions test/fixtures/node_modules/pkgexports/custom-condition.mjs

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

1 change: 1 addition & 0 deletions 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 b62910c

Please sign in to comment.