From 65111a29e7fd77dea554d07042ff498aae3b0afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lenon?= Date: Wed, 7 Jun 2023 07:44:51 -0300 Subject: [PATCH 01/15] module: implement `register` utility --- doc/api/errors.md | 17 ++ doc/api/esm.md | 12 ++ doc/api/module.md | 98 +++++++++ lib/internal/errors.js | 5 + lib/internal/modules/esm/hooks.js | 21 +- lib/internal/modules/esm/loader.js | 55 ++++- lib/internal/modules/esm/utils.js | 15 +- lib/internal/process/esm_loader.js | 11 +- lib/internal/util.js | 19 ++ lib/module.js | 2 + test/es-module/test-esm-loader-hooks.mjs | 22 ++ .../test-esm-loader-programmatically.mjs | 200 ++++++++++++++++++ .../es-module-loaders/hooks-input.mjs | 6 +- .../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 | 4 + 17 files changed, 470 insertions(+), 29 deletions(-) create mode 100644 test/es-module/test-esm-loader-programmatically.mjs 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 09f866fc2869b5..da2335ae275d68 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -1267,6 +1267,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 d52ec34dd12a54..7de5ac915f5863 100644 --- a/doc/api/module.md +++ b/doc/api/module.md @@ -80,6 +80,104 @@ 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'); + +// 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 loaderPath = new URL('./my-programmatically-loader.mjs', import.meta.url).href; + +register(loaderPath); +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'; + +const firstLoader = new URL('./first-loader.mjs', import.meta.url).href; +const secondLoader = new URL('./second-loader.mjs', import.meta.url).href; + +register(firstLoader); +register(secondLoader); +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()`