From 5aef593db31b6a0a53198301dc473f8294899c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lenon?= Date: Sun, 11 Jun 2023 21:00:46 -0300 Subject: [PATCH] module: implement `register` utility PR-URL: https://github.com/nodejs/node/pull/46826 Reviewed-By: Jacob Smith Reviewed-By: Geoffrey Booth Reviewed-By: Antoine du Hamel --- doc/api/errors.md | 17 ++ doc/api/esm.md | 12 + doc/api/module.md | 95 +++++++ lib/internal/errors.js | 5 + lib/internal/modules/esm/hooks.js | 19 ++ lib/internal/modules/esm/loader.js | 59 ++++- lib/internal/modules/esm/utils.js | 2 +- lib/module.js | 2 + test/es-module/test-esm-loader-hooks.mjs | 22 ++ .../test-esm-loader-programmatically.mjs | 236 ++++++++++++++++++ .../es-module-loaders/hooks-input.mjs | 6 +- .../loader-load-passthru.mjs | 1 - .../loader-resolve-passthru.mjs | 1 - .../node_modules/load/index.mjs | 1 + .../node_modules/load/package.json | 3 + .../node_modules/resolve/index.mjs | 1 + .../node_modules/resolve/package.json | 3 + .../es-module-loaders/register-loader.cjs | 4 + .../es-module-loaders/register-loader.mjs | 4 + .../register-programmatically-loader-load.mjs | 4 + ...gister-programmatically-loader-resolve.mjs | 3 + 21 files changed, 490 insertions(+), 10 deletions(-) create mode 100644 test/es-module/test-esm-loader-programmatically.mjs create mode 100644 test/fixtures/es-module-loaders/node_modules/load/index.mjs create mode 100644 test/fixtures/es-module-loaders/node_modules/load/package.json create mode 100644 test/fixtures/es-module-loaders/node_modules/resolve/index.mjs create mode 100644 test/fixtures/es-module-loaders/node_modules/resolve/package.json create mode 100644 test/fixtures/es-module-loaders/register-loader.cjs create mode 100644 test/fixtures/es-module-loaders/register-loader.mjs create mode 100644 test/fixtures/es-module-loaders/register-programmatically-loader-load.mjs create mode 100644 test/fixtures/es-module-loaders/register-programmatically-loader-resolve.mjs diff --git a/doc/api/errors.md b/doc/api/errors.md index 5a8637a0ecc8b0..3e338a4e8f8cd6 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -1233,6 +1233,23 @@ provided. Encoding provided to `TextDecoder()` API was not one of the [WHATWG Supported Encodings][]. + + +### `ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE` + + + +Programmatically registering custom ESM loaders +currently requires at least one custom loader to have been +registered via the `--experimental-loader` flag. A no-op +loader registered via CLI is sufficient +(for example: `--experimental-loader data:text/javascript,`; +do not omit the necessary trailing comma). +A future version of Node.js will support the programmatic +registration of loaders without needing to also use the flag. + ### `ERR_EVAL_ESM_CANNOT_PRINT` diff --git a/doc/api/esm.md b/doc/api/esm.md index 3d450ce3c69310..68ecad93b8ad3b 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -1225,6 +1225,17 @@ console.log('some module!'); If you run `node --experimental-loader ./import-map-loader.js main.js` the output will be `some module!`. +### Register loaders programmatically + + + +In addition to using the `--experimental-loader` option in the CLI, +loaders can also be registered programmatically. You can find +detailed information about this process in the documentation page +for [`module.register()`][]. + ## Resolution and loading algorithm ### Features @@ -1599,6 +1610,7 @@ for ESM specifiers is [commonjs-extension-resolution-loader][]. [`import.meta.url`]: #importmetaurl [`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import [`module.createRequire()`]: module.md#modulecreaterequirefilename +[`module.register()`]: module.md#moduleregister [`module.syncBuiltinESMExports()`]: module.md#modulesyncbuiltinesmexports [`package.json`]: packages.md#nodejs-packagejson-field-definitions [`port.ref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portref diff --git a/doc/api/module.md b/doc/api/module.md index d77b6e29cc7a17..8dd5fd4fa59f6c 100644 --- a/doc/api/module.md +++ b/doc/api/module.md @@ -80,6 +80,101 @@ isBuiltin('fs'); // true isBuiltin('wss'); // false ``` +### `module.register()` + + + +In addition to using the `--experimental-loader` option in the CLI, +loaders can be registered programmatically using the +`module.register()` method. + +```mjs +import { register } from 'node:module'; + +register('http-to-https', import.meta.url); + +// Because this is a dynamic `import()`, the `http-to-https` hooks will run +// before importing `./my-app.mjs`. +await import('./my-app.mjs'); +``` + +In the example above, we are registering the `http-to-https` loader, +but it will only be available for subsequently imported modules—in +this case, `my-app.mjs`. If the `await import('./my-app.mjs')` had +instead been a static `import './my-app.mjs'`, _the app would already +have been loaded_ before the `http-to-https` hooks were +registered. This is part of the design of ES modules, where static +imports are evaluated from the leaves of the tree first back to the +trunk. There can be static imports _within_ `my-app.mjs`, which +will not be evaluated until `my-app.mjs` is when it's dynamically +imported. + +The `--experimental-loader` flag of the CLI can be used together +with the `register` function; the loaders registered with the +function will follow the same evaluation chain of loaders registered +within the CLI: + +```console +node \ + --experimental-loader unpkg \ + --experimental-loader http-to-https \ + --experimental-loader cache-buster \ + entrypoint.mjs +``` + +```mjs +// entrypoint.mjs +import { URL } from 'node:url'; +import { register } from 'node:module'; + +const loaderURL = new URL('./my-programmatically-loader.mjs', import.meta.url); + +register(loaderURL); +await import('./my-app.mjs'); +``` + +The `my-programmatic-loader.mjs` can leverage `unpkg`, +`http-to-https`, and `cache-buster` loaders. + +It's also possible to use `register` more than once: + +```mjs +// entrypoint.mjs +import { URL } from 'node:url'; +import { register } from 'node:module'; + +register(new URL('./first-loader.mjs', import.meta.url)); +register('./second-loader.mjs', import.meta.url); +await import('./my-app.mjs'); +``` + +Both loaders (`first-loader.mjs` and `second-loader.mjs`) can use +all the resources provided by the loaders registered in the CLI. But +remember that they will only be available in the next imported +module (`my-app.mjs`). The evaluation order of the hooks when +importing `my-app.mjs` and consecutive modules in the example above +will be: + +```console +resolve: second-loader.mjs +resolve: first-loader.mjs +resolve: cache-buster +resolve: http-to-https +resolve: unpkg +load: second-loader.mjs +load: first-loader.mjs +load: cache-buster +load: http-to-https +load: unpkg +globalPreload: second-loader.mjs +globalPreload: first-loader.mjs +globalPreload: cache-buster +globalPreload: http-to-https +globalPreload: unpkg +``` + ### `module.syncBuiltinESMExports()`