All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.gwt.client.JsRunnerMain Maven / Gradle / Ivy

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

There is a newer version: v20240317
Show newest version
/*
 * Copyright 2016 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.javascript.jscomp.gwt.client;

import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.base.Strings.nullToEmpty;
import static com.google.javascript.jscomp.AbstractCommandLineRunner.createDefineOrTweakReplacements;
import static com.google.javascript.jscomp.AbstractCommandLineRunner.createJsModules;
import static com.google.javascript.jscomp.AbstractCommandLineRunner.parseModuleWrappers;

import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.javascript.jscomp.AbstractCommandLineRunner.JsModuleSpec;
import com.google.javascript.jscomp.BasicErrorManager;
import com.google.javascript.jscomp.CheckLevel;
import com.google.javascript.jscomp.ClosureCodingConvention;
import com.google.javascript.jscomp.CompilationLevel;
import com.google.javascript.jscomp.Compiler;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.CompilerOptions.IsolationMode;
import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
import com.google.javascript.jscomp.CompilerOptions.TracerMode;
import com.google.javascript.jscomp.DefaultExterns;
import com.google.javascript.jscomp.DependencyOptions;
import com.google.javascript.jscomp.DependencyOptions.DependencyMode;
import com.google.javascript.jscomp.DiagnosticGroups;
import com.google.javascript.jscomp.DiagnosticType;
import com.google.javascript.jscomp.JSError;
import com.google.javascript.jscomp.JSModule;
import com.google.javascript.jscomp.ModuleIdentifier;
import com.google.javascript.jscomp.PropertyRenamingPolicy;
import com.google.javascript.jscomp.ShowByPathWarningsGuard;
import com.google.javascript.jscomp.SourceFile;
import com.google.javascript.jscomp.SourceMap;
import com.google.javascript.jscomp.SourceMapInput;
import com.google.javascript.jscomp.VariableRenamingPolicy;
import com.google.javascript.jscomp.WarningLevel;
import com.google.javascript.jscomp.deps.ModuleLoader.ResolutionMode;
import com.google.javascript.jscomp.deps.SourceCodeEscapers;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.jscomp.resources.ResourceLoader;
import elemental2.core.JsArray;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import jsinterop.annotations.JsMethod;
import jsinterop.annotations.JsOverlay;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsType;
import jsinterop.base.Any;
import jsinterop.base.Js;
import jsinterop.base.JsPropertyMap;

/** Runner for the GWT-compiled JSCompiler. */
public final class JsRunnerMain {
  private static final Logger phaseLogger =
      Logger.getLogger("com.google.javascript.jscomp.PhaseOptimizer");

  private static final CompilationLevel DEFAULT_COMPILATION_LEVEL =
      CompilationLevel.SIMPLE_OPTIMIZATIONS;

  private static final String OUTPUT_MARKER = "%output%";
  private static final String OUTPUT_MARKER_JS_STRING = "%output|jsstring%";

  private static final String EXTERNS_PREFIX = "externs/";

  @JsType(
      isNative = true,
      name = "com_google_javascript_jscomp_gwt_client_JsRunnerMain$Flags",
      namespace = JsPackage.GLOBAL)
  abstract static class Flags {
    boolean angularPass;
    boolean applyInputSourceMaps;
    boolean assumeFunctionWrapper;
    boolean checksOnly;
    String[] chunk;
    String[] chunkWrapper;
    String chunkOutputPathPrefix;
    String compilationLevel;
    Object createSourceMap;
    boolean dartPass;
    boolean debug;
    String[] define;
    String dependencyMode;
    String[] entryPoint;
    String env;
    boolean exportLocalPropertyDefinitions;
    Object[] externs;
    String[] extraAnnotationName;
    String[] forceInjectLibraries;
    String[] formatting;
    boolean generateExports;
    String[] hideWarningsFor;
    boolean injectLibraries;
    String isolationMode;
    String[] js;
    String[] jscompError;
    String[] jscompOff;
    String[] jscompWarning;
    String[] jsModuleRoot;
    String jsOutputFile;
    String languageIn;
    String languageOut;
    String moduleResolution;
    @Deprecated boolean newTypeInf;
    String outputWrapper;
    String packageJsonEntryNames;
    boolean parseInlineSourceMaps;
    @Deprecated boolean polymerPass;
    Double polymerVersion; // nb. nullable JS number represented by java.lang.Double in GWT.
    boolean preserveTypeAnnotations;
    boolean processClosurePrimitives;
    boolean processCommonJsModules;
    boolean renaming;
    String renamePrefixNamespace;
    String renameVariablePrefix;
    boolean rewritePolyfills;
    boolean sourceMapIncludeContent;
    boolean strictModeInput;
    String tracerMode;
    boolean useTypesForOptimization;
    String warningLevel;

