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!