add initWebComponents

R=sigmund@google.com

Review URL: https://codereview.chromium.org//1026153002
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6de608f..a46dbea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,21 @@
+#### 0.11.1
+  * Added `initWebComponents` function which performs html import aware
+    initialization of an application. This is done by crawling all imported
+    documents for dart script tags and initializing them. Any applications using
+    this package should switch to this method instead of calling `run` from the
+    `initialize` package directly.
+  * You may also now just export `package:web_components/init.dart` to
+    initialize your app, and then stick your startup code inside a method marked
+    with `@initMethod`, for instance:
+
+        library my_app;
+        export 'package:web_components/init.dart';
+
+        @initMethod
+        void startup() {
+          // custom app code here.
+        }
+
 #### 0.11.0
   * Add `bindingStartDelimiters` option to the `ImportInlinerTransformer`. Any
     urls which contain any of the supplied delimiters before the first `/` will
diff --git a/lib/build/mirrors_remover.dart b/lib/build/mirrors_remover.dart
new file mode 100644
index 0000000..4cb8862
--- /dev/null
+++ b/lib/build/mirrors_remover.dart
@@ -0,0 +1,25 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+library web_components.build.mirrors_remover;
+
+import 'dart:async';
+import 'package:barback/barback.dart';
+
+/// Removes `mirror_initializer.dart` and replaces it with
+/// `static_initializer.dart`.
+class MirrorsRemoverTransformer extends Transformer {
+  MirrorsRemoverTransformer();
+  MirrorsRemoverTransformer.asPlugin(BarbackSettings settings);
+
+  bool isPrimary(AssetId id) => id.path == 'lib/src/init.dart';
+
+  @override
+  Future apply(Transform transform) async {
+    String source = await transform.primaryInput.readAsString();
+    source = source.replaceFirst(
+        'mirror_initializer.dart', 'static_initializer.dart');
+    transform.addOutput(
+        new Asset.fromString(transform.primaryInput.id, source));
+  }
+}
diff --git a/lib/init.dart b/lib/init.dart
new file mode 100644
index 0000000..1ebb141
--- /dev/null
+++ b/lib/init.dart
@@ -0,0 +1,9 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+library web_components.init;
+
+import 'dart:async';
+import 'src/init.dart';
+
+Future main() => initWebComponents();
diff --git a/lib/src/init.dart b/lib/src/init.dart
new file mode 100644
index 0000000..512134d
--- /dev/null
+++ b/lib/src/init.dart
@@ -0,0 +1,29 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+library web_components.src.init;
+
+import 'dart:async';
+import 'package:initialize/initialize.dart' show InitializerFilter;
+import 'package:web_components/web_components.dart';
+import 'mirror_initializer.dart' as init;
+
+/// Performs html import aware initialization by crawling all imported documents
+/// and initializing any script tags which appear in them.
+///
+/// By default, this will run all [HtmlImport] initializers, followed in a 2nd
+/// phase by all [CustomElement] and [CustomElementProxy] initializers. Then,
+/// unless [initAll] is [false], it will run all remaining initializers.
+///
+/// If a [typeFilter] or [customFilter] are supplied, only one phase is ran
+/// with the supplied filters.
+Future initWebComponents({List<Type> typeFilter, InitializerFilter customFilter,
+    bool initAll: true}) {
+  if (typeFilter != null || customFilter != null) {
+    return init.run(typeFilter: typeFilter, customFilter: customFilter);
+  } else {
+    return init.run(typeFilter: [HtmlImport])
+      .then((_) => init.run(typeFilter: [CustomElement, CustomElementProxy]))
+      .then((_) => initAll ? init.run() : null);
+  }
+}
diff --git a/lib/src/mirror_initializer.dart b/lib/src/mirror_initializer.dart
new file mode 100644
index 0000000..652031f
--- /dev/null
+++ b/lib/src/mirror_initializer.dart
@@ -0,0 +1,130 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Contains logic to initialize web_components apps during development. This
+/// implementation uses dart:mirrors to load each library as they are discovered
+/// through HTML imports. This is only meant to be used during development in
+/// dartium, and the web_components transformers replace this implementation
+/// for deployment.
+library web_components.src.mirror_initializer;
+
+import 'dart:async';
+import 'dart:collection' show LinkedHashMap;
+import 'dart:mirrors';
+import 'dart:html';
+import 'package:initialize/initialize.dart' as init;
+
+Future run({List<Type> typeFilter, init.InitializerFilter customFilter}) async {
+  var libraryUris = _discoverLibrariesToLoad(document, window.location.href)
+      .map(Uri.parse);
+
+  for (var uri in libraryUris) {
+    await init.run(
+        typeFilter: typeFilter, customFilter: customFilter, from: uri);
+  }
+  return null;
+}
+
+/// Walks the HTML import structure to discover all script tags that are
+/// implicitly loaded. This code is only used in Dartium and should only be
+/// called after all HTML imports are resolved. Polymer ensures this by asking
+/// users to put their Dart script tags after all HTML imports (this is checked
+/// by the linter, and Dartium will otherwise show an error message).
+Iterable<_ScriptInfo> _discoverScripts(
+    Document doc, String baseUri, [_State state]) {
+  if (state == null) state = new _State();
+  if (doc == null) {
+    print('warning: $baseUri not found.');
+    return state.scripts.values;
+  }
+  if (!state.seen.add(doc)) return state.scripts.values;
+
+  for (var node in doc.querySelectorAll('script,link[rel="import"]')) {
+    if (node is LinkElement) {
+      _discoverScripts(node.import, node.href, state);
+    } else if (node is ScriptElement && node.type == 'application/dart') {
+      var info = _scriptInfoFor(node, baseUri);
+      if (state.scripts.containsKey(info.resolvedUrl)) {
+        // TODO(jakemac): Move this to a web_components uri.
+        print('warning: Script `${info.resolvedUrl}` included more than once. '
+            'See http://goo.gl/5HPeuP#polymer_44 for more details.');
+      } else {
+        state.scripts[info.resolvedUrl] = info;
+      }
+    }
+  }
+  return state.scripts.values;
+}
+
+/// Internal state used in [_discoverScripts].
+class _State {
+  /// Documents that we have visited thus far.
+  final Set<Document> seen = new Set();
+
+  /// Scripts that have been discovered, in tree order.
+  final LinkedHashMap<String, _ScriptInfo> scripts = {};
+}
+
+/// Holds information about a Dart script tag.
+class _ScriptInfo {
+  /// The original URL seen in the tag fully resolved.
+  final String resolvedUrl;
+
+  /// Whether it seems to be a 'package:' URL (starts with the package-root).
+  bool get isPackage => packageUrl != null;
+
+  /// The equivalent 'package:' URL, if any.
+  final String packageUrl;
+
+  _ScriptInfo(this.resolvedUrl, {this.packageUrl});
+}
+
+
+// TODO(sigmund): explore other (cheaper) ways to resolve URIs relative to the
+// root library (see dartbug.com/12612)
+final _rootUri = currentMirrorSystem().isolate.rootLibrary.uri;
+
+/// Returns [_ScriptInfo] for [script] which was seen in [baseUri].
+_ScriptInfo _scriptInfoFor(script, baseUri) {
+  var uriString = script.src;
+  if (uriString != '') {
+    var uri = _rootUri.resolve(uriString);
+    if (!_isHttpStylePackageUrl(uri)) return new _ScriptInfo('$uri');
+    // Use package: urls if available. This rule here is more permissive than
+    // how we translate urls in polymer-build, but we expect Dartium to limit
+    // the cases where there are differences. The polymer-build issues an error
+    // when using packages/ inside lib without properly stepping out all the way
+    // to the packages folder. If users don't create symlinks in the source
+    // tree, then Dartium will also complain because it won't find the file seen
+    // in an HTML import.
+    var packagePath = uri.path.substring(
+        uri.path.lastIndexOf('packages/') + 'packages/'.length);
+    return new _ScriptInfo('$uri', packageUrl: 'package:$packagePath');
+  }
+
+  // Even in the case of inline scripts its ok to just use the baseUri since
+  // there can only be one per page.
+  return new _ScriptInfo(baseUri);
+}
+
+/// Whether [uri] is an http URI that contains a 'packages' segment, and
+/// therefore could be converted into a 'package:' URI.
+bool _isHttpStylePackageUrl(Uri uri) {
+  var uriPath = uri.path;
+  return uri.scheme == _rootUri.scheme &&
+      // Don't process cross-domain uris.
+      uri.authority == _rootUri.authority &&
+      uriPath.endsWith('.dart') &&
+      (uriPath.contains('/packages/') || uriPath.startsWith('packages/'));
+}
+
+Iterable<String> _discoverLibrariesToLoad(Document doc, String baseUri) =>
+    _discoverScripts(doc, baseUri).map(
+        (info) => _packageUrlExists(info) ? info.packageUrl : info.resolvedUrl);
+
+/// All libraries in the current isolate.
+final _libs = currentMirrorSystem().libraries;
+
+bool _packageUrlExists(_ScriptInfo info) =>
+    info.isPackage && _libs[Uri.parse(info.packageUrl)] != null;
diff --git a/lib/src/static_initializer.dart b/lib/src/static_initializer.dart
new file mode 100644
index 0000000..dd23f8a
--- /dev/null
+++ b/lib/src/static_initializer.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Static logic for initializing web_components. This assumes the entire app
+/// is reachable from the entry point.
+library web_components.src.static_initializer;
+
+import 'dart:async';
+import 'package:initialize/initialize.dart' as init;
+
+Future run({List<Type> typeFilter, init.InitializerFilter customFilter}) =>
+    init.run(typeFilter: typeFilter, customFilter: customFilter);
diff --git a/lib/web_components.dart b/lib/web_components.dart
index 379d04e..9ad6d84 100644
--- a/lib/web_components.dart
+++ b/lib/web_components.dart
@@ -4,5 +4,6 @@
 library web_components;
 
 export 'src/custom_element.dart';
