Skip to content

Commit b373202

Browse files
efekrsklrichardlau
authored andcommitted
esm: add --experimental-import-text flag
Signed-off-by: Efe Karasakal <hi@efe.dev> PR-URL: #62300 Reviewed-By: Aviv Keller <me@aviv.sh> Reviewed-By: Ilyas Shabi <ilyasshabi94@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent c7e57f5 commit b373202

13 files changed

Lines changed: 178 additions & 4 deletions

doc/api/cli.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,6 +1255,18 @@ passing a second `parentURL` argument for contextual resolution.
12551255

12561256
Previously gated the entire `import.meta.resolve` feature.
12571257

1258+
### `--experimental-import-text`
1259+
1260+
<!-- YAML
1261+
added:
1262+
- REPLACEME
1263+
-->
1264+
1265+
> Stability: 1.0 - Early development
1266+
1267+
Enable experimental support for importing modules with
1268+
`with { type: 'text' }`.
1269+
12581270
### `--experimental-inspector-network-resource`
12591271

12601272
<!-- YAML
@@ -3781,6 +3793,7 @@ one is included in the list below.
37813793
* `--experimental-eventsource`
37823794
* `--experimental-ffi`
37833795
* `--experimental-import-meta-resolve`
3796+
* `--experimental-import-text`
37843797
* `--experimental-json-modules`
37853798
* `--experimental-loader`
37863799
* `--experimental-modules`

doc/api/esm.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,8 +294,10 @@ Node.js only supports the `type` attribute, for which it supports the following
294294
| Attribute `type` | Needed for |
295295
| ---------------- | ---------------- |
296296
| `'json'` | [JSON modules][] |
297+
| `'text'` | [Text modules][] |
297298

298299
The `type: 'json'` attribute is mandatory when importing JSON modules.
300+
The `type: 'text'` attribute is mandatory when importing text modules.
299301

300302
## Built-in modules
301303

@@ -707,6 +709,23 @@ exports. A cache entry is created in the CommonJS cache to avoid duplication.
707709
The same object is returned in CommonJS if the JSON module has already been
708710
imported from the same path.
709711
712+
## Text modules
713+
714+
> Stability: 1.0 - Early development
715+
716+
Text modules are available behind the `--experimental-import-text` flag.
717+
718+
Text files can be referenced by `import`:
719+
720+
```js
721+
import message from './message.txt' with { type: 'text' };
722+
```
723+
724+
The `with { type: 'text' }` syntax is mandatory; see [Import Attributes][].
725+
726+
The imported text only exposes a `default` export whose value is the module
727+
source as a string.
728+
710729
<i id="esm_experimental_wasm_modules"></i>
711730
712731
## Wasm modules
@@ -1313,6 +1332,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
13131332
[Package maps]: packages.md#package-maps
13141333
[Source Phase Imports]: https://github.com/tc39/proposal-source-phase-imports
13151334
[Terminology]: #terminology
1335+
[Text modules]: #text-modules
13161336
[URL]: https://url.spec.whatwg.org/
13171337
[WebAssembly JS String Builtins Proposal]: https://github.com/WebAssembly/js-string-builtins
13181338
[`"exports"`]: packages.md#exports

lib/internal/modules/esm/assert.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const {
88
ObjectValues,
99
} = primordials;
1010
const { validateString } = require('internal/validators');
11+
const { getOptionValue } = require('internal/options');
1112

