Skip to content

Commit

Permalink
add basic support for import assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Nov 20, 2023
1 parent 6b9737a commit 2dad830
Show file tree
Hide file tree
Showing 15 changed files with 476 additions and 44 deletions.
6 changes: 6 additions & 0 deletions cmd/esbuild/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,11 @@ func (service *serviceType) convertPlugins(key int, jsPlugins interface{}, activ
return result, nil
}

with := make(map[string]interface{})
for k, v := range args.With {
with[k] = v
}

response, ok := service.sendRequest(map[string]interface{}{
"command": "on-load",
"key": key,
Expand All @@ -1063,6 +1068,7 @@ func (service *serviceType) convertPlugins(key int, jsPlugins interface{}, activ
"namespace": args.Namespace,
"suffix": args.Suffix,
"pluginData": args.PluginData,
"with": with,
}).(map[string]interface{})
if !ok {
return result, errors.New("The service was stopped")
Expand Down
208 changes: 172 additions & 36 deletions internal/bundler/bundler.go

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions internal/bundler_tests/bundler_default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8032,6 +8032,26 @@ func TestMetafileVeryLongExternalPaths(t *testing.T) {
})
}

func TestMetafileImportWithTypeJSON(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/project/entry.js": `
import a from './data.json'
import b from './data.json' assert { type: 'json' }
import c from './data.json' with { type: 'json' }
x = [a, b, c]
`,
"/project/data.json": `{"some": "data"}`,
},
entryPaths: []string{"/project/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
NeedsMetafile: true,
},
})
}

func TestCommentPreservation(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
Expand Down
68 changes: 62 additions & 6 deletions internal/bundler_tests/bundler_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,7 @@ func TestAssertTypeJSONWrongLoader(t *testing.T) {
files: map[string]string{
"/entry.js": `
import foo from './foo.json' assert { type: 'json' }
console.log(foo)
`,
"/foo.json": `{}`,
},
Expand All @@ -1286,6 +1287,62 @@ NOTE: You need to either reconfigure esbuild to ensure that the loader for this
})
}

func TestWithTypeJSONOverrideLoader(t *testing.T) {
loader_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import foo from './foo.js' with { type: 'json' }
console.log(foo)
`,
"/foo.js": `{ "this is json not js": true }`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
},
})
}

func TestWithBadType(t *testing.T) {
loader_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import foo from './foo.json' with { type: '' }
import bar from './foo.json' with { type: 'garbage' }
console.log(bar)
`,
"/foo.json": `{}`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
},
expectedScanLog: `entry.js: ERROR: Importing with a type attribute of "" is not supported
entry.js: ERROR: Importing with a type attribute of "garbage" is not supported
`,
})
}

func TestWithBadAttribute(t *testing.T) {
loader_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import foo from './foo.json' with { '': 'json' }
import bar from './foo.json' with { garbage: 'json' }
console.log(bar)
`,
"/foo.json": `{}`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
},
expectedScanLog: `entry.js: ERROR: Importing with the "" attribute is not supported
entry.js: ERROR: Importing with the "garbage" attribute is not supported
`,
})
}

