Skip to content

Commit bac5f17

Browse files
efekrskladuh95
authored andcommitted
esm: add --experimental-import-text flag
Signed-off-by: Efe Karasakal <[email protected]> PR-URL: #62300 Reviewed-By: Aviv Keller <[email protected]> Reviewed-By: Ilyas Shabi <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent 1078da9 commit bac5f17

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
@@ -1079,6 +1079,18 @@ passing a second `parentURL` argument for contextual resolution.
10791079

10801080
Previously gated the entire `import.meta.resolve` feature.
10811081

1082+
### `--experimental-import-text`
1083+
1084+
<!-- YAML
1085+
added:
1086+
- REPLACEME
1087+
-->
1088+
1089+
> Stability: 1.0 - Early development
1090+
1091+
Enable experimental support for importing modules with
1092+
`with { type: 'text' }`.
1093+
10821094
### `--experimental-inspector-network-resource`
10831095

10841096
<!-- YAML
@@ -3529,6 +3541,7 @@ one is included in the list below.
35293541
* `--experimental-detect-module`
35303542
* `--experimental-eventsource`
35313543
* `--experimental-import-meta-resolve`
3544+
* `--experimental-import-text`
35323545
* `--experimental-json-modules`
35333546
* `--experimental-loader`
35343547
* `--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

@@ -702,6 +704,23 @@ exports. A cache entry is created in the CommonJS cache to avoid duplication.
702704
The same object is returned in CommonJS if the JSON module has already been
703705
imported from the same path.
704706
707+
## Text modules
708+
709+
> Stability: 1.0 - Early development
710+
711+
Text modules are available behind the `--experimental-import-text` flag.
712+
713+
Text files can be referenced by `import`:
714+
715+
```js
716+
import message from './message.txt' with { type: 'text' };
717+
```
718+
719+
The `with { type: 'text' }` syntax is mandatory; see [Import Attributes][].
720+
721+
The imported text only exposes a `default` export whose value is the module
722+
source as a string.
723+
705724
<i id="esm_experimental_wasm_modules"></i>
706725
707726
## Wasm modules
@@ -1295,6 +1314,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
12951314
[Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification
12961315
[Source Phase Imports]: https://github.com/tc39/proposal-source-phase-imports
12971316
[Terminology]: #terminology
1317+
[Text modules]: #text-modules
12981318
[URL]: https://url.spec.whatwg.org/
12991319
[WebAssembly JS String Builtins Proposal]: https://github.com/WebAssembly/js-string-builtins
13001320
[`"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
@@ -95,7 +95,7 @@ const { defaultLoadSync, throwUnknownModuleFormat } = require('internal/modules/
9595
*/
9696

9797
/**
98-
* @typedef {'builtin'|'commonjs'|'json'|'module'|'wasm'} ModuleFormat
98+
* @typedef {'builtin'|'commonjs'|'json'|'module'|'text'|'wasm'} ModuleFormat
9999
*/
100100

101101
/**

lib/internal/modules/esm/translators.js

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

src/node_options.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
614614
AddAlias("--loader", "--experimental-loader");
615615
AddOption("--experimental-modules", "", NoOp{}, kAllowedInEnvvar);
616616
AddOption("--experimental-wasm-modules", "", NoOp{}, kAllowedInEnvvar);
617+
AddOption("--experimental-import-text",
618+
"experimental support for importing source as text with import "
619+
"attributes",
620+
&EnvironmentOptions::experimental_import_text,
621+
kAllowedInEnvvar);
617622
AddOption("--experimental-import-meta-resolve",
618623
"experimental ES Module import.meta.resolve() parentURL support",
619624
&EnvironmentOptions::experimental_import_meta_resolve,

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ class EnvironmentOptions : public Options {
137137
std::string localstorage_file;
138138
bool experimental_global_navigator = true;
139139
bool experimental_global_web_crypto = true;
140+
bool experimental_import_text = EXPERIMENTALS_DEFAULT_VALUE;
140141
bool experimental_import_meta_resolve = EXPERIMENTALS_DEFAULT_VALUE;
141142
std::string input_type; // Value of --input-type
142143
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)