1818
1919import com .google .common .annotations .GwtIncompatible ;
2020import com .google .common .base .CharMatcher ;
21+ import com .google .javascript .jscomp .CheckLevel ;
2122import com .google .javascript .jscomp .ErrorManager ;
22-
23+ import com . google . javascript . jscomp . JSError ;
2324import java .io .Reader ;
2425import java .io .StringReader ;
2526import java .util .ArrayList ;
27+ import java .util .LinkedHashMap ;
2628import java .util .List ;
29+ import java .util .Map ;
2730import java .util .logging .Logger ;
2831import java .util .regex .Matcher ;
2932import java .util .regex .Pattern ;
3033
3134/**
32- * A parser that can extract goog.require() and goog.provide() dependency
33- * information from a .js file .
35+ * A parser that can extract dependency information from a .js file, including
36+ * goog.require, goog.provide, goog.module, import statements, and export statements .
3437 *
3538 * @author [email protected] (Andrew Grieve) 3639 */
3740@ GwtIncompatible ("java.util.regex" )
3841public final class JsFileParser extends JsFileLineParser {
3942
40- private static Logger logger = Logger .getLogger (JsFileParser .class .getName ());
43+ private static final Logger logger = Logger .getLogger (JsFileParser .class .getName ());
4144
4245 /** Pattern for matching goog.provide(*) and goog.require(*). */
4346 private static final Pattern GOOG_PROVIDE_REQUIRE_PATTERN =
4447 Pattern .compile (
4548 "(?:^|;)(?:[a-zA-Z0-9$_,:{}\\ s]+=)?\\ s*"
4649 + "goog\\ .(provide|module|require|addDependency)\\ s*\\ ((.*?)\\ )" );
4750
51+ /**
52+ * Pattern for matching import ... from './path/to/file'.
53+ *
54+ * <p>Unlike the goog.require() pattern above, this pattern does not
55+ * allow multiple statements per line. The import/export <b>must</b>
56+ * be at the beginning of the line to match.
57+ */
58+ private static final Pattern ES6_MODULE_PATTERN =
59+ Pattern .compile (
60+ // Require the import/export to be at the beginning of the line
61+ "^"
62+ // Either an import or export, but we don't care which, followed by at least one space
63+ + "(?:import|export)\\ b\\ s*"
64+ // Skip any identifier chars, as well as star, comma, braces, and spaces
65+ // This should match, e.g., "* as foo from ", or "Foo, {Bar as Baz} from ".
66+ // The 'from' keyword is required except in the case of "import '...';",
67+ // where there's nothing between 'import' and the module key string literal.
68+ + "(?:[a-zA-Z0-9$_*,{}\\ s]+\\ bfrom\\ s*|)"
69+ // Imports require a string literal at the end; it's optional for exports
70+ // (e.g. "export * from './other';", which is effectively also an import).
71+ // This optionally captures group #1, which is the imported module name.
72+ + "(?:['\" ]([^'\" ]+)['\" ])?"
73+ // Finally, this should be the entire statement, so ensure there's a semicolon.
74+ + "\\ s*;" );
75+
76+ /**
77+ * Pattern for 'export' keyword, e.g. "export default class ..." or "export {blah}".
78+ * The '\b' ensures we don't also match "exports = ...", which is not an ES6 module.
79+ */
80+ private static final Pattern ES6_EXPORT_PATTERN = Pattern .compile ("^export\\ b" );
81+
4882 /** The first non-comment line of base.js */
4983 private static final String BASE_JS_START = "var COMPILED = false;" ;
5084
5185 /** The start of a bundled goog.module, i.e. one that is wrapped in a goog.loadModule call */
5286 private static final String BUNDLED_GOOG_MODULE_START = "goog.loadModule(function(" ;
5387
5488 /** Matchers used in the parsing. */
55- private Matcher googMatcher = GOOG_PROVIDE_REQUIRE_PATTERN .matcher ("" );
89+ private final Matcher googMatcher = GOOG_PROVIDE_REQUIRE_PATTERN .matcher ("" );
90+
91+ /** Matchers used in the parsing. */
92+ private final Matcher es6Matcher = ES6_MODULE_PATTERN .matcher ("" );
5693
5794 /** The info for the file we are currently parsing. */
5895 private List <String > provides ;
5996 private List <String > requires ;
6097 private boolean fileHasProvidesOrRequires ;
98+ private ModuleLoader loader = ModuleLoader .EMPTY ;
99+ private ModuleLoader .ModuleUri fileUri ;
61100
62101 private enum ModuleType {
63102 NON_MODULE ,
64103 UNWRAPPED_GOOG_MODULE ,
65104 WRAPPED_GOOG_MODULE ,
105+ ES6_MODULE ,
66106 }
67107
68108 private ModuleType moduleType ;
@@ -97,6 +137,17 @@ public JsFileParser setIncludeGoogBase(boolean include) {
97137 return this ;
98138 }
99139
140+ /**
141+ * Sets a list of "module root" URIs, which allow relativizing filenames
142+ * for modules.
143+ *
144+ * @return this for easy chaining.
145+ */
146+ public JsFileParser setModuleLoader (ModuleLoader loader ) {
147+ this .loader = loader ;
148+ return this ;
149+ }
150+
100151 /**
101152 * Parses the given file and returns the dependency information that it
102153 * contained.
@@ -115,21 +166,46 @@ public DependencyInfo parseFile(String filePath, String closureRelativePath,
115166
116167 private DependencyInfo parseReader (String filePath ,
117168 String closureRelativePath , Reader fileContents ) {
118- provides = new ArrayList <>();
119- requires = new ArrayList <>();
120- fileHasProvidesOrRequires = false ;
121- moduleType = ModuleType .NON_MODULE ;
169+ this .provides = new ArrayList <>();
170+ this .requires = new ArrayList <>();
171+ this .fileHasProvidesOrRequires = false ;
172+ this .fileUri = loader .resolve (filePath );
173+ this .moduleType = ModuleType .NON_MODULE ;
122174
123175 logger .fine ("Parsing Source: " + filePath );
124176 doParse (filePath , fileContents );
125177
178+ if (moduleType == ModuleType .ES6_MODULE ) {
179+ provides .add (fileUri .toModuleName ());
180+ }
181+
182+ Map <String , String > loadFlags = new LinkedHashMap <>();
183+ switch (moduleType ) {
184+ case UNWRAPPED_GOOG_MODULE :
185+ loadFlags .put ("module" , "goog" );
186+ break ;
187+ case ES6_MODULE :
188+ loadFlags .put ("module" , "es6" );
189+ break ;
190+ default :
191+ // Nothing to do here.
192+ }
193+
126194 DependencyInfo dependencyInfo = new SimpleDependencyInfo (
127- closureRelativePath , filePath , provides , requires ,
128- moduleType == ModuleType .UNWRAPPED_GOOG_MODULE );
195+ closureRelativePath , filePath , provides , requires , loadFlags );
129196 logger .fine ("DepInfo: " + dependencyInfo );
130197 return dependencyInfo ;
131198 }
132199
200+ private void setModuleType (ModuleType type ) {
201+ if (moduleType != type && moduleType != ModuleType .NON_MODULE ) {
202+ // TODO(sdh): should this be an error?
203+ errorManager .report (
204+ CheckLevel .WARNING , JSError .make (ModuleLoader .MODULE_CONFLICT , fileUri .toString ()));
205+ }
206+ moduleType = type ;
207+ }
208+
133209 /**
134210 * Parses a line of JavaScript, extracting goog.provide and goog.require
135211 * information.
@@ -161,7 +237,7 @@ protected boolean parseLine(String line) throws ParseException {
161237 boolean isRequire = firstChar == 'r' ;
162238
163239 if (isModule && this .moduleType != ModuleType .WRAPPED_GOOG_MODULE ) {
164- this . moduleType = ModuleType .UNWRAPPED_GOOG_MODULE ;
240+ setModuleType ( ModuleType .UNWRAPPED_GOOG_MODULE ) ;
165241 }
166242
167243 if (isProvide || isRequire ) {
@@ -187,7 +263,29 @@ protected boolean parseLine(String line) throws ParseException {
187263 // base.js can't provide or require anything else.
188264 return false ;
189265 } else if (line .startsWith (BUNDLED_GOOG_MODULE_START )) {
190- this .moduleType = ModuleType .WRAPPED_GOOG_MODULE ;
266+ setModuleType (ModuleType .WRAPPED_GOOG_MODULE );
267+ }
268+
269+ if (line .startsWith ("import" ) || line .startsWith ("export" )) {
270+ es6Matcher .reset (line );
271+ while (es6Matcher .find ()) {
272+ setModuleType (ModuleType .ES6_MODULE );
273+ lineHasProvidesOrRequires = true ;
274+
275+ String arg = es6Matcher .group (1 );
276+ if (arg != null ) {
277+ if (arg .startsWith ("goog:" )) {
278+ requires .add (arg .substring (5 )); // cut off the "goog:" prefix
279+ } else {
280+ requires .add (fileUri .resolveEs6Module (arg ).toModuleName ());
281+ }
282+ }
283+ }
284+
285+ // This check is only relevant for modules that don't import anything.
286+ if (moduleType != ModuleType .ES6_MODULE && ES6_EXPORT_PATTERN .matcher (line ).lookingAt ()) {
287+ setModuleType (ModuleType .ES6_MODULE );
288+ }
191289 }
192290
193291 return !shortcutMode || lineHasProvidesOrRequires
0 commit comments