Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support type-only import/export specifiers #1637

Merged
merged 10 commits into from
Sep 28, 2021
Merged
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