diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 67d2e5b8a54..43f4828fcec 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -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 @@ -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: diff --git a/internal/js_ast/js_ast.go b/internal/js_ast/js_ast.go index 1b9adac56fc..760c2e23809 100644 --- a/internal/js_ast/js_ast.go +++ b/internal/js_ast/js_ast.go @@ -465,6 +465,7 @@ func (*ERequireString) isExpr() {} func (*ERequireResolveString) isExpr() {} func (*EImportString) isExpr() {} func (*EImportCall) isExpr() {} +func (*EImportMetaResolve) isExpr() {} type EArray struct { Items []Expr @@ -855,7 +856,6 @@ type ERequireResolveString struct { ImportRecordIndex uint32 CloseParenLoc logger.Loc } - type EImportString struct { ImportRecordIndex uint32 CloseParenLoc logger.Loc @@ -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 diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index f006d5f18a4..7f899633159 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -3,6 +3,7 @@ package js_parser import ( "fmt" "math" + "os" "regexp" "sort" "strings" @@ -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 @@ -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://github.com/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}} } @@ -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: @@ -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 { @@ -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 @@ -17011,6 +17094,7 @@ const ( whyESMUnknown whyESM = iota whyESMExportKeyword whyESMImportMeta + whyESMImportMetaResolve whyESMTopLevelAwait whyESMFileMJS whyESMFileMTS @@ -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:")} @@ -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 = @@ -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 diff --git a/internal/js_printer/js_printer.go b/internal/js_printer/js_printer.go index 18a0e7e65ef..b4932a7bbb2 100644 --- a/internal/js_printer/js_printer.go +++ b/internal/js_printer/js_printer.go @@ -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 { diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index 77703b0d0ad..9f1598a61ea 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -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 diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index d8b919758ee..64bab2ecc15 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -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: diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index 3c5d822c2a1..818da5fbc44 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -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')