    // These flags do not match the Java compiler JAR.
    @Deprecated File[] jsCode;
    JsPropertyMap defines;

    @JsOverlay
    static Flags create() {
      return Js.uncheckedCast(JsPropertyMap.of());
    }
  }

  /**
   * defaultFlags must have a value set for each field. Otherwise, GWT has no way to create the
   * fields inside Flags (as it's native). If Flags is not-native, GWT eats its field names anyway.
   */
  private static Flags defaultFlags;

  /**
   * Lazy initialize due to GWT. If things are exported then Object is not available when the static
   * initialization runs.
   */
  private static Flags getDefaultFlags() {
    if (defaultFlags != null) {
      return defaultFlags;
    }
    defaultFlags = Flags.create();
    defaultFlags.angularPass = false;
    defaultFlags.applyInputSourceMaps = true;
    defaultFlags.assumeFunctionWrapper = false;
    defaultFlags.checksOnly = false;
    defaultFlags.chunk = null;
    defaultFlags.chunkWrapper = null;
    defaultFlags.chunkOutputPathPrefix = "./";
    defaultFlags.compilationLevel = "SIMPLE";
    defaultFlags.createSourceMap = true;
    defaultFlags.dartPass = false;
    defaultFlags.debug = false;
    defaultFlags.define = null;
    defaultFlags.defines = null;
    defaultFlags.dependencyMode = null;
    defaultFlags.entryPoint = null;
    defaultFlags.env = "BROWSER";
    defaultFlags.exportLocalPropertyDefinitions = false;
    defaultFlags.extraAnnotationName = null;
    defaultFlags.externs = null;
    defaultFlags.forceInjectLibraries = null;
    defaultFlags.formatting = null;
    defaultFlags.generateExports = false;
    defaultFlags.hideWarningsFor = null;
    defaultFlags.injectLibraries = true;
    defaultFlags.js = null;
    defaultFlags.jsCode = null;
    defaultFlags.jscompError = null;
    defaultFlags.jscompOff = null;
    defaultFlags.jscompWarning = null;
    defaultFlags.jsModuleRoot = null;
    defaultFlags.jsOutputFile = "compiled.js";
    defaultFlags.languageIn = "ECMASCRIPT_2017";
    defaultFlags.languageOut = "ECMASCRIPT5";
    defaultFlags.moduleResolution = "BROWSER";
    defaultFlags.newTypeInf = false;
    defaultFlags.isolationMode = "NONE";
    defaultFlags.outputWrapper = null;
    defaultFlags.packageJsonEntryNames = null;
    defaultFlags.parseInlineSourceMaps = true;
    defaultFlags.polymerPass = false;
    defaultFlags.polymerVersion = null;
    defaultFlags.preserveTypeAnnotations = false;
    defaultFlags.processClosurePrimitives = true;
    defaultFlags.processCommonJsModules = false;
    defaultFlags.renamePrefixNamespace = null;
    defaultFlags.renameVariablePrefix = null;
    defaultFlags.renaming = true;
    defaultFlags.rewritePolyfills = true;
    defaultFlags.sourceMapIncludeContent = false;
    defaultFlags.strictModeInput = true;
    defaultFlags.tracerMode = "OFF";
    defaultFlags.warningLevel = "DEFAULT";
    defaultFlags.useTypesForOptimization = true;

    return defaultFlags;
  }

