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

com.google.javascript.jscomp.deps.ModuleLoader 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.deps;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.annotations.GwtIncompatible;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.CheckLevel;
import com.google.javascript.jscomp.DiagnosticType;
import com.google.javascript.jscomp.ErrorHandler;
import com.google.javascript.jscomp.JSError;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.annotation.Nullable;

/**
 * Provides compile-time locate semantics for ES6 and CommonJS modules.
 *
 * @see "https://tc39.github.io/ecma262/#sec-module-semantics"
 * @see "http://wiki.commonjs.org/wiki/Modules/1.1"
 */
public final class ModuleLoader {

  public static final DiagnosticType MODULE_CONFLICT = DiagnosticType.warning(
      "JSC_MODULE_CONFLICT", "File has both goog.module and ES6 modules: {0}");

  /** According to the spec, the forward slash should be the delimiter on all platforms. */
  public static final String MODULE_SLASH = ModuleNames.MODULE_SLASH;

  /** The default module root, the current directory. */
  public static final String DEFAULT_FILENAME_PREFIX = "." + MODULE_SLASH;

  public static final DiagnosticType LOAD_WARNING = DiagnosticType.warning(
      "JSC_ES6_MODULE_LOAD_WARNING",
      "Failed to load module \"{0}\"");

  @Nullable private final ErrorHandler errorHandler;

  /** Root URIs to match module roots against. */
  private final ImmutableList moduleRootPaths;
  /** The set of all known input module URIs (including trailing .js), after normalization. */
  private final ImmutableSet modulePaths;

  /** Named modules found in node_modules folders */
  private final ImmutableSortedMap> nodeModulesRegistry;

  /** Named modules found in node_modules folders */
  private ImmutableMap packageJsonMainEntries;

  /** Used to canonicalize paths before resolution. */
  private final PathResolver pathResolver;

  /**
   * Creates an instance of the module loader which can be used to locate ES6 and CommonJS modules.
   *
   * @param inputs All inputs to the compilation process.
   */
  public ModuleLoader(
      @Nullable ErrorHandler errorHandler,
      Iterable moduleRoots,
      Iterable inputs,
      PathResolver pathResolver) {
    checkNotNull(moduleRoots);
    checkNotNull(inputs);
    checkNotNull(pathResolver);
    this.pathResolver = pathResolver;
    this.errorHandler = errorHandler;
    this.moduleRootPaths = createRootPaths(moduleRoots, pathResolver);
    this.modulePaths =
        resolvePaths(
            Iterables.transform(Iterables.transform(inputs, UNWRAP_DEPENDENCY_INFO), pathResolver),
            moduleRootPaths);

    this.packageJsonMainEntries = ImmutableMap.of();

    this.nodeModulesRegistry = buildRegistry(this.modulePaths);
  }

  public ModuleLoader(@Nullable ErrorHandler errorHandler,
      Iterable moduleRoots, Iterable inputs) {
    this(errorHandler, moduleRoots, inputs, PathResolver.RELATIVE);
  }

  public Map getPackageJsonMainEntries() {
    return this.packageJsonMainEntries;
  }

  public void setPackageJsonMainEntries(Map packageJsonMainEntries) {
    this.packageJsonMainEntries = ImmutableMap.copyOf(packageJsonMainEntries);
  }

  /**
   * A path to a module.  Provides access to the module's closurized name
   * and a way to resolve relative paths.
   */
  public class ModulePath {
    private final String path;

    private ModulePath(String path) {
      this.path = path;
    }

    @Override
    public String toString() {
      return path;
    }

    /**
     * Turns a filename into a JS identifier that can be used in rewritten code.
     * Removes leading ./, replaces / with $, removes trailing .js
     * and replaces - with _.
     */
    public String toJSIdentifier() {
      return ModuleNames.toJSIdentifier(path);
    }

    /**
     * Turns a filename into a JS identifier that is used for moduleNames in
     * rewritten code. Removes leading ./, replaces / with $, removes trailing .js
     * and replaces - with _. All moduleNames get a "module$" prefix.
     */
    public String toModuleName() {
      return ModuleNames.toModuleName(path);
    }

