Skip to content

Commit

Permalink
fix #3041: allow injecting copied files
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Apr 10, 2023
1 parent ab15c70 commit c7c5a86
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 11 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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))
Expand Down
12 changes: 10 additions & 2 deletions internal/bundler/bundler.go
Expand Up @@ -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,
}
}

Expand Down
39 changes: 39 additions & 0 deletions internal/bundler_tests/bundler_loader_test.go
Expand Up @@ -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,
},
},
})
}
9 changes: 9 additions & 0 deletions internal/bundler_tests/snapshots/snapshots_loader.txt
Expand Up @@ -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 ----------
Expand Down
7 changes: 4 additions & 3 deletions internal/config/config.go
Expand Up @@ -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 {
Expand Down
20 changes: 14 additions & 6 deletions internal/js_parser/js_parser.go
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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("<runtime>", keys, &sourceIndex, before, p.runtimeImports)
before = p.generateImportStmt("<runtime>", keys, before, p.runtimeImports, &sourceIndex, nil)
}

// Insert an import statement for any jsx runtime imports we generated
Expand All @@ -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
Expand Down

0 comments on commit c7c5a86

Please sign in to comment.