  /** Properties here should match the AbstractCommandLineRunner.JsonFileSpec */
  @JsType(
      isNative = true,
      name = "com_google_javascript_jscomp_gwt_client_JsRunnerMain$File",
      namespace = JsPackage.GLOBAL)
  abstract static class File {
    String path;
    String src;
    String sourceMap;
    String webpackId;

    @JsOverlay
    static File create() {
      return Js.uncheckedCast(JsPropertyMap.of());
    }
  }

  @JsType(
      isNative = true,
      name = "com_google_javascript_jscomp_gwt_client_JsRunnerMain$ChunkOutput",
      namespace = JsPackage.GLOBAL)
  abstract static class ChunkOutput {
    @Deprecated String compiledCode;
    @Deprecated String sourceMap;
    JsArray compiledFiles;
    JsArray> errors;
    JsArray> warnings;

    @JsOverlay
    static ChunkOutput create() {
      return Js.uncheckedCast(JsPropertyMap.of());
    }
  }

  /** Reliably returns a string array from the flags/key combo. */
  @JsMethod
  private static String[] toStringArray(Object value) {
    if (value == null) {
      return new String[0];
    } else if (value instanceof Any[]) {
      return Js.uncheckedCast(value);
    }
    return new String[] {Js.uncheckedCast(value)};
  }

  /**
   * Validates that the values of this {@code JsMap} are primitives: either number, string or
   * boolean. Note that {@code typeof null} is object.
   */
  private static void validatePrimitiveTypes(JsPropertyMap jsmap) {
    jsmap.forEach(
        (key) -> {
          String type = Js.typeof(jsmap.get(key));
          switch (type) {
            case "number":
            case "boolean":
            case "string":
              return;
            default:
              throw new IllegalArgumentException(
                  "Type of define `" + key + "` unsupported: " + type);
          }
        });
  }

  /**
   * @param jsFilePaths Array of file paths. If running under NodeJS, they will be loaded via the
   *     native node fs module.
   * @return Array of File objects. If called without running under node, return null to indicate
   *     failure.
   */
  @JsMethod
  private static native File[] filesFromPaths(String[] jsFilePaths) /*-{
    if (!(typeof process === 'object' && process.version)) {
      return null;
    }
    var jsFiles = [];
    for (var i = 0; i < jsFilePaths.length; i++) {
      if (typeof process === 'object' && process.version) {
        jsFiles.push({
          path: jsFilePaths[i],
          src: require('fs').readFileSync(jsFilePaths[i], 'utf8')
        });
      }
    }
    return jsFiles;
  }-*/;

  private static JsPropertyMap createError(
      String file, String description, String type, int lineNo, int charNo) {
    JsPropertyMap result = JsPropertyMap.of();
    result.set("file", file);
    result.set("description", description);
    result.set("type", type);
    result.set("lineNo", lineNo);
    result.set("charNo", charNo);
    return result;
  }

  /** Convert a list of {@link JSError} instances to a JS array containing plain objects. */
  private static JsArray> toNativeErrorArray(List errors) {
    JsArray> out = new JsArray<>();
    for (JSError error : errors) {
      DiagnosticType type = error.getType();
      out.push(
          createError(
              error.getSourceName(),
              error.getDescription(),
              type != null ? type.key : null,
              error.getLineNumber(),
              error.getCharno()));
    }
    return out;
  }

