Skip to content

Commit fc3da57

Browse files
committed
fix #4238: add defer and source import phases
1 parent 492e299 commit fc3da57

File tree

13 files changed

+721
-28
lines changed

13 files changed

+721
-28
lines changed

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,34 @@
22

33
## Unreleased
44

5+
* Parse and print JavaScript imports with an explicit phase ([#4238](https://github.com/evanw/esbuild/issues/4238))
6+
7+
This release adds basic syntax support for the `defer` and `source` import phases in JavaScript:
8+
9+
* `defer`
10+
11+
This is a [stage 3 proposal](https://github.com/tc39/proposal-defer-import-eval) for an upcoming JavaScript feature that will provide one way to eagerly load but lazily initialize imported modules. The imported module is automatically initialized on first use. Support for this syntax will also be part of the upcoming release of [TypeScript 5.9](https://devblogs.microsoft.com/typescript/announcing-typescript-5-9-beta/#support-for-import-defer). The syntax looks like this:
12+
13+
```js
14+
import defer * as foo from "<specifier>";
15+
const bar = await import.defer("<specifier>");
16+
```
17+
18+
Note that this feature deliberately cannot be used with the syntax `import defer foo from "<specifier>"` or `import defer { foo } from "<specifier>"`.
19+
20+
* `source`
21+
22+
This is a [stage 3 proposal](https://github.com/tc39/proposal-source-phase-imports) for an upcoming JavaScript feature that will provide another way to eagerly load but lazily initialize imported modules. The imported module is returned in an uninitialized state. Support for this syntax may or may not be a part of TypeScript 5.9 (see [this issue](https://github.com/microsoft/TypeScript/issues/61216) for details). The syntax looks like this:
23+
24+
```js
25+
import source foo from "<specifier>";
26+
const bar = await import.source("<specifier>");
27+
```
28+
29+
Note that this feature deliberately cannot be used with the syntax `import defer * as foo from "<specifier>"` or `import defer { foo } from "<specifier>"`.
30+
31+
This change only adds support for this syntax. These imports cannot currently be bundled by esbuild. To use these new features with esbuild's bundler, the imported paths must be external to the bundle and the output format must be set to `esm`.
32+
533
* Support optionally emitting absolute paths instead of relative paths ([#338](https://github.com/evanw/esbuild/issues/338), [#2082](https://github.com/evanw/esbuild/issues/2082), [#3023](https://github.com/evanw/esbuild/issues/3023))
634
735
This release introduces the `--abs-paths=` feature which takes a comma-separated list of situations where esbuild should use absolute paths instead of relative paths. There are currently three supported situations: `code` (comments and string literals), `log` (log message text and location info), and `metafile` (the JSON build metadata).

compat-table/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ export const jsFeatures = {
5959
Hashbang: true,
6060
ImportAssertions: true,
6161
ImportAttributes: true,
62+
ImportDefer: true,
6263
ImportMeta: true,
64+
ImportSource: true,
6365
InlineScript: true,
6466
LogicalAssignment: true,
6567
NestedRestBinding: true,

internal/ast/ast.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ func (kind ImportKind) MustResolveToCSS() bool {
7878
return false
7979
}
8080

81+
type ImportPhase uint8
82+
83+
const (
84+
EvaluationPhase ImportPhase = iota
85+
86+
// See: https://github.com/tc39/proposal-defer-import-eval
87+
DeferPhase
88+
89+
// See: https://github.com/tc39/proposal-source-phase-imports
90+
SourcePhase
91+
)
92+
8193
type ImportRecordFlags uint16
8294

8395
const (
@@ -167,6 +179,7 @@ type ImportRecord struct {
167179
CopySourceIndex Index32
168180

169181
Flags ImportRecordFlags
182+
Phase ImportPhase
170183
Kind ImportKind
171184
}
172185

internal/bundler/bundler.go

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -469,11 +469,18 @@ func parseFile(args parseArgs) {
469469
// Special-case glob pattern imports
470470
if record.GlobPattern != nil {
471471
prettyPath := helpers.GlobPatternToString(record.GlobPattern.Parts)
472+
phase := ""
473+
switch record.Phase {
474+
case ast.DeferPhase:
475+
phase = ".defer"
476+
case ast.SourcePhase:
477+
phase = ".source"
478+
}
472479
switch record.GlobPattern.Kind {
473480
case ast.ImportRequire:
474-
prettyPath = fmt.Sprintf("require(%q)", prettyPath)
481+
prettyPath = fmt.Sprintf("require%s(%q)", phase, prettyPath)
475482
case ast.ImportDynamic:
476-
prettyPath = fmt.Sprintf("import(%q)", prettyPath)
483+
prettyPath = fmt.Sprintf("import%s(%q)", phase, prettyPath)
477484
}
478485
if results, msg := args.res.ResolveGlob(absResolveDir, record.GlobPattern.Parts, record.GlobPattern.Kind, prettyPath); results != nil {
479486
if msg != nil {
@@ -482,7 +489,11 @@ func parseFile(args parseArgs) {
482489
if result.globResolveResults == nil {
483490
result.globResolveResults = make(map[uint32]globResolveResult)
484491
}
492+
allAreExternal := true
485493
for key, result := range results {
494+
if !result.PathPair.IsExternal {
495+
allAreExternal = false
496+
}
486497
result.PathPair.Primary.ImportAttributes = attrs
487498
if result.PathPair.HasSecondary() {
488499
result.PathPair.Secondary.ImportAttributes = attrs
@@ -498,6 +509,12 @@ func parseFile(args parseArgs) {
498509
},
499510
exportAlias: record.GlobPattern.ExportAlias,
500511
}
512+
513+
// Forbid bundling of imports with explicit phases
514+
if record.Phase != ast.EvaluationPhase {
515+
reportExplicitPhaseImport(args.log, &tracker, record.Range,
516+
record.Phase, allAreExternal, args.options.OutputFormat)
517+
}
501518
} else {
502519
args.log.AddError(&tracker, record.Range, fmt.Sprintf("Could not resolve %s", prettyPath))
503520
}
@@ -593,6 +610,12 @@ func parseFile(args parseArgs) {
593610
continue
594611
}
595612

613+
// Forbid bundling of imports with explicit phases
614+
if record.Phase != ast.EvaluationPhase {
615+
reportExplicitPhaseImport(args.log, &tracker, record.Range,
616+
record.Phase, entry.resolveResult.PathPair.IsExternal, args.options.OutputFormat)
617+
}
618+
596619
result.resolveResults[importRecordIndex] = entry.resolveResult
597620
}
598621
}
@@ -736,6 +759,30 @@ func parseFile(args parseArgs) {
736759
args.results <- result
737760
}
738761

762+
func reportExplicitPhaseImport(
763+
log logger.Log,
764+
tracker *logger.LineColumnTracker,
765+
r logger.Range,
766+
phase ast.ImportPhase,
767+
isExternal bool,
768+
format config.Format,
769+
) {
770+
var phaseText string
771+
switch phase {
772+
case ast.DeferPhase:
773+
phaseText = "deferred"
774+
case ast.SourcePhase:
775+
phaseText = "source phase"
776+
default:
777+
return
778+
}
779+
if format != config.FormatESModule {
780+
log.AddError(tracker, r, fmt.Sprintf("Bundling %s imports with the %q output format is not supported", phaseText, format.String()))
781+
} else if !isExternal {
782+
log.AddError(tracker, r, fmt.Sprintf("Bundling with %s imports is not supported unless they are external", phaseText))
783+
}
784+
}
785+
739786
func ResolveFailureErrorTextSuggestionNotes(
740787
res *resolver.Resolver,
741788
path string,
@@ -2144,7 +2191,7 @@ func (s *scanner) scanAllDependencies() {
21442191
sourceIndex := s.allocateGlobSourceIndex(result.file.inputFile.Source.Index, uint32(importRecordIndex))
21452192
record.SourceIndex = ast.MakeIndex32(sourceIndex)
21462193
s.results[sourceIndex] = s.generateResultForGlobResolve(sourceIndex, globResults.absPath,
2147-
&result.file.inputFile.Source, record.Range, with, record.GlobPattern.Kind, globResults, record.AssertOrWith)
2194+
&result.file.inputFile.Source, record.Range, with, record.GlobPattern.Kind, record.Phase, globResults, record.AssertOrWith)
21482195
}
21492196
continue
21502197
}
@@ -2192,6 +2239,7 @@ func (s *scanner) generateResultForGlobResolve(
21922239
importRange logger.Range,
21932240
importWith *ast.ImportAssertOrWith,
21942241
kind ast.ImportKind,
2242+
phase ast.ImportPhase,
21952243
result globResolveResult,
21962244
assertions *ast.ImportAssertOrWith,
21972245
) parseResult {
@@ -2245,6 +2293,7 @@ func (s *scanner) generateResultForGlobResolve(
22452293
SourceIndex: sourceIndex,
22462294
AssertOrWith: assertions,
22472295
Kind: kind,
2296+
Phase: phase,
22482297
})
22492298

22502299
switch kind {

0 commit comments

Comments
 (0)