From a193be3dc2c38fe821845c5d89e565be554ac088 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 12 Dec 2023 19:18:28 +0100 Subject: [PATCH] esm: use import attributes instead of import assertions The old import assertions proposal has been renamed to "import attributes" with the follwing major changes: 1. The keyword is now `with` instead of `assert`. 2. Unknown assertions cause an error rather than being ignored, This commit updates the documentation to encourage folks to use the new syntax, and add aliases for module customization hooks. PR-URL: https://github.com/nodejs/node/pull/50140 Backport-PR-URL: https://github.com/nodejs/node/pull/51136 Fixes: https://github.com/nodejs/node/issues/50134 Refs: https://github.com/v8/v8/commit/159c82c5e6392e78b9bba7161b1bed6e23758984 Reviewed-By: Geoffrey Booth Reviewed-By: Jacob Smith Reviewed-By: Benjamin Gruenbaum --- .eslintrc.js | 5 +---- doc/api/esm.md | 20 ++++++++++--------- src/node.cc | 7 +++++++ .../test-esm-assertionless-json-import.js | 10 +++++----- test/es-module/test-esm-data-urls.js | 10 +++++----- .../test-esm-dynamic-import-attribute.js | 8 ++++---- .../test-esm-dynamic-import-attribute.mjs | 8 ++++---- .../test-esm-import-attributes-1.mjs | 2 +- .../test-esm-import-attributes-2.mjs | 4 ++-- .../test-esm-import-attributes-3.mjs | 4 ++-- .../test-esm-import-attributes-errors.js | 16 +++++++-------- .../test-esm-import-attributes-errors.mjs | 14 ++++++------- test/es-module/test-esm-json-cache.mjs | 2 +- test/es-module/test-esm-json.mjs | 10 +++++----- test/es-module/test-esm-virtual-json.mjs | 4 ++-- .../es-modules/import-json-named-export.mjs | 2 +- test/fixtures/es-modules/json-modules.mjs | 2 +- .../parallel/test-vm-module-dynamic-import.js | 2 +- 18 files changed, 68 insertions(+), 62 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index b427c9986f679f..49a810249146d7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -44,10 +44,7 @@ module.exports = { parserOptions: { babelOptions: { plugins: [ - [ - Module._findPath('@babel/plugin-syntax-import-attributes'), - { deprecatedAssertSyntax: true }, - ], + Module._findPath('@babel/plugin-syntax-import-attributes'), ], }, requireConfigFile: false, diff --git a/doc/api/esm.md b/doc/api/esm.md index 9b629f2a2a997d..448a3c119513fd 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -7,6 +7,9 @@ @@ -251,18 +254,17 @@ changes: > Stability: 1.1 - Active development > This feature was previously named "Import assertions", and using the `assert` -> keyword instead of `with`. Because the version of V8 on this release line does -> not support the `with` keyword, you need to keep using `assert` to support -> this version of Node.js. +> keyword instead of `with`. Any uses in code of the prior `assert` keyword +> should be updated to use `with` instead. The [Import Attributes proposal][] adds an inline syntax for module import statements to pass on more information alongside the module specifier. ```js -import fooData from './foo.json' assert { type: 'json' }; +import fooData from './foo.json' with { type: 'json' }; const { default: barData } = - await import('./bar.json', { assert: { type: 'json' } }); + await import('./bar.json', { with: { type: 'json' } }); ``` Node.js supports the following `type` values, for which the attribute is @@ -555,10 +557,10 @@ separate cache. JSON files can be referenced by `import`: ```js -import packageConfig from './package.json' assert { type: 'json' }; +import packageConfig from './package.json' with { type: 'json' }; ``` -The `assert { type: 'json' }` syntax is mandatory; see [Import Attributes][]. +The `with { type: 'json' }` syntax is mandatory; see [Import Attributes][]. The imported JSON only exposes a `default` export. There is no support for named exports. A cache entry is created in the CommonJS cache to avoid duplication. diff --git a/src/node.cc b/src/node.cc index 0c75b5a08edd05..367d47325b7d5e 100644 --- a/src/node.cc +++ b/src/node.cc @@ -745,6 +745,13 @@ int ProcessGlobalArgs(std::vector* args, "--no-harmony-import-assertions") == v8_args.end()) { v8_args.emplace_back("--harmony-import-assertions"); } + // TODO(aduh95): remove this when the harmony-import-attributes flag + // is removed in V8. + if (std::find(v8_args.begin(), + v8_args.end(), + "--no-harmony-import-attributes") == v8_args.end()) { + v8_args.emplace_back("--harmony-import-attributes"); + } auto env_opts = per_process::cli_options->per_isolate->per_env; if (std::find(v8_args.begin(), v8_args.end(), diff --git a/test/es-module/test-esm-assertionless-json-import.js b/test/es-module/test-esm-assertionless-json-import.js index 23c71a1ba105d2..bd8acdae995a76 100644 --- a/test/es-module/test-esm-assertionless-json-import.js +++ b/test/es-module/test-esm-assertionless-json-import.js @@ -9,7 +9,7 @@ async function test() { import('../fixtures/experimental.json'), import( '../fixtures/experimental.json', - { assert: { type: 'json' } } + { with: { type: 'json' } } ), ]); @@ -24,7 +24,7 @@ async function test() { import('../fixtures/experimental.json?test'), import( '../fixtures/experimental.json?test', - { assert: { type: 'json' } } + { with: { type: 'json' } } ), ]); @@ -39,7 +39,7 @@ async function test() { import('../fixtures/experimental.json#test'), import( '../fixtures/experimental.json#test', - { assert: { type: 'json' } } + { with: { type: 'json' } } ), ]); @@ -54,7 +54,7 @@ async function test() { import('../fixtures/experimental.json?test2#test'), import( '../fixtures/experimental.json?test2#test', - { assert: { type: 'json' } } + { with: { type: 'json' } } ), ]); @@ -69,7 +69,7 @@ async function test() { import('data:application/json,{"ofLife":42}'), import( 'data:application/json,{"ofLife":42}', - { assert: { type: 'json' } } + { with: { type: 'json' } } ), ]); diff --git a/test/es-module/test-esm-data-urls.js b/test/es-module/test-esm-data-urls.js index 5be45d0f7af3b6..a0add97b6f64ad 100644 --- a/test/es-module/test-esm-data-urls.js +++ b/test/es-module/test-esm-data-urls.js @@ -60,21 +60,21 @@ function createBase64URL(mime, body) { } { const ns = await import('data:application/json;foo="test,"this"', - { assert: { type: 'json' } }); + { with: { type: 'json' } }); assert.deepStrictEqual(Object.keys(ns), ['default']); assert.strictEqual(ns.default, 'this'); } { const ns = await import(`data:application/json;foo=${ encodeURIComponent('test,') - },0`, { assert: { type: 'json' } }); + },0`, { with: { type: 'json' } }); assert.deepStrictEqual(Object.keys(ns), ['default']); assert.strictEqual(ns.default, 0); } { await assert.rejects(async () => import('data:application/json;foo="test,",0', - { assert: { type: 'json' } }), { + { with: { type: 'json' } }), { name: 'SyntaxError', message: /Unexpected end of JSON input/ }); @@ -82,14 +82,14 @@ function createBase64URL(mime, body) { { const body = '{"x": 1}'; const plainESMURL = createURL('application/json', body); - const ns = await import(plainESMURL, { assert: { type: 'json' } }); + const ns = await import(plainESMURL, { with: { type: 'json' } }); assert.deepStrictEqual(Object.keys(ns), ['default']); assert.strictEqual(ns.default.x, 1); } { const body = '{"default": 2}'; const plainESMURL = createURL('application/json', body); - const ns = await import(plainESMURL, { assert: { type: 'json' } }); + const ns = await import(plainESMURL, { with: { type: 'json' } }); assert.deepStrictEqual(Object.keys(ns), ['default']); assert.strictEqual(ns.default.default, 2); } diff --git a/test/es-module/test-esm-dynamic-import-attribute.js b/test/es-module/test-esm-dynamic-import-attribute.js index 71ef9cd1d1d30b..4558cd27ca4237 100644 --- a/test/es-module/test-esm-dynamic-import-attribute.js +++ b/test/es-module/test-esm-dynamic-import-attribute.js @@ -5,7 +5,7 @@ const { strictEqual } = require('assert'); async function test() { { const results = await Promise.allSettled([ - import('../fixtures/empty.js', { assert: { type: 'json' } }), + import('../fixtures/empty.js', { with: { type: 'json' } }), import('../fixtures/empty.js'), ]); @@ -16,7 +16,7 @@ async function test() { { const results = await Promise.allSettled([ import('../fixtures/empty.js'), - import('../fixtures/empty.js', { assert: { type: 'json' } }), + import('../fixtures/empty.js', { with: { type: 'json' } }), ]); strictEqual(results[0].status, 'fulfilled'); @@ -25,7 +25,7 @@ async function test() { { const results = await Promise.allSettled([ - import('../fixtures/empty.json', { assert: { type: 'json' } }), + import('../fixtures/empty.json', { with: { type: 'json' } }), import('../fixtures/empty.json'), ]); @@ -36,7 +36,7 @@ async function test() { { const results = await Promise.allSettled([ import('../fixtures/empty.json'), - import('../fixtures/empty.json', { assert: { type: 'json' } }), + import('../fixtures/empty.json', { with: { type: 'json' } }), ]); strictEqual(results[0].status, 'rejected'); diff --git a/test/es-module/test-esm-dynamic-import-attribute.mjs b/test/es-module/test-esm-dynamic-import-attribute.mjs index 4010259b743cbd..b3d2cb20c36e34 100644 --- a/test/es-module/test-esm-dynamic-import-attribute.mjs +++ b/test/es-module/test-esm-dynamic-import-attribute.mjs @@ -3,7 +3,7 @@ import { strictEqual } from 'assert'; { const results = await Promise.allSettled([ - import('../fixtures/empty.js', { assert: { type: 'json' } }), + import('../fixtures/empty.js', { with: { type: 'json' } }), import('../fixtures/empty.js'), ]); @@ -14,7 +14,7 @@ import { strictEqual } from 'assert'; { const results = await Promise.allSettled([ import('../fixtures/empty.js'), - import('../fixtures/empty.js', { assert: { type: 'json' } }), + import('../fixtures/empty.js', { with: { type: 'json' } }), ]); strictEqual(results[0].status, 'fulfilled'); @@ -23,7 +23,7 @@ import { strictEqual } from 'assert'; { const results = await Promise.allSettled([ - import('../fixtures/empty.json', { assert: { type: 'json' } }), + import('../fixtures/empty.json', { with: { type: 'json' } }), import('../fixtures/empty.json'), ]); @@ -34,7 +34,7 @@ import { strictEqual } from 'assert'; { const results = await Promise.allSettled([ import('../fixtures/empty.json'), - import('../fixtures/empty.json', { assert: { type: 'json' } }), + import('../fixtures/empty.json', { with: { type: 'json' } }), ]); strictEqual(results[0].status, 'rejected'); diff --git a/test/es-module/test-esm-import-attributes-1.mjs b/test/es-module/test-esm-import-attributes-1.mjs index 72b3426bdbb601..c699a27bb51a75 100644 --- a/test/es-module/test-esm-import-attributes-1.mjs +++ b/test/es-module/test-esm-import-attributes-1.mjs @@ -1,6 +1,6 @@ import '../common/index.mjs'; import { strictEqual } from 'assert'; -import secret from '../fixtures/experimental.json' assert { type: 'json' }; +import secret from '../fixtures/experimental.json' with { type: 'json' }; strictEqual(secret.ofLife, 42); diff --git a/test/es-module/test-esm-import-attributes-2.mjs b/test/es-module/test-esm-import-attributes-2.mjs index 1b4669ac276474..85d6020019af05 100644 --- a/test/es-module/test-esm-import-attributes-2.mjs +++ b/test/es-module/test-esm-import-attributes-2.mjs @@ -1,9 +1,9 @@ import '../common/index.mjs'; import { strictEqual } from 'assert'; -import secret0 from '../fixtures/experimental.json' assert { type: 'json' }; +import secret0 from '../fixtures/experimental.json' with { type: 'json' }; const secret1 = await import('../fixtures/experimental.json', { - assert: { type: 'json' }, + with: { type: 'json' }, }); strictEqual(secret0.ofLife, 42); diff --git a/test/es-module/test-esm-import-attributes-3.mjs b/test/es-module/test-esm-import-attributes-3.mjs index b9de9232cfff4d..8950ca5c595e12 100644 --- a/test/es-module/test-esm-import-attributes-3.mjs +++ b/test/es-module/test-esm-import-attributes-3.mjs @@ -1,9 +1,9 @@ import '../common/index.mjs'; import { strictEqual } from 'assert'; -import secret0 from '../fixtures/experimental.json' assert { type: 'json' }; +import secret0 from '../fixtures/experimental.json' with { type: 'json' }; const secret1 = await import('../fixtures/experimental.json', - { assert: { type: 'json' } }); + { with: { type: 'json' } }); strictEqual(secret0.ofLife, 42); strictEqual(secret1.default.ofLife, 42); diff --git a/test/es-module/test-esm-import-attributes-errors.js b/test/es-module/test-esm-import-attributes-errors.js index f6e57f19b6b432..4521248db176e5 100644 --- a/test/es-module/test-esm-import-attributes-errors.js +++ b/test/es-module/test-esm-import-attributes-errors.js @@ -7,27 +7,27 @@ const jsonModuleDataUrl = 'data:application/json,""'; async function test() { await rejects( - import('data:text/css,', { assert: { type: 'css' } }), + import('data:text/css,', { with: { type: 'css' } }), { code: 'ERR_UNKNOWN_MODULE_FORMAT' } ); await rejects( - import('data:text/css,', { assert: { unsupportedAttribute: 'value' } }), + import('data:text/css,', { with: { unsupportedAttribute: 'value' } }), { code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' } ); await rejects( - import(`data:text/javascript,import${JSON.stringify(jsModuleDataUrl)}assert{type:"json"}`), + import(`data:text/javascript,import${JSON.stringify(jsModuleDataUrl)}with{type:"json"}`), { code: 'ERR_IMPORT_ASSERTION_TYPE_FAILED' } ); await rejects( - import(jsModuleDataUrl, { assert: { type: 'json' } }), + import(jsModuleDataUrl, { with: { type: 'json' } }), { code: 'ERR_IMPORT_ASSERTION_TYPE_FAILED' } ); await rejects( - import(jsModuleDataUrl, { assert: { type: 'unsupported' } }), + import(jsModuleDataUrl, { with: { type: 'unsupported' } }), { code: 'ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED' } ); @@ -37,17 +37,17 @@ async function test() { ); await rejects( - import(jsonModuleDataUrl, { assert: {} }), + import(jsonModuleDataUrl, { with: {} }), { code: 'ERR_IMPORT_ASSERTION_TYPE_MISSING' } ); await rejects( - import(jsonModuleDataUrl, { assert: { foo: 'bar' } }), + import(jsonModuleDataUrl, { with: { foo: 'bar' } }), { code: 'ERR_IMPORT_ASSERTION_TYPE_MISSING' } ); await rejects( - import(jsonModuleDataUrl, { assert: { type: 'unsupported' } }), + import(jsonModuleDataUrl, { with: { type: 'unsupported' } }), { code: 'ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED' } ); } diff --git a/test/es-module/test-esm-import-attributes-errors.mjs b/test/es-module/test-esm-import-attributes-errors.mjs index 6621585be412e0..ff932636e39a5f 100644 --- a/test/es-module/test-esm-import-attributes-errors.mjs +++ b/test/es-module/test-esm-import-attributes-errors.mjs @@ -7,22 +7,22 @@ const jsonModuleDataUrl = 'data:application/json,""'; await rejects( // This rejects because of the unsupported MIME type, not because of the // unsupported assertion. - import('data:text/css,', { assert: { type: 'css' } }), + import('data:text/css,', { with: { type: 'css' } }), { code: 'ERR_UNKNOWN_MODULE_FORMAT' } ); await rejects( - import(`data:text/javascript,import${JSON.stringify(jsModuleDataUrl)}assert{type:"json"}`), + import(`data:text/javascript,import${JSON.stringify(jsModuleDataUrl)}with{type:"json"}`), { code: 'ERR_IMPORT_ASSERTION_TYPE_FAILED' } ); await rejects( - import(jsModuleDataUrl, { assert: { type: 'json' } }), + import(jsModuleDataUrl, { with: { type: 'json' } }), { code: 'ERR_IMPORT_ASSERTION_TYPE_FAILED' } ); await rejects( - import(import.meta.url, { assert: { type: 'unsupported' } }), + import(import.meta.url, { with: { type: 'unsupported' } }), { code: 'ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED' } ); @@ -32,16 +32,16 @@ await rejects( ); await rejects( - import(jsonModuleDataUrl, { assert: {} }), + import(jsonModuleDataUrl, { with: {} }), { code: 'ERR_IMPORT_ASSERTION_TYPE_MISSING' } ); await rejects( - import(jsonModuleDataUrl, { assert: { foo: 'bar' } }), + import(jsonModuleDataUrl, { with: { foo: 'bar' } }), { code: 'ERR_IMPORT_ASSERTION_TYPE_MISSING' } ); await rejects( - import(jsonModuleDataUrl, { assert: { type: 'unsupported' } }), + import(jsonModuleDataUrl, { with: { type: 'unsupported' } }), { code: 'ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED' } ); diff --git a/test/es-module/test-esm-json-cache.mjs b/test/es-module/test-esm-json-cache.mjs index b766519d663f9a..f8d24fd394a4a2 100644 --- a/test/es-module/test-esm-json-cache.mjs +++ b/test/es-module/test-esm-json-cache.mjs @@ -6,7 +6,7 @@ import { createRequire } from 'module'; import mod from '../fixtures/es-modules/json-cache/mod.cjs'; import another from '../fixtures/es-modules/json-cache/another.cjs'; -import test from '../fixtures/es-modules/json-cache/test.json' assert +import test from '../fixtures/es-modules/json-cache/test.json' with { type: 'json' }; const require = createRequire(import.meta.url); diff --git a/test/es-module/test-esm-json.mjs b/test/es-module/test-esm-json.mjs index e5a0ab9f74e2cf..422a8f717594ab 100644 --- a/test/es-module/test-esm-json.mjs +++ b/test/es-module/test-esm-json.mjs @@ -7,7 +7,7 @@ import { describe, it, test } from 'node:test'; import { mkdir, rm, writeFile } from 'node:fs/promises'; import * as tmpdir from '../common/tmpdir.js'; -import secret from '../fixtures/experimental.json' assert { type: 'json' }; +import secret from '../fixtures/experimental.json' with { type: 'json' }; describe('ESM: importing JSON', () => { it('should load JSON', () => { @@ -34,19 +34,19 @@ describe('ESM: importing JSON', () => { const url = new URL('./foo.json', root); await writeFile(url, JSON.stringify({ id: i++ })); const absoluteURL = await import(`${url}`, { - assert: { type: 'json' }, + with: { type: 'json' }, }); await writeFile(url, JSON.stringify({ id: i++ })); const queryString = await import(`${url}?a=2`, { - assert: { type: 'json' }, + with: { type: 'json' }, }); await writeFile(url, JSON.stringify({ id: i++ })); const hash = await import(`${url}#a=2`, { - assert: { type: 'json' }, + with: { type: 'json' }, }); await writeFile(url, JSON.stringify({ id: i++ })); const queryStringAndHash = await import(`${url}?a=2#a=2`, { - assert: { type: 'json' }, + with: { type: 'json' }, }); assert.notDeepStrictEqual(absoluteURL, queryString); diff --git a/test/es-module/test-esm-virtual-json.mjs b/test/es-module/test-esm-virtual-json.mjs index 8876eea013ced8..a42b037fc1f200 100644 --- a/test/es-module/test-esm-virtual-json.mjs +++ b/test/es-module/test-esm-virtual-json.mjs @@ -25,6 +25,6 @@ function load(url, context, next) { register(`data:text/javascript,export ${encodeURIComponent(resolve)};export ${encodeURIComponent(load)}`); assert.notDeepStrictEqual( - await import(fixtures.fileURL('empty.json'), { assert: { type: 'json' } }), - await import(fixtures.fileURL('empty.json'), { assert: { type: 'json' } }), + await import(fixtures.fileURL('empty.json'), { with: { type: 'json' } }), + await import(fixtures.fileURL('empty.json'), { with: { type: 'json' } }), ); diff --git a/test/fixtures/es-modules/import-json-named-export.mjs b/test/fixtures/es-modules/import-json-named-export.mjs index 01798c59ac587d..be1a4116eb5ffa 100644 --- a/test/fixtures/es-modules/import-json-named-export.mjs +++ b/test/fixtures/es-modules/import-json-named-export.mjs @@ -1 +1 @@ -import { ofLife } from '../experimental.json' assert { type: 'json' }; +import { ofLife } from '../experimental.json' with { type: 'json' }; diff --git a/test/fixtures/es-modules/json-modules.mjs b/test/fixtures/es-modules/json-modules.mjs index 607c09e51cda2b..c1eae2b689a696 100644 --- a/test/fixtures/es-modules/json-modules.mjs +++ b/test/fixtures/es-modules/json-modules.mjs @@ -1 +1 @@ -import secret from '../experimental.json' assert { type: 'json' }; +import secret from '../experimental.json' with { type: 'json' }; diff --git a/test/parallel/test-vm-module-dynamic-import.js b/test/parallel/test-vm-module-dynamic-import.js index 5bca08b8c9c3bb..b74d3b28d7a547 100644 --- a/test/parallel/test-vm-module-dynamic-import.js +++ b/test/parallel/test-vm-module-dynamic-import.js @@ -58,7 +58,7 @@ async function test() { } { - const s = new Script('import("foo", { assert: { key: "value" } })', { + const s = new Script('import("foo", { with: { key: "value" } })', { importModuleDynamically: common.mustCall((specifier, wrap, attributes) => { assert.strictEqual(specifier, 'foo'); assert.strictEqual(wrap, s);