-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
ES6ModuleLoader.java
208 lines (186 loc) · 6.79 KB
/
ES6ModuleLoader.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
/*
* Copyright 2014 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;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/**
* Provides compile-time locate semantics for ES6 and CommonJS modules.
*
* @see "Section 26.3.3.18.2 of the ES6 spec"
* @see "http://wiki.commonjs.org/wiki/Modules/1.1"
*/
public final class ES6ModuleLoader {
/** According to the spec, the forward slash should be the delimiter on all platforms. */
static final String MODULE_SLASH = "/";
/** The default module root, the current directory. */
public static final String DEFAULT_FILENAME_PREFIX = "." + MODULE_SLASH;
static final DiagnosticType LOAD_WARNING = DiagnosticType.warning(
"JSC_ES6_MODULE_LOAD_WARNING",
"Failed to load module \"{0}\"");
private final AbstractCompiler compiler;
/** The root URIs that modules are resolved against. */
private final List<URI> moduleRootUris;
/** The set of all known input module URIs (including trailing .js), after normalization. */
private final Set<URI> moduleUris;
/**
* Creates an instance of the module loader which can be used to locate ES6 and CommonJS modules.
*
* @param moduleRoots The root directories to locate modules in.
* @param inputs All inputs to the compilation process.
*/
public ES6ModuleLoader(AbstractCompiler compiler,
List<String> moduleRoots, Iterable<CompilerInput> inputs) {
this.compiler = compiler;
this.moduleRootUris =
Lists.transform(
moduleRoots,
new Function<String, URI>() {
@Override
public URI apply(String path) {
return createUri(path);
}
});
this.moduleUris = new HashSet<>();
for (CompilerInput input : inputs) {
if (!moduleUris.add(normalizeInputAddress(input))) {
// Having root URIs "a" and "b" and source files "a/f.js" and "b/f.js" is ambiguous.
throw new IllegalArgumentException(
"Duplicate module URI after resolving: " + input.getName());
}
}
}
/**
* Find a CommonJS module {@code requireName} relative to {@code context}.
* @return The normalized module URI, or {@code null} if not found.
*/
URI locateCommonJsModule(String requireName, CompilerInput context) {
// * the immediate name require'd
URI loadAddress = locate(requireName, context);
if (loadAddress == null) {
// * the require'd name + /index.js
loadAddress = locate(requireName + MODULE_SLASH + "index.js", context);
}
if (loadAddress == null) {
// * the require'd name with a potential trailing ".js"
loadAddress = locate(requireName + ".js", context);
}
return loadAddress; // could be null.
}
/**
* Find an ES6 module {@code moduleName} relative to {@code context}.
* @return The normalized module URI, or {@code null} if not found.
*/
URI locateEs6Module(String moduleName, CompilerInput context) {
URI uri = locateNoCheck(moduleName + ".js", context);
if (!moduleUris.contains(uri)) {
compiler.report(JSError.make(LOAD_WARNING, moduleName));
}
return uri;
}
/**
* Locates the module with the given name, but returns successfully even if
* there is no JS file corresponding to the returned URI.
*/
private URI locateNoCheck(String name, CompilerInput referrer) {
URI uri = createUri(name);
if (isRelativeIdentifier(name)) {
URI referrerUri = normalizeInputAddress(referrer);
uri = referrerUri.resolve(uri);
}
return normalizeAddress(uri);
}
/**
* Locates the module with the given name, but returns null if there is no JS
* file in the expected location.
*/
@Nullable
private URI locate(String name, CompilerInput referrer) {
URI uri = locateNoCheck(name, referrer);
if (moduleUris.contains(uri)) {
return uri;
}
return null;
}
/**
* Normalizes the address of {@code input} and resolves it against the module roots.
*/
URI normalizeInputAddress(CompilerInput input) {
String name = input.getName();
return normalizeAddress(createUri(name));
}
/**
* Normalizes the URI for the given {@code uri} by resolving it against the known
* {@link #moduleRootUris}.
*/
private URI normalizeAddress(URI uri) {
// Find a moduleRoot that this URI is under. If none, use as is.
for (URI moduleRoot : moduleRootUris) {
if (uri.toString().startsWith(moduleRoot.toString())) {
return moduleRoot.relativize(uri);
}
}
// Not underneath any of the roots.
return uri;
}
static URI createUri(String input) {
// Handle special characters
String encodedInput = input.replace(':', '-')
.replace('\\', '/')
.replace(" ", "%20")
.replace("[", "%5B")
.replace("]", "%5D")
.replace("<", "%3C")
.replace(">", "%3E");
return URI.create(encodedInput).normalize();
}
private static String stripJsExtension(String fileName) {
if (fileName.endsWith(".js")) {
return fileName.substring(0, fileName.length() - ".js".length());
}
return fileName;
}
/** Whether this is relative to the current file, or a top-level identifier. */
static boolean isRelativeIdentifier(String name) {
return name.startsWith("." + MODULE_SLASH) || name.startsWith(".." + MODULE_SLASH);
}
/** Whether this is absolute to the compilation. */
static boolean isAbsoluteIdentifier(String name) {
return name.startsWith(MODULE_SLASH);
}
/**
* 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 static String toModuleName(URI filename) {
String moduleName =
stripJsExtension(filename.toString())
.replaceAll("^\\." + Pattern.quote(MODULE_SLASH), "")
.replace(MODULE_SLASH, "$")
.replace('\\', '$')
.replace('-', '_')
.replace(':', '_')
.replace('.', '_')
.replace("%20", "_");
return "module$" + moduleName;
}
}