diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f45e767377..1f41d6298d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,12 @@ } ``` +* Support `--inject` with a file loaded using the `copy` loader ([#3041](https://github.com/evanw/esbuild/issues/3041)) + + This release now allows you to use `--inject` with a file that is loaded using the `copy` loader. The `copy` loader copies the imported file to the output directory verbatim and rewrites the path in the `import` statement to point to the copied output file. When used with `--inject`, this means the injected file will be copied to the output directory as-is and a bare `import` statement for that file will be inserted in any non-copy output files that esbuild generates. + + Note that since esbuild doesn't parse the contents of copied files, esbuild will not expose any of the export names as usable imports when you do this (in the way that esbuild's `--inject` feature is typically used). However, any side-effects that the injected file has will still occur. + ## 0.17.15 * Allow keywords as type parameter names in mapped types ([#3033](https://github.com/evanw/esbuild/issues/3033)) diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index a9a3be0b93f..1e92ccc38e7 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -530,9 +530,17 @@ func parseFile(args parseArgs) { // "options" object to populate the "InjectedFiles" field. So we must // only send on the "inject" channel after we're done using the "options" // object so we don't introduce a data race. + isCopyLoader := loader == config.LoaderCopy + if isCopyLoader && args.skipResolve { + // This is not allowed because the import path would have to be rewritten, + // but import paths are not rewritten when bundling isn't enabled. + args.log.AddError(nil, logger.Range{}, + fmt.Sprintf("Cannot inject %q with the \"copy\" loader without bundling enabled", source.PrettyPath)) + } args.inject <- config.InjectedFile{ - Source: source, - Exports: exports, + Source: source, + Exports: exports, + IsCopyLoader: isCopyLoader, } } diff --git a/internal/bundler_tests/bundler_loader_test.go b/internal/bundler_tests/bundler_loader_test.go index e9d646a6dce..3c91e04958c 100644 --- a/internal/bundler_tests/bundler_loader_test.go +++ b/internal/bundler_tests/bundler_loader_test.go @@ -1498,3 +1498,42 @@ func TestLoaderCopyStartsWithDotRelPath(t *testing.T) { }, }) } + +func TestLoaderCopyWithInjectedFileNoBundle(t *testing.T) { + loader_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/src/entry.ts": `console.log('in entry.ts')`, + "/src/inject.js": `console.log('in inject.js')`, + }, + entryPaths: []string{"/src/entry.ts"}, + options: config.Options{ + AbsOutputDir: "/out", + InjectPaths: []string{"/src/inject.js"}, + ExtensionToLoader: map[string]config.Loader{ + ".ts": config.LoaderTS, + ".js": config.LoaderCopy, + }, + }, + expectedScanLog: `ERROR: Cannot inject "src/inject.js" with the "copy" loader without bundling enabled +`, + }) +} + +func TestLoaderCopyWithInjectedFileBundle(t *testing.T) { + loader_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/src/entry.ts": `console.log('in entry.ts')`, + "/src/inject.js": `console.log('in inject.js')`, + }, + entryPaths: []string{"/src/entry.ts"}, + options: config.Options{ + Mode: config.ModeBundle, + AbsOutputDir: "/out", + InjectPaths: []string{"/src/inject.js"}, + ExtensionToLoader: map[string]config.Loader{ + ".ts": config.LoaderTS, + ".js": config.LoaderCopy, + }, + }, + }) +} diff --git a/internal/bundler_tests/snapshots/snapshots_loader.txt b/internal/bundler_tests/snapshots/snapshots_loader.txt index be00b9996c5..9a49a8006d5 100644 --- a/internal/bundler_tests/snapshots/snapshots_loader.txt +++ b/internal/bundler_tests/snapshots/snapshots_loader.txt @@ -342,6 +342,15 @@ TestLoaderCopyWithFormat ---------- /out/assets/some.file ---------- stuff +================================================================================ +TestLoaderCopyWithInjectedFileBundle +---------- /out/inject-IFR6YGWW.js ---------- +console.log('in inject.js') +---------- /out/entry.js ---------- +// src/entry.ts +import "./inject-IFR6YGWW.js"; +console.log("in entry.ts"); + ================================================================================ TestLoaderCopyWithTransform ---------- /out/src/entry.js ---------- diff --git a/internal/config/config.go b/internal/config/config.go index 1aaf82667e8..3edd60d2729 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -559,9 +559,10 @@ type InjectedDefine struct { } type InjectedFile struct { - Exports []InjectableExport - DefineName string - Source logger.Source + Exports []InjectableExport + DefineName string // For injected files generated when you "--define" a non-literal + Source logger.Source + IsCopyLoader bool // If you set the loader to "copy" (see https://github.com/evanw/esbuild/issues/3041) } type InjectableExport struct { diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 56ff8952483..63efda79bb0 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -15394,7 +15394,7 @@ func (p *parser) scanForImportsAndExports(stmts []js_ast.Stmt) (result importsEx if p.options.ts.Parse && foundImports && isUnusedInTypeScript && (p.options.unusedImportFlagsTS&config.UnusedImportKeepStmt) == 0 { // Ignore import records with a pre-filled source index. These are // for injected files and we definitely do not want to trim these. - if !record.SourceIndex.IsValid() { + if !record.SourceIndex.IsValid() && !record.CopySourceIndex.IsValid() { record.Flags |= ast.IsUnused continue } @@ -15879,7 +15879,11 @@ func Parse(log logger.Log, source logger.Source, options Options) (result js_ast } } - before = p.generateImportStmt(file.Source.KeyPath.Text, exportsNoConflict, &file.Source.Index, before, symbols) + if file.IsCopyLoader { + before = p.generateImportStmt(file.Source.KeyPath.Text, exportsNoConflict, before, symbols, nil, &file.Source.Index) + } else { + before = p.generateImportStmt(file.Source.KeyPath.Text, exportsNoConflict, before, symbols, &file.Source.Index, nil) + } } // Bind symbols in a second pass over the AST. I started off doing this in a @@ -16326,9 +16330,10 @@ func (p *parser) computeCharacterFrequency() *js_ast.CharFreq { func (p *parser) generateImportStmt( path string, imports []string, - sourceIndex *uint32, parts []js_ast.Part, symbols map[string]js_ast.LocRef, + sourceIndex *uint32, + copySourceIndex *uint32, ) []js_ast.Part { var loc logger.Loc isFirst := true @@ -16347,6 +16352,9 @@ func (p *parser) generateImportStmt( if sourceIndex != nil { p.importRecords[importRecordIndex].SourceIndex = ast.MakeIndex32(*sourceIndex) } + if copySourceIndex != nil { + p.importRecords[importRecordIndex].CopySourceIndex = ast.MakeIndex32(*copySourceIndex) + } declaredSymbols[0] = js_ast.DeclaredSymbol{Ref: namespaceRef, IsTopLevel: true} // Create per-import information @@ -16396,7 +16404,7 @@ func (p *parser) toAST(before, parts, after []js_ast.Part, hashbang string, dire if len(p.runtimeImports) > 0 && !p.options.omitRuntimeForTests { keys := sortedKeysOfMapStringLocRef(p.runtimeImports) sourceIndex := runtime.SourceIndex - before = p.generateImportStmt("", keys, &sourceIndex, before, p.runtimeImports) + before = p.generateImportStmt("", keys, before, p.runtimeImports, &sourceIndex, nil) } // Insert an import statement for any jsx runtime imports we generated @@ -16411,14 +16419,14 @@ func (p *parser) toAST(before, parts, after []js_ast.Part, hashbang string, dire path = path + "/jsx-runtime" } - before = p.generateImportStmt(path, keys, nil, before, p.jsxRuntimeImports) + before = p.generateImportStmt(path, keys, before, p.jsxRuntimeImports, nil, nil) } // Insert an import statement for any legacy jsx imports we generated (i.e., createElement) if len(p.jsxLegacyImports) > 0 && !p.options.omitJSXRuntimeForTests { keys := sortedKeysOfMapStringLocRef(p.jsxLegacyImports) path := p.options.jsx.ImportSource - before = p.generateImportStmt(path, keys, nil, before, p.jsxLegacyImports) + before = p.generateImportStmt(path, keys, before, p.jsxLegacyImports, nil, nil) } // Generated imports are inserted before other code instead of appending them