Skip to content

Commit

Permalink
fix #281: --out-extension for custom extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jul 25, 2020
1 parent 3e027f6 commit b0e1f6f
Show file tree
Hide file tree
Showing 11 changed files with 110 additions and 8 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions cmd/esbuild/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions internal/bundler/bundler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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");
`,
},
})
}
6 changes: 2 additions & 4 deletions internal/bundler/linker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ type Options struct {

AbsOutputFile string
AbsOutputDir string
OutputExtensions map[string]string
ModuleName string
TsConfigOverride string
ExtensionToLoader map[string]Loader
Expand All @@ -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
}
20 changes: 18 additions & 2 deletions lib/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface BuildOptions extends CommonOptions {
resolveExtensions?: string[];
write?: boolean;
tsconfig?: string;
outExtension?: { [ext: string]: string };

entryPoints?: string[];
stdin?: StdinOptions;
Expand Down
1 change: 1 addition & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ type BuildOptions struct {
Loaders map[string]Loader
ResolveExtensions []string
Tsconfig string
OutExtensions map[string]string

EntryPoints []string
Stdin *StdinOptions
Expand Down
23 changes: 21 additions & 2 deletions pkg/api/api_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down
11 changes: 11 additions & 0 deletions pkg/cli/cli_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit b0e1f6f

Please sign in to comment.