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

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

/*
 * 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() {} }