| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 2ef80f1 commit 65dfe85
21 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1234,6 +1234,23 @@ provided. | |||
| 1234 | 1234 | Encoding provided to `TextDecoder()` API was not one of the | |
| 1235 | 1235 | [WHATWG Supported Encodings][]. | |
| 1236 | 1236 | ||
| 1237 | + <a id="ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE"></a> | ||
| 1238 | + | ||
| 1239 | + ### `ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE` | ||
| 1240 | + | ||
| 1241 | + <!-- YAML | ||
| 1242 | + added: REPLACEME | ||
| 1243 | + --> | ||
| 1244 | + | ||
| 1245 | + Programmatically registering custom ESM loaders | ||
| 1246 | + currently requires at least one custom loader to have been | ||
| 1247 | + registered via the `--experimental-loader` flag. A no-op | ||
| 1248 | + loader registered via CLI is sufficient | ||
| 1249 | + (for example: `--experimental-loader data:text/javascript,`; | ||
| 1250 | + do not omit the necessary trailing comma). | ||
| 1251 | + A future version of Node.js will support the programmatic | ||
| 1252 | + registration of loaders without needing to also use the flag. | ||
| 1253 | + | ||
| 1237 | 1254 | <a id="ERR_EVAL_ESM_CANNOT_PRINT"></a> | |
| 1238 | 1255 | ||
| 1239 | 1256 | ### `ERR_EVAL_ESM_CANNOT_PRINT` | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1236,6 +1236,17 @@ console.log('some module!'); | |||
| 1236 | 1236 | If you run `node --experimental-loader ./import-map-loader.js main.js` | |
| 1237 | 1237 | the output will be `some module!`. | |
| 1238 | 1238 | ||
| 1239 | + ### Register loaders programmatically | ||
| 1240 | + | ||
| 1241 | + <!-- YAML | ||
| 1242 | + added: REPLACEME | ||
| 1243 | + --> | ||
| 1244 | + | ||
| 1245 | + In addition to using the `--experimental-loader` option in the CLI, | ||
| 1246 | + loaders can also be registered programmatically. You can find | ||
| 1247 | + detailed information about this process in the documentation page | ||
| 1248 | + for [`module.register()`][]. | ||
| 1249 | + | ||
| 1239 | 1250 | ## Resolution and loading algorithm | |
| 1240 | 1251 | ||
| 1241 | 1252 | ### Features | |
@@ -1632,6 +1643,7 @@ success! | |||
| 1632 | 1643 | [`import.meta.url`]: #importmetaurl | |
| 1633 | 1644 | [`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import | |
| 1634 | 1645 | [`module.createRequire()`]: module.md#modulecreaterequirefilename | |
| 1646 | + [`module.register()`]: module.md#moduleregister | ||
| 1635 | 1647 | [`module.syncBuiltinESMExports()`]: module.md#modulesyncbuiltinesmexports | |
| 1636 | 1648 | [`package.json`]: packages.md#nodejs-packagejson-field-definitions | |
| 1637 | 1649 | [`port.ref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portref | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -78,6 +78,101 @@ isBuiltin('fs'); // true | |||
| 78 | 78 | isBuiltin('wss'); // false | |
| 79 | 79 | ``` | |
| 80 | 80 | ||
| 81 | + ### `module.register()` | ||
| 82 | + | ||
| 83 | + <!-- YAML | ||
| 84 | + added: REPLACEME | ||
| 85 | + --> | ||
| 86 | + | ||
| 87 | + In addition to using the `--experimental-loader` option in the CLI, | ||
| 88 | + loaders can be registered programmatically using the | ||
| 89 | + `module.register()` method. | ||
| 90 | + | ||
| 91 | + ```mjs | ||
| 92 | + import { register } from 'node:module'; | ||
| 93 | + | ||
| 94 | + register('http-to-https', import.meta.url); | ||
| 95 | + | ||
| 96 | + // Because this is a dynamic `import()`, the `http-to-https` hooks will run | ||
| 97 | + // before importing `./my-app.mjs`. | ||
| 98 | + await import('./my-app.mjs'); | ||
| 99 | + ``` | ||
| 100 | + | ||
| 101 | + In the example above, we are registering the `http-to-https` loader, | ||
| 102 | + but it will only be available for subsequently imported modules—in | ||
| 103 | + this case, `my-app.mjs`. If the `await import('./my-app.mjs')` had | ||
| 104 | + instead been a static `import './my-app.mjs'`, _the app would already | ||
| 105 | + have been loaded_ before the `http-to-https` hooks were | ||
| 106 | + registered. This is part of the design of ES modules, where static | ||
| 107 | + imports are evaluated from the leaves of the tree first back to the | ||
| 108 | + trunk. There can be static imports _within_ `my-app.mjs`, which | ||
| 109 | + will not be evaluated until `my-app.mjs` is when it's dynamically | ||
| 110 | + imported. | ||
| 111 | + | ||
| 112 | + The `--experimental-loader` flag of the CLI can be used together | ||
| 113 | + with the `register` function; the loaders registered with the | ||
| 114 | + function will follow the same evaluation chain of loaders registered | ||
| 115 | + within the CLI: | ||
| 116 | + | ||
| 117 | + ```console | ||
| 118 | + node \ | ||
| 119 | + --experimental-loader unpkg \ | ||
| 120 | + --experimental-loader http-to-https \ | ||
| 121 | + --experimental-loader cache-buster \ | ||
| 122 | + entrypoint.mjs | ||
| 123 | + ``` | ||
| 124 | + | ||
| 125 | + ```mjs | ||
| 126 | + // entrypoint.mjs | ||
| 127 | + import { URL } from 'node:url'; | ||
| 128 | + import { register } from 'node:module'; | ||
| 129 | + | ||
| 130 | + const loaderURL = new URL('./my-programmatically-loader.mjs', import.meta.url); | ||
| 131 | + | ||
| 132 | + register(loaderURL); | ||
| 133 | + await import('./my-app.mjs'); | ||
| 134 | + ``` | ||
| 135 | + | ||
| 136 | + The `my-programmatic-loader.mjs` can leverage `unpkg`, | ||
| 137 | + `http-to-https`, and `cache-buster` loaders. | ||
| 138 | + | ||
| 139 | + It's also possible to use `register` more than once: | ||
| 140 | + | ||
| 141 | + ```mjs | ||
| 142 | + // entrypoint.mjs | ||
| 143 | + import { URL } from 'node:url'; | ||
| 144 | + import { register } from 'node:module'; | ||
| 145 | + | ||
| 146 | + register(new URL('./first-loader.mjs', import.meta.url)); | ||
| 147 | + register('./second-loader.mjs', import.meta.url); | ||
| 148 | + await import('./my-app.mjs'); | ||
| 149 | + ``` | ||
| 150 | + | ||
| 151 | + Both loaders (`first-loader.mjs` and `second-loader.mjs`) can use | ||
| 152 | + all the resources provided by the loaders registered in the CLI. But | ||
| 153 | + remember that they will only be available in the next imported | ||
| 154 | + module (`my-app.mjs`). The evaluation order of the hooks when | ||
| 155 | + importing `my-app.mjs` and consecutive modules in the example above | ||
| 156 | + will be: | ||
| 157 | + | ||
| 158 | + ```console | ||
| 159 | + resolve: second-loader.mjs | ||
| 160 | + resolve: first-loader.mjs | ||
| 161 | + resolve: cache-buster | ||
| 162 | + resolve: http-to-https | ||
| 163 | + resolve: unpkg | ||
| 164 | + load: second-loader.mjs | ||
| 165 | + load: first-loader.mjs | ||
| 166 | + load: cache-buster | ||
| 167 | + load: http-to-https | ||
| 168 | + load: unpkg | ||
| 169 | + globalPreload: second-loader.mjs | ||
| 170 | + globalPreload: first-loader.mjs | ||
| 171 | + globalPreload: cache-buster | ||
| 172 | + globalPreload: http-to-https | ||
| 173 | + globalPreload: unpkg | ||
| 174 | + ``` | ||
| 175 | + | ||
| 81 | 176 | ### `module.syncBuiltinESMExports()` | |
| 82 | 177 | ||
| 83 | 178 | <!-- YAML | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1039,6 +1039,11 @@ E('ERR_ENCODING_INVALID_ENCODED_DATA', function(encoding, ret) { | |||
| 1039 | 1039 | }, TypeError); | |
| 1040 | 1040 | E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported', | |
| 1041 | 1041 | RangeError); | |
| 1042 | + E('ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE', 'Programmatically registering custom ESM loaders ' + | ||
| 1043 | + 'currently requires at least one custom loader to have been registered via the --experimental-loader ' + | ||
| 1044 | + 'flag. A no-op loader registered via CLI is sufficient (for example: `--experimental-loader ' + | ||
| 1045 | + '"data:text/javascript,"` with the necessary trailing comma). A future version of Node.js ' + | ||
| 1046 | + 'will remove this requirement.', Error); | ||
| 1042 | 1047 | E('ERR_EVAL_ESM_CANNOT_PRINT', '--print cannot be used with ESM input', Error); | |
| 1043 | 1048 | E('ERR_EVENT_RECURSION', 'The event "%s" is already being dispatched', Error); | |
| 1044 | 1049 | E('ERR_FALSY_VALUE_REJECTION', function(reason) { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -44,6 +44,8 @@ const { | |||
| 44 | 44 | validateString, | |
| 45 | 45 | } = require('internal/validators'); | |
| 46 | 46 | ||
| 47 | + const { kEmptyObject } = require('internal/util'); | ||
| 48 | + | ||
| 47 | 49 | const { | |
| 48 | 50 | defaultResolve, | |
| 49 | 51 | throwIfInvalidParentURL, | |
@@ -116,6 +118,23 @@ class Hooks { | |||
| 116 | 118 | // Cache URLs we've already validated to avoid repeated validation | |
| 117 | 119 | #validatedUrls = new SafeSet(); | |
| 118 | 120 | ||
| 121 | + /** | ||
| 122 | + * Import and register custom/user-defined module loader hook(s). | ||
| 123 | + * @param {string} urlOrSpecifier | ||
| 124 | + * @param {string} parentURL | ||
| 125 | + */ | ||
| 126 | + async register(urlOrSpecifier, parentURL) { | ||
| 127 | + const moduleLoader = require('internal/process/esm_loader').esmLoader; | ||
| 128 | + | ||
| 129 | + const keyedExports = await moduleLoader.import( | ||
| 130 | + urlOrSpecifier, | ||
| 131 | + parentURL, | ||
| 132 | + kEmptyObject, | ||
| 133 | + ); | ||
| 134 | + | ||
| 135 | + this.addCustomLoader(urlOrSpecifier, keyedExports); | ||
| 136 | + } | ||
| 137 | + | ||
| 119 | 138 | /** | |
| 120 | 139 | * Collect custom/user-defined module loader hook(s). | |
| 121 | 140 | * After all hooks have been collected, the global preload hook(s) must be initialized. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -11,6 +11,7 @@ const { | |||
| 11 | 11 | } = primordials; | |
| 12 | 12 | ||
| 13 | 13 | const { | |
| 14 | + ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE, | ||
| 14 | 15 | ERR_UNKNOWN_MODULE_FORMAT, | |
| 15 | 16 | } = require('internal/errors').codes; | |
| 16 | 17 | const { getOptionValue } = require('internal/options'); | |
@@ -297,12 +298,19 @@ class CustomizedModuleLoader extends DefaultModuleLoader { | |||
| 297 | 298 | constructor() { | |
| 298 | 299 | super(); | |
| 299 | 300 | ||
| 300 | - if (hooksProxy) { | ||
| 301 | - // The worker proxy is shared across all instances, so don't recreate it if it already exists. | ||
| 302 | - return; | ||
| 303 | - } | ||
| 304 | - const { HooksProxy } = require('internal/modules/esm/hooks'); | ||
| 305 | - hooksProxy = new HooksProxy(); // The user's custom hooks are loaded within the worker as part of its startup. | ||
| 301 | + getHooksProxy(); | ||
| 302 | + } | ||
| 303 | + | ||
| 304 | + /** | ||
| 305 | + * Register some loader specifier. | ||
| 306 | + * @param {string} originalSpecifier The specified URL path of the loader to | ||
| 307 | + * be registered. | ||
| 308 | + * @param {string} parentURL The parent URL from where the loader will be | ||
| 309 | + * registered if using it package name as specifier | ||
| 310 | + * @returns {{ format: string, url: URL['href'] }} | ||
| 311 | + */ | ||
| 312 | + register(originalSpecifier, parentURL) { | ||
| 313 | + return hooksProxy.makeSyncRequest('register', originalSpecifier, parentURL); | ||
| 306 | 314 | } | |
| 307 | 315 | ||
| 308 | 316 | /** | |
@@ -380,7 +388,46 @@ function createModuleLoader(useCustomLoadersIfPresent = true) { | |||
| 380 | 388 | return new DefaultModuleLoader(); | |
| 381 | 389 | } | |
| 382 | 390 | ||
| 391 | + /** | ||
| 392 | + * Get the HooksProxy instance. If it is not defined, then create a new one. | ||
| 393 | + * @returns {HooksProxy} | ||
| 394 | + */ | ||
| 395 | + function getHooksProxy() { | ||
| 396 | + if (!hooksProxy) { | ||
| 397 | + const { HooksProxy } = require('internal/modules/esm/hooks'); | ||
| 398 | + hooksProxy = new HooksProxy(); | ||
| 399 | + } | ||
| 400 | + | ||
| 401 | + return hooksProxy; | ||
| 402 | + } | ||
| 403 | + | ||
| 404 | + /** | ||
| 405 | + * Register a single loader programmatically. | ||
| 406 | + * @param {string} specifier | ||
| 407 | + * @param {string} [parentURL] | ||
| 408 | + * @returns {void} | ||
| 409 | + * @example | ||
| 410 | + * ```js | ||
| 411 | + * register('./myLoader.js'); | ||
| 412 | + * register('ts-node/esm', import.meta.url); | ||
| 413 | + * register('./myLoader.js', import.meta.url); | ||
| 414 | + * register(new URL('./myLoader.js', import.meta.url)); | ||
| 415 | + * ``` | ||
| 416 | + */ | ||
| 417 | + function register(specifier, parentURL = 'data:') { | ||
| 418 | + // TODO: Remove this limitation in a follow-up before `register` is released publicly | ||
| 419 | + if (getOptionValue('--experimental-loader').length < 1) { | ||
| 420 | + throw new ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE(); | ||
| 421 | + } | ||
| 422 | + | ||
| 423 | + const moduleLoader = require('internal/process/esm_loader').esmLoader; | ||
| 424 | + | ||
| 425 | + moduleLoader.register(`${specifier}`, parentURL); | ||
| 426 | + } | ||
| 427 | + | ||
| 383 | 428 | module.exports = { | |
| 384 | 429 | DefaultModuleLoader, | |
| 385 | 430 | createModuleLoader, | |
| 431 | + getHooksProxy, | ||
| 432 | + register, | ||
| 386 | 433 | }; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -146,9 +146,9 @@ async function initializeHooks() { | |||
| 146 | 146 | load(url, context) { return hooks.load(url, context); } | |
| 147 | 147 | } | |
| 148 | 148 | const privateModuleLoader = new ModuleLoader(); | |
| 149 | - | ||
| 150 | 149 | const parentURL = pathToFileURL(cwd).href; | |
| 151 | 150 | ||
| 151 | + // TODO(jlenon7): reuse the `Hooks.register()` method for registering loaders. | ||
| 152 | 152 | for (let i = 0; i < customLoaderURLs.length; i++) { | |
| 153 | 153 | const customLoaderURL = customLoaderURLs[i]; | |
| 154 | 154 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,8 +2,10 @@ | |||
| 2 | 2 | ||
| 3 | 3 | const { findSourceMap } = require('internal/source_map/source_map_cache'); | |
| 4 | 4 | const { Module } = require('internal/modules/cjs/loader'); | |
| 5 | + const { register } = require('internal/modules/esm/loader'); | ||
| 5 | 6 | const { SourceMap } = require('internal/source_map/source_map'); | |
| 6 | 7 | ||
| 7 | 8 | Module.findSourceMap = findSourceMap; | |
| 9 | + Module.register = register; | ||
| 8 | 10 | Module.SourceMap = SourceMap; | |
| 9 | 11 | module.exports = Module; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -24,6 +24,28 @@ describe('Loader hooks', { concurrency: true }, () => { | |||
| 24 | 24 | assert.match(lines[3], /{"source":{"type":"Buffer","data":\[.*\]},"format":"json","shortCircuit":true}/); | |
| 25 | 25 | }); | |
| 26 | 26 | ||
| 27 | + it('are called with all expected arguments using register function', async () => { | ||
| 28 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | ||
| 29 | + '--no-warnings', | ||
| 30 | + '--experimental-loader=data:text/javascript,', | ||
| 31 | + '--input-type=module', | ||
| 32 | + '--eval', | ||
| 33 | + "import { register } from 'node:module';" + | ||
| 34 | + `register(${JSON.stringify(fixtures.fileURL('/es-module-loaders/hooks-input.mjs'))});` + | ||
| 35 | + `await import(${JSON.stringify(fixtures.fileURL('/es-modules/json-modules.mjs'))});`, | ||
| 36 | + ]); | ||
| 37 | + | ||
| 38 | + assert.strictEqual(stderr, ''); | ||
| 39 | + assert.strictEqual(code, 0); | ||
| 40 | + assert.strictEqual(signal, null); | ||
| 41 | + | ||
| 42 | + const lines = stdout.split('\n'); | ||
| 43 | + assert.match(lines[0], /{"url":"file:\/\/\/.*\/json-modules\.mjs","format":"test","shortCircuit":true}/); | ||
| 44 | + assert.match(lines[1], /{"source":{"type":"Buffer","data":\[.*\]},"format":"module","shortCircuit":true}/); | ||
| 45 | + assert.match(lines[2], /{"url":"file:\/\/\/.*\/experimental\.json","format":"test","shortCircuit":true}/); | ||
| 46 | + assert.match(lines[3], /{"source":{"type":"Buffer","data":\[.*\]},"format":"json","shortCircuit":true}/); | ||
| 47 | + }); | ||
| 48 | + | ||
| 27 | 49 | describe('should handle never-settling hooks in ESM files', { concurrency: true }, () => { | |
| 28 | 50 | it('top-level await of a never-settling resolve', async () => { | |
| 29 | 51 | const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | |
| Back | FazBrowse Home | New Git URL |
0 commit comments