    /**
     * Find a CommonJS module {@code requireName}. See
     * https://nodejs.org/api/modules.html#modules_all_together
     *
     * @return The normalized module URI, or {@code null} if not found.
     */
    public ModulePath resolveCommonJsModule(String requireName) {
      String loadAddress;

      // * the immediate name require'd
      if (isAbsoluteIdentifier(requireName) || isRelativeIdentifier(requireName)) {
        loadAddress = resolveCommonJsModuleFileOrDirectory(requireName);
      } else {
        loadAddress = resolveCommonJsModuleFromRegistry(requireName);
      }
      if (loadAddress != null) {
        return new ModulePath(loadAddress);
      }
      return null;
    }

    private String resolveCommonJsModuleFile(String requireName) {
      String[] extensions = {"", ".js", ".json"};

      // Load as a file
      for (int i = 0; i < extensions.length; i++) {
        String loadAddress = locate(requireName + extensions[i]);
        if (loadAddress != null) {
          return loadAddress;
        }
      }

      return null;
    }

    private String resolveCommonJsModuleFileOrDirectory(String requireName) {
      String loadAddress = resolveCommonJsModuleFile(requireName);
      if (loadAddress == null) {
        loadAddress = resolveCommonJsModuleDirectory(requireName);
      }
      return loadAddress;
    }

    private String resolveCommonJsModuleDirectory(String requireName) {
      String[] extensions = {
        MODULE_SLASH + "package.json", MODULE_SLASH + "index.js", MODULE_SLASH + "index.json"
      };

      // Load as a file
      for (int i = 0; i < extensions.length; i++) {
        String loadAddress = locate(requireName + extensions[i]);
        if (loadAddress != null) {
          if (i == 0) {
            if (packageJsonMainEntries.containsKey(loadAddress)) {
              return resolveCommonJsModuleFile(packageJsonMainEntries.get(loadAddress));
            }
          } else {
            return loadAddress;
          }
        }
      }

      return null;
    }

    private String resolveCommonJsModuleFromRegistry(String requireName) {
      for (Map.Entry> nodeModulesFolder :
          nodeModulesRegistry.entrySet()) {
        if (!this.path.startsWith(nodeModulesFolder.getKey())) {
          continue;
        }

        // Load as a file
        String fullModulePath = nodeModulesFolder.getKey() + "node_modules/" + requireName;
        String loadAddress = resolveCommonJsModuleFile(fullModulePath);
        if (loadAddress != null) {
          return loadAddress;
        }

        // Load as a directory
        loadAddress = resolveCommonJsModuleDirectory(fullModulePath);
        if (loadAddress != null) {
          return loadAddress;
        }
      }

      return null;
    }

    /**
     * Find an ES6 module {@code moduleName} relative to {@code context}.
     * @return The normalized module URI, or {@code null} if not found.
     */
    public ModulePath resolveEs6Module(String moduleName) {
      // Allow module names with or without the ".js" extension.
      if (!moduleName.endsWith(".js")) {
        moduleName += ".js";
      }
      String resolved = locateNoCheck(moduleName);
      if (!modulePaths.contains(resolved) && errorHandler != null) {
        errorHandler.report(CheckLevel.WARNING, JSError.make(LOAD_WARNING, moduleName));
      }
      return new ModulePath(resolved);
    }

    /**
     * Locates the module with the given name, but returns successfully even if
     * there is no JS file corresponding to the returned URI.
     */
    private String locateNoCheck(String name) {
      String path = ModuleNames.escapePath(name);
      if (isRelativeIdentifier(name)) {
        String ourPath = this.path;
        int lastIndex = ourPath.lastIndexOf('/');
        path = ModuleNames.canonicalizePath(ourPath.substring(0, lastIndex + 1) + path);
      }
      return normalize(path, moduleRootPaths);
    }

    /**
     * Locates the module with the given name, but returns null if there is no JS
     * file in the expected location.
     */
    @Nullable
    private String locate(String name) {
      String path = locateNoCheck(name);
      if (modulePaths.contains(path)) {
        return path;
      }
      return null;
    }
  }

  /** Resolves a path into a {@link ModulePath}. */
  public ModulePath resolve(String path) {
    return new ModulePath(
        normalize(ModuleNames.escapePath(pathResolver.apply(path)), moduleRootPaths));
  }

  /** Whether this is relative to the current file, or a top-level identifier. */
  public static boolean isRelativeIdentifier(String name) {
    return name.startsWith("." + MODULE_SLASH) || name.startsWith(".." + MODULE_SLASH);
  }

  /** Whether this is absolute to the compilation. */
  public static boolean isAbsoluteIdentifier(String name) {
    return name.startsWith(MODULE_SLASH);
  }

