From b40bedff9701518b0f861c1117949038c1dbd747 Mon Sep 17 00:00:00 2001 From: Grzegorz Rozdzialik Date: Sun, 15 Nov 2020 14:05:42 +0100 Subject: [PATCH] add support for adding banner and footer to output files --- cmd/esbuild/main.go | 2 ++ internal/bundler/linker.go | 12 ++++++++++++ internal/config/config.go | 2 ++ lib/common.ts | 5 +++++ lib/types.ts | 2 ++ pkg/api/api.go | 4 ++++ pkg/api/api_impl.go | 4 ++++ pkg/cli/cli_impl.go | 16 ++++++++++++++++ scripts/end-to-end-tests.js | 26 ++++++++++++++++++++++++++ scripts/verify-source-map.js | 5 +++++ 10 files changed, 78 insertions(+) diff --git a/cmd/esbuild/main.go b/cmd/esbuild/main.go index 6dab7ec258a..8cdc6928095 100644 --- a/cmd/esbuild/main.go +++ b/cmd/esbuild/main.go @@ -31,6 +31,8 @@ Options: is browser and cjs when platform is node) --splitting Enable code splitting (currently only for esm) --global-name=... The name of the global for the IIFE format + --banner=... Text to be prepended to each output file + --footer=... Text to be appended to each output file --minify Sets all --minify-* flags --minify-whitespace Remove whitespace diff --git a/internal/bundler/linker.go b/internal/bundler/linker.go index db278a7eded..0fa731955fd 100644 --- a/internal/bundler/linker.go +++ b/internal/bundler/linker.go @@ -3400,6 +3400,13 @@ func (repr *chunkReprJS) generate(c *linkerContext, chunk *chunkInfo) func([]ast } } + if len(c.options.Banner) > 0 { + prevOffset.advanceString(c.options.Banner) + prevOffset.advanceString("\n") + j.AddString(c.options.Banner) + j.AddString("\n") + } + // Optionally wrap with an IIFE if c.options.OutputFormat == config.FormatIIFE { var text string @@ -3634,6 +3641,11 @@ func (repr *chunkReprJS) generate(c *linkerContext, chunk *chunkInfo) func([]ast j.AddString("\n") } + if len(c.options.Footer) > 0 { + j.AddString(c.options.Footer) + j.AddString("\n") + } + if c.options.SourceMap != config.SourceMapNone { sourceMap := c.generateSourceMapForChunk(compileResultsForSourceMap) diff --git a/internal/config/config.go b/internal/config/config.go index c2b932111a8..198b6f8efce 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -201,6 +201,8 @@ type Options struct { PublicPath string InjectAbsPaths []string InjectedFiles []InjectedFile + Banner string + Footer string Plugins []Plugin diff --git a/lib/common.ts b/lib/common.ts index b9a694355be..4fabd243746 100644 --- a/lib/common.ts +++ b/lib/common.ts @@ -89,6 +89,8 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe let pure = getFlag(options, keys, 'pure', mustBeArray); let avoidTDZ = getFlag(options, keys, 'avoidTDZ', mustBeBoolean); let keepNames = getFlag(options, keys, 'keepNames', mustBeBoolean); + let banner = getFlag(options, keys, 'banner', mustBeString); + let footer = getFlag(options, keys, 'footer', mustBeString); if (target) { if (Array.isArray(target)) flags.push(`--target=${Array.from(target).map(validateTarget).join(',')}`) @@ -114,6 +116,9 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe if (pure) for (let fn of pure) flags.push(`--pure:${fn}`); if (avoidTDZ) flags.push(`--avoid-tdz`); if (keepNames) flags.push(`--keep-names`); + + if (banner) flags.push(`--banner=${banner}`); + if (footer) flags.push(`--footer=${footer}`); } function flagsForBuildOptions(options: types.BuildOptions, isTTY: boolean, logLevelDefault: types.LogLevel): diff --git a/lib/types.ts b/lib/types.ts index fe2d8e7e066..ba2a7079331 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -22,6 +22,8 @@ interface CommonOptions { pure?: string[]; avoidTDZ?: boolean; keepNames?: boolean; + banner?: string; + footer?: string; color?: boolean; logLevel?: LogLevel; diff --git a/pkg/api/api.go b/pkg/api/api.go index a9a496fc5fa..55fe9c098fe 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -230,6 +230,8 @@ type BuildOptions struct { OutExtensions map[string]string PublicPath string Inject []string + Banner string + Footer string EntryPoints []string Stdin *StdinOptions @@ -282,6 +284,8 @@ type TransformOptions struct { JSXFactory string JSXFragment string TsconfigRaw string + Footer string + Banner string Define map[string]string Pure []string diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index bd546a92e92..ee11a4735fd 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -494,6 +494,8 @@ func buildImpl(buildOpts BuildOptions) BuildResult { AvoidTDZ: buildOpts.AvoidTDZ, KeepNames: buildOpts.KeepNames, InjectAbsPaths: make([]string, len(buildOpts.Inject)), + Banner: buildOpts.Banner, + Footer: buildOpts.Footer, } for i, path := range buildOpts.Inject { options.InjectAbsPaths[i] = validatePath(log, realFS, path) @@ -728,6 +730,8 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult Contents: input, SourceFile: transformOpts.Sourcefile, }, + Banner: transformOpts.Banner, + Footer: 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 c9a8d311e70..e8dc00575c2 100644 --- a/pkg/cli/cli_impl.go +++ b/pkg/cli/cli_impl.go @@ -302,6 +302,22 @@ 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, "--footer="): + value := arg[len("--footer="):] + if buildOpts != nil { + buildOpts.Footer = value + } else { + transformOpts.Footer = value + } + case strings.HasPrefix(arg, "--error-limit="): value := arg[len("--error-limit="):] limit, err := strconv.Atoi(value) diff --git a/scripts/end-to-end-tests.js b/scripts/end-to-end-tests.js index e642cb15b97..f415640161f 100644 --- a/scripts/end-to-end-tests.js +++ b/scripts/end-to-end-tests.js @@ -1808,6 +1808,32 @@ ) } + // Test injecting banner and footer + tests.push( + test(['in.js', '--outfile=node.js', '--banner=const bannerDefined = true;'], { + 'in.js': `if (!bannerDefined) throw 'fail'` + }), + test(['in.js', '--outfile=node.js', '--footer=function footer() { }'], { + 'in.js': `footer()` + }), + test(['a.js', 'b.js', '--outdir=out', '--bundle', '--format=cjs', '--banner=const bannerDefined = true;', '--footer=function footer() { }'], { + 'a.js': ` + module.exports = { banner: bannerDefined, footer }; + `, + 'b.js': ` + module.exports = { banner: bannerDefined, footer }; + `, + 'node.js': ` + const a = require('./out/a'); + const b = require('./out/b'); + + if (!a.banner || !b.banner) throw 'fail'; + a.footer(); + b.footer(); + ` + }), + ) + // Test writing to stdout tests.push( // These should succeed diff --git a/scripts/verify-source-map.js b/scripts/verify-source-map.js index c54404bf4e6..8d538952bd4 100644 --- a/scripts/verify-source-map.js +++ b/scripts/verify-source-map.js @@ -376,6 +376,11 @@ async function main() { entryPoints: ['entry.js'], crlf, }), + check('banner-footer' + suffix, testCaseES6, toSearchBundle, { + flags: flags.concat('--outfile=out.js', '--bundle', '--banner="/* LICENSE abc */"', '--footer="/* end of file banner */"'), + entryPoints: ['a.js'], + crlf, + }), ) } }