  /** Generates the output code, taking into account the passed {@code flags}. */
  private static ChunkOutput writeChunkOutput(
      Compiler compiler, Flags flags, List chunks) {
    JsArray outputFiles = new JsArray<>();
    ChunkOutput output = ChunkOutput.create();

    Map parsedModuleWrappers =
        parseModuleWrappers(Arrays.asList(toStringArray(flags.chunkWrapper)), chunks);

    for (JSModule c : chunks) {
      if (flags.createSourceMap != null && !flags.createSourceMap.equals(false)) {
        compiler.resetAndIntitializeSourceMap();
      }

      File file = File.create();
      file.path = flags.chunkOutputPathPrefix + c.getName() + ".js";

      String code = compiler.toSource(c);

      int lastSeparatorIndex = file.path.lastIndexOf('/');
      if (lastSeparatorIndex < 0) {
        lastSeparatorIndex = file.path.lastIndexOf('\\');
      }
      String baseName = file.path.substring(Math.max(0, lastSeparatorIndex));
      String wrapper = parsedModuleWrappers.get(c.getName()).replace("%basename%", baseName);
      StringBuilder out = new StringBuilder();
      int pos = wrapper.indexOf("%s");
      if (pos != -1) {
        String prefix = "";

        if (pos > 0) {
          prefix = wrapper.substring(0, pos);
          out.append(prefix);
        }

        out.append(code);

        int suffixStart = pos + "%s".length();
        if (suffixStart != wrapper.length()) {
          // Something after placeholder?
          out.append(wrapper, suffixStart, wrapper.length());
        }
        // Make sure we always end output with a line feed.
        out.append('\n');

        // If we have a source map, adjust its offsets to match
        // the code WITHIN the wrapper.
        if (compiler != null && compiler.getSourceMap() != null) {
          compiler.getSourceMap().setWrapperPrefix(prefix);
        }

      } else {
        out.append(code);
        out.append('\n');
      }

      file.src = out.toString();

      if (flags.createSourceMap != null && !flags.createSourceMap.equals(false)) {
        StringBuilder b = new StringBuilder();
        try {
          compiler.getSourceMap().appendTo(b, file.path);
        } catch (IOException e) {
          // ignore
        }
        file.sourceMap = b.toString();
      }
      outputFiles.push(file);
    }

    output.compiledFiles = outputFiles;
    return output;
  }

  /** Generates the output code, taking into account the passed {@code flags}. */
  private static ChunkOutput writeOutput(Compiler compiler, Flags flags) {
    JsArray outputFiles = new JsArray<>();
    ChunkOutput output = ChunkOutput.create();

    File file = File.create();
    file.path = flags.jsOutputFile;

    String code = compiler.toSource();
    String prefix = "";
    String postfix = "";
    if (flags.outputWrapper != null) {
      String marker = null;
      int pos = flags.outputWrapper.indexOf(OUTPUT_MARKER_JS_STRING);
      if (pos != -1) {
        // With jsstring, run SourceCodeEscapers (as per AbstractCommandLineRunner).
        code = SourceCodeEscapers.javascriptEscaper().escape(code);
        marker = OUTPUT_MARKER_JS_STRING;
      } else {
        pos = flags.outputWrapper.indexOf(OUTPUT_MARKER);
        if (pos != -1) {
          marker = OUTPUT_MARKER;
        }
      }

      if (marker != null) {
        prefix = flags.outputWrapper.substring(0, pos);
        SourceMap sourceMap = compiler.getSourceMap();
        if (sourceMap != null) {
          sourceMap.setWrapperPrefix(prefix);
        }
      }
      postfix = flags.outputWrapper.substring(pos + marker.length());
    }
    if (flags.createSourceMap != null && !flags.createSourceMap.equals(false)) {
      StringBuilder b = new StringBuilder();
      try {
        compiler.getSourceMap().appendTo(b, flags.jsOutputFile);
      } catch (IOException e) {
        // ignore
      }
      file.sourceMap = b.toString();
    }

    file.src = prefix + code + postfix;
    outputFiles.push(file);
    output.compiledFiles = outputFiles;
    output.compiledCode = file.src;
    output.sourceMap = file.sourceMap;
    return output;
  }

  private static List createExterns(CompilerOptions.Environment environment) {
    String[] resources = ResourceLoader.resourceList(JsRunnerMain.class);
    Map all = new HashMap<>();
    for (String res : resources) {
      if (res.startsWith(EXTERNS_PREFIX)) {
        String filename = res.substring(EXTERNS_PREFIX.length());
        all.put(
            filename,
            SourceFile.fromCode(
                "externs.zip//" + res, ResourceLoader.loadTextResource(JsRunnerMain.class, res)));
      }
    }
    return DefaultExterns.prepareExterns(environment, all);
  }

