GeezerVison is showing github.com — esm: add `--experimental-import-text` flag · nodejs/node@b373202
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/cli.md +13Lines changed: 13 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1255,6 +1255,18 @@ passing a second `parentURL` argument for contextual resolution. | 1255 | 1255 |
| 1256 | 1256 | Previously gated the entire `import.meta.resolve` feature. | 1257 | 1257 |
| | 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 | + | 1258 | 1270 | ### `--experimental-inspector-network-resource` | 1259 | 1271 |
| 1260 | 1272 | <!-- YAML | @@ -3781,6 +3793,7 @@ one is included in the list below. | 3781 | 3793 | * `--experimental-eventsource` | 3782 | 3794 | * `--experimental-ffi` | 3783 | 3795 | * `--experimental-import-meta-resolve` | | 3796 | +* `--experimental-import-text` | 3784 | 3797 | * `--experimental-json-modules` | 3785 | 3798 | * `--experimental-loader` | 3786 | 3799 | * `--experimental-modules` | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/esm.md +20Lines changed: 20 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -294,8 +294,10 @@ Node.js only supports the `type` attribute, for which it supports the following | 294 | 294 | | Attribute `type` | Needed for | | 295 | 295 | | ---------------- | ---------------- | | 296 | 296 | | `'json'` | [JSON modules][] | | | 297 | +| `'text'` | [Text modules][] | | 297 | 298 |
| 298 | 299 | The `type: 'json'` attribute is mandatory when importing JSON modules. | | 300 | +The `type: 'text'` attribute is mandatory when importing text modules. | 299 | 301 |
| 300 | 302 | ## Built-in modules | 301 | 303 |
| @@ -707,6 +709,23 @@ exports. A cache entry is created in the CommonJS cache to avoid duplication. | 707 | 709 | The same object is returned in CommonJS if the JSON module has already been | 708 | 710 | imported from the same path. | 709 | 711 |
| | 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 | +
| 710 | 729 | <i id="esm_experimental_wasm_modules"></i> | 711 | 730 |
| 712 | 731 | ## Wasm modules | @@ -1313,6 +1332,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][]. | 1313 | 1332 | [Package maps]: packages.md#package-maps | 1314 | 1333 | [Source Phase Imports]: https://github.com/tc39/proposal-source-phase-imports | 1315 | 1334 | [Terminology]: #terminology | | 1335 | +[Text modules]: #text-modules | 1316 | 1336 | [URL]: https://url.spec.whatwg.org/ | 1317 | 1337 | [WebAssembly JS String Builtins Proposal]: https://github.com/WebAssembly/js-string-builtins | 1318 | 1338 | [`"exports"`]: packages.md#exports | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/assert.js +12-1Lines changed: 12 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 8 | 8 | ObjectValues, | 9 | 9 | } = primordials; | 10 | 10 | const { validateString } = require('internal/validators'); | | 11 | +const { getOptionValue } = require('internal/options'); | 11 | 12 |
| 12 | 13 | const { | 13 | 14 | ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE, | @@ -29,8 +30,13 @@ const formatTypeMap = { | 29 | 30 | 'commonjs': kImplicitTypeAttribute, | 30 | 31 | 'json': 'json', | 31 | 32 | 'module': kImplicitTypeAttribute, | | 33 | + 'text': 'text', | 32 | 34 | '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 | 33 | 35 | }; | | 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 | + | 34 | 40 |
| 35 | 41 | /** | 36 | 42 | * The HTML spec disallows the default type to be explicitly specified | @@ -42,7 +48,6 @@ const supportedTypeAttributes = ArrayPrototypeFilter( | 42 | 48 | ObjectValues(formatTypeMap), | 43 | 49 | (type) => type !== kImplicitTypeAttribute); | 44 | 50 |
| 45 | | - | 46 | 51 | /** | 47 | 52 | * Test a module's import attributes. | 48 | 53 | * @param {string} url The URL of the imported module, for error reporting. | @@ -62,6 +67,12 @@ function validateAttributes(url, format, | 62 | 67 | } | 63 | 68 | const validType = formatTypeMap[format]; | 64 | 69 |
| | 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 | + | 65 | 76 | switch (validType) { | 66 | 77 | case undefined: | 67 | 78 | // Ignore attributes for module formats we don't recognize, to allow new | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/get_format.js +23-2Lines changed: 23 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -47,6 +47,13 @@ function mimeToFormat(mime) { | 47 | 47 | ) { return 'module'; } | 48 | 48 | if (mime === 'application/json') { return 'json'; } | 49 | 49 | 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'; } | 50 | 57 | return null; | 51 | 58 | } | 52 | 59 |
| @@ -236,12 +243,22 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE | 236 | 243 | throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath); | 237 | 244 | } | 238 | 245 |
| | 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 | + | 239 | 252 | /** | 240 | 253 | * @param {URL} url | 241 | | - * @param {{parentURL: string}} context | | 254 | + * @param {{parentURL: string, importAttributes?: Record<string, string>}} context | 242 | 255 | * @returns {Promise<string> | string | undefined} only works when enabled | 243 | 256 | */ | 244 | 257 | function defaultGetFormatWithoutErrors(url, context) { | | 258 | + if (isExperimentalTextImport(context?.importAttributes)) { | | 259 | + return 'text'; | | 260 | + } | | 261 | + | 245 | 262 | const protocol = url.protocol; | 246 | 263 | if (!ObjectPrototypeHasOwnProperty(protocolHandlers, protocol)) { | 247 | 264 | return null; | @@ -251,10 +268,14 @@ function defaultGetFormatWithoutErrors(url, context) { | 251 | 268 |
| 252 | 269 | /** | 253 | 270 | * @param {URL} url | 254 | | - * @param {{parentURL: string}} context | | 271 | + * @param {{parentURL: string, importAttributes?: Record<string, string>}} context | 255 | 272 | * @returns {Promise<string> | string | undefined} only works when enabled | 256 | 273 | */ | 257 | 274 | function defaultGetFormat(url, context) { | | 275 | + if (isExperimentalTextImport(context?.importAttributes)) { | | 276 | + return 'text'; | | 277 | + } | | 278 | + | 258 | 279 | const protocol = url.protocol; | 259 | 280 | if (!ObjectPrototypeHasOwnProperty(protocolHandlers, protocol)) { | 260 | 281 | return null; | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/loader.js +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -99,7 +99,7 @@ const { defaultLoadSync, throwUnknownModuleFormat } = require('internal/modules/ | 99 | 99 | */ | 100 | 100 |
| 101 | 101 | /** | 102 | | - * @typedef {'builtin'|'commonjs'|'json'|'module'|'wasm'} ModuleFormat | | 102 | + * @typedef {'builtin'|'commonjs'|'json'|'module'|'text'|'wasm'} ModuleFormat | 103 | 103 | */ | 104 | 104 |
| 105 | 105 | /** | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/translators.js +11Lines changed: 11 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -681,3 +681,14 @@ translators.set('module-typescript', function(url, translateContext, parentURL) | 681 | 681 | translateContext.source = stripTypeScriptModuleTypes(stringify(source), url); | 682 | 682 | return FunctionPrototypeCall(translators.get('module'), this, url, translateContext, parentURL); | 683 | 683 | }); | | 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 | +}); |
Collapse file Copy file name to clipboardExpand all lines: src/node_options.cc +5Lines changed: 5 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -647,6 +647,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { | 647 | 647 | AddAlias("--loader", "--experimental-loader"); | 648 | 648 | AddOption("--experimental-modules", "", NoOp{}, kAllowedInEnvvar); | 649 | 649 | 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); | 650 | 655 | AddOption("--experimental-import-meta-resolve", | 651 | 656 | "experimental ES Module import.meta.resolve() parentURL support", | 652 | 657 | &EnvironmentOptions::experimental_import_meta_resolve, | |
Collapse file Copy file name to clipboardExpand all lines: src/node_options.h +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -138,6 +138,7 @@ class EnvironmentOptions : public Options { | 138 | 138 | std::string localstorage_file; | 139 | 139 | bool experimental_global_navigator = true; | 140 | 140 | bool experimental_global_web_crypto = true; | | 141 | + bool experimental_import_text = EXPERIMENTALS_DEFAULT_VALUE; | 141 | 142 | bool experimental_import_meta_resolve = EXPERIMENTALS_DEFAULT_VALUE; | 142 | 143 | std::string input_type; // Value of --input-type | 143 | 144 | bool entry_is_url = false; | |
Collapse file Copy file name to clipboardExpand all lines: test/es-module/test-esm-import-attributes-errors.js +5Lines changed: 5 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -26,6 +26,11 @@ async function test() { | 26 | 26 | { code: 'ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE' } | 27 | 27 | ); | 28 | 28 |
| | 29 | + await assert.rejects( | | 30 | + import(jsModuleDataUrl, { with: { type: 'text' } }), | | 31 | + { code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' } | | 32 | + ); | | 33 | + | 29 | 34 | await assert.rejects( | 30 | 35 | import(jsModuleDataUrl, { with: { type: 'json', other: 'unsupported' } }), | 31 | 36 | { code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' } | |
Collapse file Copy file name to clipboardExpand all lines: test/es-module/test-esm-import-attributes-errors.mjs +5Lines changed: 5 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -21,6 +21,11 @@ await assert.rejects( | 21 | 21 | { code: 'ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE' } | 22 | 22 | ); | 23 | 23 |
| | 24 | +await assert.rejects( | | 25 | + import(jsModuleDataUrl, { with: { type: 'text' } }), | | 26 | + { code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' } | | 27 | +); | | 28 | + | 24 | 29 | await assert.rejects( | 25 | 30 | import(jsModuleDataUrl, { with: { type: 'json', other: 'unsupported' } }), | 26 | 31 | { code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' } | |
|