Files
esbuild/pkg/cli/cli_impl.go
T

1475 lines
44 KiB
Go

package cli
// This file implements the public CLI. It's deliberately implemented using
// esbuild's public "Build", "Transform", and "AnalyzeMetafile" APIs instead of
// using internal APIs so that any tests that cover the CLI also implicitly
// cover the public API as well.
import (
"fmt"
"io/ioutil"
"net"
"os"
"sort"
"strconv"
"strings"
"github.com/evanw/esbuild/internal/cli_helpers"
"github.com/evanw/esbuild/internal/fs"
"github.com/evanw/esbuild/internal/logger"
"github.com/evanw/esbuild/pkg/api"
)
func newBuildOptions() api.BuildOptions {
return api.BuildOptions{
Banner: make(map[string]string),
Define: make(map[string]string),
Footer: make(map[string]string),
Loader: make(map[string]api.Loader),
LogOverride: make(map[string]api.LogLevel),
Supported: make(map[string]bool),
}
}
func newTransformOptions() api.TransformOptions {
return api.TransformOptions{
Define: make(map[string]string),
LogOverride: make(map[string]api.LogLevel),
Supported: make(map[string]bool),
}
}
type parseOptionsKind uint8
const (
// This means we're parsing it for our own internal use
kindInternal parseOptionsKind = iota
// This means the result is returned through a public API
kindExternal
)
type parseOptionsExtras struct {
watch bool
metafile *string
mangleCache *string
}
func isBoolFlag(arg string, flag string) bool {
if strings.HasPrefix(arg, flag) {
remainder := arg[len(flag):]
return len(remainder) == 0 || remainder[0] == '='
}
return false
}
func parseBoolFlag(arg string, defaultValue bool) (bool, *cli_helpers.ErrorWithNote) {
equals := strings.IndexByte(arg, '=')
if equals == -1 {
return defaultValue, nil
}
value := arg[equals+1:]
switch value {
case "false":
return false, nil
case "true":
return true, nil
}
return false, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Invalid value %q in %q", value, arg),
"Valid values are \"true\" or \"false\".",
)
}
func parseOptionsImpl(
osArgs []string,
buildOpts *api.BuildOptions,
transformOpts *api.TransformOptions,
kind parseOptionsKind,
) (extras parseOptionsExtras, err *cli_helpers.ErrorWithNote) {
hasBareSourceMapFlag := false
// Parse the arguments now that we know what we're parsing
for _, arg := range osArgs {
switch {
case isBoolFlag(arg, "--bundle") && buildOpts != nil:
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else {
buildOpts.Bundle = value
}
case isBoolFlag(arg, "--preserve-symlinks") && buildOpts != nil:
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else {
buildOpts.PreserveSymlinks = value
}
case isBoolFlag(arg, "--splitting") && buildOpts != nil:
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else {
buildOpts.Splitting = value
}
case isBoolFlag(arg, "--allow-overwrite") && buildOpts != nil:
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else {
buildOpts.AllowOverwrite = value
}
case isBoolFlag(arg, "--watch") && buildOpts != nil:
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else {
extras.watch = value
}
case isBoolFlag(arg, "--minify"):
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else if buildOpts != nil {
buildOpts.MinifySyntax = value
buildOpts.MinifyWhitespace = value
buildOpts.MinifyIdentifiers = value
} else {
transformOpts.MinifySyntax = value
transformOpts.MinifyWhitespace = value
transformOpts.MinifyIdentifiers = value
}
case isBoolFlag(arg, "--minify-syntax"):
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else if buildOpts != nil {
buildOpts.MinifySyntax = value
} else {
transformOpts.MinifySyntax = value
}
case isBoolFlag(arg, "--minify-whitespace"):
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else if buildOpts != nil {
buildOpts.MinifyWhitespace = value
} else {
transformOpts.MinifyWhitespace = value
}
case isBoolFlag(arg, "--minify-identifiers"):
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else if buildOpts != nil {
buildOpts.MinifyIdentifiers = value
} else {
transformOpts.MinifyIdentifiers = value
}
case isBoolFlag(arg, "--mangle-quoted"):
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else {
var mangleQuoted *api.MangleQuoted
if buildOpts != nil {
mangleQuoted = &buildOpts.MangleQuoted
} else {
mangleQuoted = &transformOpts.MangleQuoted
}
if value {
*mangleQuoted = api.MangleQuotedTrue
} else {
*mangleQuoted = api.MangleQuotedFalse
}
}
case strings.HasPrefix(arg, "--mangle-props="):
value := arg[len("--mangle-props="):]
if buildOpts != nil {
buildOpts.MangleProps = value
} else {
transformOpts.MangleProps = value
}
case strings.HasPrefix(arg, "--reserve-props="):
value := arg[len("--reserve-props="):]
if buildOpts != nil {
buildOpts.ReserveProps = value
} else {
transformOpts.ReserveProps = value
}
case strings.HasPrefix(arg, "--mangle-cache=") && buildOpts != nil && kind == kindInternal:
value := arg[len("--mangle-cache="):]
extras.mangleCache = &value
case strings.HasPrefix(arg, "--drop:"):
value := arg[len("--drop:"):]
switch value {
case "console":
if buildOpts != nil {
buildOpts.Drop |= api.DropConsole
} else {
transformOpts.Drop |= api.DropConsole
}
case "debugger":
if buildOpts != nil {
buildOpts.Drop |= api.DropDebugger
} else {
transformOpts.Drop |= api.DropDebugger
}
default:
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Invalid value %q in %q", value, arg),
"Valid values are \"console\" or \"debugger\".",
)
}
case strings.HasPrefix(arg, "--drop-labels="):
if buildOpts != nil {
buildOpts.DropLabels = splitWithEmptyCheck(arg[len("--drop-labels="):], ",")
} else {
transformOpts.DropLabels = splitWithEmptyCheck(arg[len("--drop-labels="):], ",")
}
case strings.HasPrefix(arg, "--legal-comments="):
value := arg[len("--legal-comments="):]
var legalComments api.LegalComments
switch value {
case "none":
legalComments = api.LegalCommentsNone
case "inline":
legalComments = api.LegalCommentsInline
case "eof":
legalComments = api.LegalCommentsEndOfFile
case "linked":
legalComments = api.LegalCommentsLinked
case "external":
legalComments = api.LegalCommentsExternal
default:
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Invalid value %q in %q", value, arg),
"Valid values are \"none\", \"inline\", \"eof\", \"linked\", or \"external\".",
)
}
if buildOpts != nil {
buildOpts.LegalComments = legalComments
} else {
transformOpts.LegalComments = legalComments
}
case strings.HasPrefix(arg, "--charset="):
var value *api.Charset
if buildOpts != nil {
value = &buildOpts.Charset
} else {
value = &transformOpts.Charset
}
name := arg[len("--charset="):]
switch name {
case "ascii":
*value = api.CharsetASCII
case "utf8":
*value = api.CharsetUTF8
default:
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Invalid value %q in %q", name, arg),
"Valid values are \"ascii\" or \"utf8\".",
)
}
case isBoolFlag(arg, "--tree-shaking"):
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else {
var treeShaking *api.TreeShaking
if buildOpts != nil {
treeShaking = &buildOpts.TreeShaking
} else {
treeShaking = &transformOpts.TreeShaking
}
if value {
*treeShaking = api.TreeShakingTrue
} else {
*treeShaking = api.TreeShakingFalse
}
}
case isBoolFlag(arg, "--ignore-annotations"):
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else if buildOpts != nil {
buildOpts.IgnoreAnnotations = value
} else {
transformOpts.IgnoreAnnotations = value
}
case isBoolFlag(arg, "--keep-names"):
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else if buildOpts != nil {
buildOpts.KeepNames = value
} else {
transformOpts.KeepNames = value
}
case arg == "--sourcemap":
if buildOpts != nil {
buildOpts.Sourcemap = api.SourceMapLinked
} else {
transformOpts.Sourcemap = api.SourceMapInline
}
hasBareSourceMapFlag = true
case strings.HasPrefix(arg, "--sourcemap="):
value := arg[len("--sourcemap="):]
var sourcemap api.SourceMap
switch value {
case "linked":
sourcemap = api.SourceMapLinked
case "inline":
sourcemap = api.SourceMapInline
case "external":
sourcemap = api.SourceMapExternal
case "both":
sourcemap = api.SourceMapInlineAndExternal
default:
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Invalid value %q in %q", value, arg),
"Valid values are \"linked\", \"inline\", \"external\", or \"both\".",
)
}
if buildOpts != nil {
buildOpts.Sourcemap = sourcemap
} else {
transformOpts.Sourcemap = sourcemap
}
hasBareSourceMapFlag = false
case strings.HasPrefix(arg, "--source-root="):
sourceRoot := arg[len("--source-root="):]
if buildOpts != nil {
buildOpts.SourceRoot = sourceRoot
} else {
transformOpts.SourceRoot = sourceRoot
}
case isBoolFlag(arg, "--sources-content"):
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else {
var sourcesContent *api.SourcesContent
if buildOpts != nil {
sourcesContent = &buildOpts.SourcesContent
} else {
sourcesContent = &transformOpts.SourcesContent
}
if value {
*sourcesContent = api.SourcesContentInclude
} else {
*sourcesContent = api.SourcesContentExclude
}
}
case strings.HasPrefix(arg, "--sourcefile="):
if buildOpts != nil {
if buildOpts.Stdin == nil {
buildOpts.Stdin = &api.StdinOptions{}
}
buildOpts.Stdin.Sourcefile = arg[len("--sourcefile="):]
} else {
transformOpts.Sourcefile = arg[len("--sourcefile="):]
}
case strings.HasPrefix(arg, "--resolve-extensions=") && buildOpts != nil:
buildOpts.ResolveExtensions = splitWithEmptyCheck(arg[len("--resolve-extensions="):], ",")
case strings.HasPrefix(arg, "--main-fields=") && buildOpts != nil:
buildOpts.MainFields = splitWithEmptyCheck(arg[len("--main-fields="):], ",")
case strings.HasPrefix(arg, "--conditions=") && buildOpts != nil:
buildOpts.Conditions = splitWithEmptyCheck(arg[len("--conditions="):], ",")
case strings.HasPrefix(arg, "--public-path=") && buildOpts != nil:
buildOpts.PublicPath = arg[len("--public-path="):]
case strings.HasPrefix(arg, "--global-name="):
if buildOpts != nil {
buildOpts.GlobalName = arg[len("--global-name="):]
} else {
transformOpts.GlobalName = arg[len("--global-name="):]
}
case arg == "--metafile" && buildOpts != nil && kind == kindExternal:
buildOpts.Metafile = true
case strings.HasPrefix(arg, "--metafile=") && buildOpts != nil && kind == kindInternal:
value := arg[len("--metafile="):]
buildOpts.Metafile = true
extras.metafile = &value
case strings.HasPrefix(arg, "--outfile=") && buildOpts != nil:
buildOpts.Outfile = arg[len("--outfile="):]
case strings.HasPrefix(arg, "--outdir=") && buildOpts != nil:
buildOpts.Outdir = arg[len("--outdir="):]
case strings.HasPrefix(arg, "--outbase=") && buildOpts != nil:
buildOpts.Outbase = arg[len("--outbase="):]
case strings.HasPrefix(arg, "--tsconfig=") && buildOpts != nil:
buildOpts.Tsconfig = arg[len("--tsconfig="):]
case strings.HasPrefix(arg, "--tsconfig-raw="):
if buildOpts != nil {
buildOpts.TsconfigRaw = arg[len("--tsconfig-raw="):]
} else {
transformOpts.TsconfigRaw = arg[len("--tsconfig-raw="):]
}
case strings.HasPrefix(arg, "--entry-names=") && buildOpts != nil:
buildOpts.EntryNames = arg[len("--entry-names="):]
case strings.HasPrefix(arg, "--chunk-names=") && buildOpts != nil:
buildOpts.ChunkNames = arg[len("--chunk-names="):]
case strings.HasPrefix(arg, "--asset-names=") && buildOpts != nil:
buildOpts.AssetNames = arg[len("--asset-names="):]
case strings.HasPrefix(arg, "--define:"):
value := arg[len("--define:"):]
equals := strings.IndexByte(value, '=')
if equals == -1 {
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Missing \"=\" in %q", arg),
"You need to use \"=\" to specify both the original value and the replacement value. "+
"For example, \"--define:DEBUG=true\" replaces \"DEBUG\" with \"true\".",
)
}
if buildOpts != nil {
buildOpts.Define[value[:equals]] = value[equals+1:]
} else {
transformOpts.Define[value[:equals]] = value[equals+1:]
}
case strings.HasPrefix(arg, "--log-override:"):
value := arg[len("--log-override:"):]
equals := strings.IndexByte(value, '=')
if equals == -1 {
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Missing \"=\" in %q", arg),
"You need to use \"=\" to specify both the message name and the log level. "+
"For example, \"--log-override:css-syntax-error=error\" turns all \"css-syntax-error\" log messages into errors.",
)
}
logLevel, err := parseLogLevel(value[equals+1:], arg)
if err != nil {
return parseOptionsExtras{}, err
}
if buildOpts != nil {
buildOpts.LogOverride[value[:equals]] = logLevel
} else {
transformOpts.LogOverride[value[:equals]] = logLevel
}
case strings.HasPrefix(arg, "--supported:"):
value := arg[len("--supported:"):]
equals := strings.IndexByte(value, '=')
if equals == -1 {
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Missing \"=\" in %q", arg),
"You need to use \"=\" to specify both the name of the feature and whether it is supported or not. "+
"For example, \"--supported:arrow=false\" marks arrow functions as unsupported.",
)
}
if isSupported, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else if buildOpts != nil {
buildOpts.Supported[value[:equals]] = isSupported
} else {
transformOpts.Supported[value[:equals]] = isSupported
}
case strings.HasPrefix(arg, "--pure:"):
value := arg[len("--pure:"):]
if buildOpts != nil {
buildOpts.Pure = append(buildOpts.Pure, value)
} else {
transformOpts.Pure = append(transformOpts.Pure, value)
}
case strings.HasPrefix(arg, "--loader:") && buildOpts != nil:
value := arg[len("--loader:"):]
equals := strings.IndexByte(value, '=')
if equals == -1 {
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Missing \"=\" in %q", arg),
"You need to specify the file extension that the loader applies to. "+
"For example, \"--loader:.js=jsx\" applies the \"jsx\" loader to files with the \".js\" extension.",
)
}
ext, text := value[:equals], value[equals+1:]
loader, err := cli_helpers.ParseLoader(text)
if err != nil {
return parseOptionsExtras{}, err
}
buildOpts.Loader[ext] = loader
case strings.HasPrefix(arg, "--loader="):
value := arg[len("--loader="):]
loader, err := cli_helpers.ParseLoader(value)
if err != nil {
return parseOptionsExtras{}, err
}
if loader == api.LoaderFile || loader == api.LoaderCopy {
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("%q is not supported when transforming stdin", arg),
fmt.Sprintf("Using esbuild to transform stdin only generates one output file, so you cannot use the %q loader "+
"since that needs to generate two output files.", value),
)
}
if buildOpts != nil {
if buildOpts.Stdin == nil {
buildOpts.Stdin = &api.StdinOptions{}
}
buildOpts.Stdin.Loader = loader
} else {
transformOpts.Loader = loader
}
case strings.HasPrefix(arg, "--target="):
target, engines, err := parseTargets(splitWithEmptyCheck(arg[len("--target="):], ","), arg)
if err != nil {
return parseOptionsExtras{}, err
}
if buildOpts != nil {
buildOpts.Target = target
buildOpts.Engines = engines
} else {
transformOpts.Target = target
transformOpts.Engines = engines
}
case strings.HasPrefix(arg, "--out-extension:") && buildOpts != nil:
value := arg[len("--out-extension:"):]
equals := strings.IndexByte(value, '=')
if equals == -1 {
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Missing \"=\" in %q", arg),
"You need to use either \"--out-extension:.js=...\" or \"--out-extension:.css=...\" "+
"to specify the file type that the output extension applies to .",
)
}
if buildOpts.OutExtension == nil {
buildOpts.OutExtension = make(map[string]string)
}
buildOpts.OutExtension[value[:equals]] = value[equals+1:]
case strings.HasPrefix(arg, "--platform="):
value := arg[len("--platform="):]
var platform api.Platform
switch value {
case "browser":
platform = api.PlatformBrowser
case "node":
platform = api.PlatformNode
case "neutral":
platform = api.PlatformNeutral
default:
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Invalid value %q in %q", value, arg),
"Valid values are \"browser\", \"node\", or \"neutral\".",
)
}
if buildOpts != nil {
buildOpts.Platform = platform
} else {
transformOpts.Platform = platform
}
case strings.HasPrefix(arg, "--format="):
value := arg[len("--format="):]
var format api.Format
switch value {
case "iife":
format = api.FormatIIFE
case "cjs":
format = api.FormatCommonJS
case "esm":
format = api.FormatESModule
default:
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Invalid value %q in %q", value, arg),
"Valid values are \"iife\", \"cjs\", or \"esm\".",
)
}
if buildOpts != nil {
buildOpts.Format = format
} else {
transformOpts.Format = format
}
case strings.HasPrefix(arg, "--packages=") && buildOpts != nil:
value := arg[len("--packages="):]
var packages api.Packages
if value == "external" {
packages = api.PackagesExternal
} else {
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Invalid value %q in %q", value, arg),
"The only valid value is \"external\".",
)
}
buildOpts.Packages = packages
case strings.HasPrefix(arg, "--external:") && buildOpts != nil:
buildOpts.External = append(buildOpts.External, arg[len("--external:"):])
case strings.HasPrefix(arg, "--inject:") && buildOpts != nil:
buildOpts.Inject = append(buildOpts.Inject, arg[len("--inject:"):])
case strings.HasPrefix(arg, "--alias:") && buildOpts != nil:
value := arg[len("--alias:"):]
equals := strings.IndexByte(value, '=')
if equals == -1 {
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Missing \"=\" in %q", arg),
"You need to use \"=\" to specify both the original package name and the replacement package name. "+
"For example, \"--alias:old=new\" replaces package \"old\" with package \"new\".",
)
}
if buildOpts.Alias == nil {
buildOpts.Alias = make(map[string]string)
}
buildOpts.Alias[value[:equals]] = value[equals+1:]
case strings.HasPrefix(arg, "--jsx="):
value := arg[len("--jsx="):]
var mode api.JSX
switch value {
case "transform":
mode = api.JSXTransform
case "preserve":
mode = api.JSXPreserve
case "automatic":
mode = api.JSXAutomatic
default:
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Invalid value %q in %q", value, arg),
"Valid values are \"transform\", \"automatic\", or \"preserve\".",
)
}
if buildOpts != nil {
buildOpts.JSX = mode
} else {
transformOpts.JSX = mode
}
case strings.HasPrefix(arg, "--jsx-factory="):
value := arg[len("--jsx-factory="):]
if buildOpts != nil {
buildOpts.JSXFactory = value
} else {
transformOpts.JSXFactory = value
}
case strings.HasPrefix(arg, "--jsx-fragment="):
value := arg[len("--jsx-fragment="):]
if buildOpts != nil {
buildOpts.JSXFragment = value
} else {
transformOpts.JSXFragment = value
}
case strings.HasPrefix(arg, "--jsx-import-source="):
value := arg[len("--jsx-import-source="):]
if buildOpts != nil {
buildOpts.JSXImportSource = value
} else {
transformOpts.JSXImportSource = value
}
case isBoolFlag(arg, "--jsx-dev"):
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else if buildOpts != nil {
buildOpts.JSXDev = value
} else {
transformOpts.JSXDev = value
}
case isBoolFlag(arg, "--jsx-side-effects"):
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else if buildOpts != nil {
buildOpts.JSXSideEffects = value
} else {
transformOpts.JSXSideEffects = 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 parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Missing \"=\" in %q", arg),
"You need to use either \"--banner:js=...\" or \"--banner:css=...\" to specify the language that the banner applies to.",
)
}
buildOpts.Banner[value[:equals]] = value[equals+1:]
case strings.HasPrefix(arg, "--footer:") && buildOpts != nil:
value := arg[len("--footer:"):]
equals := strings.IndexByte(value, '=')
if equals == -1 {
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Missing \"=\" in %q", arg),
"You need to use either \"--footer:js=...\" or \"--footer:css=...\" to specify the language that the footer applies to.",
)
}
buildOpts.Footer[value[:equals]] = value[equals+1:]
case strings.HasPrefix(arg, "--log-limit="):
value := arg[len("--log-limit="):]
limit, err := strconv.Atoi(value)
if err != nil || limit < 0 {
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Invalid value %q in %q", value, arg),
"The log limit must be a non-negative integer.",
)
}
if buildOpts != nil {
buildOpts.LogLimit = limit
} else {
transformOpts.LogLimit = limit
}
case strings.HasPrefix(arg, "--line-limit="):
value := arg[len("--line-limit="):]
limit, err := strconv.Atoi(value)
if err != nil || limit < 0 {
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Invalid value %q in %q", value, arg),
"The line limit must be a non-negative integer.",
)
}
if buildOpts != nil {
buildOpts.LineLimit = limit
} else {
transformOpts.LineLimit = limit
}
// Make sure this stays in sync with "PrintErrorToStderr"
case isBoolFlag(arg, "--color"):
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else {
var color *api.StderrColor
if buildOpts != nil {
color = &buildOpts.Color
} else {
color = &transformOpts.Color
}
if value {
*color = api.ColorAlways
} else {
*color = api.ColorNever
}
}
// Make sure this stays in sync with "PrintErrorToStderr"
case strings.HasPrefix(arg, "--log-level="):
value := arg[len("--log-level="):]
logLevel, err := parseLogLevel(value, arg)
if err != nil {
return parseOptionsExtras{}, err
}
if buildOpts != nil {
buildOpts.LogLevel = logLevel
} else {
transformOpts.LogLevel = logLevel
}
case strings.HasPrefix(arg, "'--"):
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Unexpected single quote character before flag: %s", arg),
"This typically happens when attempting to use single quotes to quote arguments with a shell that doesn't recognize single quotes. "+
"Try using double quote characters to quote arguments instead.",
)
case !strings.HasPrefix(arg, "-") && buildOpts != nil:
if equals := strings.IndexByte(arg, '='); equals != -1 {
buildOpts.EntryPointsAdvanced = append(buildOpts.EntryPointsAdvanced, api.EntryPoint{
OutputPath: arg[:equals],
InputPath: arg[equals+1:],
})
} else {
buildOpts.EntryPoints = append(buildOpts.EntryPoints, arg)
}
default:
bare := map[string]bool{
"allow-overwrite": true,
"bundle": true,
"ignore-annotations": true,
"jsx-dev": true,
"jsx-side-effects": true,
"keep-names": true,
"minify-identifiers": true,
"minify-syntax": true,
"minify-whitespace": true,
"minify": true,
"preserve-symlinks": true,
"sourcemap": true,
"splitting": true,
"watch": true,
}
equals := map[string]bool{
"allow-overwrite": true,
"asset-names": true,
"banner": true,
"bundle": true,
"certfile": true,
"charset": true,
"chunk-names": true,
"color": true,
"conditions": true,
"drop-labels": true,
"entry-names": true,
"footer": true,
"format": true,
"global-name": true,
"ignore-annotations": true,
"jsx-factory": true,
"jsx-fragment": true,
"jsx-import-source": true,
"jsx": true,
"keep-names": true,
"keyfile": true,
"legal-comments": true,
"loader": true,
"log-level": true,
"log-limit": true,
"main-fields": true,
"mangle-cache": true,
"mangle-props": true,
"mangle-quoted": true,
"metafile": true,
"minify-identifiers": true,
"minify-syntax": true,
"minify-whitespace": true,
"minify": true,
"outbase": true,
"outdir": true,
"outfile": true,
"packages": true,
"platform": true,
"preserve-symlinks": true,
"public-path": true,
"reserve-props": true,
"resolve-extensions": true,
"serve-fallback": true,
"serve": true,
"servedir": true,
"source-root": true,
"sourcefile": true,
"sourcemap": true,
"sources-content": true,
"splitting": true,
"target": true,
"tree-shaking": true,
"tsconfig-raw": true,
"tsconfig": true,
"watch": true,
}
colon := map[string]bool{
"alias": true,
"banner": true,
"define": true,
"drop": true,
"external": true,
"footer": true,
"inject": true,
"loader": true,
"log-override": true,
"out-extension": true,
"pure": true,
"supported": true,
}
note := ""
// Try to provide helpful hints when we can recognize the mistake
switch {
case arg == "-o":
note = "Use \"--outfile=\" to configure the output file instead of \"-o\"."
case arg == "-v":
note = "Use \"--log-level=verbose\" to generate verbose logs instead of \"-v\"."
case strings.HasPrefix(arg, "--"):
if i := strings.IndexByte(arg, '='); i != -1 && colon[arg[2:i]] {
note = fmt.Sprintf("Use %q instead of %q. Flags that can be re-specified multiple times use \":\" instead of \"=\".",
arg[:i]+":"+arg[i+1:], arg)
}
if i := strings.IndexByte(arg, ':'); i != -1 && equals[arg[2:i]] {
note = fmt.Sprintf("Use %q instead of %q. Flags that can only be specified once use \"=\" instead of \":\".",
arg[:i]+"="+arg[i+1:], arg)
}
case strings.HasPrefix(arg, "-"):
isValid := bare[arg[1:]]
fix := "-" + arg
if i := strings.IndexByte(arg, '='); i != -1 && equals[arg[1:i]] {
isValid = true
} else if i != -1 && colon[arg[1:i]] {
isValid = true
fix = fmt.Sprintf("-%s:%s", arg[:i], arg[i+1:])
} else if i := strings.IndexByte(arg, ':'); i != -1 && colon[arg[1:i]] {
isValid = true
} else if i != -1 && equals[arg[1:i]] {
isValid = true
fix = fmt.Sprintf("-%s=%s", arg[:i], arg[i+1:])
}
if isValid {
note = fmt.Sprintf("Use %q instead of %q. Flags are always specified with two dashes instead of one dash.",
fix, arg)
}
}
if buildOpts != nil {
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(fmt.Sprintf("Invalid build flag: %q", arg), note)
} else {
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(fmt.Sprintf("Invalid transform flag: %q", arg), note)
}
}
}
// If we're building, the last source map flag is "--sourcemap", and there
// is no output path, change the source map option to "inline" because we're
// going to be writing to stdout which can only represent a single file.
if buildOpts != nil && hasBareSourceMapFlag && buildOpts.Outfile == "" && buildOpts.Outdir == "" {
buildOpts.Sourcemap = api.SourceMapInline
}
return
}
func parseTargets(targets []string, arg string) (target api.Target, engines []api.Engine, err *cli_helpers.ErrorWithNote) {
validTargets := map[string]api.Target{
"esnext": api.ESNext,
"es5": api.ES5,
"es6": api.ES2015,
"es2015": api.ES2015,
"es2016": api.ES2016,
"es2017": api.ES2017,
"es2018": api.ES2018,
"es2019": api.ES2019,
"es2020": api.ES2020,
"es2021": api.ES2021,
"es2022": api.ES2022,
}
outer:
for _, value := range targets {
if valid, ok := validTargets[strings.ToLower(value)]; ok {
target = valid
continue
}
for engine, name := range validEngines {
if strings.HasPrefix(value, engine) {
version := value[len(engine):]
if version == "" {
return 0, nil, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Target %q is missing a version number in %q", value, arg),
"",
)
}
engines = append(engines, api.Engine{Name: name, Version: version})
continue outer
}
}
engines := make([]string, 0, len(validEngines))
engines = append(engines, "\"esN\"")
for key := range validEngines {
engines = append(engines, fmt.Sprintf("%q", key+"N"))
}
sort.Strings(engines)
return 0, nil, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Invalid target %q in %q", value, arg),
fmt.Sprintf("Valid values are %s, or %s where N is a version number.",
strings.Join(engines[:len(engines)-1], ", "), engines[len(engines)-1]),
)
}
return
}
// This returns either BuildOptions, TransformOptions, or an error
func parseOptionsForRun(osArgs []string) (*api.BuildOptions, *api.TransformOptions, parseOptionsExtras, *cli_helpers.ErrorWithNote) {
// If there's an entry point or we're bundling, then we're building
for _, arg := range osArgs {
if !strings.HasPrefix(arg, "-") || arg == "--bundle" {
options := newBuildOptions()
// Apply defaults appropriate for the CLI
options.LogLimit = 6
options.LogLevel = api.LogLevelInfo
options.Write = true
extras, err := parseOptionsImpl(osArgs, &options, nil, kindInternal)
if err != nil {
return nil, nil, parseOptionsExtras{}, err
}
return &options, nil, extras, nil
}
}
// Otherwise, we're transforming
options := newTransformOptions()
// Apply defaults appropriate for the CLI
options.LogLimit = 6
options.LogLevel = api.LogLevelInfo
_, err := parseOptionsImpl(osArgs, nil, &options, kindInternal)
if err != nil {
return nil, nil, parseOptionsExtras{}, err
}
if options.Sourcemap != api.SourceMapNone && options.Sourcemap != api.SourceMapInline {
var sourceMapMode string
switch options.Sourcemap {
case api.SourceMapExternal:
sourceMapMode = "external"
case api.SourceMapInlineAndExternal:
sourceMapMode = "both"
case api.SourceMapLinked:
sourceMapMode = "linked"
}
return nil, nil, parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Use \"--sourcemap\" instead of \"--sourcemap=%s\" when transforming stdin", sourceMapMode),
fmt.Sprintf("Using esbuild to transform stdin only generates one output file. You cannot use the %q source map mode "+
"since that needs to generate two output files.", sourceMapMode),
)
}
return nil, &options, parseOptionsExtras{}, nil
}
func splitWithEmptyCheck(s string, sep string) []string {
// Special-case the empty string to return [] instead of [""]
if s == "" {
return []string{}
}
return strings.Split(s, sep)
}
func runImpl(osArgs []string) int {
analyze := false
analyzeVerbose := false
end := 0
for _, arg := range osArgs {
// Special-case running a server
if arg == "--serve" ||
strings.HasPrefix(arg, "--serve=") ||
strings.HasPrefix(arg, "--servedir=") ||
strings.HasPrefix(arg, "--serve-fallback=") {
serveImpl(osArgs)
return 1 // There was an error starting the server if we get here
}
// Special-case analyze just for our CLI
if arg == "--analyze" {
analyze = true
analyzeVerbose = false
continue
}
if arg == "--analyze=verbose" {
analyze = true
analyzeVerbose = true
continue
}
osArgs[end] = arg
end++
}
osArgs = osArgs[:end]
buildOptions, transformOptions, extras, err := parseOptionsForRun(osArgs)
switch {
case buildOptions != nil:
// Read the "NODE_PATH" from the environment. This is part of node's
// module resolution algorithm. Documentation for this can be found here:
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
if value, ok := os.LookupEnv("NODE_PATH"); ok {
separator := ":"
if fs.CheckIfWindows() {
// On Windows, NODE_PATH is delimited by semicolons instead of colons
separator = ";"
}
buildOptions.NodePaths = splitWithEmptyCheck(value, separator)
}
// Read from stdin when there are no entry points
if len(buildOptions.EntryPoints)+len(buildOptions.EntryPointsAdvanced) == 0 {
if buildOptions.Stdin == nil {
buildOptions.Stdin = &api.StdinOptions{}
}
bytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
logger.PrintErrorToStderr(osArgs, fmt.Sprintf(
"Could not read from stdin: %s", err.Error()))
return 1
}
buildOptions.Stdin.Contents = string(bytes)
buildOptions.Stdin.ResolveDir, _ = os.Getwd()
} else if buildOptions.Stdin != nil {
if buildOptions.Stdin.Sourcefile != "" {
logger.PrintErrorToStderr(osArgs,
"\"sourcefile\" only applies when reading from stdin")
} else {
logger.PrintErrorToStderr(osArgs,
"\"loader\" without extension only applies when reading from stdin")
}
return 1
}
// Validate the metafile absolute path and directory ahead of time so we
// don't write any output files if it's incorrect. That makes this API
// option consistent with how we handle all other API options.
var writeMetafile func(string)
if extras.metafile != nil {
var metafileAbsPath string
var metafileAbsDir string
if buildOptions.Outfile == "" && buildOptions.Outdir == "" {
// Cannot use "metafile" when writing to stdout
logger.PrintErrorToStderr(osArgs, "Cannot use \"metafile\" without an output path")
return 1
}
realFS, realFSErr := fs.RealFS(fs.RealFSOptions{AbsWorkingDir: buildOptions.AbsWorkingDir})
if realFSErr == nil {
absPath, ok := realFS.Abs(*extras.metafile)
if !ok {
logger.PrintErrorToStderr(osArgs, fmt.Sprintf("Invalid metafile path: %s", *extras.metafile))
return 1
}
metafileAbsPath = absPath
metafileAbsDir = realFS.Dir(absPath)
} else {
// Don't fail in this case since the error will be reported by "api.Build"
}
writeMetafile = func(json string) {
if json == "" || realFSErr != nil {
return // Don't write out the metafile on build errors
}
if err != nil {
// This should already have been checked above
panic(err.Text)
}
fs.BeforeFileOpen()
defer fs.AfterFileClose()
if err := fs.MkdirAll(realFS, metafileAbsDir, 0755); err != nil {
logger.PrintErrorToStderr(osArgs, fmt.Sprintf(
"Failed to create output directory: %s", err.Error()))
} else {
if err := ioutil.WriteFile(metafileAbsPath, []byte(json), 0644); err != nil {
logger.PrintErrorToStderr(osArgs, fmt.Sprintf(
"Failed to write to output file: %s", err.Error()))
}
}
}
}
// Also validate the mangle cache absolute path and directory ahead of time
// for the same reason
var writeMangleCache func(map[string]interface{})
if extras.mangleCache != nil {
var mangleCacheAbsPath string
var mangleCacheAbsDir string
var mangleCacheOrder []string
realFS, realFSErr := fs.RealFS(fs.RealFSOptions{AbsWorkingDir: buildOptions.AbsWorkingDir})
if realFSErr == nil {
absPath, ok := realFS.Abs(*extras.mangleCache)
if !ok {
logger.PrintErrorToStderr(osArgs, fmt.Sprintf("Invalid mangle cache path: %s", *extras.mangleCache))
return 1
}
mangleCacheAbsPath = absPath
mangleCacheAbsDir = realFS.Dir(absPath)
buildOptions.MangleCache, mangleCacheOrder = parseMangleCache(osArgs, realFS, *extras.mangleCache)
if buildOptions.MangleCache == nil {
return 1 // Stop now if parsing failed
}
} else {
// Don't fail in this case since the error will be reported by "api.Build"
}
writeMangleCache = func(mangleCache map[string]interface{}) {
if mangleCache == nil || realFSErr != nil {
return // Don't write out the metafile on build errors
}
if err != nil {
// This should already have been checked above
panic(err.Text)
}
fs.BeforeFileOpen()
defer fs.AfterFileClose()
if err := fs.MkdirAll(realFS, mangleCacheAbsDir, 0755); err != nil {
logger.PrintErrorToStderr(osArgs, fmt.Sprintf(
"Failed to create output directory: %s", err.Error()))
} else {
bytes := printMangleCache(mangleCache, mangleCacheOrder, buildOptions.Charset == api.CharsetASCII)
if err := ioutil.WriteFile(mangleCacheAbsPath, bytes, 0644); err != nil {
logger.PrintErrorToStderr(osArgs, fmt.Sprintf(
"Failed to write to output file: %s", err.Error()))
}
}
}
}
// Print metafile analysis after the build if it's enabled
var printAnalysis func(metafile string)
if analyze {
printAnalysis = func(metafile string) {
if metafile == "" {
return
}
logger.PrintTextWithColor(os.Stderr, logger.OutputOptionsForArgs(osArgs).Color, func(colors logger.Colors) string {
return api.AnalyzeMetafile(metafile, api.AnalyzeMetafileOptions{
Color: colors != logger.Colors{},
Verbose: analyzeVerbose,
})
})
os.Stderr.WriteString("\n")
}
// Always generate a metafile if we're analyzing, even if it won't be written out
buildOptions.Metafile = true
}
// Handle post-build actions with a plugin so they also work in watch mode
buildOptions.Plugins = append(buildOptions.Plugins, api.Plugin{
Name: "PostBuildActions",
Setup: func(build api.PluginBuild) {
build.OnEnd(func(result *api.BuildResult) (api.OnEndResult, error) {
// Print our analysis of the metafile
if printAnalysis != nil {
printAnalysis(result.Metafile)
}
// Write the metafile to the file system
if writeMetafile != nil {
writeMetafile(result.Metafile)
}
// Write the mangle cache to the file system
if writeMangleCache != nil {
writeMangleCache(result.MangleCache)
}
return api.OnEndResult{}, nil
})
},
})
// Handle watch mode
if extras.watch {
ctx, err := api.Context(*buildOptions)
// Only start watching if the build options passed validation
if err != nil {
return 1
}
ctx.Watch(api.WatchOptions{})
// Do not exit if we're in watch mode
<-make(chan struct{})
}
// This prints the summary which the context API doesn't do
result := api.Build(*buildOptions)
// Return a non-zero exit code if there were errors
if len(result.Errors) > 0 {
return 1
}
case transformOptions != nil:
// Read the input from stdin
bytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
logger.PrintErrorToStderr(osArgs, fmt.Sprintf(
"Could not read from stdin: %s", err.Error()))
return 1
}
// Run the transform and stop if there were errors
result := api.Transform(string(bytes), *transformOptions)
if len(result.Errors) > 0 {
return 1
}
// Write the output to stdout
os.Stdout.Write(result.Code)
case err != nil:
logger.PrintErrorWithNoteToStderr(osArgs, err.Text, err.Note)
return 1
}
return 0
}
func parseServeOptionsImpl(osArgs []string) (api.ServeOptions, []string, error) {
host := ""
portText := "0"
servedir := ""
keyfile := ""
certfile := ""
fallback := ""
// Filter out server-specific flags
filteredArgs := make([]string, 0, len(osArgs))
for _, arg := range osArgs {
if arg == "--serve" {
// Just ignore this flag
} else if strings.HasPrefix(arg, "--serve=") {
portText = arg[len("--serve="):]
} else if strings.HasPrefix(arg, "--servedir=") {
servedir = arg[len("--servedir="):]
} else if strings.HasPrefix(arg, "--keyfile=") {
keyfile = arg[len("--keyfile="):]
} else if strings.HasPrefix(arg, "--certfile=") {
certfile = arg[len("--certfile="):]
} else if strings.HasPrefix(arg, "--serve-fallback=") {
fallback = arg[len("--serve-fallback="):]
} else {
filteredArgs = append(filteredArgs, arg)
}
}
// Specifying the host is optional
if strings.ContainsRune(portText, ':') {
var err error
host, portText, err = net.SplitHostPort(portText)
if err != nil {
return api.ServeOptions{}, nil, err
}
}
// Parse the port
port, err := strconv.ParseInt(portText, 10, 32)
if err != nil {
return api.ServeOptions{}, nil, err
}
if port < 0 || port > 0xFFFF {
return api.ServeOptions{}, nil, fmt.Errorf("Invalid port number: %s", portText)
}
return api.ServeOptions{
Port: uint16(port),
Host: host,
Servedir: servedir,
Keyfile: keyfile,
Certfile: certfile,
Fallback: fallback,
}, filteredArgs, nil
}
func serveImpl(osArgs []string) {
serveOptions, filteredArgs, err := parseServeOptionsImpl(osArgs)
if err != nil {
logger.PrintErrorWithNoteToStderr(osArgs, err.Error(), "")
return
}
options := newBuildOptions()
// Apply defaults appropriate for the CLI
options.LogLimit = 5
options.LogLevel = api.LogLevelInfo
extras, errWithNote := parseOptionsImpl(filteredArgs, &options, nil, kindInternal)
if errWithNote != nil {
logger.PrintErrorWithNoteToStderr(osArgs, errWithNote.Text, errWithNote.Note)
return
}
serveOptions.OnRequest = func(args api.ServeOnRequestArgs) {
logger.PrintText(os.Stderr, logger.LevelInfo, filteredArgs, func(colors logger.Colors) string {
statusColor := colors.Red
if args.Status >= 200 && args.Status <= 299 {
statusColor = colors.Green
} else if args.Status >= 300 && args.Status <= 399 {
statusColor = colors.Yellow
}
return fmt.Sprintf("%s%s - %q %s%d%s [%dms]%s\n",
colors.Dim, args.RemoteAddress, args.Method+" "+args.Path,
statusColor, args.Status, colors.Dim, args.TimeInMS, colors.Reset)
})
}
// Validate build options
ctx, ctxErr := api.Context(options)
if ctxErr != nil {
return
}
// Try to enable serve mode
if _, err = ctx.Serve(serveOptions); err != nil {
logger.PrintErrorWithNoteToStderr(osArgs, err.Error(), "")
return
}
// Also enable watch mode if it was requested
if extras.watch {
if err := ctx.Watch(api.WatchOptions{}); err != nil {
logger.PrintErrorWithNoteToStderr(osArgs, err.Error(), "")
return
}
}
// Do not exit if we're in serve mode
<-make(chan struct{})
}
func parseLogLevel(value string, arg string) (api.LogLevel, *cli_helpers.ErrorWithNote) {
switch value {
case "verbose":
return api.LogLevelVerbose, nil
case "debug":
return api.LogLevelDebug, nil
case "info":
return api.LogLevelInfo, nil
case "warning":
return api.LogLevelWarning, nil
case "error":
return api.LogLevelError, nil
case "silent":
return api.LogLevelSilent, nil
default:
return api.LogLevelSilent, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Invalid value %q in %q", value, arg),
"Valid values are \"verbose\", \"debug\", \"info\", \"warning\", \"error\", or \"silent\".",
)
}
}