From 65dfe85f03fb10bea87ae427013e78a207917f34 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 Backport-PR-URL: https://github.com/nodejs/node/pull/50669 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 c589fcd375136d..cb10d48f2dd9ec 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -1234,6 +1234,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 ad03dec7ba5f03..3c32625bda0af2 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -1236,6 +1236,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 @@ -1632,6 +1643,7 @@ success! [`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 cb0b27cb612be7..2fb1266df48b68 100644 --- a/doc/api/module.md +++ b/doc/api/module.md @@ -78,6 +78,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()`