Skip to content

Commit

Permalink
esm: doc & validate source values for formats
Browse files Browse the repository at this point in the history
PR-URL: nodejs#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]>
  • Loading branch information
bfarias-godaddy authored and himself65 committed May 21, 2020
1 parent 19d9e20 commit cd4985c
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 13 deletions.
35 changes: 26 additions & 9 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -1196,16 +1196,27 @@ export async function resolve(specifier, context, defaultResolve) {
> signature may change. Do not rely on the API described below.
The `getFormat` hook provides a way to define a custom method of determining how
a URL should be interpreted. This can be one of the following:
a URL should be interpreted. The `format` returned also affects what the
acceptable forms of source values are for a module when parsing. This can be one
of the following:
| `format` | Description |
| --- | --- |
| `'builtin'` | Load a Node.js builtin module |
| `'commonjs'` | Load a Node.js CommonJS module |
| `'dynamic'` | Use a [dynamic instantiate hook][] |
| `'json'` | Load a JSON file |
| `'module'` | Load a standard JavaScript module (ES module) |
| `'wasm'` | Load a WebAssembly module |
| `format` | Description | Acceptable Types For `source` Returned by `getSource` or `transformSource` |
| --- | --- | --- |
| `'builtin'` | Load a Node.js builtin module | Not applicable |
| `'commonjs'` | Load a Node.js CommonJS module | Not applicable |
| `'dynamic'` | Use a [dynamic instantiate hook][] | Not applicable |
| `'json'` | Load a JSON file | { [ArrayBuffer][], [string][], [TypedArray][] } |
| `'module'` | Load an ES module | { [ArrayBuffer][], [string][], [TypedArray][] } |
| `'wasm'` | Load a WebAssembly module | { [ArrayBuffer][], [string][], [TypedArray][] } |
Note: These types all correspond to classes defined in ECMAScript.
* The specific [ArrayBuffer][] object is a [SharedArrayBuffer][].
* The specific [string][] object is not the class constructor, but an instance.
* The specific [TypedArray][] object is a [Uint8Array][].
Note: If the source value of a text-based format (i.e., `'json'`, `'module'`) is
not a string, it will be converted to a string using [`util.TextDecoder`][].
```js
/**
Expand Down Expand Up @@ -1842,6 +1853,12 @@ success!
[`module.createRequire()`]: modules.html#modules_module_createrequire_filename
[`module.syncBuiltinESMExports()`]: modules.html#modules_module_syncbuiltinesmexports
[`transformSource` hook]: #esm_code_transformsource_code_hook
[ArrayBuffer]: http://www.ecma-international.org/ecma-262/6.0/#sec-arraybuffer-constructor
[SharedArrayBuffer]: https://tc39.es/ecma262/#sec-sharedarraybuffer-constructor
[string]: http://www.ecma-international.org/ecma-262/6.0/#sec-string-constructor
[TypedArray]: http://www.ecma-international.org/ecma-262/6.0/#sec-typedarray-objects
[Uint8Array]: http://www.ecma-international.org/ecma-262/6.0/#sec-uint8array
[`util.TextDecoder`]: util.html#util_class_util_textdecoder
[dynamic instantiate hook]: #esm_code_dynamicinstantiate_code_hook
[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
[special scheme]: https://url.spec.whatwg.org/#special-scheme
Expand Down
43 changes: 40 additions & 3 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ const {
StringPrototypeReplace,
} = primordials;

let _TYPES = null;
function lazyTypes() {
if (_TYPES !== null) return _TYPES;
return _TYPES = require('internal/util/types');
}

const {
stripBOM,
loadNativeModule
Expand All @@ -26,7 +32,10 @@ const createDynamicModule = require(
const { fileURLToPath, URL } = require('url');
const { debuglog } = require('internal/util/debuglog');
const { emitExperimentalWarning } = require('internal/util');
const { ERR_UNKNOWN_BUILTIN_MODULE } = require('internal/errors').codes;
const {
ERR_UNKNOWN_BUILTIN_MODULE,
ERR_INVALID_RETURN_PROPERTY_VALUE
} = require('internal/errors').codes;
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
const moduleWrap = internalBinding('module_wrap');
const { ModuleWrap } = moduleWrap;
Expand All @@ -39,6 +48,30 @@ const debug = debuglog('esm');
const translators = new SafeMap();
exports.translators = translators;

let DECODER = null;
function assertBufferSource(body, allowString, hookName) {
if (allowString && typeof body === 'string') {
return;
}
const { isArrayBufferView, isAnyArrayBuffer } = lazyTypes();
if (isArrayBufferView(body) || isAnyArrayBuffer(body)) {
return;
}
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
`${allowString ? 'string, ' : ''}array buffer, or typed array`,
hookName,
'source',
body
);
}

function stringify(body) {
if (typeof body === 'string') return body;
assertBufferSource(body, false, 'transformSource');
DECODER = DECODER === null ? new TextDecoder() : DECODER;
return DECODER.decode(body);
}

function errPath(url) {
const parsed = new URL(url);
if (parsed.protocol === 'file:') {
Expand Down Expand Up @@ -80,9 +113,10 @@ function initializeImportMeta(meta, { url }) {
translators.set('module', async function moduleStrategy(url) {
let { source } = await this._getSource(
url, { format: 'module' }, defaultGetSource);
source = `${source}`;
assertBufferSource(source, true, 'getSource');
({ source } = await this._transformSource(
source, { url, format: 'module' }, defaultTransformSource));
source = stringify(source);
maybeCacheSourceMap(url, source);
debug(`Translating StandardModule ${url}`);
const module = new ModuleWrap(url, undefined, source, 0, 0);
Expand Down Expand Up @@ -157,9 +191,10 @@ translators.set('json', async function jsonStrategy(url) {
}
let { source } = await this._getSource(
url, { format: 'json' }, defaultGetSource);
source = `${source}`;
assertBufferSource(source, true, 'getSource');
({ source } = await this._transformSource(
source, { url, format: 'json' }, defaultTransformSource));
source = stringify(source);
if (pathname) {
// A require call could have been called on the same file during loading and
// that resolves synchronously. To make sure we always return the identical
Expand Down Expand Up @@ -200,8 +235,10 @@ translators.set('wasm', async function(url) {
emitExperimentalWarning('Importing Web Assembly modules');
let { source } = await this._getSource(
url, { format: 'wasm' }, defaultGetSource);
assertBufferSource(source, false, 'getSource');
({ source } = await this._transformSource(
source, { url, format: 'wasm' }, defaultTransformSource));
assertBufferSource(source, false, 'transformSource');
debug(`Translating WASMModule ${url}`);
let compiled;
try {
Expand Down
50 changes: 50 additions & 0 deletions test/es-module/test-esm-loader-stringify-text.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/string-sources.mjs
import { mustCall, mustNotCall } from '../common/index.mjs';
import assert from 'assert';

import('test:Array').then(
mustNotCall('Should not accept Arrays'),
mustCall((e) => {
assert.strictEqual(e.code, 'ERR_INVALID_RETURN_PROPERTY_VALUE');
})
);
import('test:ArrayBuffer').then(
mustCall(),
mustNotCall('Should accept ArrayBuffers'),
);
import('test:null').then(
mustNotCall('Should not accept null'),
mustCall((e) => {
assert.strictEqual(e.code, 'ERR_INVALID_RETURN_PROPERTY_VALUE');
})
);
import('test:Object').then(
mustNotCall('Should not stringify or valueOf Objects'),
mustCall((e) => {
assert.strictEqual(e.code, 'ERR_INVALID_RETURN_PROPERTY_VALUE');
})
);
import('test:SharedArrayBuffer').then(
mustCall(),
mustNotCall('Should accept SharedArrayBuffers'),
);
import('test:string').then(
mustCall(),
mustNotCall('Should accept strings'),
);
import('test:String').then(
mustNotCall('Should not accept wrapper Strings'),
mustCall((e) => {
assert.strictEqual(e.code, 'ERR_INVALID_RETURN_PROPERTY_VALUE');
})
);
import('test:Uint8Array').then(
mustCall(),
mustNotCall('Should accept Uint8Arrays'),
);
import('test:undefined').then(
mustNotCall('Should not accept undefined'),
mustCall((e) => {
assert.strictEqual(e.code, 'ERR_INVALID_RETURN_PROPERTY_VALUE');
})
);
30 changes: 30 additions & 0 deletions test/fixtures/es-module-loaders/string-sources.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const SOURCES = {
__proto__: null,
'test:Array': ['1', '2'], // both `1,2` and `12` are valid ESM
'test:ArrayBuffer': new ArrayBuffer(0),
'test:null': null,
'test:Object': {},
'test:SharedArrayBuffer': new SharedArrayBuffer(0),
'test:string': '',
'test:String': new String(''),
'test:Uint8Array': new Uint8Array(0),
'test:undefined': undefined,
}
export function resolve(specifier, context, defaultFn) {
if (specifier.startsWith('test:')) {
return { url: specifier };
}
return defaultFn(specifier, context);
}
export function getFormat(href, context, defaultFn) {
if (href.startsWith('test:')) {
return { format: 'module' };
}
return defaultFn(href, context);
}
export function getSource(href, context, defaultFn) {
if (href.startsWith('test:')) {
return { source: SOURCES[href] };
}
return defaultFn(href, context);
}
5 changes: 4 additions & 1 deletion test/fixtures/es-module-loaders/transform-source.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export async function transformSource(
source, { url, format }, defaultTransformSource) {
if (source && source.replace) {
if (format === 'module') {
if (typeof source !== 'string') {
source = new TextDecoder().decode(source);
}
return {
source: source.replace(`'A message';`, `'A message'.toUpperCase();`)
};
Expand Down

0 comments on commit cd4985c

Please sign in to comment.