Adds @HtmlImport with both dynamic and transformer based implementations. Still need an HtmlInliner to inline the imports, but this will statically add them to the entry document.

R=jmesserly@google.com

Review URL: https://codereview.chromium.org//882823003
diff --git a/.gitignore b/.gitignore
index e979170..6e8cd5c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
 # Don’t commit the following directories created by pub.
 .pub
-build/
+/build/
 packages
 
 # Or the files created by dart2js.
diff --git a/.status b/.status
index 2ef412d..c3743e9 100644
--- a/.status
+++ b/.status
@@ -23,3 +23,7 @@
 
 [ $compiler == dart2js ]
 test/*: Skip # use pub-build to run tests (they need the companion .html file)
+
+[ $browser ]
+test/build/*: Skip # vm only tests
+build/test/build/*: Skip # vm only tests
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6aeadae..1421740 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,27 @@
+#### 0.10.2-dev
+  * Added the `HtmlImport` annotation. This can be added to any library
+    declaration and it will inject an html import to the specified path into the
+    head of the current document, which allows dart files to declare their html
+    dependencies. Paths can be relative to the current dart file or they can be
+    in `package:` form.
+
+    *Note*: Html imports included this way cannot contain dart script tags. The
+    mirror based implementation injects the imports dynamically and dart script
+    tags are not allowed to be injected in that way.
+
+    There is also a transformer supplied which will inline these imports into
+    the head of your document at compile time, it can be used like this:
+
+      transformers:
+      # Must include the `initialize` transformer first.
+      - initialize:
+          entry_point: web/index.dart
+          html_entry_point: web/index.html
+      - web_components/build/html_import_annotation_inliner:
+          # The bootstrap file created by the `initialize` transformer.
+          bootstrap_file: test/index.bootstrap.dart
+          html_entry_point: test/index.html
+
 #### 0.10.1
   * Added the `CustomElementProxy` annotation. This can be added to any class
     which proxies a javascript custom element and is the equivalent of calling
diff --git a/lib/build/html_import_annotation_inliner.dart b/lib/build/html_import_annotation_inliner.dart
new file mode 100644
index 0000000..f8ffaa2
--- /dev/null
+++ b/lib/build/html_import_annotation_inliner.dart
@@ -0,0 +1,124 @@
+// 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.html_import_annotation_inliner;
+
+import 'dart:async';
+import 'package:analyzer/analyzer.dart';
+import 'package:barback/barback.dart';
+import 'package:html5lib/dom.dart' as dom;
+import 'package:html5lib/parser.dart';
+import 'package:initialize/plugin_transformer.dart';
+import 'package:source_maps/refactor.dart';
+import '../src/normalize_path.dart';
+
+/// Given an html entry point with a single dart bootstrap file created by the
+/// `initialize` transformer, this will open that dart file and remove all
+/// `HtmlImport` initializers from it. Then it appends those imports to the head
+/// of the html entry point.
+/// Notes: Does not inline the import, it just puts the <link rel="import"> tag.
+/// This also has a few limitations, it only supports string literals (to avoid
+/// using the analyzer to resolve references) and doesn't support const
+/// references to HtmlImport annotations (for the same reason).
+class HtmlImportAnnotationInliner extends InitializePluginTransformer {
+  final String _bootstrapFile;
+  final String _htmlEntryPoint;
+  TransformLogger _logger;
+  final Set<String> importPaths = new Set();
+
+  HtmlImportAnnotationInliner(String bootstrapFile, this._htmlEntryPoint)
+      : super(bootstrapFile),
+        _bootstrapFile = bootstrapFile;
+
+  factory HtmlImportAnnotationInliner.asPlugin(BarbackSettings settings) {
+    var bootstrapFile = settings.configuration['bootstrap_file'];
+    if (bootstrapFile is! String || !bootstrapFile.endsWith('.dart')) {
+      throw new ArgumentError(
+          '`bootstrap_file` should be a string path to a dart file');
+    }
+    var htmlEntryPoint = settings.configuration['html_entry_point'];
+    if (htmlEntryPoint is! String || !htmlEntryPoint.endsWith('.html')) {
+      throw new ArgumentError(
+          '`html_entry_point` should be a string path to an html file');
+    }
+    return new HtmlImportAnnotationInliner(bootstrapFile, htmlEntryPoint);
+  }
+
+  classifyPrimary(AssetId id) {
+    var superValue = super.classifyPrimary(id);
+    if (superValue != null) return superValue;
+    // Group it with the bootstrap file.
+    if (_htmlEntryPoint == id.path) return _bootstrapFile;
+    return null;
+  }
+
+  apply(AggregateTransform transform) {
+    _logger = transform.logger;
+    return super.apply(transform).then((_) {
+      var htmlEntryPoint =
+          allAssets.firstWhere((asset) => asset.id.path == _htmlEntryPoint);
+      return htmlEntryPoint.readAsString().then((html) {
+        var doc = parse(html);
+        for (var importPath in importPaths) {
+          var import = new dom.Element.tag('link')
+            ..attributes = {'rel': 'import', 'href': importPath,};
+          doc.head.append(import);
+        }
+        transform
+            .addOutput(new Asset.fromString(htmlEntryPoint.id, doc.outerHtml));
+      });
+    });
+  }
+
+  // Executed for each initializer constructor in the bootstrap file. We filter
+  // out the HtmlImport ones and inline them.
+  initEntry(
+      InstanceCreationExpression expression, TextEditTransaction transaction) {
+    // Filter out extraneous values.
+    if (expression is! InstanceCreationExpression) return;
+    var args = expression.argumentList.arguments;
+    // Only support InstanceCreationExpressions. Const references to HtmlImport
+    // annotations can't be cheaply discovered.
+    if (args[0] is! InstanceCreationExpression) return;
+    if (!'${args[0].constructorName.type.name}'.contains('.HtmlImport')) return;
+
+    // Grab the raw path supplied to the HtmlImport. Only string literals are
+    // supported for the transformer.
+    var originalPath = args[0].argumentList.arguments[0];
+    if (originalPath is SimpleStringLiteral) {
+      originalPath = originalPath.value;
+    } else {
+      _logger.warning('Found HtmlImport constructor which was supplied an '
+          'expression. Only raw strings are currently supported for the '
+          'transformer, so $originalPath will be injected dynamically');
+      return;
+    }
+
+    // Now grab the package from the LibraryIdentifier, we know its either a
+    // string or null literal.
+    var package = args[1].argumentList.arguments[1];
+    if (package is SimpleStringLiteral) {
+      package = package.value;
+    } else if (package is NullLiteral) {
+      package = null;
+    } else {
+      _logger.error('Invalid LibraryIdentifier declaration. The 2nd argument '
+          'be a literal string or null. `${args[1]}`');
+    }
+
+    // And finally get the original dart file path, this is always a string
+    // literal.
+    var dartPath = args[1].argumentList.arguments[2];
+    if (dartPath is SimpleStringLiteral) {
+      dartPath = dartPath.value;
+    } else {
+      _logger.error('Invalid LibraryIdentifier declaration. The 3rd argument '
+          'be a literal string. `${args[1]}`');
+    }
+
+    // Add the normalized path to our list and remove the expression from the
+    // bootstrap file.
+    importPaths.add(normalizeHtmlImportPath(originalPath, package, dartPath));
+    removeInitializer(expression, transaction);
+  }
+}
diff --git a/lib/custom_element_proxy.dart b/lib/custom_element_proxy.dart
index f9df88f..01e63d5 100644
--- a/lib/custom_element_proxy.dart
+++ b/lib/custom_element_proxy.dart
@@ -1,6 +1,8 @@
 // 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.custom_element_proxy;
+
 import 'package:initialize/initialize.dart';
 import 'interop.dart';
 
diff --git a/lib/html_import_annotation.dart b/lib/html_import_annotation.dart
new file mode 100644
index 0000000..04738d5
--- /dev/null
+++ b/lib/html_import_annotation.dart
@@ -0,0 +1,36 @@
+// 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.html_import_annotation;
+
+import 'dart:async';
+import 'dart:html';
+import 'package:initialize/initialize.dart';
+import 'src/normalize_path.dart';
+
+/// Annotation for a dart library which injects an html import into the
+/// current html document. The imported file must not contain any dart script
+/// tags, as they cannot be dynamically loaded.
+class HtmlImport implements Initializer<LibraryIdentifier> {
+  final String filePath;
+
+  const HtmlImport(this.filePath);
+
+  Future initialize(LibraryIdentifier library) {
+    var element = new LinkElement()
+      ..rel = 'import'
+      ..href = normalizeHtmlImportPath(filePath, library.package, library.path);
+    document.head.append(element);
+    var completer = new Completer();
+    var listeners = [
+      element.on['load'].listen((_) => completer.complete()),
+      element.on['error'].listen((_) {
+        print('Error loading html import from path `$filePath`');
+        completer.complete();
+      }),
+    ];
+    return completer.future.then((_) {
+      listeners.forEach((listener) => listener.cancel());
+    });
+  }
+}
diff --git a/lib/src/normalize_path.dart b/lib/src/normalize_path.dart
new file mode 100644
index 0000000..bac6b8b
--- /dev/null
+++ b/lib/src/normalize_path.dart
@@ -0,0 +1,28 @@
+// 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.normalizePath;
+
+import 'package:path/path.dart' as path;
+
+String normalizeHtmlImportPath(
+    String filePath, String dartFilePackage, String dartFilePath) {
+  // If they already supplied a packages path, just return that.
+  if (filePath.startsWith('package:')) {
+    return filePath.replaceFirst('package:', 'packages/');
+  }
+
+  var dartFileDir = path.url.dirname(dartFilePath);
+  var segments = path.url.split(dartFileDir);
+  // The dartFileDir without the leading dir (web, lib, test, etc).
+  var dartFileSubDir = path.url.joinAll(segments.getRange(1, segments.length));
+
+  // Relative paths have no package supplied.
+  if (dartFilePackage == null) {
+    return path.url.normalize(path.url.join(dartFileSubDir, filePath));
+  }
+
+  // Only option left is a packages/ path.
+  return path.url.normalize(
+      path.url.join('packages/', dartFilePackage, dartFileSubDir, filePath));
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index ca97552..b4e908e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: web_components
-version: 0.10.1
+version: 0.10.2-dev
 author: Polymer.dart Authors <web-ui-dev@dartlang.org>
 homepage: https://www.dartlang.org/polymer-dart/
 description: >
@@ -10,13 +10,23 @@
   elements, by hiding DOM subtrees under shadow roots. HTML Imports let authors
   bundle code and HTML as if they were libraries.
 dependencies:
-  initialize: '>=0.2.0 <0.3.0'
+  analyzer: '>=0.22.4 <0.23.0'
+  html5lib: '>=0.12.0 <0.13.0'
+  initialize: '>=0.3.1 <0.4.0'
+  path: '>=1.3.0 <2.0.0'
 dev_dependencies:
+  code_transformers: '>=0.2.4 <0.3.0'
   unittest: '>0.11.0 <0.12.0'
   browser: '>0.10.0 <0.11.0'
 transformers:
 - initialize:
     entry_point: test/custom_element_proxy_test.dart
     html_entry_point: test/custom_element_proxy_test.html
+- initialize:
+    entry_point: test/html_import_annotation_test.dart
+    html_entry_point: test/html_import_annotation_test.html
+- web_components/build/html_import_annotation_inliner:
+    bootstrap_file: test/html_import_annotation_test.bootstrap.dart
+    html_entry_point: test/html_import_annotation_test.html
 environment:
   sdk: ">=1.4.0-dev.6.6 <2.0.0"
diff --git a/test/build/html_import_annotation_inliner_test.dart b/test/build/html_import_annotation_inliner_test.dart
new file mode 100644
index 0000000..0856a66
--- /dev/null
+++ b/test/build/html_import_annotation_inliner_test.dart
@@ -0,0 +1,80 @@
+// 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.html_import_annotation_inliner_test;
+
+import 'package:code_transformers/tests.dart';
+import 'package:web_components/build/html_import_annotation_inliner.dart';
+import 'package:unittest/compact_vm_config.dart';
+
+main() {
+  useCompactVMConfiguration();
+
+  var transformer = new HtmlImportAnnotationInliner(
+      'web/index.bootstrap.dart', 'web/index.html');
+
+  testPhases('basic', [[transformer]], {
+    'foo|web/index.html': '''
+        <html>
+          <head></head>
+          <body>
+            <script type="application/dart" src="index.bootstrap.dart"></script>
+          </body>
+        </html>
+        ''',
+    'foo|web/index.bootstrap.dart': '''
+        import 'package:initialize/src/static_loader.dart';
+        import 'package:initialize/src/initializer.dart';
+        import 'index.dart' as i0;
+        import 'package:web_components/html_import_annotation.dart' as i1;
+        import 'package:baz/baz.dart' as i2;
+
+        main() {
+          initializers.addAll([
+            new InitEntry(i2.initMethod, i0.baz),
+            new InitEntry(const i1.HtmlImport('foo.html'), const LibraryIdentifier(#foo, null, 'web/foo.dart')),
+            new InitEntry(const i1.HtmlImport('foo.html'), const LibraryIdentifier(#foo, null, 'web/foo/foo.dart')),
+            new InitEntry(const i1.HtmlImport('../foo.html'), const LibraryIdentifier(#foo, null, 'web/foo/foo.dart')),
+            new InitEntry(const i1.HtmlImport('package:foo/foo.html'), const LibraryIdentifier(#foo, null, 'lib/foo.dart')),
+            new InitEntry(const i1.HtmlImport('package:foo/foo/foo.html'), const LibraryIdentifier(#foo, null, 'lib/foo/foo.dart')),
+            new InitEntry(const i1.HtmlImport('bar.html'), const LibraryIdentifier(#bar, 'bar', 'lib/bar.dart')),
+            new InitEntry(const i1.HtmlImport('bar.html'), const LibraryIdentifier(#bar.Bar, 'bar', 'lib/bar/bar.dart')),
+            new InitEntry(const i1.HtmlImport('package:bar/bar.html'), const LibraryIdentifier(#bar, 'bar', 'lib/bar.dart')),
+            new InitEntry(const i1.HtmlImport('package:bar/bar/bar.html'), const LibraryIdentifier(#bar.Bar, 'bar', 'lib/bar/bar.dart')),
+          ]);
+
+          i0.main();
+        }
+        ''',
+  }, {
+    'foo|web/index.html': '''
+        <html>
+          <head>
+            <link rel="import" href="foo.html">
+            <link rel="import" href="foo/foo.html">
+            <link rel="import" href="packages/foo/foo.html">
+            <link rel="import" href="packages/foo/foo/foo.html">
+            <link rel="import" href="packages/bar/bar.html">
+            <link rel="import" href="packages/bar/bar/bar.html">
+          </head>
+          <body>
+            <script type="application/dart" src="index.bootstrap.dart"></script>
+          </body>
+        </html>''',
+    'foo|web/index.bootstrap.dart': '''
+        import 'package:initialize/src/static_loader.dart';
+        import 'package:initialize/src/initializer.dart';
+        import 'index.dart' as i0;
+        import 'package:web_components/html_import_annotation.dart' as i1;
+        import 'package:baz/baz.dart' as i2;
+
+        main() {
+          initializers.addAll([
+            new InitEntry(i2.initMethod, i0.baz),
+          ]);
+
+          i0.main();
+        }
+        '''
+  }, [], StringFormatter.noNewlinesOrSurroundingWhitespace);
+}
diff --git a/test/custom_element_proxy_test.dart b/test/custom_element_proxy_test.dart
index ed2342f..3437440 100644
--- a/test/custom_element_proxy_test.dart
+++ b/test/custom_element_proxy_test.dart
@@ -35,42 +35,40 @@
 main() {
   useHtmlConfiguration();
   init.run().then((_) {
+    var container = querySelector('#container') as DivElement;
 
-  var container = querySelector('#container') as DivElement;
-
-  tearDown(() {
-    container.children.clear();
-  });
-
-  test('basic custom element', () {
-    container.append(new BasicElement());
-    container.appendHtml('<basic-element></basic_element>');
-    // TODO(jakemac): after appendHtml elements are upgraded asynchronously,
-    // why? https://github.com/dart-lang/web-components/issues/4
-    return new Future(() {}).then((_) {
-      var elements = container.querySelectorAll('basic-element');
-      expect(elements.length, 2);
-      for (BasicElement element in elements) {
-        print(element.outerHtml);
-        print(element.runtimeType);
-        expect(element.isBasicElement, isTrue);
-      }
+    tearDown(() {
+      container.children.clear();
     });
-  });
 
-  test('extends custom element', () {
-    container.append(new ExtendedElement());
-    container.appendHtml('<input is="extended-element" />');
-    // TODO(jakemac): after appendHtml elements are upgraded asynchronously,
-    // why? https://github.com/dart-lang/web-components/issues/4
-    return new Future(() {}).then((_) {
-      var elements = container.querySelectorAll('input');
-      expect(elements.length, 2);
-      for (ExtendedElement element in elements) {
-        expect(element.isExtendedElement, isTrue);
-      }
+    test('basic custom element', () {
+      container.append(new BasicElement());
+      container.appendHtml('<basic-element></basic_element>');
+      // TODO(jakemac): after appendHtml elements are upgraded asynchronously,
+      // why? https://github.com/dart-lang/web-components/issues/4
+      return new Future(() {}).then((_) {
+        var elements = container.querySelectorAll('basic-element');
+        expect(elements.length, 2);
+        for (BasicElement element in elements) {
+          print(element.outerHtml);
+          print(element.runtimeType);
+          expect(element.isBasicElement, isTrue);
+        }
+      });
     });
-  });
 
+    test('extends custom element', () {
+      container.append(new ExtendedElement());
+      container.appendHtml('<input is="extended-element" />');
+      // TODO(jakemac): after appendHtml elements are upgraded asynchronously,
+      // why? https://github.com/dart-lang/web-components/issues/4
+      return new Future(() {}).then((_) {
+        var elements = container.querySelectorAll('input');
+        expect(elements.length, 2);
+        for (ExtendedElement element in elements) {
+          expect(element.isExtendedElement, isTrue);
+        }
+      });
+    });
   });
 }
diff --git a/test/html_import_annotation_test.dart b/test/html_import_annotation_test.dart
new file mode 100644
index 0000000..cae624a
--- /dev/null
+++ b/test/html_import_annotation_test.dart
@@ -0,0 +1,28 @@
+// 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.
+@HtmlImport(importPath)
+// This one will throw a build time warning, but should still work dynamically.
+@HtmlImport('bad_import.html')
+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';
+
+const String importPath = 'my_import.html';
+
+main() {
+  useHtmlConfiguration();
+
+  test('adds import to head', () {
+    return init.run().then((_) {
+      var good = document.head.querySelector('link[href="$importPath"]');
+      expect(good.import.body.text, 'Hello world!\n');
+      var bad = document.head.querySelector('link[href="bad_import.html"]');
+      expect(bad.import, isNull);
+    });
+  });
+}
diff --git a/test/html_import_annotation_test.html b/test/html_import_annotation_test.html
new file mode 100644
index 0000000..a3455c5
--- /dev/null
+++ b/test/html_import_annotation_test.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="dart.unittest" content="full-stack-traces">
+  <title> custom_element_proxy 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>
+</head>
+<body>
+  <h1> Running html_import_annotation test </h1>
+
+  <script type="text/javascript"
+      src="/root_dart/tools/testing/dart/test_controller.js"></script>
+  <script type="application/dart" src="html_import_annotation_test.dart"></script>
+  <script type="text/javascript" src="/packages/browser/dart.js"></script>
+</body>
+</html>
diff --git a/test/my_import.html b/test/my_import.html
new file mode 100644
index 0000000..cd08755
--- /dev/null
+++ b/test/my_import.html
@@ -0,0 +1 @@
+Hello world!