diff --git a/doc/api/esm.md b/doc/api/esm.md index 951ce418195d72..1a93b613bc013a 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -776,6 +776,17 @@ With the list of module exports provided upfront, the `execute` function will then be called at the exact point of module evaluation order for that module in the import tree. +### Transform hook + +This hook is called only for modules that return `format: 'module'` from +the `resolve` hook. + +```js +export async function transformSource(url, source) { + return source.replace(/'original'/, "'replacement'"); +} +``` + ## Resolution Algorithm ### Features diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 138cf8b5ecc3ed..cbbb8702e6c1d0 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -61,6 +61,10 @@ class Loader { // an object with the same keys as `exports`, whose values are get/set // functions for the actual exported values. this._dynamicInstantiate = undefined; + // These hooks are called FILO when resolve(...).format is 'module' and + // has the signature + // (code : string, url : string) -> Promise + this._transformSource = []; // The index for assigning unique URLs to anonymous module evaluation this.evalIndex = 0; } @@ -133,7 +137,15 @@ class Loader { return module.getNamespace(); } - hook({ resolve, dynamicInstantiate }) { + async transformSource(url, code) { + for (const transformFn of this._transformSource) { + code = await transformFn(url, code); + } + + return code; + } + + hook({ resolve, dynamicInstantiate, transformSource }) { // Use .bind() to avoid giving access to the Loader instance when called. if (resolve !== undefined) this._resolve = FunctionPrototype.bind(resolve, null); @@ -141,6 +153,10 @@ class Loader { this._dynamicInstantiate = FunctionPrototype.bind(dynamicInstantiate, null); } + if (transformSource !== undefined) { + // this.transformSource protects `this`, no need to bind. + this._transformSource.push(transformSource); + } } async getModuleJob(specifier, parentURL) { diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index b4d41685ed094b..4095eea91bc262 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -76,7 +76,7 @@ async function importModuleDynamically(specifier, { url }) { // Strategy for loading a standard JavaScript module translators.set('module', async function moduleStrategy(url) { - const source = `${await getSource(url)}`; + const source = await this.transformSource(url, `${await getSource(url)}`); maybeCacheSourceMap(url, source); debug(`Translating StandardModule ${url}`); const module = new ModuleWrap(source, url); diff --git a/test/es-module/test-esm-transform-source.mjs b/test/es-module/test-esm-transform-source.mjs new file mode 100644 index 00000000000000..7b151ddc7d07b7 --- /dev/null +++ b/test/es-module/test-esm-transform-source.mjs @@ -0,0 +1,10 @@ +// Flags: --experimental-modules --experimental-loader ./test/fixtures/es-module-loaders/transform-loader.mjs +/* eslint-disable node-core/require-common-first, node-core/required-modules */ +import { + foo, + bar +} from '../fixtures/es-module-loaders/module-named-exports.mjs'; +import assert from 'assert'; + +assert.strictEqual(foo, 'transformed-foo'); +assert.strictEqual(bar, 'transformed-bar'); diff --git a/test/fixtures/es-module-loaders/transform-loader.mjs b/test/fixtures/es-module-loaders/transform-loader.mjs new file mode 100644 index 00000000000000..20bbc0c20d27d6 --- /dev/null +++ b/test/fixtures/es-module-loaders/transform-loader.mjs @@ -0,0 +1,11 @@ +import { promisify } from 'util'; + +const delay = promisify(setTimeout); + +export async function transformSource(url, source) { + await delay(50); + + return source + .replace(/'bar'/, "'transformed-bar'") + .replace(/'foo'/, "'transformed-foo'"); +}