From 841f8789fd40f8549a3ae1aa1fed2fec01dd33dc Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 29 Sep 2015 17:27:44 -0700 Subject: [PATCH] refactor(transformer): precompile stylesheets Part of #3605 --- gulpfile.js | 32 +- modules/angular2/pubspec.yaml | 1 + .../src/core/dom/abstract_html_adapter.dart | 435 ++++++++++++++++++ .../angular2/src/core/dom/emulated_css.dart | 102 ++++ .../shadow_css_html5lib.server.spec.dart | 11 + .../render/dom/compiler/shadow_css_spec.ts | 11 +- .../stylesheet_compiler/processor.dart | 37 ++ .../stylesheet_compiler/transformer.dart | 40 ++ modules_dart/transform/pubspec.yaml | 23 + .../stylesheet_compiler/all_tests.dart | 96 ++++ 10 files changed, 771 insertions(+), 17 deletions(-) create mode 100644 modules/angular2/src/core/dom/abstract_html_adapter.dart create mode 100644 modules/angular2/src/core/dom/emulated_css.dart create mode 100644 modules/angular2/test/core/render/dom/compiler/shadow_css_html5lib.server.spec.dart create mode 100644 modules_dart/transform/lib/src/transform/stylesheet_compiler/processor.dart create mode 100644 modules_dart/transform/lib/src/transform/stylesheet_compiler/transformer.dart create mode 100644 modules_dart/transform/pubspec.yaml create mode 100644 modules_dart/transform/test/transform/stylesheet_compiler/all_tests.dart diff --git a/gulpfile.js b/gulpfile.js index 71766e7486..298c9621da 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -744,24 +744,31 @@ gulp.task('test.unit.cjs', ['build/clean.js', 'build.tools'], function (neverDon watch('modules/**', buildAndTest); }); - +// Use this target to continuously run dartvm unit-tests (such as transformer +// tests) while coding. Note: these tests do not use Karma. gulp.task('test.unit.dartvm', function (done) { runSequence( 'build/tree.dart', 'build/pure-packages.dart', - 'build/pubspec.dart', + '!build/pubget.angular2.dart', '!build/change_detect.dart', '!test.unit.dartvm/run', function(error) { - // if initial build failed (likely due to build or formatting step) then exit - // otherwise karma server doesn't start and we can't continue running properly - if (error) { - done(error); - return; - } - + // Watch for changes made in the TS and Dart code under "modules" and + // run ts2dart and test change detector generator prior to rerunning the + // tests. watch('modules/angular2/**', { ignoreInitial: true }, [ '!build/tree.dart', + '!build/change_detect.dart', + '!test.unit.dartvm/run' + ]); + + // Watch for changes made in Dart code under "modules_dart", then copy it + // to dist and run test change detector generator prior to retunning the + // tests. + watch('modules_dart/**', { ignoreInitial: true }, [ + 'build/pure-packages.dart', + '!build/change_detect.dart', '!test.unit.dartvm/run' ]); } @@ -863,14 +870,17 @@ gulp.task('build/pure-packages.dart', function() { var transformStream = gulp .src([ 'modules_dart/transform/**/*', - '!modules_dart/transform/**/*.proto' + '!modules_dart/transform/**/*.proto', + '!modules_dart/transform/pubspec.yaml', + '!modules_dart/transform/**/packages{,/**}', ]) .pipe(gulp.dest(path.join(CONFIG.dest.dart, 'angular2'))); var moveStream = gulp.src([ 'modules_dart/**/*.dart', 'modules_dart/**/pubspec.yaml', - '!modules_dart/transform/**' + '!modules_dart/transform/**', + '!modules_dart/**/packages{,/**}' ]) .pipe(through2.obj(function(file, enc, done) { if (/pubspec.yaml$/.test(file.path)) { diff --git a/modules/angular2/pubspec.yaml b/modules/angular2/pubspec.yaml index cbbb49b315..db1f18e131 100644 --- a/modules/angular2/pubspec.yaml +++ b/modules/angular2/pubspec.yaml @@ -11,6 +11,7 @@ environment: dependencies: analyzer: '>=0.24.4 <0.27.0' barback: '^0.15.2+2' + csslib: '>=0.12.0 <1.0.0' code_transformers: '^0.2.8' dart_style: '>=0.1.8 <0.3.0' glob: '^1.0.0' diff --git a/modules/angular2/src/core/dom/abstract_html_adapter.dart b/modules/angular2/src/core/dom/abstract_html_adapter.dart new file mode 100644 index 0000000000..09d57b0f0d --- /dev/null +++ b/modules/angular2/src/core/dom/abstract_html_adapter.dart @@ -0,0 +1,435 @@ +library angular2.dom.abstractHtmlAdapter; + +import 'package:html/parser.dart' as parser; +import 'package:html/dom.dart'; + +import 'dom_adapter.dart'; +import 'emulated_css.dart'; + +abstract class AbstractHtml5LibAdapter implements DomAdapter { + hasProperty(element, String name) { + // This is needed for serverside compile to generate the right getters/setters. + // TODO: change this once we have property schema support. + // Attention: Keep this in sync with browser_adapter.dart! + return true; + } + + void setProperty(Element element, String name, Object value) => + throw 'not implemented'; + + getProperty(Element element, String name) => throw 'not implemented'; + + invoke(Element element, String methodName, List args) => + throw 'not implemented'; + + @override + final attrToPropMap = const { + 'innerHtml': 'innerHTML', + 'readonly': 'readOnly', + 'tabindex': 'tabIndex', + }; + + set attrToPropMap(value) { + throw 'readonly'; + } + + @override + getGlobalEventTarget(String target) { + throw 'not implemented'; + } + + @override + getTitle() { + throw 'not implemented'; + } + + @override + setTitle(String newTitle) { + throw 'not implemented'; + } + + @override + String getEventKey(event) { + throw 'not implemented'; + } + + @override + void replaceChild(el, newNode, oldNode) { + throw 'not implemented'; + } + + @override + dynamic getBoundingClientRect(el) { + throw 'not implemented'; + } + + Element parse(String templateHtml) => parser.parse(templateHtml).firstChild; + query(selector) { + throw 'not implemented'; + } + + querySelector(el, String selector) { + return el.querySelector(selector); + } + + List querySelectorAll(el, String selector) { + return el.querySelectorAll(selector); + } + + on(el, evt, listener) { + throw 'not implemented'; + } + + Function onAndCancel(el, evt, listener) { + throw 'not implemented'; + } + + dispatchEvent(el, evt) { + throw 'not implemented'; + } + + createMouseEvent(eventType) { + throw 'not implemented'; + } + + createEvent(eventType) { + throw 'not implemented'; + } + + preventDefault(evt) { + throw 'not implemented'; + } + + isPrevented(evt) { + throw 'not implemented'; + } + + getInnerHTML(el) { + return el.innerHtml; + } + + getOuterHTML(el) { + return el.outerHtml; + } + + String nodeName(node) { + switch (node.nodeType) { + case Node.ELEMENT_NODE: + return (node as Element).localName; + case Node.TEXT_NODE: + return '#text'; + default: + throw 'not implemented for type ${node.nodeType}. ' + 'See http://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-1950641247' + ' for node types definitions.'; + } + } + + String nodeValue(node) => node.data; + String type(node) { + throw 'not implemented'; + } + + content(node) { + return node; + } + + firstChild(el) => el is NodeList ? el.first : el.firstChild; + + nextSibling(el) { + final parentNode = el.parentNode; + if (parentNode == null) return null; + final siblings = parentNode.nodes; + final index = siblings.indexOf(el); + if (index < siblings.length - 1) { + return siblings[index + 1]; + } + return null; + } + + parentElement(el) { + return el.parent; + } + + List childNodes(el) => el.nodes; + List childNodesAsList(el) => el.nodes; + clearNodes(el) { + el.nodes.forEach((e) => e.remove()); + } + + appendChild(el, node) => el.append(node.remove()); + removeChild(el, node) { + throw 'not implemented'; + } + + remove(el) => el.remove(); + insertBefore(el, node) { + if (el.parent == null) throw '$el must have a parent'; + el.parent.insertBefore(node, el); + } + + insertAllBefore(el, nodes) { + throw 'not implemented'; + } + + insertAfter(el, node) { + throw 'not implemented'; + } + + setInnerHTML(el, value) { + el.innerHtml = value; + } + + getText(el) { + return el.text; + } + + setText(el, String value) => el.text = value; + + getValue(el) { + throw 'not implemented'; + } + + setValue(el, String value) { + throw 'not implemented'; + } + + getChecked(el) { + throw 'not implemented'; + } + + setChecked(el, bool value) { + throw 'not implemented'; + } + + createComment(String text) => new Comment(text); + createTemplate(String html) => createElement('template')..innerHtml = html; + createElement(tagName, [doc]) { + return new Element.tag(tagName); + } + + createTextNode(String text, [doc]) => new Text(text); + + createScriptTag(String attrName, String attrValue, [doc]) { + throw 'not implemented'; + } + + createStyleElement(String css, [doc]) { + throw 'not implemented'; + } + + createShadowRoot(el) { + throw 'not implemented'; + } + + getShadowRoot(el) { + throw 'not implemented'; + } + + getHost(el) { + throw 'not implemented'; + } + + clone(node) => node.clone(true); + getElementsByClassName(element, String name) { + throw 'not implemented'; + } + + getElementsByTagName(element, String name) { + throw 'not implemented'; + } + + List classList(element) => element.classes.toList(); + + addClass(element, String classname) { + element.classes.add(classname); + } + + removeClass(element, String classname) { + throw 'not implemented'; + } + + hasClass(element, String classname) => element.classes.contains(classname); + + setStyle(element, String stylename, String stylevalue) { + throw 'not implemented'; + } + + removeStyle(element, String stylename) { + throw 'not implemented'; + } + + getStyle(element, String stylename) { + throw 'not implemented'; + } + + String tagName(element) => element.localName; + + attributeMap(element) { + // `attributes` keys can be {@link AttributeName}s. + var map = {}; + element.attributes.forEach((key, value) { + map['$key'] = value; + }); + return map; + } + + hasAttribute(element, String attribute) { + // `attributes` keys can be {@link AttributeName}s. + return element.attributes.keys.any((key) => '$key' == attribute); + } + + getAttribute(element, String attribute) { + // `attributes` keys can be {@link AttributeName}s. + var key = element.attributes.keys.firstWhere((key) => '$key' == attribute, + orElse: () {}); + return element.attributes[key]; + } + + setAttribute(element, String name, String value) { + element.attributes[name] = value; + } + + removeAttribute(element, String attribute) { + element.attributes.remove(attribute); + } + + templateAwareRoot(el) => el; + + createHtmlDocument() { + throw 'not implemented'; + } + + defaultDoc() { + throw 'not implemented'; + } + + bool elementMatches(n, String selector) { + throw 'not implemented'; + } + + bool isTemplateElement(Element el) { + return el != null && el.localName.toLowerCase() == 'template'; + } + + bool isTextNode(node) => node.nodeType == Node.TEXT_NODE; + bool isCommentNode(node) => node.nodeType == Node.COMMENT_NODE; + + bool isElementNode(node) => node.nodeType == Node.ELEMENT_NODE; + + bool hasShadowRoot(node) { + throw 'not implemented'; + } + + bool isShadowRoot(node) { + throw 'not implemented'; + } + + importIntoDoc(node) { + throw 'not implemented'; + } + + adoptNode(node) { + throw 'not implemented'; + } + + bool isPageRule(rule) => (rule.type == 6); + + bool isStyleRule(rule) => (rule.type == 1); + + bool isMediaRule(rule) => (rule.type == 4); + + bool isKeyframesRule(rule) => (rule.type == 7); + + String getHref(element) { + throw 'not implemented'; + } + + void resolveAndSetHref(element, baseUrl, href) { + throw 'not implemented'; + } + + List cssToRules(String css) { + return parseAndEmulateCssRules(css); + } + + List getDistributedNodes(Node) { + throw 'not implemented'; + } + + bool supportsDOMEvents() { + return false; + } + + bool supportsNativeShadowDOM() { + return false; + } + + bool supportsUnprefixedCssAnimation() { + // Currently during code transformation we do not know what + // browsers we are targetting. To play it safe, we assume + // unprefixed animations are not supported. + return false; + } + + getHistory() { + throw 'not implemented'; + } + + getLocation() { + throw 'not implemented'; + } + + getBaseHref() { + throw 'not implemented'; + } + + resetBaseElement() { + throw 'not implemented'; + } + + String getUserAgent() { + return 'Angular 2 Dart Transformer'; + } + + void setData(Element element, String name, String value) { + this.setAttribute(element, 'data-${name}', value); + } + + getComputedStyle(element) { + throw 'not implemented'; + } + + String getData(Element element, String name) { + return this.getAttribute(element, 'data-${name}'); + } + + // TODO(tbosch): move this into a separate environment class once we have it + setGlobalVar(String name, value) { + // noop on the server + } + + requestAnimationFrame(callback) { + throw 'not implemented'; + } + + cancelAnimationFrame(id) { + throw 'not implemented'; + } + + performanceNow() { + throw 'not implemented'; + } + + getAnimationPrefix() { + throw 'not implemented'; + } + + getTransitionEnd() { + throw 'not implemented'; + } + + supportsAnimation() { + throw 'not implemented'; + } +} diff --git a/modules/angular2/src/core/dom/emulated_css.dart b/modules/angular2/src/core/dom/emulated_css.dart new file mode 100644 index 0000000000..e5ca210d3d --- /dev/null +++ b/modules/angular2/src/core/dom/emulated_css.dart @@ -0,0 +1,102 @@ +/** + * Emulates browser CSS API. + * + * WARNING: this is a very incomplete emulation; it only has enough to support + * Angular's CSS scoping (a.k.a. shimming). + */ +library angular2.dom.emulated_css; + +import 'package:csslib/parser.dart' as cssp; +import 'package:csslib/visitor.dart' as cssv; + +/// Parses [css] string and emits the list of top-level CSS rules in it via +/// data structures that mimick browser CSS APIs. +List parseAndEmulateCssRules(String css) { + var stylesheet = cssp.parse(css); + return emulateRules(stylesheet.topLevels); +} + +/// Converts `csslib` [rules] to their emulated counterparts. +List emulateRules(Iterable rules) { + return rules + .map((cssv.TreeNode node) { + if (node is cssv.RuleSet) { + if (node.declarationGroup.span.text.isEmpty) { + // Skip CSS matchers with no bodies + return null; + } + return new EmulatedCssStyleRule(node); + } else if (node is cssv.MediaDirective) { + return new EmulatedCssMedialRule(node); + } + }) + .where((r) => r != null) + .toList(); +} + +/// Emulates [CSSRule](https://developer.mozilla.org/en-US/docs/Web/API/CSSRule) +abstract class EmulatedCssRule { + int type; + String cssText; +} + +/// Emulates [CSSStyleRule](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleRule) +class EmulatedCssStyleRule extends EmulatedCssRule { + String selectorText; + EmulatedCssStyleDeclaration style; + + EmulatedCssStyleRule(cssv.RuleSet ruleSet) { + final declarationText = new StringBuffer(); + ruleSet.declarationGroup.declarations.forEach((d) { + if (d is! cssv.Declaration) { + // Nested selectors not supported + return; + } + // TODO: expression spans are currently broken in csslib; see: + // https://github.com/dart-lang/csslib/pull/14 + var declarationSpan = d.span.text; + var colonIdx = declarationSpan.indexOf(':'); + var expression = declarationSpan.substring(colonIdx + 1); + declarationText.write('${d.property}: ${expression};'); + }); + + final style = new EmulatedCssStyleDeclaration() + ..cssText = declarationText.toString(); + + this + ..type = 1 + ..cssText = ruleSet.span.text + ..selectorText = ruleSet.selectorGroup.span.text + ..style = style; + } +} + +/// Emulates [CSSStyleDeclaration](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration) +class EmulatedCssStyleDeclaration { + final String content = ''; + String cssText; +} + +/// Emulates [CSSMediaRule](https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule) +class EmulatedCssMedialRule extends EmulatedCssRule { + List cssRules; + EmulatedMediaList media; + + EmulatedCssMedialRule(cssv.MediaDirective directive) { + this + ..type = 4 + ..media = new EmulatedMediaList(directive) + ..cssText = directive.span.text + ..cssRules = emulateRules(directive.rulesets); + } +} + +/// Emulates [MediaList](https://developer.mozilla.org/en-US/docs/Web/API/MediaList) +class EmulatedMediaList { + String mediaText; + + EmulatedMediaList(cssv.MediaDirective directive) { + this.mediaText = directive.mediaQueries + .map((q) => q.span.text).join(' and '); + } +} diff --git a/modules/angular2/test/core/render/dom/compiler/shadow_css_html5lib.server.spec.dart b/modules/angular2/test/core/render/dom/compiler/shadow_css_html5lib.server.spec.dart new file mode 100644 index 0000000000..ab9b5dbe91 --- /dev/null +++ b/modules/angular2/test/core/render/dom/compiler/shadow_css_html5lib.server.spec.dart @@ -0,0 +1,11 @@ +library angular2.compiler.shadow_css_html5lib.test; + +import 'package:angular2/src/core/dom/html_adapter.dart'; +import 'package:angular2/src/test_lib/test_lib.dart' show testSetup; +import 'shadow_css_spec.dart' as shadow_css_spec_test; + +void main() { + Html5LibDomAdapter.makeCurrent(); + testSetup(); + shadow_css_spec_test.main(); +} diff --git a/modules/angular2/test/core/render/dom/compiler/shadow_css_spec.ts b/modules/angular2/test/core/render/dom/compiler/shadow_css_spec.ts index 41665dc297..10f2d99ed1 100644 --- a/modules/angular2/test/core/render/dom/compiler/shadow_css_spec.ts +++ b/modules/angular2/test/core/render/dom/compiler/shadow_css_spec.ts @@ -33,7 +33,7 @@ export function main() { expect(s(css, 'a')).toEqual(expected); }); - it('should hanlde invalid css', () => { + it('should handle invalid css', () => { var css = 'one {color: red;}garbage'; var expected = 'one[a] {color:red;}'; expect(s(css, 'a')).toEqual(expected); @@ -58,8 +58,7 @@ export function main() { }); // Check that the browser supports unprefixed CSS animation - if (isPresent(DOM.defaultDoc().body.style) && - isPresent(DOM.defaultDoc().body.style.animationName)) { + if (DOM.supportsUnprefixedCssAnimation()) { it('should handle keyframes rules', () => { var css = '@keyframes foo {0% {transform: translate(-50%) scaleX(0);}}'; var passRe = @@ -80,9 +79,9 @@ export function main() { it('should handle complicated selectors', () => { expect(s('one::before {}', 'a')).toEqual('one[a]::before {}'); expect(s('one two {}', 'a')).toEqual('one[a] two[a] {}'); - expect(s('one>two {}', 'a')).toEqual('one[a] > two[a] {}'); - expect(s('one+two {}', 'a')).toEqual('one[a] + two[a] {}'); - expect(s('one~two {}', 'a')).toEqual('one[a] ~ two[a] {}'); + expect(s('one > two {}', 'a')).toEqual('one[a] > two[a] {}'); + expect(s('one + two {}', 'a')).toEqual('one[a] + two[a] {}'); + expect(s('one ~ two {}', 'a')).toEqual('one[a] ~ two[a] {}'); var res = s('.one.two > three {}', 'a'); // IE swap classes expect(res == '.one.two[a] > three[a] {}' || res == '.two.one[a] > three[a] {}') .toEqual(true); diff --git a/modules_dart/transform/lib/src/transform/stylesheet_compiler/processor.dart b/modules_dart/transform/lib/src/transform/stylesheet_compiler/processor.dart new file mode 100644 index 0000000000..8b3c7445ee --- /dev/null +++ b/modules_dart/transform/lib/src/transform/stylesheet_compiler/processor.dart @@ -0,0 +1,37 @@ +library angular2.transform.stylesheet_compiler.processor; + +import 'dart:async'; + +import 'package:angular2/src/transform/common/asset_reader.dart'; +import 'package:angular2/src/transform/common/code/source_module.dart'; +import 'package:angular2/src/transform/common/names.dart'; +import 'package:angular2/src/transform/common/ng_compiler.dart'; +import 'package:angular2/src/compiler/source_module.dart'; + +import 'package:barback/barback.dart'; + +AssetId shimmedStylesheetAssetId(AssetId cssAssetId) => new AssetId( + cssAssetId.package, toShimmedStylesheetExtension(cssAssetId.path)); + +AssetId nonShimmedStylesheetAssetId(AssetId cssAssetId) => new AssetId( + cssAssetId.package, toNonShimmedStylesheetExtension(cssAssetId.path)); + +Future> processStylesheet( + AssetReader reader, AssetId stylesheetId) async { + final stylesheetUrl = '${stylesheetId.package}|${stylesheetId.path}'; + final templateCompiler = createTemplateCompiler(reader); + final cssText = await reader.readAsString(stylesheetId); + final sourceModules = + templateCompiler.compileStylesheetCodeGen(stylesheetUrl, cssText); + + var libraryIdx = 0; + return sourceModules.map((SourceModule module) => new Asset.fromString( + new AssetId.parse('${module.moduleUrl}'), + writeSourceModule(module, + libraryName: '${_getLibBase(module.moduleUrl)}${libraryIdx++}'))); +} + +final _unsafeCharsPattern = new RegExp(r'[^a-zA-Z0-9_]'); +String _getLibBase(String libraryName) { + return libraryName.replaceAll('/', '.').replaceAll(_unsafeCharsPattern, '_'); +} diff --git a/modules_dart/transform/lib/src/transform/stylesheet_compiler/transformer.dart b/modules_dart/transform/lib/src/transform/stylesheet_compiler/transformer.dart new file mode 100644 index 0000000000..4ed0663e03 --- /dev/null +++ b/modules_dart/transform/lib/src/transform/stylesheet_compiler/transformer.dart @@ -0,0 +1,40 @@ +library angular2.transform.stylesheet_compiler.transformer; + +import 'dart:async'; + +import 'package:angular2/src/core/dom/html_adapter.dart'; +import 'package:angular2/src/transform/common/asset_reader.dart'; +import 'package:angular2/src/transform/common/logging.dart' as log; +import 'package:angular2/src/transform/common/names.dart'; + +import 'package:barback/barback.dart'; + +import 'processor.dart'; + +/// Pre-compiles CSS stylesheet files to Dart code for Angular 2. +class StylesheetCompiler extends Transformer implements DeclaringTransformer { + StylesheetCompiler(); + + @override + bool isPrimary(AssetId id) { + return id.path.endsWith(CSS_EXTENSION); + } + + @override + declareOutputs(DeclaringTransform transform) { + transform.declareOutput(nonShimmedStylesheetAssetId(transform.primaryId)); + transform.declareOutput(shimmedStylesheetAssetId(transform.primaryId)); + } + + @override + Future apply(Transform transform) async { + await log.initZoned(transform, () async { + Html5LibDomAdapter.makeCurrent(); + var reader = new AssetReader.fromTransform(transform); + var outputs = await processStylesheet(reader, transform.primaryInput.id); + outputs.forEach((Asset compiledStylesheet) { + transform.addOutput(compiledStylesheet); + }); + }); + } +} diff --git a/modules_dart/transform/pubspec.yaml b/modules_dart/transform/pubspec.yaml new file mode 100644 index 0000000000..66d9fe82e6 --- /dev/null +++ b/modules_dart/transform/pubspec.yaml @@ -0,0 +1,23 @@ +name: ng2_transform +version: 0.0.0 +environment: + sdk: '>=1.8.0 <2.0.0' +dependencies: + angular2: + path: ../../dist/dart/angular2 + analyzer: '>=0.24.4 <0.27.0' + barback: '^0.15.2+2' + code_transformers: '^0.2.8' + dart_style: '>=0.1.8 <0.3.0' + glob: '^1.0.0' + guinness: any + html: '^0.12.0' + intl: '^0.12.4' + logging: '>=0.9.0 <0.12.0' + observe: '^0.13.1' + protobuf: '^0.4.2' + quiver: '^0.21.4' + source_span: '^1.0.0' + stack_trace: '^1.1.1' +dev_dependencies: + test: '>=0.12.0 <0.13.0' diff --git a/modules_dart/transform/test/transform/stylesheet_compiler/all_tests.dart b/modules_dart/transform/test/transform/stylesheet_compiler/all_tests.dart new file mode 100644 index 0000000000..d9271f3caf --- /dev/null +++ b/modules_dart/transform/test/transform/stylesheet_compiler/all_tests.dart @@ -0,0 +1,96 @@ +library angular2.test.transform.stylesheet_compiler.all_tests; + +import 'dart:async'; +import 'dart:convert'; + +import 'package:angular2/src/transform/stylesheet_compiler/transformer.dart'; + +import 'package:barback/barback.dart'; +import 'package:guinness/guinness.dart'; + +const SIMPLE_CSS = ''' +.foo { + width: 10px; +} +'''; + +main() { + Html5LibDomAdapter.makeCurrent(); + allTests(); +} + +allTests() { + StylesheetCompiler subject; + + beforeEach(() { + subject = new StylesheetCompiler(); + }); + + it('should accept CSS assets', () { + expect(subject.isPrimary(new AssetId('somepackage', 'lib/style.css'))) + .toBe(true); + }); + + it('should reject non-CSS assets', () { + expect(subject.isPrimary(new AssetId('somepackage', 'lib/style.scss'))) + .toBe(false); + }); + + it('should declare outputs', () { + var transform = new FakeDeclaringTransform() + ..primaryId = new AssetId('somepackage', 'lib/style.css'); + subject.declareOutputs(transform); + expect(transform.outputs.length).toBe(2); + expect(transform.outputs[0].toString()) + .toEqual('somepackage|lib/style.css.dart'); + expect(transform.outputs[1].toString()) + .toEqual('somepackage|lib/style.css.shim.dart'); + }); + + it('should compile stylesheets', () async { + var cssFile = new Asset.fromString( + new AssetId('somepackage', 'lib/style.css'), SIMPLE_CSS); + var transform = new FakeTransform()..primaryInput = cssFile; + await subject.apply(transform); + expect(transform.outputs.length).toBe(2); + expect(transform.outputs[0].id.toString()) + .toEqual('somepackage|lib/style.css.dart'); + expect(transform.outputs[1].id.toString()) + .toEqual('somepackage|lib/style.css.shim.dart'); + }); +} + +@proxy +class FakeTransform implements Transform { + final outputs = []; + Asset primaryInput; + + addOutput(Asset output) { + this.outputs.add(output); + } + + readInputAsString(AssetId id, {Encoding encoding}) { + if (id == primaryInput.id) { + return primaryInput.readAsString(encoding: encoding); + } + throw 'Could not read input $id'; + } + + noSuchMethod(Invocation i) { + throw '${i.memberName} not implemented'; + } +} + +@proxy +class FakeDeclaringTransform implements DeclaringTransform { + final outputs = []; + AssetId primaryId; + + declareOutput(AssetId output) { + this.outputs.add(output); + } + + noSuchMethod(Invocation i) { + throw '${i.memberName} not implemented'; + } +}