1213
const {
1314
ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE,
@@ -29,8 +30,13 @@ const formatTypeMap = {
2930
'commonjs': kImplicitTypeAttribute,
3031
'json': 'json',
3132
'module': kImplicitTypeAttribute,
33+
'text': 'text',
3234
'wasm': kImplicitTypeAttribute, // It's unclear whether the HTML spec will require an type attribute or not for Wasm; see https://github.com/WebAssembly/esm-integration/issues/42
3335
};
36+
// NOTE: Don't add bytes support yet as it requires Uint8Arrays backed by immutable ArrayBuffers,
37+
// which V8 does not support yet.
38+
// see: https://github.com/nodejs/node/pull/62300#issuecomment-4079163816
39+
3440

3541
/**
3642
* The HTML spec disallows the default type to be explicitly specified
@@ -42,7 +48,6 @@ const supportedTypeAttributes = ArrayPrototypeFilter(
4248
ObjectValues(formatTypeMap),
4349
(type) => type !== kImplicitTypeAttribute);
4450

45-
4651
/**
4752
* Test a module's import attributes.
4853
* @param {string} url The URL of the imported module, for error reporting.
@@ -62,6 +67,12 @@ function validateAttributes(url, format,
6267
}
6368
const validType = formatTypeMap[format];
6469

70+
if (validType !== undefined &&
71+
importAttributes.type === 'text' &&
72+
!getOptionValue('--experimental-import-text')) {
73+
throw new ERR_IMPORT_ATTRIBUTE_UNSUPPORTED('type', importAttributes.type, url);
74+
}
75+
6576
switch (validType) {
6677
case undefined:
6778
// Ignore attributes for module formats we don't recognize, to allow new

lib/internal/modules/esm/get_format.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ function mimeToFormat(mime) {
4747
) { return 'module'; }
4848
if (mime === 'application/json') { return 'json'; }
4949
if (mime === 'application/wasm') { return 'wasm'; }
50+
if (
51+
getOptionValue('--experimental-import-text') &&
52+
RegExpPrototypeExec(
53+
/^\s*text\/plain\s*(;\s*charset=utf-?8\s*)?$/i,
54+
mime,
55+
) !== null
56+
) { return 'text'; }
5057
return null;
5158
}
5259

@@ -236,12 +243,22 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
236243
throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath);
237244
}
238245

246+
// If the caller explicitly requests text format via import attributes, honor it regardless of file extension.
247+
function isExperimentalTextImport(importAttributes) {
248+
return getOptionValue('--experimental-import-text') &&
249+
importAttributes?.type === 'text';
250+
}
251+
239252
/**
240253
* @param {URL} url
241-
* @param {{parentURL: string}} context
254+
* @param {{parentURL: string, importAttributes?: Record<string, string>}} context
242255
* @returns {Promise<string> | string | undefined} only works when enabled
243256
*/
244257
function defaultGetFormatWithoutErrors(url, context) {
258+
if (isExperimentalTextImport(context?.importAttributes)) {
259+
return 'text';
260+
}
261+
245262
const protocol = url.protocol;
246263
if (!ObjectPrototypeHasOwnProperty(protocolHandlers, protocol)) {
247264
return null;
@@ -251,10 +268,14 @@ function defaultGetFormatWithoutErrors(url, context) {
251268

252269
/**
253270
* @param {URL} url
254-
* @param {{parentURL: string}} context
271+
* @param {{parentURL: string, importAttributes?: Record<string, string>}} context
255272
* @returns {Promise<string> | string | undefined} only works when enabled
256273
*/
257274
function defaultGetFormat(url, context) {
275+
if (isExperimentalTextImport(context?.importAttributes)) {
276+
return 'text';
277+
}
278+
258279
const protocol = url.protocol;
259280
if (!ObjectPrototypeHasOwnProperty(protocolHandlers, protocol)) {
260281
return null;

lib/internal/modules/esm/loader.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ const { defaultLoadSync, throwUnknownModuleFormat } = require('internal/modules/
9999
*/
100100

101101
/**
102-
* @typedef {'builtin'|'commonjs'|'json'|'module'|'wasm'} ModuleFormat
102+
* @typedef {'builtin'|'commonjs'|'json'|'module'|'text'|'wasm'} ModuleFormat
103103
*/
104104

105105
/**

lib/internal/modules/esm/translators.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,3 +681,14 @@ translators.set('module-typescript', function(url, translateContext, parentURL)
681681
translateContext.source = stripTypeScriptModuleTypes(stringify(source), url);
682682
return FunctionPrototypeCall(translators.get('module'), this, url, translateContext, parentURL);
683683
});
684+
685+
// Strategy for loading source as text.
686+
translators.set('text', function textStrategy(url, translateContext) {
687+
emitExperimentalWarning('Text import');
688+
let { source } = translateContext;
689+
assertBufferSource(source, true, 'load');
690+
source = stringify(source);
691+
return new ModuleWrap(url, undefined, ['default'], function() {
692+
this.setExport('default', source);
693+
});
694+
});

src/node_options.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
647647
AddAlias("--loader", "--experimental-loader");
648648
AddOption("--experimental-modules", "", NoOp{}, kAllowedInEnvvar);
649649
AddOption("--experimental-wasm-modules", "", NoOp{}, kAllowedInEnvvar);
650+
AddOption("--experimental-import-text",
651+
"experimental support for importing source as text with import "
652+
"attributes",
653+
&EnvironmentOptions::experimental_import_text,
654+
kAllowedInEnvvar);
650655
AddOption("--experimental-import-meta-resolve",
651656
"experimental ES Module import.meta.resolve() parentURL support",
652657
&EnvironmentOptions::experimental_import_meta_resolve,

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ class EnvironmentOptions : public Options {
138138
std::string localstorage_file;
139139
bool experimental_global_navigator = true;
140140
bool experimental_global_web_crypto = true;
141+
bool experimental_import_text = EXPERIMENTALS_DEFAULT_VALUE;
141142
bool experimental_import_meta_resolve = EXPERIMENTALS_DEFAULT_VALUE;
142143
std::string input_type; // Value of --input-type
143144
bool entry_is_url = false;

test/es-module/test-esm-import-attributes-errors.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ async function test() {
2626
{ code: 'ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE' }
2727
);
2828

29+
await assert.rejects(
30+
import(jsModuleDataUrl, { with: { type: 'text' } }),
31+
{ code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' }
32+
);
33+
2934
await assert.rejects(
3035
import(jsModuleDataUrl, { with: { type: 'json', other: 'unsupported' } }),
3136
{ code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' }

test/es-module/test-esm-import-attributes-errors.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ await assert.rejects(
2121
{ code: 'ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE' }
2222
);
2323

24+
await assert.rejects(
25+
import(jsModuleDataUrl, { with: { type: 'text' } }),
26+
{ code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' }
27+
);
28+
2429
await assert.rejects(
2530
import(jsModuleDataUrl, { with: { type: 'json', other: 'unsupported' } }),
2631
{ code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' }

0 commit comments

Comments
 (0)