support multiple entry points and only html entry points

R=sigmund@google.com

Review URL: https://codereview.chromium.org//906413002
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e63ca43..fcde62e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,18 @@
+## 0.4.0
+
+Lots of transformer updates:
+
+* The `new_entry_point` option is gone. The bootstrapped file will now always
+just be the original name but `.dart` will be replaced with `.initialize.dart`.
+* The `html_entry_point` option is gone, and the file extension is now used to
+detect if it is an html or dart file. You should no longer list the dart file
+contained in the html file. Effectively resolves
+[13](https://github.com/dart-lang/initialize/issues/13).
+* The `entry_point` option has been renamed `entry_points` and now accepts
+either a single file path or list of file paths. Additionally, it now supports
+Glob syntax so many files can be selected at once. Resolves
+[19](https://github.com/dart-lang/initialize/issues/19).
+
 ## 0.3.1
 
 * Added `InitializePluginTransformer` class in `plugin_transformer.dart` which
@@ -25,7 +40,6 @@
 initializer constructors, as well as List and Map literals,
 [5](https://github.com/dart-lang/initialize/issues/5).
 
-
 ## 0.1.0+1
 
 Quick fix for the transformer on windows.
diff --git a/README.md b/README.md
index 64dfb79..99c8cec 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@
 
 ### @initMethod
 
-Ther is one initializer which comes with this package, `@initMethod`. Annotate
+There is one initializer which comes with this package, `@initMethod`. Annotate
 any top level function with this and it will be invoked automatically. For
 example, the program below will print `hello`:
 
@@ -51,11 +51,13 @@
 
 During development a mirror based system is used to find and run the initializers,
 but for deployment there is a transformer which can replace that with a static list
-of initializers to be ran. 
+of initializers to be ran.
 
-This will create a new entry point which bootstraps your existing app. If you
-supply an `html_entry_point` then any script tags whose src is the same as
-`entry_point` will be rewritten to the bootstrapped file `new_entry_point`.
+This will create a new entry point which bootstraps your existing app, this will
+have the same file name except `.dart` with be replaced with `.initialize.dart`.
+If you supply an html file to `entry_points` then it will bootstrap the dart
+script tag on that page and replace the `src` attribute to with the new
+bootstrap file.
 
 Below is an example pubspec with the transformer:
 
@@ -64,9 +66,7 @@
       initialize: any
     transformers:
     - initialize:
-        entry_point: web/index.dart
-        new_entry_point: web/index.bootstrap.dart
-        html_entry_point: web/index.html
+        entry_points: web/index.html
 
 ## Creating your own initializer
 
diff --git a/lib/transformer.dart b/lib/transformer.dart
index 8036761..6374bbb 100644
--- a/lib/transformer.dart
+++ b/lib/transformer.dart
@@ -8,7 +8,9 @@
 import 'package:analyzer/src/generated/ast.dart';
 import 'package:analyzer/src/generated/element.dart';
 import 'package:barback/barback.dart';
+import 'package:code_transformers/assets.dart';
 import 'package:code_transformers/resolver.dart';
+import 'package:glob/glob.dart';
 import 'package:html5lib/dom.dart' as dom;
 import 'package:html5lib/parser.dart' show parse;
 import 'package:path/path.dart' as path;
@@ -17,18 +19,18 @@
 /// logic.
 class InitializeTransformer extends Transformer {
   final Resolvers _resolvers;
-  final String _entryPoint;
-  final String _newEntryPoint;
-  final String _htmlEntryPoint;
+  final Iterable<Glob> _entryPointGlobs;
+  final bool _errorIfNotFound;
 
-  InitializeTransformer(
-      this._entryPoint, this._newEntryPoint, this._htmlEntryPoint)
-      : _resolvers = new Resolvers.fromMock({
-        // The list of types below is derived from:
-        //   * types that are used internally by the resolver (see
-        //   _initializeFrom in resolver.dart).
-        // TODO(jakemac): Move this into code_transformers so it can be shared.
-        'dart:core': '''
+  InitializeTransformer(List<String> entryPoints, {bool errorIfNotFound: true})
+      : _entryPointGlobs = entryPoints.map((e) => new Glob(e)),
+        _errorIfNotFound = errorIfNotFound,
+        _resolvers = new Resolvers.fromMock({
+          // The list of types below is derived from:
+          //   * types that are used internally by the resolver (see
+          //   _initializeFrom in resolver.dart).
+          // TODO(jakemac): Move this into code_transformers so it can be shared.
+          'dart:core': '''
             library dart.core;
             class Object {}
             class Function {}
@@ -57,96 +59,109 @@
             class List<V> extends Object {}
             class Map<K, V> extends Object {}
             ''',
-        'dart:html': '''
+          'dart:html': '''
             library dart.html;
             class HtmlElement {}
             ''',
-      });
+        });
 
-  factory InitializeTransformer.asPlugin(BarbackSettings settings) {
-    var entryPoint = settings.configuration['entry_point'];
-    var newEntryPoint = settings.configuration['new_entry_point'];
-    if (newEntryPoint == null) {
-      newEntryPoint = entryPoint.replaceFirst('.dart', '.bootstrap.dart');
-    }
-    var htmlEntryPoint = settings.configuration['html_entry_point'];
-    return new InitializeTransformer(entryPoint, newEntryPoint, htmlEntryPoint);
-  }
+  factory InitializeTransformer.asPlugin(BarbackSettings settings) =>
+      new InitializeTransformer(_readFileList(settings, 'entry_points'));
 
-  bool isPrimary(AssetId id) =>
-      _entryPoint == id.path || _htmlEntryPoint == id.path;
+  bool isPrimary(AssetId id) => _entryPointGlobs.any((g) => g.matches(id.path));
 
   Future apply(Transform transform) {
-    if (transform.primaryInput.id.path == _entryPoint) {
+    if (transform.primaryInput.id.path.endsWith('.dart')) {
       return _buildBootstrapFile(transform);
-    } else if (transform.primaryInput.id.path == _htmlEntryPoint) {
-      return _replaceEntryWithBootstrap(transform);
+    } else if (transform.primaryInput.id.path.endsWith('.html')) {
+      return transform.primaryInput.readAsString().then((html) {
+        var document = parse(html);
+        var originalDartFile =
+            _findMainScript(document, transform.primaryInput.id, transform);
+        return _buildBootstrapFile(transform, primaryId: originalDartFile).then(
+            (AssetId newDartFile) {
+          return _replaceEntryWithBootstrap(transform, document,
+              transform.primaryInput.id, originalDartFile, newDartFile);
+        });
+      });
+    } else {
+      transform.logger.warning(
+          'Invalid entry point ${transform.primaryInput.id}. Must be either a '
+          '.dart or .html file.');
     }
-    return null;
+    return new Future.value();
   }
 
-  Future _buildBootstrapFile(Transform transform) {
-    var newEntryPointId =
-        new AssetId(transform.primaryInput.id.package, _newEntryPoint);
+  // Returns the AssetId of the newly created bootstrap file.
+  Future<AssetId> _buildBootstrapFile(Transform transform,
+      {AssetId primaryId}) {
+    if (primaryId == null) primaryId = transform.primaryInput.id;
+    var newEntryPointId = new AssetId(primaryId.package,
+        '${path.url.withoutExtension(primaryId.path)}.initialize.dart');
     return transform.hasInput(newEntryPointId).then((exists) {
       if (exists) {
         transform.logger
             .error('New entry point file $newEntryPointId already exists.');
-      } else {
-        return _resolvers.get(transform).then((resolver) {
-          new _BootstrapFileBuilder(resolver, transform,
-              transform.primaryInput.id, newEntryPointId).run();
-          resolver.release();
-        });
+        return null;
       }
+
+      return _resolvers.get(transform, [primaryId]).then((resolver) {
+        new _BootstrapFileBuilder(resolver, transform, primaryId,
+            newEntryPointId, _errorIfNotFound).run();
+        resolver.release();
+        return newEntryPointId;
+      });
     });
   }
 
-  Future _replaceEntryWithBootstrap(Transform transform) {
-    // For now at least, _htmlEntryPoint, _entryPoint, and _newEntryPoint need
-    // to be in the same folder.
-    // TODO(jakemac): support package urls with _entryPoint or _newEntryPoint
-    // in `lib`, and _htmlEntryPoint in another directory.
-    var _expectedDir = path.split(_htmlEntryPoint)[0];
-    if (_expectedDir != path.split(_entryPoint)[0] ||
-        _expectedDir != path.split(_newEntryPoint)[0]) {
+  // Replaces script tags pointing to [originalDartFile] with [newDartFile] in
+  // [entryPoint].
+  void _replaceEntryWithBootstrap(Transform transform, dom.Document document,
+      AssetId entryPoint, AssetId originalDartFile, AssetId newDartFile) {
+    var found = false;
+    var scripts = document
+        .querySelectorAll('script[type="application/dart"]')
+        .where((script) => uriToAssetId(entryPoint, script.attributes['src'],
+            transform.logger, script.sourceSpan) == originalDartFile)
+        .toList();
+
+    if (scripts.length != 1) {
       transform.logger.error(
-          'htmlEntryPoint, entryPoint, and newEntryPoint(if supplied) all must '
-          'be in the same top level directory.');
+          'Expected exactly one script pointing to $originalDartFile in '
+          '$entryPoint, but found ${scripts.length}.');
+      return;
+    }
+    scripts[0].attributes['src'] = path.url.relative(newDartFile.path,
+        from: path.dirname(entryPoint.path));
+    transform.addOutput(new Asset.fromString(entryPoint, document.outerHtml));
+  }
+
+  AssetId _findMainScript(
+      dom.Document document, AssetId entryPoint, Transform transform) {
+    var scripts = document.querySelectorAll('script[type="application/dart"]');
+    if (scripts.length != 1) {
+      transform.logger.error('Expected exactly one dart script in $entryPoint '
+          'but found ${scripts.length}.');
+      return null;
     }
 
-    return transform.primaryInput.readAsString().then((String html) {
-      var found = false;
-      var doc = parse(html);
-      var scripts = doc.querySelectorAll('script[type="application/dart"]');
-      for (dom.Element script in scripts) {
-        if (!_isEntryPointScript(script)) continue;
-        script.attributes['src'] = _relativeDartEntryPath(_newEntryPoint);
-        found = true;
-      }
-      if (!found) {
-        transform.logger.error(
-            'Unable to find script for $_entryPoint in $_htmlEntryPoint.');
-      }
-      return transform.addOutput(
-          new Asset.fromString(transform.primaryInput.id, doc.outerHtml));
-    });
+    var src = scripts[0].attributes['src'];
+    // TODO(jakemac): Support inline scripts,
+    // https://github.com/dart-lang/initialize/issues/20
+    if (src == null) {
+      transform.logger.error('Inline scripts are not supported at this time.');
+      return null;
+    }
+
+    return uriToAssetId(
+        entryPoint, src, transform.logger, scripts[0].sourceSpan);
   }
-
-  // Checks if the src of this script tag is pointing at `_entryPoint`.
-  bool _isEntryPointScript(dom.Element script) =>
-      path.normalize(script.attributes['src']) ==
-          _relativeDartEntryPath(_entryPoint);
-
-  // The relative path from `_htmlEntryPoint` to `dartEntry`. You must ensure
-  // that neither of these is null before calling this function.
-  String _relativeDartEntryPath(String dartEntry) =>
-      path.relative(dartEntry, from: path.dirname(_htmlEntryPoint));
 }
 
 class _BootstrapFileBuilder {
   final Resolver _resolver;
   final Transform _transform;
+  final bool _errorIfNotFound;
   AssetId _entryPoint;
   AssetId _newEntryPoint;
 
@@ -162,12 +177,18 @@
 
   TransformLogger _logger;
 
-  _BootstrapFileBuilder(
-      this._resolver, this._transform, this._entryPoint, this._newEntryPoint) {
+  _BootstrapFileBuilder(this._resolver, this._transform, this._entryPoint,
+      this._newEntryPoint, this._errorIfNotFound) {
     _logger = _transform.logger;
     _initializeLibrary =
         _resolver.getLibrary(new AssetId('initialize', 'lib/initialize.dart'));
-    _initializer = _initializeLibrary.getType('Initializer');
+    if (_initializeLibrary != null) {
+      _initializer = _initializeLibrary.getType('Initializer');
+    } else if (_errorIfNotFound) {
+      _logger.warning('Unable to read "package:initialize/initialize.dart". '
+          'This file must be imported via $_entryPoint or a transitive '
+          'dependency.');
+    }
   }
 
   /// Adds the new entry point file to the transform. Should only be ran once.
@@ -452,6 +473,9 @@
   }
 
   bool _isInitializer(InterfaceType type) {
+    // If `_initializer` wasn't found then it was never loaded (even
+    // transitively), and so no annotations can be initializers.
+    if (_initializer == null) return false;
     if (type == null) return false;
     if (type.element.type == _initializer.type) return true;
     if (_isInitializer(type.superclass)) return true;
@@ -544,3 +568,25 @@
 
   _InitializerData(this.element, this.annotation);
 }
+
+// Reads a file list from a barback settings configuration field.
+_readFileList(BarbackSettings settings, String field) {
+  var value = settings.configuration[field];
+  if (value == null) return null;
+  var files = [];
+  bool error;
+  if (value is List) {
+    files = value;
+    error = value.any((e) => e is! String);
+  } else if (value is String) {
+    files = [value];
+    error = false;
+  } else {
+    error = true;
+  }
+  if (error) {
+    print('Bad value for "$field" in the initialize transformer. '
+        'Expected either one String or a list of Strings.');
+  }
+  return files;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 06b97c5..6ea1c27 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,11 +1,12 @@
 name: initialize
-version: 0.3.1+1
+version: 0.4.0
 author: Polymer.dart Authors <web@dartlang.org>
 description: Generic building blocks for doing static initialization.
 homepage: https://github.com/dart-lang/initialize
 dependencies:
   barback: '>=0.14.2 <0.16.0'
   code_transformers: '>=0.2.3 <0.3.0'
+  glob: ">=1.0.4 <2.0.0"
   html5lib: '>=0.12.0 <0.13.0'
   path: '>=1.3.0 <2.0.0'
 dev_dependencies:
@@ -18,19 +19,12 @@
 - initialize/build/loader_replacer:
     $include: lib/initialize.dart
 - initialize:
-    $include: test/initializer_test.dart
-    entry_point: test/initializer_test.dart
-- initialize:
-    $include: test/initializer_cycle_error_test.dart
-    entry_point: test/initializer_cycle_error_test.dart
-- initialize:
-    $include: test/initializer_custom_filter_test.dart
-    entry_point: test/initializer_custom_filter_test.dart
-- initialize:
-    $include: test/initializer_type_filter_test.dart
-    entry_point: test/initializer_type_filter_test.dart
-- initialize:
-    $include: test/init_method_test.dart
-    entry_point: test/init_method_test.dart
+    $include: '**/*_test.dart'
+    entry_points:
+      - test/initializer_test.dart
+      - test/initializer_cycle_error_test.dart
+      - test/initializer_custom_filter_test.dart
+      - test/initializer_type_filter_test.dart
+      - test/init_method_test.dart
 - $dart2js:
     $exclude: '**/*.dart'
diff --git a/test/transformer_test.dart b/test/transformer_test.dart
index a427e70..02b749d 100644
--- a/test/transformer_test.dart
+++ b/test/transformer_test.dart
@@ -10,10 +10,10 @@
 main() {
   useCompactVMConfiguration();
 
-  var transformer = new InitializeTransformer(
-      'web/index.dart', 'web/index.bootstrap.dart', 'web/index.html');
+  var htmlTransformer = new InitializeTransformer(['web/*.html']);
+  var dartTransformer = new InitializeTransformer(['web/index.dart']);
 
-  testPhases('basic', [[transformer]], {
+  testPhases('basic', [[htmlTransformer]], {
     'a|web/index.html': '''
         <html><head></head><body>
           <script type="application/dart" src="index.dart"></script>
@@ -67,10 +67,10 @@
   }, {
     'a|web/index.html': '''
         <html><head></head><body>
-          <script type="application/dart" src="index.bootstrap.dart"></script>
+          <script type="application/dart" src="index.initialize.dart"></script>
 
         </body></html>'''.replaceAll('        ', ''),
-    'a|web/index.bootstrap.dart': '''
+    'a|web/index.initialize.dart': '''
         import 'package:initialize/src/static_loader.dart';
         import 'package:initialize/initialize.dart';
         import 'index.dart' as i0;
@@ -99,7 +99,7 @@
         '''.replaceAll('        ', '')
   }, []);
 
-  testPhases('constructor arguments', [[transformer]], {
+  testPhases('constructor arguments', [[dartTransformer]], {
     'a|web/index.dart': '''
         @DynamicInit(foo)
         @DynamicInit(Foo.foo)
@@ -129,7 +129,7 @@
     'initialize|lib/initialize.dart': mockInitialize,
     'test_initializers|lib/common.dart': commonInitializers,
   }, {
-    'a|web/index.bootstrap.dart': '''
+    'a|web/index.initialize.dart': '''
         import 'package:initialize/src/static_loader.dart';
         import 'package:initialize/initialize.dart';
         import 'index.dart' as i0;