From 594e652bad788efc6696dff0652a93efeb67d6b8 Mon Sep 17 00:00:00 2001 From: johnplaisted Date: Thu, 10 May 2018 16:30:58 -0700 Subject: [PATCH] Add a new module resolver that acts like BrowserModuleResolver but replaces some path prefixes before resolving. This is a bit like a very, very watered down package map. Left more simple / vague on purpose as package map is not a define / accepted proposal. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196189544 --- .../javascript/jscomp/CommandLineRunner.java | 10 + .../google/javascript/jscomp/Compiler.java | 44 +++-- .../javascript/jscomp/CompilerOptions.java | 25 ++- .../jscomp/deps/BrowserModuleResolver.java | 5 +- ...WithTransformedPrefixesModuleResolver.java | 173 ++++++++++++++++++ .../javascript/jscomp/deps/JsFileParser.java | 5 +- .../javascript/jscomp/deps/ModuleLoader.java | 97 ++++------ .../jscomp/deps/ModuleResolver.java | 15 ++ .../jscomp/deps/NodeModuleResolver.java | 21 +++ .../jscomp/deps/WebpackModuleResolver.java | 32 ++++ .../jscomp/deps/DepsGeneratorTest.java | 52 +++--- .../jscomp/deps/JsFileParserTest.java | 33 +++- .../jscomp/deps/ModuleLoaderTest.java | 153 +++++++++++++--- 13 files changed, 525 insertions(+), 140 deletions(-) create mode 100644 src/com/google/javascript/jscomp/deps/BrowserWithTransformedPrefixesModuleResolver.java diff --git a/src/com/google/javascript/jscomp/CommandLineRunner.java b/src/com/google/javascript/jscomp/CommandLineRunner.java index 94522a9bcb5..2bc6e5c1901 100644 --- a/src/com/google/javascript/jscomp/CommandLineRunner.java +++ b/src/com/google/javascript/jscomp/CommandLineRunner.java @@ -783,6 +783,14 @@ private static class Flags { ) private ModuleLoader.ResolutionMode moduleResolutionMode = ModuleLoader.ResolutionMode.BROWSER; + @Option( + name = "--browser_resolver_prefix_replacements", + hidden = false, + usage = + "Prefixes to replace in ES6 import paths before resolving. " + + "module_resolution must be BROWSER_WITH_TRANSFORMED_PREFIXES to take effect.") + private Map browserResolverPrefixReplacements = ImmutableMap.of(); + @Option( name = "--package_json_entry_names", usage = @@ -1808,6 +1816,8 @@ protected CompilerOptions createOptions() { } options.setSourceMapIncludeSourcesContent(flags.sourceMapIncludeSourcesContent); options.setModuleResolutionMode(flags.moduleResolutionMode); + options.setBrowserResolverPrefixReplacements( + ImmutableMap.copyOf(flags.browserResolverPrefixReplacements)); if (flags.packageJsonEntryNames != null) { try { diff --git a/src/com/google/javascript/jscomp/Compiler.java b/src/com/google/javascript/jscomp/Compiler.java index 3c2a74aaa52..cdef73acb82 100644 --- a/src/com/google/javascript/jscomp/Compiler.java +++ b/src/com/google/javascript/jscomp/Compiler.java @@ -32,9 +32,14 @@ import com.google.javascript.jscomp.CompilerOptions.DevMode; import com.google.javascript.jscomp.CoverageInstrumentationPass.CoverageReach; import com.google.javascript.jscomp.CoverageInstrumentationPass.InstrumentOption; +import com.google.javascript.jscomp.deps.BrowserModuleResolver; +import com.google.javascript.jscomp.deps.BrowserWithTransformedPrefixesModuleResolver; import com.google.javascript.jscomp.deps.JsFileParser; import com.google.javascript.jscomp.deps.ModuleLoader; +import com.google.javascript.jscomp.deps.ModuleLoader.ModuleResolverFactory; +import com.google.javascript.jscomp.deps.NodeModuleResolver; import com.google.javascript.jscomp.deps.SortedDependencies.MissingProvideException; +import com.google.javascript.jscomp.deps.WebpackModuleResolver; import com.google.javascript.jscomp.ijs.CheckTypeSummaryWarningsGuard; import com.google.javascript.jscomp.parsing.Config; import com.google.javascript.jscomp.parsing.ParserRunner; @@ -1674,27 +1679,34 @@ Node parseInputs() { if (options.getLanguageIn().toFeatureSet().has(FeatureSet.Feature.MODULES) || options.processCommonJSModules) { + ModuleResolverFactory moduleResolverFactory = null; + + switch (options.getModuleResolutionMode()) { + case BROWSER: + moduleResolverFactory = BrowserModuleResolver.FACTORY; + break; + case NODE: + // processJsonInputs requires a module loader to already be defined + // so we redefine it afterwards with the package.json inputs + moduleResolverFactory = new NodeModuleResolver.Factory(processJsonInputs(inputs)); + break; + case WEBPACK: + moduleResolverFactory = new WebpackModuleResolver.Factory(inputPathByWebpackId); + break; + case BROWSER_WITH_TRANSFORMED_PREFIXES: + moduleResolverFactory = + new BrowserWithTransformedPrefixesModuleResolver.Factory( + options.getBrowserResolverPrefixReplacements()); + break; + } + this.moduleLoader = new ModuleLoader( null, options.moduleRoots, inputs, - ModuleLoader.PathResolver.RELATIVE, - options.moduleResolutionMode, - inputPathByWebpackId); - - if (options.moduleResolutionMode == ModuleLoader.ResolutionMode.NODE) { - // processJsonInputs requires a module loader to already be defined - // so we redefine it afterwards with the package.json inputs - this.moduleLoader = - new ModuleLoader( - null, - options.moduleRoots, - inputs, - ModuleLoader.PathResolver.RELATIVE, - options.moduleResolutionMode, - processJsonInputs(inputs)); - } + moduleResolverFactory, + ModuleLoader.PathResolver.RELATIVE); } else { // Use an empty module loader if we're not actually dealing with modules. this.moduleLoader = ModuleLoader.EMPTY; diff --git a/src/com/google/javascript/jscomp/CompilerOptions.java b/src/com/google/javascript/jscomp/CompilerOptions.java index 7022add3157..d467fe864aa 100644 --- a/src/com/google/javascript/jscomp/CompilerOptions.java +++ b/src/com/google/javascript/jscomp/CompilerOptions.java @@ -31,6 +31,7 @@ import com.google.common.collect.Multimap; import com.google.common.primitives.Chars; import com.google.javascript.jscomp.deps.ModuleLoader; +import com.google.javascript.jscomp.deps.ModuleLoader.ResolutionMode; import com.google.javascript.jscomp.parsing.Config; import com.google.javascript.jscomp.parsing.parser.FeatureSet; import com.google.javascript.jscomp.resources.ResourceLoader; @@ -1168,7 +1169,13 @@ public void setWrapGoogModulesForWhitespaceOnly(boolean enable) { private Optional isStrictModeInput = Optional.absent(); /** Which algorithm to use for locating ES6 and CommonJS modules */ - ModuleLoader.ResolutionMode moduleResolutionMode; + ResolutionMode moduleResolutionMode; + + /** + * Map of prefix replacements for use when moduleResolutionMode is {@link + * ResolutionMode#BROWSER_WITH_TRANSFORMED_PREFIXES}. + */ + private ImmutableMap browserResolverPrefixReplacements; /** Which entries to look for in package.json files when processing modules */ List packageJsonEntryNames; @@ -1195,6 +1202,7 @@ public CompilerOptions() { // Which environment to use environment = Environment.BROWSER; + browserResolverPrefixReplacements = ImmutableMap.of(); // Modules moduleResolutionMode = ModuleLoader.ResolutionMode.BROWSER; @@ -2763,12 +2771,21 @@ public CompilerOptions setEmitUseStrict(boolean emitUseStrict) { return this; } - public ModuleLoader.ResolutionMode getModuleResolutionMode() { + public ResolutionMode getModuleResolutionMode() { return this.moduleResolutionMode; } - public void setModuleResolutionMode(ModuleLoader.ResolutionMode mode) { - this.moduleResolutionMode = mode; + public void setModuleResolutionMode(ResolutionMode moduleResolutionMode) { + this.moduleResolutionMode = moduleResolutionMode; + } + + public ImmutableMap getBrowserResolverPrefixReplacements() { + return this.browserResolverPrefixReplacements; + } + + public void setBrowserResolverPrefixReplacements( + ImmutableMap browserResolverPrefixReplacements) { + this.browserResolverPrefixReplacements = browserResolverPrefixReplacements; } public List getPackageJsonEntryNames() { diff --git a/src/com/google/javascript/jscomp/deps/BrowserModuleResolver.java b/src/com/google/javascript/jscomp/deps/BrowserModuleResolver.java index eb8cf5f2a74..fd5d50d5606 100644 --- a/src/com/google/javascript/jscomp/deps/BrowserModuleResolver.java +++ b/src/com/google/javascript/jscomp/deps/BrowserModuleResolver.java @@ -21,6 +21,7 @@ import com.google.javascript.jscomp.CheckLevel; import com.google.javascript.jscomp.ErrorHandler; import com.google.javascript.jscomp.JSError; +import com.google.javascript.jscomp.deps.ModuleLoader.ModuleResolverFactory; import javax.annotation.Nullable; /** @@ -30,6 +31,8 @@ */ public class BrowserModuleResolver extends ModuleResolver { + public static final ModuleResolverFactory FACTORY = BrowserModuleResolver::new; + public BrowserModuleResolver( ImmutableSet modulePaths, ImmutableList moduleRootPaths, @@ -51,7 +54,7 @@ public String resolveJsModule( colno, ModuleLoader.INVALID_MODULE_PATH, moduleAddress, - "BROWSER")); + ModuleLoader.ResolutionMode.BROWSER.toString())); return null; } diff --git a/src/com/google/javascript/jscomp/deps/BrowserWithTransformedPrefixesModuleResolver.java b/src/com/google/javascript/jscomp/deps/BrowserWithTransformedPrefixesModuleResolver.java new file mode 100644 index 00000000000..765abac7fdf --- /dev/null +++ b/src/com/google/javascript/jscomp/deps/BrowserWithTransformedPrefixesModuleResolver.java @@ -0,0 +1,173 @@ +/* + * Copyright 2017 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.collect.ImmutableSortedSet.toImmutableSortedSet; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +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 com.google.javascript.jscomp.deps.ModuleLoader.ModuleResolverFactory; +import java.util.Comparator; +import java.util.Set; +import javax.annotation.Nullable; + +/** + * Limited superset of the {@link BrowserModuleResolver} that allows for replacing some path + * prefixes before resolving. + */ +public class BrowserWithTransformedPrefixesModuleResolver extends ModuleResolver { + + static final DiagnosticType TRANSFORMED_PATH_IS_AMBIGUOUS = + DiagnosticType.error( + "JSC_TRANSFORMED_PATH_IS_AMBIGUOUS", + "Replacing \"{0}\" with \"{1}\" in the import path \"{2}\" is an ambiguous address " + + "(\"{3}\")."); + + /** Factory for {@link BrowserWithTransformedPrefixesModuleResolver}. */ + public static final class Factory implements ModuleResolverFactory { + private final ImmutableMap prefixReplacements; + + public Factory( + ImmutableMap prefixReplacements) { + this.prefixReplacements = prefixReplacements; + } + + @Override + public ModuleResolver create( + ImmutableSet modulePaths, + ImmutableList moduleRootPaths, + ErrorHandler errorHandler) { + return new BrowserWithTransformedPrefixesModuleResolver( + modulePaths, moduleRootPaths, errorHandler, prefixReplacements); + } + } + + /** + * Struct of prefix and replacement. Has a natural ordering from longest prefix to shortest + * prefix. + */ + @AutoValue + abstract static class PrefixReplacement { + abstract String prefix(); + abstract String replacement(); + + public static PrefixReplacement of(String prefix, String replacement) { + return new AutoValue_BrowserWithTransformedPrefixesModuleResolver_PrefixReplacement( + prefix, replacement); + } + } + + private final ImmutableSet prefixReplacements; + + public BrowserWithTransformedPrefixesModuleResolver( + ImmutableSet modulePaths, + ImmutableList moduleRootPaths, + ErrorHandler errorHandler, + ImmutableMap prefixReplacements) { + super(modulePaths, moduleRootPaths, errorHandler); + Set p = + prefixReplacements + .entrySet() + .stream() + .map(entry -> PrefixReplacement.of(entry.getKey(), entry.getValue())) + .collect( + toImmutableSortedSet( + // Sort by length in descending order to prefixes are applied most specific to + // least specific. + Comparator.comparingInt(r -> r.prefix().length()) + .reversed() + .thenComparing(r -> r.prefix()))); + this.prefixReplacements = ImmutableSet.copyOf(p); + } + + @Nullable + @Override + public String resolveJsModule( + String scriptAddress, String moduleAddress, String sourcename, int lineno, int colno) { + String transformedAddress = moduleAddress; + for (PrefixReplacement prefixReplacement : prefixReplacements) { + if (moduleAddress.startsWith(prefixReplacement.prefix())) { + transformedAddress = + prefixReplacement.replacement() + + moduleAddress.substring(prefixReplacement.prefix().length()); + + if (ModuleLoader.isAmbiguousIdentifier(transformedAddress)) { + errorHandler.report( + CheckLevel.WARNING, + JSError.make( + sourcename, + lineno, + colno, + TRANSFORMED_PATH_IS_AMBIGUOUS, + prefixReplacement.prefix(), + prefixReplacement.replacement(), + moduleAddress, + transformedAddress)); + } + break; + } + } + + // If ambiguous after the loop it was not transformed and the original moduleAddress is + // ambiguous. + if (ModuleLoader.isAmbiguousIdentifier(transformedAddress)) { + errorHandler.report( + CheckLevel.WARNING, + JSError.make( + sourcename, + lineno, + colno, + ModuleLoader.INVALID_MODULE_PATH, + transformedAddress, + ModuleLoader.ResolutionMode.BROWSER_WITH_TRANSFORMED_PREFIXES.toString())); + return null; + } + + String loadAddress = locate(scriptAddress, transformedAddress); + if (transformedAddress == null) { + errorHandler.report( + CheckLevel.WARNING, + JSError.make(sourcename, lineno, colno, ModuleLoader.LOAD_WARNING, moduleAddress)); + } + return loadAddress; + } + + @Override + public String resolveModuleAsPath(String scriptAddress, String moduleAddress) { + if (ModuleLoader.isRelativeIdentifier(moduleAddress)) { + return super.resolveModuleAsPath(scriptAddress, moduleAddress); + } + + String transformedAddress = moduleAddress; + for (PrefixReplacement prefixReplacement : prefixReplacements) { + if (moduleAddress.startsWith(prefixReplacement.prefix())) { + transformedAddress = + prefixReplacement.replacement() + + moduleAddress.substring(prefixReplacement.prefix().length()); + break; + } + } + + return ModuleLoader.normalize(transformedAddress, moduleRootPaths); + } +} diff --git a/src/com/google/javascript/jscomp/deps/JsFileParser.java b/src/com/google/javascript/jscomp/deps/JsFileParser.java index 6dc4f5a6ce2..e26a5eacc03 100644 --- a/src/com/google/javascript/jscomp/deps/JsFileParser.java +++ b/src/com/google/javascript/jscomp/deps/JsFileParser.java @@ -315,10 +315,7 @@ protected boolean parseLine(String line) throws ParseException { // cut off the "goog:" prefix requires.add(Require.googRequireSymbol(arg.substring(5))); } else { - ModuleLoader.ModulePath path = file.resolveJsModule(arg); - if (path == null) { - path = file.resolveModuleAsPath(arg); - } + ModuleLoader.ModulePath path = file.resolveModuleAsPath(arg); requires.add(Require.es6Import(path.toModuleName(), arg)); } } diff --git a/src/com/google/javascript/jscomp/deps/ModuleLoader.java b/src/com/google/javascript/jscomp/deps/ModuleLoader.java index 35b33403e7e..dc0f93daff4 100644 --- a/src/com/google/javascript/jscomp/deps/ModuleLoader.java +++ b/src/com/google/javascript/jscomp/deps/ModuleLoader.java @@ -29,10 +29,8 @@ import com.google.javascript.jscomp.ErrorHandler; import com.google.javascript.jscomp.JSError; import java.nio.file.Paths; -import java.util.HashMap; import java.util.HashSet; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; import javax.annotation.Nullable; @@ -85,9 +83,8 @@ public ModuleLoader( @Nullable ErrorHandler errorHandler, Iterable moduleRoots, Iterable inputs, - PathResolver pathResolver, - ResolutionMode resolutionMode, - Map lookupMap) { + ModuleResolverFactory factory, + PathResolver pathResolver) { checkNotNull(moduleRoots); checkNotNull(inputs); checkNotNull(pathResolver); @@ -98,51 +95,16 @@ public ModuleLoader( resolvePaths( Iterables.transform(Iterables.transform(inputs, DependencyInfo::getName), pathResolver), moduleRootPaths); - - switch (resolutionMode) { - case BROWSER: - this.moduleResolver = - new BrowserModuleResolver(this.modulePaths, this.moduleRootPaths, this.errorHandler); - break; - case NODE: - this.moduleResolver = - new NodeModuleResolver( - this.modulePaths, this.moduleRootPaths, lookupMap, this.errorHandler); - break; - case WEBPACK: - Map normalizedPathsById = new HashMap<>(); - for (Entry moduleEntry : lookupMap.entrySet()) { - String canonicalizedPath = - normalize(ModuleNames.escapePath(moduleEntry.getValue()), moduleRootPaths); - if (isAmbiguousIdentifier(canonicalizedPath)) { - canonicalizedPath = MODULE_SLASH + canonicalizedPath; - } - normalizedPathsById.put(moduleEntry.getKey(), canonicalizedPath); - } - this.moduleResolver = - new WebpackModuleResolver( - this.modulePaths, this.moduleRootPaths, normalizedPathsById, this.errorHandler); - break; - default: - throw new RuntimeException("Unexpected resolution mode " + resolutionMode); - } - } - - public ModuleLoader( - @Nullable ErrorHandler errorHandler, - Iterable moduleRoots, - Iterable inputs, - ResolutionMode resolutionMode) { - this(errorHandler, moduleRoots, inputs, PathResolver.RELATIVE, resolutionMode); + this.moduleResolver = + factory.create(this.modulePaths, this.moduleRootPaths, this.errorHandler); } public ModuleLoader( @Nullable ErrorHandler errorHandler, Iterable moduleRoots, Iterable inputs, - PathResolver pathResolver, - ResolutionMode resolutionMode) { - this(errorHandler, moduleRoots, inputs, pathResolver, resolutionMode, null); + ModuleResolverFactory factory) { + this(errorHandler, moduleRoots, inputs, factory, PathResolver.RELATIVE); } @VisibleForTesting @@ -229,18 +191,7 @@ public ModulePath resolveJsModule( *

Primarily used for per-file ES6 module transpilation */ public ModulePath resolveModuleAsPath(String moduleAddress) { - if (!moduleAddress.endsWith(".js")) { - moduleAddress += ".js"; - } - String path = ModuleNames.escapePath(moduleAddress); - if (isRelativeIdentifier(moduleAddress)) { - String ourPath = this.path; - int lastIndex = ourPath.lastIndexOf(MODULE_SLASH); - path = - ModuleNames.canonicalizePath( - ourPath.substring(0, lastIndex + MODULE_SLASH.length()) + path); - } - return new ModulePath(normalize(path, moduleRootPaths)); + return new ModulePath(moduleResolver.resolveModuleAsPath(this.path, moduleAddress)); } } @@ -250,6 +201,12 @@ public ModulePath resolve(String path) { normalize(ModuleNames.escapePath(pathResolver.apply(path)), moduleRootPaths)); } + /** Resolves a path into a {@link ModulePath}. */ + public ModulePath resolveWithoutEscapingPath(String path) { + return new ModulePath( + normalize(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); @@ -313,7 +270,7 @@ private static ImmutableSet resolvePaths( } /** Normalizes the name and resolves it against the module roots. */ - private static String normalize(String path, Iterable moduleRootPaths) { + static String normalize(String path, Iterable moduleRootPaths) { String normalizedPath = path; if (isAmbiguousIdentifier(normalizedPath)) { normalizedPath = MODULE_SLASH + normalizedPath; @@ -364,15 +321,25 @@ public String apply(String path) { } }; } + + /** An enum used to specify what algorithm to use to locate non path-based modules */ + @FunctionalInterface + public interface ModuleResolverFactory { + ModuleResolver create( + ImmutableSet modulePaths, + ImmutableList moduleRootPaths, + ErrorHandler errorHandler); + } + /** A trivial module loader with no roots. */ public static final ModuleLoader EMPTY = new ModuleLoader( - null, + /** errorReporter= */ null, ImmutableList.of(), ImmutableList.of(), - ResolutionMode.BROWSER); + BrowserModuleResolver.FACTORY); - /** An enum used to specify what algorithm to use to locate non path-based modules */ + /** Standard path base resolution algorithms that are accepted as a command line flag. */ public enum ResolutionMode { /** * Mimics the behavior of MS Edge. @@ -384,6 +351,14 @@ public enum ResolutionMode { */ BROWSER, + /** + * A limited superset of BROWSER that transforms some path prefixes. + * + *

For example one could configure this so that "@root/" is replaced with + * "/my/path/to/project/" within import paths.

+ */ + BROWSER_WITH_TRANSFORMED_PREFIXES, + /** * Uses the node module resolution algorithm. * @@ -396,7 +371,7 @@ public enum ResolutionMode { /** * Uses a lookup map provided by webpack to locate modules from a numeric id used during import */ - WEBPACK + WEBPACK, } private static final class NoopErrorHandler implements ErrorHandler { diff --git a/src/com/google/javascript/jscomp/deps/ModuleResolver.java b/src/com/google/javascript/jscomp/deps/ModuleResolver.java index efd52b275da..c364c061851 100644 --- a/src/com/google/javascript/jscomp/deps/ModuleResolver.java +++ b/src/com/google/javascript/jscomp/deps/ModuleResolver.java @@ -50,6 +50,21 @@ Map getPackageJsonMainEntries() { public abstract String resolveJsModule( String scriptAddress, String moduleAddress, String sourcename, int lineno, int colno); + public String resolveModuleAsPath(String scriptAddress, String moduleAddress) { + if (!moduleAddress.endsWith(".js")) { + moduleAddress += ".js"; + } + String path = ModuleNames.escapePath(moduleAddress); + if (ModuleLoader.isRelativeIdentifier(moduleAddress)) { + String ourPath = scriptAddress; + int lastIndex = ourPath.lastIndexOf(ModuleLoader.MODULE_SLASH); + path = + ModuleNames.canonicalizePath( + ourPath.substring(0, lastIndex + ModuleLoader.MODULE_SLASH.length()) + path); + } + return ModuleLoader.normalize(path, moduleRootPaths); + } + /** * Locates the module with the given name, but returns null if there is no JS file in the expected * location. diff --git a/src/com/google/javascript/jscomp/deps/NodeModuleResolver.java b/src/com/google/javascript/jscomp/deps/NodeModuleResolver.java index 25048fae033..3352d7db86a 100644 --- a/src/com/google/javascript/jscomp/deps/NodeModuleResolver.java +++ b/src/com/google/javascript/jscomp/deps/NodeModuleResolver.java @@ -23,6 +23,7 @@ import com.google.javascript.jscomp.CheckLevel; import com.google.javascript.jscomp.ErrorHandler; import com.google.javascript.jscomp.JSError; +import com.google.javascript.jscomp.deps.ModuleLoader.ModuleResolverFactory; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; @@ -100,6 +101,26 @@ private static ImmutableSortedSet buildNodeModulesFoldersRegistry( return ImmutableSortedSet.copyOfSorted(registry); } + /** Factory for {@link NodeModuleResolver}. */ + public static final class Factory implements ModuleResolverFactory { + private final Map packageJsonMainEntries; + + public Factory() { + this(/* packageJsonMainEntries= */ null); + } + + public Factory(@Nullable Map packageJsonMainEntries) { + this.packageJsonMainEntries = packageJsonMainEntries; + } + + @Override + public ModuleResolver create(ImmutableSet modulePaths, + ImmutableList moduleRootPaths, ErrorHandler errorHandler) { + return new NodeModuleResolver( + modulePaths, moduleRootPaths, packageJsonMainEntries, errorHandler); + } + } + public NodeModuleResolver( ImmutableSet modulePaths, ImmutableList moduleRootPaths, diff --git a/src/com/google/javascript/jscomp/deps/WebpackModuleResolver.java b/src/com/google/javascript/jscomp/deps/WebpackModuleResolver.java index 238d963085e..7cffeeb64b7 100644 --- a/src/com/google/javascript/jscomp/deps/WebpackModuleResolver.java +++ b/src/com/google/javascript/jscomp/deps/WebpackModuleResolver.java @@ -20,7 +20,10 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.javascript.jscomp.ErrorHandler; +import com.google.javascript.jscomp.deps.ModuleLoader.ModuleResolverFactory; +import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; import javax.annotation.Nullable; /** @@ -32,6 +35,35 @@ public class WebpackModuleResolver extends NodeModuleResolver { private final ImmutableMap modulesById; + /** + * Uses a lookup map provided by webpack to locate modules from a numeric id used during import + */ + public static final class Factory implements ModuleResolverFactory { + private final Map lookupMap; + + public Factory(Map lookupMap) { + this.lookupMap = lookupMap; + } + + @Override + public ModuleResolver create( + ImmutableSet modulePaths, + ImmutableList moduleRootPaths, + ErrorHandler errorHandler) { + Map normalizedPathsById = new HashMap<>(); + for (Entry moduleEntry : lookupMap.entrySet()) { + String canonicalizedPath = + ModuleLoader.normalize(ModuleNames.escapePath(moduleEntry.getValue()), moduleRootPaths); + if (ModuleLoader.isAmbiguousIdentifier(canonicalizedPath)) { + canonicalizedPath = ModuleLoader.MODULE_SLASH + canonicalizedPath; + } + normalizedPathsById.put(moduleEntry.getKey(), canonicalizedPath); + } + return new WebpackModuleResolver( + modulePaths, moduleRootPaths, normalizedPathsById, errorHandler); + } + } + public WebpackModuleResolver( ImmutableSet modulePaths, ImmutableList moduleRootPaths, diff --git a/test/com/google/javascript/jscomp/deps/DepsGeneratorTest.java b/test/com/google/javascript/jscomp/deps/DepsGeneratorTest.java index 4c0fe7db405..b8052e78af9 100644 --- a/test/com/google/javascript/jscomp/deps/DepsGeneratorTest.java +++ b/test/com/google/javascript/jscomp/deps/DepsGeneratorTest.java @@ -61,8 +61,8 @@ public void testEs6ModuleWithGoogProvide() throws Exception { null, ImmutableList.of("/base/"), ImmutableList.of(), - ModuleLoader.PathResolver.ABSOLUTE, - ModuleLoader.ResolutionMode.BROWSER)); + BrowserModuleResolver.FACTORY, + ModuleLoader.PathResolver.ABSOLUTE)); String output = depsGenerator.computeDependencyCalls(); assertWarnings( @@ -98,8 +98,8 @@ public void testEs6Modules() throws Exception { null, ImmutableList.of("/base/"), ImmutableList.of(), - ModuleLoader.PathResolver.ABSOLUTE, - ModuleLoader.ResolutionMode.BROWSER)); + BrowserModuleResolver.FACTORY, + ModuleLoader.PathResolver.ABSOLUTE)); String output = depsGenerator.computeDependencyCalls(); assertNoWarnings(); @@ -137,8 +137,8 @@ public void testGoogPathRequireForEs6ModuleFromGoogModule() throws Exception { null, ImmutableList.of("/base/"), ImmutableList.of(), - ModuleLoader.PathResolver.ABSOLUTE, - ModuleLoader.ResolutionMode.BROWSER)); + BrowserModuleResolver.FACTORY, + ModuleLoader.PathResolver.ABSOLUTE)); String output = depsGenerator.computeDependencyCalls(); assertNoWarnings(); @@ -180,8 +180,8 @@ public void testGoogPathRequireForEs6ModuleInDepsFileFromGoogModule() throws Exc null, ImmutableList.of("/base/"), ImmutableList.of(), - ModuleLoader.PathResolver.ABSOLUTE, - ModuleLoader.ResolutionMode.BROWSER)); + BrowserModuleResolver.FACTORY, + ModuleLoader.PathResolver.ABSOLUTE)); String output = depsGenerator.computeDependencyCalls(); assertNoWarnings(); @@ -324,9 +324,9 @@ public void testWithDepsAndSources() throws Exception { new ModuleLoader( null, ImmutableList.of("/base/"), - ImmutableList.of(), - ModuleLoader.PathResolver.ABSOLUTE, - ModuleLoader.ResolutionMode.BROWSER)); + ImmutableList.of(), + BrowserModuleResolver.FACTORY, + ModuleLoader.PathResolver.ABSOLUTE)); String output = depsGenerator.computeDependencyCalls(); @@ -345,9 +345,9 @@ public void testWithDepsAndSources() throws Exception { new ModuleLoader( null, ImmutableList.of("/base/"), - ImmutableList.of(), - ModuleLoader.PathResolver.ABSOLUTE, - ModuleLoader.ResolutionMode.BROWSER)); + ImmutableList.of(), + BrowserModuleResolver.FACTORY, + ModuleLoader.PathResolver.ABSOLUTE)); String expectedWithDepsAsSources = LINE_JOINER.join( @@ -400,9 +400,9 @@ public void testDepsAsSrcs() throws Exception { new ModuleLoader( null, ImmutableList.of("/base/" + "/"), - ImmutableList.of(), - ModuleLoader.PathResolver.ABSOLUTE, - ModuleLoader.ResolutionMode.BROWSER)); + ImmutableList.of(), + BrowserModuleResolver.FACTORY, + ModuleLoader.PathResolver.ABSOLUTE)); String output = depsGenerator.computeDependencyCalls(); @@ -457,9 +457,9 @@ private String testMergeStrategyHelper(DepsGenerator.InclusionStrategy mergeStra new ModuleLoader( null, ImmutableList.of("/base/"), - ImmutableList.of(), - ModuleLoader.PathResolver.ABSOLUTE, - ModuleLoader.ResolutionMode.BROWSER)); + ImmutableList.of(), + BrowserModuleResolver.FACTORY, + ModuleLoader.PathResolver.ABSOLUTE)); String output = depsGenerator.computeDependencyCalls(); @@ -483,9 +483,9 @@ private void doErrorMessagesRun( new ModuleLoader( null, ImmutableList.of(""), - ImmutableList.of(), - ModuleLoader.PathResolver.ABSOLUTE, - ModuleLoader.ResolutionMode.BROWSER)); + ImmutableList.of(), + BrowserModuleResolver.FACTORY, + ModuleLoader.PathResolver.ABSOLUTE)); String output = depsGenerator.computeDependencyCalls(); if (fatal) { @@ -550,9 +550,9 @@ public void testDuplicateProvidesIgnoredIfInClosureDirectory() throws Exception new ModuleLoader( null, ImmutableList.of("."), - ImmutableList.of(), - ModuleLoader.PathResolver.ABSOLUTE, - ModuleLoader.ResolutionMode.BROWSER)); + ImmutableList.of(), + BrowserModuleResolver.FACTORY, + ModuleLoader.PathResolver.ABSOLUTE)); String output = worker.computeDependencyCalls(); diff --git a/test/com/google/javascript/jscomp/deps/JsFileParserTest.java b/test/com/google/javascript/jscomp/deps/JsFileParserTest.java index 13f09f9c771..8b3452fd8b2 100644 --- a/test/com/google/javascript/jscomp/deps/JsFileParserTest.java +++ b/test/com/google/javascript/jscomp/deps/JsFileParserTest.java @@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableMap; import com.google.javascript.jscomp.ErrorManager; import com.google.javascript.jscomp.PrintStreamErrorManager; +import com.google.javascript.jscomp.deps.DependencyInfo.Require; import junit.framework.TestCase; /** @@ -281,8 +282,8 @@ public void testParseEs6Module4() { new ModuleLoader( null, ImmutableList.of("/foo"), - ImmutableList.of(), - ModuleLoader.ResolutionMode.BROWSER); + ImmutableList.of(), + BrowserModuleResolver.FACTORY); String contents = "" + "import './a';\n" @@ -321,7 +322,7 @@ public void testParseEs6ModuleWithGoogProvide() { null, ImmutableList.of("/foo"), ImmutableList.of(), - ModuleLoader.ResolutionMode.BROWSER); + BrowserModuleResolver.FACTORY); String contents = "goog.provide('my.namespace');\nexport {};"; @@ -348,7 +349,7 @@ public void testEs6ModuleWithDeclareNamespace() { null, ImmutableList.of("/foo"), ImmutableList.of(), - ModuleLoader.ResolutionMode.BROWSER); + BrowserModuleResolver.FACTORY); String contents = "goog.module.declareNamespace('my.namespace');\nexport {};"; @@ -364,6 +365,30 @@ public void testEs6ModuleWithDeclareNamespace() { assertDeps(expected, result); } + public void testEs6ModuleWithBrowserTransformedPrefixResolver() { + ModuleLoader loader = + new ModuleLoader( + null, + ImmutableList.of(), + ImmutableList.of(), + new BrowserWithTransformedPrefixesModuleResolver.Factory( + ImmutableMap.of("@root/", "/path/to/project/"))); + + String contents = "import '@root/my/file.js';"; + + DependencyInfo expected = + SimpleDependencyInfo.builder("../bar/baz.js", "/foo/js/bar/baz.js") + .setProvides(ImmutableList.of("module$foo$js$bar$baz")) + .setRequires(Require.es6Import("module$path$to$project$my$file", "@root/my/file.js")) + .setLoadFlags(ImmutableMap.of("module", "es6")) + .build(); + + DependencyInfo result = + parser.setModuleLoader(loader).parseFile("/foo/js/bar/baz.js", "../bar/baz.js", contents); + + assertDeps(expected, result); + } + /** * Tests: * -Shortcut mode doesn't stop at setTestOnly() or declareLegacyNamespace(). diff --git a/test/com/google/javascript/jscomp/deps/ModuleLoaderTest.java b/test/com/google/javascript/jscomp/deps/ModuleLoaderTest.java index 1ad94e2aa54..70767871bd7 100644 --- a/test/com/google/javascript/jscomp/deps/ModuleLoaderTest.java +++ b/test/com/google/javascript/jscomp/deps/ModuleLoaderTest.java @@ -20,8 +20,11 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.javascript.jscomp.CompilerInput; +import com.google.javascript.jscomp.ErrorHandler; import com.google.javascript.jscomp.SourceFile; +import javax.annotation.Nullable; import junit.framework.TestCase; /** Tests for {@link ModuleLoader}. */ @@ -38,7 +41,7 @@ public void testWindowsAddresses() { null, ImmutableList.of("."), inputs("js\\a.js", "js\\b.js"), - ModuleLoader.ResolutionMode.NODE); + new NodeModuleResolver.Factory()); assertUri("js/a.js", loader.resolve("js\\a.js")); assertUri("js/b.js", loader.resolve("js\\a.js").resolveJsModule("./b")); } @@ -49,9 +52,8 @@ public void testJsExtensionNode() { null, ImmutableList.of("."), inputs("js/a.js", "js/b.js"), - ModuleLoader.PathResolver.RELATIVE, - ModuleLoader.ResolutionMode.NODE, - packageJsonMainEntries); + new NodeModuleResolver.Factory(packageJsonMainEntries), + ModuleLoader.PathResolver.RELATIVE); assertUri("js/a.js", loader.resolve("js/a.js")); assertUri("js/b.js", loader.resolve("js/a.js").resolveJsModule("./b")); assertUri("js/b.js", loader.resolve("js/a.js").resolveJsModule("./b.js")); @@ -63,9 +65,8 @@ public void testLocateJsNode() throws Exception { null, ImmutableList.of("."), inputs("A/index.js", "B/index.js", "app.js"), - ModuleLoader.PathResolver.RELATIVE, - ModuleLoader.ResolutionMode.NODE, - packageJsonMainEntries); + new NodeModuleResolver.Factory(packageJsonMainEntries), + ModuleLoader.PathResolver.RELATIVE); input("A/index.js"); input("B/index.js"); @@ -98,9 +99,8 @@ public void testLocateNodeModuleNode() throws Exception { null, (new ImmutableList.Builder()).build(), compilerInputs, - ModuleLoader.PathResolver.RELATIVE, - ModuleLoader.ResolutionMode.NODE, - packageJsonMainEntries); + new NodeModuleResolver.Factory(packageJsonMainEntries), + ModuleLoader.PathResolver.RELATIVE); assertUri("/A/index.js", loader.resolve(" /foo.js").resolveJsModule("/A")); assertUri("/A/index.js", loader.resolve("/foo.js").resolveJsModule("/A/index.js")); @@ -130,7 +130,7 @@ public void testJsExtensionBrowser() { null, ImmutableList.of("."), inputs("js/a.js", "js/b.js"), - ModuleLoader.ResolutionMode.BROWSER); + BrowserModuleResolver.FACTORY); assertUri("js/a.js", loader.resolve("js/a.js")); assertNull(loader.resolve("js/a.js").resolveJsModule("./b")); assertUri("js/b.js", loader.resolve("js/a.js").resolveJsModule("./b.js")); @@ -142,7 +142,7 @@ public void testLocateJsBrowser() throws Exception { null, ImmutableList.of("."), inputs("A/index.js", "B/index.js", "app.js"), - ModuleLoader.ResolutionMode.BROWSER); + BrowserModuleResolver.FACTORY); input("A/index.js"); input("B/index.js"); @@ -180,7 +180,7 @@ public void testLocateNodeModuleBrowser() throws Exception { null, (new ImmutableList.Builder()).build(), compilerInputs, - ModuleLoader.ResolutionMode.BROWSER); + BrowserModuleResolver.FACTORY); assertNull(loader.resolve("/foo.js").resolveJsModule("/A")); assertUri("/A/index.js", loader.resolve("/foo.js").resolveJsModule("/A/index.js")); @@ -205,7 +205,10 @@ public void testLocateNodeModuleBrowser() throws Exception { public void testNormalizeUris() throws Exception { ModuleLoader loader = new ModuleLoader( - null, ImmutableList.of("a", "b", "/c"), inputs(), ModuleLoader.ResolutionMode.BROWSER); + null, + ImmutableList.of("a", "b", "/c"), + inputs(), + BrowserModuleResolver.FACTORY); assertUri("a.js", loader.resolve("a/a.js")); assertUri("a.js", loader.resolve("a.js")); assertUri("some.js", loader.resolve("some.js")); @@ -220,7 +223,7 @@ public void testDuplicateUris() throws Exception { null, ImmutableList.of("a", "b"), inputs("a/f.js", "b/f.js"), - ModuleLoader.ResolutionMode.BROWSER); + BrowserModuleResolver.FACTORY); fail("Expected error"); } catch (IllegalArgumentException e) { assertThat(e).hasMessageThat().contains("Duplicate module path"); @@ -264,9 +267,8 @@ public void testLocateNodeModulesNoLeadingSlash() throws Exception { null, (new ImmutableList.Builder()).build(), compilerInputs, - ModuleLoader.PathResolver.RELATIVE, - ModuleLoader.ResolutionMode.NODE, - packageJsonMainEntries); + new NodeModuleResolver.Factory(packageJsonMainEntries), + ModuleLoader.PathResolver.RELATIVE); assertUri("/A/index.js", loader.resolve(" /foo.js").resolveJsModule("/A")); assertUri("/A/index.js", loader.resolve("/foo.js").resolveJsModule("/A/index.js")); @@ -318,9 +320,8 @@ public void testLocateNodeModulesBrowserFieldAdvancedUsage() throws Exception { null, (new ImmutableList.Builder()).build(), compilerInputs, - ModuleLoader.PathResolver.RELATIVE, - ModuleLoader.ResolutionMode.NODE, - packageJsonMainEntries); + new NodeModuleResolver.Factory(packageJsonMainEntries), + ModuleLoader.PathResolver.RELATIVE); assertUri( "/node_modules/mymodule/client.js", loader.resolve("/foo.js").resolveJsModule("mymodule")); @@ -352,9 +353,8 @@ public void testWebpack() throws Exception { null, ImmutableList.of("."), inputs("A/index.js", "B/index.js", "app.js"), - ModuleLoader.PathResolver.RELATIVE, - ModuleLoader.ResolutionMode.WEBPACK, - webpackModulesById); + new WebpackModuleResolver.Factory(webpackModulesById), + ModuleLoader.PathResolver.RELATIVE); input("A/index.js"); input("B/index.js"); @@ -376,6 +376,111 @@ public void testWebpack() throws Exception { assertUri("A/index.js", loader.resolve("app.js").resolveJsModule("./A/index")); } + public void testBrowserWithPrefixReplacement() { + ModuleLoader loader = + new ModuleLoader( + null, + ImmutableList.of("."), + inputs("/path/to/project0/index.js", "/path/to/project1/index.js", "app.js"), + new BrowserWithTransformedPrefixesModuleResolver.Factory( + ImmutableMap.builder() + .put("@project0/", "/path/to/project0/") + .put("+project1/", "/path/to/project1/") + .put("@root/", "/") + .build())); + + assertUri( + "/path/to/project0/index.js", + loader.resolve("fake.js").resolveJsModule("@project0/index.js")); + assertUri( + "/path/to/project1/index.js", + loader.resolve("fake.js").resolveJsModule("+project1/index.js")); + assertUri("/app.js", loader.resolve("fake.js").resolveJsModule("@root/app.js")); + } + + public void testBrowserWithPrefixReplacementResolveModuleAsPath() { + ModuleLoader loader = + new ModuleLoader( + null, + ImmutableList.of(".", "/path/to/project0/", "/path/to/project1/"), + inputs(), + new BrowserWithTransformedPrefixesModuleResolver.Factory( + ImmutableMap.builder() + .put("@project0/", "/path/to/project0/") + .put("+project1/", "/path/to/project1/") + .put("@root/", "/") + .build())); + + assertUri( + "index.js", + loader.resolve("fake.js").resolveModuleAsPath("@project0/index.js")); + assertUri( + "foo/bar/index.js", + loader.resolve("fake.js").resolveModuleAsPath("+project1/foo/bar/index.js")); + assertUri( + "@not/a/root/index.js", + loader.resolve("fake.js").resolveModuleAsPath("@not/a/root/index.js")); + } + + public void testBrowserWithPrefixReplacementAppliedMostSpecificToLeast() { + ModuleLoader loader = + new ModuleLoader( + null, + ImmutableList.of("."), + inputs("/p0/p1/p2/file.js"), + new BrowserWithTransformedPrefixesModuleResolver.Factory( + ImmutableMap.builder() + .put("0/1/2/", "/p0/p1/p2/") + .put("0/", "/p0/") + .put("0/1/", "/p0/p1/") + .build())); + + assertUri( + "/p0/p1/p2/file.js", + loader.resolve("fake.js").resolveJsModule("0/p1/p2/file.js")); + assertUri( + "/p0/p1/p2/file.js", + loader.resolve("fake.js").resolveJsModule("0/1/p2/file.js")); + assertUri( + "/p0/p1/p2/file.js", + loader.resolve("fake.js").resolveJsModule("0/1/2/file.js")); + } + + public void testCustomResolution() { + ModuleLoader loader = + new ModuleLoader( + null, + ImmutableList.of("."), + inputs("A/index.js", "B/index.js", "app.js"), + (ImmutableSet modulePaths, + ImmutableList moduleRootPaths, + ErrorHandler errorHandler) -> + new ModuleResolver(modulePaths, moduleRootPaths, errorHandler) { + @Nullable + @Override + public String resolveJsModule( + String scriptAddress, + String moduleAddress, + String sourcename, + int lineno, + int colno) { + if (moduleAddress.startsWith("@custom/")) { + moduleAddress = moduleAddress.substring(8); + } + return super.locate(scriptAddress, moduleAddress); + } + }); + + assertUri("A/index.js", loader.resolve("A/index.js"));; + assertUri("A/index.js", loader.resolve("B/index.js").resolveJsModule("../A/index.js")); + assertUri("A/index.js", loader.resolve("app.js").resolveJsModule("./A/index.js")); + assertUri("A/index.js", loader.resolve("folder/app.js").resolveJsModule("../A/index.js")); + + assertUri("A/index.js", loader.resolve("B/index.js").resolveJsModule("@custom/A/index.js")); + assertUri("A/index.js", loader.resolve("app.js").resolveJsModule("@custom/A/index.js")); + assertUri("A/index.js", loader.resolve("folder/app.js").resolveJsModule("@custom/A/index.js")); + } + CompilerInput input(String name) { return new CompilerInput(SourceFile.fromCode(name, ""), false); }