From 4f425873ec4e47ff4233e30a4483d066fde279ec Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Wed, 29 Sep 2021 01:15:33 +0800 Subject: [PATCH] support type-only import/export specifiers (#1637) --- CHANGELOG.md | 32 +++++ internal/js_parser/js_parser.go | 200 ++++++++++++++++++++++----- internal/js_parser/ts_parser_test.go | 74 ++++++++++ 3 files changed, 274 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01071a4dad1..188ca9ac35d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## Unreleased + +* Support TypeScript type-only import/export specifiers ([#1637](https://github.com/evanw/esbuild/pull/1637)) + + This release adds support for a new TypeScript syntax feature in the upcoming version 4.5 of TypeScript. This feature lets you prefix individual imports and exports with the `type` keyword to indicate that they are types instead of values. This helps tools such as esbuild omit them from your source code, and is necessary because esbuild compiles files one-at-a-time and doesn't know at parse time which imports/exports are types and which are values. The new syntax looks like this: + + ```ts + // Input TypeScript code + import { type Foo } from 'foo' + export { type Bar } + + // Output JavaScript code (requires "importsNotUsedAsValues": "preserve" in "tsconfig.json") + import {} from "foo"; + export {}; + ``` + + See [microsoft/TypeScript#45998](https://github.com/microsoft/TypeScript/pull/45998) for full details. From what I understand this is a purely ergonomic improvement since this was already previously possible using a type-only import/export statements like this: + + ```ts + // Input TypeScript code + import type { Foo } from 'foo' + export type { Bar } + import 'foo' + export {} + + // Output JavaScript code (requires "importsNotUsedAsValues": "preserve" in "tsconfig.json") + import "foo"; + export {}; + ``` + + This feature was contributed by [@g-plane](https://github.com/g-plane). + ## 0.13.2 * Fix `export {}` statements with `--tree-shaking=true` ([#1628](https://github.com/evanw/esbuild/issues/1628)) diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index ec51020d56e..e800946b9ad 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -4669,28 +4669,94 @@ func (p *parser) parseImportClause() ([]js_ast.ClauseItem, bool) { originalName := alias p.lexer.Next() - if p.lexer.IsContextualKeyword("as") { - p.lexer.Next() - originalName = p.lexer.Identifier - name = js_ast.LocRef{Loc: p.lexer.Loc(), Ref: p.storeNameInRef(originalName)} - p.lexer.Expect(js_lexer.TIdentifier) - } else if !isIdentifier { - // An import where the name is a keyword must have an alias - p.lexer.ExpectedString("\"as\"") - } + // "import { type xx } from 'mod'" + // "import { type xx as yy } from 'mod'" + // "import { type 'xx' as yy } from 'mod'" + // "import { type as } from 'mod'" + // "import { type as as } from 'mod'" + // "import { type as as as } from 'mod'" + if p.options.ts.Parse && alias == "type" && p.lexer.Token != js_lexer.TComma && p.lexer.Token != js_lexer.TCloseBrace { + if p.lexer.IsContextualKeyword("as") { + p.lexer.Next() + if p.lexer.IsContextualKeyword("as") { + originalName = p.lexer.Identifier + name = js_ast.LocRef{Loc: p.lexer.Loc(), Ref: p.storeNameInRef(originalName)} + p.lexer.Next() - // Reject forbidden names - if isEvalOrArguments(originalName) { - r := js_lexer.RangeOfIdentifier(p.source, name.Loc) - p.log.AddRangeError(&p.tracker, r, fmt.Sprintf("Cannot use %q as an identifier here", originalName)) - } + if p.lexer.Token == js_lexer.TIdentifier { + // "import { type as as as } from 'mod'" + // "import { type as as foo } from 'mod'" + p.lexer.Next() + } else { + // "import { type as as } from 'mod'" + items = append(items, js_ast.ClauseItem{ + Alias: alias, + AliasLoc: aliasLoc, + Name: name, + OriginalName: originalName, + }) + } + } else if p.lexer.Token == js_lexer.TIdentifier { + // "import { type as xxx } from 'mod'" + originalName = p.lexer.Identifier + name = js_ast.LocRef{Loc: p.lexer.Loc(), Ref: p.storeNameInRef(originalName)} + p.lexer.Expect(js_lexer.TIdentifier) - items = append(items, js_ast.ClauseItem{ - Alias: alias, - AliasLoc: aliasLoc, - Name: name, - OriginalName: originalName, - }) + // Reject forbidden names + if isEvalOrArguments(originalName) { + r := js_lexer.RangeOfIdentifier(p.source, name.Loc) + p.log.AddRangeError(&p.tracker, r, fmt.Sprintf("Cannot use %q as an identifier here", originalName)) + } + + items = append(items, js_ast.ClauseItem{ + Alias: alias, + AliasLoc: aliasLoc, + Name: name, + OriginalName: originalName, + }) + } + } else { + isIdentifier := p.lexer.Token == js_lexer.TIdentifier + + // "import { type xx } from 'mod'" + // "import { type xx as yy } from 'mod'" + // "import { type if as yy } from 'mod'" + // "import { type 'xx' as yy } from 'mod'" + p.parseClauseAlias("import") + p.lexer.Next() + + if p.lexer.IsContextualKeyword("as") { + p.lexer.Next() + p.lexer.Expect(js_lexer.TIdentifier) + } else if !isIdentifier { + // An import where the name is a keyword must have an alias + p.lexer.ExpectedString("\"as\"") + } + } + } else { + if p.lexer.IsContextualKeyword("as") { + p.lexer.Next() + originalName = p.lexer.Identifier + name = js_ast.LocRef{Loc: p.lexer.Loc(), Ref: p.storeNameInRef(originalName)} + p.lexer.Expect(js_lexer.TIdentifier) + } else if !isIdentifier { + // An import where the name is a keyword must have an alias + p.lexer.ExpectedString("\"as\"") + } + + // Reject forbidden names + if isEvalOrArguments(originalName) { + r := js_lexer.RangeOfIdentifier(p.source, name.Loc) + p.log.AddRangeError(&p.tracker, r, fmt.Sprintf("Cannot use %q as an identifier here", originalName)) + } + + items = append(items, js_ast.ClauseItem{ + Alias: alias, + AliasLoc: aliasLoc, + Name: name, + OriginalName: originalName, + }) + } if p.lexer.Token != js_lexer.TComma { break @@ -4738,19 +4804,89 @@ func (p *parser) parseExportClause() ([]js_ast.ClauseItem, bool) { } p.lexer.Next() - if p.lexer.IsContextualKeyword("as") { - p.lexer.Next() - alias = p.parseClauseAlias("export") - aliasLoc = p.lexer.Loc() - p.lexer.Next() - } + if p.options.ts.Parse && alias == "type" && p.lexer.Token != js_lexer.TComma && p.lexer.Token != js_lexer.TCloseBrace { + if p.lexer.IsContextualKeyword("as") { + p.lexer.Next() + if p.lexer.IsContextualKeyword("as") { + alias = p.parseClauseAlias("export") + aliasLoc = p.lexer.Loc() + p.lexer.Next() - items = append(items, js_ast.ClauseItem{ - Alias: alias, - AliasLoc: aliasLoc, - Name: name, - OriginalName: originalName, - }) + if p.lexer.Token != js_lexer.TComma && p.lexer.Token != js_lexer.TCloseBrace { + // "export { type as as as }" + // "export { type as as foo }" + // "export { type as as 'foo' }" + p.parseClauseAlias("export") + p.lexer.Next() + } else { + // "export { type as as }" + items = append(items, js_ast.ClauseItem{ + Alias: alias, + AliasLoc: aliasLoc, + Name: name, + OriginalName: originalName, + }) + } + } else if p.lexer.Token != js_lexer.TComma && p.lexer.Token != js_lexer.TCloseBrace { + // "export { type as xxx }" + // "export { type as 'xxx' }" + alias = p.parseClauseAlias("export") + aliasLoc = p.lexer.Loc() + p.lexer.Next() + + items = append(items, js_ast.ClauseItem{ + Alias: alias, + AliasLoc: aliasLoc, + Name: name, + OriginalName: originalName, + }) + } + } else { + // The name can actually be a keyword if we're really an "export from" + // statement. However, we won't know until later. Allow keywords as + // identifiers for now and throw an error later if there's no "from". + // + // // This is fine + // export { type default } from 'path' + // + // // This is a syntax error + // export { type default } + // + if p.lexer.Token != js_lexer.TIdentifier && firstNonIdentifierLoc.Start == 0 { + firstNonIdentifierLoc = p.lexer.Loc() + } + + // "export { type xx }" + // "export { type xx as yy }" + // "export { type xx as if }" + // "export { type default } from 'path'" + // "export { type default as if } from 'path'" + // "export { type xx as 'yy' }" + // "export { type 'xx' } from 'mod'" + p.parseClauseAlias("export") + p.lexer.Next() + + if p.lexer.IsContextualKeyword("as") { + p.lexer.Next() + p.parseClauseAlias("export") + p.lexer.Next() + } + } + } else { + if p.lexer.IsContextualKeyword("as") { + p.lexer.Next() + alias = p.parseClauseAlias("export") + aliasLoc = p.lexer.Loc() + p.lexer.Next() + } + + items = append(items, js_ast.ClauseItem{ + Alias: alias, + AliasLoc: aliasLoc, + Name: name, + OriginalName: originalName, + }) + } if p.lexer.Token != js_lexer.TComma { break diff --git a/internal/js_parser/ts_parser_test.go b/internal/js_parser/ts_parser_test.go index a677a6ee11c..4f7e91afb83 100644 --- a/internal/js_parser/ts_parser_test.go +++ b/internal/js_parser/ts_parser_test.go @@ -1475,6 +1475,20 @@ func TestTSTypeOnlyImport(t *testing.T) { expectPrintedTS(t, "import type = require('type'); type", "const type = require(\"type\");\ntype;\n") expectPrintedTS(t, "import type from 'bar'; type", "import type from \"bar\";\ntype;\n") + expectPrintedTS(t, "import { type } from 'mod'; type", "import { type } from \"mod\";\ntype;\n") + expectPrintedTS(t, "import { x, type foo } from 'mod'; x", "import { x } from \"mod\";\nx;\n") + expectPrintedTS(t, "import { x, type as } from 'mod'; x", "import { x } from \"mod\";\nx;\n") + expectPrintedTS(t, "import { x, type foo as bar } from 'mod'; x", "import { x } from \"mod\";\nx;\n") + expectPrintedTS(t, "import { x, type foo as as } from 'mod'; x", "import { x } from \"mod\";\nx;\n") + expectPrintedTS(t, "import { type as as } from 'mod'; as", "import { type as as } from \"mod\";\nas;\n") + expectPrintedTS(t, "import { type as foo } from 'mod'; foo", "import { type as foo } from \"mod\";\nfoo;\n") + expectPrintedTS(t, "import { type as type } from 'mod'; type", "import { type } from \"mod\";\ntype;\n") + expectPrintedTS(t, "import { x, type as as foo } from 'mod'; x", "import { x } from \"mod\";\nx;\n") + expectPrintedTS(t, "import { x, type as as as } from 'mod'; x", "import { x } from \"mod\";\nx;\n") + expectPrintedTS(t, "import { x, type type as as } from 'mod'; x", "import { x } from \"mod\";\nx;\n") + expectPrintedTS(t, "import { x, \\u0074ype y } from 'mod'; x, y", "import { x } from \"mod\";\nx, y;\n") + expectPrintedTS(t, "import { x, type if as y } from 'mod'; x, y", "import { x } from \"mod\";\nx, y;\n") + expectPrintedTS(t, "import a = b; import c = a.c", "") expectPrintedTS(t, "import c = a.c; import a = b", "") expectPrintedTS(t, "import a = b; import c = a.c; c()", "const a = b;\nconst c = a.c;\nc();\n") @@ -1489,6 +1503,30 @@ func TestTSTypeOnlyImport(t *testing.T) { expectParseErrorTS(t, "import type foo, {foo} from 'bar'", ": error: Expected \"from\" but found \",\"\n") expectParseErrorTS(t, "import type * as foo = require('bar')", ": error: Expected \"from\" but found \"=\"\n") expectParseErrorTS(t, "import type {foo} = require('bar')", ": error: Expected \"from\" but found \"=\"\n") + + expectParseErrorTS(t, "import { type as export } from 'mod'", ": error: Expected \"}\" but found \"export\"\n") + expectParseErrorTS(t, "import { type as as export } from 'mod'", ": error: Expected \"}\" but found \"export\"\n") + expectParseErrorTS(t, "import { type import } from 'mod'", ": error: Expected \"as\" but found \"}\"\n") + expectParseErrorTS(t, "import { type foo bar } from 'mod'", ": error: Expected \"}\" but found \"bar\"\n") + expectParseErrorTS(t, "import { type foo as } from 'mod'", ": error: Expected identifier but found \"}\"\n") + expectParseErrorTS(t, "import { type foo as bar baz } from 'mod'", ": error: Expected \"}\" but found \"baz\"\n") + expectParseErrorTS(t, "import { type as as as as } from 'mod'", ": error: Expected \"}\" but found \"as\"\n") + expectParseErrorTS(t, "import { type \\u0061s x } from 'mod'", ": error: Expected \"}\" but found \"x\"\n") + expectParseErrorTS(t, "import { type x \\u0061s y } from 'mod'", ": error: Expected \"}\" but found \"\\\\u0061s\"\n") + expectParseErrorTS(t, "import { type x as if } from 'mod'", ": error: Expected identifier but found \"if\"\n") + expectParseErrorTS(t, "import { type as if } from 'mod'", ": error: Expected \"}\" but found \"if\"\n") + + // Forbidden names + expectParseErrorTS(t, "import { type as eval } from 'mod'", ": error: Cannot use \"eval\" as an identifier here\n") + expectParseErrorTS(t, "import { type as arguments } from 'mod'", ": error: Cannot use \"arguments\" as an identifier here\n") + + // Arbitrary module namespace identifier names + expectPrintedTS(t, "import { x, type 'y' as z } from 'mod'; x, z", "import { x } from \"mod\";\nx, z;\n") + expectParseErrorTS(t, "import { x, type 'y' } from 'mod'", ": error: Expected \"as\" but found \"}\"\n") + expectParseErrorTS(t, "import { x, type 'y' as } from 'mod'", ": error: Expected identifier but found \"}\"\n") + expectParseErrorTS(t, "import { x, type 'y' as 'z' } from 'mod'", ": error: Expected identifier but found \"'z'\"\n") + expectParseErrorTS(t, "import { x, type as 'y' } from 'mod'", ": error: Expected \"}\" but found \"'y'\"\n") + expectParseErrorTS(t, "import { x, type y as 'z' } from 'mod'", ": error: Expected identifier but found \"'z'\"\n") } func TestTSTypeOnlyExport(t *testing.T) { @@ -1499,6 +1537,42 @@ func TestTSTypeOnlyExport(t *testing.T) { expectPrintedTS(t, "export type {default} from 'bar'", "") expectParseErrorTS(t, "export type {default}", ": error: Expected identifier but found \"default\"\n") + expectPrintedTS(t, "export { type } from 'mod'; type", "export { type } from \"mod\";\ntype;\n") + expectPrintedTS(t, "export { type, as } from 'mod'", "export { type, as } from \"mod\";\n") + expectPrintedTS(t, "export { x, type foo } from 'mod'; x", "export { x } from \"mod\";\nx;\n") + expectPrintedTS(t, "export { x, type as } from 'mod'; x", "export { x } from \"mod\";\nx;\n") + expectPrintedTS(t, "export { x, type foo as bar } from 'mod'; x", "export { x } from \"mod\";\nx;\n") + expectPrintedTS(t, "export { x, type foo as as } from 'mod'; x", "export { x } from \"mod\";\nx;\n") + expectPrintedTS(t, "export { type as as } from 'mod'; as", "export { type as as } from \"mod\";\nas;\n") + expectPrintedTS(t, "export { type as foo } from 'mod'; foo", "export { type as foo } from \"mod\";\nfoo;\n") + expectPrintedTS(t, "export { type as type } from 'mod'; type", "export { type } from \"mod\";\ntype;\n") + expectPrintedTS(t, "export { x, type as as foo } from 'mod'; x", "export { x } from \"mod\";\nx;\n") + expectPrintedTS(t, "export { x, type as as as } from 'mod'; x", "export { x } from \"mod\";\nx;\n") + expectPrintedTS(t, "export { x, type type as as } from 'mod'; x", "export { x } from \"mod\";\nx;\n") + expectPrintedTS(t, "export { x, \\u0074ype y }; let x, y", "export { x };\nlet x, y;\n") + expectPrintedTS(t, "export { x, \\u0074ype y } from 'mod'", "export { x } from \"mod\";\n") + expectPrintedTS(t, "export { x, type if } from 'mod'", "export { x } from \"mod\";\n") + expectPrintedTS(t, "export { x, type y as if }; let x", "export { x };\nlet x;\n") + + expectParseErrorTS(t, "export { type foo bar } from 'mod'", ": error: Expected \"}\" but found \"bar\"\n") + expectParseErrorTS(t, "export { type foo as } from 'mod'", ": error: Expected identifier but found \"}\"\n") + expectParseErrorTS(t, "export { type foo as bar baz } from 'mod'", ": error: Expected \"}\" but found \"baz\"\n") + expectParseErrorTS(t, "export { type as as as as } from 'mod'", ": error: Expected \"}\" but found \"as\"\n") + expectParseErrorTS(t, "export { type \\u0061s x } from 'mod'", ": error: Expected \"}\" but found \"x\"\n") + expectParseErrorTS(t, "export { type x \\u0061s y } from 'mod'", ": error: Expected \"}\" but found \"\\\\u0061s\"\n") + expectParseErrorTS(t, "export { x, type if }", ": error: Expected identifier but found \"if\"\n") + + // Arbitrary module namespace identifier names + expectPrintedTS(t, "export { type as \"\" } from 'mod'", "export { type as \"\" } from \"mod\";\n") + expectPrintedTS(t, "export { type as as \"\" } from 'mod'", "export {} from \"mod\";\n") + expectPrintedTS(t, "export { type x as \"\" } from 'mod'", "export {} from \"mod\";\n") + expectPrintedTS(t, "export { type \"\" as x } from 'mod'", "export {} from \"mod\";\n") + expectPrintedTS(t, "export { type \"\" as \" \" } from 'mod'", "export {} from \"mod\";\n") + expectPrintedTS(t, "export { type \"\" } from 'mod'", "export {} from \"mod\";\n") + expectParseErrorTS(t, "export { type \"\" }", ": error: Expected identifier but found \"\\\"\\\"\"\n") + expectParseErrorTS(t, "export { type \"\" as x }", ": error: Expected identifier but found \"\\\"\\\"\"\n") + expectParseErrorTS(t, "export { type \"\" as \" \" }", ": error: Expected identifier but found \"\\\"\\\"\"\n") + // Named exports should be removed if they don't refer to a local symbol expectPrintedTS(t, "const Foo = {}; export {Foo}", "const Foo = {};\nexport { Foo };\n") expectPrintedTS(t, "type Foo = {}; export {Foo}", "export {};\n")