+export 'src/init.dart';
 export 'custom_element_proxy.dart';
 export 'html_import_annotation.dart';
diff --git a/pubspec.yaml b/pubspec.yaml
index 9133a77..4cc22dd 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: web_components
-version: 0.11.0
+version: 0.11.1
 author: Polymer.dart Authors <web-ui-dev@dartlang.org>
 homepage: https://www.dartlang.org/polymer-dart/
 description: >
@@ -14,18 +14,21 @@
   barback: '>=0.14.2 <0.16.0'
   code_transformers: '^0.2.7'
   html5lib: '^0.12.0'
-  initialize: '^0.5.0+1'
+  initialize: '^0.6.0'
   path: '^1.3.0'
 dev_dependencies:
   unittest: '^0.11.0'
   browser: '^0.10.0'
 transformers:
+- web_components/build/mirrors_remover:
+    $include: 'lib/src/init.dart'
 - web_components:
     $include: '**/*_test.html'
     entry_points:
       - test/custom_element_test.html
       - test/custom_element_proxy_test.html
       - test/html_import_annotation_test.html
+      - test/init_web_components_test.html
 
 environment:
   sdk: ">=1.9.0-dev.7.1 <2.0.0"
diff --git a/test/build/mirrors_remover_test.dart b/test/build/mirrors_remover_test.dart
new file mode 100644
index 0000000..ab9fa08
--- /dev/null
+++ b/test/build/mirrors_remover_test.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+library web_components.test.build.mirrors_remover_test;
+
+import 'package:code_transformers/tests.dart';
+import 'package:web_components/build/mirrors_remover.dart';
+import 'package:unittest/compact_vm_config.dart';
+import 'package:unittest/unittest.dart';
+
+main() {
+  useCompactVMConfiguration();
+
+  var transformer = new MirrorsRemoverTransformer();
+  var phases = [[transformer]];
+
+  testPhases('basic', phases, {
+    'a|lib/src/init.dart': '''
+        libary web_components.init;
+
+        import 'src/mirror_initializer.dart';
+
+        foo() {}
+        ''',
+  }, {
+    'a|lib/src/init.dart': '''
+        libary web_components.init;
+
+        import 'src/static_initializer.dart';
+
+        foo() {}
+        ''',
+  }, []);
+}
diff --git a/test/custom_element_proxy_test.dart b/test/custom_element_proxy_test.dart
index 3437440..5fa11e0 100644
--- a/test/custom_element_proxy_test.dart
+++ b/test/custom_element_proxy_test.dart
@@ -6,10 +6,9 @@
 import 'dart:async';
 import 'dart:html';
 import 'dart:js';