  private static ImmutableList createEntryPoints(String[] entryPoints) {
    ImmutableList.Builder builder = new ImmutableList.Builder<>();
    for (String entryPoint : entryPoints) {
      if (entryPoint.startsWith("goog:")) {
        builder.add(ModuleIdentifier.forClosure(entryPoint));
      } else {
        builder.add(ModuleIdentifier.forFile(entryPoint));
      }
    }
    return builder.build();
  }

  private static void applyWarnings(
      String[] warningGuards,
      CompilerOptions options,
      DiagnosticGroups diagnosticGroups,
      CheckLevel checkLevel) {
    for (String warningGuardName : warningGuards) {
      if ("*".equals(warningGuardName)) {
        for (String groupName : diagnosticGroups.getRegisteredGroups().keySet()) {
          if (!DiagnosticGroups.wildcardExcludedGroups.contains(groupName)) {
            diagnosticGroups.setWarningLevel(options, groupName, checkLevel);
          }
        }
      } else {
        diagnosticGroups.setWarningLevel(options, warningGuardName, checkLevel);
      }
    }
  }

  private static void applyOptionsFromFlags(
      CompilerOptions options, Flags flags, DiagnosticGroups diagnosticGroups) {

    // order matches createOptions in CommandLineRunner.java

    LanguageMode languageIn = LanguageMode.fromString(flags.languageIn);
    if (languageIn != null) {
      options.setLanguageIn(languageIn);
    } else {
      throw new RuntimeException("Bad value for languageIn: " + flags.languageIn);
    }
    LanguageMode languageOut = LanguageMode.fromString(flags.languageOut);
    if (languageOut != null) {
      options.setLanguageOut(languageOut);
    } else {
      throw new RuntimeException("Bad value for languageOut: " + flags.languageOut);
    }

    options.setCodingConvention(new ClosureCodingConvention());

    if (flags.extraAnnotationName != null) {
      options.setExtraAnnotationNames(Arrays.asList(flags.extraAnnotationName));
    }

    CompilationLevel level = DEFAULT_COMPILATION_LEVEL;
    if (flags.compilationLevel != null) {
      level = CompilationLevel.fromString(Ascii.toUpperCase(flags.compilationLevel));
      if (level == null) {
        throw new RuntimeException("Bad value for compilationLevel: " + flags.compilationLevel);
      }
    }
    if (level == CompilationLevel.ADVANCED_OPTIMIZATIONS && !flags.renaming) {
      throw new RuntimeException("renaming cannot be disabled when ADVANCED_OPTIMIZATIONS is used");
    }
    level.setOptionsForCompilationLevel(options);
    if (flags.debug) {
      level.setDebugOptionsForCompilationLevel(options);
    }

    CompilerOptions.Environment environment = CompilerOptions.Environment.BROWSER;
    if (flags.env != null) {
      environment = CompilerOptions.Environment.valueOf(Ascii.toUpperCase(flags.env));
    }
    options.setEnvironment(environment);

    options.setChecksOnly(flags.checksOnly);
    if (flags.checksOnly) {
      options.setOutputJs(CompilerOptions.OutputJs.NONE);
    }

    if (flags.useTypesForOptimization) {
      level.setTypeBasedOptimizationOptions(options);
    }

    if (flags.isolationMode != null
        && IsolationMode.valueOf(flags.isolationMode) == IsolationMode.IIFE) {
      flags.outputWrapper = "(function(){%output%}).call(this);";
      flags.assumeFunctionWrapper = true;
    }
    if (flags.assumeFunctionWrapper) {
      level.setWrappedOutputOptimizations(options);
    }

    options.setGenerateExports(flags.generateExports);
    options.setExportLocalPropertyDefinitions(flags.exportLocalPropertyDefinitions);

    WarningLevel warningLevel = WarningLevel.DEFAULT;
    if (flags.warningLevel != null) {
      warningLevel = WarningLevel.valueOf(flags.warningLevel);
    }
    warningLevel.setOptionsForWarningLevel(options);

    if (flags.formatting != null) {
      List formattingOptions = Arrays.asList(toStringArray(flags.formatting));
      for (String formattingOption : formattingOptions) {
        switch (formattingOption) {
          case "PRETTY_PRINT":
            options.setPrettyPrint(true);
            break;
          case "PRINT_INPUT_DELIMITER":
            options.printInputDelimiter = true;
            break;
          case "SINGLE_QUOTES":
            options.setPreferSingleQuotes(true);
            break;
          default:
            throw new RuntimeException("Unknown formatting option: " + formattingOption);
        }
      }
    }

    options.setClosurePass(flags.processClosurePrimitives);

    options.setAngularPass(flags.angularPass);

    if (flags.polymerPass) {
      options.setPolymerVersion(1);
    } else if (flags.polymerVersion != null) {
      options.setPolymerVersion(flags.polymerVersion.intValue());
    }

    options.setDartPass(flags.dartPass);

    options.setRenamePrefix(flags.renameVariablePrefix);
    options.setRenamePrefixNamespace(flags.renamePrefixNamespace);

    options.setPreventLibraryInjection(!flags.injectLibraries);

    if (flags.forceInjectLibraries != null) {
      options.setForceLibraryInjection(Arrays.asList(flags.forceInjectLibraries));
    }

    options.setPreserveTypeAnnotations(flags.preserveTypeAnnotations);

    options.setRewritePolyfills(
        flags.rewritePolyfills && options.getLanguageIn().toFeatureSet().contains(FeatureSet.ES6));

    // We don't support conformance configs
    options.clearConformanceConfigs();

    if (flags.tracerMode != null) {
      options.setTracerMode(TracerMode.valueOf(flags.tracerMode));
    }
    options.setStrictModeInput(flags.strictModeInput);

    options.setSourceMapIncludeSourcesContent(flags.sourceMapIncludeContent);

    if (flags.moduleResolution != null) {
      options.setModuleResolutionMode(ResolutionMode.valueOf(flags.moduleResolution));
    }

    if (flags.packageJsonEntryNames != null) {
      options.setPackageJsonEntryNames(Arrays.asList(flags.packageJsonEntryNames.split(",\\s*")));
    }

    if (!flags.renaming) {
      options.setVariableRenaming(VariableRenamingPolicy.OFF);
      options.setPropertyRenaming(PropertyRenamingPolicy.OFF);
    }

    // order matches setRunOptions in AbstractCommandLineRunner.java

    applyWarnings(toStringArray(flags.jscompOff), options, diagnosticGroups, CheckLevel.OFF);
    applyWarnings(
        toStringArray(flags.jscompWarning), options, diagnosticGroups, CheckLevel.WARNING);
    applyWarnings(toStringArray(flags.jscompError), options, diagnosticGroups, CheckLevel.ERROR);

    if (flags.hideWarningsFor != null) {
      options.addWarningsGuard(
          new ShowByPathWarningsGuard(
              toStringArray(flags.hideWarningsFor), ShowByPathWarningsGuard.ShowType.EXCLUDE));
    }

    if (flags.define != null) {
      createDefineOrTweakReplacements(Arrays.asList(toStringArray(flags.define)), options, false);
    }

    if (flags.defines != null) {
      // CompilerOptions also validates types, but uses Preconditions and therefore won't generate
      // a useful exception.
      validatePrimitiveTypes(flags.defines);
      options.setDefineReplacements(toMap(flags.defines));
    }

    DependencyMode dependencyMode = null;
    if (flags.dependencyMode != null) {
      dependencyMode = DependencyMode.valueOf(Ascii.toUpperCase(flags.dependencyMode));
    }
    List entryPoints = Arrays.asList(toStringArray(flags.entryPoint));
    DependencyOptions dependencyOptions =
        DependencyOptions.fromFlags(
            dependencyMode,
            entryPoints,
            /* closureEntryPointFlag= */ ImmutableList.of(),
            /* commonJsEntryModuleFlag= */ null,
            /* manageClosureDependenciesFlag= */ false,
            /* onlyClosureDependenciesFlag= */ false);
    if (dependencyOptions != null) {
      options.setDependencyOptions(dependencyOptions);
    }

    options.setTrustedStrings(true);

    if (flags.createSourceMap != null) {
      if (flags.createSourceMap instanceof String) {
        options.setSourceMapOutputPath((String) flags.createSourceMap);
      } else if (!flags.createSourceMap.equals(false)) {
        options.setSourceMapOutputPath("%output%.map");
      }
    }
    options.setSourceMapIncludeSourcesContent(flags.sourceMapIncludeContent);
    options.setParseInlineSourceMaps(flags.parseInlineSourceMaps);
    options.setApplyInputSourceMaps(flags.applyInputSourceMaps);

    options.setProcessCommonJSModules(flags.processCommonJsModules);

    options.setModuleRoots(Arrays.asList(toStringArray(flags.jsModuleRoot)));
  }

