esm: support loading data URLs
Co-Authored-By: Jan Olaf Krems <jan.krems@gmail.com> PR-URL: https://github.com/nodejs/node/pull/28614 Reviewed-By: Jan Krems <jan.krems@gmail.com>
This commit is contained in:
parent
317fa3a757
commit
9fd9efa492
@ -312,13 +312,38 @@ There are four types of specifiers:
|
|||||||
Bare specifiers, and the bare specifier portion of deep import specifiers, are
|
Bare specifiers, and the bare specifier portion of deep import specifiers, are
|
||||||
strings; but everything else in a specifier is a URL.
|
strings; but everything else in a specifier is a URL.
|
||||||
|
|
||||||
Only `file://` URLs are supported. A specifier like
|
Only `file:` and `data:` URLs are supported. A specifier like
|
||||||
`'https://example.com/app.js'` may be supported by browsers but it is not
|
`'https://example.com/app.js'` may be supported by browsers but it is not
|
||||||
supported in Node.js.
|
supported in Node.js.
|
||||||
|
|
||||||
Specifiers may not begin with `/` or `//`. These are reserved for potential
|
Specifiers may not begin with `/` or `//`. These are reserved for potential
|
||||||
future use. The root of the current volume may be referenced via `file:///`.
|
future use. The root of the current volume may be referenced via `file:///`.
|
||||||
|
|
||||||
|
#### `data:` Imports
|
||||||
|
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
[`data:` URLs][] are supported for importing with the following MIME types:
|
||||||
|
|
||||||
|
* `text/javascript` for ES Modules
|
||||||
|
* `application/json` for JSON
|
||||||
|
* `application/wasm` for WASM.
|
||||||
|
|
||||||
|
`data:` URLs only resolve [_Bare specifiers_][Terminology] for builtin modules
|
||||||
|
and [_Absolute specifiers_][Terminology]. Resolving
|
||||||
|
[_Relative specifiers_][Terminology] will not work because `data:` is not a
|
||||||
|
[special scheme][]. For example, attempting to load `./foo`
|
||||||
|
from `data:text/javascript,import "./foo";` will fail to resolve since there
|
||||||
|
is no concept of relative resolution for `data:` URLs. An example of a `data:`
|
||||||
|
URLs being used is:
|
||||||
|
|
||||||
|
```mjs
|
||||||
|
import 'data:text/javascript,console.log("hello!");'
|
||||||
|
import _ from 'data:application/json,"world!"'
|
||||||
|
```
|
||||||
|
|
||||||
## import.meta
|
## import.meta
|
||||||
|
|
||||||
* {Object}
|
* {Object}
|
||||||
@ -869,6 +894,8 @@ $ node --experimental-modules --es-module-specifier-resolution=node index
|
|||||||
success!
|
success!
|
||||||
```
|
```
|
||||||
|
|
||||||
|
[Terminology]: #esm_terminology
|
||||||
|
[`data:` URLs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
|
||||||
[`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
|
[`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
|
||||||
[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
|
[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
|
||||||
[`import()`]: #esm_import-expressions
|
[`import()`]: #esm_import-expressions
|
||||||
@ -877,6 +904,7 @@ success!
|
|||||||
[CommonJS]: modules.html
|
[CommonJS]: modules.html
|
||||||
[ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md
|
[ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md
|
||||||
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
|
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
|
||||||
|
[special scheme]: https://url.spec.whatwg.org/#special-scheme
|
||||||
[WHATWG JSON modules specification]: https://html.spec.whatwg.org/#creating-a-json-module-script
|
[WHATWG JSON modules specification]: https://html.spec.whatwg.org/#creating-a-json-module-script
|
||||||
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
|
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
|
||||||
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
|
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
|
||||||
|
@ -12,7 +12,7 @@ const typeFlag = getOptionValue('--input-type');
|
|||||||
const experimentalWasmModules = getOptionValue('--experimental-wasm-modules');
|
const experimentalWasmModules = getOptionValue('--experimental-wasm-modules');
|
||||||
const { resolve: moduleWrapResolve,
|
const { resolve: moduleWrapResolve,
|
||||||
getPackageType } = internalBinding('module_wrap');
|
getPackageType } = internalBinding('module_wrap');
|
||||||
const { pathToFileURL, fileURLToPath } = require('internal/url');
|
const { URL, pathToFileURL, fileURLToPath } = require('internal/url');
|
||||||
const { ERR_INPUT_TYPE_NOT_ALLOWED,
|
const { ERR_INPUT_TYPE_NOT_ALLOWED,
|
||||||
ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
|
ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
|
||||||
|
|
||||||
@ -45,12 +45,32 @@ if (experimentalWasmModules)
|
|||||||
extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm';
|
extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm';
|
||||||
|
|
||||||
function resolve(specifier, parentURL) {
|
function resolve(specifier, parentURL) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(specifier);
|
||||||
|
if (parsed.protocol === 'data:') {
|
||||||
|
const [ , mime ] = /^([^/]+\/[^;,]+)(;base64)?,/.exec(parsed.pathname) || [ null, null, null ];
|
||||||
|
const format = ({
|
||||||
|
'__proto__': null,
|
||||||
|
'text/javascript': 'module',
|
||||||
|
'application/json': 'json',
|
||||||
|
'application/wasm': experimentalWasmModules ? 'wasm' : null
|
||||||
|
})[mime] || null;
|
||||||
|
return {
|
||||||
|
url: specifier,
|
||||||
|
format
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
if (NativeModule.canBeRequiredByUsers(specifier)) {
|
if (NativeModule.canBeRequiredByUsers(specifier)) {
|
||||||
return {
|
return {
|
||||||
url: specifier,
|
url: specifier,
|
||||||
format: 'builtin'
|
format: 'builtin'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (parentURL && parentURL.startsWith('data:')) {
|
||||||
|
// This is gonna blow up, we want the error
|
||||||
|
new URL(specifier, parentURL);
|
||||||
|
}
|
||||||
|
|
||||||
const isMain = parentURL === undefined;
|
const isMain = parentURL === undefined;
|
||||||
if (isMain)
|
if (isMain)
|
||||||
|
@ -102,9 +102,12 @@ class Loader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format !== 'dynamic' && !url.startsWith('file:'))
|
if (format !== 'dynamic' &&
|
||||||
|
!url.startsWith('file:') &&
|
||||||
|
!url.startsWith('data:')
|
||||||
|
)
|
||||||
throw new ERR_INVALID_RETURN_PROPERTY(
|
throw new ERR_INVALID_RETURN_PROPERTY(
|
||||||
'file: url', 'loader resolve', 'url', url
|
'file: or data: url', 'loader resolve', 'url', url
|
||||||
);
|
);
|
||||||
|
|
||||||
return { url, format };
|
return { url, format };
|
||||||
|
@ -9,6 +9,8 @@ const {
|
|||||||
StringPrototype
|
StringPrototype
|
||||||
} = primordials;
|
} = primordials;
|
||||||
|
|
||||||
|
const { Buffer } = require('buffer');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
stripBOM,
|
stripBOM,
|
||||||
loadNativeModule
|
loadNativeModule
|
||||||
@ -23,6 +25,8 @@ const { debuglog } = require('internal/util/debuglog');
|
|||||||
const { promisify } = require('internal/util');
|
const { promisify } = require('internal/util');
|
||||||
const esmLoader = require('internal/process/esm_loader');
|
const esmLoader = require('internal/process/esm_loader');
|
||||||
const {
|
const {
|
||||||
|
ERR_INVALID_URL,
|
||||||
|
ERR_INVALID_URL_SCHEME,
|
||||||
ERR_UNKNOWN_BUILTIN_MODULE
|
ERR_UNKNOWN_BUILTIN_MODULE
|
||||||
} = require('internal/errors').codes;
|
} = require('internal/errors').codes;
|
||||||
const readFileAsync = promisify(fs.readFile);
|
const readFileAsync = promisify(fs.readFile);
|
||||||
@ -33,6 +37,31 @@ const debug = debuglog('esm');
|
|||||||
const translators = new SafeMap();
|
const translators = new SafeMap();
|
||||||
exports.translators = translators;
|
exports.translators = translators;
|
||||||
|
|
||||||
|
const DATA_URL_PATTERN = /^[^/]+\/[^,;]+(;base64)?,([\s\S]*)$/;
|
||||||
|
function getSource(url) {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
if (parsed.protocol === 'file:') {
|
||||||
|
return readFileAsync(parsed);
|
||||||
|
} else if (parsed.protocol === 'data:') {
|
||||||
|
const match = DATA_URL_PATTERN.exec(parsed.pathname);
|
||||||
|
if (!match) {
|
||||||
|
throw new ERR_INVALID_URL(url);
|
||||||
|
}
|
||||||
|
const [ , base64, body ] = match;
|
||||||
|
return Buffer.from(body, base64 ? 'base64' : 'utf8');
|
||||||
|
} else {
|
||||||
|
throw new ERR_INVALID_URL_SCHEME(['file', 'data']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function errPath(url) {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
if (parsed.protocol === 'file:') {
|
||||||
|
return fileURLToPath(parsed);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
function initializeImportMeta(meta, { url }) {
|
function initializeImportMeta(meta, { url }) {
|
||||||
meta.url = url;
|
meta.url = url;
|
||||||
}
|
}
|
||||||
@ -44,7 +73,7 @@ async function importModuleDynamically(specifier, { url }) {
|
|||||||
|
|
||||||
// Strategy for loading a standard JavaScript module
|
// Strategy for loading a standard JavaScript module
|
||||||
translators.set('module', async function moduleStrategy(url) {
|
translators.set('module', async function moduleStrategy(url) {
|
||||||
const source = `${await readFileAsync(new URL(url))}`;
|
const source = `${await getSource(url)}`;
|
||||||
debug(`Translating StandardModule ${url}`);
|
debug(`Translating StandardModule ${url}`);
|
||||||
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
|
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
|
||||||
const module = new ModuleWrap(source, url);
|
const module = new ModuleWrap(source, url);
|
||||||
@ -111,17 +140,22 @@ translators.set('builtin', async function builtinStrategy(url) {
|
|||||||
translators.set('json', async function jsonStrategy(url) {
|
translators.set('json', async function jsonStrategy(url) {
|
||||||
debug(`Translating JSONModule ${url}`);
|
debug(`Translating JSONModule ${url}`);
|
||||||
debug(`Loading JSONModule ${url}`);
|
debug(`Loading JSONModule ${url}`);
|
||||||
const pathname = fileURLToPath(url);
|
const pathname = url.startsWith('file:') ? fileURLToPath(url) : null;
|
||||||
const modulePath = isWindows ?
|
let modulePath;
|
||||||
|
let module;
|
||||||
|
if (pathname) {
|
||||||
|
modulePath = isWindows ?
|
||||||
StringPrototype.replace(pathname, winSepRegEx, '\\') : pathname;
|
StringPrototype.replace(pathname, winSepRegEx, '\\') : pathname;
|
||||||
let module = CJSModule._cache[modulePath];
|
module = CJSModule._cache[modulePath];
|
||||||
if (module && module.loaded) {
|
if (module && module.loaded) {
|
||||||
const exports = module.exports;
|
const exports = module.exports;
|
||||||
return createDynamicModule([], ['default'], url, (reflect) => {
|
return createDynamicModule([], ['default'], url, (reflect) => {
|
||||||
reflect.exports.default.set(exports);
|
reflect.exports.default.set(exports);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const content = await readFileAsync(pathname, 'utf-8');
|
}
|
||||||
|
const content = `${await getSource(url)}`;
|
||||||
|
if (pathname) {
|
||||||
// A require call could have been called on the same file during loading and
|
// 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
|
// that resolves synchronously. To make sure we always return the identical
|
||||||
// export, we have to check again if the module already exists or not.
|
// export, we have to check again if the module already exists or not.
|
||||||
@ -132,6 +166,7 @@ translators.set('json', async function jsonStrategy(url) {
|
|||||||
reflect.exports.default.set(exports);
|
reflect.exports.default.set(exports);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const exports = JsonParse(stripBOM(content));
|
const exports = JsonParse(stripBOM(content));
|
||||||
module = {
|
module = {
|
||||||
@ -143,10 +178,12 @@ translators.set('json', async function jsonStrategy(url) {
|
|||||||
// parse error instead of just manipulating the original error message.
|
// parse error instead of just manipulating the original error message.
|
||||||
// That would allow to add further properties and maybe additional
|
// That would allow to add further properties and maybe additional
|
||||||
// debugging information.
|
// debugging information.
|
||||||
err.message = pathname + ': ' + err.message;
|
err.message = errPath(url) + ': ' + err.message;
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
if (pathname) {
|
||||||
CJSModule._cache[modulePath] = module;
|
CJSModule._cache[modulePath] = module;
|
||||||
|
}
|
||||||
return createDynamicModule([], ['default'], url, (reflect) => {
|
return createDynamicModule([], ['default'], url, (reflect) => {
|
||||||
debug(`Parsing JSONModule ${url}`);
|
debug(`Parsing JSONModule ${url}`);
|
||||||
reflect.exports.default.set(module.exports);
|
reflect.exports.default.set(module.exports);
|
||||||
@ -155,14 +192,13 @@ translators.set('json', async function jsonStrategy(url) {
|
|||||||
|
|
||||||
// Strategy for loading a wasm module
|
// Strategy for loading a wasm module
|
||||||
translators.set('wasm', async function(url) {
|
translators.set('wasm', async function(url) {
|
||||||
const pathname = fileURLToPath(url);
|
const buffer = await getSource(url);
|
||||||
const buffer = await readFileAsync(pathname);
|
|
||||||
debug(`Translating WASMModule ${url}`);
|
debug(`Translating WASMModule ${url}`);
|
||||||
let compiled;
|
let compiled;
|
||||||
try {
|
try {
|
||||||
compiled = await WebAssembly.compile(buffer);
|
compiled = await WebAssembly.compile(buffer);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
err.message = pathname + ': ' + err.message;
|
err.message = errPath(url) + ': ' + err.message;
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
63
test/es-module/test-esm-data-urls.js
Normal file
63
test/es-module/test-esm-data-urls.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// Flags: --experimental-modules
|
||||||
|
'use strict';
|
||||||
|
const common = require('../common');
|
||||||
|
const assert = require('assert');
|
||||||
|
function createURL(mime, body) {
|
||||||
|
return `data:${mime},${body}`;
|
||||||
|
}
|
||||||
|
function createBase64URL(mime, body) {
|
||||||
|
return `data:${mime};base64,${Buffer.from(body).toString('base64')}`;
|
||||||
|
}
|
||||||
|
(async () => {
|
||||||
|
{
|
||||||
|
const body = 'export default {a:"aaa"};';
|
||||||
|
const plainESMURL = createURL('text/javascript', body);
|
||||||
|
const ns = await import(plainESMURL);
|
||||||
|
assert.deepStrictEqual(Object.keys(ns), ['default']);
|
||||||
|
assert.deepStrictEqual(ns.default.a, 'aaa');
|
||||||
|
const importerOfURL = createURL(
|
||||||
|
'text/javascript',
|
||||||
|
`export {default as default} from ${JSON.stringify(plainESMURL)}`
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
(await import(importerOfURL)).default,
|
||||||
|
ns.default
|
||||||
|
);
|
||||||
|
const base64ESMURL = createBase64URL('text/javascript', body);
|
||||||
|
assert.notStrictEqual(
|
||||||
|
await import(base64ESMURL),
|
||||||
|
ns
|
||||||
|
);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const body = 'export default import.meta.url;';
|
||||||
|
const plainESMURL = createURL('text/javascript', body);
|
||||||
|
const ns = await import(plainESMURL);
|
||||||
|
assert.deepStrictEqual(Object.keys(ns), ['default']);
|
||||||
|
assert.deepStrictEqual(ns.default, plainESMURL);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const body = '{"x": 1}';
|
||||||
|
const plainESMURL = createURL('application/json', body);
|
||||||
|
const ns = await import(plainESMURL);
|
||||||
|
assert.deepStrictEqual(Object.keys(ns), ['default']);
|
||||||
|
assert.deepStrictEqual(ns.default.x, 1);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const body = '{"default": 2}';
|
||||||
|
const plainESMURL = createURL('application/json', body);
|
||||||
|
const ns = await import(plainESMURL);
|
||||||
|
assert.deepStrictEqual(Object.keys(ns), ['default']);
|
||||||
|
assert.deepStrictEqual(ns.default.default, 2);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const body = 'null';
|
||||||
|
const plainESMURL = createURL('invalid', body);
|
||||||
|
try {
|
||||||
|
await import(plainESMURL);
|
||||||
|
common.mustNotCall()();
|
||||||
|
} catch (e) {
|
||||||
|
assert.strictEqual(e.code, 'ERR_INVALID_RETURN_PROPERTY_VALUE');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})().then(common.mustCall());
|
Loading…
x
Reference in New Issue
Block a user