Skip to content

Commit

Permalink
support type-only import/export specifiers (#1637)
Browse files Browse the repository at this point in the history
  • Loading branch information
g-plane committed Sep 28, 2021
1 parent 4adbb29 commit 4f42587
Show file tree
Hide file tree
Showing 3 changed files with 274 additions and 32 deletions.
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
200 changes: 168 additions & 32 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions internal/js_parser/ts_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -1489,6 +1503,30 @@ func TestTSTypeOnlyImport(t *testing.T) {
expectParseErrorTS(t, "import type foo, {foo} from 'bar'", "<stdin>: error: Expected \"from\" but found \",\"\n")
expectParseErrorTS(t, "import type * as foo = require('bar')", "<stdin>: error: Expected \"from\" but found \"=\"\n")
expectParseErrorTS(t, "import type {foo} = require('bar')", "<stdin>: error: Expected \"from\" but found \"=\"\n")

expectParseErrorTS(t, "import { type as export } from 'mod'", "<stdin>: error: Expected \"}\" but found \"export\"\n")
expectParseErrorTS(t, "import { type as as export } from 'mod'", "<stdin>: error: Expected \"}\" but found \"export\"\n")
expectParseErrorTS(t, "import { type import } from 'mod'", "<stdin>: error: Expected \"as\" but found \"}\"\n")
expectParseErrorTS(t, "import { type foo bar } from 'mod'", "<stdin>: error: Expected \"}\" but found \"bar\"\n")
expectParseErrorTS(t, "import { type foo as } from 'mod'", "<stdin>: error: Expected identifier but found \"}\"\n")
expectParseErrorTS(t, "import { type foo as bar baz } from 'mod'", "<stdin>: error: Expected \"}\" but found \"baz\"\n")
expectParseErrorTS(t, "import { type as as as as } from 'mod'", "<stdin>: error: Expected \"}\" but found \"as\"\n")
expectParseErrorTS(t, "import { type \\u0061s x } from 'mod'", "<stdin>: error: Expected \"}\" but found \"x\"\n")
expectParseErrorTS(t, "import { type x \\u0061s y } from 'mod'", "<stdin>: error: Expected \"}\" but found \"\\\\u0061s\"\n")
expectParseErrorTS(t, "import { type x as if } from 'mod'", "<stdin>: error: Expected identifier but found \"if\"\n")
expectParseErrorTS(t, "import { type as if } from 'mod'", "<stdin>: error: Expected \"}\" but found \"if\"\n")

// Forbidden names
expectParseErrorTS(t, "import { type as eval } from 'mod'", "<stdin>: error: Cannot use \"eval\" as an identifier here\n")
expectParseErrorTS(t, "import { type as arguments } from 'mod'", "<stdin>: 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'", "<stdin>: error: Expected \"as\" but found \"}\"\n")
expectParseErrorTS(t, "import { x, type 'y' as } from 'mod'", "<stdin>: error: Expected identifier but found \"}\"\n")
expectParseErrorTS(t, "import { x, type 'y' as 'z' } from 'mod'", "<stdin>: error: Expected identifier but found \"'z'\"\n")
expectParseErrorTS(t, "import { x, type as 'y' } from 'mod'", "<stdin>: error: Expected \"}\" but found \"'y'\"\n")
expectParseErrorTS(t, "import { x, type y as 'z' } from 'mod'", "<stdin>: error: Expected identifier but found \"'z'\"\n")
}

func TestTSTypeOnlyExport(t *testing.T) {
Expand All @@ -1499,6 +1537,42 @@ func TestTSTypeOnlyExport(t *testing.T) {
expectPrintedTS(t, "export type {default} from 'bar'", "")
expectParseErrorTS(t, "export type {default}", "<stdin>: 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'", "<stdin>: error: Expected \"}\" but found \"bar\"\n")
expectParseErrorTS(t, "export { type foo as } from 'mod'", "<stdin>: error: Expected identifier but found \"}\"\n")
expectParseErrorTS(t, "export { type foo as bar baz } from 'mod'", "<stdin>: error: Expected \"}\" but found \"baz\"\n")
expectParseErrorTS(t, "export { type as as as as } from 'mod'", "<stdin>: error: Expected \"}\" but found \"as\"\n")
expectParseErrorTS(t, "export { type \\u0061s x } from 'mod'", "<stdin>: error: Expected \"}\" but found \"x\"\n")
expectParseErrorTS(t, "export { type x \\u0061s y } from 'mod'", "<stdin>: error: Expected \"}\" but found \"\\\\u0061s\"\n")
expectParseErrorTS(t, "export { x, type if }", "<stdin>: 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 \"\" }", "<stdin>: error: Expected identifier but found \"\\\"\\\"\"\n")
expectParseErrorTS(t, "export { type \"\" as x }", "<stdin>: error: Expected identifier but found \"\\\"\\\"\"\n")
expectParseErrorTS(t, "export { type \"\" as \" \" }", "<stdin>: 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")
Expand Down

0 comments on commit 4f42587

Please sign in to comment.