  /**
   * @param externs Array of strings or File[]. If running under NodeJS, an array of strings will be
   *     treated as file paths and loaded via the native node fs module.
   * @return Array of extern File objects. If an array of strings is passed without running under
   *     node, return null to indicate failure.
   */
  private static File[] filesFromFilesOrPaths(Object[] externs) {
    if (externs.length == 0) {
      return new File[0];
    }

    File first = Js.uncheckedCast(externs[0]);
    if (first.path != null && first.src != null) {
      return Js.uncheckedCast(Arrays.copyOf(externs, externs.length));
    }

    return filesFromPaths(Js.uncheckedCast(externs));
  }

  private static List fromFileArray(File[] src, String unknownPrefix) {
    List out = new ArrayList<>();
    if (src != null) {
      for (int i = 0; i < src.length; ++i) {
        File file = src[i];
        String path = file.path;
        if (path == null) {
          path = unknownPrefix + i;
        }
        out.add(SourceFile.fromCode(path, nullToEmpty(file.src)));
      }
    }
    return out;
  }

  private static ImmutableMap toMap(JsPropertyMap jsmap) {
    ImmutableMap.Builder builder = ImmutableMap.builder();
    jsmap.forEach(
        (k) -> {
          builder.put(k, jsmap.get(k));
        });
    return builder.build();
  }