func TestEmptyLoaderJS(t *testing.T) {
loader_suite.expectBundled(t, bundled{
files: map[string]string{
Expand Down Expand Up @@ -1542,18 +1599,17 @@ 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 x from "./data.json"
import y from "./data.json" assert { type: 'json' }
import z from "./data.json" with { type: 'json' }
console.log(x === y, x !== z)
`,
"/import.js": `{}`,
"/data.json": `{ "works": true }`,
},
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
`,
})
}
71 changes: 71 additions & 0 deletions internal/bundler_tests/snapshots/snapshots_default.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4148,6 +4148,77 @@ var shared_default = 123;
// e39.js
console.log(shared_default);

================================================================================
TestMetafileImportWithTypeJSON
---------- /out/entry.js ----------
// project/data.json
var data_default = { some: "data" };

// project/data.json with { type: 'json' }
var data_default2 = { some: "data" };

// project/entry.js
x = [data_default, data_default, data_default2];
---------- metafile.json ----------
{
"inputs": {
"project/data.json": {
"bytes": 16,
"imports": []
},
"project/data.json with { type: 'json' }": {
"bytes": 16,
"imports": [],
"with": {
"type": "json"
}
},
"project/entry.js": {
"bytes": 164,
"imports": [
{
"path": "project/data.json",
"kind": "import-statement",
"original": "./data.json"
},
{
"path": "project/data.json",
"kind": "import-statement",
"original": "./data.json"
},
{
"path": "project/data.json with { type: 'json' }",
"kind": "import-statement",
"original": "./data.json",
"with": {
"type": "json"
}
}
],
"format": "esm"
}
},
"outputs": {
"out/entry.js": {
"imports": [],
"exports": [],
"entryPoint": "project/entry.js",
"inputs": {
"project/data.json": {
"bytesInOutput": 37
},
"project/data.json with { type: 'json' }": {
"bytesInOutput": 38
},
"project/entry.js": {
"bytesInOutput": 49
}
},
"bytes": 210
}
}
}

================================================================================
TestMetafileNoBundle
---------- /out/entry.js ----------
Expand Down
21 changes: 21 additions & 0 deletions internal/bundler_tests/snapshots/snapshots_loader.txt
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,18 @@ var y_default = "eQ==";
var x_b64 = require_x();
console.log(x_b64, y_default);

================================================================================
TestLoaderBundleWithImportAttributes
---------- /out.js ----------
// data.json
var data_default = { works: true };

// data.json with { type: 'json' }
var data_default2 = { works: true };

// entry.js
console.log(data_default === data_default, data_default !== data_default2);

================================================================================
TestLoaderCopyEntryPointAdvanced
---------- /out/xyz-DYPYXS7B.copy ----------
Expand Down Expand Up @@ -978,3 +990,12 @@ var require_test = __commonJS({

// entry.js
console.log(require_test());

================================================================================
TestWithTypeJSONOverrideLoader
---------- entry.js ----------
// foo.js
var foo_default = { "this is json not js": true };

// entry.js
console.log(foo_default);
16 changes: 15 additions & 1 deletion internal/js_lexer/js_lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,21 @@ func RangeOfIdentifier(source logger.Source, loc logger.Loc) logger.Range {
return source.RangeOfString(loc)
}

func RangeOfImportAssertOrWith(source logger.Source, assertOrWith ast.AssertOrWithEntry) logger.Range {
type KeyOrValue uint8

const (
KeyRange KeyOrValue = iota
ValueRange
KeyAndValueRange
)

func RangeOfImportAssertOrWith(source logger.Source, assertOrWith ast.AssertOrWithEntry, which KeyOrValue) logger.Range {
if which == KeyRange {
return RangeOfIdentifier(source, assertOrWith.KeyLoc)
}
if which == ValueRange {
return source.RangeOfString(assertOrWith.ValueLoc)
}
loc := RangeOfIdentifier(source, assertOrWith.KeyLoc).Loc
return logger.Range{Loc: loc, Len: source.RangeOfString(assertOrWith.ValueLoc).End() - loc.Start}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -1989,7 +1989,7 @@ func (p *parser) checkForLegacyOctalLiteral(e js_ast.E) {

func (p *parser) notesForAssertTypeJSON(record *ast.ImportRecord, alias string) []logger.MsgData {
return []logger.MsgData{p.tracker.MsgData(
js_lexer.RangeOfImportAssertOrWith(p.source, *ast.FindAssertOrWithEntry(record.AssertOrWith.Entries, "type")),
js_lexer.RangeOfImportAssertOrWith(p.source, *ast.FindAssertOrWithEntry(record.AssertOrWith.Entries, "type"), js_lexer.KeyAndValueRange),
"This is considered an import of a standard JSON module because of the import assertion here:"),
{Text: fmt.Sprintf("You can either keep the import assertion and only use the \"default\" import, "+
"or you can remove the import assertion and use the %q import (which is non-standard behavior).", alias)}}
Expand Down
58 changes: 58 additions & 0 deletions internal/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package logger
// default.

import (
"encoding/binary"
"fmt"
"os"
"runtime"
Expand Down Expand Up @@ -248,9 +249,66 @@ type Path struct {
// the output. This is supported by other bundlers, so we also support this.
IgnoredSuffix string

// Import attributes (the "with" keyword after an import) can affect path
// resolution. In other words, two paths in the same file that are otherwise
// equal but that have different import attributes may resolve to different
// paths.
ImportAttributes ImportAttributes

Flags PathFlags
}

// We rely on paths as map keys. Go doesn't support custom hash codes and
// only implements hash codes for certain types. In particular, hash codes
// are implemented for strings but not for arrays of strings. So we have to
// pack these import attributes into a string.
type ImportAttributes struct {
packedData string
}

type ImportAttribute struct {
Key string
Value string
}

// This returns a sorted array instead of a map to make determinism easier
func (attrs ImportAttributes) Decode() (result []ImportAttribute) {
if attrs.packedData == "" {
return nil
}
bytes := []byte(attrs.packedData)
for len(bytes) > 0 {
kn := 4 + binary.LittleEndian.Uint32(bytes[:4])
k := string(bytes[4:kn])
bytes = bytes[kn:]
vn := 4 + binary.LittleEndian.Uint32(bytes[:4])
v := string(bytes[4:vn])
bytes = bytes[vn:]
result = append(result, ImportAttribute{Key: k, Value: v})
}
return result
}

func EncodeImportAttributes(value map[string]string) ImportAttributes {
keys := make([]string, 0, len(value))
for k := range value {
keys = append(keys, k)
}
sort.Strings(keys)
var sb strings.Builder
var n [4]byte
for _, k := range keys {
v := value[k]
binary.LittleEndian.PutUint32(n[:], uint32(len(k)))
sb.Write(n[:])
sb.WriteString(k)
binary.LittleEndian.PutUint32(n[:], uint32(len(v)))
sb.Write(n[:])
sb.WriteString(v)
}
return ImportAttributes{packedData: sb.String()}
}

type PathFlags uint8

const (
Expand Down
1 change: 1 addition & 0 deletions lib/shared/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1432,6 +1432,7 @@ let handlePlugins = async (
namespace: request.namespace,
suffix: request.suffix,
pluginData: details.load(request.pluginData),
with: request.with,
})

if (result != null) {
Expand Down
1 change: 1 addition & 0 deletions lib/shared/stdio_protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export interface OnLoadRequest {
namespace: string
suffix: string
pluginData: number
with: Record<string, string>
}

export interface OnLoadResponse {
Expand Down
3 changes: 3 additions & 0 deletions lib/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ export interface OnLoadArgs {
namespace: string
suffix: string
pluginData: any
with: Record<string, string>
}

/** Documentation: https://esbuild.github.io/plugins/#on-load-results */
Expand Down Expand Up @@ -467,8 +468,10 @@ export interface Metafile {
kind: ImportKind
external?: boolean
original?: string
with?: Record<string, string>
}[]
format?: 'cjs' | 'esm'
with?: Record<string, string>
}
}
outputs: {
Expand Down
Loading

0 comments on commit 2dad830

Please sign in to comment.