  /** Whether name is a path-based identifier (has a '/' character) */
  public static boolean isPathIdentifier(String name) {
    return name.contains(MODULE_SLASH);
  }

  private static ImmutableList createRootPaths(
      Iterable roots, PathResolver resolver) {
    ImmutableList.Builder builder = ImmutableList.builder();
    for (String root : roots) {
      builder.add(ModuleNames.escapePath(resolver.apply(root)));
    }
    return builder.build();
  }

  private static ImmutableSet resolvePaths(
      Iterable names, ImmutableList roots) {
    HashSet resolved = new HashSet<>();
    for (String name : names) {
      if (!resolved.add(normalize(ModuleNames.escapePath(name), roots))) {
        // Having root paths "a" and "b" and source files "a/f.js" and "b/f.js" is ambiguous.
        throw new IllegalArgumentException(
            "Duplicate module path after resolving: " + name);
      }
    }
    return ImmutableSet.copyOf(resolved);
  }

  /**
   * Normalizes the name and resolves it against the module roots.
   */
  private static String normalize(String path, Iterable moduleRootPaths) {
    // Find a moduleRoot that this URI is under. If none, use as is.
    for (String moduleRoot : moduleRootPaths) {
      if (path.startsWith(moduleRoot)) {
        // Make sure that e.g. path "foobar/test.js" is not matched by module "foo", by checking for
        // a leading slash.
        String trailing = path.substring(moduleRoot.length());
        if (trailing.startsWith(MODULE_SLASH)) {
          return trailing.substring(MODULE_SLASH.length());
        }
      }
    }
    // Not underneath any of the roots.
    return path;
  }

  /** Build the module registry from the set of module paths */
  private static ImmutableSortedMap> buildRegistry(
      ImmutableSet modulePaths) {
    SortedMap> registry =
        new TreeMap<>(
            new Comparator() {
              @Override
              public int compare(String a, String b) {
                // Order longest path first
                int comparison = Integer.compare(b.length(), a.length());
                if (comparison != 0) {
                  return comparison;
                }

                return a.compareTo(b);
              }
            });

    // For each modulePath, find all the node_modules folders
    // There might be more than one:
    //    /foo/node_modules/bar/node_modules/baz/foo_bar_baz.js
    // Should add:
    //   /foo/ -> bar/node_modules/baz/foo_bar_baz.js
    //   /foo/node_modules/bar/ -> baz/foo_bar_baz.js
    for (String modulePath : modulePaths) {
      String[] nodeModulesDirs = modulePath.split("/node_modules/");
      String parentPath = "";
      for (int i = 0; i < nodeModulesDirs.length - 1; i++) {
        if (i + 1 < nodeModulesDirs.length) {
          parentPath += nodeModulesDirs[i] + "/";
        }
        String subPath = modulePath.substring(parentPath.length() + "node_modules/".length());

        if (!registry.containsKey(parentPath)) {
          registry.put(parentPath, new HashSet());
        }
        registry.get(parentPath).add(subPath);

        parentPath += "node_modules/";
      }
    }

    SortedMap> immutableRegistry =
        new TreeMap<>(
            new Comparator() {
              @Override
              public int compare(String a, String b) {
                // Order longest path first
                int comparison = Integer.compare(b.length(), a.length());
                if (comparison != 0) {
                  return comparison;
                }

                return a.compareTo(b);
              }
            });
    for (Map.Entry> entry : registry.entrySet()) {
      immutableRegistry.put(entry.getKey(), ImmutableSet.copyOf(entry.getValue()));
    }
    return ImmutableSortedMap.copyOfSorted(immutableRegistry);
  }

  /** An enum indicating whether to absolutize paths. */
  public enum PathResolver implements Function {
    RELATIVE {
      @Override
      public String apply(String path) {
        return path;
      }
    },

    @GwtIncompatible("Paths.get, Path.toAbsolutePath")
    ABSOLUTE {
      @Override
      public String apply(String path) {
        return Paths.get(path).toAbsolutePath().toString();
      }
    };
  }

  private static final Function UNWRAP_DEPENDENCY_INFO =
      new Function() {
        @Override
        public String apply(DependencyInfo info) {
          return info.getName();
        }
      };

  /** A trivial module loader with no roots. */
  public static final ModuleLoader EMPTY =
      new ModuleLoader(null, ImmutableList.of(), ImmutableList.of());
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy