From b0e1f6fadcaceae54e8015ed72e18efd1eb7eb76 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Fri, 24 Jul 2020 23:00:42 -0700 Subject: [PATCH] fix #281: --out-extension for custom extensions --- CHANGELOG.md | 4 +++ README.md | 1 + cmd/esbuild/main.go | 1 + internal/bundler/bundler_test.go | 42 ++++++++++++++++++++++++++++++++ internal/bundler/linker.go | 6 ++--- internal/config/config.go | 8 ++++++ lib/common.ts | 20 +++++++++++++-- lib/types.ts | 1 + pkg/api/api.go | 1 + pkg/api/api_impl.go | 23 +++++++++++++++-- pkg/cli/cli_impl.go | 11 +++++++++ 11 files changed, 110 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7057aaa0832..e37db3068d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ When the `tsconfig.json` settings have been force-overridden using the new `--tsconfig` flag, the path resolution behavior behaved subtly differently than if esbuild naturally discovers the `tsconfig.json` file without the flag. The difference caused package paths present in a `node_modules` folder to incorrectly take precedence over custom path aliases configured in `tsconfig.json`. The ordering has been corrected such that custom path aliases always take place over `node_modules`. +* Add the `--out-extension` flag for custom output extensions ([#281](https://github.com/evanw/esbuild/issues/281)) + + Previously esbuild could only output files ending in `.js`. Now you can override this to another extension by passing something like `--out-extension:.js=.mjs`. This allows generating output files with the node-specific `.cjs` and `.mjs` extensions without having to use a separate command to rename them afterwards. + ## 0.6.5 * Fix IIFE wrapper for ES5 diff --git a/README.md b/README.md index 0e841a9e5cb..06ed1c024fa 100644 --- a/README.md +++ b/README.md @@ -383,6 +383,7 @@ Advanced options: --strict Transforms handle edge cases but have more overhead --pure=N Mark the name N as a pure function for tree shaking --tsconfig=... Use this tsconfig.json file instead of other ones + --out-extension:.js=.mjs Use a custom output extension instead of ".js" Examples: # Produces dist/entry_point.js and dist/entry_point.js.map diff --git a/cmd/esbuild/main.go b/cmd/esbuild/main.go index 9ac2c4d77d3..b6b5a3924b3 100644 --- a/cmd/esbuild/main.go +++ b/cmd/esbuild/main.go @@ -54,6 +54,7 @@ Advanced options: --strict Transforms handle edge cases but have more overhead --pure=N Mark the name N as a pure function for tree shaking --tsconfig=... Use this tsconfig.json file instead of other ones + --out-extension:.js=.mjs Use a custom output extension instead of ".js" Examples: # Produces dist/entry_point.js and dist/entry_point.js.map diff --git a/internal/bundler/bundler_test.go b/internal/bundler/bundler_test.go index dd5856f0ea9..0870f0789f4 100644 --- a/internal/bundler/bundler_test.go +++ b/internal/bundler/bundler_test.go @@ -4659,3 +4659,45 @@ func TestIIFE_ES5(t *testing.T) { }, }) } + +func TestOutputExtensionRemappingFile(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + console.log('test'); + `, + }, + entryPaths: []string{"/entry.js"}, + options: config.Options{ + IsBundling: true, + OutputExtensions: map[string]string{".js": ".notjs"}, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `// /entry.js +console.log("test"); +`, + }, + }) +} + +func TestOutputExtensionRemappingDir(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + console.log('test'); + `, + }, + entryPaths: []string{"/entry.js"}, + options: config.Options{ + IsBundling: true, + OutputExtensions: map[string]string{".js": ".notjs"}, + AbsOutputDir: "/out", + }, + expected: map[string]string{ + "/out/entry.notjs": `// /entry.js +console.log("test"); +`, + }, + }) +} diff --git a/internal/bundler/linker.go b/internal/bundler/linker.go index 23c3c41b01e..98e9523162f 100644 --- a/internal/bundler/linker.go +++ b/internal/bundler/linker.go @@ -2077,9 +2077,7 @@ func (c *linkerContext) computeChunks() []chunkMeta { // Swap the extension for ".js" ext := c.fs.Ext(chunkRelPath) - if ext != ".js" { - chunkRelPath = chunkRelPath[:len(chunkRelPath)-len(ext)] + ".js" - } + chunkRelPath = chunkRelPath[:len(chunkRelPath)-len(ext)] + c.options.OutputExtensionFor(".js") } // Always use cross-platform path separators to avoid problems with Windows @@ -2128,7 +2126,7 @@ func (c *linkerContext) computeChunks() []chunkMeta { bytes := []byte(lowerCaseAbsPathForWindows(chunk.relPath)) hashBytes := sha1.Sum(bytes) hash := base64.URLEncoding.EncodeToString(hashBytes[:])[:8] - chunk.relPath = "chunk." + hash + ".js" + chunk.relPath = "chunk." + hash + c.options.OutputExtensionFor(".js") } chunk.entryBits = partMeta.entryBits diff --git a/internal/config/config.go b/internal/config/config.go index ea25d18e568..99152d46036 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -152,6 +152,7 @@ type Options struct { AbsOutputFile string AbsOutputDir string + OutputExtensions map[string]string ModuleName string TsConfigOverride string ExtensionToLoader map[string]Loader @@ -163,3 +164,10 @@ type Options struct { SourceMap SourceMap Stdin *StdinInfo } + +func (options *Options) OutputExtensionFor(key string) string { + if ext, ok := options.OutputExtensions[key]; ok { + return ext + } + return key +} diff --git a/lib/common.ts b/lib/common.ts index 663bee0870a..be4c1328d15 100644 --- a/lib/common.ts +++ b/lib/common.ts @@ -22,7 +22,12 @@ function pushCommonFlags(flags: string[], options: types.CommonOptions, isTTY: b if (options.jsxFactory) flags.push(`--jsx-factory=${options.jsxFactory}`); if (options.jsxFragment) flags.push(`--jsx-fragment=${options.jsxFragment}`); - if (options.define) for (let key in options.define) flags.push(`--define:${key}=${options.define[key]}`); + if (options.define) { + for (let key in options.define) { + if (key.indexOf('=') >= 0) throw new Error(`Invalid define: ${key}`); + flags.push(`--define:${key}=${options.define[key]}`); + } + } if (options.pure) for (let fn of options.pure) flags.push(`--pure:${fn}`); if (options.color) flags.push(`--color=${options.color}`); @@ -49,7 +54,18 @@ function flagsForBuildOptions(options: types.BuildOptions, isTTY: boolean): [str if (options.tsconfig) flags.push(`--tsconfig=${options.tsconfig}`); if (options.resolveExtensions) flags.push(`--resolve-extensions=${options.resolveExtensions.join(',')}`); if (options.external) for (let name of options.external) flags.push(`--external:${name}`); - if (options.loader) for (let ext in options.loader) flags.push(`--loader:${ext}=${options.loader[ext]}`); + if (options.loader) { + for (let ext in options.loader) { + if (ext.indexOf('=') >= 0) throw new Error(`Invalid extension: ${ext}`); + flags.push(`--loader:${ext}=${options.loader[ext]}`); + } + } + if (options.outExtension) { + for (let ext in options.outExtension) { + if (ext.indexOf('=') >= 0) throw new Error(`Invalid extension: ${ext}`); + flags.push(`--out-extension:${ext}=${options.outExtension[ext]}`); + } + } if (options.entryPoints) { for (let entryPoint of options.entryPoints) { diff --git a/lib/types.ts b/lib/types.ts index 8d52244a162..32c3247ea83 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -39,6 +39,7 @@ export interface BuildOptions extends CommonOptions { resolveExtensions?: string[]; write?: boolean; tsconfig?: string; + outExtension?: { [ext: string]: string }; entryPoints?: string[]; stdin?: StdinOptions; diff --git a/pkg/api/api.go b/pkg/api/api.go index fd2b5bc88e4..28fb1030810 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -217,6 +217,7 @@ type BuildOptions struct { Loaders map[string]Loader ResolveExtensions []string Tsconfig string + OutExtensions map[string]string EntryPoints []string Stdin *StdinOptions diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index 96e92feef4e..d7f61f24c31 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -216,12 +216,16 @@ func validateExternals(log logging.Log, fs fs.FS, paths []string) config.Externa return result } +func isValidExtension(ext string) bool { + return len(ext) >= 2 && ext[0] == '.' && ext[len(ext)-1] != '.' +} + func validateResolveExtensions(log logging.Log, order []string) []string { if order == nil { return []string{".tsx", ".ts", ".jsx", ".mjs", ".cjs", ".js", ".json"} } for _, ext := range order { - if len(ext) < 2 || ext[0] != '.' { + if !isValidExtension(ext) { log.AddError(nil, ast.Loc{}, fmt.Sprintf("Invalid file extension: %q", ext)) } } @@ -232,7 +236,7 @@ func validateLoaders(log logging.Log, loaders map[string]Loader) map[string]conf result := bundler.DefaultExtensionToLoaderMap() if loaders != nil { for ext, loader := range loaders { - if len(ext) < 2 || ext[0] != '.' || ext[len(ext)-1] == '.' { + if !isValidExtension(ext) { log.AddError(nil, ast.Loc{}, fmt.Sprintf("Invalid file extension: %q", ext)) } result[ext] = validateLoader(loader) @@ -343,6 +347,20 @@ func validatePath(log logging.Log, fs fs.FS, relPath string) string { return absPath } +func validateOutputExtensions(log logging.Log, outExtensions map[string]string) map[string]string { + result := make(map[string]string) + for key, value := range outExtensions { + if key != ".js" { + log.AddError(nil, ast.Loc{}, fmt.Sprintf("Invalid output extension: %q (valid: .js)", key)) + } + if !isValidExtension(value) { + log.AddError(nil, ast.Loc{}, fmt.Sprintf("Invalid output extension: %q", value)) + } + result[key] = value + } + return result +} + func messagesOfKind(kind logging.MsgKind, msgs []logging.Msg) []Message { var filtered []Message for _, msg := range msgs { @@ -407,6 +425,7 @@ func buildImpl(buildOpts BuildOptions) BuildResult { AbsOutputFile: validatePath(log, realFS, buildOpts.Outfile), AbsOutputDir: validatePath(log, realFS, buildOpts.Outdir), AbsMetadataFile: validatePath(log, realFS, buildOpts.Metafile), + OutputExtensions: validateOutputExtensions(log, buildOpts.OutExtensions), ExtensionToLoader: validateLoaders(log, buildOpts.Loaders), ExtensionOrder: validateResolveExtensions(log, buildOpts.ResolveExtensions), ExternalModules: validateExternals(log, realFS, buildOpts.Externals), diff --git a/pkg/cli/cli_impl.go b/pkg/cli/cli_impl.go index ecb3ca95243..2657ba30343 100644 --- a/pkg/cli/cli_impl.go +++ b/pkg/cli/cli_impl.go @@ -185,6 +185,17 @@ func parseOptionsImpl(osArgs []string, buildOpts *api.BuildOptions, transformOpt transformOpts.Engines = engines } + case strings.HasPrefix(arg, "--out-extension:") && buildOpts != nil: + value := arg[len("--out-extension:"):] + equals := strings.IndexByte(value, '=') + if equals == -1 { + return fmt.Errorf("Missing \"=\": %q", value) + } + if buildOpts.OutExtensions == nil { + buildOpts.OutExtensions = make(map[string]string) + } + buildOpts.OutExtensions[value[:equals]] = value[equals+1:] + case arg == "--strict": value := api.StrictOptions{ NullishCoalescing: true,