Skip to content

Commit

Permalink
First pass at implementing import.meta.resolve(), still not working.
Browse files Browse the repository at this point in the history
`make js-api-tests` fails to include the worker file in the build.

This is an adaptation of evanw#2470 , for evanw#2866
  • Loading branch information
lgarron committed Oct 13, 2023
1 parent b94c7a0 commit e4b6e42
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 4 deletions.
5 changes: 5 additions & 0 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const (
// An "import()" expression with a string argument
ImportDynamic

// An "import.meta.resolve()" expression with a string argument
ImportMetaResolve

// A call to "require.resolve()"
ImportRequireResolve

Expand All @@ -49,6 +52,8 @@ func (kind ImportKind) StringForMetafile() string {
return "dynamic-import"
case ImportRequireResolve:
return "require-resolve"
case ImportMetaResolve:
return "dynamic-import"
case ImportAt:
return "import-rule"
case ImportComposesFrom:
Expand Down
7 changes: 6 additions & 1 deletion internal/js_ast/js_ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ func (*ERequireString) isExpr() {}
func (*ERequireResolveString) isExpr() {}
func (*EImportString) isExpr() {}
func (*EImportCall) isExpr() {}
func (*EImportMetaResolve) isExpr() {}

type EArray struct {
Items []Expr
Expand Down Expand Up @@ -855,7 +856,6 @@ type ERequireResolveString struct {
ImportRecordIndex uint32
CloseParenLoc logger.Loc
}

type EImportString struct {
ImportRecordIndex uint32
CloseParenLoc logger.Loc
Expand All @@ -867,6 +867,11 @@ type EImportCall struct {
CloseParenLoc logger.Loc
}

type EImportMetaResolve struct {
Expr Expr
CloseParenLoc logger.Loc
}

type Stmt struct {
Data S
Loc logger.Loc
Expand Down
93 changes: 91 additions & 2 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package js_parser
import (
"fmt"
"math"
"os"
"regexp"
"sort"
"strings"
Expand Down Expand Up @@ -236,6 +237,7 @@ type parser struct {

esmImportStatementKeyword logger.Range
esmImportMeta logger.Range
esmImportMetaResolve logger.Range
esmExportKeyword logger.Range
enclosingClassKeyword logger.Range
topLevelAwaitKeyword logger.Range
Expand Down Expand Up @@ -3936,8 +3938,61 @@ func (p *parser) parseImportExpr(loc logger.Loc, level js_ast.L) js_ast.Expr {
if !p.lexer.IsContextualKeyword("meta") {
p.lexer.ExpectedString("\"meta\"")
}
p.esmImportMeta = logger.Range{Loc: loc, Len: p.lexer.Range().End() - loc.Start}
loggerRange := logger.Range{Loc: loc, Len: p.lexer.Range().End() - loc.Start}
p.lexer.Next()
// Handle `import.meta.resolve("[import path]")` (https:/evanw/esbuild/issues/2866)
if p.lexer.Token == js_lexer.TDot {
fmt.Fprintf(os.Stderr, "dot next!")
p.lexer.Next()
if p.lexer.IsContextualKeyword("resolve") {
fmt.Fprintf(os.Stderr, "resolve!!")
p.lexer.Next()
p.lexer.Expect(js_lexer.TOpenParen)
fmt.Fprintf(os.Stderr, "paren!!!")
value := p.parseExpr(js_ast.LLowest)
closeParenLoc := p.saveExprCommentsHere()
p.lexer.Expect(js_lexer.TCloseParen)
p.esmImportMetaResolve = loggerRange
return js_ast.Expr{Loc: loc, Data: &js_ast.EImportMetaResolve{
Expr: value,
CloseParenLoc: closeParenLoc,
}}
}
p.esmImportMeta = loggerRange
}

// isImportGraphURL := false

// e.Target = p.visitExpr(e.Target)
// if id, ok := e.Target.Data.(*js_ast.EIdentifier); ok {
// symbol := p.symbols[id.Ref.InnerIndex]
// if symbol.OriginalName == "URL" && len(e.Args) == 2 {
// if s, ok := e.Args[0].Data.(*js_ast.EString); ok {
// if eDot, ok := e.Args[1].Data.(*js_ast.EDot); ok {
// if _, ok := eDot.Target.Data.(*js_ast.EImportMeta); ok {
// if eDot.Name == "url" {
// // TODO: This should:
// // - Include more file types.
// // - Understand the relative JS and TS files even if the file extension is not provided (like in TypeScript).
// // - Check if the actual file exists in the import graph?
// isInImportGraph := strings.HasSuffix(helpers.UTF16ToString(s.Value), ".js") || strings.HasSuffix(helpers.UTF16ToString(s.Value), ".ts")

// if isInImportGraph {
// isImportGraphURL = true
// }
// }
// }
// }
// }
// }
// }
// if isImportGraphURL {
// s, _ := e.Args[0].Data.(*js_ast.EString)
// importRecordIndex := p.addImportRecord(ast.ImportDynamic, e.Args[0].Loc, helpers.UTF16ToString(s.Value), nil)
// p.importRecordsForCurrentPart = append(p.importRecordsForCurrentPart, importRecordIndex)
// e.Args[0] = js_ast.Expr{Loc: e.Args[0].Loc, Data: &js_ast.ERelativeURL{
// ImportRecordIndex: importRecordIndex,
// }}
return js_ast.Expr{Loc: loc, Data: &js_ast.EImportMeta{RangeLen: p.esmImportMeta.Len}}
}

Expand Down Expand Up @@ -9233,6 +9288,21 @@ func (p *parser) substituteSingleUseSymbolInExpr(
return expr, substituteContinue
}

// TODO
case *js_ast.EImportMetaResolve:
if value, status := p.substituteSingleUseSymbolInExpr(e.Expr, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
e.Expr = value
return expr, status
}

// The "import()" expression has side effects but the side effects are
// always asynchronous so there is no way for the side effects to modify
// the replacement value. So it's ok to reorder the replacement value
// past the "import()" expression assuming everything else checks out.
if replacementCanBeRemoved && js_ast.ExprCanBeRemovedIfUnused(e.Expr, p.isUnbound) {
return expr, substituteContinue
}

case *js_ast.EUnary:
switch e.Op {
case js_ast.UnOpPreInc, js_ast.UnOpPostInc, js_ast.UnOpPreDec, js_ast.UnOpPostDec, js_ast.UnOpDelete:
Expand Down Expand Up @@ -11712,6 +11782,11 @@ func (p *parser) isDotOrIndexDefineMatch(expr js_ast.Expr, parts []string) bool
// Allow matching on "import.meta"
return len(parts) == 2 && parts[0] == "import" && parts[1] == "meta"

// TODO?
case *js_ast.EImportMetaResolve:
// Allow matching on "import.meta.resolve"
return len(parts) == 3 && parts[0] == "import" && parts[1] == "meta" && parts[2] == "resolve"

case *js_ast.EIdentifier:
// The last expression must be an identifier
if len(parts) == 1 {
Expand Down Expand Up @@ -14203,6 +14278,14 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
}}
}), exprOut{}

case *js_ast.EImportMetaResolve:
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EImportMetaResolve{
Expr: p.visitExpr(e.Expr),
CloseParenLoc: e.CloseParenLoc,
}}, exprOut{}

// TODO

case *js_ast.ECall:
p.callTarget = e.Target.Data

Expand Down Expand Up @@ -17011,6 +17094,7 @@ const (
whyESMUnknown whyESM = iota
whyESMExportKeyword
whyESMImportMeta
whyESMImportMetaResolve
whyESMTopLevelAwait
whyESMFileMJS
whyESMFileMTS
Expand All @@ -17030,6 +17114,10 @@ func (p *parser) whyESModule() (whyESM, []logger.MsgData) {
return whyESMImportMeta, []logger.MsgData{p.tracker.MsgData(p.esmImportMeta,
because+" of the use of \"import.meta\" here:")}

case p.esmImportMetaResolve.Len > 0:
return whyESMImportMetaResolve, []logger.MsgData{p.tracker.MsgData(p.esmImportMeta,
because+" of the use of \"import.meta.resolve\" here:")}

case p.topLevelAwaitKeyword.Len > 0:
return whyESMTopLevelAwait, []logger.MsgData{p.tracker.MsgData(p.topLevelAwaitKeyword,
because+" of the top-level \"await\" keyword here:")}
Expand Down Expand Up @@ -17069,6 +17157,7 @@ func (p *parser) prepareForVisitPass() {
p.isFileConsideredToHaveESMExports =
p.esmExportKeyword.Len > 0 ||
p.esmImportMeta.Len > 0 ||
p.esmImportMetaResolve.Len > 0 ||
p.topLevelAwaitKeyword.Len > 0 ||
p.options.moduleTypeData.Type.IsESM()
p.isFileConsideredESM =
Expand Down Expand Up @@ -17523,7 +17612,7 @@ func (p *parser) toAST(before, parts, after []js_ast.Part, hashbang string, dire
usesExportsRef := p.symbols[p.exportsRef.InnerIndex].UseCountEstimate > 0
usesModuleRef := p.symbols[p.moduleRef.InnerIndex].UseCountEstimate > 0

if p.esmExportKeyword.Len > 0 || p.esmImportMeta.Len > 0 || p.topLevelAwaitKeyword.Len > 0 {
if p.esmExportKeyword.Len > 0 || p.esmImportMeta.Len > 0 || p.esmImportMetaResolve.Len > 0 || p.topLevelAwaitKeyword.Len > 0 {
exportsKind = js_ast.ExportsESM
} else if usesExportsRef || usesModuleRef || p.hasTopLevelReturn {
exportsKind = js_ast.ExportsCommonJS
Expand Down
30 changes: 30 additions & 0 deletions internal/js_printer/js_printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2497,6 +2497,36 @@ func (p *printer) printExpr(expr js_ast.Expr, level js_ast.L, flags printExprFla
p.print(")")
}

case *js_ast.EImportMetaResolve:
// Just omit import assertions if they aren't supported
isMultiLine := !p.options.MinifyWhitespace &&
(p.willPrintExprCommentsAtLoc(e.Expr.Loc) ||
p.willPrintExprCommentsAtLoc(e.CloseParenLoc))
wrap := level >= js_ast.LNew || (flags&forbidCall) != 0
if wrap {
p.print("(")
}
p.printSpaceBeforeIdentifier()
p.addSourceMapping(expr.Loc)
p.print("import.meta.resolve(")
if isMultiLine {
p.printNewline()
p.options.Indent++
p.printIndent()
}
p.printExpr(e.Expr, js_ast.LComma, 0)

if isMultiLine {
p.printNewline()
p.printExprCommentsAfterCloseTokenAtLoc(e.CloseParenLoc)
p.options.Indent--
p.printIndent()
}
p.print(")")
if wrap {
p.print(")")
}

case *js_ast.EDot:
wrap := false
if e.OptionalChain == js_ast.OptionalChainNone {
Expand Down
2 changes: 1 addition & 1 deletion internal/resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2269,7 +2269,7 @@ func (r resolverQuery) esmResolveAlgorithm(esmPackageName string, esmPackageSubp
// The condition set is determined by the kind of import
conditions := r.esmConditionsDefault
switch r.kind {
case ast.ImportStmt, ast.ImportDynamic:
case ast.ImportStmt, ast.ImportDynamic, ast.ImportMetaResolve:
conditions = r.esmConditionsImport
case ast.ImportRequire, ast.ImportRequireResolve:
conditions = r.esmConditionsRequire
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/api_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -1882,6 +1882,8 @@ func importKindToResolveKind(kind ast.ImportKind) ResolveKind {
return ResolveJSRequireCall
case ast.ImportDynamic:
return ResolveJSDynamicImport
case ast.ImportMetaResolve:
return ResolveJSDynamicImport
case ast.ImportRequireResolve:
return ResolveJSRequireResolve
case ast.ImportAt:
Expand Down
46 changes: 46 additions & 0 deletions scripts/js-api-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,52 @@ body {
assert.deepStrictEqual(json.outputs[fileKey].inputs, { [makePath(file)]: { bytesInOutput: 14 } })
},

async metafileSplittingRelativeNewURL({ esbuild, testDir }) {
const entry = path.join(testDir, 'entry.js')
const worker = path.join(testDir, 'worker.js')
const outdir = path.join(testDir, 'out')
await writeFileAsync(entry, `
const workerURL = import.meta.resolve("./worker.js");
const blob = new Blob([\`import \${JSON.stringify(workerURL)};\`], { type: "text/javascript" });
new Worker(URL.createObjectURL(blob), { type: "module" }).addEventListener("message", console.log)
`)
await writeFileAsync(worker, `
postMessage("hello!")
`)
const result = await esbuild.build({
entryPoints: [entry],
bundle: true,
outdir,
metafile: true,
format: 'esm',
splitting: true,
})

const json = result.metafile

console.log(json)
assert.strictEqual(Object.keys(json.inputs).length, 2)
assert.strictEqual(Object.keys(json.outputs).length, 2)
const cwd = process.cwd()
const makeOutPath = basename => path.relative(cwd, path.join(outdir, basename)).split(path.sep).join('/')
const makeInPath = pathname => path.relative(cwd, pathname).split(path.sep).join('/')

// Check metafile
const inEntry = makeInPath(entry);
const inWorker = makeInPath(worker);
const outEntry = makeOutPath(path.basename(entry));
const outWorkerChunk = makeOutPath('worker-UGPIWIMF.js');

assert.deepStrictEqual(json.inputs[inEntry], { bytes: 275, imports: [{ path: inWorker, kind: 'dynamic-import' }] })
assert.deepStrictEqual(json.inputs[inWorker], { bytes: 33, imports: [] })

assert.deepStrictEqual(json.outputs[outEntry].imports, [{ path: outWorkerChunk, kind: 'dynamic-import' }])
assert.deepStrictEqual(json.outputs[outWorkerChunk].imports, [])

assert.deepStrictEqual(json.outputs[outEntry].inputs, { [inEntry]: { bytesInOutput: 263 } })
assert.deepStrictEqual(json.outputs[outWorkerChunk].inputs, { [inWorker]: { bytesInOutput: 23 } })
},

// Test in-memory output files
async writeFalse({ esbuild, testDir }) {
const input = path.join(testDir, 'in.js')
Expand Down

0 comments on commit e4b6e42

Please sign in to comment.