Skip to content

Commit d09f6d5

Browse files
bfarias-godaddycodebytere
authored andcommitted
esm: doc & validate source values for formats
PR-URL: #32202 Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: Geoffrey Booth <[email protected]> Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: Zeyu Yang <[email protected]>
1 parent 70d025f commit d09f6d5

File tree

5 files changed

+150
-13
lines changed

5 files changed

+150
-13
lines changed

doc/api/esm.md

+26-9
Original file line numberDiff line numberDiff line change
@@ -1170,16 +1170,27 @@ export async function resolve(specifier, context, defaultResolve) {
11701170
> signature may change. Do not rely on the API described below.
11711171
11721172
The `getFormat` hook provides a way to define a custom method of determining how
1173-
a URL should be interpreted. This can be one of the following:
1173+
a URL should be interpreted. The `format` returned also affects what the
1174+
acceptable forms of source values are for a module when parsing. This can be one
1175+
of the following:
11741176
1175-
| `format` | Description |
1176-
| --- | --- |
1177-
| `'builtin'` | Load a Node.js builtin module |
1178-
| `'commonjs'` | Load a Node.js CommonJS module |
1179-
| `'dynamic'` | Use a [dynamic instantiate hook][] |
1180-
| `'json'` | Load a JSON file |
1181-
| `'module'` | Load a standard JavaScript module (ES module) |
1182-
| `'wasm'` | Load a WebAssembly module |
1177+
| `format` | Description | Acceptable Types For `source` Returned by `getSource` or `transformSource` |
1178+
| --- | --- | --- |
1179+
| `'builtin'` | Load a Node.js builtin module | Not applicable |
1180+
| `'commonjs'` | Load a Node.js CommonJS module | Not applicable |
1181+
| `'dynamic'` | Use a [dynamic instantiate hook][] | Not applicable |
1182+
| `'json'` | Load a JSON file | { [ArrayBuffer][], [string][], [TypedArray][] } |
1183+
| `'module'` | Load an ES module | { [ArrayBuffer][], [string][], [TypedArray][] } |
1184+
| `'wasm'` | Load a WebAssembly module | { [ArrayBuffer][], [string][], [TypedArray][] } |
1185+
1186+
Note: These types all correspond to classes defined in ECMAScript.
1187+
1188+
* The specific [ArrayBuffer][] object is a [SharedArrayBuffer][].
1189+
* The specific [string][] object is not the class constructor, but an instance.
1190+
* The specific [TypedArray][] object is a [Uint8Array][].
1191+
1192+
Note: If the source value of a text-based format (i.e., `'json'`, `'module'`) is
1193+
not a string, it will be converted to a string using [`util.TextDecoder`][].
11831194
11841195
```js
11851196
/**
@@ -1819,6 +1830,12 @@ success!
18191830
[`module.createRequire()`]: modules.html#modules_module_createrequire_filename
18201831
[`module.syncBuiltinESMExports()`]: modules.html#modules_module_syncbuiltinesmexports
18211832
[`transformSource` hook]: #esm_code_transformsource_code_hook
1833+
[ArrayBuffer]: http://www.ecma-international.org/ecma-262/6.0/#sec-arraybuffer-constructor
1834+
[SharedArrayBuffer]: https://tc39.es/ecma262/#sec-sharedarraybuffer-constructor
1835+
[string]: http://www.ecma-international.org/ecma-262/6.0/#sec-string-constructor
1836+
[TypedArray]: http://www.ecma-international.org/ecma-262/6.0/#sec-typedarray-objects
1837+
[Uint8Array]: http://www.ecma-international.org/ecma-262/6.0/#sec-uint8array
1838+
[`util.TextDecoder`]: util.html#util_class_util_textdecoder
18221839
[dynamic instantiate hook]: #esm_code_dynamicinstantiate_code_hook
18231840
[import an ES or CommonJS module for its side effects only]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Import_a_module_for_its_side_effects_only
18241841
[special scheme]: https://url.spec.whatwg.org/#special-scheme

lib/internal/modules/esm/translators.js

+40-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ const {
1111
StringPrototypeReplace,
1212
} = primordials;
1313

14+
let _TYPES = null;
15+
function lazyTypes() {
16+
if (_TYPES !== null) return _TYPES;
17+
return _TYPES = require('internal/util/types');
18+
}
19+
1420
const {
1521
stripBOM,
1622
loadNativeModule
@@ -26,7 +32,10 @@ const createDynamicModule = require(
2632
const { fileURLToPath, URL } = require('url');
2733
const { debuglog } = require('internal/util/debuglog');
2834
const { emitExperimentalWarning } = require('internal/util');
29-
const { ERR_UNKNOWN_BUILTIN_MODULE } = require('internal/errors').codes;
35+
const {
36+
ERR_UNKNOWN_BUILTIN_MODULE,
37+
ERR_INVALID_RETURN_PROPERTY_VALUE
38+
} = require('internal/errors').codes;
3039
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
3140
const moduleWrap = internalBinding('module_wrap');
3241
const { ModuleWrap } = moduleWrap;
@@ -39,6 +48,30 @@ const debug = debuglog('esm');
3948
const translators = new SafeMap();
4049
exports.translators = translators;
4150

51+
let DECODER = null;
52+
function assertBufferSource(body, allowString, hookName) {
53+
if (allowString && typeof body === 'string') {
54+
return;
55+
}
56+
const { isArrayBufferView, isAnyArrayBuffer } = lazyTypes();
57+
if (isArrayBufferView(body) || isAnyArrayBuffer(body)) {
58+
return;
59+
}
60+
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
61+
`${allowString ? 'string, ' : ''}array buffer, or typed array`,
62+
hookName,
63+
'source',
64+
body
65+
);
66+
}
67+
68+
function stringify(body) {
69+
if (typeof body === 'string') return body;
70+
assertBufferSource(body, false, 'transformSource');
71+
DECODER = DECODER === null ? new TextDecoder() : DECODER;
72+
return DECODER.decode(body);
73+
}
74+
4275
function errPath(url) {
4376
const parsed = new URL(url);
4477
if (parsed.protocol === 'file:') {
@@ -80,9 +113,10 @@ function initializeImportMeta(meta, { url }) {
80113
translators.set('module', async function moduleStrategy(url) {
81114
let { source } = await this._getSource(
82115
url, { format: 'module' }, defaultGetSource);
83-
source = `${source}`;
116+
assertBufferSource(source, true, 'getSource');
84117
({ source } = await this._transformSource(
85118
source, { url, format: 'module' }, defaultTransformSource));
119+
source = stringify(source);
86120
maybeCacheSourceMap(url, source);
87121
debug(`Translating StandardModule ${url}`);
88122
const module = new ModuleWrap(url, undefined, source, 0, 0);
@@ -157,9 +191,10 @@ translators.set('json', async function jsonStrategy(url) {
157191
}
158192
let { source } = await this._getSource(
159193
url, { format: 'json' }, defaultGetSource);
160-
source = `${source}`;
194+
assertBufferSource(source, true, 'getSource');
161195
({ source } = await this._transformSource(
162196
source, { url, format: 'json' }, defaultTransformSource));
197+
source = stringify(source);
163198
if (pathname) {
164199
// A require call could have been called on the same file during loading and
165200
// that resolves synchronously. To make sure we always return the identical
@@ -200,8 +235,10 @@ translators.set('wasm', async function(url) {
200235
emitExperimentalWarning('Importing Web Assembly modules');
201236
let { source } = await this._getSource(
202237
url, { format: 'wasm' }, defaultGetSource);
238+
assertBufferSource(source, false, 'getSource');
203239
({ source } = await this._transformSource(
204240
source, { url, format: 'wasm' }, defaultTransformSource));
241+
assertBufferSource(source, false, 'transformSource');
205242
debug(`Translating WASMModule ${url}`);
206243
let compiled;
207244
try {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/string-sources.mjs
2+
import { mustCall, mustNotCall } from '../common/index.mjs';
3+
import assert from 'assert';
4+
5+
import('test:Array').then(
6+
mustNotCall('Should not accept Arrays'),
7+
mustCall((e) => {
8+
assert.strictEqual(e.code, 'ERR_INVALID_RETURN_PROPERTY_VALUE');
9+
})
10+
);
11+
import('test:ArrayBuffer').then(
12+
mustCall(),
13+
mustNotCall('Should accept ArrayBuffers'),
14+
);
15+
import('test:null').then(
16+
mustNotCall('Should not accept null'),
17+
mustCall((e) => {
18+
assert.strictEqual(e.code, 'ERR_INVALID_RETURN_PROPERTY_VALUE');
19+
})
20+
);
21+
import('test:Object').then(
22+
mustNotCall('Should not stringify or valueOf Objects'),
23+
mustCall((e) => {
24+
assert.strictEqual(e.code, 'ERR_INVALID_RETURN_PROPERTY_VALUE');
25+
})
26+
);
27+
import('test:SharedArrayBuffer').then(
28+
mustCall(),
29+
mustNotCall('Should accept SharedArrayBuffers'),
30+
);
31+
import('test:string').then(
32+
mustCall(),
33+
mustNotCall('Should accept strings'),
34+
);
35+
import('test:String').then(
36+
mustNotCall('Should not accept wrapper Strings'),
37+
mustCall((e) => {
38+
assert.strictEqual(e.code, 'ERR_INVALID_RETURN_PROPERTY_VALUE');
39+
})
40+
);
41+
import('test:Uint8Array').then(
42+
mustCall(),
43+
mustNotCall('Should accept Uint8Arrays'),
44+
);
45+
import('test:undefined').then(
46+
mustNotCall('Should not accept undefined'),
47+
mustCall((e) => {
48+
assert.strictEqual(e.code, 'ERR_INVALID_RETURN_PROPERTY_VALUE');
49+
})
50+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const SOURCES = {
2+
__proto__: null,
3+
'test:Array': ['1', '2'], // both `1,2` and `12` are valid ESM
4+
'test:ArrayBuffer': new ArrayBuffer(0),
5+
'test:null': null,
6+
'test:Object': {},
7+
'test:SharedArrayBuffer': new SharedArrayBuffer(0),
8+
'test:string': '',
9+
'test:String': new String(''),
10+
'test:Uint8Array': new Uint8Array(0),
11+
'test:undefined': undefined,
12+
}
13+
export function resolve(specifier, context, defaultFn) {
14+
if (specifier.startsWith('test:')) {
15+
return { url: specifier };
16+
}
17+
return defaultFn(specifier, context);
18+
}
19+
export function getFormat(href, context, defaultFn) {
20+
if (href.startsWith('test:')) {
21+
return { format: 'module' };
22+
}
23+
return defaultFn(href, context);
24+
}
25+
export function getSource(href, context, defaultFn) {
26+
if (href.startsWith('test:')) {
27+
return { source: SOURCES[href] };
28+
}
29+
return defaultFn(href, context);
30+
}

test/fixtures/es-module-loaders/transform-source.mjs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
export async function transformSource(
22
source, { url, format }, defaultTransformSource) {
3-
if (source && source.replace) {
3+
if (format === 'module') {
4+
if (typeof source !== 'string') {
5+
source = new TextDecoder().decode(source);
6+
}
47
return {
58
source: source.replace(`'A message';`, `'A message'.toUpperCase();`)
69
};

0 commit comments

Comments
 (0)