  private static ImmutableMap buildSourceMaps(
      File[] src, String unknownPrefix) {
    ImmutableMap.Builder inputSourceMaps = new ImmutableMap.Builder<>();
    if (src != null) {
      for (int i = 0; i < src.length; ++i) {
        File file = src[i];
        if (isNullOrEmpty(file.sourceMap)) {
          continue;
        }
        String path = file.path;
        if (path == null) {
          path = unknownPrefix + i;
        }
        SourceFile sf = SourceFile.fromCode(path + ".map", file.sourceMap);
        inputSourceMaps.put(path, new SourceMapInput(sf));
      }
    }
    return inputSourceMaps.build();
  }

  /**
   * Updates the destination flags (user input) with source flags (the defaults). Returns a list of
   * flags that are on the destination, but not on the source.
   */
  private static JsArray updateFlags(Flags dst, Flags src) {
    JsPropertyMap jssrc = Js.asPropertyMap(src);
    JsPropertyMap jsdst = Js.asPropertyMap(dst);

    jssrc.forEach(
        (k) -> {
          if (!jsdst.has(k)) {
            jsdst.set(k, jssrc.get(k));
          }
        });

    JsArray unhandled = new JsArray<>();
    jsdst.forEach(
        (k) -> {
          if (!jssrc.has(k)) {
            unhandled.push(k);
          }
        });

    return unhandled;
  }

