diff --git a/generator/generateReadWordTree.ts b/generator/generateReadWordTree.ts index 80e8959e..a5453abd 100644 --- a/generator/generateReadWordTree.ts +++ b/generator/generateReadWordTree.ts @@ -44,6 +44,7 @@ const CONTEXTUAL_KEYWORDS = [ "abstract", "accessor", "as", + "assert", "asserts", "async", "await", diff --git a/src/TokenProcessor.ts b/src/TokenProcessor.ts index 04c4be28..7588a665 100644 --- a/src/TokenProcessor.ts +++ b/src/TokenProcessor.ts @@ -189,6 +189,24 @@ export default class TokenProcessor { this.replaceTokenTrimmingLeftWhitespace(""); } + /** + * Remove all code until the next }, accounting for balanced braces. + */ + removeBalancedCode(): void { + let braceDepth = 0; + while (!this.isAtEnd()) { + if (this.matches1(tt.braceL)) { + braceDepth++; + } else if (this.matches1(tt.braceR)) { + if (braceDepth === 0) { + return; + } + braceDepth--; + } + this.removeToken(); + } + } + copyExpectedToken(tokenType: TokenType): void { if (this.tokens[this.tokenIndex].type !== tokenType) { throw new Error(`Expected token ${tokenType}`); diff --git a/src/parser/tokenizer/keywords.ts b/src/parser/tokenizer/keywords.ts index b886bf7b..9964f408 100644 --- a/src/parser/tokenizer/keywords.ts +++ b/src/parser/tokenizer/keywords.ts @@ -3,6 +3,7 @@ export enum ContextualKeyword { _abstract, _accessor, _as, + _assert, _asserts, _async, _await, diff --git a/src/parser/tokenizer/readWordTree.ts b/src/parser/tokenizer/readWordTree.ts index 3b947c00..1468b817 100644 --- a/src/parser/tokenizer/readWordTree.ts +++ b/src/parser/tokenizer/readWordTree.ts @@ -45,7 +45,7 @@ export const READ_WORD_TREE = new Int32Array([ // "asser" -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 540, -1, -1, -1, -1, -1, -1, // "assert" - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 567, -1, -1, -1, -1, -1, -1, -1, + ContextualKeyword._assert << 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 567, -1, -1, -1, -1, -1, -1, -1, // "asserts" ContextualKeyword._asserts << 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // "asy" diff --git a/src/parser/traverser/statement.ts b/src/parser/traverser/statement.ts index 24ccfc92..cc8891f2 100644 --- a/src/parser/traverser/statement.ts +++ b/src/parser/traverser/statement.ts @@ -64,6 +64,7 @@ import { parseIdentifier, parseMaybeAssign, parseMethod, + parseObj, parseParenExpression, parsePropertyName, } from "./expression"; @@ -78,6 +79,7 @@ import { eatContextual, expect, expectContextual, + hasPrecedingLineBreak, isContextual, isLineTerminator, semicolon, @@ -996,6 +998,7 @@ function parseExportSpecifiersMaybe(): void { export function parseExportFrom(): void { if (eatContextual(ContextualKeyword._from)) { parseExprAtom(); + maybeParseImportAssertions(); } semicolon(); } @@ -1118,6 +1121,7 @@ export function parseImport(): void { expectContextual(ContextualKeyword._from); parseExprAtom(); } + maybeParseImportAssertions(); semicolon(); } @@ -1191,3 +1195,16 @@ function parseImportSpecifier(): void { parseImportedIdentifier(); } } + +/** + * Parse import assertions like `assert {type: "json"}`. + * + * Import assertions technically have their own syntax, but are always parseable + * as a plain JS object, so just do that for simplicity. + */ +function maybeParseImportAssertions(): void { + if (isContextual(ContextualKeyword._assert) && !hasPrecedingLineBreak()) { + next(); + parseObj(false, false); + } +} diff --git a/src/transformers/CJSImportTransformer.ts b/src/transformers/CJSImportTransformer.ts index 7b29047b..204b99b8 100644 --- a/src/transformers/CJSImportTransformer.ts +++ b/src/transformers/CJSImportTransformer.ts @@ -10,6 +10,7 @@ import getDeclarationInfo, { EMPTY_DECLARATION_INFO, } from "../util/getDeclarationInfo"; import getImportExportSpecifierInfo from "../util/getImportExportSpecifierInfo"; +import {removeMaybeImportAssertion} from "../util/removeMaybeImportAssertion"; import shouldElideDefaultExport from "../util/shouldElideDefaultExport"; import type ReactHotLoaderTransformer from "./ReactHotLoaderTransformer"; import type RootTransformer from "./RootTransformer"; @@ -143,6 +144,7 @@ export default class CJSImportTransformer extends Transformer { this.tokens.replaceTokenTrimmingLeftWhitespace(this.importProcessor.claimImportCode(path)); this.tokens.appendCode(this.importProcessor.claimImportCode(path)); } + removeMaybeImportAssertion(this.tokens); if (this.tokens.matches1(tt.semi)) { this.tokens.removeToken(); } @@ -330,6 +332,7 @@ export default class CJSImportTransformer extends Transformer { ) { this.tokens.removeToken(); this.tokens.removeToken(); + removeMaybeImportAssertion(this.tokens); } return true; } else { @@ -781,6 +784,7 @@ export default class CJSImportTransformer extends Transformer { this.tokens.removeToken(); const path = this.tokens.stringValue(); this.tokens.replaceTokenTrimmingLeftWhitespace(this.importProcessor.claimImportCode(path)); + removeMaybeImportAssertion(this.tokens); } else { // This is a normal named export, so use that. this.tokens.appendCode(exportStatements.join(" ")); @@ -798,6 +802,7 @@ export default class CJSImportTransformer extends Transformer { } const path = this.tokens.stringValue(); this.tokens.replaceTokenTrimmingLeftWhitespace(this.importProcessor.claimImportCode(path)); + removeMaybeImportAssertion(this.tokens); if (this.tokens.matches1(tt.semi)) { this.tokens.removeToken(); } diff --git a/src/transformers/ESMImportTransformer.ts b/src/transformers/ESMImportTransformer.ts index 84e64c4d..9a4db8ab 100644 --- a/src/transformers/ESMImportTransformer.ts +++ b/src/transformers/ESMImportTransformer.ts @@ -11,6 +11,7 @@ import getDeclarationInfo, { } from "../util/getDeclarationInfo"; import getImportExportSpecifierInfo from "../util/getImportExportSpecifierInfo"; import {getNonTypeIdentifiers} from "../util/getNonTypeIdentifiers"; +import {removeMaybeImportAssertion} from "../util/removeMaybeImportAssertion"; import shouldElideDefaultExport from "../util/shouldElideDefaultExport"; import type ReactHotLoaderTransformer from "./ReactHotLoaderTransformer"; import Transformer from "./Transformer"; @@ -102,6 +103,7 @@ export default class ESMImportTransformer extends Transformer { ) { this.tokens.removeToken(); this.tokens.removeToken(); + removeMaybeImportAssertion(this.tokens); } return true; } @@ -145,6 +147,7 @@ export default class ESMImportTransformer extends Transformer { this.tokens.removeToken(); } this.tokens.removeToken(); + removeMaybeImportAssertion(this.tokens); if (this.tokens.matches1(tt.semi)) { this.tokens.removeToken(); } diff --git a/src/util/removeMaybeImportAssertion.ts b/src/util/removeMaybeImportAssertion.ts new file mode 100644 index 00000000..dce8c187 --- /dev/null +++ b/src/util/removeMaybeImportAssertion.ts @@ -0,0 +1,19 @@ +import {ContextualKeyword} from "../parser/tokenizer/keywords"; +import {TokenType as tt} from "../parser/tokenizer/types"; +import type TokenProcessor from "../TokenProcessor"; + +/** + * Starting at a potential `assert` token remove the import assertion if there + * is one. + */ +export function removeMaybeImportAssertion(tokens: TokenProcessor): void { + if (tokens.matches2(tt.name, tt.braceL) && tokens.matchesContextual(ContextualKeyword._assert)) { + // assert + tokens.removeToken(); + // { + tokens.removeToken(); + tokens.removeBalancedCode(); + // } + tokens.removeToken(); + } +} diff --git a/test/imports-test.ts b/test/imports-test.ts index 4841e13d..5139c364 100644 --- a/test/imports-test.ts +++ b/test/imports-test.ts @@ -327,6 +327,29 @@ return obj && obj.__esModule ? obj : { default: obj }; } ); }); + it("removes import assertions", () => { + assertResult( + ` + import DefaultName from 'module1' assert {type: "json"}; + import {namedName} from 'module2' assert {type: "json"}; + import "module3" assert {type: "json"}; + export * from "module4" assert {type: "json"}; + // Arbitrary expressions like these aren't actually allowed right now, but + // exercise the ability to detect matching braces. + import test from "module5" assert {type: {foo: "test"}}; + `, + `"use strict";${ESMODULE_PREFIX}${IMPORT_DEFAULT_PREFIX}${CREATE_STAR_EXPORT_PREFIX} + var _module1 = require('module1'); var _module12 = _interopRequireDefault(_module1); + var _module2 = require('module2'); + require('module3'); + var _module4 = require('module4'); _createStarExport(_module4); + // Arbitrary expressions like these aren't actually allowed right now, but + // exercise the ability to detect matching braces. + var _module5 = require('module5'); var _module52 = _interopRequireDefault(_module5); + `, + ); + }); + it("allows an import statement with no import bindings", () => { assertResult( ` diff --git a/test/tokens-test.ts b/test/tokens-test.ts index 9f852bcd..0de511dc 100644 --- a/test/tokens-test.ts +++ b/test/tokens-test.ts @@ -15,7 +15,17 @@ function assertTokens( ): void { const tokens: Array = parse(code, true, !isFlow, isFlow).tokens; const helpMessage = `Tokens did not match. Starting point with just token types: [${tokens - .map((t) => `{type: tt.${tt[t.type]}}`) + .map((t) => { + let tokenCode = "{"; + tokenCode += `type: tt.${tt[t.type]}`; + if (t.contextualKeyword) { + tokenCode += `, contextualKeyword: ContextualKeyword.${ + ContextualKeyword[t.contextualKeyword] + }`; + } + tokenCode += "}"; + return tokenCode; + }) .join(", ")}]`; assert.strictEqual(tokens.length, expectedTokens.length, helpMessage); const projectedTokens = tokens.map((token, i) => { @@ -506,6 +516,42 @@ describe("tokens", () => { ); }); + it("parses import assertions", () => { + assertTokens( + ` + import foo from "./foo.json" assert {type: "json"}; + export {val} from './foo.js' assert {type: "javascript"}; + `, + [ + {type: tt._import}, + {type: tt.name}, + {type: tt.name, contextualKeyword: ContextualKeyword._from}, + {type: tt.string}, + {type: tt.name, contextualKeyword: ContextualKeyword._assert}, + {type: tt.braceL}, + {type: tt.name, contextualKeyword: ContextualKeyword._type}, + {type: tt.colon}, + {type: tt.string}, + {type: tt.braceR}, + {type: tt.semi}, + {type: tt._export}, + {type: tt.braceL}, + {type: tt.name}, + {type: tt.braceR}, + {type: tt.name, contextualKeyword: ContextualKeyword._from}, + {type: tt.string}, + {type: tt.name, contextualKeyword: ContextualKeyword._assert}, + {type: tt.braceL}, + {type: tt.name, contextualKeyword: ContextualKeyword._type}, + {type: tt.colon}, + {type: tt.string}, + {type: tt.braceR}, + {type: tt.semi}, + {type: tt.eof}, + ], + ); + }); + it("treats single-child JSX as non-static", () => { assertFirstJSXRole("const elem =
Hello
;", JSXRole.OneChild); }); diff --git a/test/typescript-test.ts b/test/typescript-test.ts index 1f70928e..e4ef163b 100644 --- a/test/typescript-test.ts +++ b/test/typescript-test.ts @@ -1,5 +1,6 @@ import type {Options} from "../src/Options"; import { + CREATE_NAMED_EXPORT_FROM_PREFIX, CREATE_REQUIRE_PREFIX, CREATE_STAR_EXPORT_PREFIX, ESMODULE_PREFIX, @@ -3413,4 +3414,52 @@ describe("typescript transform", () => { {transforms: ["typescript", "jsx"]}, ); }); + + it("allows and preserves import assertions when targeting ESM", () => { + assertResult( + ` + import jsonValue from "./file1.json" assert {type: "json"}; + import implicitlyElidedImport from "./file2.json" assert {type: "json"}; + import type explicitlyElidedImport from "./file3.json" assert {type: "json"}; + import "./file4.json" assert {type: "json"}; + export {val} from './file5.json' assert {type: "json"}; + export type {val} from './file6.json' assert {type: "json"}; + console.log(jsonValue); + `, + ` + import jsonValue from "./file1.json" assert {type: "json"}; + + + import "./file4.json" assert {type: "json"}; + export {val} from './file5.json' assert {type: "json"}; + ; + console.log(jsonValue); + `, + {transforms: ["typescript"]}, + ); + }); + + it("removes import assertions when targeting CJS", () => { + assertResult( + ` + import jsonValue from "./file1.json" assert {type: "json"}; + import implicitlyElidedImport from "./file2.json" assert {type: "json"}; + import type explicitlyElidedImport from "./file3.json" assert {type: "json"}; + import "./file4.json" assert {type: "json"}; + export {val} from './file5.json' assert {type: "json"}; + export type {val} from './file6.json' assert {type: "json"}; + console.log(jsonValue); + `, + `"use strict";${ESMODULE_PREFIX}${IMPORT_DEFAULT_PREFIX}${CREATE_NAMED_EXPORT_FROM_PREFIX} + var _file1json = require('./file1.json'); var _file1json2 = _interopRequireDefault(_file1json); + + + require('./file4.json'); + var _file5json = require('./file5.json'); _createNamedExportFrom(_file5json, 'val', 'val'); + ; + console.log(_file1json2.default); + `, + {transforms: ["typescript", "imports"]}, + ); + }); }); diff --git a/website/src/Worker.worker.ts b/website/src/Worker.worker.ts index ffa85138..50c468ae 100644 --- a/website/src/Worker.worker.ts +++ b/website/src/Worker.worker.ts @@ -154,6 +154,7 @@ function runBabel(): {code: string; time: number | null} { "proposal-optional-catch-binding", "proposal-nullish-coalescing-operator", "proposal-optional-chaining", + "syntax-import-assertions", ); }