package cli import ( "fmt" "io/ioutil" "os" "sort" "strconv" "strings" "github.com/evanw/esbuild/internal/helpers" "github.com/evanw/esbuild/internal/logger" "github.com/evanw/esbuild/pkg/api" ) func newBuildOptions() api.BuildOptions { return api.BuildOptions{ Loader: make(map[string]api.Loader), Define: make(map[string]string), } } func newTransformOptions() api.TransformOptions { return api.TransformOptions{ Define: make(map[string]string), } } func parseOptionsImpl(osArgs []string, buildOpts *api.BuildOptions, transformOpts *api.TransformOptions) error { hasBareSourceMapFlag := false // Parse the arguments now that we know what we're parsing for _, arg := range osArgs { switch { case arg == "--bundle" && buildOpts != nil: buildOpts.Bundle = true case arg == "--splitting" && buildOpts != nil: buildOpts.Splitting = true case arg == "--minify": if buildOpts != nil { buildOpts.MinifySyntax = true buildOpts.MinifyWhitespace = true buildOpts.MinifyIdentifiers = true } else { transformOpts.MinifySyntax = true transformOpts.MinifyWhitespace = true transformOpts.MinifyIdentifiers = true } case arg == "--minify-syntax": if buildOpts != nil { buildOpts.MinifySyntax = true } else { transformOpts.MinifySyntax = true } case arg == "--minify-whitespace": if buildOpts != nil { buildOpts.MinifyWhitespace = true } else { transformOpts.MinifyWhitespace = true } case arg == "--minify-identifiers": if buildOpts != nil { buildOpts.MinifyIdentifiers = true } else { transformOpts.MinifyIdentifiers = true } 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 fmt.Errorf("Invalid charset value: %q (valid: ascii, utf8)", name) } case arg == "--avoid-tdz": if buildOpts != nil { buildOpts.AvoidTDZ = true } else { transformOpts.AvoidTDZ = true } case arg == "--keep-names": if buildOpts != nil { buildOpts.KeepNames = true } else { transformOpts.KeepNames = true } case arg == "--sourcemap": if buildOpts != nil { buildOpts.Sourcemap = api.SourceMapLinked } else { transformOpts.Sourcemap = api.SourceMapInline } hasBareSourceMapFlag = true case arg == "--sourcemap=external": if buildOpts != nil { buildOpts.Sourcemap = api.SourceMapExternal } else { transformOpts.Sourcemap = api.SourceMapExternal } hasBareSourceMapFlag = false case arg == "--sourcemap=inline": if buildOpts != nil { buildOpts.Sourcemap = api.SourceMapInline } else { transformOpts.Sourcemap = api.SourceMapInline } hasBareSourceMapFlag = false 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 = strings.Split(arg[len("--resolve-extensions="):], ",") case strings.HasPrefix(arg, "--main-fields=") && buildOpts != nil: buildOpts.MainFields = strings.Split(arg[len("--main-fields="):], ",") 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 strings.HasPrefix(arg, "--metafile=") && buildOpts != nil: buildOpts.Metafile = arg[len("--metafile="):] 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=") && transformOpts != nil: transformOpts.TsconfigRaw = arg[len("--tsconfig-raw="):] case strings.HasPrefix(arg, "--define:"): value := arg[len("--define:"):] equals := strings.IndexByte(value, '=') if equals == -1 { return fmt.Errorf("Missing \"=\": %q", value) } if buildOpts != nil { buildOpts.Define[value[:equals]] = value[equals+1:] } else { transformOpts.Define[value[:equals]] = value[equals+1:] } 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 fmt.Errorf("Missing \"=\": %q", value) } ext, text := value[:equals], value[equals+1:] loader, err := helpers.ParseLoader(text) if err != nil { return err } buildOpts.Loader[ext] = loader case strings.HasPrefix(arg, "--loader="): value := arg[len("--loader="):] loader, err := helpers.ParseLoader(value) if err != nil { return err } if loader == api.LoaderFile { return fmt.Errorf("Cannot transform using the \"file\" loader") } 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(strings.Split(arg[len("--target="):], ",")) if err != nil { return 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 fmt.Errorf("Missing \"=\": %q", value) } if buildOpts.OutExtensions == nil { buildOpts.OutExtensions = make(map[string]string) } buildOpts.OutExtensions[value[:equals]] = value[equals+1:] case strings.HasPrefix(arg, "--platform=") && buildOpts != nil: value := arg[len("--platform="):] switch value { case "browser": buildOpts.Platform = api.PlatformBrowser case "node": buildOpts.Platform = api.PlatformNode default: return fmt.Errorf("Invalid platform: %q (valid: browser, node)", value) } case strings.HasPrefix(arg, "--format="): value := arg[len("--format="):] switch value { case "iife": if buildOpts != nil { buildOpts.Format = api.FormatIIFE } else { transformOpts.Format = api.FormatIIFE } case "cjs": if buildOpts != nil { buildOpts.Format = api.FormatCommonJS } else { transformOpts.Format = api.FormatCommonJS } case "esm": if buildOpts != nil { buildOpts.Format = api.FormatESModule } else { transformOpts.Format = api.FormatESModule } default: return fmt.Errorf("Invalid format: %q (valid: iife, cjs, esm)", value) } 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, "--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, "--error-limit="): value := arg[len("--error-limit="):] limit, err := strconv.Atoi(value) if err != nil || limit < 0 { return fmt.Errorf("Invalid error limit: %q", value) } if buildOpts != nil { buildOpts.ErrorLimit = limit } else { transformOpts.ErrorLimit = limit } // Make sure this stays in sync with "PrintErrorToStderr" case strings.HasPrefix(arg, "--color="): value := arg[len("--color="):] var color api.StderrColor switch value { case "false": color = api.ColorNever case "true": color = api.ColorAlways default: return fmt.Errorf("Invalid color: %q (valid: false, true)", value) } if buildOpts != nil { buildOpts.Color = color } else { transformOpts.Color = color } // Make sure this stays in sync with "PrintErrorToStderr" case strings.HasPrefix(arg, "--log-level="): value := arg[len("--log-level="):] var logLevel api.LogLevel switch value { case "info": logLevel = api.LogLevelInfo case "warning": logLevel = api.LogLevelWarning case "error": logLevel = api.LogLevelError case "silent": logLevel = api.LogLevelSilent default: return fmt.Errorf("Invalid log level: %q (valid: info, warning, error, silent)", arg) } if buildOpts != nil { buildOpts.LogLevel = logLevel } else { transformOpts.LogLevel = logLevel } case !strings.HasPrefix(arg, "-") && buildOpts != nil: buildOpts.EntryPoints = append(buildOpts.EntryPoints, arg) default: if buildOpts != nil { return fmt.Errorf("Invalid build flag: %q", arg) } else { return fmt.Errorf("Invalid transform flag: %q", arg) } } } // 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 nil } func parseTargets(targets []string) (target api.Target, engines []api.Engine, err error) { 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, } validEngines := map[string]api.EngineName{ "chrome": api.EngineChrome, "firefox": api.EngineFirefox, "safari": api.EngineSafari, "edge": api.EngineEdge, "node": api.EngineNode, "ios": api.EngineIOS, } outer: for _, value := range targets { if valid, ok := validTargets[value]; ok { target = valid continue } for engine, name := range validEngines { if strings.HasPrefix(value, engine) { version := value[len(engine):] if version == "" { return 0, nil, fmt.Errorf("Target missing version number: %q", value) } engines = append(engines, api.Engine{Name: name, Version: version}) continue outer } } var engines []string for key := range validEngines { engines = append(engines, key+"N") } sort.Strings(engines) return 0, nil, fmt.Errorf( "Invalid target: %q (valid: esN, "+strings.Join(engines, ", ")+")", value) } return } // This returns either BuildOptions, TransformOptions, or an error func parseOptionsForRun(osArgs []string) (*api.BuildOptions, *api.TransformOptions, error) { // 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.ErrorLimit = 10 options.LogLevel = api.LogLevelInfo options.Write = true err := parseOptionsImpl(osArgs, &options, nil) if err != nil { return nil, nil, err } return &options, nil, nil } } // Otherwise, we're transforming options := newTransformOptions() // Apply defaults appropriate for the CLI options.ErrorLimit = 10 options.LogLevel = api.LogLevelInfo err := parseOptionsImpl(osArgs, nil, &options) if err != nil { return nil, nil, err } if options.Sourcemap != api.SourceMapNone && options.Sourcemap != api.SourceMapInline { return nil, nil, fmt.Errorf("Must use \"inline\" source map when transforming stdin") } return nil, &options, nil } func runImpl(osArgs []string) int { buildOptions, transformOptions, err := parseOptionsForRun(osArgs) switch { case buildOptions != nil: // Read from stdin when there are no entry points if len(buildOptions.EntryPoints) == 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 } // Run the build and stop if there were errors result := api.Build(*buildOptions) 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.PrintErrorToStderr(osArgs, err.Error()) return 1 } return 0 }