  /** Public compiler call. Exposed in {@link #exportCompile}. */
  @JsMethod
  public static ChunkOutput compile(Flags flags, File[] inputs) throws IOException {
    // The PhaseOptimizer logs skipped pass warnings that interfere with capturing
    // output and errors in the open source runners.
    phaseLogger.setLevel(Level.OFF);

    JsArray unhandled = updateFlags(flags, getDefaultFlags());
    if (unhandled.getLength() > 0) {
      throw new RuntimeException("Unhandled flag: " + unhandled.getAt(0));
    }

    List jsCode = new ArrayList<>();
    ImmutableMap sourceMaps = null;
    if (flags.jsCode != null) {
      jsCode = fromFileArray(flags.jsCode, "Input_");
      sourceMaps = buildSourceMaps(flags.jsCode, "Input_");
    }

    ImmutableMap.Builder inputPathByWebpackId = new ImmutableMap.Builder<>();
    if (inputs != null) {
      List sourceFiles = fromFileArray(inputs, "Input_");
      ImmutableMap inputSourceMaps = buildSourceMaps(inputs, "Input_");

      jsCode.addAll(sourceFiles);

      if (sourceMaps == null) {
        sourceMaps = inputSourceMaps;
      } else {
        HashMap tempMaps = new HashMap<>(sourceMaps);
        tempMaps.putAll(inputSourceMaps);
        sourceMaps = ImmutableMap.copyOf(tempMaps);
      }

      for (JsRunnerMain.File element : inputs) {
        if (element.webpackId != null && element.path != null) {
          inputPathByWebpackId.put(element.webpackId, element.path);
        }
      }
    }

    if (flags.js != null) {
      File[] jsFiles = filesFromPaths(toStringArray(flags.js));
      if (jsFiles == null) {
        throw new RuntimeException(
            "Can only load files from the filesystem when running in NodeJS.");
      } else {
        jsCode.addAll(fromFileArray(jsFiles, "Input_"));
      }
    }

    Compiler compiler = new Compiler(new NodePrintStream());
    CompilerOptions options = new CompilerOptions();
    applyOptionsFromFlags(options, flags, compiler.getDiagnosticGroups());
    options.setInputSourceMaps(sourceMaps);

    List externs = new ArrayList<>();
    if (flags.externs != null) {
      File[] externFiles = filesFromFilesOrPaths(toStringArray(flags.externs));
      if (externFiles == null) {
        throw new RuntimeException(
            "Can only load files from the filesystem when running in NodeJS.");
      }
      externs = fromFileArray(externFiles, "Extern_");
    }
    externs.addAll(createExterns(options.getEnvironment()));

    NodeErrorManager errorManager = new NodeErrorManager();
    compiler.initWebpackMap(inputPathByWebpackId.build());
    compiler.setErrorManager(errorManager);

    List chunkSpecs = new ArrayList<>();
    if (flags.chunk != null) {
      Collections.addAll(chunkSpecs, flags.chunk);
    }
    List jsChunkSpecs = new ArrayList<>();
    for (int i = 0; i < chunkSpecs.size(); i++) {
      jsChunkSpecs.add(JsModuleSpec.create(chunkSpecs.get(i), i == 0));
    }
    ChunkOutput output;
    if (!jsChunkSpecs.isEmpty()) {
      List chunks = createJsModules(jsChunkSpecs, jsCode);

      compiler.compileModules(externs, chunks, options);
      output = writeChunkOutput(compiler, flags, chunks);
    } else {
      compiler.compile(externs, jsCode, options);
      output = writeOutput(compiler, flags);
    }

    output.errors = toNativeErrorArray(errorManager.errors);
    output.warnings = toNativeErrorArray(errorManager.warnings);

    return output;
  }

  /**
   * Exports the {@link #compile} method via JSNI.
   *
   * 

This will be placed on {@code module.exports}, {@code self.compile} or {@code * window.compile}. */ @SuppressWarnings({"unusable-by-js"}) public static native void exportCompile() /*-{ var fn = $entry(@com.google.javascript.jscomp.gwt.client.JsRunnerMain::compile(*)); if (typeof module !== 'undefined' && module.exports) { module.exports = fn; } else if (typeof self === 'object') { self.compile = fn; } else { window.compile = fn; } }-*/; /** Custom {@link BasicErrorManager} to record {@link JSError} instances. */ private static class NodeErrorManager extends BasicErrorManager { final List errors = new ArrayList<>(); final List warnings = new ArrayList<>(); @Override public void println(CheckLevel level, JSError error) { if (level == CheckLevel.ERROR) { errors.add(error); } else if (level == CheckLevel.WARNING) { warnings.add(error); } } @Override public void printSummary() {} } private JsRunnerMain() {} }