diff --git a/CHANGELOG.md b/CHANGELOG.md index 2985c3769dd..f1b20523388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,40 @@ If you are using esbuild in the browser, you now need to call `esbuild.initialize({ wasmURL })` and wait for the returned promise before calling `esbuild.transform()`. It takes the same options that `esbuild.startService()` used to take. Note that the `esbuild.buildSync()` and `esbuild.transformSync()` APIs still exist when using esbuild in node. Nothing has changed about the synchronous esbuild APIs. +* The banner and footer options are now language-specific ([#712](https://github.com/evanw/esbuild/issues/712)) + + The `--banner=` and `--footer=` options now require you to pass the file type: + + * CLI: + + ``` + esbuild --banner:js=//banner --footer:js=//footer + esbuild --banner:css=/*banner*/ --footer:css=/*footer*/ + ``` + + * JavaScript + + ```js + esbuild.build({ + banner: { js: '//banner', css: '/*banner*/' }, + footer: { js: '//footer', css: '/*footer*/' }, + }) + ``` + + * Go + + ```go + api.Build(api.BuildOptions{ + Banner: map[string]string{"js": "//banner"}, + Footer: map[string]string{"js": "//footer"}, + }) + api.Build(api.BuildOptions{ + Banner: map[string]string{"css": "/*banner*/"}, + Footer: map[string]string{"css": "/*footer*/"}, + }) + ``` + + This was changed because the feature was originally added in a JavaScript-specific manner, which was an oversight. CSS banners and footers must be separate from JavaScript banners and footers to avoid injecting JavaScript syntax into your CSS files. ## Unreleased diff --git a/cmd/esbuild/main.go b/cmd/esbuild/main.go index 849d2aa908e..9cee25ff95c 100644 --- a/cmd/esbuild/main.go +++ b/cmd/esbuild/main.go @@ -45,11 +45,13 @@ var helpText = func(colors logger.Colors) string { --watch Watch mode: rebuild on file system changes ` + colors.Bold + `Advanced options:` + colors.Default + ` - --banner=... Text to be prepended to each output file + --banner:T=... Text to be prepended to each output file of type T + where T is one of: css | js --charset=utf8 Do not escape UTF-8 code points --color=... Force use of color terminal escapes (true | false) --error-limit=... Maximum error count or 0 to disable (default 10) - --footer=... Text to be appended to each output file + --footer:T=... Text to be appended to each output file of type T + where T is one of: css | js --global-name=... The name of the global for the IIFE format --inject:F Import the file F into all input files and automatically replace matching globals with imports diff --git a/internal/bundler/linker.go b/internal/bundler/linker.go index f39f35aaae3..1ba1a2ba210 100644 --- a/internal/bundler/linker.go +++ b/internal/bundler/linker.go @@ -3748,10 +3748,10 @@ func (repr *chunkReprJS) generate(c *linkerContext, chunk *chunkInfo) func(gener } } - if len(c.options.Banner) > 0 { - prevOffset.advanceString(c.options.Banner) + if len(c.options.JSBanner) > 0 { + prevOffset.advanceString(c.options.JSBanner) prevOffset.advanceString("\n") - j.AddString(c.options.Banner) + j.AddString(c.options.JSBanner) j.AddString("\n") } @@ -3969,8 +3969,8 @@ func (repr *chunkReprJS) generate(c *linkerContext, chunk *chunkInfo) func(gener j.AddString("\n") } - if len(c.options.Footer) > 0 { - j.AddString(c.options.Footer) + if len(c.options.JSFooter) > 0 { + j.AddString(c.options.JSFooter) j.AddString("\n") } @@ -4179,6 +4179,11 @@ func (repr *chunkReprCSS) generate(c *linkerContext, chunk *chunkInfo) func(gene j := js_printer.Joiner{} newlineBeforeComment := false + if len(c.options.CSSBanner) > 0 { + j.AddString(c.options.CSSBanner) + j.AddString("\n") + } + // Generate any prefix rules now { ast := css_ast.AST{} @@ -4278,6 +4283,11 @@ func (repr *chunkReprCSS) generate(c *linkerContext, chunk *chunkInfo) func(gene j.AddString("\n") } + if len(c.options.CSSFooter) > 0 { + j.AddString(c.options.CSSFooter) + j.AddString("\n") + } + // The CSS contents are done now that the source map comment is in cssContents := j.Done() diff --git a/internal/config/config.go b/internal/config/config.go index b84e83f1da7..675ff6c6a4b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -217,8 +217,11 @@ type Options struct { InjectAbsPaths []string InjectedDefines []InjectedDefine InjectedFiles []InjectedFile - Banner string - Footer string + + JSBanner string + JSFooter string + CSSBanner string + CSSFooter string ChunkPathTemplate []PathTemplate AssetPathTemplate []PathTemplate diff --git a/lib/common.ts b/lib/common.ts index 014d80babf5..66e9d83fe46 100644 --- a/lib/common.ts +++ b/lib/common.ts @@ -109,8 +109,6 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe let define = getFlag(options, keys, 'define', mustBeObject); let pure = getFlag(options, keys, 'pure', mustBeArray); let keepNames = getFlag(options, keys, 'keepNames', mustBeBoolean); - let banner = getFlag(options, keys, 'banner', mustBeString); - let footer = getFlag(options, keys, 'footer', mustBeString); if (sourcesContent !== void 0) flags.push(`--sources-content=${sourcesContent}`); if (target) { @@ -137,9 +135,6 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe } if (pure) for (let fn of pure) flags.push(`--pure:${fn}`); if (keepNames) flags.push(`--keep-names`); - - if (banner) flags.push(`--banner=${banner}`); - if (footer) flags.push(`--footer=${footer}`); } function flagsForBuildOptions( @@ -188,6 +183,8 @@ function flagsForBuildOptions( let chunkNames = getFlag(options, keys, 'chunkNames', mustBeString); let assetNames = getFlag(options, keys, 'assetNames', mustBeString); let inject = getFlag(options, keys, 'inject', mustBeArray); + let banner = getFlag(options, keys, 'banner', mustBeObject); + let footer = getFlag(options, keys, 'footer', mustBeObject); let entryPoints = getFlag(options, keys, 'entryPoints', mustBeArray); let absWorkingDir = getFlag(options, keys, 'absWorkingDir', mustBeString); let stdin = getFlag(options, keys, 'stdin', mustBeObject); @@ -239,6 +236,18 @@ function flagsForBuildOptions( flags.push(`--main-fields=${values.join(',')}`); } if (external) for (let name of external) flags.push(`--external:${name}`); + if (banner) { + for (let type in banner) { + if (type.indexOf('=') >= 0) throw new Error(`Invalid banner file type: ${type}`); + flags.push(`--banner:${type}=${banner[type]}`); + } + } + if (footer) { + for (let type in footer) { + if (type.indexOf('=') >= 0) throw new Error(`Invalid footer file type: ${type}`); + flags.push(`--footer:${type}=${footer[type]}`); + } + } if (inject) for (let path of inject) flags.push(`--inject:${path}`); if (loader) { for (let ext in loader) { @@ -311,12 +320,16 @@ function flagsForTransformOptions( let tsconfigRaw = getFlag(options, keys, 'tsconfigRaw', mustBeStringOrObject); let sourcefile = getFlag(options, keys, 'sourcefile', mustBeString); let loader = getFlag(options, keys, 'loader', mustBeString); + let banner = getFlag(options, keys, 'banner', mustBeString); + let footer = getFlag(options, keys, 'footer', mustBeString); checkForInvalidFlags(options, keys, `in ${callName}() call`); if (sourcemap) flags.push(`--sourcemap=${sourcemap === true ? 'external' : sourcemap}`); if (tsconfigRaw) flags.push(`--tsconfig-raw=${typeof tsconfigRaw === 'string' ? tsconfigRaw : JSON.stringify(tsconfigRaw)}`); if (sourcefile) flags.push(`--sourcefile=${sourcefile}`); if (loader) flags.push(`--loader=${loader}`); + if (banner) flags.push(`--banner=${banner}`); + if (footer) flags.push(`--footer=${footer}`); return flags; } diff --git a/lib/types.ts b/lib/types.ts index 702926f0be0..55bca7f7477 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -25,8 +25,6 @@ interface CommonOptions { define?: { [key: string]: string }; pure?: string[]; keepNames?: boolean; - banner?: string; - footer?: string; color?: boolean; logLevel?: LogLevel; @@ -53,6 +51,8 @@ export interface BuildOptions extends CommonOptions { chunkNames?: string; assetNames?: string; inject?: string[]; + banner?: { [type: string]: string }; + footer?: { [type: string]: string }; incremental?: boolean; entryPoints?: string[]; stdin?: StdinOptions; @@ -158,6 +158,8 @@ export interface TransformOptions extends CommonOptions { sourcefile?: string; loader?: Loader; + banner?: string; + footer?: string; } export interface TransformResult { diff --git a/pkg/api/api.go b/pkg/api/api.go index 32b66acb0a6..82b8f1af8ab 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -260,8 +260,8 @@ type BuildOptions struct { OutExtensions map[string]string PublicPath string Inject []string - Banner string - Footer string + Banner map[string]string + Footer map[string]string NodePaths []string // The "NODE_PATH" variable from Node.js ChunkNames string diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index 45b6380733d..db3bde18c9d 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -503,6 +503,20 @@ func validateOutputExtensions(log logger.Log, outExtensions map[string]string) ( return } +func validateBannerOrFooter(log logger.Log, name string, values map[string]string) (js string, css string) { + for key, value := range values { + switch key { + case "js": + js = value + case "css": + css = value + default: + log.AddError(nil, logger.Loc{}, fmt.Sprintf("Invalid %s file type: %q (valid: css, js)", name, key)) + } + } + return +} + func convertLocationToPublic(loc *logger.MsgLocation) *Location { if loc != nil { return &Location{ @@ -685,6 +699,8 @@ func rebuildImpl( } jsFeatures, cssFeatures := validateFeatures(log, buildOpts.Target, buildOpts.Engines) outJS, outCSS := validateOutputExtensions(log, buildOpts.OutExtensions) + bannerJS, bannerCSS := validateBannerOrFooter(log, "banner", buildOpts.Banner) + footerJS, footerCSS := validateBannerOrFooter(log, "footer", buildOpts.Footer) defines, injectedDefines := validateDefines(log, buildOpts.Define, buildOpts.Pure) options := config.Options{ UnsupportedJSFeatures: jsFeatures, @@ -723,8 +739,10 @@ func rebuildImpl( KeepNames: buildOpts.KeepNames, InjectAbsPaths: make([]string, len(buildOpts.Inject)), AbsNodePaths: make([]string, len(buildOpts.NodePaths)), - Banner: buildOpts.Banner, - Footer: buildOpts.Footer, + JSBanner: bannerJS, + JSFooter: footerJS, + CSSBanner: bannerCSS, + CSSFooter: footerCSS, PreserveSymlinks: buildOpts.PreserveSymlinks, WatchMode: buildOpts.Watch != nil, Plugins: plugins, @@ -1157,8 +1175,13 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult Contents: input, SourceFile: transformOpts.Sourcefile, }, - Banner: transformOpts.Banner, - Footer: transformOpts.Footer, + } + if options.Stdin.Loader == config.LoaderCSS { + options.CSSBanner = transformOpts.Banner + options.CSSFooter = transformOpts.Footer + } else { + options.JSBanner = transformOpts.Banner + options.JSFooter = transformOpts.Footer } if options.SourceMap == config.SourceMapLinkedWithComment { // Linked source maps don't make sense because there's no output file name diff --git a/pkg/cli/cli_impl.go b/pkg/cli/cli_impl.go index 864e0dcfd5c..187fc261cd6 100644 --- a/pkg/cli/cli_impl.go +++ b/pkg/cli/cli_impl.go @@ -19,6 +19,8 @@ func newBuildOptions() api.BuildOptions { return api.BuildOptions{ Loader: make(map[string]api.Loader), Define: make(map[string]string), + Banner: make(map[string]string), + Footer: make(map[string]string), } } @@ -347,21 +349,27 @@ func parseOptionsImpl(osArgs []string, buildOpts *api.BuildOptions, transformOpt transformOpts.JSXFragment = value } - case strings.HasPrefix(arg, "--banner="): - value := arg[len("--banner="):] - if buildOpts != nil { - buildOpts.Banner = value - } else { - transformOpts.Banner = value + case strings.HasPrefix(arg, "--banner=") && transformOpts != nil: + transformOpts.Banner = arg[len("--banner="):] + + case strings.HasPrefix(arg, "--footer=") && transformOpts != nil: + transformOpts.Footer = arg[len("--footer="):] + + case strings.HasPrefix(arg, "--banner:") && buildOpts != nil: + value := arg[len("--banner:"):] + equals := strings.IndexByte(value, '=') + if equals == -1 { + return fmt.Errorf("Missing \"=\": %q", value) } + buildOpts.Banner[value[:equals]] = value[equals+1:] - case strings.HasPrefix(arg, "--footer="): - value := arg[len("--footer="):] - if buildOpts != nil { - buildOpts.Footer = value - } else { - transformOpts.Footer = value + case strings.HasPrefix(arg, "--footer:") && buildOpts != nil: + value := arg[len("--footer:"):] + equals := strings.IndexByte(value, '=') + if equals == -1 { + return fmt.Errorf("Missing \"=\": %q", value) } + buildOpts.Footer[value[:equals]] = value[equals+1:] case strings.HasPrefix(arg, "--error-limit="): value := arg[len("--error-limit="):] diff --git a/scripts/end-to-end-tests.js b/scripts/end-to-end-tests.js index 31ae78e2cfc..979c62b1df3 100644 --- a/scripts/end-to-end-tests.js +++ b/scripts/end-to-end-tests.js @@ -2706,13 +2706,13 @@ // Test injecting banner and footer tests.push( - test(['in.js', '--outfile=node.js', '--banner=const bannerDefined = true;'], { + test(['in.js', '--outfile=node.js', '--banner:js=const bannerDefined = true;'], { 'in.js': `if (!bannerDefined) throw 'fail'` }), - test(['in.js', '--outfile=node.js', '--footer=function footer() { }'], { + test(['in.js', '--outfile=node.js', '--footer:js=function footer() { }'], { 'in.js': `footer()` }), - test(['a.js', 'b.js', '--outdir=out', '--bundle', '--format=cjs', '--banner=const bannerDefined = true;', '--footer=function footer() { }'], { + test(['a.js', 'b.js', '--outdir=out', '--bundle', '--format=cjs', '--banner:js=const bannerDefined = true;', '--footer:js=function footer() { }'], { 'a.js': ` module.exports = { banner: bannerDefined, footer }; `, diff --git a/scripts/esbuild.js b/scripts/esbuild.js index b5a0448ef90..2772cf3858e 100644 --- a/scripts/esbuild.js +++ b/scripts/esbuild.js @@ -144,8 +144,8 @@ exports.buildWasmLib = async (esbuildPath) => { '--format=cjs', '--define:ESBUILD_VERSION=' + JSON.stringify(version), '--define:WEB_WORKER_SOURCE_CODE=' + JSON.stringify(wasmExecCode + workerCode), - '--banner=' + umdPrefix, - '--footer=' + umdSuffix, + '--banner:js=' + umdPrefix, + '--footer:js=' + umdSuffix, '--log-level=warning', ].concat(minifyFlags), { cwd: repoDir }).toString() fs.writeFileSync(path.join(libDir, minify ? 'browser.min.js' : 'browser.js'), browserCJS) diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index 4cb370bb570..c434551ffad 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -1679,7 +1679,7 @@ console.log("success"); await esbuild.build({ entryPoints: [input], outfile, - banner: 'const bannerDefined = true', + banner: { js: 'const bannerDefined = true' }, }) require(outfile) }, @@ -1691,7 +1691,7 @@ console.log("success"); await esbuild.build({ entryPoints: [input], outfile, - footer: 'function footer() {}', + footer: { js: 'function footer() {}' }, }) require(outfile) }, @@ -1705,8 +1705,8 @@ console.log("success"); await esbuild.build({ entryPoints: [aPath, bPath], outdir, - banner: 'const bannerDefined = true', - footer: 'function footer() {}', + banner: { js: 'const bannerDefined = true' }, + footer: { js: 'function footer() {}' }, }) const a = require(path.join(outdir, path.basename(aPath))) const b = require(path.join(outdir, path.basename(bPath))) @@ -1722,11 +1722,11 @@ console.log("success"); await esbuild.build({ entryPoints: [input], outfile, - banner: '/* banner */', - footer: '/* footer */', + banner: { css: '/* banner */' }, + footer: { css: '/* footer */' }, }) const code = await readFileAsync(outfile, 'utf8') - assert.strictEqual(code, `div {\n color: red;\n}\n`) + assert.strictEqual(code, `/* banner */\ndiv {\n color: red;\n}\n/* footer */\n`) }, async buildRelativeIssue693({ esbuild }) { @@ -2576,7 +2576,7 @@ let transformTests = { banner: '/* banner */', footer: '/* footer */', }) - assert.strictEqual(code, `div {\n color: red;\n}\n`) + assert.strictEqual(code, `/* banner */\ndiv {\n color: red;\n}\n/* footer */\n`) }, async transformDirectEval({ esbuild }) { diff --git a/scripts/verify-source-map.js b/scripts/verify-source-map.js index 2fc6465044f..5a12655e644 100644 --- a/scripts/verify-source-map.js +++ b/scripts/verify-source-map.js @@ -445,7 +445,7 @@ async function main() { crlf, }), check('banner-footer' + suffix, testCaseES6, toSearchBundle, { - flags: flags.concat('--outfile=out.js', '--bundle', '--banner="/* LICENSE abc */"', '--footer="/* end of file banner */"'), + flags: flags.concat('--outfile=out.js', '--bundle', '--banner:js="/* LICENSE abc */"', '--footer:js="/* end of file banner */"'), entryPoints: ['a.js'], crlf, }),