Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 36.4k
module: expose exports conditions to loaders#31303
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1050,11 +1050,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 | ||
hybrist marked this conversation as resolved.
Outdated
Uh oh!There was an error while loading. Please reload this page. hybrist marked this conversation as resolved.
Outdated
Uh oh!There was an error while loading. Please reload this page. guybedford marked this conversation as resolved.
Outdated
Uh oh!There was an error while loading. Please reload this page. | ||
| * @param {function} defaultResolve | ||
| * @returns {object} response | ||
| * @returns {string} response.url | ||
| @@ -1069,6 +1080,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); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
hybrist marked this conversation as resolved.
Outdated
Uh oh!There was an error while loading. Please reload this page. | ||
| } = require('internal/modules/esm/resolve'); | ||
| const { defaultGetFormat } = require('internal/modules/esm/get_format'); | ||
| const { defaultGetSource } = require( | ||
| 'internal/modules/esm/get_source'); | ||
| @@ -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); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -4,9 +4,11 @@ const { | ||
| ArrayIsArray, | ||
| JSONParse, | ||
| JSONStringify, | ||
| ObjectFreeze, | ||
| ObjectGetOwnPropertyNames, | ||
| ObjectPrototypeHasOwnProperty, | ||
| SafeMap, | ||
| SafeSet, | ||
| StringPrototypeEndsWith, | ||
| StringPrototypeIncludes, | ||
| StringPrototypeIndexOf, | ||
| @@ -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, | ||
| @@ -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; | ||
| } | ||
guybedford marked this conversation as resolved.
Outdated
Uh oh!There was an error while loading. Please reload this page. | ||
| const realpathCache = new SafeMap(); | ||
| const packageJSONCache = new SafeMap(); /* string -> PackageConfig */ | ||
| @@ -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); | ||
| @@ -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' || | ||
| @@ -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)) { | ||
hybrist marked this conversation as resolved.
Outdated
Uh oh!There was an error while loading. Please reload this page. | ||
| 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; | ||
| @@ -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, '.'); | ||
| @@ -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)) { | ||
| @@ -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); | ||
| } | ||
| @@ -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); | ||
| } | ||
| @@ -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; | ||
| @@ -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); | ||
| } | ||
| } | ||
| } | ||
| @@ -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); | ||
| @@ -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; | ||
| @@ -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); | ||
| @@ -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); | ||
| @@ -658,6 +706,7 @@ function defaultResolve(specifier, { parentURL } = {}, defaultResolveUnused) { | ||
| } | ||
| module.exports = { | ||
| DEFAULT_CONDITIONS, | ||
| defaultResolve, | ||
| getPackageType | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| // Flags: --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' }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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], | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { default } from 'pkgexports/condition'; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for adding this. @nodejs/modules-active-members does this seem understandable and good UX to you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense to me