From f55d97cc1a1cd4b516fcaf4ecaa05485ee7e159e Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sun, 27 Oct 2024 20:12:24 +0900 Subject: [PATCH] Add support for ES2025 Import Attributes FEATURE: Support ES2025 import attributes. Closes https://github.com/acornjs/acorn/issues/1289 --- acorn-loose/src/expression.js | 4 +- acorn-loose/src/statement.js | 37 ++ acorn-walk/src/index.js | 11 + acorn/src/acorn.d.ts | 12 +- acorn/src/expression.js | 31 +- acorn/src/statement.js | 41 ++ bin/test262.unsupported-features | 1 - bin/test262.whitelist | 4 + test/run.js | 1 + test/tests-import-attributes.js | 845 +++++++++++++++++++++++++++++++ 10 files changed, 978 insertions(+), 9 deletions(-) create mode 100644 test/tests-import-attributes.js diff --git a/acorn-loose/src/expression.js b/acorn-loose/src/expression.js index 14b65bc5d..78c77f24f 100644 --- a/acorn-loose/src/expression.js +++ b/acorn-loose/src/expression.js @@ -352,7 +352,9 @@ lp.parseExprImport = function() { } lp.parseDynamicImport = function(node) { - node.source = this.parseExprList(tt.parenR)[0] || this.dummyString() + const list = this.parseExprList(tt.parenR) + node.source = list[0] || this.dummyString() + node.options = list[1] || null return this.finishNode(node, "ImportExpression") } diff --git a/acorn-loose/src/statement.js b/acorn-loose/src/statement.js index b795e0f22..1befa702e 100644 --- a/acorn-loose/src/statement.js +++ b/acorn-loose/src/statement.js @@ -461,6 +461,8 @@ lp.parseExport = function() { } } node.source = this.eatContextual("from") ? this.parseExprAtom() : this.dummyString() + if (this.options.ecmaVersion >= 16) + node.attributes = this.parseWithClause() this.semicolon() return this.finishNode(node, "ExportAllDeclaration") } @@ -488,6 +490,8 @@ lp.parseExport = function() { node.declaration = null node.specifiers = this.parseExportSpecifierList() node.source = this.eatContextual("from") ? this.parseExprAtom() : null + if (this.options.ecmaVersion >= 16) + node.attributes = this.parseWithClause() this.semicolon() } return this.finishNode(node, "ExportNamedDeclaration") @@ -511,6 +515,8 @@ lp.parseImport = function() { node.source = this.eatContextual("from") && this.tok.type === tt.string ? this.parseExprAtom() : this.dummyString() if (elt) node.specifiers.unshift(elt) } + if (this.options.ecmaVersion >= 16) + node.attributes = this.parseWithClause() this.semicolon() return this.finishNode(node, "ImportDeclaration") } @@ -548,6 +554,37 @@ lp.parseImportSpecifiers = function() { return elts } +lp.parseWithClause = function() { + let nodes = [] + if (!this.eat(tt._with)) { + return nodes + } + + let indent = this.curIndent, line = this.curLineStart, continuedLine = this.nextLineStart + this.pushCx() + this.eat(tt.braceL) + if (this.curLineStart > continuedLine) continuedLine = this.curLineStart + while (!this.closes(tt.braceR, indent + (this.curLineStart <= continuedLine ? 1 : 0), line)) { + const attr = this.startNode() + attr.key = this.tok.type === tt.string ? this.parseExprAtom() : this.parseIdent() + if (this.eat(tt.colon)) { + if (this.tok.type === tt.string) + attr.value = this.parseExprAtom() + else attr.value = this.dummyString() + } else { + if (isDummy(attr.key)) break + if (this.tok.type === tt.string) + attr.value = this.parseExprAtom() + else break + } + nodes.push(this.finishNode(attr, "ImportAttribute")) + this.eat(tt.comma) + } + this.eat(tt.braceR) + this.popCx() + return nodes +} + lp.parseExportSpecifierList = function() { let elts = [] let indent = this.curIndent, line = this.curLineStart, continuedLine = this.nextLineStart diff --git a/acorn-walk/src/index.js b/acorn-walk/src/index.js index ce4c2cd8e..c22d2405a 100644 --- a/acorn-walk/src/index.js +++ b/acorn-walk/src/index.js @@ -347,19 +347,30 @@ base.ExportNamedDeclaration = base.ExportDefaultDeclaration = (node, st, c) => { if (node.declaration) c(node.declaration, st, node.type === "ExportNamedDeclaration" || node.declaration.id ? "Statement" : "Expression") if (node.source) c(node.source, st, "Expression") + if (node.attributes) + for (let attr of node.attributes) + c(attr, st) } base.ExportAllDeclaration = (node, st, c) => { if (node.exported) c(node.exported, st) c(node.source, st, "Expression") + for (let attr of node.attributes) + c(attr, st) +} +base.ImportAttribute = (node, st, c) => { + c(node.value, st, "Expression") } base.ImportDeclaration = (node, st, c) => { for (let spec of node.specifiers) c(spec, st) c(node.source, st, "Expression") + for (let attr of node.attributes) + c(attr, st) } base.ImportExpression = (node, st, c) => { c(node.source, st, "Expression") + if (node.options) c(node.options, st, "Expression") } base.ImportSpecifier = base.ImportDefaultSpecifier = base.ImportNamespaceSpecifier = base.Identifier = base.PrivateIdentifier = base.Literal = ignore diff --git a/acorn/src/acorn.d.ts b/acorn/src/acorn.d.ts index cd204b1c5..81f4e38fd 100644 --- a/acorn/src/acorn.d.ts +++ b/acorn/src/acorn.d.ts @@ -403,6 +403,7 @@ export interface ImportDeclaration extends Node { type: "ImportDeclaration" specifiers: Array source: Literal + attributes: Array } export interface ImportSpecifier extends Node { @@ -421,11 +422,18 @@ export interface ImportNamespaceSpecifier extends Node { local: Identifier } +export interface ImportAttribute extends Node { + type: "ImportAttribute" + key: Identifier | Literal + value: Literal +} + export interface ExportNamedDeclaration extends Node { type: "ExportNamedDeclaration" declaration?: Declaration | null specifiers: Array source?: Literal | null + attributes: Array } export interface ExportSpecifier extends Node { @@ -454,6 +462,7 @@ export interface ExportAllDeclaration extends Node { type: "ExportAllDeclaration" source: Literal exported?: Identifier | Literal | null + attributes: Array } export interface AwaitExpression extends Node { @@ -469,6 +478,7 @@ export interface ChainExpression extends Node { export interface ImportExpression extends Node { type: "ImportExpression" source: Expression + options: Expression | null } export interface ParenthesizedExpression extends Node { @@ -562,7 +572,7 @@ export type ModuleDeclaration = | ExportDefaultDeclaration | ExportAllDeclaration -export type AnyNode = Statement | Expression | Declaration | ModuleDeclaration | Literal | Program | SwitchCase | CatchClause | Property | Super | SpreadElement | TemplateElement | AssignmentProperty | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern | ClassBody | MethodDefinition | MetaProperty | ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier | AnonymousFunctionDeclaration | AnonymousClassDeclaration | PropertyDefinition | PrivateIdentifier | StaticBlock | VariableDeclarator +export type AnyNode = Statement | Expression | Declaration | ModuleDeclaration | Literal | Program | SwitchCase | CatchClause | Property | Super | SpreadElement | TemplateElement | AssignmentProperty | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern | ClassBody | MethodDefinition | MetaProperty | ImportAttribute | ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier | AnonymousFunctionDeclaration | AnonymousClassDeclaration | PropertyDefinition | PrivateIdentifier | StaticBlock | VariableDeclarator export function parse(input: string, options: Options): Program diff --git a/acorn/src/expression.js b/acorn/src/expression.js index 06dcc6f92..c335a7096 100644 --- a/acorn/src/expression.js +++ b/acorn/src/expression.js @@ -546,13 +546,32 @@ pp.parseDynamicImport = function(node) { // Parse node.source. node.source = this.parseMaybeAssign() - // Verify ending. - if (!this.eat(tt.parenR)) { - const errorPos = this.start - if (this.eat(tt.comma) && this.eat(tt.parenR)) { - this.raiseRecoverable(errorPos, "Trailing comma is not allowed in import()") + if (this.options.ecmaVersion >= 16) { + if (!this.eat(tt.parenR)) { + this.expect(tt.comma) + if (!this.afterTrailingComma(tt.parenR)) { + node.options = this.parseMaybeAssign() + if (!this.eat(tt.parenR)) { + this.expect(tt.comma) + if (!this.afterTrailingComma(tt.parenR)) { + this.unexpected() + } + } + } else { + node.options = null + } } else { - this.unexpected(errorPos) + node.options = null + } + } else { + // Verify ending. + if (!this.eat(tt.parenR)) { + const errorPos = this.start + if (this.eat(tt.comma) && this.eat(tt.parenR)) { + this.raiseRecoverable(errorPos, "Trailing comma is not allowed in import()") + } else { + this.unexpected(errorPos) + } } } diff --git a/acorn/src/statement.js b/acorn/src/statement.js index c5fea3956..0dcfd04f9 100644 --- a/acorn/src/statement.js +++ b/acorn/src/statement.js @@ -859,6 +859,8 @@ pp.parseExportAllDeclaration = function(node, exports) { this.expectContextual("from") if (this.type !== tt.string) this.unexpected() node.source = this.parseExprAtom() + if (this.options.ecmaVersion >= 16) + node.attributes = this.parseWithClause() this.semicolon() return this.finishNode(node, "ExportAllDeclaration") } @@ -889,6 +891,8 @@ pp.parseExport = function(node, exports) { if (this.eatContextual("from")) { if (this.type !== tt.string) this.unexpected() node.source = this.parseExprAtom() + if (this.options.ecmaVersion >= 16) + node.attributes = this.parseWithClause() } else { for (let spec of node.specifiers) { // check for keywords used as local names @@ -1017,6 +1021,8 @@ pp.parseImport = function(node) { this.expectContextual("from") node.source = this.type === tt.string ? this.parseExprAtom() : this.unexpected() } + if (this.options.ecmaVersion >= 16) + node.attributes = this.parseWithClause() this.semicolon() return this.finishNode(node, "ImportDeclaration") } @@ -1077,6 +1083,41 @@ pp.parseImportSpecifiers = function() { return nodes } +pp.parseWithClause = function() { + let nodes = [] + if (!this.eat(tt._with)) { + return nodes + } + this.expect(tt.braceL) + const attributeKeys = {} + let first = true + while (!this.eat(tt.braceR)) { + if (!first) { + this.expect(tt.comma) + if (this.afterTrailingComma(tt.braceR)) break + } else first = false + + const attr = this.parseImportAttribute() + const keyName = attr.key.type === "Identifier" ? attr.key.name : attr.key.value + if (hasOwn(attributeKeys, keyName)) + this.raiseRecoverable(attr.key.start, "Duplicate attribute key '" + keyName + "'") + attributeKeys[keyName] = true + nodes.push(attr) + } + return nodes +} + +pp.parseImportAttribute = function() { + const node = this.startNode() + node.key = this.type === tt.string ? this.parseExprAtom() : this.parseIdent(this.options.allowReserved !== "never") + this.expect(tt.colon) + if (this.type !== tt.string) { + this.unexpected() + } + node.value = this.parseExprAtom() + return this.finishNode(node, "ImportAttribute") +} + pp.parseModuleExportName = function() { if (this.options.ecmaVersion >= 13 && this.type === tt.string) { const stringLiteral = this.parseLiteral(this.value) diff --git a/bin/test262.unsupported-features b/bin/test262.unsupported-features index 3bdec7c11..3eb8b1525 100644 --- a/bin/test262.unsupported-features +++ b/bin/test262.unsupported-features @@ -1,5 +1,4 @@ decorators explicit-resource-management regexp-modifiers -import-attributes source-phase-imports diff --git a/bin/test262.whitelist b/bin/test262.whitelist index 77b4cc0de..a5e3a296e 100644 --- a/bin/test262.whitelist +++ b/bin/test262.whitelist @@ -26,3 +26,7 @@ built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Todhri.js (defau built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Todhri.js (strict mode) built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Tulu_Tigalari.js (default) built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Tulu_Tigalari.js (strict mode) +language/import/import-attributes/json-invalid.js (default) +language/import/import-attributes/json-invalid.js (strict mode) +language/import/import-attributes/json-named-bindings.js (default) +language/import/import-attributes/json-named-bindings.js (strict mode) diff --git a/test/run.js b/test/run.js index 0587bc571..77ec4551e 100644 --- a/test/run.js +++ b/test/run.js @@ -28,6 +28,7 @@ require("./tests-numeric-separators.js"); require("./tests-class-features-2022.js"); require("./tests-module-string-names.js"); + require("./tests-import-attributes.js"); var acorn = require("../acorn") var acorn_loose = require("../acorn-loose") diff --git a/test/tests-import-attributes.js b/test/tests-import-attributes.js new file mode 100644 index 000000000..98913f47d --- /dev/null +++ b/test/tests-import-attributes.js @@ -0,0 +1,845 @@ +/* eslint quote-props: ["error", "as-needed"] */ +/* eslint quotes: ["error", "double", { "avoidEscape": true }] */ +if (typeof exports !== "undefined") { + var test = require("./driver.js").test + var testFail = require("./driver.js").testFail +} + +test( + 'import json from "./foo.json" with { type: "json" };', + { + type: "Program", + start: 0, + end: 52, + body: [ + { + type: "ImportDeclaration", + start: 0, + end: 52, + specifiers: [ + { + type: "ImportDefaultSpecifier", + start: 7, + end: 11, + local: { + type: "Identifier", + start: 7, + end: 11, + name: "json" + } + } + ], + source: { + type: "Literal", + start: 17, + end: 29, + value: "./foo.json", + raw: "\"./foo.json\"" + }, + attributes: [ + { + type: "ImportAttribute", + start: 37, + end: 49, + key: { + type: "Identifier", + start: 37, + end: 41, + name: "type" + }, + value: { + type: "Literal", + start: 43, + end: 49, + value: "json", + raw: "\"json\"" + } + } + ] + } + ], + sourceType: "module" + }, + {sourceType: "module", ecmaVersion: 16} +) + +test( + 'import "./foo.json" with { type: "json" };', + { + type: "Program", + start: 0, + end: 42, + body: [ + { + type: "ImportDeclaration", + start: 0, + end: 42, + specifiers: [], + source: { + type: "Literal", + start: 7, + end: 19, + value: "./foo.json", + raw: "\"./foo.json\"" + }, + attributes: [ + { + type: "ImportAttribute", + start: 27, + end: 39, + key: { + type: "Identifier", + start: 27, + end: 31, + name: "type" + }, + value: { + type: "Literal", + start: 33, + end: 39, + value: "json", + raw: "\"json\"" + } + } + ] + } + ], + sourceType: "module" + }, + {sourceType: "module", ecmaVersion: 16} +) + +test( + 'import json from "./foo.json";', // Without attributes + { + type: "Program", + start: 0, + end: 30, + body: [ + { + type: "ImportDeclaration", + start: 0, + end: 30, + specifiers: [ + { + type: "ImportDefaultSpecifier", + start: 7, + end: 11, + local: { + type: "Identifier", + start: 7, + end: 11, + name: "json" + } + } + ], + source: { + type: "Literal", + start: 17, + end: 29, + value: "./foo.json", + raw: "\"./foo.json\"" + }, + attributes: [] + } + ], + sourceType: "module" + }, + {sourceType: "module", ecmaVersion: 16} +) + +test( + 'import "./foo.json";', // Without attributes + { + type: "Program", + start: 0, + end: 20, + body: [ + { + type: "ImportDeclaration", + start: 0, + end: 20, + specifiers: [], + source: { + type: "Literal", + start: 7, + end: 19, + value: "./foo.json", + raw: "\"./foo.json\"" + }, + attributes: [] + } + ], + sourceType: "module" + }, + {sourceType: "module", ecmaVersion: 16} +) + +test( + 'export {v} from "./foo.json" with { type: "json" };', + { + type: "Program", + start: 0, + end: 51, + body: [ + { + type: "ExportNamedDeclaration", + start: 0, + end: 51, + declaration: null, + specifiers: [ + { + type: "ExportSpecifier", + start: 8, + end: 9, + local: { + type: "Identifier", + start: 8, + end: 9, + name: "v" + }, + exported: { + type: "Identifier", + start: 8, + end: 9, + name: "v" + } + } + ], + source: { + type: "Literal", + start: 16, + end: 28, + value: "./foo.json", + raw: "\"./foo.json\"" + }, + attributes: [ + { + type: "ImportAttribute", + start: 36, + end: 48, + key: { + type: "Identifier", + start: 36, + end: 40, + name: "type" + }, + value: { + type: "Literal", + start: 42, + end: 48, + value: "json", + raw: "\"json\"" + } + } + ] + } + ], + sourceType: "module" + }, + {sourceType: "module", ecmaVersion: 16} +) + +test( + 'export {v} from "./foo.json";', // Without attributes + { + type: "Program", + start: 0, + end: 29, + body: [ + { + type: "ExportNamedDeclaration", + start: 0, + end: 29, + declaration: null, + specifiers: [ + { + type: "ExportSpecifier", + start: 8, + end: 9, + local: { + type: "Identifier", + start: 8, + end: 9, + name: "v" + }, + exported: { + type: "Identifier", + start: 8, + end: 9, + name: "v" + } + } + ], + source: { + type: "Literal", + start: 16, + end: 28, + value: "./foo.json", + raw: "\"./foo.json\"" + }, + attributes: [] + } + ], + sourceType: "module" + }, + {sourceType: "module", ecmaVersion: 16} +) + +test( + 'export * as json from "./foo.json" with { type: "json" };', + { + type: "Program", + start: 0, + end: 57, + body: [ + { + type: "ExportAllDeclaration", + start: 0, + end: 57, + exported: { + type: "Identifier", + start: 12, + end: 16, + name: "json" + }, + source: { + type: "Literal", + start: 22, + end: 34, + value: "./foo.json", + raw: "\"./foo.json\"" + }, + attributes: [ + { + type: "ImportAttribute", + start: 42, + end: 54, + key: { + type: "Identifier", + start: 42, + end: 46, + name: "type" + }, + value: { + type: "Literal", + start: 48, + end: 54, + value: "json", + raw: "\"json\"" + } + } + ] + } + ], + sourceType: "module" + }, + {sourceType: "module", ecmaVersion: 16} +) + +test( + 'export * as json from "./foo.json";', // Without attributes + { + type: "Program", + start: 0, + end: 35, + body: [ + { + type: "ExportAllDeclaration", + start: 0, + end: 35, + exported: { + type: "Identifier", + start: 12, + end: 16, + name: "json" + }, + source: { + type: "Literal", + start: 22, + end: 34, + value: "./foo.json", + raw: "\"./foo.json\"" + }, + attributes: [] + } + ], + sourceType: "module" + }, + {sourceType: "module", ecmaVersion: 16} +) + +test( + 'import("foo.json", { with: { type: "json" } });', + { + type: "Program", + start: 0, + end: 47, + body: [ + { + type: "ExpressionStatement", + start: 0, + end: 47, + expression: { + type: "ImportExpression", + start: 0, + end: 46, + source: { + type: "Literal", + start: 7, + end: 17, + value: "foo.json", + raw: "\"foo.json\"" + }, + options: { + type: "ObjectExpression", + start: 19, + end: 45, + properties: [ + { + type: "Property", + start: 21, + end: 43, + method: false, + shorthand: false, + computed: false, + key: { + type: "Identifier", + start: 21, + end: 25, + name: "with" + }, + value: { + type: "ObjectExpression", + start: 27, + end: 43, + properties: [ + { + type: "Property", + start: 29, + end: 41, + method: false, + shorthand: false, + computed: false, + key: { + type: "Identifier", + start: 29, + end: 33, + name: "type" + }, + value: { + type: "Literal", + start: 35, + end: 41, + value: "json", + raw: "\"json\"" + }, + kind: "init" + } + ] + }, + kind: "init" + } + ] + } + } + } + ], + sourceType: "module" + }, + {sourceType: "module", ecmaVersion: 16} +) + +test( + 'import("foo.json", );', // Allow trailing comma + { + type: "Program", + start: 0, + end: 21, + body: [ + { + type: "ExpressionStatement", + start: 0, + end: 21, + expression: { + type: "ImportExpression", + start: 0, + end: 20, + source: { + type: "Literal", + start: 7, + end: 17, + value: "foo.json", + raw: "\"foo.json\"" + }, + options: null + } + } + ], + sourceType: "module" + }, + {sourceType: "module", ecmaVersion: 16} +) + +test( + 'import("foo.json", { with: { type: "json" } }, );', + { + type: "Program", + start: 0, + end: 49, + body: [ + { + type: "ExpressionStatement", + start: 0, + end: 49, + expression: { + type: "ImportExpression", + start: 0, + end: 48, + source: { + type: "Literal", + start: 7, + end: 17, + value: "foo.json", + raw: "\"foo.json\"" + }, + options: { + type: "ObjectExpression", + start: 19, + end: 45, + properties: [ + { + type: "Property", + start: 21, + end: 43, + method: false, + shorthand: false, + computed: false, + key: { + type: "Identifier", + start: 21, + end: 25, + name: "with" + }, + kind: "init", + value: { + type: "ObjectExpression", + start: 27, + end: 43, + properties: [ + { + type: "Property", + start: 29, + end: 41, + method: false, + shorthand: false, + computed: false, + key: { + type: "Identifier", + start: 29, + end: 33, + name: "type" + }, + kind: "init", + value: { + type: "Literal", + start: 35, + end: 41, + value: "json", + raw: "\"json\"" + } + } + ] + } + } + ] + } + } + } + ], + sourceType: "module" + }, + {sourceType: "module", ecmaVersion: 16} +) + +test( + 'import(\"foo.json\", foo );', + { + type: "Program", + start: 0, + end: 25, + body: [ + { + type: "ExpressionStatement", + start: 0, + end: 25, + expression: { + type: "ImportExpression", + start: 0, + end: 24, + source: { + type: "Literal", + start: 7, + end: 17, + value: "foo.json", + raw: "\"foo.json\"" + }, + options: { + type: "Identifier", + start: 19, + end: 22, + name: "foo" + } + } + } + ], + sourceType: "module" + }, + {sourceType: "module", ecmaVersion: 16} +) + +test( + 'import "./foo.json" with { foo: "bar" };', // Allow unknown attributes + { + type: "Program", + start: 0, + end: 40, + body: [ + { + type: "ImportDeclaration", + start: 0, + end: 40, + specifiers: [], + source: { + type: "Literal", + start: 7, + end: 19, + value: "./foo.json", + raw: "\"./foo.json\"" + }, + attributes: [ + { + type: "ImportAttribute", + start: 27, + end: 37, + key: { + type: "Identifier", + start: 27, + end: 30, + name: "foo" + }, + value: { + type: "Literal", + start: 32, + end: 37, + value: "bar", + raw: "\"bar\"" + } + } + ] + } + ], + sourceType: "module" + }, + {sourceType: "module", ecmaVersion: 16} +) + +test( + 'import "./foo.json" with { "type": "json" };', // Allow string key + { + type: "Program", + start: 0, + end: 44, + body: [ + { + type: "ImportDeclaration", + start: 0, + end: 44, + specifiers: [], + source: { + type: "Literal", + start: 7, + end: 19, + value: "./foo.json", + raw: "\"./foo.json\"" + }, + attributes: [ + { + type: "ImportAttribute", + start: 27, + end: 41, + key: { + type: "Literal", + start: 27, + end: 33, + value: "type", + raw: "\"type\"" + }, + value: { + type: "Literal", + start: 35, + end: 41, + value: "json", + raw: "\"json\"" + } + } + ] + } + ], + sourceType: "module" + }, + {sourceType: "module", ecmaVersion: 16} +) + +test( + 'import "./foo.json" with { a: "a", b: "b" };', // Allow multiple attributes + { + type: "Program", + start: 0, + end: 44, + body: [ + { + type: "ImportDeclaration", + start: 0, + end: 44, + specifiers: [], + source: { + type: "Literal", + start: 7, + end: 19, + value: "./foo.json", + raw: "\"./foo.json\"" + }, + attributes: [ + { + type: "ImportAttribute", + start: 27, + end: 33, + key: { + type: "Identifier", + start: 27, + end: 28, + name: "a" + }, + value: { + type: "Literal", + start: 30, + end: 33, + value: "a", + raw: "\"a\"" + } + }, + { + type: "ImportAttribute", + start: 35, + end: 41, + key: { + type: "Identifier", + start: 35, + end: 36, + name: "b" + }, + value: { + type: "Literal", + start: 38, + end: 41, + value: "b", + raw: "\"b\"" + } + } + ] + } + ], + sourceType: "module" + }, + {sourceType: "module", ecmaVersion: 16} +) + +test( + 'import "./foo.json" with { "type": "json", };', // Allow trailing comma + { + type: "Program", + start: 0, + end: 46, + body: [ + { + type: "ImportDeclaration", + start: 0, + end: 46, + specifiers: [], + source: { + type: "Literal", + start: 7, + end: 19, + value: "./foo.json", + raw: "\"./foo.json\"" + }, + attributes: [ + { + type: "ImportAttribute", + start: 27, + end: 41, + key: { + type: "Literal", + start: 27, + end: 33, + value: "type", + raw: "\"type\"" + }, + value: { + type: "Literal", + start: 35, + end: 41, + value: "json", + raw: "\"json\"" + } + } + ] + } + ], + sourceType: "module" + }, + {sourceType: "module", ecmaVersion: 16} +) + +testFail( + 'import "./foo.json" with { 42: "s" };', // Disallow number key + "Unexpected token (1:27)", + {sourceType: "module", ecmaVersion: 16} +) + +testFail( + 'import "./foo.json" with { type: 42 };', // Disallow number value + "Unexpected token (1:33)", + {sourceType: "module", ecmaVersion: 16} +) + +testFail( + 'import "./foo.json" with { type: "json", type: "html" };', // Disallow duplicate key + "Duplicate attribute key 'type' (1:41)", + {sourceType: "module", ecmaVersion: 16} +) + +testFail( + 'import "./foo.json" with { type: "json", , };', + "Unexpected token (1:41)", + {sourceType: "module", ecmaVersion: 16} +) + +testFail( + "export { json } with { type: \"json\" };", + "Unexpected token (1:16)", + {sourceType: "module", ecmaVersion: 16} +) + +testFail( + "import(\"foo.json\", , );", + "Unexpected token (1:19)", + {sourceType: "module", ecmaVersion: 16} +) + +testFail( + "import(\"foo.json\", foo , , );", + "Unexpected token (1:25)", + {sourceType: "module", ecmaVersion: 16} +)