GeezerVison is showing github.com — module: fix conditions override in synchronous resolve hooks · nodejs/node@fe0195f
File treeExpand file treeCollapse file tree Open diff view settings fixtures/es-modules/custom-condition/node_modules/foo
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/cjs/loader.js +22-6Lines changed: 22 additions & 6 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -51,6 +51,7 @@ const { | 51 | 51 | ReflectSet, | 52 | 52 | RegExpPrototypeExec, | 53 | 53 | SafeMap, | | 54 | + SafeSet, | 54 | 55 | String, | 55 | 56 | StringPrototypeCharAt, | 56 | 57 | StringPrototypeCharCodeAt, | @@ -154,6 +155,7 @@ const internalFsBinding = internalBinding('fs'); | 154 | 155 | const { safeGetenv } = internalBinding('credentials'); | 155 | 156 | const { | 156 | 157 | getCjsConditions, | | 158 | + getCjsConditionsArray, | 157 | 159 | initializeCjsConditions, | 158 | 160 | loadBuiltinModule, | 159 | 161 | makeRequireFunction, | @@ -653,7 +655,7 @@ const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/; | 653 | 655 | * Resolves the exports for a given module path and request. | 654 | 656 | * @param {string} nmPath The path to the module. | 655 | 657 | * @param {string} request The request for the module. | 656 | | - * @param {unknown} conditions | | 658 | + * @param {Set<string>} conditions The conditions to use for resolution. | 657 | 659 | * @returns {undefined|string} | 658 | 660 | */ | 659 | 661 | function resolveExports(nmPath, request, conditions) { | @@ -1068,17 +1070,30 @@ function resolveForCJSWithHooks(specifier, parent, isMain) { | 1068 | 1070 | function defaultResolve(specifier, context) { | 1069 | 1071 | // TODO(joyeecheung): parent and isMain should be part of context, then we | 1070 | 1072 | // no longer need to use a different defaultResolve for every resolution. | | 1073 | + // In the hooks, context.conditions is passed around as an array, but internally | | 1074 | + // the resolution helpers expect a SafeSet. Do the conversion here. | | 1075 | + let conditionSet; | | 1076 | + const conditions = context.conditions; | | 1077 | + if (conditions !== undefined && conditions !== getCjsConditionsArray()) { | | 1078 | + if (!ArrayIsArray(conditions)) { | | 1079 | + throw new ERR_INVALID_ARG_VALUE('context.conditions', conditions, | | 1080 | + 'expected an array'); | | 1081 | + } | | 1082 | + conditionSet = new SafeSet(conditions); | | 1083 | + } else { | | 1084 | + conditionSet = getCjsConditions(); | | 1085 | + } | 1071 | 1086 | defaultResolvedFilename = defaultResolveImpl(specifier, parent, isMain, { | 1072 | 1087 | __proto__: null, | 1073 | | - conditions: context.conditions, | | 1088 | + conditions: conditionSet, | 1074 | 1089 | }); | 1075 | 1090 |
| 1076 | 1091 | defaultResolvedURL = convertCJSFilenameToURL(defaultResolvedFilename); | 1077 | 1092 | return { __proto__: null, url: defaultResolvedURL }; | 1078 | 1093 | } | 1079 | 1094 |
| 1080 | 1095 | const resolveResult = resolveWithHooks(specifier, parentURL, /* importAttributes */ undefined, | 1081 | | - getCjsConditions(), defaultResolve); | | 1096 | + getCjsConditionsArray(), defaultResolve); | 1082 | 1097 | const { url } = resolveResult; | 1083 | 1098 | format = resolveResult.format; | 1084 | 1099 |
| @@ -1154,7 +1169,7 @@ function loadBuiltinWithHooks(id, url, format) { | 1154 | 1169 | url ??= `node:${id}`; | 1155 | 1170 | // TODO(joyeecheung): do we really want to invoke the load hook for the builtins? | 1156 | 1171 | const loadResult = loadWithHooks(url, format || 'builtin', /* importAttributes */ undefined, | 1157 | | - getCjsConditions(), getDefaultLoad(url, id)); | | 1172 | + getCjsConditionsArray(), getDefaultLoad(url, id)); | 1158 | 1173 | if (loadResult.format && loadResult.format !== 'builtin') { | 1159 | 1174 | return undefined; // Format has been overridden, return undefined for the caller to continue loading. | 1160 | 1175 | } | @@ -1306,7 +1321,7 @@ Module._load = function(request, parent, isMain) { | 1306 | 1321 | * @param {ResolveFilenameOptions} options Options object | 1307 | 1322 | * @typedef {object} ResolveFilenameOptions | 1308 | 1323 | * @property {string[]} paths Paths to search for modules in | 1309 | | - * @property {string[]} conditions Conditions used for resolution. | | 1324 | + * @property {Set<string>?} conditions The conditions to use for resolution. | 1310 | 1325 | * @returns {void|string} | 1311 | 1326 | */ | 1312 | 1327 | Module._resolveFilename = function(request, parent, isMain, options) { | @@ -1755,7 +1770,8 @@ function loadSource(mod, filename, formatFromNode) { | 1755 | 1770 | mod[kURL] = convertCJSFilenameToURL(filename); | 1756 | 1771 | } | 1757 | 1772 |
| 1758 | | - const loadResult = loadWithHooks(mod[kURL], mod[kFormat], /* importAttributes */ undefined, getCjsConditions(), | | 1773 | + const loadResult = loadWithHooks(mod[kURL], mod[kFormat], /* importAttributes */ undefined, | | 1774 | + getCjsConditionsArray(), | 1759 | 1775 | getDefaultLoad(mod[kURL], filename)); | 1760 | 1776 |
| 1761 | 1777 | // Reset the module properties with load hook results. | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/loader.js +17-24Lines changed: 17 additions & 24 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -709,24 +709,30 @@ class ModuleLoader { | 709 | 709 | if (this.#customizations) { // Only has module.register hooks. | 710 | 710 | return this.#customizations.resolve(specifier, parentURL, importAttributes); | 711 | 711 | } | 712 | | - return this.#cachedDefaultResolve(specifier, parentURL, importAttributes); | | 712 | + return this.#cachedDefaultResolve(specifier, { | | 713 | + __proto__: null, | | 714 | + conditions: this.#defaultConditions, | | 715 | + parentURL, | | 716 | + importAttributes, | | 717 | + }); | 713 | 718 | } | 714 | 719 |
| 715 | 720 | /** | 716 | 721 | * Either return a cached resolution, or perform the default resolution which is synchronous, and | 717 | 722 | * cache the result. | 718 | 723 | * @param {string} specifier See {@link resolve}. | 719 | | - * @param {string} [parentURL] See {@link resolve}. | 720 | | - * @param {ImportAttributes} importAttributes See {@link resolve}. | | 724 | + * @param {{ parentURL?: string, importAttributes: ImportAttributes, conditions?: string[]}} context | 721 | 725 | * @returns {{ format: string, url: string }} | 722 | 726 | */ | 723 | | - #cachedDefaultResolve(specifier, parentURL, importAttributes) { | | 727 | + #cachedDefaultResolve(specifier, context) { | | 728 | + const { parentURL, importAttributes } = context; | 724 | 729 | const requestKey = this.#resolveCache.serializeKey(specifier, importAttributes); | 725 | 730 | const cachedResult = this.#resolveCache.get(requestKey, parentURL); | 726 | 731 | if (cachedResult != null) { | 727 | 732 | return cachedResult; | 728 | 733 | } | 729 | | - const result = this.defaultResolve(specifier, parentURL, importAttributes); | | 734 | + defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve; | | 735 | + const result = defaultResolve(specifier, context); | 730 | 736 | this.#resolveCache.set(requestKey, parentURL, result); | 731 | 737 | return result; | 732 | 738 | } | @@ -754,14 +760,15 @@ class ModuleLoader { | 754 | 760 | * This is the default resolve step for module.registerHooks(), which incorporates asynchronous hooks | 755 | 761 | * from module.register() which are run in a blocking fashion for it to be synchronous. | 756 | 762 | * @param {string|URL} specifier See {@link resolveSync}. | 757 | | - * @param {{ parentURL?: string, importAttributes: ImportAttributes}} context See {@link resolveSync}. | | 763 | + * @param {{ parentURL?: string, importAttributes: ImportAttributes, conditions?: string[]}} context | | 764 | + * See {@link resolveSync}. | 758 | 765 | * @returns {{ format: string, url: string }} | 759 | 766 | */ | 760 | 767 | #resolveAndMaybeBlockOnLoaderThread(specifier, context) { | 761 | 768 | if (this.#customizations) { | 762 | 769 | return this.#customizations.resolveSync(specifier, context.parentURL, context.importAttributes); | 763 | 770 | } | 764 | | - return this.#cachedDefaultResolve(specifier, context.parentURL, context.importAttributes); | | 771 | + return this.#cachedDefaultResolve(specifier, context); | 765 | 772 | } | 766 | 773 |
| 767 | 774 | /** | @@ -784,26 +791,12 @@ class ModuleLoader { | 784 | 791 | return resolveWithHooks(specifier, parentURL, importAttributes, this.#defaultConditions, | 785 | 792 | this.#resolveAndMaybeBlockOnLoaderThread.bind(this)); | 786 | 793 | } | 787 | | - return this.#resolveAndMaybeBlockOnLoaderThread(specifier, { parentURL, importAttributes }); | 788 | | - } | 789 | | - | 790 | | - /** | 791 | | - * Our `defaultResolve` is synchronous and can be used in both | 792 | | - * `resolve` and `resolveSync`. This function is here just to avoid | 793 | | - * repeating the same code block twice in those functions. | 794 | | - * @returns {string} | 795 | | - */ | 796 | | - defaultResolve(originalSpecifier, parentURL, importAttributes) { | 797 | | - defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve; | 798 | | - | 799 | | - const context = { | | 794 | + return this.#resolveAndMaybeBlockOnLoaderThread(specifier, { | 800 | 795 | __proto__: null, | 801 | 796 | conditions: this.#defaultConditions, | 802 | | - importAttributes, | 803 | 797 | parentURL, | 804 | | - }; | 805 | | - | 806 | | - return defaultResolve(originalSpecifier, context); | | 798 | + importAttributes, | | 799 | + }); | 807 | 800 | } | 808 | 801 |
| 809 | 802 | /** | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/helpers.js +16-3Lines changed: 16 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -66,6 +66,9 @@ function toRealPath(requestPath) { | 66 | 66 |
| 67 | 67 | /** @type {Set<string>} */ | 68 | 68 | let cjsConditions; | | 69 | +/** @type {string[]} */ | | 70 | +let cjsConditionsArray; | | 71 | + | 69 | 72 | /** | 70 | 73 | * Define the conditions that apply to the CommonJS loader. | 71 | 74 | * @returns {void} | @@ -75,15 +78,17 @@ function initializeCjsConditions() { | 75 | 78 | const noAddons = getOptionValue('--no-addons'); | 76 | 79 | const addonConditions = noAddons ? [] : ['node-addons']; | 77 | 80 | // TODO: Use this set when resolving pkg#exports conditions in loader.js. | 78 | | - cjsConditions = new SafeSet([ | | 81 | + cjsConditionsArray = [ | 79 | 82 | 'require', | 80 | 83 | 'node', | 81 | 84 | ...addonConditions, | 82 | 85 | ...userConditions, | 83 | | - ]); | | 86 | + ]; | 84 | 87 | if (getOptionValue('--experimental-require-module')) { | 85 | | - cjsConditions.add('module-sync'); | | 88 | + cjsConditionsArray.push('module-sync'); | 86 | 89 | } | | 90 | + ObjectFreeze(cjsConditionsArray); | | 91 | + cjsConditions = new SafeSet(cjsConditionsArray); | 87 | 92 | } | 88 | 93 |
| 89 | 94 | /** | @@ -97,6 +102,13 @@ function getCjsConditions() { | 97 | 102 | return cjsConditions; | 98 | 103 | } | 99 | 104 |
| | 105 | +function getCjsConditionsArray() { | | 106 | + if (cjsConditionsArray === undefined) { | | 107 | + initializeCjsConditions(); | | 108 | + } | | 109 | + return cjsConditionsArray; | | 110 | +} | | 111 | + | 100 | 112 | /** | 101 | 113 | * Provide one of Node.js' public modules to user code. | 102 | 114 | * @param {string} id - The identifier/specifier of the builtin module to load | @@ -418,6 +430,7 @@ module.exports = { | 418 | 430 | flushCompileCache, | 419 | 431 | getBuiltinModule, | 420 | 432 | getCjsConditions, | | 433 | + getCjsConditionsArray, | 421 | 434 | getCompileCacheDir, | 422 | 435 | initializeCjsConditions, | 423 | 436 | loadBuiltinModule, | |
Collapse file Copy file name to clipboardExpand all lines: test/fixtures/es-modules/custom-condition/node_modules/foo/default.cjs +2Lines changed: 2 additions & 0 deletions Collapse file Copy file name to clipboardExpand all lines: test/fixtures/es-modules/custom-condition/node_modules/foo/foo-esm.mjs +2Lines changed: 2 additions & 0 deletions Collapse file Copy file name to clipboardExpand all lines: test/fixtures/es-modules/custom-condition/node_modules/foo/foo.cjs +2Lines changed: 2 additions & 0 deletions Collapse file Copy file name to clipboardExpand all lines: test/fixtures/es-modules/custom-condition/node_modules/foo/package.json +28Lines changed: 28 additions & 0 deletions Collapse file Copy file name to clipboard +57Lines changed: 57 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +// Similar to test-module-hooks-custom-conditions.mjs, but checking the | | 2 | +// real require() instead of the re-invented one for imported CJS. | | 3 | +'use strict'; | | 4 | +const common = require('../common'); | | 5 | +const { registerHooks } = require('node:module'); | | 6 | +const assert = require('node:assert'); | | 7 | +const { cjs, esm } = require('../fixtures/es-modules/custom-condition/load.cjs'); | | 8 | + | | 9 | +(async () => { | | 10 | + // Without hooks, the default condition is used. | | 11 | + assert.strictEqual(cjs('foo').result, 'default'); | | 12 | + assert.strictEqual((await esm('foo')).result, 'default'); | | 13 | + | | 14 | + // Prepending 'foo' to the conditions array in the resolve hook should | | 15 | + // allow a CJS to be resolved with that condition. | | 16 | + { | | 17 | + const hooks = registerHooks({ | | 18 | + resolve(specifier, context, nextResolve) { | | 19 | + assert(Array.isArray(context.conditions)); | | 20 | + context.conditions = ['foo', ...context.conditions]; | | 21 | + return nextResolve(specifier, context); | | 22 | + }, | | 23 | + }); | | 24 | + assert.strictEqual(cjs('foo/second').result, 'foo'); | | 25 | + assert.strictEqual((await esm('foo/second')).result, 'foo'); | | 26 | + hooks.deregister(); | | 27 | + } | | 28 | + | | 29 | + // Prepending 'foo-esm' to the conditions array in the resolve hook should | | 30 | + // allow a ESM to be resolved with that condition. | | 31 | + { | | 32 | + const hooks = registerHooks({ | | 33 | + resolve(specifier, context, nextResolve) { | | 34 | + assert(Array.isArray(context.conditions)); | | 35 | + context.conditions = ['foo-esm', ...context.conditions]; | | 36 | + return nextResolve(specifier, context); | | 37 | + }, | | 38 | + }); | | 39 | + assert.strictEqual(cjs('foo/third').result, 'foo-esm'); | | 40 | + assert.strictEqual((await esm('foo/third')).result, 'foo-esm'); | | 41 | + hooks.deregister(); | | 42 | + } | | 43 | + | | 44 | + // Duplicating the 'foo' condition in the resolve hook should not change the result. | | 45 | + { | | 46 | + const hooks = registerHooks({ | | 47 | + resolve(specifier, context, nextResolve) { | | 48 | + assert(Array.isArray(context.conditions)); | | 49 | + context.conditions = ['foo', ...context.conditions, 'foo']; | | 50 | + return nextResolve(specifier, context); | | 51 | + }, | | 52 | + }); | | 53 | + assert.strictEqual(cjs('foo/fourth').result, 'foo'); | | 54 | + assert.strictEqual((await esm('foo/fourth')).result, 'foo'); | | 55 | + hooks.deregister(); | | 56 | + } | | 57 | +})().then(common.mustCall()); |
Collapse file Copy file name to clipboard +70Lines changed: 70 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +// Check various special values of `conditions` in the context object | | 2 | +// when using synchronous module hooks to override the loaders in a | | 3 | +// CJS module. | | 4 | +'use strict'; | | 5 | +const common = require('../common'); | | 6 | +const { registerHooks } = require('node:module'); | | 7 | +const assert = require('node:assert'); | | 8 | +const { cjs, esm } = require('../fixtures/es-modules/custom-condition/load.cjs'); | | 9 | + | | 10 | +(async () => { | | 11 | + // Setting it to undefined would lead to the default conditions being used. | | 12 | + { | | 13 | + const hooks = registerHooks({ | | 14 | + resolve(specifier, context, nextResolve) { | | 15 | + context.conditions = undefined; | | 16 | + return nextResolve(specifier, context); | | 17 | + }, | | 18 | + }); | | 19 | + assert.strictEqual(cjs('foo').result, 'default'); | | 20 | + assert.strictEqual((await esm('foo')).result, 'default'); | | 21 | + hooks.deregister(); | | 22 | + } | | 23 | + | | 24 | + // Setting it to an empty array would lead to the default conditions being used. | | 25 | + { | | 26 | + const hooks = registerHooks({ | | 27 | + resolve(specifier, context, nextResolve) { | | 28 | + context.conditions = []; | | 29 | + return nextResolve(specifier, context); | | 30 | + }, | | 31 | + }); | | 32 | + assert.strictEqual(cjs('foo/second').result, 'default'); | | 33 | + assert.strictEqual((await esm('foo/second')).result, 'default'); | | 34 | + hooks.deregister(); | | 35 | + } | | 36 | + | | 37 | + // If the exports have no default export, it should error. | | 38 | + { | | 39 | + const hooks = registerHooks({ | | 40 | + resolve(specifier, context, nextResolve) { | | 41 | + context.conditions = []; | | 42 | + return nextResolve(specifier, context); | | 43 | + }, | | 44 | + }); | | 45 | + assert.throws(() => cjs('foo/no-default'), { | | 46 | + code: 'ERR_PACKAGE_PATH_NOT_EXPORTED', | | 47 | + }); | | 48 | + await assert.rejects(esm('foo/no-default'), { | | 49 | + code: 'ERR_PACKAGE_PATH_NOT_EXPORTED', | | 50 | + }); | | 51 | + hooks.deregister(); | | 52 | + } | | 53 | + | | 54 | + // If the exports have no default export, it should error. | | 55 | + { | | 56 | + const hooks = registerHooks({ | | 57 | + resolve(specifier, context, nextResolve) { | | 58 | + context.conditions = 'invalid'; | | 59 | + return nextResolve(specifier, context); | | 60 | + }, | | 61 | + }); | | 62 | + assert.throws(() => cjs('foo/third'), { | | 63 | + code: 'ERR_INVALID_ARG_VALUE', | | 64 | + }); | | 65 | + await assert.rejects(esm('foo/third'), { | | 66 | + code: 'ERR_INVALID_ARG_VALUE', | | 67 | + }); | | 68 | + hooks.deregister(); | | 69 | + } | | 70 | +})().then(common.mustCall()); |
|