Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ typically configured server.
ES modules are resolved and cached as URLs. This means that special characters
must be [percent-encoded][], such as `#` with `%23` and `?` with `%3F`.

`file:`, `node:`, and `data:` URL schemes are supported. A specifier like
`file:`, `node:`, `data:`, and `blob:` URL schemes are supported. A specifier like
`'https://example.com/app.js'` is not supported natively in Node.js unless using
a [custom HTTPS loader][].

Expand Down Expand Up @@ -231,6 +231,36 @@ and [absolute specifiers][Terminology]. Resolving
from `data:text/javascript,import "./foo";` fails to resolve because there
is no concept of relative resolution for `data:` URLs.

#### `blob:` imports

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

`blob:` URLs are supported for importing with the following MIME types:

* `text/javascript` for ES modules
* `application/json` for JSON
* `application/wasm` for Wasm

Blob URLs are registered within the current thread. Loaders will not be
able to resolve Blob URLs created from the main thread.

```mjs
import { Blob } from 'node:buffer';

const mjs = new Blob(['export "Hello, World!"'], { type: 'text/javascript' });
await import(URL.createObjectURL(mjs));

const json = new Blob(['{}'], { type: 'application/json' });
await import(URL.createObjectURL(json), { with: { type: 'json' } });

const wasm = new Blob(['WASM Data'], { type: 'application/wasm' });
await import(URL.createObjectURL(wasm));
```

#### `node:` imports

<!-- YAML
Expand Down Expand Up @@ -721,7 +751,7 @@ The default resolver has the following properties:
The default loader has the following properties

* Support for builtin module loading via `node:` URLs
* Support for "inline" module loading via `data:` URLs
* Support for "inline" module loading via `data:` and `blob:` URLs
* Support for `file:` module loading
* Fails on any other URL protocol
* Fails on unknown extensions for `file:` loading
Expand All @@ -739,7 +769,7 @@ does not determine whether the resolved URL protocol can be loaded,
or whether the file extensions are permitted, instead these validations
are applied by Node.js during the load phase
(for example, if it was asked to load a URL that has a protocol that is
not `file:`, `data:` or `node:`.
not `file:`, `data:`, `blob:`, or `node:`).

The algorithm also tries to determine the format of the file based
on the extension (see `ESM_FILE_FORMAT` algorithm below). If it does
Expand Down
10 changes: 9 additions & 1 deletion lib/internal/modules/esm/get_format.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@ const {

const detectModule = getOptionValue('--experimental-detect-module');
const { containsModuleSyntax } = internalBinding('contextify');
const { resolveObjectURL } = require('buffer');
const { getPackageScopeConfig, getPackageType } = require('internal/modules/package_json_reader');
const { fileURLToPath } = require('internal/url');
const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
const { ERR_UNKNOWN_FILE_EXTENSION, ERR_INVALID_URL } = require('internal/errors').codes;

const protocolHandlers = {
'__proto__': null,
'data:': getDataProtocolModuleFormat,
'file:': getFileProtocolModuleFormat,
'blob:': getBlobProtocolModuleFormat,
'node:'() { return 'builtin'; },
};

Expand Down Expand Up @@ -218,6 +220,12 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath);
}

function getBlobProtocolModuleFormat(url, context = { __proto__: null }, ignoreErrors) {
const blob = resolveObjectURL(url);
if (blob === undefined) { throw new ERR_INVALID_URL(blob); }
return mimeToFormat(blob.type);
}

/**
* @param {URL} url
* @param {{parentURL: string}} context
Expand Down
9 changes: 7 additions & 2 deletions lib/internal/modules/esm/load.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const { readFileSync } = require('fs');
const defaultType =
getOptionValue('--experimental-default-type');

const { Buffer: { from: BufferFrom } } = require('buffer');
const { Buffer: { from: BufferFrom }, resolveObjectURL } = require('buffer');
const {
isUnderNodeModules,
} = require('internal/modules/helpers');
Expand Down Expand Up @@ -48,8 +48,12 @@ async function getSource(url, context) {
}
const { 1: base64, 2: body } = match;
source = BufferFrom(decodeURIComponent(body), base64 ? 'base64' : 'utf8');
} else if (protocol === 'blob:') {
const blob = resolveObjectURL(url);
if (blob === undefined) { throw new ERR_INVALID_URL(blob); }
source = await blob.text();
} else {
const supportedSchemes = ['file', 'data'];
const supportedSchemes = ['file', 'data', 'blob'];
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(url, supportedSchemes);
}
return { __proto__: null, responseURL, source };
Expand Down Expand Up @@ -219,6 +223,7 @@ function throwIfUnsupportedURLScheme(parsed) {
protocol !== 'file:' &&
protocol !== 'data:' &&
protocol !== 'node:' &&
protocol !== 'blob:' &&
(
protocol !== 'https:' &&
protocol !== 'http:'
Expand Down
68 changes: 68 additions & 0 deletions test/es-module/test-esm-blob-urls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use strict';
const common = require('../common');
const fixtures = require('../common/fixtures');
const { test } = require('node:test');

if (!common.hasCrypto) common.skip('missing crypto');

async function createURL(mime, body) {
const blob = new Blob([body], { type: mime });
return URL.createObjectURL(blob);
}

function createBase64URL(mime, body) {
return `data:${mime};base64,${Buffer.from(body).toString('base64')}`;
}

test('importing JavaScript from a Blob: URL should work', async (t) => {
const url = await createURL('text/javascript', 'export default { "key": "value" }');
const { default: expected } = await import(url);
t.assert.deepStrictEqual(expected, { key: 'value' });
});

test('import.meta.url should be equal to the blob URL', async (t) => {
const url = await createURL('text/javascript', 'export default import.meta.url;');
const { default: expected } = await import(url);
t.assert.deepStrictEqual(expected, url);
});


test('importing JSON from a Blob: URL should work', async (t) => {
const url = await createURL('application/json', '{ "key": "value" }');
const { default: expected } = await import(url, { with: { type: 'json' } });
t.assert.deepStrictEqual(expected, { key: 'value' });
});

test('importing a module from within a blob: URL should work', async (t) => {
await t.test('file: URLs', async (t) => {
const url = await createURL('text/javascript', `import { message } from ${JSON.stringify(fixtures.fileURL('es-modules/message.mjs'))}; export default message;`);
const { default: expected } = await import(url);
t.assert.strictEqual(expected, 'A message');
});

await t.test('data: URLs', async (t) => {
const dataURL = createBase64URL('text/javascript', 'export const message = "Base64 Imported";');
const url = await createURL('text/javascript', `import { message } from "${dataURL}"; export default message;`);
const { default: expected } = await import(url);
t.assert.strictEqual(expected, 'Base64 Imported');
});

await t.test('blob: URLs', async (t) => {
const url = await createURL('text/javascript',
`const blob = new Blob(["export default 'Blob Imported'"], { type: 'application/javascript' });` +
'const url = URL.createObjectURL(blob);' +
'export default await import(url);'
);
const { default: { default: expected } } = await import(url);
t.assert.strictEqual(expected, 'Blob Imported');
});
});

test('importing a disallowed mimetype should fail', async (t) => {
const url = await createURL('invalid/mime', '');
await t.assert.rejects(import(url), /ERR_UNKNOWN_MODULE_FORMAT/);
});

test('importing an invalid blob should fail', async (t) => {
await t.assert.rejects(import('blob:nodedata:123456'), /ERR_INVALID_URL/);
});