-import 'package:initialize/initialize.dart' as init;
 import 'package:unittest/html_config.dart';
 import 'package:unittest/unittest.dart';
-import 'package:web_components/custom_element_proxy.dart';
+import 'package:web_components/web_components.dart';
 
 @CustomElementProxy('basic-element')
 class BasicElement extends HtmlElement {
@@ -34,7 +33,7 @@
 
 main() {
   useHtmlConfiguration();
-  init.run().then((_) {
+  initWebComponents().then((_) {
     var container = querySelector('#container') as DivElement;
 
     tearDown(() {
diff --git a/test/custom_element_test.dart b/test/custom_element_test.dart
index 2418def..641c8c9 100644
--- a/test/custom_element_test.dart
+++ b/test/custom_element_test.dart
@@ -5,7 +5,6 @@
 
 import 'dart:async';
 import 'dart:html';
-import 'package:initialize/initialize.dart' as init;
 import 'package:unittest/html_config.dart';
 import 'package:unittest/unittest.dart';
 import 'package:web_components/web_components.dart';
@@ -34,7 +33,7 @@
 
 main() {
   useHtmlConfiguration();
-  init.run().then((_) {
+  initWebComponents().then((_) {
     var container = querySelector('#container') as DivElement;
 
     setUp(() {
diff --git a/test/deps/a.dart b/test/deps/a.dart
new file mode 100644
index 0000000..8e2d6a1
--- /dev/null
+++ b/test/deps/a.dart
@@ -0,0 +1,7 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+@initializeTracker
+library web_components.test.deps.a;
+
+import 'package:initialize/src/initialize_tracker.dart';
diff --git a/test/deps/a.html b/test/deps/a.html
new file mode 100644
index 0000000..7636d42
--- /dev/null
+++ b/test/deps/a.html
@@ -0,0 +1,3 @@
+<link rel="import" href="b.html">
+<link rel="import" href="c.html">
+<script type="application/dart" src="a.dart"></script>
diff --git a/test/deps/b.dart b/test/deps/b.dart
new file mode 100644
index 0000000..63b8dfa
--- /dev/null
+++ b/test/deps/b.dart
@@ -0,0 +1,7 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+@initializeTracker
+library web_components.test.deps.b;
+
+import 'package:initialize/src/initialize_tracker.dart';
diff --git a/test/deps/b.html b/test/deps/b.html
new file mode 100644
index 0000000..14abd88
--- /dev/null
+++ b/test/deps/b.html
@@ -0,0 +1 @@
+<script type="application/dart" src="b.dart"></script>
diff --git a/test/deps/c.html b/test/deps/c.html
new file mode 100644
index 0000000..e312085
--- /dev/null
+++ b/test/deps/c.html
@@ -0,0 +1,6 @@
+<script type="application/dart">
+  @initializeTracker
+  library web_components.test.deps.c;
+
+  import 'package:initialize/src/initialize_tracker.dart';
+</script>
diff --git a/test/html_import_annotation_test.dart b/test/html_import_annotation_test.dart
index 09a3ffd..6b81a58 100644
--- a/test/html_import_annotation_test.dart
+++ b/test/html_import_annotation_test.dart
@@ -7,10 +7,9 @@
 library web_components.test.html_import_annotation;
 
 import 'dart:html';
-import 'package:initialize/initialize.dart' as init;
 import 'package:unittest/html_config.dart';
 import 'package:unittest/unittest.dart';
-import 'package:web_components/html_import_annotation.dart';
+import 'package:web_components/web_components.dart';
 import 'foo/bar.dart';
 
 const String importPath = 'my_import.html';
@@ -19,7 +18,7 @@
   useHtmlConfiguration();
 
   test('adds import to head', () {
-    return init.run().then((_) {
+    return initWebComponents().then((_) {
       var my_import = document.head.querySelector('link[href="$importPath"]');
       expect(my_import, isNotNull);
       expect(my_import.import.body.text, 'Hello world!\n');
diff --git a/test/init_web_components_test.dart b/test/init_web_components_test.dart
new file mode 100644
index 0000000..15279e5
--- /dev/null
+++ b/test/init_web_components_test.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+@initializeTracker
+library web_components.test.init_web_components_test;
+
+import 'package:unittest/html_config.dart';
+import 'package:unittest/unittest.dart';
+import 'package:initialize/initialize.dart' show LibraryIdentifier;
+import 'package:initialize/src/initialize_tracker.dart';
+import 'package:web_components/web_components.dart';
+
+const String importPath = 'my_import.html';
+
+main() {
+  useHtmlConfiguration();
+
+  test('can initialize scripts from html imports', () {
+    return initWebComponents().then((_) {
+      var expectedInitializers = [
+        const LibraryIdentifier(
+            #web_components.test.deps.b, null, 'deps/b.dart'),
+        const LibraryIdentifier(
+            #web_components.test.deps.c, null, 'deps/c.html'),
+        const LibraryIdentifier(
+            #web_components.test.deps.a, null, 'deps/a.dart'),
+        const LibraryIdentifier(#web_components.test.init_web_components_test,
+            null, 'init_web_components_test.dart'),
+      ];
+      expect(InitializeTracker.seen, expectedInitializers);
+    });
+  });
+}
diff --git a/test/init_web_components_test.html b/test/init_web_components_test.html
new file mode 100644
index 0000000..d418e6e
--- /dev/null
+++ b/test/init_web_components_test.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="dart.unittest" content="full-stack-traces">
+  <title> init_web_components test </title>
+  <style>
+     .unittest-table { font-family:monospace; border:1px; }
+     .unittest-pass { background: #6b3;}
+     .unittest-fail { background: #d55;}
+     .unittest-error { background: #a11;}
+  </style>
+  <script src="/packages/web_components/webcomponents.js"></script>
+  <script src="/packages/web_components/interop_support.js"></script>
+  <script src="/packages/web_components/dart_support.js"></script>
+  <link rel="import" href="deps/a.html">
+</head>
+<body>
+  <h1> Running init_web_components test </h1>
+
+  <script type="text/javascript"
+      src="/root_dart/tools/testing/dart/test_controller.js"></script>
+  <script type="application/dart" src="init_web_components_test.dart"></script>
+  <script type="text/javascript" src="/packages/browser/dart.js"></script>
+</body>
+</html>