summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/api/esm.md30
-rw-r--r--lib/internal/modules/esm/default_resolve.js22
-rw-r--r--lib/internal/modules/esm/loader.js7
-rw-r--r--lib/internal/modules/esm/translators.js86
-rw-r--r--test/es-module/test-esm-data-urls.js63
5 files changed, 179 insertions, 29 deletions
diff --git a/doc/api/esm.md b/doc/api/esm.md
index 17d98ab208..277f5cf830 100644
--- a/doc/api/esm.md
+++ b/doc/api/esm.md
@@ -312,13 +312,38 @@ There are four types of specifiers:
Bare specifiers, and the bare specifier portion of deep import specifiers, are
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
supported in Node.js.
Specifiers may not begin with `/` or `//`. These are reserved for potential
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
* {Object}
@@ -869,6 +894,8 @@ $ node --experimental-modules --es-module-specifier-resolution=node index
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
[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
[`import()`]: #esm_import-expressions
@@ -877,6 +904,7 @@ success!
[CommonJS]: modules.html
[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
+[special scheme]: https://url.spec.whatwg.org/#special-scheme
[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
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
diff --git a/lib/internal/modules/esm/default_resolve.js b/lib/internal/modules/esm/default_resolve.js
index 46e7b2415a..580419deac 100644
--- a/lib/internal/modules/esm/default_resolve.js
+++ b/lib/internal/modules/esm/default_resolve.js
@@ -12,7 +12,7 @@ const typeFlag = getOptionValue('--input-type');
const experimentalWasmModules = getOptionValue('--experimental-wasm-modules');
const { resolve: moduleWrapResolve,
getPackageType } = internalBinding('module_wrap');
-const { pathToFileURL, fileURLToPath } = require('internal/url');
+const { URL, pathToFileURL, fileURLToPath } = require('internal/url');
const { ERR_INPUT_TYPE_NOT_ALLOWED,
ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
@@ -45,12 +45,32 @@ if (experimentalWasmModules)
extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm';
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)) {
return {
url: specifier,
format: 'builtin'
};
}
+ if (parentURL && parentURL.startsWith('data:')) {
+ // This is gonna blow up, we want the error
+ new URL(specifier, parentURL);
+ }
const isMain = parentURL === undefined;
if (isMain)
diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js
index bffefa884e..09109d3c71 100644
--- a/lib/internal/modules/esm/loader.js
+++ b/lib/internal/modules/esm/loader.js
@@ -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(
- 'file: url', 'loader resolve', 'url', url
+ 'file: or data: url', 'loader resolve', 'url', url
);
return { url, format };
diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js
index d693b79448..9cb0781ab9 100644
--- a/lib/internal/modules/esm/translators.js
+++ b/lib/internal/modules/esm/translators.js
@@ -9,6 +9,8 @@ const {
StringPrototype
} = primordials;
+const { Buffer } = require('buffer');
+
const {
stripShebang,
stripBOM,
@@ -24,6 +26,8 @@ const { debuglog } = require('internal/util/debuglog');
const { promisify } = require('internal/util');
const esmLoader = require('internal/process/esm_loader');
const {
+ ERR_INVALID_URL,
+ ERR_INVALID_URL_SCHEME,
ERR_UNKNOWN_BUILTIN_MODULE
} = require('internal/errors').codes;
const readFileAsync = promisify(fs.readFile);
@@ -34,6 +38,31 @@ const debug = debuglog('esm');
const translators = new SafeMap();
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 }) {
meta.url = url;
}
@@ -45,7 +74,7 @@ async function importModuleDynamically(specifier, { url }) {
// Strategy for loading a standard JavaScript module
translators.set('module', async function moduleStrategy(url) {
- const source = `${await readFileAsync(new URL(url))}`;
+ const source = `${await getSource(url)}`;
debug(`Translating StandardModule ${url}`);
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
const module = new ModuleWrap(stripShebang(source), url);
@@ -112,26 +141,32 @@ translators.set('builtin', async function builtinStrategy(url) {
translators.set('json', async function jsonStrategy(url) {
debug(`Translating JSONModule ${url}`);
debug(`Loading JSONModule ${url}`);
- const pathname = fileURLToPath(url);
- const modulePath = isWindows ?
- StringPrototype.replace(pathname, winSepRegEx, '\\') : pathname;
- let module = CJSModule._cache[modulePath];
- if (module && module.loaded) {
- const exports = module.exports;
- return createDynamicModule([], ['default'], url, (reflect) => {
- reflect.exports.default.set(exports);
- });
+ const pathname = url.startsWith('file:') ? fileURLToPath(url) : null;
+ let modulePath;
+ let module;
+ if (pathname) {
+ modulePath = isWindows ?
+ StringPrototype.replace(pathname, winSepRegEx, '\\') : pathname;
+ module = CJSModule._cache[modulePath];
+ if (module && module.loaded) {
+ const exports = module.exports;
+ return createDynamicModule([], ['default'], url, (reflect) => {
+ reflect.exports.default.set(exports);
+ });
+ }
}
- const content = await readFileAsync(pathname, 'utf-8');
- // 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
- // export, we have to check again if the module already exists or not.
- module = CJSModule._cache[modulePath];
- if (module && module.loaded) {
- const exports = module.exports;
- return createDynamicModule(['default'], url, (reflect) => {
- reflect.exports.default.set(exports);
- });
+ const content = `${await getSource(url)}`;
+ 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
+ // export, we have to check again if the module already exists or not.
+ module = CJSModule._cache[modulePath];
+ if (module && module.loaded) {
+ const exports = module.exports;
+ return createDynamicModule(['default'], url, (reflect) => {
+ reflect.exports.default.set(exports);
+ });
+ }
}
try {
const exports = JsonParse(stripBOM(content));
@@ -144,10 +179,12 @@ translators.set('json', async function jsonStrategy(url) {
// parse error instead of just manipulating the original error message.
// That would allow to add further properties and maybe additional
// debugging information.
- err.message = pathname + ': ' + err.message;
+ err.message = errPath(url) + ': ' + err.message;
throw err;
}
- CJSModule._cache[modulePath] = module;
+ if (pathname) {
+ CJSModule._cache[modulePath] = module;
+ }
return createDynamicModule([], ['default'], url, (reflect) => {
debug(`Parsing JSONModule ${url}`);
reflect.exports.default.set(module.exports);
@@ -156,14 +193,13 @@ translators.set('json', async function jsonStrategy(url) {
// Strategy for loading a wasm module
translators.set('wasm', async function(url) {
- const pathname = fileURLToPath(url);
- const buffer = await readFileAsync(pathname);
+ const buffer = await getSource(url);
debug(`Translating WASMModule ${url}`);
let compiled;
try {
compiled = await WebAssembly.compile(buffer);
} catch (err) {
- err.message = pathname + ': ' + err.message;
+ err.message = errPath(url) + ': ' + err.message;
throw err;
}
diff --git a/test/es-module/test-esm-data-urls.js b/test/es-module/test-esm-data-urls.js
new file mode 100644
index 0000000000..bc781b0363
--- /dev/null
+++ b/test/es-module/test-esm-data-urls.js
@@ -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());