Skip to content

Commit

Permalink
basic support for parsing import attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Sep 14, 2023
1 parent 7ece556 commit 6402f11
Show file tree
Hide file tree
Showing 12 changed files with 298 additions and 123 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,24 @@
this error and leave the unresolved path in the bundle.
```

* Parse and print the `with` keyword in `import` statements

JavaScript was going to have a feature called "import assertions" that adds an `assert` keyword to `import` statements. It looked like this:

```js
import stuff from './stuff.json' assert { type: 'json' }
```

The feature provided a way to assert that the imported file is of a certain type (but was not allowed to affect how the import is interpreted, even though that's how everyone expected it to behave). The feature was fully specified and then actually implemented and shipped in Chrome before the people behind the feature realized that they should allow it to affect how the import is interpreted after all. So import assertions are no longer going to be added to the language.

Instead, the [current proposal](https://github.com/tc39/proposal-import-attributes) is to add a feature called "import attributes" instead that adds a `with` keyword to import statements. It looks like this:

```js
import stuff from './stuff.json' with { type: 'json' }
```

This feature provides a way to affect how the import is interpreted. With this release, esbuild now has preliminary support for parsing and printing this new `with` keyword. The `with` keyword is not yet interpreted by esbuild, however, so bundling code with it will generate a build error. All this release does is allow you to use esbuild to process code containing it (such as removing types from TypeScript code). Note that this syntax is not yet a part of JavaScript and may be removed or altered in the future if the specification changes (which it already has once, as described above). If that happens, esbuild reserves the right to remove or alter its support for this syntax too.

## 0.19.2

* Update how CSS nesting is parsed again
Expand Down
1 change: 1 addition & 0 deletions compat-table/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const jsFeatures = {
Generator: true,
Hashbang: true,
ImportAssertions: true,
ImportAttributes: true,
ImportMeta: true,
InlineScript: true,
LogicalAssignment: true,
Expand Down
33 changes: 24 additions & 9 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@ func (flags ImportRecordFlags) Has(flag ImportRecordFlags) bool {
}

type ImportRecord struct {
Assertions *ImportAssertions
GlobPattern *GlobPattern
Path logger.Path
Range logger.Range
AssertOrWith *ImportAssertOrWith
GlobPattern *GlobPattern
Path logger.Path
Range logger.Range

// If the "HandlesImportErrors" flag is present, then this is the location
// of the error handler. This is used for error reporting.
Expand All @@ -170,24 +170,39 @@ type ImportRecord struct {
Kind ImportKind
}

type ImportAssertions struct {
Entries []AssertEntry
AssertLoc logger.Loc
type AssertOrWithKeyword uint8

const (
AssertKeyword AssertOrWithKeyword = iota
WithKeyword
)

func (kw AssertOrWithKeyword) String() string {
if kw == AssertKeyword {
return "assert"
}
return "with"
}

type ImportAssertOrWith struct {
Entries []AssertOrWithEntry
KeywordLoc logger.Loc
InnerOpenBraceLoc logger.Loc
InnerCloseBraceLoc logger.Loc
OuterOpenBraceLoc logger.Loc
OuterCloseBraceLoc logger.Loc
Keyword AssertOrWithKeyword
}

type AssertEntry struct {
type AssertOrWithEntry struct {
Key []uint16 // An identifier or a string
Value []uint16 // Always a string
KeyLoc logger.Loc
ValueLoc logger.Loc
PreferQuotedKey bool
}

func FindAssertion(assertions []AssertEntry, name string) *AssertEntry {
func FindAssertOrWithEntry(assertions []AssertOrWithEntry, name string) *AssertOrWithEntry {
for _, assertion := range assertions {
if helpers.UTF16EqualsString(assertion.Key, name) {
return &assertion
Expand Down
21 changes: 14 additions & 7 deletions internal/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,13 @@ func parseFile(args parseArgs) {
continue
}

// TODO: Implement bundling with import attributes
if record.AssertOrWith != nil && record.AssertOrWith.Keyword == ast.WithKeyword {
args.log.AddError(&tracker, js_lexer.RangeOfIdentifier(result.file.inputFile.Source, record.AssertOrWith.KeywordLoc),
"Bundling with import attributes is not currently supported")
continue
}

// Special-case glob pattern imports
if record.GlobPattern != nil {
prettyPath := helpers.GlobPatternToString(record.GlobPattern.Parts)
Expand Down Expand Up @@ -1962,7 +1969,7 @@ func (s *scanner) scanAllDependencies() {
sourceIndex := s.allocateGlobSourceIndex(result.file.inputFile.Source.Index, uint32(importRecordIndex))
record.SourceIndex = ast.MakeIndex32(sourceIndex)
s.results[sourceIndex] = s.generateResultForGlobResolve(sourceIndex, globResults.absPath,
&result.file.inputFile.Source, record.Range, record.GlobPattern.Kind, globResults, record.Assertions)
&result.file.inputFile.Source, record.Range, record.GlobPattern.Kind, globResults, record.AssertOrWith)
}
continue
}
Expand Down Expand Up @@ -2010,7 +2017,7 @@ func (s *scanner) generateResultForGlobResolve(
importRange logger.Range,
kind ast.ImportKind,
result globResolveResult,
assertions *ast.ImportAssertions,
assertions *ast.ImportAssertOrWith,
) parseResult {
keys := make([]string, 0, len(result.resolveResults))
for key := range result.resolveResults {
Expand Down Expand Up @@ -2057,10 +2064,10 @@ func (s *scanner) generateResultForGlobResolve(

resolveResults = append(resolveResults, &resolveResult)
importRecords = append(importRecords, ast.ImportRecord{
Path: path,
SourceIndex: sourceIndex,
Assertions: assertions,
Kind: kind,
Path: path,
SourceIndex: sourceIndex,
AssertOrWith: assertions,
Kind: kind,
})

switch kind {
Expand Down Expand Up @@ -2203,7 +2210,7 @@ func (s *scanner) processScannedFiles(entryPointMeta []graph.EntryPoint) []scann
s.log.AddErrorWithNotes(&tracker, record.Range,
fmt.Sprintf("The file %q was loaded with the %q loader", otherFile.inputFile.Source.PrettyPath, config.LoaderToString[otherFile.inputFile.Loader]),
[]logger.MsgData{
tracker.MsgData(js_lexer.RangeOfImportAssertion(result.file.inputFile.Source, *ast.FindAssertion(record.Assertions.Entries, "type")),
tracker.MsgData(js_lexer.RangeOfImportAssertOrWith(result.file.inputFile.Source, *ast.FindAssertOrWithEntry(record.AssertOrWith.Entries, "type")),
"This import assertion requires the loader to be \"json\" instead:"),
{Text: "You need to either reconfigure esbuild to ensure that the loader for this file is \"json\" or you need to remove this import assertion."}})
}
Expand Down
20 changes: 20 additions & 0 deletions internal/bundler_tests/bundler_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1537,3 +1537,23 @@ func TestLoaderCopyWithInjectedFileBundle(t *testing.T) {
},
})
}

func TestLoaderBundleWithImportAttributes(t *testing.T) {
loader_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import x from "./import.js"
import y from "./import.js" with { type: 'json' }
console.log(x === y)
`,
"/import.js": `{}`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.js",
},
expectedScanLog: `entry.js: ERROR: Bundling with import attributes is not currently supported
`,
})
}
3 changes: 3 additions & 0 deletions internal/compat/js_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ const (
Generator
Hashbang
ImportAssertions
ImportAttributes
ImportMeta
InlineScript
LogicalAssignment
Expand Down Expand Up @@ -149,6 +150,7 @@ var StringToJSFeature = map[string]JSFeature{
"generator": Generator,
"hashbang": Hashbang,
"import-assertions": ImportAssertions,
"import-attributes": ImportAttributes,
"import-meta": ImportMeta,
"inline-script": InlineScript,
"logical-assignment": LogicalAssignment,
Expand Down Expand Up @@ -504,6 +506,7 @@ var jsTable = map[JSFeature]map[Engine][]versionRange{
Edge: {{start: v{91, 0, 0}}},
Node: {{start: v{16, 14, 0}}},
},
ImportAttributes: {},
ImportMeta: {
Chrome: {{start: v{64, 0, 0}}},
Deno: {{start: v{1, 0, 0}}},
Expand Down
2 changes: 1 addition & 1 deletion internal/graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ func CloneLinkerGraph(
// For example, "import('./foo.json', { assert: { type: 'json' } })"
// will likely be converted into an import of a JavaScript file and
// leaving the import assertion there will prevent it from working.
record.Assertions = nil
record.AssertOrWith = nil
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions internal/js_lexer/js_lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -673,9 +673,9 @@ func RangeOfIdentifier(source logger.Source, loc logger.Loc) logger.Range {
return source.RangeOfString(loc)
}

func RangeOfImportAssertion(source logger.Source, assertion ast.AssertEntry) logger.Range {
loc := RangeOfIdentifier(source, assertion.KeyLoc).Loc
return logger.Range{Loc: loc, Len: source.RangeOfString(assertion.ValueLoc).End() - loc.Start}
func RangeOfImportAssertOrWith(source logger.Source, assertOrWith ast.AssertOrWithEntry) logger.Range {
loc := RangeOfIdentifier(source, assertOrWith.KeyLoc).Loc
return logger.Range{Loc: loc, Len: source.RangeOfString(assertOrWith.ValueLoc).End() - loc.Start}
}

func (lexer *Lexer) ExpectJSXElementChild(token T) {
Expand Down
Loading

0 comments on commit 6402f11

Please sign in to comment.