diff --git a/modules/angular2/angular2.ts b/modules/angular2/angular2.ts index e3d0361346..4087b9eef6 100644 --- a/modules/angular2/angular2.ts +++ b/modules/angular2/angular2.ts @@ -5,4 +5,4 @@ export * from './platform/browser'; export * from './src/platform/dom/dom_adapter'; export * from './src/platform/dom/events/event_manager'; export * from './upgrade'; -export {UrlResolver, AppRootUrl} from './compiler'; +export {UrlResolver, AppRootUrl, getUrlScheme, DEFAULT_PACKAGE_URL_PROVIDER} from './compiler'; diff --git a/modules/angular2/core.ts b/modules/angular2/core.ts index 8f75db928f..25d31ebb2e 100644 --- a/modules/angular2/core.ts +++ b/modules/angular2/core.ts @@ -13,6 +13,7 @@ export { APP_ID, APP_COMPONENT, APP_INITIALIZER, + PACKAGE_ROOT_URL, PLATFORM_INITIALIZER } from './src/core/application_tokens'; export * from './src/core/zone'; @@ -29,4 +30,4 @@ export * from './src/core/change_detection'; export * from './src/core/platform_directives_and_pipes'; export * from './src/core/platform_common_providers'; export * from './src/core/application_common_providers'; -export * from './src/core/reflection/reflection'; \ No newline at end of file +export * from './src/core/reflection/reflection'; diff --git a/modules/angular2/src/compiler/compiler.ts b/modules/angular2/src/compiler/compiler.ts index fcbc6abf83..93e4c3e32c 100644 --- a/modules/angular2/src/compiler/compiler.ts +++ b/modules/angular2/src/compiler/compiler.ts @@ -24,7 +24,7 @@ import {Compiler} from 'angular2/src/core/linker/compiler'; import {RuntimeCompiler} from 'angular2/src/compiler/runtime_compiler'; import {ElementSchemaRegistry} from 'angular2/src/compiler/schema/element_schema_registry'; import {DomElementSchemaRegistry} from 'angular2/src/compiler/schema/dom_element_schema_registry'; -import {UrlResolver} from 'angular2/src/compiler/url_resolver'; +import {UrlResolver, DEFAULT_PACKAGE_URL_PROVIDER} from 'angular2/src/compiler/url_resolver'; import {AppRootUrl} from 'angular2/src/compiler/app_root_url'; import {AnchorBasedAppRootUrl} from 'angular2/src/compiler/anchor_based_app_root_url'; import {Parser, Lexer} from 'angular2/src/core/change_detection/change_detection'; @@ -40,6 +40,7 @@ export const COMPILER_PROVIDERS: Array = CONST_EXPR([ TemplateParser, TemplateNormalizer, RuntimeMetadataResolver, + DEFAULT_PACKAGE_URL_PROVIDER, StyleCompiler, CommandCompiler, ChangeDetectionCompiler, diff --git a/modules/angular2/src/compiler/runtime_metadata.ts b/modules/angular2/src/compiler/runtime_metadata.ts index 26e428a696..2f7951a553 100644 --- a/modules/angular2/src/compiler/runtime_metadata.ts +++ b/modules/angular2/src/compiler/runtime_metadata.ts @@ -19,6 +19,7 @@ import {reflector} from 'angular2/src/core/reflection/reflection'; import {Injectable, Inject, Optional} from 'angular2/src/core/di'; import {PLATFORM_DIRECTIVES} from 'angular2/src/core/platform_directives_and_pipes'; import {MODULE_SUFFIX} from './util'; +import {getUrlScheme} from 'angular2/src/compiler/url_resolver'; @Injectable() export class RuntimeMetadataResolver { @@ -107,8 +108,11 @@ function isValidDirective(value: Type): boolean { } function calcModuleUrl(type: Type, dirMeta: md.DirectiveMetadata): string { - if (isPresent(dirMeta.moduleId)) { - return `package:${dirMeta.moduleId}${MODULE_SUFFIX}`; + var moduleId = dirMeta.moduleId; + if (isPresent(moduleId)) { + var scheme = getUrlScheme(moduleId); + return isPresent(scheme) && scheme.length > 0 ? moduleId : + `package:${moduleId}${MODULE_SUFFIX}`; } else { return reflector.importUri(type); } diff --git a/modules/angular2/src/compiler/url_resolver.dart b/modules/angular2/src/compiler/url_resolver.dart index 2f9b4a4562..9af5236100 100644 --- a/modules/angular2/src/compiler/url_resolver.dart +++ b/modules/angular2/src/compiler/url_resolver.dart @@ -1,18 +1,22 @@ library angular2.src.services.url_resolver; -import 'package:angular2/src/core/di.dart' show Injectable, Provider; +import 'package:angular2/src/core/di.dart' show Injectable, Inject, Provider; +import 'package:angular2/src/facade/lang.dart' show isPresent, StringWrapper; +import 'package:angular2/src/core/application_tokens.dart' show PACKAGE_ROOT_URL; UrlResolver createWithoutPackagePrefix() { return new UrlResolver.withUrlPrefix(null); } +const DEFAULT_PACKAGE_URL_PROVIDER = const Provider(PACKAGE_ROOT_URL, useValue: "/packages"); + @Injectable() class UrlResolver { /// This will be the location where 'package:' Urls will resolve. Default is /// '/packages' final String _packagePrefix; - const UrlResolver() : _packagePrefix = '/packages'; + UrlResolver([@Inject(PACKAGE_ROOT_URL) this._packagePrefix]); /// Creates a UrlResolver that will resolve 'package:' Urls to a different /// prefixed location. @@ -32,15 +36,23 @@ class UrlResolver { */ String resolve(String baseUrl, String url) { Uri uri = Uri.parse(url); - if (!uri.isAbsolute) { + + if (isPresent(baseUrl) && baseUrl.length > 0) { Uri baseUri = Uri.parse(baseUrl); uri = baseUri.resolveUri(uri); } - if (_packagePrefix != null && uri.scheme == 'package') { - return '$_packagePrefix/${uri.path}'; + var prefix = this._packagePrefix; + if (prefix != null && uri.scheme == 'package') { + prefix = StringWrapper.stripRight(prefix, '/'); + var path = StringWrapper.stripLeft(uri.path, '/'); + return '$prefix/$path'; } else { return uri.toString(); } } } + +String getUrlScheme(String url) { + return Uri.parse(url).scheme; +} diff --git a/modules/angular2/src/compiler/url_resolver.ts b/modules/angular2/src/compiler/url_resolver.ts index 93cf9e5a66..7c914f5348 100644 --- a/modules/angular2/src/compiler/url_resolver.ts +++ b/modules/angular2/src/compiler/url_resolver.ts @@ -1,12 +1,21 @@ -import {Injectable} from 'angular2/src/core/di'; -import {isPresent, isBlank, RegExpWrapper, normalizeBlank} from 'angular2/src/facade/lang'; +import {Injectable, Inject} from 'angular2/src/core/di'; +import { + StringWrapper, + isPresent, + isBlank, + RegExpWrapper, + normalizeBlank +} from 'angular2/src/facade/lang'; import {BaseException, WrappedException} from 'angular2/src/facade/exceptions'; import {ListWrapper} from 'angular2/src/facade/collection'; +import {PACKAGE_ROOT_URL} from 'angular2/src/core/application_tokens'; +import {Provider} from 'angular2/src/core/di'; export function createWithoutPackagePrefix(): UrlResolver { return new UrlResolver(); } +export var DEFAULT_PACKAGE_URL_PROVIDER = new Provider(PACKAGE_ROOT_URL, {useValue: "/"}); /** * Used by the {@link Compiler} when resolving HTML and CSS template URLs. @@ -17,6 +26,14 @@ export function createWithoutPackagePrefix(): UrlResolver { */ @Injectable() export class UrlResolver { + private _packagePrefix: string; + + constructor(@Inject(PACKAGE_ROOT_URL) packagePrefix: string = null) { + if (isPresent(packagePrefix)) { + this._packagePrefix = StringWrapper.stripRight(packagePrefix, "/") + "/"; + } + } + /** * Resolves the `url` given the `baseUrl`: * - when the `url` is null, the `baseUrl` is returned, @@ -29,7 +46,21 @@ export class UrlResolver { * @param {string} url * @returns {string} the resolved URL */ - resolve(baseUrl: string, url: string): string { return _resolveUrl(baseUrl, url); } + resolve(baseUrl: string, url: string): string { + var resolvedUrl = url; + if (isPresent(baseUrl) && baseUrl.length > 0) { + resolvedUrl = _resolveUrl(baseUrl, resolvedUrl); + } + if (isPresent(this._packagePrefix) && getUrlScheme(resolvedUrl) == "package") { + resolvedUrl = resolvedUrl.replace("package:", this._packagePrefix); + } + return resolvedUrl; + } +} + +export function getUrlScheme(url: string): string { + var match = _split(url); + return (match && match[_ComponentIndex.Scheme]) || ""; } // The code below is adapted from Traceur: diff --git a/modules/angular2/src/core/application_tokens.ts b/modules/angular2/src/core/application_tokens.ts index 0a3780a44f..24cca2b943 100644 --- a/modules/angular2/src/core/application_tokens.ts +++ b/modules/angular2/src/core/application_tokens.ts @@ -58,4 +58,10 @@ export const PLATFORM_INITIALIZER: OpaqueToken = /** * A function that will be executed when an application is initialized. */ -export const APP_INITIALIZER: OpaqueToken = CONST_EXPR(new OpaqueToken("Application Initializer")); \ No newline at end of file +export const APP_INITIALIZER: OpaqueToken = CONST_EXPR(new OpaqueToken("Application Initializer")); + +/** + * A token which indicates the root directory of the application + */ +export const PACKAGE_ROOT_URL: OpaqueToken = + CONST_EXPR(new OpaqueToken("Application Packages Root URL")); diff --git a/modules/angular2/src/facade/lang.dart b/modules/angular2/src/facade/lang.dart index 3353bfddbd..26ed8ec7de 100644 --- a/modules/angular2/src/facade/lang.dart +++ b/modules/angular2/src/facade/lang.dart @@ -72,6 +72,30 @@ class StringWrapper { return s == s2; } + static String stripLeft(String s, String charVal) { + if (isPresent(s) && s.length > 0) { + var pos = 0; + for (var i = 0; i < s.length; i++) { + if (s[i] != charVal) break; + pos++; + } + s = s.substring(pos); + } + return s; + } + + static String stripRight(String s, String charVal) { + if (isPresent(s) && s.length > 0) { + var pos = s.length; + for (var i = s.length - 1; i >= 0; i--) { + if (s[i] != charVal) break; + pos--; + } + s = s.substring(0, pos); + } + return s; + } + static String replace(String s, Pattern from, String replace) { return s.replaceFirst(from, replace); } diff --git a/modules/angular2/src/facade/lang.ts b/modules/angular2/src/facade/lang.ts index 08609f43f8..bd4be8060d 100644 --- a/modules/angular2/src/facade/lang.ts +++ b/modules/angular2/src/facade/lang.ts @@ -166,6 +166,30 @@ export class StringWrapper { static equals(s: string, s2: string): boolean { return s === s2; } + static stripLeft(s: string, charVal: string): string { + if (s && s.length) { + var pos = 0; + for (var i = 0; i < s.length; i++) { + if (s[i] != charVal) break; + pos++; + } + s = s.substring(pos); + } + return s; + } + + static stripRight(s: string, charVal: string): string { + if (s && s.length) { + var pos = s.length; + for (var i = s.length - 1; i >= 0; i--) { + if (s[i] != charVal) break; + pos--; + } + s = s.substring(0, pos); + } + return s; + } + static replace(s: string, from: string, replace: string): string { return s.replace(from, replace); } diff --git a/modules/angular2/test/compiler/url_resolver_spec.ts b/modules/angular2/test/compiler/url_resolver_spec.ts index acaa5ae10c..21f4e13ac0 100644 --- a/modules/angular2/test/compiler/url_resolver_spec.ts +++ b/modules/angular2/test/compiler/url_resolver_spec.ts @@ -1,4 +1,15 @@ -import {describe, it, expect, beforeEach, ddescribe, iit, xit, el} from 'angular2/testing_internal'; +import { + describe, + it, + expect, + beforeEach, + ddescribe, + iit, + xit, + el, + inject +} from 'angular2/testing_internal'; +import {IS_DART} from 'angular2/src/facade/lang'; import {UrlResolver} from 'angular2/src/compiler/url_resolver'; export function main() { @@ -69,14 +80,50 @@ export function main() { expect(resolver.resolve('foo/baz', '/bar')).toEqual('/bar'); expect(resolver.resolve('foo/baz/', '/bar')).toEqual('/bar'); }); - }); - describe('corner and error cases', () => { - it('should encode URLs before resolving', () => { - expect(resolver.resolve('foo/baz', `

Hello -

`)) - .toEqual('foo/%3Cp%20#p%3EHello%0A%20%20%20%20%20%20%20%20%3C/p%3E'); + it('should not resolve urls against the baseUrl when the url contains a scheme', () => { + resolver = new UrlResolver('my_packages_dir'); + expect(resolver.resolve("base/", 'package:file')).toEqual('my_packages_dir/file'); + expect(resolver.resolve("base/", 'http:super_file')).toEqual('http:super_file'); + expect(resolver.resolve("base/", './mega_file')).toEqual('base/mega_file'); }); }); + + describe('packages', + () => { + it('should resolve a url based on the application package', () => { + resolver = new UrlResolver('my_packages_dir'); + expect(resolver.resolve(null, 'package:some/dir/file.txt')) + .toEqual('my_packages_dir/some/dir/file.txt'); + expect(resolver.resolve(null, 'some/dir/file.txt')).toEqual('some/dir/file.txt'); + }); + + it('should contain a default value of "/packages" when nothing is provided for DART', + inject([UrlResolver], (resolver) => { + if (IS_DART) { + expect(resolver.resolve(null, 'package:file')).toEqual('/packages/file'); + } + })); + + it('should contain a default value of "/" when nothing is provided for TS/ESM', + inject([UrlResolver], (resolver) => { + if (!IS_DART) { + expect(resolver.resolve(null, 'package:file')).toEqual('/file'); + } + })); + + it('should resolve a package value when present within the baseurl', () => { + resolver = new UrlResolver('/my_special_dir'); + expect(resolver.resolve('package:some_dir/', 'matias.html')) + .toEqual('/my_special_dir/some_dir/matias.html'); + }); + }) + + describe('corner and error cases', () => { + it('should encode URLs before resolving', () => { + expect(resolver.resolve('foo/baz', `

Hello +

`)).toEqual('foo/%3Cp%20#p%3EHello%0A%20%20%20%20%20%20%20%20%3C/p%3E'); + }); + }); }); } diff --git a/modules/angular2/test/facade/lang_spec.ts b/modules/angular2/test/facade/lang_spec.ts index 70295fff5c..ceef4e2a6d 100644 --- a/modules/angular2/test/facade/lang_spec.ts +++ b/modules/angular2/test/facade/lang_spec.ts @@ -65,5 +65,59 @@ export function main() { }); }); + describe('stripLeft', () => { + it('should strip the first character of the string if it matches the provided input', () => { + var input = "~angular2 is amazing"; + var expectedOutput = "angular2 is amazing"; + + expect(StringWrapper.stripLeft(input, "~")).toEqual(expectedOutput); + }); + + it('should keep stripping characters from the start until the first unmatched character', + () => { + var input = "#####hello"; + var expectedOutput = "hello"; + expect(StringWrapper.stripLeft(input, "#")).toEqual(expectedOutput); + }); + + it('should not alter the provided input if the first charater does not match the provided input', + () => { + var input = "+angular2 is amazing"; + expect(StringWrapper.stripLeft(input, "*")).toEqual(input); + }); + + it('should not do any alterations when an empty string or null value is passed in', () => { + expect(StringWrapper.stripLeft("", "S")).toEqual(""); + expect(StringWrapper.stripLeft(null, "S")).toEqual(null); + }); + }); + + describe('stripRight', () => { + it('should strip the first character of the string if it matches the provided input', () => { + var input = "angular2 is amazing!"; + var expectedOutput = "angular2 is amazing"; + + expect(StringWrapper.stripRight(input, "!")).toEqual(expectedOutput); + }); + + it('should not alter the provided input if the first charater does not match the provided input', + () => { + var input = "angular2 is amazing+"; + + expect(StringWrapper.stripRight(input, "*")).toEqual(input); + }); + + it('should keep stripping characters from the end until the first unmatched character', + () => { + var input = "hi&!&&&&&"; + var expectedOutput = "hi&!"; + expect(StringWrapper.stripRight(input, "&")).toEqual(expectedOutput); + }); + + it('should not do any alterations when an empty string or null value is passed in', () => { + expect(StringWrapper.stripRight("", "S")).toEqual(""); + expect(StringWrapper.stripRight(null, "S")).toEqual(null); + }); + }); }); } diff --git a/modules/angular2/test/public_api_spec.ts b/modules/angular2/test/public_api_spec.ts index 7aba158065..6dab9fb13c 100644 --- a/modules/angular2/test/public_api_spec.ts +++ b/modules/angular2/test/public_api_spec.ts @@ -1196,6 +1196,9 @@ var NG_ALL = [ 'UpperCasePipe.transform()', 'UrlResolver', 'UrlResolver.resolve()', + 'getUrlScheme()', + 'PACKAGE_ROOT_URL', + 'DEFAULT_PACKAGE_URL_PROVIDER', 'Validators#compose()', 'Validators#composeAsync()', 'Validators#nullValidator()', diff --git a/modules/playground/e2e_test/relative_assets/assets_spec.dart b/modules/playground/e2e_test/relative_assets/assets_spec.dart new file mode 100644 index 0000000000..200a722456 --- /dev/null +++ b/modules/playground/e2e_test/relative_assets/assets_spec.dart @@ -0,0 +1,3 @@ +library playground.e2e_test.relative_assets.assets_spec; + +main() {} diff --git a/modules/playground/e2e_test/relative_assets/assets_spec.ts b/modules/playground/e2e_test/relative_assets/assets_spec.ts new file mode 100644 index 0000000000..69e815807d --- /dev/null +++ b/modules/playground/e2e_test/relative_assets/assets_spec.ts @@ -0,0 +1,34 @@ +import {verifyNoBrowserErrors} from 'angular2/src/testing/e2e_util'; +import {Promise} from 'angular2/src/facade/async'; + +function waitForElement(selector) { + var EC = (protractor).ExpectedConditions; + // Waits for the element with id 'abc' to be present on the dom. + browser.wait(EC.presenceOf($(selector)), 20000); +} + +describe('relative assets relative-app', () => { + + afterEach(verifyNoBrowserErrors); + + var URL = 'playground/src/relative_assets/'; + + it('should load in the templateUrl relative to the my-cmp component', () => { + browser.get(URL); + + waitForElement('my-cmp .inner-container'); + expect(element.all(by.css('my-cmp .inner-container')).count()).toEqual(1); + }); + + it('should load in the styleUrls relative to the my-cmp component', () => { + browser.get(URL); + + waitForElement('my-cmp .inner-container'); + var elem = element(by.css('my-cmp .inner-container')); + var width = browser.executeScript(function(e) { + return parseInt(window.getComputedStyle(e).width); + }, elem.getWebElement()); + + expect(width).toBe(432); + }); +}); diff --git a/modules/playground/pubspec.yaml b/modules/playground/pubspec.yaml index 35538a25aa..907f00cf0b 100644 --- a/modules/playground/pubspec.yaml +++ b/modules/playground/pubspec.yaml @@ -31,6 +31,7 @@ transformers: - web/src/routing/index.dart - web/src/template_driven_forms/index.dart - web/src/zippy_component/index.dart + - web/src/relative_assets/index.dart - web/src/svg/index.dart - web/src/material/button/index.dart - web/src/material/checkbox/index.dart diff --git a/modules/playground/src/relative_assets/index.html b/modules/playground/src/relative_assets/index.html new file mode 100644 index 0000000000..0420c19883 --- /dev/null +++ b/modules/playground/src/relative_assets/index.html @@ -0,0 +1,11 @@ + + + Relative URLs + + + Loading... + + + $SCRIPTS$ + + diff --git a/modules/playground/src/relative_assets/index.ts b/modules/playground/src/relative_assets/index.ts new file mode 100644 index 0000000000..0df4d5f707 --- /dev/null +++ b/modules/playground/src/relative_assets/index.ts @@ -0,0 +1,19 @@ +import {bootstrap} from 'angular2/bootstrap'; +import {reflector} from 'angular2/src/core/reflection/reflection'; +import {ReflectionCapabilities} from 'angular2/src/core/reflection/reflection_capabilities'; + +import {Renderer, ElementRef, Component, Directive, Injectable} from 'angular2/core'; +import {MyCmp} from './my_cmp/my_cmp'; + +export function main() { + reflector.reflectionCapabilities = new ReflectionCapabilities(); + bootstrap(RelativeApp); +} + +@Component({ + selector: 'relative-app', + directives: [MyCmp], + template: `component = `, +}) +export class RelativeApp { +} diff --git a/modules/playground/src/relative_assets/my_cmp/my_cmp.dart b/modules/playground/src/relative_assets/my_cmp/my_cmp.dart new file mode 100644 index 0000000000..2f097936e3 --- /dev/null +++ b/modules/playground/src/relative_assets/my_cmp/my_cmp.dart @@ -0,0 +1,9 @@ +import 'package:angular2/core.dart' show Component; + +@Component( + selector: 'my-cmp', + templateUrl: 'tpl.html', + styleUrls: const ['style.css'] +) +class MyCmp { +} diff --git a/modules/playground/src/relative_assets/my_cmp/my_cmp.ts b/modules/playground/src/relative_assets/my_cmp/my_cmp.ts new file mode 100644 index 0000000000..34fb1d8389 --- /dev/null +++ b/modules/playground/src/relative_assets/my_cmp/my_cmp.ts @@ -0,0 +1,5 @@ +import {Component} from 'angular2/core'; +@Component( + {selector: 'my-cmp', templateUrl: 'tpl.html', styleUrls: ['style.css'], moduleId: module.id}) +export class MyCmp { +} diff --git a/modules/playground/src/relative_assets/my_cmp/style.css b/modules/playground/src/relative_assets/my_cmp/style.css new file mode 100644 index 0000000000..ed72d5deab --- /dev/null +++ b/modules/playground/src/relative_assets/my_cmp/style.css @@ -0,0 +1,12 @@ +:host { + background:red; + padding:1em; + display:block; + font-size:20px; + color:white; +} + +.inner-container { + width:432px; + font-weight:bold; +} diff --git a/modules/playground/src/relative_assets/my_cmp/tpl.html b/modules/playground/src/relative_assets/my_cmp/tpl.html new file mode 100644 index 0000000000..94da65d56d --- /dev/null +++ b/modules/playground/src/relative_assets/my_cmp/tpl.html @@ -0,0 +1,3 @@ +
+ my component content goes here +
diff --git a/tools/broccoli/trees/browser_tree.ts b/tools/broccoli/trees/browser_tree.ts index 443808e459..3a2f030725 100644 --- a/tools/broccoli/trees/browser_tree.ts +++ b/tools/broccoli/trees/browser_tree.ts @@ -49,6 +49,7 @@ const kServedPaths = [ 'playground/src/http', 'playground/src/jsonp', 'playground/src/key_events', + 'playground/src/relative_assets', 'playground/src/routing', 'playground/src/sourcemap', 'playground/src/svg', diff --git a/tools/broccoli/trees/dart_tree.ts b/tools/broccoli/trees/dart_tree.ts index ca25f37423..06a713fc70 100644 --- a/tools/broccoli/trees/dart_tree.ts +++ b/tools/broccoli/trees/dart_tree.ts @@ -68,8 +68,8 @@ function getSourceTree() { translateBuiltins: true, }); // Native sources, dart only examples, etc. - var dartSrcs = - modulesFunnel(['**/*.dart', '**/*.ng_meta.json', '**/*.aliases.json', '**/css/**']); + var dartSrcs = modulesFunnel( + ['**/*.dart', '**/*.ng_meta.json', '**/*.aliases.json', '**/css/**', '**/*.css']); return mergeTrees([transpiled, dartSrcs]); }