GeezerVison is showing github.com — esm: leverage loaders when resolving subsequent loaders · nodejs/node@490b598
File treeExpand file treeCollapse file tree Open diff view settings fixtures/es-module-loaders
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/esm.md +3-2Lines changed: 3 additions & 2 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -700,8 +700,9 @@ changes: | 700 | 700 | To customize the default module resolution, loader hooks can optionally be | 701 | 701 | provided via a `--experimental-loader ./loader-name.mjs` argument to Node.js. | 702 | 702 |
| 703 | | -When hooks are used they apply to the entry point and all `import` calls. They | 704 | | -won't apply to `require` calls; those still follow [CommonJS][] rules. | | 703 | +When hooks are used they apply to each subsequent loader, the entry point, and | | 704 | +all `import` calls. They won't apply to `require` calls; those still follow | | 705 | +[CommonJS][] rules. | 705 | 706 |
| 706 | 707 | Loaders follow the pattern of `--require`: | 707 | 708 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/loader.js +1-3Lines changed: 1 addition & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -322,7 +322,7 @@ class ESMLoader { | 322 | 322 |
| 323 | 323 | /** | 324 | 324 | * Collect custom/user-defined hook(s). After all hooks have been collected, | 325 | | - * calls global preload hook(s). | | 325 | + * the global preload hook(s) must be called. | 326 | 326 | * @param {KeyedExports} customLoaders | 327 | 327 | * A list of exports from user-defined loaders (as returned by | 328 | 328 | * ESMLoader.import()). | @@ -369,8 +369,6 @@ class ESMLoader { | 369 | 369 | ); | 370 | 370 | } | 371 | 371 | } | 372 | | - | 373 | | - this.preload(); | 374 | 372 | } | 375 | 373 |
| 376 | 374 | async eval( | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/process/esm_loader.js +35-14Lines changed: 35 additions & 14 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 2 | 2 |
| 3 | 3 | const { | 4 | 4 | ArrayIsArray, | 5 | | - ObjectCreate, | | 5 | + ArrayPrototypePushApply, | 6 | 6 | } = primordials; | 7 | 7 |
| 8 | 8 | const { ESMLoader } = require('internal/modules/esm/loader'); | 9 | 9 | const { | 10 | 10 | hasUncaughtExceptionCaptureCallback, | 11 | 11 | } = require('internal/process/execution'); | 12 | 12 | const { pathToFileURL } = require('internal/url'); | | 13 | +const { kEmptyObject } = require('internal/util'); | 13 | 14 |
| 14 | 15 | const esmLoader = new ESMLoader(); | 15 | 16 | exports.esmLoader = esmLoader; | @@ -29,41 +30,61 @@ async function initializeLoader() { | 29 | 30 | const { getOptionValue } = require('internal/options'); | 30 | 31 | const customLoaders = getOptionValue('--experimental-loader'); | 31 | 32 | const preloadModules = getOptionValue('--import'); | 32 | | - const loaders = await loadModulesInIsolation(customLoaders); | | 33 | + | | 34 | + let cwd; | | 35 | + try { | | 36 | + cwd = process.cwd() + '/'; | | 37 | + } catch { | | 38 | + cwd = '/'; | | 39 | + } | | 40 | + | | 41 | + const internalEsmLoader = new ESMLoader(); | | 42 | + const allLoaders = []; | | 43 | + | | 44 | + const parentURL = pathToFileURL(cwd).href; | | 45 | + | | 46 | + for (let i = 0; i < customLoaders.length; i++) { | | 47 | + const customLoader = customLoaders[i]; | | 48 | + | | 49 | + // Importation must be handled by internal loader to avoid polluting user-land | | 50 | + const keyedExportsSublist = await internalEsmLoader.import( | | 51 | + [customLoader], | | 52 | + parentURL, | | 53 | + kEmptyObject, | | 54 | + ); | | 55 | + | | 56 | + internalEsmLoader.addCustomLoaders(keyedExportsSublist); | | 57 | + ArrayPrototypePushApply(allLoaders, keyedExportsSublist); | | 58 | + } | 33 | 59 |
| 34 | 60 | // Hooks must then be added to external/public loader | 35 | 61 | // (so they're triggered in userland) | 36 | | - esmLoader.addCustomLoaders(loaders); | | 62 | + esmLoader.addCustomLoaders(allLoaders); | | 63 | + esmLoader.preload(); | 37 | 64 |
| 38 | 65 | // Preload after loaders are added so they can be used | 39 | 66 | if (preloadModules?.length) { | 40 | | - await loadModulesInIsolation(preloadModules, loaders); | | 67 | + await loadModulesInIsolation(parentURL, preloadModules, allLoaders); | 41 | 68 | } | 42 | 69 |
| 43 | 70 | isESMInitialized = true; | 44 | 71 | } | 45 | 72 |
| 46 | | -function loadModulesInIsolation(specifiers, loaders = []) { | | 73 | +function loadModulesInIsolation(parentURL, specifiers, loaders = []) { | 47 | 74 | if (!ArrayIsArray(specifiers) || specifiers.length === 0) { return; } | 48 | 75 |
| 49 | | - let cwd; | 50 | | - try { | 51 | | - cwd = process.cwd() + '/'; | 52 | | - } catch { | 53 | | - cwd = 'file:///'; | 54 | | - } | 55 | | - | 56 | 76 | // A separate loader instance is necessary to avoid cross-contamination | 57 | 77 | // between internal Node.js and userland. For example, a module with internal | 58 | 78 | // state (such as a counter) should be independent. | 59 | 79 | const internalEsmLoader = new ESMLoader(); | 60 | 80 | internalEsmLoader.addCustomLoaders(loaders); | | 81 | + internalEsmLoader.preload(); | 61 | 82 |
| 62 | 83 | // Importation must be handled by internal loader to avoid poluting userland | 63 | 84 | return internalEsmLoader.import( | 64 | 85 | specifiers, | 65 | | - pathToFileURL(cwd).href, | 66 | | - ObjectCreate(null), | | 86 | + parentURL, | | 87 | + kEmptyObject, | 67 | 88 | ); | 68 | 89 | } | 69 | 90 |
| |
Collapse file Copy file name to clipboardExpand all lines: test/es-module/test-esm-loader-chaining.mjs +17-1Lines changed: 17 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -4,7 +4,6 @@ import assert from 'node:assert'; | 4 | 4 | import { execPath } from 'node:process'; | 5 | 5 | import { describe, it } from 'node:test'; | 6 | 6 |
| 7 | | - | 8 | 7 | const setupArgs = [ | 9 | 8 | '--no-warnings', | 10 | 9 | '--input-type=module', | @@ -253,6 +252,23 @@ describe('ESM: loader chaining', { concurrency: true }, () => { | 253 | 252 | assert.strictEqual(code, 0); | 254 | 253 | }); | 255 | 254 |
| | 255 | + it('should allow loaders to influence subsequent loader resolutions', async () => { | | 256 | + const { code, stderr } = await spawnPromisified( | | 257 | + execPath, | | 258 | + [ | | 259 | + '--loader', | | 260 | + fixtures.fileURL('es-module-loaders', 'loader-resolve-strip-xxx.mjs'), | | 261 | + '--loader', | | 262 | + 'xxx/loader-resolve-strip-yyy.mjs', | | 263 | + ...commonArgs, | | 264 | + ], | | 265 | + { encoding: 'utf8', cwd: fixtures.path('es-module-loaders') }, | | 266 | + ); | | 267 | + | | 268 | + assert.strictEqual(stderr, ''); | | 269 | + assert.strictEqual(code, 0); | | 270 | + }); | | 271 | + | 256 | 272 | it('should throw when the resolve chain is broken', async () => { | 257 | 273 | const { code, stderr, stdout } = await spawnPromisified( | 258 | 274 | execPath, | |
Collapse file Copy file name to clipboardExpand all lines: test/fixtures/es-module-loaders/loader-load-foo-or-42.mjs +9-1Lines changed: 9 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 1 | | -export async function load(url) { | | 1 | +export async function load(url, context, next) { | | 2 | + // This check is needed to make sure that we don't prevent the | | 3 | + // resolution from follow-up loaders. It wouldn't be a problem | | 4 | + // in real life because loaders aren't supposed to break the | | 5 | + // resolution, but the ones used in our tests do, for convenience. | | 6 | + if (url.includes('loader')) { | | 7 | + return next(url); | | 8 | + } | | 9 | + | 2 | 10 | const val = url.includes('42') | 3 | 11 | ? '42' | 4 | 12 | : '"foo"'; | |
Collapse file Copy file name to clipboardExpand all lines: test/fixtures/es-module-loaders/loader-load-incomplete.mjs +9-1Lines changed: 9 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 1 | | -export async function load() { | | 1 | +export async function load(url, context, next) { | | 2 | + // This check is needed to make sure that we don't prevent the | | 3 | + // resolution from follow-up loaders. It wouldn't be a problem | | 4 | + // in real life because loaders aren't supposed to break the | | 5 | + // resolution, but the ones used in our tests do, for convenience. | | 6 | + if (url.includes('loader')) { | | 7 | + return next(url); | | 8 | + } | | 9 | + | 2 | 10 | return { | 3 | 11 | format: 'module', | 4 | 12 | source: 'export default 42', | |
Collapse file Copy file name to clipboard +8Lines changed: 8 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | export async function load(url, context, next) { | | 2 | + // This check is needed to make sure that we don't prevent the | | 3 | + // resolution from follow-up loaders. It wouldn't be a problem | | 4 | + // in real life because loaders aren't supposed to break the | | 5 | + // resolution, but the ones used in our tests do, for convenience. | | 6 | + if (url.includes('loader')) { | | 7 | + return next(url); | | 8 | + } | | 9 | + | 2 | 10 | console.log('load passthru'); // This log is deliberate | 3 | 11 | return next(url); | 4 | 12 | } |
Collapse file Copy file name to clipboardExpand all lines: test/fixtures/es-module-loaders/loader-resolve-42.mjs +8Lines changed: 8 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | export async function resolve(specifier, context, next) { | | 2 | + // This check is needed to make sure that we don't prevent the | | 3 | + // resolution from follow-up loaders. It wouldn't be a problem | | 4 | + // in real life because loaders aren't supposed to break the | | 5 | + // resolution, but the ones used in our tests do, for convenience. | | 6 | + if (specifier.includes('loader')) { | | 7 | + return next(specifier); | | 8 | + } | | 9 | + | 2 | 10 | console.log('resolve 42'); // This log is deliberate | 3 | 11 | console.log('next<HookName>:', next.name); // This log is deliberate | 4 | 12 |
| |
Collapse file Copy file name to clipboard +8Lines changed: 8 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | export async function resolve(specifier, context, next) { | | 2 | + // This check is needed to make sure that we don't prevent the | | 3 | + // resolution from follow-up loaders. It wouldn't be a problem | | 4 | + // in real life because loaders aren't supposed to break the | | 5 | + // resolution, but the ones used in our tests do, for convenience. | | 6 | + if (specifier.includes('loader')) { | | 7 | + return next(specifier); | | 8 | + } | | 9 | + | 2 | 10 | console.log('resolve foo'); // This log is deliberate | 3 | 11 | return next('file:///foo.mjs'); | 4 | 12 | } |
Collapse file Copy file name to clipboardExpand all lines: test/fixtures/es-module-loaders/loader-resolve-incomplete.mjs +9-1Lines changed: 9 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 1 | | -export async function resolve() { | | 1 | +export async function resolve(specifier, context, next) { | | 2 | + // This check is needed to make sure that we don't prevent the | | 3 | + // resolution from follow-up loaders. It wouldn't be a problem | | 4 | + // in real life because loaders aren't supposed to break the | | 5 | + // resolution, but the ones used in our tests do, for convenience. | | 6 | + if (specifier.includes('loader')) { | | 7 | + return next(specifier); | | 8 | + } | | 9 | + | 2 | 10 | return { | 3 | 11 | url: 'file:///incomplete-resolve-chain.js', | 4 | 12 | }; | |
|