Skip to content

Commit d30faa9

Browse files
committed
esm: support loader hook transformSource
Refs: nodejs/modules#390
1 parent 179f423 commit d30faa9

File tree

5 files changed

+50
-2
lines changed

5 files changed

+50
-2
lines changed

doc/api/esm.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,17 @@ With the list of module exports provided upfront, the `execute` function will
776776
then be called at the exact point of module evaluation order for that module
777777
in the import tree.
778778
779+
### Transform hook
780+
781+
This hook is called only for modules that return `format: 'module'` from
782+
the `resolve` hook.
783+
784+
```js
785+
export async function transformSource(url, source) {
786+
return source.replace(/'original'/, "'replacement'");
787+
}
788+
```
789+
779790
## Resolution Algorithm
780791
781792
### Features

lib/internal/modules/esm/loader.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ class Loader {
6161
// an object with the same keys as `exports`, whose values are get/set
6262
// functions for the actual exported values.
6363
this._dynamicInstantiate = undefined;
64+
// These hooks are called FILO when resolve(...).format is 'module' and
65+
// has the signature
66+
// (code : string, url : string) -> Promise<code : string>
67+
this._transformSource = [];
6468
// The index for assigning unique URLs to anonymous module evaluation
6569
this.evalIndex = 0;
6670
}
@@ -133,14 +137,26 @@ class Loader {
133137
return module.getNamespace();
134138
}
135139

136-
hook({ resolve, dynamicInstantiate }) {
140+
async transformSource(url, code) {
141+
for (const transformFn of this._transformSource) {
142+
code = await transformFn(url, code);
143+
}
144+
145+
return code;
146+
}
147+
148+
hook({ resolve, dynamicInstantiate, transformSource }) {
137149
// Use .bind() to avoid giving access to the Loader instance when called.
138150
if (resolve !== undefined)
139151
this._resolve = FunctionPrototype.bind(resolve, null);
140152
if (dynamicInstantiate !== undefined) {
141153
this._dynamicInstantiate =
142154
FunctionPrototype.bind(dynamicInstantiate, null);
143155
}
156+
if (transformSource !== undefined) {
157+
// this.transformSource protects `this`, no need to bind.
158+
this._transformSource.push(transformSource);
159+
}
144160
}
145161

146162
async getModuleJob(specifier, parentURL) {

lib/internal/modules/esm/translators.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ async function importModuleDynamically(specifier, { url }) {
7676

7777
// Strategy for loading a standard JavaScript module
7878
translators.set('module', async function moduleStrategy(url) {
79-
const source = `${await getSource(url)}`;
79+
const source = await this.transformSource(url, `${await getSource(url)}`);
8080
maybeCacheSourceMap(url, source);
8181
debug(`Translating StandardModule ${url}`);
8282
const module = new ModuleWrap(source, url);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Flags: --experimental-modules --experimental-loader ./test/fixtures/es-module-loaders/transform-loader.mjs
2+
/* eslint-disable node-core/require-common-first, node-core/required-modules */
3+
import {
4+
foo,
5+
bar
6+
} from '../fixtures/es-module-loaders/module-named-exports.mjs';
7+
import assert from 'assert';
8+
9+
assert.strictEqual(foo, 'transformed-foo');
10+
assert.strictEqual(bar, 'transformed-bar');
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { promisify } from 'util';
2+
3+
const delay = promisify(setTimeout);
4+
5+
export async function transformSource(url, source) {
6+
await delay(50);
7+
8+
return source
9+
.replace(/'bar'/, "'transformed-bar'")
10+
.replace(/'foo'/, "'transformed-foo'");
11+
}

0 commit comments

Comments
 (0)