upstream: Merge remote-tracking branch 'upstream/master' into merge-10.1.3

# Conflicts:
#	.circleci/config.yml
#	.github/ISSUE_TEMPLATE/1-bug-report.md
#	.github/ISSUE_TEMPLATE/2-feature-request.md
#	.github/ISSUE_TEMPLATE/5-support-request.md
#	.github/ISSUE_TEMPLATE/6-angular-cli.md
#	.github/ISSUE_TEMPLATE/7-angular-components.md
#	.ng-dev/commit-message.ts
#	CODE_OF_CONDUCT.md
#	CONTRIBUTING.md
#	README.md
#	aio/README.md
#	aio/content/guide/architecture-modules.md
#	aio/content/guide/architecture-next-steps.md
#	aio/content/guide/architecture-services.md
#	aio/content/guide/architecture.md
#	aio/content/guide/attribute-binding.md
#	aio/content/guide/bootstrapping.md
#	aio/content/guide/glossary.md
#	aio/content/guide/ngmodules.md
#	aio/content/guide/template-statements.md
#	aio/content/marketing/analytics.md
#	aio/content/marketing/docs.md
#	aio/content/marketing/events.html
#	aio/content/navigation.json
#	aio/content/tutorial/toh-pt4.md
#	aio/content/tutorial/toh-pt6.md
#	aio/package.json
#	aio/src/app/shared/ga.service.spec.ts
#	aio/src/app/shared/ga.service.ts
#	aio/src/app/shared/location.service.spec.ts
#	aio/tests/e2e/src/onerror.e2e-spec.ts
#	aio/yarn.lock
This commit is contained in:
Michael Prentice
2020-10-22 11:28:49 -04:00
1611 changed files with 70249 additions and 31841 deletions

View File

@ -5,8 +5,7 @@ exports_files([
"tsconfig.json",
])
load("@npm_bazel_typescript//:index.bzl", "ts_config")
load("//tools:defaults.bzl", "ts_library")
load("//tools:defaults.bzl", "ts_config", "ts_library")
ts_library(
name = "types",

View File

@ -72,7 +72,7 @@ export class CssKeyframesDriver implements AnimationDriver {
keyframeStr += `}\n`;
const kfElm = document.createElement('style');
kfElm.innerHTML = keyframeStr;
kfElm.textContent = keyframeStr;
return kfElm;
}

View File

@ -106,7 +106,7 @@ describe('CssKeyframesDriver tests', () => {
it('should animate until the `animationend` method is emitted, but stil retain the <style> method and the element animation details',
fakeAsync(() => {
// IE10 and IE11 cannot create an instanceof AnimationEvent
// IE11 cannot create an instanceof AnimationEvent
if (!supportsAnimationEventCreation()) return;
const elm = createElement();

View File

@ -195,7 +195,7 @@ const EMPTY_FN = () => {};
});
it('should fire the onDone method when the matching animationend event is emitted', () => {
// IE10 and IE11 cannot create an instanceof AnimationEvent
// IE11 cannot create an instanceof AnimationEvent
if (!supportsAnimationEventCreation()) return;
const element = createElement();

View File

@ -1,3 +1,4 @@
# BEGIN-DEV-ONLY
load("//tools:defaults.bzl", "pkg_npm")
pkg_npm(
@ -17,9 +18,8 @@ pkg_npm(
],
substitutions = {
"(#|\/\/)\\s+BEGIN-DEV-ONLY[\\w\W]+?(#|\/\/)\\s+END-DEV-ONLY": "",
"//packages/bazel/src/ngc-wrapped": "@npm//@angular/bazel/bin:ngc-wrapped",
"//packages/bazel/": "//",
"angular/packages/bazel/": "npm_angular_bazel/",
"//packages/bazel/": "//@angular/bazel/",
"@npm//@bazel/typescript/internal:": "//@bazel/typescript/internal:",
},
tags = ["release-with-framework"],
# Do not add more to this list.
@ -31,3 +31,4 @@ pkg_npm(
"//packages/bazel/src/ngc-wrapped:ngc_lib",
],
)
# END-DEV-ONLY

View File

@ -29,11 +29,11 @@ def rules_angular_dev_dependencies():
_maybe(
http_archive,
name = "bazel_toolchains",
sha256 = "db48eed61552e25d36fe051a65d2a329cc0fb08442627e8f13960c5ab087a44e",
strip_prefix = "bazel-toolchains-3.2.0",
sha256 = "4fb3ceea08101ec41208e3df9e56ec72b69f3d11c56629d6477c0ff88d711cf7",
strip_prefix = "bazel-toolchains-3.6.0",
urls = [
"https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/releases/download/3.2.0/bazel-toolchains-3.2.0.tar.gz",
"https://github.com/bazelbuild/bazel-toolchains/releases/download/3.2.0/bazel-toolchains-3.2.0.tar.gz",
"https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/releases/download/3.6.0/bazel-toolchains-3.6.0.tar.gz",
"https://github.com/bazelbuild/bazel-toolchains/releases/download/3.6.0/bazel-toolchains-3.6.0.tar.gz",
],
)

View File

@ -19,12 +19,6 @@
}
}
},
"bazelWorkspaces": {
"npm_angular_bazel": {
"version": "0.0.0-PLACEHOLDER",
"rootPath": "."
}
},
"dependencies": {
"@microsoft/api-extractor": "^7.7.13",
"shelljs": "0.8.2",
@ -34,7 +28,7 @@
"@angular/compiler-cli": "0.0.0-PLACEHOLDER",
"@bazel/typescript": ">=1.0.0",
"terser": "^4.3.1",
"typescript": ">=3.9 <4.0",
"typescript": ">=4.0 <4.1",
"rollup": ">=1.20.0",
"rollup-plugin-commonjs": ">=9.0.0",
"rollup-plugin-node-resolve": ">=4.2.0",

View File

@ -1,3 +1,4 @@
# BEGIN-DEV-ONLY
package(default_visibility = ["//packages/bazel:__subpackages__"])
filegroup(
@ -22,3 +23,4 @@ nodejs_binary(
node_modules = ":empty_node_modules",
visibility = ["//visibility:public"],
)
# END-DEV-ONLY

View File

@ -1,9 +1,8 @@
# BEGIN-DEV-ONLY
package(default_visibility = ["//packages:__subpackages__"])
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
# BEGIN-DEV-ONLY
load("//tools:defaults.bzl", "ts_library")
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
ts_library(
name = "lib",
@ -17,7 +16,6 @@ ts_library(
],
)
# END-DEV-ONLY
nodejs_binary(
name = "api_extractor",
data = [
@ -33,3 +31,4 @@ filegroup(
name = "package_assets",
srcs = ["BUILD.bazel"],
)
# END-DEV-ONLY

View File

@ -2,11 +2,13 @@
"""
load(
"@npm_bazel_typescript//internal:build_defs.bzl",
# Replaced with "//@bazel/typescript/internal:..." in published package
"@npm//@bazel/typescript/internal:build_defs.bzl",
_tsc_wrapped_tsconfig = "tsc_wrapped_tsconfig",
)
load(
"@npm_bazel_typescript//internal:common/compilation.bzl",
# Replaced with "//@bazel/typescript/internal:..." in published package
"@npm//@bazel/typescript/internal:common/compilation.bzl",
_COMMON_ATTRIBUTES = "COMMON_ATTRIBUTES",
_COMMON_OUTPUTS = "COMMON_OUTPUTS",
_DEPS_ASPECTS = "DEPS_ASPECTS",
@ -14,7 +16,8 @@ load(
_ts_providers_dict_to_struct = "ts_providers_dict_to_struct",
)
load(
"@npm_bazel_typescript//internal:ts_config.bzl",
# Replaced with "//@bazel/typescript/internal:..." in published package
"@npm//@bazel/typescript/internal:ts_config.bzl",
_TsConfigInfo = "TsConfigInfo",
)
load(
@ -22,6 +25,7 @@ load(
_LinkablePackageInfo = "LinkablePackageInfo",
_NpmPackageInfo = "NpmPackageInfo",
_js_ecma_script_module_info = "js_ecma_script_module_info",
_js_module_info = "js_module_info",
_js_named_module_info = "js_named_module_info",
_node_modules_aspect = "node_modules_aspect",
)
@ -42,10 +46,26 @@ ts_providers_dict_to_struct = _ts_providers_dict_to_struct
# is loaded differently anyways where this file is overridden.
BuildSettingInfo = provider(doc = "Not used outside google3.")
DEFAULT_API_EXTRACTOR = "@npm//@angular/bazel/bin:api-extractor"
DEFAULT_NG_COMPILER = "@npm//@angular/bazel/bin:ngc-wrapped"
DEFAULT_NG_XI18N = "@npm//@angular/bazel/bin:xi18n"
DEFAULT_API_EXTRACTOR = (
# BEGIN-DEV-ONLY
"@npm" +
# END-DEV-ONLY
"//@angular/bazel/bin:api-extractor"
)
DEFAULT_NG_COMPILER = (
# BEGIN-DEV-ONLY
"@npm" +
# END-DEV-ONLY
"//@angular/bazel/bin:ngc-wrapped"
)
DEFAULT_NG_XI18N = (
# BEGIN-DEV-ONLY
"@npm" +
# END-DEV-ONLY
"//@angular/bazel/bin:xi18n"
)
FLAT_DTS_FILE_SUFFIX = ".bundle.d.ts"
TsConfigInfo = _TsConfigInfo
js_ecma_script_module_info = _js_ecma_script_module_info
js_module_info = _js_module_info
js_named_module_info = _js_named_module_info

View File

@ -19,6 +19,7 @@ load(
"TsConfigInfo",
"compile_ts",
"js_ecma_script_module_info",
"js_module_info",
"js_named_module_info",
"node_modules_aspect",
"ts_providers_dict_to_struct",
@ -47,14 +48,6 @@ def is_ivy_enabled(ctx):
ctx.attr._renderer[BuildSettingInfo].value == "ivy")):
return True
# TODO(josephperott): Remove after ~Feb 2020, to allow local script migrations
if "compile" in ctx.var and ctx.workspace_name == "angular":
fail(
msg = "Setting ViewEngine/Ivy using --define=compile is deprecated, please use " +
"--config=ivy or --config=view-engine instead.",
attr = "ng_module",
)
# This attribute is only defined in google's private ng_module rule and not
# available externally. For external users, this is effectively a no-op.
if hasattr(ctx.attr, "ivy") and ctx.attr.ivy == True:
@ -644,6 +637,10 @@ def _ng_module_impl(ctx):
# See design doc https://docs.google.com/document/d/1ggkY5RqUkVL4aQLYm7esRW978LgX3GUCnQirrk5E1C0/edit#
# and issue https://github.com/bazelbuild/rules_nodejs/issues/57 for more details.
ts_providers["providers"].extend([
js_module_info(
sources = ts_providers["typescript"]["es5_sources"],
deps = ctx.attr.deps,
),
js_named_module_info(
sources = ts_providers["typescript"]["es5_sources"],
deps = ctx.attr.deps,
@ -701,12 +698,9 @@ NG_MODULE_ATTRIBUTES = {
"compiler": attr.label(
doc = """Sets a different ngc compiler binary to use for this library.
The default ngc compiler depends on the `@npm//@angular/bazel`
The default ngc compiler depends on the `//@angular/bazel`
target which is setup for projects that use bazel managed npm deps that
fetch the @angular/bazel npm package. It is recommended that you use
the workspace name `@npm` for bazel managed deps so the default
compiler works out of the box. Otherwise, you'll have to override
the compiler attribute manually.
fetch the @angular/bazel npm package.
""",
default = Label(DEFAULT_NG_COMPILER),
executable = True,
@ -725,14 +719,11 @@ NG_MODULE_RULE_ATTRS = dict(dict(COMMON_ATTRIBUTES, **NG_MODULE_ATTRIBUTES), **{
"node_modules": attr.label(
doc = """The npm packages which should be available during the compile.
The default value of `@npm//typescript:typescript__typings` is
for projects that use bazel managed npm deps. It is recommended
that you use the workspace name `@npm` for bazel managed deps so the
default value works out of the box. Otherwise, you'll have to
override the node_modules attribute manually. This default is in place
The default value of `//typescript:typescript__typings` is
for projects that use bazel managed npm deps. This default is in place
since code compiled by ng_module will always depend on at least the
typescript default libs which are provided by
`@npm//typescript:typescript__typings`.
`//typescript:typescript__typings`.
This attribute is DEPRECATED. As of version 0.18.0 the recommended
approach to npm dependencies is to use fine grained npm dependencies
@ -784,7 +775,12 @@ NG_MODULE_RULE_ATTRS = dict(dict(COMMON_ATTRIBUTES, **NG_MODULE_ATTRIBUTES), **{
yarn_lock = "//:yarn.lock",
)
""",
default = Label("@npm//typescript:typescript__typings"),
default = Label(
# BEGIN-DEV-ONLY
"@npm" +
# END-DEV-ONLY
"//typescript:typescript__typings",
),
),
"entry_point": attr.label(allow_single_file = True),

View File

@ -2,8 +2,46 @@ package(default_visibility = ["//visibility:public"])
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
nodejs_binary(
name = "rollup_for_ng_package",
data = [
# BEGIN-DEV-ONLY
"@npm" +
# END-DEV-ONLY
"//rollup",
# BEGIN-DEV-ONLY
"@npm" +
# END-DEV-ONLY
"//rollup-plugin-commonjs",
# BEGIN-DEV-ONLY
"@npm" +
# END-DEV-ONLY
"//rollup-plugin-node-resolve",
# BEGIN-DEV-ONLY
"@npm" +
# END-DEV-ONLY
"//rollup-plugin-sourcemaps",
# BEGIN-DEV-ONLY
"@npm" +
# END-DEV-ONLY
"//typescript",
],
entry_point = (
# BEGIN-DEV-ONLY
"@npm" +
# END-DEV-ONLY
"//:node_modules/rollup/dist/bin/rollup"
),
)
exports_files([
"ng_package.bzl",
"rollup.config.js",
"terser_config.default.json",
])
# BEGIN-DEV-ONLY
load("@npm_bazel_typescript//:index.bzl", "ts_library")
load("@npm//@bazel/typescript:index.bzl", "ts_library")
ts_library(
name = "lib",
@ -16,34 +54,6 @@ ts_library(
],
)
# END-DEV-ONLY
nodejs_binary(
name = "packager",
data = [
"lib",
"@npm//shelljs",
],
entry_point = ":packager.ts",
)
nodejs_binary(
name = "rollup_for_ng_package",
data = [
"@npm//rollup",
"@npm//rollup-plugin-commonjs",
"@npm//rollup-plugin-node-resolve",
"@npm//rollup-plugin-sourcemaps",
"@npm//typescript",
],
entry_point = "@npm//:node_modules/rollup/dist/bin/rollup",
)
exports_files([
"ng_package.bzl",
"rollup.config.js",
"terser_config.default.json",
])
filegroup(
name = "package_assets",
srcs = glob(["*.bzl"]) + [
@ -52,3 +62,13 @@ filegroup(
"terser_config.default.json",
],
)
nodejs_binary(
name = "packager",
data = [
"lib",
"@npm//shelljs",
],
entry_point = ":packager.ts",
)
# END-DEV-ONLY

View File

@ -28,11 +28,16 @@ def _debug(vars, *args):
if "VERBOSE_LOGS" in vars.keys():
print("[ng_package.bzl]", args)
_DEFAULT_NG_PACKAGER = "@npm//@angular/bazel/bin:packager"
_DEFAULT_ROLLUP_CONFIG_TMPL = "@npm_angular_bazel//src/ng_package:rollup.config.js"
_DEFALUT_TERSER_CONFIG_FILE = "@npm_angular_bazel//src/ng_package:terser_config.default.json"
_DEFAULT_ROLLUP = "@npm_angular_bazel//src/ng_package:rollup_for_ng_package"
_DEFAULT_TERSER = "@npm//terser/bin:terser"
_DEFAULT_NG_PACKAGER = "//@angular/bazel/bin:packager"
_DEFAULT_ROLLUP_CONFIG_TMPL = "//:node_modules/@angular/bazel/src/ng_package/rollup.config.js"
_DEFALUT_TERSER_CONFIG_FILE = "//:node_modules/@angular/bazel/src/ng_package/terser_config.default.json"
_DEFAULT_ROLLUP = "//@angular/bazel/src/ng_package:rollup_for_ng_package"
_DEFAULT_TERSER = (
# BEGIN-DEV-ONLY
"@npm" +
# END-DEV-ONLY
"//terser/bin:terser"
)
_NG_PACKAGE_MODULE_MAPPINGS_ATTR = "ng_package_module_mappings"
@ -102,8 +107,6 @@ WELL_KNOWN_GLOBALS = {p: _global_name(p) for p in [
"@angular/core",
"@angular/platform-server/testing",
"@angular/platform-server",
"@angular/platform-webworker-dynamic",
"@angular/platform-webworker",
"@angular/common/testing",
"@angular/common",
"@angular/common/http/testing",

View File

@ -1,5 +1,6 @@
# BEGIN-DEV-ONLY
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
load("@npm_bazel_typescript//:index.bzl", "ts_library")
load("@npm//@bazel/typescript:index.bzl", "ts_library")
ts_library(
name = "ngc_lib",
@ -15,11 +16,7 @@ ts_library(
"//packages/bazel/test/ngc-wrapped:__subpackages__",
],
deps = [
# BEGIN-INTERNAL
# Only needed when compiling within the Angular repo.
# Users will get this dependency from node_modules.
"//packages/compiler-cli",
# END-INTERNAL
"@npm//@bazel/typescript",
"@npm//@types/node",
"@npm//tsickle",
@ -55,3 +52,4 @@ filegroup(
srcs = ["BUILD.bazel"],
visibility = ["//packages/bazel:__subpackages__"],
)
# END-DEV-ONLY

View File

@ -107,7 +107,6 @@ export declare enum Style {
*/
export declare enum ViewEncapsulation {
Emulated = 'Emulated',
Native = 'Native',
None = 'None',
ShadowDom = 'ShadowDom'
}

View File

@ -32,7 +32,7 @@ jasmine_node_test(
"//packages/bazel/test/ngc-wrapped/empty:empty_tsconfig.json",
"//packages/bazel/test/ngc-wrapped/empty:tsconfig.json",
"//packages/private/testing",
"@npm_bazel_typescript//third_party/github.com/bazelbuild/bazel/src/main/protobuf:worker_protocol.proto",
"@npm//@bazel/typescript/third_party/github.com/bazelbuild/bazel/src/main/protobuf:worker_protocol.proto",
],
)

View File

@ -3,9 +3,11 @@ licenses(["notice"])
package(default_visibility = ["//visibility:public"])
# BEGIN-DEV-ONLY
filegroup(
name = "package_assets",
srcs = glob(["*"]),
)
# END-DEV-ONLY
exports_files(["worker_protocol.proto"])

View File

@ -11,8 +11,7 @@ const path = require('path');
module.exports = {
baseDir: '../',
goldenFile: '../goldens/circular-deps/packages.json',
// The test should not capture deprecated packages such as `http`, or the `webworker` platform.
glob: `./!(http|platform-webworker|platform-webworker-dynamic)/**/*.ts`,
glob: `./**/*.ts`,
// Command that will be displayed if the golden needs to be updated.
approveCommand: 'yarn ts-circular-deps:approve',
resolveModule: resolveModule

View File

@ -2448,6 +2448,8 @@ export class HttpClient {
*/
put<T>(url: string, body: any|null, options: {
headers?: HttpHeaders|{[header: string]: string | string[]}, observe: 'events',
params?: HttpParams|{[param: string]: string | string[]},
reportProgress?: boolean,
responseType?: 'json',
withCredentials?: boolean,
}): Observable<HttpEvent<T>>;
@ -2526,8 +2528,8 @@ export class HttpClient {
}): Observable<HttpResponse<Object>>;
/**
* Constructs a `PUT` request that interprets the body as a JSON object and returns the full HTTP
* response.
* Constructs a `PUT` request that interprets the body as an instance of the requested type and
* returns the full HTTP response.
*
* @param url The endpoint URL.
* @param body The resources to add/update.
@ -2545,14 +2547,14 @@ export class HttpClient {
}): Observable<HttpResponse<T>>;
/**
* Constructs a `PUT` request that interprets the body as a JSON object and returns the response
* body as a JSON object.
* Constructs a `PUT` request that interprets the body as a JSON object
* and returns an observable of JSON object.
*
* @param url The endpoint URL.
* @param body The resources to add/update.
* @param options HTTP options
*
* @return An `Observable` of the response, with the response body as a JSON object.
* @return An `Observable` of the response as a JSON object.
*/
put(url: string, body: any|null, options?: {
headers?: HttpHeaders|{[header: string]: string | string[]},
@ -2564,15 +2566,14 @@ export class HttpClient {
}): Observable<Object>;
/**
* Constructs a `PUT` request that interprets the body as a JSON object
* and returns an observable of the response.
* Constructs a `PUT` request that interprets the body as an instance of the requested type
* and returns an observable of the requested type.
*
* @param url The endpoint URL.
* @param body The resources to add/update.
* @param options HTTP options
*
* @return An `Observable` of the `HTTPResponse` for the request, with a response body in the
* requested type.
* @return An `Observable` of the requested type.
*/
put<T>(url: string, body: any|null, options?: {
headers?: HttpHeaders|{[header: string]: string | string[]},

View File

@ -79,9 +79,10 @@ export class HttpXhrBackend implements HttpBackend {
*/
handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {
// Quick check to give a better error message when a user attempts to use
// HttpClient.jsonp() without installing the JsonpClientModule
// HttpClient.jsonp() without installing the HttpClientJsonpModule
if (req.method === 'JSONP') {
throw new Error(`Attempted to construct Jsonp request without JsonpClientModule installed.`);
throw new Error(
`Attempted to construct Jsonp request without HttpClientJsonpModule installed.`);
}
// Everything happens on Observable subscription.

View File

@ -31,6 +31,13 @@ import {toArray} from 'rxjs/operators';
});
backend.expectOne('/test').flush({'data': 'hello world'});
});
it('should allow flushing requests with a boolean value', (done: DoneFn) => {
client.get('/test').subscribe(res => {
expect((res as any)).toEqual(true);
done();
});
backend.expectOne('/test').flush(true);
});
it('for text data', done => {
client.get('/test', {responseType: 'text'}).subscribe(res => {
expect(res).toEqual('hello world');

View File

@ -40,11 +40,14 @@ export class TestRequest {
*
* Both successful and unsuccessful responses can be delivered via `flush()`.
*/
flush(body: ArrayBuffer|Blob|string|number|Object|(string|number|Object|null)[]|null, opts: {
headers?: HttpHeaders|{[name: string]: string | string[]},
status?: number,
statusText?: string,
} = {}): void {
flush(
body: ArrayBuffer|Blob|boolean|string|number|Object|(boolean|string|number|Object|null)[]|
null,
opts: {
headers?: HttpHeaders|{[name: string]: string | string[]},
status?: number,
statusText?: string,
} = {}): void {
if (this.cancelled) {
throw new Error(`Cannot flush a cancelled request.`);
}
@ -146,7 +149,8 @@ function _toBlob(body: ArrayBuffer|Blob|string|number|Object|
* Helper function to convert a response body to JSON data.
*/
function _toJsonBody(
body: ArrayBuffer|Blob|string|number|Object|(string | number | Object | null)[],
body: ArrayBuffer|Blob|boolean|string|number|Object|
(boolean | string | number | Object | null)[],
format: string = 'JSON'): Object|string|number|(Object | string | number)[] {
if (typeof ArrayBuffer !== 'undefined' && body instanceof ArrayBuffer) {
throw new Error(`Automatic conversion to ${format} is not supported for ArrayBuffers.`);
@ -155,7 +159,7 @@ function _toJsonBody(
throw new Error(`Automatic conversion to ${format} is not supported for Blobs.`);
}
if (typeof body === 'string' || typeof body === 'number' || typeof body === 'object' ||
Array.isArray(body)) {
typeof body === 'boolean' || Array.isArray(body)) {
return body;
}
throw new Error(`Automatic conversion to ${format} is not supported for response type.`);

View File

@ -20,7 +20,7 @@ export default [
[['a', 'p'], ['AM', 'PM'], u],
[['AM', 'PM'], u, u],
[
['D', 'L', 'M', 'M', 'H', 'B', 'S'], ['Dom', 'Lun', 'Mar', 'Mks', 'Hu', 'Bi', 'Sa'],
['D', 'L', 'M', 'M', 'H', 'B', 'S'], ['Dom', 'Lun', 'Mar', 'Miy', 'Huw', 'Biy', 'Sab'],
['Domingo', 'Lunes', 'Martes', 'Miyerkules', 'Huwebes', 'Biyernes', 'Sabado'],
['Dom', 'Lun', 'Mar', 'Miy', 'Huw', 'Biy', 'Sab']
],
@ -33,8 +33,15 @@ export default [
'Oktubre', 'Nobyembre', 'Disyembre'
]
],
u,
[['WK', 'KP'], u, u],
[
['E', 'P', 'M', 'A', 'M', 'H', 'H', 'A', 'S', 'O', 'N', 'D'],
['Ene', 'Peb', 'Mar', 'Abr', 'May', 'Hun', 'Hul', 'Ago', 'Set', 'Okt', 'Nob', 'Dis'],
[
'Enero', 'Pebrero', 'Marso', 'Abril', 'Mayo', 'Hunyo', 'Hulyo', 'Agosto', 'Setyembre',
'Oktubre', 'Nobyembre', 'Disyembre'
]
],
[['BC', 'KP'], u, ['Sa Wala Pa Si Kristo', 'Anno Domini']],
0,
[6, 0],
['M/d/yy', 'MMM d, y', 'MMMM d, y', 'EEEE, MMMM d, y'],

View File

@ -1917,6 +1917,16 @@ export const locale_eu = [
['ig.', 'al.', 'ar.', 'az.', 'og.', 'or.', 'lr.']
],
u,
[
['U', 'O', 'M', 'A', 'M', 'E', 'U', 'A', 'I', 'U', 'A', 'A'],
[
'urt.', 'ots.', 'mar.', 'api.', 'mai.', 'eka.', 'uzt.', 'abu.', 'ira.', 'urr.', 'aza.', 'abe.'
],
[
'urtarrilak', 'otsailak', 'martxoak', 'apirilak', 'maiatzak', 'ekainak', 'uztailak',
'abuztuak', 'irailak', 'urriak', 'azaroak', 'abenduak'
]
],
[
['U', 'O', 'M', 'A', 'M', 'E', 'U', 'A', 'I', 'U', 'A', 'A'],
[
@ -1927,7 +1937,6 @@ export const locale_eu = [
'iraila', 'urria', 'azaroa', 'abendua'
]
],
u,
[['K.a.', 'K.o.'], u, ['K.a.', 'Kristo ondoren']],
1,
[6, 0],
@ -4031,7 +4040,7 @@ export const locale_ne = [
['HH:mm', 'HH:mm:ss', 'HH:mm:ss z', 'HH:mm:ss zzzz'],
['{1}, {0}', u, '{1} {0}', u],
['.', ',', ';', '%', '+', '-', 'E', '×', '‰', '∞', 'NaN', ':'],
['#,##0.###', '#,##0%', '¤ #,##0.00', '#E0'],
['#,##,##0.###', '#,##,##0%', '¤ #,##,##0.00', '#E0'],
'NPR',
'नेरू',
'नेपाली रूपैयाँ',

View File

@ -26,6 +26,16 @@ export default [
['ig.', 'al.', 'ar.', 'az.', 'og.', 'or.', 'lr.']
],
u,
[
['U', 'O', 'M', 'A', 'M', 'E', 'U', 'A', 'I', 'U', 'A', 'A'],
[
'urt.', 'ots.', 'mar.', 'api.', 'mai.', 'eka.', 'uzt.', 'abu.', 'ira.', 'urr.', 'aza.', 'abe.'
],
[
'urtarrilak', 'otsailak', 'martxoak', 'apirilak', 'maiatzak', 'ekainak', 'uztailak',
'abuztuak', 'irailak', 'urriak', 'azaroak', 'abenduak'
]
],
[
['U', 'O', 'M', 'A', 'M', 'E', 'U', 'A', 'I', 'U', 'A', 'A'],
[
@ -36,7 +46,6 @@ export default [
'iraila', 'urria', 'azaroa', 'abendua'
]
],
u,
[['K.a.', 'K.o.'], u, ['K.a.', 'Kristo ondoren']],
1,
[6, 0],

View File

@ -22,7 +22,7 @@ global.ng.common.locales['ceb'] = [
[['a', 'p'], ['AM', 'PM'], u],
[['AM', 'PM'], u, u],
[
['D', 'L', 'M', 'M', 'H', 'B', 'S'], ['Dom', 'Lun', 'Mar', 'Mks', 'Hu', 'Bi', 'Sa'],
['D', 'L', 'M', 'M', 'H', 'B', 'S'], ['Dom', 'Lun', 'Mar', 'Miy', 'Huw', 'Biy', 'Sab'],
['Domingo', 'Lunes', 'Martes', 'Miyerkules', 'Huwebes', 'Biyernes', 'Sabado'],
['Dom', 'Lun', 'Mar', 'Miy', 'Huw', 'Biy', 'Sab']
],
@ -35,8 +35,15 @@ global.ng.common.locales['ceb'] = [
'Oktubre', 'Nobyembre', 'Disyembre'
]
],
u,
[['WK', 'KP'], u, u],
[
['E', 'P', 'M', 'A', 'M', 'H', 'H', 'A', 'S', 'O', 'N', 'D'],
['Ene', 'Peb', 'Mar', 'Abr', 'May', 'Hun', 'Hul', 'Ago', 'Set', 'Okt', 'Nob', 'Dis'],
[
'Enero', 'Pebrero', 'Marso', 'Abril', 'Mayo', 'Hunyo', 'Hulyo', 'Agosto', 'Setyembre',
'Oktubre', 'Nobyembre', 'Disyembre'
]
],
[['BC', 'KP'], u, ['Sa Wala Pa Si Kristo', 'Anno Domini']],
0,
[6, 0],
['M/d/yy', 'MMM d, y', 'MMMM d, y', 'EEEE, MMMM d, y'],

View File

@ -28,6 +28,16 @@ global.ng.common.locales['eu'] = [
['ig.', 'al.', 'ar.', 'az.', 'og.', 'or.', 'lr.']
],
u,
[
['U', 'O', 'M', 'A', 'M', 'E', 'U', 'A', 'I', 'U', 'A', 'A'],
[
'urt.', 'ots.', 'mar.', 'api.', 'mai.', 'eka.', 'uzt.', 'abu.', 'ira.', 'urr.', 'aza.', 'abe.'
],
[
'urtarrilak', 'otsailak', 'martxoak', 'apirilak', 'maiatzak', 'ekainak', 'uztailak',
'abuztuak', 'irailak', 'urriak', 'azaroak', 'abenduak'
]
],
[
['U', 'O', 'M', 'A', 'M', 'E', 'U', 'A', 'I', 'U', 'A', 'A'],
[
@ -38,7 +48,6 @@ global.ng.common.locales['eu'] = [
'iraila', 'urria', 'azaroa', 'abendua'
]
],
u,
[['K.a.', 'K.o.'], u, ['K.a.', 'Kristo ondoren']],
1,
[6, 0],

View File

@ -47,7 +47,7 @@ global.ng.common.locales['ha-gh'] = [
['#,##0.###', '#,##0%', '¤ #,##0.00', '#E0'],
'GHS',
'GH₵',
'GHS',
'Kudin Ghana',
{'GHS': ['GH₵'], 'NGN': ['₦']},
'ltr',
plural,

View File

@ -35,7 +35,7 @@ global.ng.common.locales['kkj'] = [
['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
[
'pamba', 'wanja', 'mbiyɔ mɛndoŋgɔ', 'Nyɔlɔmbɔŋgɔ', 'Mɔnɔ ŋgbanja', 'Nyaŋgwɛ ŋgbanja',
'kuŋgwɛ', 'fɛ', 'njapi', 'nyukul', '11', 'ɓulɓusɛ'
'kuŋgwɛ', 'fɛ', 'njapi', 'nyukul', 'M11', 'ɓulɓusɛ'
],
u
],

View File

@ -22,20 +22,31 @@ global.ng.common.locales['kok'] = [
[['a', 'p'], ['AM', 'PM'], u],
[['AM', 'PM'], u, u],
[
['आ', 'सो', 'मं', 'बु', 'गु', 'शु', 'शे'],
['आयतार', 'सोमार', 'मंगळार', 'बुधवार', 'गुरुवार', 'शुक्रार', 'शेनवार'], u,
['आय', 'सोम', 'मंगळ', 'बुध', 'गुरु', 'शुक्र', 'शेन']
['आ', 'सो', 'मं', 'बु', 'बि', 'शु', 'शे'],
['आयतार', 'सोमार', 'मंगळार', 'बुधवार', 'बिरेस्तार', 'शुक्रार', 'शेनवार'], u,
['आय', 'सोम', 'मंगळ', 'बुध', 'बिरे', 'शुक्र', 'शेन']
],
[
['आ', 'सो', 'मं', 'बु', 'ब', 'शु', 'शे'],
['आयतार', 'सोमार', 'मंगळार', 'बुधवार', 'बिरेस्तार', 'शुक्रार', 'शेनवार'], u,
['आय', 'सोम', 'मंगळ', 'बुध', 'बिरे', 'शुक्र', 'शेन']
],
u,
[
['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
[
'जानेवारी', 'फेब्रुवारी', 'मार्च', 'एप्रिल', 'मे', 'जून', 'जुलय', 'आगोस्', 'सप्टेंबर', 'ऑक्टोबर',
'नोव्हेंबर', 'डिसेंबर'
'जानेवारी', 'फेब्रुवारी', 'मार्च', 'एप्रिल', 'मे', 'जून', 'जुलय', 'ऑगस्', 'सप्टेंबर', 'ऑक्टोबर', 'नोव्हेंबर',
'डिसेंबर'
],
u
],
u,
[
['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
['जाने', 'फेब्रु', 'मार्च', 'एप्री', 'मे', 'जून', 'जुल', 'ऑग', 'सप्टें', 'ऑक्टो', 'नो', 'डिसे'],
[
'जानेवारी', 'फेब्रुवारी', 'मार्च', 'एप्रिल', 'मे', 'जून', 'जुलय', 'ऑगस्ट', 'सप्टेंबर', 'ऑक्टोबर', 'नोव्हेंबर',
'डिसेंबर'
]
],
[['क्रिस्तपूर्व', 'क्रिस्तशखा'], u, u],
0,
[0, 0],

View File

@ -51,7 +51,7 @@ global.ng.common.locales['ne-in'] = [
['h:mm a', 'h:mm:ss a', 'h:mm:ss a z', 'h:mm:ss a zzzz'],
['{1}, {0}', u, '{1} {0}', u],
['.', ',', ';', '%', '+', '-', 'E', '×', '‰', '∞', 'NaN', ':'],
['#,##0.###', '#,##0%', '¤ #,##0.00', '#E0'],
['#,##,##0.###', '#,##,##0%', '¤ #,##,##0.00', '#E0'],
'INR',
'₹',
'भारतीय रूपिँया',

View File

@ -51,7 +51,7 @@ global.ng.common.locales['ne'] = [
['HH:mm', 'HH:mm:ss', 'HH:mm:ss z', 'HH:mm:ss zzzz'],
['{1}, {0}', u, '{1} {0}', u],
['.', ',', ';', '%', '+', '-', 'E', '×', '‰', '∞', 'NaN', ':'],
['#,##0.###', '#,##0%', '¤ #,##0.00', '#E0'],
['#,##,##0.###', '#,##,##0%', '¤ #,##,##0.00', '#E0'],
'NPR',
'नेरू',
'नेपाली रूपैयाँ',

View File

@ -45,7 +45,7 @@ global.ng.common.locales['so-dj'] = [
'Oktoobar', 'Nofembar', 'Desembar'
]
],
[['CH', 'CD'], u, ['Ciise Hortii', 'Ciise Dabadii']],
[['B', 'A'], ['CH', 'CD'], ['Ciise Hortii', 'Ciise Dabadii']],
6,
[6, 0],
['dd/MM/yy', 'dd-MMM-y', 'dd MMMM y', 'EEEE, MMMM dd, y'],

View File

@ -45,7 +45,7 @@ global.ng.common.locales['so-et'] = [
'Oktoobar', 'Nofembar', 'Desembar'
]
],
[['CH', 'CD'], u, ['Ciise Hortii', 'Ciise Dabadii']],
[['B', 'A'], ['CH', 'CD'], ['Ciise Hortii', 'Ciise Dabadii']],
0,
[6, 0],
['dd/MM/yy', 'dd-MMM-y', 'dd MMMM y', 'EEEE, MMMM dd, y'],

View File

@ -45,7 +45,7 @@ global.ng.common.locales['so-ke'] = [
'Oktoobar', 'Nofembar', 'Desembar'
]
],
[['CH', 'CD'], u, ['Ciise Hortii', 'Ciise Dabadii']],
[['B', 'A'], ['CH', 'CD'], ['Ciise Hortii', 'Ciise Dabadii']],
0,
[6, 0],
['dd/MM/yy', 'dd-MMM-y', 'dd MMMM y', 'EEEE, MMMM dd, y'],

View File

@ -45,7 +45,7 @@ global.ng.common.locales['so'] = [
'Oktoobar', 'Nofembar', 'Desembar'
]
],
[['CH', 'CD'], u, ['Ciise Hortii', 'Ciise Dabadii']],
[['B', 'A'], ['CH', 'CD'], ['Ciise Hortii', 'Ciise Dabadii']],
1,
[6, 0],
['dd/MM/yy', 'dd-MMM-y', 'dd MMMM y', 'EEEE, MMMM dd, y'],

View File

@ -28,11 +28,7 @@ global.ng.common.locales['vai-latn'] = [
u,
[
['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
[
'luukao kemã', 'ɓandaɓu', 'vɔɔ', 'fulu', 'goo', '6', '7', 'kɔnde', 'saah', 'galo',
'kenpkato ɓololɔ', 'luukao lɔma'
],
u
['M01', 'M02', 'M03', 'M04', 'M05', 'M06', 'M07', 'M08', 'M09', 'M10', 'M11', 'M12'], u
],
u,
[['BCE', 'CE'], u, u],

View File

@ -45,7 +45,7 @@ export default [
['#,##0.###', '#,##0%', '¤ #,##0.00', '#E0'],
'GHS',
'GH₵',
'GHS',
'Kudin Ghana',
{'GHS': ['GH₵'], 'NGN': ['₦']},
'ltr',
plural

View File

@ -33,7 +33,7 @@ export default [
['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
[
'pamba', 'wanja', 'mbiyɔ mɛndoŋgɔ', 'Nyɔlɔmbɔŋgɔ', 'Mɔnɔ ŋgbanja', 'Nyaŋgwɛ ŋgbanja',
'kuŋgwɛ', 'fɛ', 'njapi', 'nyukul', '11', 'ɓulɓusɛ'
'kuŋgwɛ', 'fɛ', 'njapi', 'nyukul', 'M11', 'ɓulɓusɛ'
],
u
],

View File

@ -20,20 +20,31 @@ export default [
[['a', 'p'], ['AM', 'PM'], u],
[['AM', 'PM'], u, u],
[
['आ', 'सो', 'मं', 'बु', 'गु', 'शु', 'शे'],
['आयतार', 'सोमार', 'मंगळार', 'बुधवार', 'गुरुवार', 'शुक्रार', 'शेनवार'], u,
['आय', 'सोम', 'मंगळ', 'बुध', 'गुरु', 'शुक्र', 'शेन']
['आ', 'सो', 'मं', 'बु', 'बि', 'शु', 'शे'],
['आयतार', 'सोमार', 'मंगळार', 'बुधवार', 'बिरेस्तार', 'शुक्रार', 'शेनवार'], u,
['आय', 'सोम', 'मंगळ', 'बुध', 'बिरे', 'शुक्र', 'शेन']
],
[
['आ', 'सो', 'मं', 'बु', 'ब', 'शु', 'शे'],
['आयतार', 'सोमार', 'मंगळार', 'बुधवार', 'बिरेस्तार', 'शुक्रार', 'शेनवार'], u,
['आय', 'सोम', 'मंगळ', 'बुध', 'बिरे', 'शुक्र', 'शेन']
],
u,
[
['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
[
'जानेवारी', 'फेब्रुवारी', 'मार्च', 'एप्रिल', 'मे', 'जून', 'जुलय', 'आगोस्', 'सप्टेंबर', 'ऑक्टोबर',
'नोव्हेंबर', 'डिसेंबर'
'जानेवारी', 'फेब्रुवारी', 'मार्च', 'एप्रिल', 'मे', 'जून', 'जुलय', 'ऑगस्', 'सप्टेंबर', 'ऑक्टोबर', 'नोव्हेंबर',
'डिसेंबर'
],
u
],
u,
[
['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
['जाने', 'फेब्रु', 'मार्च', 'एप्री', 'मे', 'जून', 'जुल', 'ऑग', 'सप्टें', 'ऑक्टो', 'नो', 'डिसे'],
[
'जानेवारी', 'फेब्रुवारी', 'मार्च', 'एप्रिल', 'मे', 'जून', 'जुलय', 'ऑगस्ट', 'सप्टेंबर', 'ऑक्टोबर', 'नोव्हेंबर',
'डिसेंबर'
]
],
[['क्रिस्तपूर्व', 'क्रिस्तशखा'], u, u],
0,
[0, 0],

View File

@ -49,7 +49,7 @@ export default [
['h:mm a', 'h:mm:ss a', 'h:mm:ss a z', 'h:mm:ss a zzzz'],
['{1}, {0}', u, '{1} {0}', u],
['.', ',', ';', '%', '+', '-', 'E', '×', '‰', '∞', 'NaN', ':'],
['#,##0.###', '#,##0%', '¤ #,##0.00', '#E0'],
['#,##,##0.###', '#,##,##0%', '¤ #,##,##0.00', '#E0'],
'INR',
'₹',
'भारतीय रूपिँया',

View File

@ -49,7 +49,7 @@ export default [
['HH:mm', 'HH:mm:ss', 'HH:mm:ss z', 'HH:mm:ss zzzz'],
['{1}, {0}', u, '{1} {0}', u],
['.', ',', ';', '%', '+', '-', 'E', '×', '‰', '∞', 'NaN', ':'],
['#,##0.###', '#,##0%', '¤ #,##0.00', '#E0'],
['#,##,##0.###', '#,##,##0%', '¤ #,##,##0.00', '#E0'],
'NPR',
'नेरू',
'नेपाली रूपैयाँ',

View File

@ -43,7 +43,7 @@ export default [
'Oktoobar', 'Nofembar', 'Desembar'
]
],
[['CH', 'CD'], u, ['Ciise Hortii', 'Ciise Dabadii']],
[['B', 'A'], ['CH', 'CD'], ['Ciise Hortii', 'Ciise Dabadii']],
6,
[6, 0],
['dd/MM/yy', 'dd-MMM-y', 'dd MMMM y', 'EEEE, MMMM dd, y'],

View File

@ -43,7 +43,7 @@ export default [
'Oktoobar', 'Nofembar', 'Desembar'
]
],
[['CH', 'CD'], u, ['Ciise Hortii', 'Ciise Dabadii']],
[['B', 'A'], ['CH', 'CD'], ['Ciise Hortii', 'Ciise Dabadii']],
0,
[6, 0],
['dd/MM/yy', 'dd-MMM-y', 'dd MMMM y', 'EEEE, MMMM dd, y'],

View File

@ -43,7 +43,7 @@ export default [
'Oktoobar', 'Nofembar', 'Desembar'
]
],
[['CH', 'CD'], u, ['Ciise Hortii', 'Ciise Dabadii']],
[['B', 'A'], ['CH', 'CD'], ['Ciise Hortii', 'Ciise Dabadii']],
0,
[6, 0],
['dd/MM/yy', 'dd-MMM-y', 'dd MMMM y', 'EEEE, MMMM dd, y'],

View File

@ -43,7 +43,7 @@ export default [
'Oktoobar', 'Nofembar', 'Desembar'
]
],
[['CH', 'CD'], u, ['Ciise Hortii', 'Ciise Dabadii']],
[['B', 'A'], ['CH', 'CD'], ['Ciise Hortii', 'Ciise Dabadii']],
1,
[6, 0],
['dd/MM/yy', 'dd-MMM-y', 'dd MMMM y', 'EEEE, MMMM dd, y'],

View File

@ -26,11 +26,7 @@ export default [
u,
[
['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
[
'luukao kemã', 'ɓandaɓu', 'vɔɔ', 'fulu', 'goo', '6', '7', 'kɔnde', 'saah', 'galo',
'kenpkato ɓololɔ', 'luukao lɔma'
],
u
['M01', 'M02', 'M03', 'M04', 'M05', 'M06', 'M07', 'M08', 'M09', 'M10', 'M11', 'M12'], u
],
u,
[['BCE', 'CE'], u, u],

View File

@ -155,7 +155,7 @@ export class NgForOf<T, U extends NgIterable<T> = NgIterable<T>> implements DoCh
* rather than the identity of the object itself.
*
* The function receives two inputs,
* the iteration index and the node object ID.
* the iteration index and the associated node data.
*/
@Input()
set ngForTrackBy(fn: TrackByFunction<T>) {

View File

@ -13,7 +13,7 @@ export const ISO8601_DATE_REGEX =
// 1 2 3 4 5 6 7 8 9 10 11
const NAMED_FORMATS: {[localeId: string]: {[format: string]: string}} = {};
const DATE_FORMATS_SPLIT =
/((?:[^GyMLwWdEabBhHmsSzZO']+)|(?:'(?:[^']|'')*')|(?:G{1,5}|y{1,4}|M{1,5}|L{1,5}|w{1,2}|W{1}|d{1,2}|E{1,6}|a{1,5}|b{1,5}|B{1,5}|h{1,2}|H{1,2}|m{1,2}|s{1,2}|S{1,3}|z{1,4}|Z{1,5}|O{1,4}))([\s\S]*)/;
/((?:[^GyrMLwWdEabBhHmsSzZO']+)|(?:'(?:[^']|'')*')|(?:G{1,5}|y{1,4}|r{1,4}|M{1,5}|L{1,5}|w{1,2}|W{1}|d{1,2}|E{1,6}|a{1,5}|b{1,5}|B{1,5}|h{1,2}|H{1,2}|m{1,2}|s{1,2}|S{1,3}|z{1,4}|Z{1,5}|O{1,4}))([\s\S]*)/;
enum ZoneWidth {
Short,
@ -394,6 +394,18 @@ function weekGetter(size: number, monthBased = false): DateFormatter {
};
}
/**
* Returns a date formatter that provides the week-numbering year for the input date.
*/
function weekNumberingYearGetter(size: number, trim = false): DateFormatter {
return function(date: Date, locale: string) {
const thisThurs = getThursdayThisWeek(date);
const weekNumberingYear = thisThurs.getFullYear();
return padNumber(
weekNumberingYear, size, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign), trim);
};
}
type DateFormatter = (date: Date, locale: string, offset: number) => string;
const DATE_FORMATS: {[format: string]: DateFormatter} = {};
@ -438,6 +450,25 @@ function getDateFormatter(format: string): DateFormatter|null {
formatter = dateGetter(DateType.FullYear, 4, 0, false, true);
break;
// 1 digit representation of the week-numbering year, e.g. (AD 1 => 1, AD 199 => 199)
case 'r':
formatter = weekNumberingYearGetter(1);
break;
// 2 digit representation of the week-numbering year, padded (00-99). (e.g. AD 2001 => 01, AD
// 2010 => 10)
case 'rr':
formatter = weekNumberingYearGetter(2, true);
break;
// 3 digit representation of the week-numbering year, padded (000-999). (e.g. AD 1 => 001, AD
// 2010 => 2010)
case 'rrr':
formatter = weekNumberingYearGetter(3);
break;
// 4 digit representation of the week-numbering year (e.g. AD 1 => 0001, AD 2010 => 2010)
case 'rrrr':
formatter = weekNumberingYearGetter(4);
break;
// Month of the year (1-12), numeric
case 'M':
case 'L':
@ -636,7 +667,7 @@ function getDateFormatter(format: string): DateFormatter|null {
}
function timezoneToOffset(timezone: string, fallback: number): number {
// Support: IE 9-11 only, Edge 13-15+
// Support: IE 11 only, Edge 13-15+
// IE/Edge do not "understand" colon (`:`) in timezone
timezone = timezone.replace(/:/g, '');
const requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000;
@ -734,7 +765,10 @@ export function isoStringToDate(match: RegExpMatchArray): Date {
const h = Number(match[4] || 0) - tzHour;
const m = Number(match[5] || 0) - tzMin;
const s = Number(match[6] || 0);
const ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000);
// The ECMAScript specification (https://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.11)
// defines that `DateTime` milliseconds should always be rounded down, so that `999.9ms`
// becomes `999ms`.
const ms = Math.floor(parseFloat('0.' + (match[7] || 0)) * 1000);
timeSetter.call(date, h, m, s, ms);
return date;
}

View File

@ -233,7 +233,7 @@ export function getLocaleId(locale: string): string {
* @publicApi
*/
export function getLocaleDayPeriods(
locale: string, formStyle: FormStyle, width: TranslationWidth): [string, string] {
locale: string, formStyle: FormStyle, width: TranslationWidth): Readonly<[string, string]> {
const data = ɵfindLocaleData(locale);
const amPmData = <[string, string][][]>[
data[ɵLocaleDataIndex.DayPeriodsFormat], data[ɵLocaleDataIndex.DayPeriodsStandalone]
@ -255,7 +255,7 @@ export function getLocaleDayPeriods(
* @publicApi
*/
export function getLocaleDayNames(
locale: string, formStyle: FormStyle, width: TranslationWidth): string[] {
locale: string, formStyle: FormStyle, width: TranslationWidth): ReadonlyArray<string> {
const data = ɵfindLocaleData(locale);
const daysData =
<string[][][]>[data[ɵLocaleDataIndex.DaysFormat], data[ɵLocaleDataIndex.DaysStandalone]];
@ -276,7 +276,7 @@ export function getLocaleDayNames(
* @publicApi
*/
export function getLocaleMonthNames(
locale: string, formStyle: FormStyle, width: TranslationWidth): string[] {
locale: string, formStyle: FormStyle, width: TranslationWidth): ReadonlyArray<string> {
const data = ɵfindLocaleData(locale);
const monthsData =
<string[][][]>[data[ɵLocaleDataIndex.MonthsFormat], data[ɵLocaleDataIndex.MonthsStandalone]];
@ -287,7 +287,6 @@ export function getLocaleMonthNames(
/**
* Retrieves Gregorian-calendar eras for the given locale.
* @param locale A locale code for the locale format rules to use.
* @param formStyle The required grammatical form.
* @param width The required character width.
* @returns An array of localized era strings.
@ -296,7 +295,8 @@ export function getLocaleMonthNames(
*
* @publicApi
*/
export function getLocaleEraNames(locale: string, width: TranslationWidth): [string, string] {
export function getLocaleEraNames(
locale: string, width: TranslationWidth): Readonly<[string, string]> {
const data = ɵfindLocaleData(locale);
const erasData = <[string, string][]>data[ɵLocaleDataIndex.Eras];
return getLastDefinedValue(erasData, width);

View File

@ -94,11 +94,10 @@ export class AsyncPipe implements OnDestroy, PipeTransform {
}
}
transform<T>(obj: null): null;
transform<T>(obj: undefined): undefined;
transform<T>(obj: Observable<T>|null|undefined): T|null;
transform<T>(obj: Promise<T>|null|undefined): T|null;
transform(obj: Observable<any>|Promise<any>|null|undefined): any {
transform<T>(obj: Observable<T>|Promise<T>): T|null;
transform<T>(obj: null|undefined): null;
transform<T>(obj: Observable<T>|Promise<T>|null|undefined): T|null;
transform<T>(obj: Observable<T>|Promise<T>|null|undefined): T|null {
if (!this._obj) {
if (obj) {
this._subscribe(obj);
@ -108,7 +107,7 @@ export class AsyncPipe implements OnDestroy, PipeTransform {
if (obj !== this._obj) {
this._dispose();
return this.transform(obj as any);
return this.transform(obj);
}
return this._latestValue;

View File

@ -29,8 +29,11 @@ export class LowerCasePipe implements PipeTransform {
/**
* @param value The string to transform to lower case.
*/
transform(value: string): string {
if (!value) return value;
transform(value: string): string;
transform(value: null|undefined): null;
transform(value: string|null|undefined): string|null;
transform(value: string|null|undefined): string|null {
if (value == null) return null;
if (typeof value !== 'string') {
throw invalidPipeArgumentError(LowerCasePipe, value);
}
@ -72,8 +75,11 @@ export class TitleCasePipe implements PipeTransform {
/**
* @param value The string to transform to title case.
*/
transform(value: string): string {
if (!value) return value;
transform(value: string): string;
transform(value: null|undefined): null;
transform(value: string|null|undefined): string|null;
transform(value: string|null|undefined): string|null {
if (value == null) return null;
if (typeof value !== 'string') {
throw invalidPipeArgumentError(TitleCasePipe, value);
}
@ -96,8 +102,11 @@ export class UpperCasePipe implements PipeTransform {
/**
* @param value The string to transform to upper case.
*/
transform(value: string): string {
if (!value) return value;
transform(value: string): string;
transform(value: null|undefined): null;
transform(value: string|null|undefined): string|null;
transform(value: string|null|undefined): string|null {
if (value == null) return null;
if (typeof value !== 'string') {
throw invalidPipeArgumentError(UpperCasePipe, value);
}

View File

@ -65,6 +65,10 @@ import {invalidPipeArgumentError} from './invalid_pipe_argument_error';
* | | yy | Numeric: 2 digits + zero padded | 02, 20, 01, 17, 73 |
* | | yyy | Numeric: 3 digits + zero padded | 002, 020, 201, 2017, 20173 |
* | | yyyy | Numeric: 4 digits or more + zero padded | 0002, 0020, 0201, 2017, 20173 |
* | Week-numbering year| r | Numeric: minimum digits | 2, 20, 201, 2017, 20173 |
* | | rr | Numeric: 2 digits + zero padded | 02, 20, 01, 17, 73 |
* | | rrr | Numeric: 3 digits + zero padded | 002, 020, 201, 2017, 20173 |
* | | rrrr | Numeric: 4 digits or more + zero padded | 0002, 0020, 0201, 2017, 20173 |
* | Month | M | Numeric: 1 digit | 9, 12 |
* | | MM | Numeric: 2 digits + zero padded | 09, 12 |
* | | MMM | Abbreviated | Sep |
@ -167,7 +171,15 @@ export class DatePipe implements PipeTransform {
* See [Setting your app locale](guide/i18n#setting-up-the-locale-of-your-app).
* @returns A date string in the desired format.
*/
transform(value: any, format = 'mediumDate', timezone?: string, locale?: string): string|null {
transform(value: Date|string|number, format?: string, timezone?: string, locale?: string): string
|null;
transform(value: null|undefined, format?: string, timezone?: string, locale?: string): null;
transform(
value: Date|string|number|null|undefined, format?: string, timezone?: string,
locale?: string): string|null;
transform(
value: Date|string|number|null|undefined, format = 'mediumDate', timezone?: string,
locale?: string): string|null {
if (value == null || value === '' || value !== value) return null;
try {

View File

@ -39,7 +39,8 @@ export class I18nPluralPipe implements PipeTransform {
* @param locale a `string` defining the locale to use (uses the current {@link LOCALE_ID} by
* default).
*/
transform(value: number, pluralMap: {[count: string]: string}, locale?: string): string {
transform(value: number|null|undefined, pluralMap: {[count: string]: string}, locale?: string):
string {
if (value == null) return '';
if (typeof pluralMap !== 'object' || pluralMap === null) {

View File

@ -50,31 +50,35 @@ export class KeyValuePipe implements PipeTransform {
private differ!: KeyValueDiffer<any, any>;
private keyValues: Array<KeyValue<any, any>> = [];
transform<K, V>(input: null, compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number): null;
transform<V>(
input: {[key: string]: V}|ReadonlyMap<string, V>,
compareFn?: (a: KeyValue<string, V>, b: KeyValue<string, V>) => number):
Array<KeyValue<string, V>>;
transform<V>(
input: {[key: string]: V}|ReadonlyMap<string, V>|null,
compareFn?: (a: KeyValue<string, V>, b: KeyValue<string, V>) => number):
Array<KeyValue<string, V>>|null;
transform<V>(
input: {[key: number]: V}|ReadonlyMap<number, V>,
compareFn?: (a: KeyValue<number, V>, b: KeyValue<number, V>) => number):
Array<KeyValue<number, V>>;
transform<V>(
input: {[key: number]: V}|ReadonlyMap<number, V>|null,
compareFn?: (a: KeyValue<number, V>, b: KeyValue<number, V>) => number):
Array<KeyValue<number, V>>|null;
/*
* NOTE: when the `input` value is a simple Record<K, V> object, the keys are extracted with
* Object.keys(). This means that even if the `input` type is Record<number, V> the keys are
* compared/returned as `string`s.
*/
transform<K, V>(
input: ReadonlyMap<K, V>,
compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number): Array<KeyValue<K, V>>;
transform<K extends number, V>(
input: Record<K, V>, compareFn?: (a: KeyValue<string, V>, b: KeyValue<string, V>) => number):
Array<KeyValue<string, V>>;
transform<K extends string, V>(
input: Record<K, V>|ReadonlyMap<K, V>,
compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number): Array<KeyValue<K, V>>;
transform(
input: null|undefined,
compareFn?: (a: KeyValue<unknown, unknown>, b: KeyValue<unknown, unknown>) => number): null;
transform<K, V>(
input: ReadonlyMap<K, V>|null,
input: ReadonlyMap<K, V>|null|undefined,
compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number): Array<KeyValue<K, V>>|null;
transform<K extends number, V>(
input: Record<K, V>|null|undefined,
compareFn?: (a: KeyValue<string, V>, b: KeyValue<string, V>) => number):
Array<KeyValue<string, V>>|null;
transform<K extends string, V>(
input: Record<K, V>|ReadonlyMap<K, V>|null|undefined,
compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number): Array<KeyValue<K, V>>|null;
transform<K, V>(
input: null|{[key: string]: V, [key: number]: V}|ReadonlyMap<K, V>,
input: undefined|null|{[key: string]: V, [key: number]: V}|ReadonlyMap<K, V>,
compareFn: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number = defaultComparator):
Array<KeyValue<K, V>>|null {
if (!input || (!(input instanceof Map) && typeof input !== 'object')) {

View File

@ -67,8 +67,12 @@ export class DecimalPipe implements PipeTransform {
* When not supplied, uses the value of `LOCALE_ID`, which is `en-US` by default.
* See [Setting your app locale](guide/i18n#setting-up-the-locale-of-your-app).
*/
transform(value: any, digitsInfo?: string, locale?: string): string|null {
if (isEmpty(value)) return null;
transform(value: number|string, digitsInfo?: string, locale?: string): string|null;
transform(value: null|undefined, digitsInfo?: string, locale?: string): null;
transform(value: number|string|null|undefined, digitsInfo?: string, locale?: string): string|null;
transform(value: number|string|null|undefined, digitsInfo?: string, locale?: string): string
|null {
if (!isValue(value)) return null;
locale = locale || this._locale;
@ -121,8 +125,12 @@ export class PercentPipe implements PipeTransform {
* When not supplied, uses the value of `LOCALE_ID`, which is `en-US` by default.
* See [Setting your app locale](guide/i18n#setting-up-the-locale-of-your-app).
*/
transform(value: any, digitsInfo?: string, locale?: string): string|null {
if (isEmpty(value)) return null;
transform(value: number|string, digitsInfo?: string, locale?: string): string|null;
transform(value: null|undefined, digitsInfo?: string, locale?: string): null;
transform(value: number|string|null|undefined, digitsInfo?: string, locale?: string): string|null;
transform(value: number|string|null|undefined, digitsInfo?: string, locale?: string): string
|null {
if (!isValue(value)) return null;
locale = locale || this._locale;
try {
const num = strToNumber(value);
@ -213,10 +221,22 @@ export class CurrencyPipe implements PipeTransform {
* See [Setting your app locale](guide/i18n#setting-up-the-locale-of-your-app).
*/
transform(
value: any, currencyCode?: string,
value: number|string, currencyCode?: string,
display?: 'code'|'symbol'|'symbol-narrow'|string|boolean, digitsInfo?: string,
locale?: string): string|null;
transform(
value: null|undefined, currencyCode?: string,
display?: 'code'|'symbol'|'symbol-narrow'|string|boolean, digitsInfo?: string,
locale?: string): null;
transform(
value: number|string|null|undefined, currencyCode?: string,
display?: 'code'|'symbol'|'symbol-narrow'|string|boolean, digitsInfo?: string,
locale?: string): string|null;
transform(
value: number|string|null|undefined, currencyCode?: string,
display: 'code'|'symbol'|'symbol-narrow'|string|boolean = 'symbol', digitsInfo?: string,
locale?: string): string|null {
if (isEmpty(value)) return null;
if (!isValue(value)) return null;
locale = locale || this._locale;
@ -246,8 +266,8 @@ export class CurrencyPipe implements PipeTransform {
}
}
function isEmpty(value: any): boolean {
return value == null || value === '' || value !== value;
function isValue(value: number|string|null|undefined): value is number|string {
return !(value == null || value === '' || value !== value);
}
/**

View File

@ -62,11 +62,13 @@ export class SlicePipe implements PipeTransform {
* - **if negative**: return all items before `end` index from the end of the list or string.
*/
transform<T>(value: ReadonlyArray<T>, start: number, end?: number): Array<T>;
transform(value: null|undefined, start: number, end?: number): null;
transform<T>(value: ReadonlyArray<T>|null|undefined, start: number, end?: number): Array<T>|null;
transform(value: string, start: number, end?: number): string;
transform(value: null, start: number, end?: number): null;
transform(value: undefined, start: number, end?: number): undefined;
transform(value: any, start: number, end?: number): any {
if (value == null) return value;
transform(value: string|null|undefined, start: number, end?: number): string|null;
transform<T>(value: ReadonlyArray<T>|string|null|undefined, start: number, end?: number):
Array<T>|string|null {
if (value == null) return null;
if (!this.supports(value)) {
throw invalidPipeArgumentError(SlicePipe, value);

View File

@ -88,7 +88,7 @@ export class BrowserViewportScroller implements ViewportScroller {
* @returns The position in screen coordinates.
*/
getScrollPosition(): [number, number] {
if (this.supportScrollRestoration()) {
if (this.supportsScrolling()) {
return [this.window.scrollX, this.window.scrollY];
} else {
return [0, 0];
@ -100,7 +100,7 @@ export class BrowserViewportScroller implements ViewportScroller {
* @param position The new position in screen coordinates.
*/
scrollToPosition(position: [number, number]): void {
if (this.supportScrollRestoration()) {
if (this.supportsScrolling()) {
this.window.scrollTo(position[0], position[1]);
}
}
@ -110,7 +110,7 @@ export class BrowserViewportScroller implements ViewportScroller {
* @param anchor The ID of the anchor element.
*/
scrollToAnchor(anchor: string): void {
if (this.supportScrollRestoration()) {
if (this.supportsScrolling()) {
const elSelected =
this.document.getElementById(anchor) || this.document.getElementsByName(anchor)[0];
if (elSelected) {
@ -163,6 +163,14 @@ export class BrowserViewportScroller implements ViewportScroller {
return false;
}
}
private supportsScrolling(): boolean {
try {
return !!this.window.scrollTo;
} catch {
return false;
}
}
}
function getScrollRestorationProperty(obj: any): PropertyDescriptor|undefined {
@ -170,8 +178,7 @@ function getScrollRestorationProperty(obj: any): PropertyDescriptor|undefined {
}
/**
* Provides an empty implementation of the viewport scroller. This will
* live in @angular/common as it will be used by both platform-server and platform-webworker.
* Provides an empty implementation of the viewport scroller.
*/
export class NullViewportScroller implements ViewportScroller {
/**

View File

@ -95,6 +95,10 @@ describe('Format date', () => {
yy: '15',
yyy: '2015',
yyyy: '2015',
r: '2015',
rr: '15',
rrr: '2015',
rrrr: '2015',
M: '6',
MM: '06',
MMM: 'Jun',
@ -153,6 +157,10 @@ describe('Format date', () => {
yy: '15',
yyy: '2015',
yyyy: '2015',
r: '2015',
rr: '15',
rrr: '2015',
rrrr: '2015',
M: '1',
MM: '01',
MMM: 'Jan',
@ -361,5 +369,14 @@ describe('Format date', () => {
expect(formatDate(3001, 'm:ss.SS', 'en')).toEqual('0:03.00');
expect(formatDate(3001, 'm:ss.SSS', 'en')).toEqual('0:03.001');
});
// https://github.com/angular/angular/issues/38739
it('should return correct ISO 8601 week-numbering year for dates close to year end/beginning',
() => {
expect(formatDate('2013-12-27', 'rrrr', 'en')).toEqual('2013');
expect(formatDate('2013-12-29', 'rrrr', 'en')).toEqual('2014');
expect(formatDate('2010-01-02', 'rrrr', 'en')).toEqual('2009');
expect(formatDate('2010-01-04', 'rrrr', 'en')).toEqual('2010');
});
});
});

View File

@ -13,7 +13,7 @@ import localeHe from '@angular/common/locales/he';
import localeZh from '@angular/common/locales/zh';
import {ɵregisterLocaleData, ɵunregisterLocaleData} from '@angular/core';
import {FormatWidth, getCurrencySymbol, getLocaleDateFormat, getLocaleDirection, getNumberOfCurrencyDigits} from '../../src/i18n/locale_data_api';
import {FormatWidth, FormStyle, getCurrencySymbol, getLocaleDateFormat, getLocaleDayNames, getLocaleDirection, getLocaleMonthNames, getNumberOfCurrencyDigits, TranslationWidth} from '../../src/i18n/locale_data_api';
{
describe('locale data api', () => {
@ -71,5 +71,96 @@ import {FormatWidth, getCurrencySymbol, getLocaleDateFormat, getLocaleDirection,
expect(getLocaleDirection('en')).toEqual('ltr');
});
});
describe('getLocaleDayNames', () => {
it('should return english short list of days', () => {
expect(
getLocaleDayNames('en-US', FormStyle.Format, TranslationWidth.Short),
)
.toEqual(['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']);
});
it('should return french short list of days', () => {
expect(
getLocaleDayNames('fr-CA', FormStyle.Format, TranslationWidth.Short),
)
.toEqual(['di', 'lu', 'ma', 'me', 'je', 've', 'sa']);
});
it('should return english wide list of days', () => {
expect(
getLocaleDayNames('en-US', FormStyle.Format, TranslationWidth.Wide),
)
.toEqual(
['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']);
});
it('should return french wide list of days', () => {
expect(
getLocaleDayNames('fr-CA', FormStyle.Format, TranslationWidth.Wide),
)
.toEqual(['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']);
});
it('should return the full short list of days after manipulations', () => {
const days =
Array.from(getLocaleDayNames('en-US', FormStyle.Format, TranslationWidth.Short));
days.splice(2);
days.push('unexisting_day');
const newDays = getLocaleDayNames('en-US', FormStyle.Format, TranslationWidth.Short);
expect(newDays.length).toBe(7);
expect(newDays).toEqual(['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']);
});
});
describe('getLocaleMonthNames', () => {
it('should return english abbreviated list of month', () => {
expect(getLocaleMonthNames('en-US', FormStyle.Format, TranslationWidth.Abbreviated))
.toEqual([
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
]);
});
it('should return french abbreviated list of month', () => {
expect(getLocaleMonthNames('fr-CA', FormStyle.Format, TranslationWidth.Abbreviated))
.toEqual([
'janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'août', 'sept.', 'oct.',
'nov.', 'déc.'
]);
});
it('should return english wide list of month', () => {
expect(getLocaleMonthNames('en-US', FormStyle.Format, TranslationWidth.Wide)).toEqual([
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September',
'October', 'November', 'December'
]);
});
it('should return french wide list of month', () => {
expect(getLocaleMonthNames('fr-CA', FormStyle.Format, TranslationWidth.Wide)).toEqual([
'janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre',
'octobre', 'novembre', 'décembre'
]);
});
it('should return the full abbreviated list of month after manipulations', () => {
const month = Array.from(
getLocaleMonthNames('en-US', FormStyle.Format, TranslationWidth.Abbreviated));
month.splice(2);
month.push('unexisting_month');
const newMonth =
getLocaleMonthNames('en-US', FormStyle.Format, TranslationWidth.Abbreviated);
expect(newMonth.length).toBe(12);
expect(newMonth).toEqual(
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']);
});
});
});
}

View File

@ -129,7 +129,7 @@ import {SpyChangeDetectorRef} from '../spies';
reject = rej;
});
ref = new SpyChangeDetectorRef();
pipe = new AsyncPipe(<any>ref);
pipe = new AsyncPipe(ref as any);
});
describe('transform', () => {
@ -218,10 +218,17 @@ import {SpyChangeDetectorRef} from '../spies';
});
});
describe('undefined', () => {
it('should return null when given undefined', () => {
const pipe = new AsyncPipe(null as any);
expect(pipe.transform(undefined)).toEqual(null);
});
});
describe('other types', () => {
it('should throw when given an invalid object', () => {
const pipe = new AsyncPipe(null as any);
expect(() => pipe.transform(<any>'some bogus object')).toThrowError();
expect(() => pipe.transform('some bogus object' as any)).toThrowError();
});
});
});

View File

@ -25,8 +25,18 @@ import {LowerCasePipe, TitleCasePipe, UpperCasePipe} from '@angular/common';
expect(pipe.transform('BAr')).toEqual('bar');
});
it('should map null to null', () => {
expect(pipe.transform(null)).toEqual(null);
});
it('should map undefined to null', () => {
expect(pipe.transform(undefined)).toEqual(null);
});
it('should not support numbers', () => {
expect(() => pipe.transform(0 as any)).toThrowError();
});
it('should not support other objects', () => {
expect(() => pipe.transform(<any>{})).toThrowError();
expect(() => pipe.transform({} as any)).toThrowError();
});
});
@ -80,8 +90,18 @@ import {LowerCasePipe, TitleCasePipe, UpperCasePipe} from '@angular/common';
expect(pipe.transform('éric')).toEqual('Éric');
});
it('should map null to null', () => {
expect(pipe.transform(null)).toEqual(null);
});
it('should map undefined to null', () => {
expect(pipe.transform(undefined)).toEqual(null);
});
it('should not support numbers', () => {
expect(() => pipe.transform(0 as any)).toThrowError();
});
it('should not support other objects', () => {
expect(() => pipe.transform(<any>{})).toThrowError();
expect(() => pipe.transform({} as any)).toThrowError();
});
});
@ -101,8 +121,18 @@ import {LowerCasePipe, TitleCasePipe, UpperCasePipe} from '@angular/common';
expect(pipe.transform('bar')).toEqual('BAR');
});
it('should map null to null', () => {
expect(pipe.transform(null)).toEqual(null);
});
it('should map undefined to null', () => {
expect(pipe.transform(undefined)).toEqual(null);
});
it('should not support numbers', () => {
expect(() => pipe.transform(0 as any)).toThrowError();
});
it('should not support other objects', () => {
expect(() => pipe.transform(<any>{})).toThrowError();
expect(() => pipe.transform({} as any)).toThrowError();
});
});
}

View File

@ -59,12 +59,20 @@ import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_refle
expect(pipe.transform(Number.NaN)).toEqual(null);
});
it('should return null for null', () => {
expect(pipe.transform(null)).toEqual(null);
});
it('should return null for undefined', () => {
expect(pipe.transform(undefined)).toEqual(null);
});
it('should support ISO string without time', () => {
expect(() => pipe.transform(isoStringWithoutTime)).not.toThrow();
});
it('should not support other objects', () => {
expect(() => pipe.transform({})).toThrowError(/InvalidPipeArgument/);
expect(() => pipe.transform({} as any)).toThrowError(/InvalidPipeArgument/);
});
});
@ -85,6 +93,13 @@ import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_refle
expect(pipe.transform('2012-12-30T00:00:00', 'w')).toEqual('1');
expect(pipe.transform('2012-12-31T00:00:00', 'w')).toEqual('1');
});
it('should round milliseconds down to the nearest millisecond', () => {
expect(pipe.transform('2020-08-01T23:59:59.999', 'yyyy-MM-dd')).toEqual('2020-08-01');
expect(pipe.transform('2020-08-01T23:59:59.9999', 'yyyy-MM-dd, h:mm:ss SSS'))
.toEqual('2020-08-01, 11:59:59 999');
});
});
});
}

View File

@ -54,12 +54,17 @@ import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_refle
});
it('should use "" if value is undefined', () => {
const val = pipe.transform(void (0) as any, mapping);
const val = pipe.transform(undefined, mapping);
expect(val).toEqual('');
});
it('should use "" if value is null', () => {
const val = pipe.transform(null, mapping);
expect(val).toEqual('');
});
it('should not support bad arguments', () => {
expect(() => pipe.transform(0, <any>'hey')).toThrowError();
expect(() => pipe.transform(0, 'hey' as any)).toThrowError();
});
});
});

View File

@ -40,7 +40,7 @@ import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_refle
});
it('should throw on bad arguments', () => {
expect(() => pipe.transform('male', <any>'hey')).toThrowError();
expect(() => pipe.transform('male', 'hey' as any)).toThrowError();
});
});
});

View File

@ -17,12 +17,12 @@ describe('KeyValuePipe', () => {
});
it('should return null when given undefined', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
expect(pipe.transform(undefined as any)).toEqual(null);
expect(pipe.transform(undefined)).toEqual(null);
});
it('should return null for an unsupported type', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
const fn = () => {};
expect(pipe.transform(fn as any)).toEqual(null);
expect(pipe.transform(fn as any as null)).toEqual(null);
});
describe('object dictionary', () => {
it('should return empty array of an empty dictionary', () => {
@ -98,8 +98,9 @@ describe('KeyValuePipe', () => {
});
it('should order by numerical and alpha', () => {
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
const input = [[2, 1], [1, 1], ['b', 1], [0, 1], [3, 1], ['a', 1]];
expect(pipe.transform(new Map(input as any))).toEqual([
const input =
[[2, 1], [1, 1], ['b', 1], [0, 1], [3, 1], ['a', 1]] as Array<[number | string, number]>;
expect(pipe.transform(new Map(input))).toEqual([
{key: 0, value: 1}, {key: 1, value: 1}, {key: 2, value: 1}, {key: 3, value: 1},
{key: 'a', value: 1}, {key: 'b', value: 1}
]);

View File

@ -50,8 +50,20 @@ import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testin
expect(pipe.transform('1.1234')).toEqual('1.123');
});
it('should return null for NaN', () => {
expect(pipe.transform(Number.NaN)).toEqual(null);
});
it('should return null for null', () => {
expect(pipe.transform(null)).toEqual(null);
});
it('should return null for undefined', () => {
expect(pipe.transform(undefined)).toEqual(null);
});
it('should not support other objects', () => {
expect(() => pipe.transform({}))
expect(() => pipe.transform({} as any))
.toThrowError(
`InvalidPipeArgument: '[object Object] is not a number' for pipe 'DecimalPipe'`);
expect(() => pipe.transform('123abc'))
@ -82,8 +94,20 @@ import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testin
expect(pipe.transform(12.3456, '0.0-10')).toEqual('1,234.56%');
});
it('should return null for NaN', () => {
expect(pipe.transform(Number.NaN)).toEqual(null);
});
it('should return null for null', () => {
expect(pipe.transform(null)).toEqual(null);
});
it('should return null for undefined', () => {
expect(pipe.transform(undefined)).toEqual(null);
});
it('should not support other objects', () => {
expect(() => pipe.transform({}))
expect(() => pipe.transform({} as any))
.toThrowError(
`InvalidPipeArgument: '[object Object] is not a number' for pipe 'PercentPipe'`);
});
@ -125,8 +149,20 @@ import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testin
expect(pipe.transform(5.1234, 'USD', 'Custom name')).toEqual('Custom name5.12');
});
it('should return null for NaN', () => {
expect(pipe.transform(Number.NaN)).toEqual(null);
});
it('should return null for null', () => {
expect(pipe.transform(null)).toEqual(null);
});
it('should return null for undefined', () => {
expect(pipe.transform(undefined)).toEqual(null);
});
it('should not support other objects', () => {
expect(() => pipe.transform({}))
expect(() => pipe.transform({} as any))
.toThrowError(
`InvalidPipeArgument: '[object Object] is not a number' for pipe 'CurrencyPipe'`);
});

View File

@ -47,8 +47,8 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
expect(pipe.transform(null, 1)).toBe(null);
});
it('should return undefined if the value is undefined', () => {
expect(pipe.transform(undefined, 1)).toBe(undefined);
it('should return null if the value is undefined', () => {
expect(pipe.transform(undefined, 1)).toBe(null);
});
it('should return all items after START index when START is positive and END is omitted',

View File

@ -15,21 +15,30 @@ describe('BrowserViewportScroller', () => {
let windowSpy: any;
beforeEach(() => {
windowSpy = jasmine.createSpyObj('window', ['history']);
windowSpy.scrollTo = 1;
windowSpy = jasmine.createSpyObj('window', ['history', 'scrollTo']);
windowSpy.history.scrollRestoration = 'auto';
documentSpy = jasmine.createSpyObj('document', ['getElementById', 'getElementsByName']);
scroller = new BrowserViewportScroller(documentSpy, windowSpy, null!);
});
describe('setHistoryScrollRestoration', () => {
it('should not crash when scrollRestoration is not writable', () => {
function createNonWritableScrollRestoration() {
Object.defineProperty(windowSpy.history, 'scrollRestoration', {
value: 'auto',
configurable: true,
});
}
it('should not crash when scrollRestoration is not writable', () => {
createNonWritableScrollRestoration();
expect(() => scroller.setHistoryScrollRestoration('manual')).not.toThrow();
});
it('should still allow scrolling if scrollRestoration is not writable', () => {
createNonWritableScrollRestoration();
scroller.scrollToPosition([10, 10]);
expect(windowSpy.scrollTo as jasmine.Spy).toHaveBeenCalledWith(10, 10);
});
});
describe('scrollToAnchor', () => {

View File

@ -689,7 +689,7 @@ export class $locationShim {
*
* This method is supported only in HTML5 mode and only in browsers supporting
* the HTML5 History API methods such as `pushState` and `replaceState`. If you need to support
* older browsers (like IE9 or Android < 4.0), don't use this method.
* older browsers (like Android < 4.0), don't use this method.
*
*/
state(): unknown;

View File

@ -309,10 +309,7 @@ function toKeyValue(obj: {[k: string]: unknown}) {
* Logic from https://github.com/angular/angular.js/blob/864c7f0/src/Angular.js#L1437
*/
function encodeUriSegment(val: string) {
return encodeUriQuery(val, true)
.replace(/%26/gi, '&')
.replace(/%3D/gi, '=')
.replace(/%2B/gi, '+');
return encodeUriQuery(val, true).replace(/%26/g, '&').replace(/%3D/gi, '=').replace(/%2B/gi, '+');
}
@ -331,7 +328,7 @@ function encodeUriSegment(val: string) {
*/
function encodeUriQuery(val: string, pctEncodeSpaces: boolean = false) {
return encodeURIComponent(val)
.replace(/%40/gi, '@')
.replace(/%40/g, '@')
.replace(/%3A/gi, ':')
.replace(/%24/g, '$')
.replace(/%2C/gi, ',')

View File

@ -1,7 +1,6 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "pkg_npm", "ts_api_guardian_test", "ts_library")
load("@npm_bazel_typescript//:index.bzl", "ts_config")
load("//tools:defaults.bzl", "pkg_npm", "ts_api_guardian_test", "ts_config", "ts_library")
ts_config(
name = "tsconfig",
@ -32,6 +31,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/shims",
"//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/typecheck",
"@npm//@bazel/typescript",
"@npm//@types/node",

View File

@ -62,5 +62,10 @@ nodejs_test(
"//packages/router:npm_package",
] + glob(["**/*"]),
entry_point = "test.js",
tags = ["no-ivy-aot"],
tags = [
# TODO(josephperrott): reenable or remove test after investigating the cause of failures
# on windows CI runs.
"manual",
"no-ivy-aot",
],
)

View File

@ -0,0 +1,15 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "linker",
srcs = ["index.ts"] + glob([
"src/**/*.ts",
]),
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/translator",
"@npm//typescript",
],
)

View File

@ -0,0 +1,18 @@
# Angular Linker
This package contains a `FileLinker` and supporting code to be able to "link" partial declarations of components, directives, etc in libraries to produce the full definitions.
The partial declaration format allows library packages to be published to npm without exposing the underlying Ivy instructions.
The tooling here allows application build tools (e.g. CLI) to produce fully compiled components, directives, etc at the point when the application is bundled.
These linked files can be cached outside `node_modules` so it does not suffer from problems of mutating packages in `node_modules`.
Generally this tooling will be wrapped in a transpiler specific plugin, such as the provided [Babel plugin](./babel).
## Unit Testing
The unit tests are built and run using Bazel:
```bash
yarn bazel test //packages/compiler-cli/linker/test
```

View File

@ -0,0 +1,19 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "babel",
srcs = ["index.ts"] + glob([
"src/**/*.ts",
]),
deps = [
"//packages/compiler",
"//packages/compiler-cli/linker",
"//packages/compiler-cli/src/ngtsc/translator",
"@npm//@babel/core",
"@npm//@babel/types",
"@npm//@types/babel__core",
"@npm//@types/babel__traverse",
],
)

View File

@ -0,0 +1,12 @@
# Angular linker - Babel plugin
This package contains a Babel plugin that can be used to find and link partially compiled declarations in library source code.
See the [linker package README](../README.md) for more information.
## Unit Testing
The unit tests are built and run using Bazel:
```bash
yarn bazel test //packages/compiler-cli/linker/babel/test
```

View File

@ -0,0 +1,8 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export {createEs2015LinkerPlugin} from './src/es2015_linker_plugin';

View File

@ -0,0 +1,164 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as t from '@babel/types';
import {assert} from '../../../../linker';
import {AstFactory, BinaryOperator, LeadingComment, ObjectLiteralProperty, SourceMapRange, TemplateLiteral, VariableDeclarationType} from '../../../../src/ngtsc/translator';
/**
* A Babel flavored implementation of the AstFactory.
*/
export class BabelAstFactory implements AstFactory<t.Statement, t.Expression> {
attachComments(statement: t.Statement, leadingComments: LeadingComment[]): void {
// We must process the comments in reverse because `t.addComment()` will add new ones in front.
for (let i = leadingComments.length - 1; i >= 0; i--) {
const comment = leadingComments[i];
t.addComment(statement, 'leading', comment.toString(), !comment.multiline);
}
}
createArrayLiteral = t.arrayExpression;
createAssignment(target: t.Expression, value: t.Expression): t.Expression {
assert(target, isLExpression, 'must be a left hand side expression');
return t.assignmentExpression('=', target, value);
}
createBinaryExpression(
leftOperand: t.Expression, operator: BinaryOperator,
rightOperand: t.Expression): t.Expression {
switch (operator) {
case '&&':
case '||':
return t.logicalExpression(operator, leftOperand, rightOperand);
default:
return t.binaryExpression(operator, leftOperand, rightOperand);
}
}
createBlock = t.blockStatement;
createCallExpression(callee: t.Expression, args: t.Expression[], pure: boolean): t.Expression {
const call = t.callExpression(callee, args);
if (pure) {
t.addComment(call, 'leading', ' @__PURE__ ', /* line */ false);
}
return call;
}
createConditional = t.conditionalExpression;
createElementAccess(expression: t.Expression, element: t.Expression): t.Expression {
return t.memberExpression(expression, element, /* computed */ true);
}
createExpressionStatement = t.expressionStatement;
createFunctionDeclaration(functionName: string, parameters: string[], body: t.Statement):
t.Statement {
assert(body, t.isBlockStatement, 'a block');
return t.functionDeclaration(
t.identifier(functionName), parameters.map(param => t.identifier(param)), body);
}
createFunctionExpression(functionName: string|null, parameters: string[], body: t.Statement):
t.Expression {
assert(body, t.isBlockStatement, 'a block');
const name = functionName !== null ? t.identifier(functionName) : null;
return t.functionExpression(name, parameters.map(param => t.identifier(param)), body);
}
createIdentifier = t.identifier;
createIfStatement = t.ifStatement;
createLiteral(value: string|number|boolean|null|undefined): t.Expression {
if (typeof value === 'string') {
return t.stringLiteral(value);
} else if (typeof value === 'number') {
return t.numericLiteral(value);
} else if (typeof value === 'boolean') {
return t.booleanLiteral(value);
} else if (value === undefined) {
return t.identifier('undefined');
} else if (value === null) {
return t.nullLiteral();
} else {
throw new Error(`Invalid literal: ${value} (${typeof value})`);
}
}
createNewExpression = t.newExpression;
createObjectLiteral(properties: ObjectLiteralProperty<t.Expression>[]): t.Expression {
return t.objectExpression(properties.map(prop => {
const key =
prop.quoted ? t.stringLiteral(prop.propertyName) : t.identifier(prop.propertyName);
return t.objectProperty(key, prop.value);
}));
}
createParenthesizedExpression = t.parenthesizedExpression;
createPropertyAccess(expression: t.Expression, propertyName: string): t.Expression {
return t.memberExpression(expression, t.identifier(propertyName), /* computed */ false);
}
createReturnStatement = t.returnStatement;
createTaggedTemplate(tag: t.Expression, template: TemplateLiteral<t.Expression>): t.Expression {
const elements = template.elements.map(
(element, i) => this.setSourceMapRange(
t.templateElement(element, i === template.elements.length - 1), element.range));
return t.taggedTemplateExpression(tag, t.templateLiteral(elements, template.expressions));
}
createThrowStatement = t.throwStatement;
createTypeOfExpression(expression: t.Expression): t.Expression {
return t.unaryExpression('typeof', expression);
}
createUnaryExpression = t.unaryExpression;
createVariableDeclaration(
variableName: string, initializer: t.Expression|null,
type: VariableDeclarationType): t.Statement {
return t.variableDeclaration(
type, [t.variableDeclarator(t.identifier(variableName), initializer)]);
}
setSourceMapRange<T extends t.Statement|t.Expression|t.TemplateElement>(
node: T, sourceMapRange: SourceMapRange|null): T {
if (sourceMapRange === null) {
return node;
}
// Note that the linker only works on a single file at a time, so there is no need to track the
// filename. Babel will just use the current filename in the source-map.
node.loc = {
start: {
line: sourceMapRange.start.line + 1, // lines are 1-based in Babel.
column: sourceMapRange.start.column,
},
end: {
line: sourceMapRange.end.line + 1, // lines are 1-based in Babel.
column: sourceMapRange.end.column,
},
};
node.start = sourceMapRange.start.offset;
node.end = sourceMapRange.end.offset;
return node;
}
}
function isLExpression(expr: t.Expression): expr is Extract<t.LVal, t.Expression> {
// Some LVal types are not expressions, which prevents us from using `t.isLVal()`
// directly with `assert()`.
return t.isLVal(expr);
}

View File

@ -0,0 +1,140 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as t from '@babel/types';
import {assert, AstHost, FatalLinkerError, Range} from '../../../../linker';
/**
* This implementation of `AstHost` is able to get information from Babel AST nodes.
*/
export class BabelAstHost implements AstHost<t.Expression> {
getSymbolName(node: t.Expression): string|null {
if (t.isIdentifier(node)) {
return node.name;
} else if (t.isMemberExpression(node) && t.isIdentifier(node.property)) {
return node.property.name;
} else {
return null;
}
}
isStringLiteral = t.isStringLiteral;
parseStringLiteral(str: t.Expression): string {
assert(str, t.isStringLiteral, 'a string literal');
return str.value;
}
isNumericLiteral = t.isNumericLiteral;
parseNumericLiteral(num: t.Expression): number {
assert(num, t.isNumericLiteral, 'a numeric literal');
return num.value;
}
isBooleanLiteral = t.isBooleanLiteral;
parseBooleanLiteral(bool: t.Expression): boolean {
assert(bool, t.isBooleanLiteral, 'a boolean literal');
return bool.value;
}
isArrayLiteral = t.isArrayExpression;
parseArrayLiteral(array: t.Expression): t.Expression[] {
assert(array, t.isArrayExpression, 'an array literal');
return array.elements.map(element => {
assert(element, isNotEmptyElement, 'element in array not to be empty');
assert(element, isNotSpreadElement, 'element in array not to use spread syntax');
return element;
});
}
isObjectLiteral = t.isObjectExpression;
parseObjectLiteral(obj: t.Expression): Map<string, t.Expression> {
assert(obj, t.isObjectExpression, 'an object literal');
const result = new Map<string, t.Expression>();
for (const property of obj.properties) {
assert(property, t.isObjectProperty, 'a property assignment');
assert(property.value, t.isExpression, 'an expression');
assert(property.key, isPropertyName, 'a property name');
const key = t.isIdentifier(property.key) ? property.key.name : property.key.value;
result.set(key, property.value);
}
return result;
}
isFunctionExpression(node: t.Expression): node is Extract<t.Function, t.Expression> {
return t.isFunction(node);
}
parseReturnValue(fn: t.Expression): t.Expression {
assert(fn, this.isFunctionExpression, 'a function');
if (!t.isBlockStatement(fn.body)) {
// it is a simple array function expression: `(...) => expr`
return fn.body;
}
// it is a function (arrow or normal) with a body. E.g.:
// * `(...) => { stmt; ... }`
// * `function(...) { stmt; ... }`
if (fn.body.body.length !== 1) {
throw new FatalLinkerError(
fn.body, 'Unsupported syntax, expected a function body with a single return statement.');
}
const stmt = fn.body.body[0];
assert(stmt, t.isReturnStatement, 'a function body with a single return statement');
if (stmt.argument === null) {
throw new FatalLinkerError(stmt, 'Unsupported syntax, expected function to return a value.');
}
return stmt.argument;
}
getRange(node: t.Expression): Range {
if (node.loc == null || node.start === null || node.end === null) {
throw new FatalLinkerError(
node, 'Unable to read range for node - it is missing location information.');
}
return {
startLine: node.loc.start.line - 1, // Babel lines are 1-based
startCol: node.loc.start.column,
startPos: node.start,
endPos: node.end,
};
}
}
/**
* Return true if the expression does not represent an empty element in an array literal.
* For example in `[,foo]` the first element is "empty".
*/
function isNotEmptyElement(e: t.Expression|t.SpreadElement|null): e is t.Expression|
t.SpreadElement {
return e !== null;
}
/**
* Return true if the expression is not a spread element of an array literal.
* For example in `[x, ...rest]` the `...rest` expression is a spread element.
*/
function isNotSpreadElement(e: t.Expression|t.SpreadElement): e is t.Expression {
return !t.isSpreadElement(e);
}
/**
* Return true if the expression can be considered a text based property name.
*/
function isPropertyName(e: t.Expression): e is t.Identifier|t.StringLiteral|t.NumericLiteral {
return t.isIdentifier(e) || t.isStringLiteral(e) || t.isNumericLiteral(e);
}

View File

@ -0,0 +1,67 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {NodePath, Scope} from '@babel/traverse';
import * as t from '@babel/types';
import {DeclarationScope} from '../../../linker';
export type ConstantScopePath = NodePath<t.Function|t.Program>;
/**
* This class represents the lexical scope of a partial declaration in Babel source code.
*
* Its only responsibility is to compute a reference object for the scope of shared constant
* statements that will be generated during partial linking.
*/
export class BabelDeclarationScope implements DeclarationScope<ConstantScopePath, t.Expression> {
/**
* Construct a new `BabelDeclarationScope`.
*
* @param declarationScope the Babel scope containing the declaration call expression.
*/
constructor(private declarationScope: Scope) {}
/**
* Compute the Babel `NodePath` that can be used to reference the lexical scope where any
* shared constant statements would be inserted.
*
* There will only be a shared constant scope if the expression is in an ECMAScript module, or a
* UMD module. Otherwise `null` is returned to indicate that constant statements must be emitted
* locally to the generated linked definition, to avoid polluting the global scope.
*
* @param expression the expression that points to the Angular core framework import.
*/
getConstantScopeRef(expression: t.Expression): ConstantScopePath|null {
// If the expression is of the form `a.b.c` then we want to get the far LHS (e.g. `a`).
let bindingExpression = expression;
while (t.isMemberExpression(bindingExpression)) {
bindingExpression = bindingExpression.object;
}
if (!t.isIdentifier(bindingExpression)) {
return null;
}
// The binding of the expression is where this identifier was declared.
// This could be a variable declaration, an import namespace or a function parameter.
const binding = this.declarationScope.getBinding(bindingExpression.name);
if (binding === undefined) {
return null;
}
// We only support shared constant statements if the binding was in a UMD module (i.e. declared
// within a `t.Function`) or an ECMASCript module (i.e. declared at the top level of a
// `t.Program` that is marked as a module).
const path = binding.scope.path;
if (!path.isFunctionParent() && !(path.isProgram() && path.node.sourceType === 'module')) {
return null;
}
return path;
}
}

View File

@ -0,0 +1,167 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {PluginObj} from '@babel/core';
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {FileLinker, isFatalLinkerError, LinkerEnvironment, LinkerOptions} from '../../../linker';
import {BabelAstFactory} from './ast/babel_ast_factory';
import {BabelAstHost} from './ast/babel_ast_host';
import {BabelDeclarationScope, ConstantScopePath} from './babel_declaration_scope';
/**
* Create a Babel plugin that visits the program, identifying and linking partial declarations.
*
* The plugin delegates most of its work to a generic `FileLinker` for each file (`t.Program` in
* Babel) that is visited.
*/
export function createEs2015LinkerPlugin(options: Partial<LinkerOptions> = {}): PluginObj {
let fileLinker: FileLinker<ConstantScopePath, t.Statement, t.Expression>|null = null;
const linkerEnvironment = LinkerEnvironment.create<t.Statement, t.Expression>(
new BabelAstHost(), new BabelAstFactory(), options);
return {
visitor: {
Program: {
/**
* Create a new `FileLinker` as we enter each file (`t.Program` in Babel).
*/
enter(path: NodePath<t.Program>): void {
assertNull(fileLinker);
const file: BabelFile = path.hub.file;
fileLinker = new FileLinker(linkerEnvironment, file.opts.filename ?? '', file.code);
},
/**
* On exiting the file, insert any shared constant statements that were generated during
* linking of the partial declarations.
*/
exit(): void {
assertNotNull(fileLinker);
for (const {constantScope, statements} of fileLinker.getConstantStatements()) {
insertStatements(constantScope, statements);
}
fileLinker = null;
}
},
/**
* Test each call expression to see if it is a partial declaration; it if is then replace it
* with the results of linking the declaration.
*/
CallExpression(call: NodePath<t.CallExpression>): void {
try {
assertNotNull(fileLinker);
const callee = call.node.callee;
if (!t.isExpression(callee)) {
return;
}
const calleeName = linkerEnvironment.host.getSymbolName(callee);
if (calleeName === null) {
return;
}
const args = call.node.arguments;
if (!fileLinker.isPartialDeclaration(calleeName) || !isExpressionArray(args)) {
return;
}
const declarationScope = new BabelDeclarationScope(call.scope);
const replacement = fileLinker.linkPartialDeclaration(calleeName, args, declarationScope);
call.replaceWith(replacement);
} catch (e) {
const node = isFatalLinkerError(e) ? e.node as t.Node : call.node;
throw buildCodeFrameError(call.hub.file, e.message, node);
}
}
}
};
}
/**
* Insert the `statements` at the location defined by `path`.
*
* The actual insertion strategy depends upon the type of the `path`.
*/
function insertStatements(path: ConstantScopePath, statements: t.Statement[]): void {
if (path.isFunction()) {
insertIntoFunction(path, statements);
} else if (path.isProgram()) {
insertIntoProgram(path, statements);
}
}
/**
* Insert the `statements` at the top of the body of the `fn` function.
*/
function insertIntoFunction(fn: NodePath<t.Function>, statements: t.Statement[]): void {
const body = fn.get('body');
body.unshiftContainer('body', statements);
}
/**
* Insert the `statements` at the top of the `program`, below any import statements.
*/
function insertIntoProgram(program: NodePath<t.Program>, statements: t.Statement[]): void {
const body = program.get('body');
const importStatements = body.filter(statement => statement.isImportDeclaration());
if (importStatements.length === 0) {
program.unshiftContainer('body', statements);
} else {
importStatements[importStatements.length - 1].insertAfter(statements);
}
}
/**
* Return true if all the `nodes` are Babel expressions.
*/
function isExpressionArray(nodes: t.Node[]): nodes is t.Expression[] {
return nodes.every(node => t.isExpression(node));
}
/**
* Assert that the given `obj` is `null`.
*/
function assertNull<T>(obj: T|null): asserts obj is null {
if (obj !== null) {
throw new Error('BUG - expected `obj` to be null');
}
}
/**
* Assert that the given `obj` is not `null`.
*/
function assertNotNull<T>(obj: T|null): asserts obj is T {
if (obj === null) {
throw new Error('BUG - expected `obj` not to be null');
}
}
/**
* Create a string representation of an error that includes the code frame of the `node`.
*/
function buildCodeFrameError(file: BabelFile, message: string, node: t.Node): string {
const filename = file.opts.filename || '(unknown file)';
const error = file.buildCodeFrameError(node, message);
return `${filename}: ${error.message}`;
}
/**
* This interface is making up for the fact that the Babel typings for `NodePath.hub.file` are
* lacking.
*/
interface BabelFile {
code: string;
opts: {filename?: string;};
buildCodeFrameError(node: t.Node, message: string): Error;
}

View File

@ -0,0 +1,36 @@
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "test_lib",
testonly = True,
srcs = glob([
"**/*.ts",
]),
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/linker",
"//packages/compiler-cli/linker/babel",
"//packages/compiler-cli/src/ngtsc/translator",
"@npm//@babel/core",
"@npm//@babel/generator",
"@npm//@babel/parser",
"@npm//@babel/template",
"@npm//@babel/traverse",
"@npm//@babel/types",
"@npm//@types/babel__core",
"@npm//@types/babel__generator",
"@npm//@types/babel__template",
"@npm//@types/babel__traverse",
],
)
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular_es5"],
deps = [
":test_lib",
],
)

View File

@ -0,0 +1,382 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {leadingComment} from '@angular/compiler';
import generate from '@babel/generator';
import {expression, statement} from '@babel/template';
import * as t from '@babel/types';
import {BabelAstFactory} from '../../src/ast/babel_ast_factory';
describe('BabelAstFactory', () => {
let factory: BabelAstFactory;
beforeEach(() => factory = new BabelAstFactory());
describe('attachComments()', () => {
it('should add the comments to the given statement', () => {
const stmt = statement.ast`x = 10;`;
factory.attachComments(
stmt, [leadingComment('comment 1', true), leadingComment('comment 2', false)]);
expect(generate(stmt).code).toEqual([
'/* comment 1 */',
'//comment 2',
'x = 10;',
].join('\n'));
});
});
describe('createArrayLiteral()', () => {
it('should create an array node containing the provided expressions', () => {
const expr1 = expression.ast`42`;
const expr2 = expression.ast`"moo"`;
const array = factory.createArrayLiteral([expr1, expr2]);
expect(generate(array).code).toEqual('[42, "moo"]');
});
});
describe('createAssignment()', () => {
it('should create an assignment node using the target and value expressions', () => {
const target = expression.ast`x`;
const value = expression.ast`42`;
const assignment = factory.createAssignment(target, value);
expect(generate(assignment).code).toEqual('x = 42');
});
});
describe('createBinaryExpression()', () => {
it('should create a binary operation node using the left and right expressions', () => {
const left = expression.ast`17`;
const right = expression.ast`42`;
const expr = factory.createBinaryExpression(left, '+', right);
expect(generate(expr).code).toEqual('17 + 42');
});
it('should create a binary operation node for logical operators', () => {
const left = expression.ast`17`;
const right = expression.ast`42`;
const expr = factory.createBinaryExpression(left, '&&', right);
expect(t.isLogicalExpression(expr)).toBe(true);
expect(generate(expr).code).toEqual('17 && 42');
});
});
describe('createBlock()', () => {
it('should create a block statement containing the given statements', () => {
const stmt1 = statement.ast`x = 10`;
const stmt2 = statement.ast`y = 20`;
const block = factory.createBlock([stmt1, stmt2]);
expect(generate(block).code).toEqual([
'{',
' x = 10;',
' y = 20;',
'}',
].join('\n'));
});
});
describe('createCallExpression()', () => {
it('should create a call on the `callee` with the given `args`', () => {
const callee = expression.ast`foo`;
const arg1 = expression.ast`42`;
const arg2 = expression.ast`"moo"`;
const call = factory.createCallExpression(callee, [arg1, arg2], false);
expect(generate(call).code).toEqual('foo(42, "moo")');
});
it('should create a call marked with a PURE comment if `pure` is true', () => {
const callee = expression.ast`foo`;
const arg1 = expression.ast`42`;
const arg2 = expression.ast`"moo"`;
const call = factory.createCallExpression(callee, [arg1, arg2], true);
expect(generate(call).code).toEqual(['/* @__PURE__ */', 'foo(42, "moo")'].join('\n'));
});
});
describe('createConditional()', () => {
it('should create a condition expression', () => {
const test = expression.ast`!test`;
const thenExpr = expression.ast`42`;
const elseExpr = expression.ast`"moo"`;
const conditional = factory.createConditional(test, thenExpr, elseExpr);
expect(generate(conditional).code).toEqual('!test ? 42 : "moo"');
});
});
describe('createElementAccess()', () => {
it('should create an expression accessing the element of an array/object', () => {
const expr = expression.ast`obj`;
const element = expression.ast`"moo"`;
const access = factory.createElementAccess(expr, element);
expect(generate(access).code).toEqual('obj["moo"]');
});
});
describe('createExpressionStatement()', () => {
it('should create a statement node from the given expression', () => {
const expr = expression.ast`x = 10`;
const stmt = factory.createExpressionStatement(expr);
expect(t.isStatement(stmt)).toBe(true);
expect(generate(stmt).code).toEqual('x = 10;');
});
});
describe('createFunctionDeclaration()', () => {
it('should create a function declaration node with the given name, parameters and body statements',
() => {
const stmts = statement.ast`{x = 10; y = 20;}`;
const fn = factory.createFunctionDeclaration('foo', ['arg1', 'arg2'], stmts);
expect(generate(fn).code).toEqual([
'function foo(arg1, arg2) {',
' x = 10;',
' y = 20;',
'}',
].join('\n'));
});
});
describe('createFunctionExpression()', () => {
it('should create a function expression node with the given name, parameters and body statements',
() => {
const stmts = statement.ast`{x = 10; y = 20;}`;
const fn = factory.createFunctionExpression('foo', ['arg1', 'arg2'], stmts);
expect(t.isStatement(fn)).toBe(false);
expect(generate(fn).code).toEqual([
'function foo(arg1, arg2) {',
' x = 10;',
' y = 20;',
'}',
].join('\n'));
});
it('should create an anonymous function expression node if the name is null', () => {
const stmts = statement.ast`{x = 10; y = 20;}`;
const fn = factory.createFunctionExpression(null, ['arg1', 'arg2'], stmts);
expect(generate(fn).code).toEqual([
'function (arg1, arg2) {',
' x = 10;',
' y = 20;',
'}',
].join('\n'));
});
});
describe('createIdentifier()', () => {
it('should create an identifier with the given name', () => {
const id = factory.createIdentifier('someId') as t.Identifier;
expect(t.isIdentifier(id)).toBe(true);
expect(id.name).toEqual('someId');
});
});
describe('createIfStatement()', () => {
it('should create an if-else statement', () => {
const test = expression.ast`!test`;
const thenStmt = statement.ast`x = 10;`;
const elseStmt = statement.ast`x = 42;`;
const ifStmt = factory.createIfStatement(test, thenStmt, elseStmt);
expect(generate(ifStmt).code).toEqual('if (!test) x = 10;else x = 42;');
});
it('should create an if statement if the else expression is null', () => {
const test = expression.ast`!test`;
const thenStmt = statement.ast`x = 10;`;
const ifStmt = factory.createIfStatement(test, thenStmt, null);
expect(generate(ifStmt).code).toEqual('if (!test) x = 10;');
});
});
describe('createLiteral()', () => {
it('should create a string literal', () => {
const literal = factory.createLiteral('moo');
expect(t.isStringLiteral(literal)).toBe(true);
expect(generate(literal).code).toEqual('"moo"');
});
it('should create a number literal', () => {
const literal = factory.createLiteral(42);
expect(t.isNumericLiteral(literal)).toBe(true);
expect(generate(literal).code).toEqual('42');
});
it('should create a number literal for `NaN`', () => {
const literal = factory.createLiteral(NaN);
expect(t.isNumericLiteral(literal)).toBe(true);
expect(generate(literal).code).toEqual('NaN');
});
it('should create a boolean literal', () => {
const literal = factory.createLiteral(true);
expect(t.isBooleanLiteral(literal)).toBe(true);
expect(generate(literal).code).toEqual('true');
});
it('should create an `undefined` literal', () => {
const literal = factory.createLiteral(undefined);
expect(t.isIdentifier(literal)).toBe(true);
expect(generate(literal).code).toEqual('undefined');
});
it('should create a null literal', () => {
const literal = factory.createLiteral(null);
expect(t.isNullLiteral(literal)).toBe(true);
expect(generate(literal).code).toEqual('null');
});
});
describe('createNewExpression()', () => {
it('should create a `new` operation on the constructor `expression` with the given `args`',
() => {
const expr = expression.ast`Foo`;
const arg1 = expression.ast`42`;
const arg2 = expression.ast`"moo"`;
const call = factory.createNewExpression(expr, [arg1, arg2]);
expect(generate(call).code).toEqual('new Foo(42, "moo")');
});
});
describe('createObjectLiteral()', () => {
it('should create an object literal node, with the given properties', () => {
const prop1 = expression.ast`42`;
const prop2 = expression.ast`"moo"`;
const obj = factory.createObjectLiteral([
{propertyName: 'prop1', value: prop1, quoted: false},
{propertyName: 'prop2', value: prop2, quoted: true},
]);
expect(generate(obj).code).toEqual([
'{',
' prop1: 42,',
' "prop2": "moo"',
'}',
].join('\n'));
});
});
describe('createParenthesizedExpression()', () => {
it('should add parentheses around the given expression', () => {
const expr = expression.ast`a + b`;
const paren = factory.createParenthesizedExpression(expr);
expect(generate(paren).code).toEqual('(a + b)');
});
});
describe('createPropertyAccess()', () => {
it('should create a property access expression node', () => {
const expr = expression.ast`obj`;
const access = factory.createPropertyAccess(expr, 'moo');
expect(generate(access).code).toEqual('obj.moo');
});
});
describe('createReturnStatement()', () => {
it('should create a return statement returning the given expression', () => {
const expr = expression.ast`42`;
const returnStmt = factory.createReturnStatement(expr);
expect(generate(returnStmt).code).toEqual('return 42;');
});
it('should create a void return statement if the expression is null', () => {
const returnStmt = factory.createReturnStatement(null);
expect(generate(returnStmt).code).toEqual('return;');
});
});
describe('createTaggedTemplate()', () => {
it('should create a tagged template node from the tag, elements and expressions', () => {
const elements = [
{raw: 'raw1', cooked: 'cooked1', range: null},
{raw: 'raw2', cooked: 'cooked2', range: null},
{raw: 'raw3', cooked: 'cooked3', range: null},
];
const expressions = [
expression.ast`42`,
expression.ast`"moo"`,
];
const tag = expression.ast`tagFn`;
const template = factory.createTaggedTemplate(tag, {elements, expressions});
expect(generate(template).code).toEqual('tagFn`raw1${42}raw2${"moo"}raw3`');
});
});
describe('createThrowStatement()', () => {
it('should create a throw statement, throwing the given expression', () => {
const expr = expression.ast`new Error("bad")`;
const throwStmt = factory.createThrowStatement(expr);
expect(generate(throwStmt).code).toEqual('throw new Error("bad");');
});
});
describe('createTypeOfExpression()', () => {
it('should create a typeof expression node', () => {
const expr = expression.ast`42`;
const typeofExpr = factory.createTypeOfExpression(expr);
expect(generate(typeofExpr).code).toEqual('typeof 42');
});
});
describe('createUnaryExpression()', () => {
it('should create a unary expression with the operator and operand', () => {
const expr = expression.ast`value`;
const unaryExpr = factory.createUnaryExpression('!', expr);
expect(generate(unaryExpr).code).toEqual('!value');
});
});
describe('createVariableDeclaration()', () => {
it('should create a variable declaration statement node for the given variable name and initializer',
() => {
const initializer = expression.ast`42`;
const varDecl = factory.createVariableDeclaration('foo', initializer, 'let');
expect(generate(varDecl).code).toEqual('let foo = 42;');
});
it('should create a constant declaration statement node for the given variable name and initializer',
() => {
const initializer = expression.ast`42`;
const varDecl = factory.createVariableDeclaration('foo', initializer, 'const');
expect(generate(varDecl).code).toEqual('const foo = 42;');
});
it('should create a downleveled variable declaration statement node for the given variable name and initializer',
() => {
const initializer = expression.ast`42`;
const varDecl = factory.createVariableDeclaration('foo', initializer, 'var');
expect(generate(varDecl).code).toEqual('var foo = 42;');
});
it('should create an uninitialized variable declaration statement node for the given variable name and a null initializer',
() => {
const varDecl = factory.createVariableDeclaration('foo', null, 'let');
expect(generate(varDecl).code).toEqual('let foo;');
});
});
describe('setSourceMapRange()', () => {
it('should attach the `sourceMapRange` to the given `node`', () => {
const expr = expression.ast`42`;
expect(expr.loc).toBeUndefined();
expect(expr.start).toBeUndefined();
expect(expr.end).toBeUndefined();
factory.setSourceMapRange(expr, {
start: {line: 0, column: 1, offset: 1},
end: {line: 2, column: 3, offset: 15},
content: '-****\n*****\n****',
url: 'original.ts'
});
// Lines are 1-based in Babel.
expect(expr.loc).toEqual({
start: {line: 1, column: 1},
end: {line: 3, column: 3},
});
expect(expr.start).toEqual(1);
expect(expr.end).toEqual(15);
});
});
});

View File

@ -0,0 +1,305 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as t from '@babel/types';
import template from '@babel/template';
import {parse} from '@babel/parser';
import {BabelAstHost} from '../../src/ast/babel_ast_host';
describe('BabelAstHost', () => {
let host: BabelAstHost;
beforeEach(() => host = new BabelAstHost());
describe('getSymbolName()', () => {
it('should return the name of an identifier', () => {
expect(host.getSymbolName(expr('someIdentifier'))).toEqual('someIdentifier');
});
it('should return the name of an identifier at the end of a property access chain', () => {
expect(host.getSymbolName(expr('a.b.c.someIdentifier'))).toEqual('someIdentifier');
});
it('should return null if the expression has no identifier', () => {
expect(host.getSymbolName(expr('42'))).toBe(null);
});
});
describe('isStringLiteral()', () => {
it('should return true if the expression is a string literal', () => {
expect(host.isStringLiteral(expr('"moo"'))).toBe(true);
expect(host.isStringLiteral(expr('\'moo\''))).toBe(true);
});
it('should return false if the expression is not a string literal', () => {
expect(host.isStringLiteral(expr('true'))).toBe(false);
expect(host.isStringLiteral(expr('someIdentifier'))).toBe(false);
expect(host.isStringLiteral(expr('42'))).toBe(false);
expect(host.isStringLiteral(expr('{}'))).toBe(false);
expect(host.isStringLiteral(expr('[]'))).toBe(false);
expect(host.isStringLiteral(expr('null'))).toBe(false);
expect(host.isStringLiteral(expr('\'a\' + \'b\''))).toBe(false);
});
it('should return false if the expression is a template string', () => {
expect(host.isStringLiteral(expr('\`moo\`'))).toBe(false);
});
});
describe('parseStringLiteral()', () => {
it('should extract the string value', () => {
expect(host.parseStringLiteral(expr('"moo"'))).toEqual('moo');
expect(host.parseStringLiteral(expr('\'moo\''))).toEqual('moo');
});
it('should error if the value is not a string literal', () => {
expect(() => host.parseStringLiteral(expr('42')))
.toThrowError('Unsupported syntax, expected a string literal.');
});
});
describe('isNumericLiteral()', () => {
it('should return true if the expression is a number literal', () => {
expect(host.isNumericLiteral(expr('42'))).toBe(true);
});
it('should return false if the expression is not a number literal', () => {
expect(host.isStringLiteral(expr('true'))).toBe(false);
expect(host.isNumericLiteral(expr('"moo"'))).toBe(false);
expect(host.isNumericLiteral(expr('\'moo\''))).toBe(false);
expect(host.isNumericLiteral(expr('someIdentifier'))).toBe(false);
expect(host.isNumericLiteral(expr('{}'))).toBe(false);
expect(host.isNumericLiteral(expr('[]'))).toBe(false);
expect(host.isNumericLiteral(expr('null'))).toBe(false);
expect(host.isNumericLiteral(expr('\'a\' + \'b\''))).toBe(false);
expect(host.isNumericLiteral(expr('\`moo\`'))).toBe(false);
});
});
describe('parseNumericLiteral()', () => {
it('should extract the number value', () => {
expect(host.parseNumericLiteral(expr('42'))).toEqual(42);
});
it('should error if the value is not a numeric literal', () => {
expect(() => host.parseNumericLiteral(expr('"moo"')))
.toThrowError('Unsupported syntax, expected a numeric literal.');
});
});
describe('isBooleanLiteral()', () => {
it('should return true if the expression is a boolean literal', () => {
expect(host.isBooleanLiteral(expr('true'))).toBe(true);
expect(host.isBooleanLiteral(expr('false'))).toBe(true);
});
it('should return false if the expression is not a boolean literal', () => {
expect(host.isBooleanLiteral(expr('"moo"'))).toBe(false);
expect(host.isBooleanLiteral(expr('\'moo\''))).toBe(false);
expect(host.isBooleanLiteral(expr('someIdentifier'))).toBe(false);
expect(host.isBooleanLiteral(expr('42'))).toBe(false);
expect(host.isBooleanLiteral(expr('{}'))).toBe(false);
expect(host.isBooleanLiteral(expr('[]'))).toBe(false);
expect(host.isBooleanLiteral(expr('null'))).toBe(false);
expect(host.isBooleanLiteral(expr('\'a\' + \'b\''))).toBe(false);
expect(host.isBooleanLiteral(expr('\`moo\`'))).toBe(false);
});
});
describe('parseBooleanLiteral()', () => {
it('should extract the boolean value', () => {
expect(host.parseBooleanLiteral(expr('true'))).toEqual(true);
expect(host.parseBooleanLiteral(expr('false'))).toEqual(false);
});
it('should error if the value is not a boolean literal', () => {
expect(() => host.parseBooleanLiteral(expr('"moo"')))
.toThrowError('Unsupported syntax, expected a boolean literal.');
});
});
describe('isArrayLiteral()', () => {
it('should return true if the expression is an array literal', () => {
expect(host.isArrayLiteral(expr('[]'))).toBe(true);
expect(host.isArrayLiteral(expr('[1, 2, 3]'))).toBe(true);
expect(host.isArrayLiteral(expr('[[], []]'))).toBe(true);
});
it('should return false if the expression is not an array literal', () => {
expect(host.isArrayLiteral(expr('"moo"'))).toBe(false);
expect(host.isArrayLiteral(expr('\'moo\''))).toBe(false);
expect(host.isArrayLiteral(expr('someIdentifier'))).toBe(false);
expect(host.isArrayLiteral(expr('42'))).toBe(false);
expect(host.isArrayLiteral(expr('{}'))).toBe(false);
expect(host.isArrayLiteral(expr('null'))).toBe(false);
expect(host.isArrayLiteral(expr('\'a\' + \'b\''))).toBe(false);
expect(host.isArrayLiteral(expr('\`moo\`'))).toBe(false);
});
});
describe('parseArrayLiteral()', () => {
it('should extract the expressions in the array', () => {
const moo = expr('\'moo\'');
expect(host.parseArrayLiteral(expr('[]'))).toEqual([]);
expect(host.parseArrayLiteral(expr('[\'moo\']'))).toEqual([moo]);
});
it('should error if there is an empty item', () => {
expect(() => host.parseArrayLiteral(expr('[,]')))
.toThrowError('Unsupported syntax, expected element in array not to be empty.');
});
it('should error if there is a spread element', () => {
expect(() => host.parseArrayLiteral(expr('[...[0,1]]')))
.toThrowError('Unsupported syntax, expected element in array not to use spread syntax.');
});
});
describe('isObjectLiteral()', () => {
it('should return true if the expression is an object literal', () => {
expect(host.isObjectLiteral(rhs('x = {}'))).toBe(true);
expect(host.isObjectLiteral(rhs('x = { foo: \'bar\' }'))).toBe(true);
});
it('should return false if the expression is not an object literal', () => {
expect(host.isObjectLiteral(rhs('x = "moo"'))).toBe(false);
expect(host.isObjectLiteral(rhs('x = \'moo\''))).toBe(false);
expect(host.isObjectLiteral(rhs('x = someIdentifier'))).toBe(false);
expect(host.isObjectLiteral(rhs('x = 42'))).toBe(false);
expect(host.isObjectLiteral(rhs('x = []'))).toBe(false);
expect(host.isObjectLiteral(rhs('x = null'))).toBe(false);
expect(host.isObjectLiteral(rhs('x = \'a\' + \'b\''))).toBe(false);
expect(host.isObjectLiteral(rhs('x = \`moo\`'))).toBe(false);
});
});
describe('parseObjectLiteral()', () => {
it('should extract the properties from the object', () => {
const moo = expr('\'moo\'');
expect(host.parseObjectLiteral(rhs('x = {}'))).toEqual(new Map());
expect(host.parseObjectLiteral(rhs('x = {a: \'moo\'}'))).toEqual(new Map([['a', moo]]));
});
it('should error if there is a method', () => {
expect(() => host.parseObjectLiteral(rhs('x = { foo() {} }')))
.toThrowError('Unsupported syntax, expected a property assignment.');
});
it('should error if there is a spread element', () => {
expect(() => host.parseObjectLiteral(rhs('x = {...{a:\'moo\'}}')))
.toThrowError('Unsupported syntax, expected a property assignment.');
});
});
describe('isFunctionExpression()', () => {
it('should return true if the expression is a function', () => {
expect(host.isFunctionExpression(rhs('x = function() {}'))).toBe(true);
expect(host.isFunctionExpression(rhs('x = function foo() {}'))).toBe(true);
expect(host.isFunctionExpression(rhs('x = () => {}'))).toBe(true);
expect(host.isFunctionExpression(rhs('x = () => true'))).toBe(true);
});
it('should return false if the expression is a function declaration', () => {
expect(host.isFunctionExpression(expr('function foo() {}'))).toBe(false);
});
it('should return false if the expression is not a function expression', () => {
expect(host.isFunctionExpression(expr('[]'))).toBe(false);
expect(host.isFunctionExpression(expr('"moo"'))).toBe(false);
expect(host.isFunctionExpression(expr('\'moo\''))).toBe(false);
expect(host.isFunctionExpression(expr('someIdentifier'))).toBe(false);
expect(host.isFunctionExpression(expr('42'))).toBe(false);
expect(host.isFunctionExpression(expr('{}'))).toBe(false);
expect(host.isFunctionExpression(expr('null'))).toBe(false);
expect(host.isFunctionExpression(expr('\'a\' + \'b\''))).toBe(false);
expect(host.isFunctionExpression(expr('\`moo\`'))).toBe(false);
});
});
describe('parseReturnValue()', () => {
it('should extract the return value of a function', () => {
const moo = expr('\'moo\'');
expect(host.parseReturnValue(rhs('x = function() { return \'moo\'; }'))).toEqual(moo);
});
it('should extract the value of a simple arrow function', () => {
const moo = expr('\'moo\'');
expect(host.parseReturnValue(rhs('x = () => \'moo\''))).toEqual(moo);
});
it('should extract the return value of an arrow function', () => {
const moo = expr('\'moo\'');
expect(host.parseReturnValue(rhs('x = () => { return \'moo\' }'))).toEqual(moo);
});
it('should error if the body has 0 statements', () => {
expect(() => host.parseReturnValue(rhs('x = function () { }')))
.toThrowError(
'Unsupported syntax, expected a function body with a single return statement.');
expect(() => host.parseReturnValue(rhs('x = () => { }')))
.toThrowError(
'Unsupported syntax, expected a function body with a single return statement.');
});
it('should error if the body has more than 1 statement', () => {
expect(() => host.parseReturnValue(rhs('x = function () { const x = 10; return x; }')))
.toThrowError(
'Unsupported syntax, expected a function body with a single return statement.');
expect(() => host.parseReturnValue(rhs('x = () => { const x = 10; return x; }')))
.toThrowError(
'Unsupported syntax, expected a function body with a single return statement.');
});
it('should error if the single statement is not a return statement', () => {
expect(() => host.parseReturnValue(rhs('x = function () { const x = 10; }')))
.toThrowError(
'Unsupported syntax, expected a function body with a single return statement.');
expect(() => host.parseReturnValue(rhs('x = () => { const x = 10; }')))
.toThrowError(
'Unsupported syntax, expected a function body with a single return statement.');
});
});
describe('getRange()', () => {
it('should extract the range from the expression', () => {
const file = parse('// preamble\nx = \'moo\';');
const stmt = file.program.body[0];
assertExpressionStatement(stmt);
assertAssignmentExpression(stmt.expression);
expect(host.getRange(stmt.expression.right))
.toEqual({startLine: 1, startCol: 4, startPos: 16, endPos: 21});
});
it('should error if there is no range information', () => {
const moo = rhs('// preamble\nx = \'moo\';');
expect(() => host.getRange(moo))
.toThrowError('Unable to read range for node - it is missing location information.');
});
});
});
function expr(code: string): t.Expression {
const stmt = template.ast(code);
return (stmt as t.ExpressionStatement).expression;
}
function rhs(code: string): t.Expression {
const e = expr(code);
assertAssignmentExpression(e);
return e.right;
}
function assertExpressionStatement(e: t.Node): asserts e is t.ExpressionStatement {
if (!t.isExpressionStatement(e)) {
throw new Error('Bad test - expected an expression statement');
}
}
function assertAssignmentExpression(e: t.Expression): asserts e is t.AssignmentExpression {
if (!t.isAssignmentExpression(e)) {
throw new Error('Bad test - expected an assignment expression');
}
}

View File

@ -0,0 +1,116 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {parse} from '@babel/parser';
import traverse, {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {BabelDeclarationScope} from '../src/babel_declaration_scope';
describe('BabelDeclarationScope', () => {
describe('getConstantScopeRef()', () => {
it('should return a path to the ES module where the expression was imported', () => {
const ast = parse(
[
'import * as core from \'@angular/core\';',
'function foo() {',
' var TEST = core;',
'}',
].join('\n'),
{sourceType: 'module'});
const nodePath = findVarDeclaration(ast, 'TEST');
const scope = new BabelDeclarationScope(nodePath.scope);
const constantScope = scope.getConstantScopeRef(nodePath.get('init').node);
expect(constantScope).not.toBe(null);
expect(constantScope!.node).toBe(ast.program);
});
it('should return a path to the ES Module where the expression is declared', () => {
const ast = parse(
[
'var core;',
'export function foo() {',
' var TEST = core;',
'}',
].join('\n'),
{sourceType: 'module'});
const nodePath = findVarDeclaration(ast, 'TEST');
const scope = new BabelDeclarationScope(nodePath.scope);
const constantScope = scope.getConstantScopeRef(nodePath.get('init').node);
expect(constantScope).not.toBe(null);
expect(constantScope!.node).toBe(ast.program);
});
it('should return null if the file is not an ES module', () => {
const ast = parse(
[
'var core;',
'function foo() {',
' var TEST = core;',
'}',
].join('\n'),
{sourceType: 'script'});
const nodePath = findVarDeclaration(ast, 'TEST');
const scope = new BabelDeclarationScope(nodePath.scope);
const constantScope = scope.getConstantScopeRef(nodePath.get('init').node);
expect(constantScope).toBe(null);
});
it('should return the IIFE factory function where the expression is a parameter', () => {
const ast = parse(
[
'var core;',
'(function(core) {',
' var BLOCK = \'block\';',
' function foo() {',
' var TEST = core;',
' }',
'})(core);',
].join('\n'),
{sourceType: 'script'});
const nodePath = findVarDeclaration(ast, 'TEST');
const fnPath = findFirstFunction(ast);
const scope = new BabelDeclarationScope(nodePath.scope);
const constantScope = scope.getConstantScopeRef(nodePath.get('init').node);
expect(constantScope).not.toBe(null);
expect(constantScope!.isFunction()).toBe(true);
expect(constantScope!.node).toEqual(fnPath.node);
});
});
});
function findVarDeclaration(
file: t.File, varName: string): NodePath<t.VariableDeclarator&{init: t.Expression}> {
let varDecl: NodePath<t.VariableDeclarator>|undefined = undefined;
traverse(file, {
VariableDeclarator: (path) => {
const id = path.get('id');
if (id.isIdentifier() && id.node.name === varName && path.get('init') !== null) {
varDecl = path;
path.stop();
}
}
});
if (varDecl === undefined) {
throw new Error(`TEST BUG: expected to find variable declaration for ${varName}.`);
}
return varDecl;
}
function findFirstFunction(file: t.File): NodePath<t.Function> {
let fn: NodePath<t.Function>|undefined = undefined;
traverse(file, {
Function: (path) => {
fn = path;
path.stop();
}
});
if (fn === undefined) {
throw new Error(`TEST BUG: expected to find a function.`);
}
return fn;
}

View File

@ -0,0 +1,256 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as o from '@angular/compiler/src/output/output_ast';
import {NodePath, PluginObj, transformSync} from '@babel/core';
import generate from '@babel/generator';
import * as t from '@babel/types';
import {FileLinker} from '../../../linker';
import {PartialDirectiveLinkerVersion1} from '../../src/file_linker/partial_linkers/partial_directive_linker_1';
import {createEs2015LinkerPlugin} from '../src/es2015_linker_plugin';
describe('createEs2015LinkerPlugin()', () => {
it('should return a Babel plugin visitor that handles Program (enter/exit) and CallExpression nodes',
() => {
const plugin = createEs2015LinkerPlugin();
expect(plugin.visitor).toEqual({
Program: {
enter: jasmine.any(Function),
exit: jasmine.any(Function),
},
CallExpression: jasmine.any(Function),
});
});
it('should return a Babel plugin that calls FileLinker.isPartialDeclaration() on each call expression',
() => {
const isPartialDeclarationSpy = spyOn(FileLinker.prototype, 'isPartialDeclaration');
transformSync(
[
'var core;', `fn1()`, 'fn2({prop: () => fn3({})});', `x.method(() => fn4());`,
'spread(...x);'
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
filename: '/test.js',
parserOpts: {sourceType: 'unambiguous'},
});
expect(isPartialDeclarationSpy.calls.allArgs()).toEqual([
['fn1'],
['fn2'],
['fn3'],
['method'],
['fn4'],
['spread'],
]);
});
it('should return a Babel plugin that calls FileLinker.linkPartialDeclaration() on each matching declaration',
() => {
const linkSpy = spyOn(FileLinker.prototype, 'linkPartialDeclaration')
.and.returnValue(t.identifier('REPLACEMENT'));
transformSync(
[
'var core;',
`$ngDeclareDirective({version: 1, ngImport: core, x: 1});`,
`$ngDeclareComponent({version: 1, ngImport: core, foo: () => $ngDeclareDirective({version: 1, ngImport: core, x: 2})});`,
`x.qux(() => $ngDeclareDirective({version: 1, ngImport: core, x: 3}));`,
'spread(...x);',
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
filename: '/test.js',
parserOpts: {sourceType: 'unambiguous'},
});
expect(humanizeLinkerCalls(linkSpy.calls)).toEqual([
['$ngDeclareDirective', '{version:1,ngImport:core,x:1}'],
[
'$ngDeclareComponent',
'{version:1,ngImport:core,foo:()=>$ngDeclareDirective({version:1,ngImport:core,x:2})}'
],
// Note we do not process `x:2` declaration since it is nested within another declaration
['$ngDeclareDirective', '{version:1,ngImport:core,x:3}']
]);
});
it('should return a Babel plugin that replaces call expressions with the return value from FileLinker.linkPartialDeclaration()',
() => {
let replaceCount = 0;
spyOn(FileLinker.prototype, 'linkPartialDeclaration')
.and.callFake(() => t.identifier('REPLACEMENT_' + ++replaceCount));
const result = transformSync(
[
'var core;',
'$ngDeclareDirective({version: 1, ngImport: core});',
'$ngDeclareDirective({version: 1, ngImport: core, foo: () => bar({})});',
'x.qux();',
'spread(...x);',
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
filename: '/test.js',
parserOpts: {sourceType: 'unambiguous'},
generatorOpts: {compact: true},
});
expect(result!.code).toEqual('var core;REPLACEMENT_1;REPLACEMENT_2;x.qux();spread(...x);');
});
it('should return a Babel plugin that adds shared statements after any imports', () => {
spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
const result = transformSync(
[
'import * as core from \'some-module\';',
'import {id} from \'other-module\';',
`$ngDeclareDirective({version: 1, ngImport: core})`,
`$ngDeclareDirective({version: 1, ngImport: core})`,
`$ngDeclareDirective({version: 1, ngImport: core})`,
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
filename: '/test.js',
parserOpts: {sourceType: 'unambiguous'},
generatorOpts: {compact: true},
});
expect(result!.code)
.toEqual(
'import*as core from\'some-module\';import{id}from\'other-module\';const _c0=[1];const _c1=[2];const _c2=[3];"REPLACEMENT";"REPLACEMENT";"REPLACEMENT";');
});
it('should return a Babel plugin that adds shared statements at the start of the program if it is an ECMAScript Module and there are no imports',
() => {
spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
const result = transformSync(
[
'var core;',
`$ngDeclareDirective({version: 1, ngImport: core})`,
`$ngDeclareDirective({version: 1, ngImport: core})`,
`$ngDeclareDirective({version: 1, ngImport: core})`,
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
filename: '/test.js',
// We declare the file as a module because this cannot be inferred from the source
parserOpts: {sourceType: 'module'},
generatorOpts: {compact: true},
});
expect(result!.code)
.toEqual(
'const _c0=[1];const _c1=[2];const _c2=[3];var core;"REPLACEMENT";"REPLACEMENT";"REPLACEMENT";');
});
it('should return a Babel plugin that adds shared statements at the start of the function body if the ngImport is from a function parameter',
() => {
spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
const result = transformSync(
[
'function run(core) {', ` $ngDeclareDirective({version: 1, ngImport: core})`,
` $ngDeclareDirective({version: 1, ngImport: core})`,
` $ngDeclareDirective({version: 1, ngImport: core})`, '}'
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
filename: '/test.js',
parserOpts: {sourceType: 'unambiguous'},
generatorOpts: {compact: true},
});
expect(result!.code)
.toEqual(
'function run(core){const _c0=[1];const _c1=[2];const _c2=[3];"REPLACEMENT";"REPLACEMENT";"REPLACEMENT";}');
});
it('should return a Babel plugin that adds shared statements into an IIFE if no scope could not be derived for the ngImport',
() => {
spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
const result = transformSync(
[
'function run() {',
` $ngDeclareDirective({version: 1, ngImport: core})`,
` $ngDeclareDirective({version: 1, ngImport: core})`,
` $ngDeclareDirective({version: 1, ngImport: core})`,
'}',
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
filename: '/test.js',
parserOpts: {sourceType: 'unambiguous'},
generatorOpts: {compact: true},
});
expect(result!.code).toEqual([
`function run(){`,
`(function(){const _c0=[1];return"REPLACEMENT";})();`,
`(function(){const _c0=[2];return"REPLACEMENT";})();`,
`(function(){const _c0=[3];return"REPLACEMENT";})();`,
`}`,
].join(''));
});
it('should still execute other plugins that match AST nodes inside the result of the replacement',
() => {
spyOnLinkPartialDeclarationWithConstants(o.fn([], [], null, null, 'FOO'));
const result = transformSync(
[
`$ngDeclareDirective({version: 1, ngImport: core}); FOO;`,
].join('\n'),
{
plugins: [
createEs2015LinkerPlugin(),
createIdentifierMapperPlugin('FOO', 'BAR'),
createIdentifierMapperPlugin('_c0', 'x1'),
],
filename: '/test.js',
parserOpts: {sourceType: 'module'},
generatorOpts: {compact: true},
});
expect(result!.code).toEqual([
`(function(){const x1=[1];return function BAR(){};})();BAR;`,
].join(''));
});
});
/**
* Convert the arguments of the spied-on `calls` into a human readable array.
*/
function humanizeLinkerCalls(
calls: jasmine.Calls<typeof FileLinker.prototype.linkPartialDeclaration>) {
return calls.all().map(({args: [fn, args]}) => [fn, generate(args[0], {compact: true}).code]);
}
/**
* Spy on the `PartialDirectiveLinkerVersion1.linkPartialDeclaration()` method, triggering
* shared constants to be created.
*/
function spyOnLinkPartialDeclarationWithConstants(replacement: o.Expression) {
let callCount = 0;
spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration')
.and.callFake(((sourceUrl, code, constantPool) => {
const constArray = o.literalArr([o.literal(++callCount)]);
// We have to add the constant twice or it will not create a shared statement
constantPool.getConstLiteral(constArray);
constantPool.getConstLiteral(constArray);
return replacement;
}) as typeof PartialDirectiveLinkerVersion1.prototype.linkPartialDeclaration);
}
/**
* A simple Babel plugin that will replace all identifiers that match `<src>` with identifiers
* called `<dest>`.
*/
function createIdentifierMapperPlugin(src: string, dest: string): PluginObj {
return {
visitor: {
Identifier(path: NodePath<t.Identifier>) {
if (path.node.name === src) {
path.replaceWith(t.identifier(dest));
}
}
},
};
}

View File

@ -0,0 +1,14 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export {AstHost, Range} from './src/ast/ast_host';
export {assert} from './src/ast/utils';
export {FatalLinkerError, isFatalLinkerError} from './src/fatal_linker_error';
export {DeclarationScope} from './src/file_linker/declaration_scope';
export {FileLinker} from './src/file_linker/file_linker';
export {LinkerEnvironment} from './src/file_linker/linker_environment';
export {LinkerOptions} from './src/file_linker/linker_options';

View File

@ -0,0 +1,95 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* An abstraction for getting information from an AST while being agnostic to the underlying AST
* implementation.
*/
export interface AstHost<TExpression> {
/**
* Get the name of the symbol represented by the given expression node, or `null` if it is not a
* symbol.
*/
getSymbolName(node: TExpression): string|null;
/**
* Return `true` if the given expression is a string literal, or false otherwise.
*/
isStringLiteral(node: TExpression): boolean;
/**
* Parse the string value from the given expression, or throw if it is not a string literal.
*/
parseStringLiteral(str: TExpression): string;
/**
* Return `true` if the given expression is a numeric literal, or false otherwise.
*/
isNumericLiteral(node: TExpression): boolean;
/**
* Parse the numeric value from the given expression, or throw if it is not a numeric literal.
*/
parseNumericLiteral(num: TExpression): number;
/**
* Return `true` if the given expression is a boolean literal, or false otherwise.
*/
isBooleanLiteral(node: TExpression): boolean;
/**
* Parse the boolean value from the given expression, or throw if it is not a boolean literal.
*/
parseBooleanLiteral(bool: TExpression): boolean;
/**
* Return `true` if the given expression is an array literal, or false otherwise.
*/
isArrayLiteral(node: TExpression): boolean;
/**
* Parse an array of expressions from the given expression, or throw if it is not an array
* literal.
*/
parseArrayLiteral(array: TExpression): TExpression[];
/**
* Return `true` if the given expression is an object literal, or false otherwise.
*/
isObjectLiteral(node: TExpression): boolean;
/**
* Parse the given expression into a map of object property names to property expressions, or
* throw if it is not an object literal.
*/
parseObjectLiteral(obj: TExpression): Map<string, TExpression>;
/**
* Return `true` if the given expression is a function, or false otherwise.
*/
isFunctionExpression(node: TExpression): boolean;
/**
* Compute the "value" of a function expression by parsing its body for a single `return`
* statement, extracting the returned expression, or throw if it is not possible.
*/
parseReturnValue(fn: TExpression): TExpression;
/**
* Compute the location range of the expression in the source file, to be used for source-mapping.
*/
getRange(node: TExpression): Range;
}
/**
* The location of the start and end of an expression in the original source file.
*/
export interface Range {
/** 0-based character position of the range start in the source file text. */
startPos: number;
/** 0-based line index of the range start in the source file text. */
startLine: number;
/** 0-based column position of the range start in the source file text. */
startCol: number;
/** 0-based character position of the range end in the source file text. */
endPos: number;
}

View File

@ -0,0 +1,240 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as o from '@angular/compiler';
import {FatalLinkerError} from '../fatal_linker_error';
import {AstHost, Range} from './ast_host';
/**
* This helper class wraps an object expression along with an `AstHost` object, exposing helper
* methods that make it easier to extract the properties of the object.
*/
export class AstObject<TExpression> {
/**
* Create a new `AstObject` from the given `expression` and `host`.
*/
static parse<TExpression>(expression: TExpression, host: AstHost<TExpression>):
AstObject<TExpression> {
const obj = host.parseObjectLiteral(expression);
return new AstObject<TExpression>(expression, obj, host);
}
private constructor(
readonly expression: TExpression, private obj: Map<string, TExpression>,
private host: AstHost<TExpression>) {}
/**
* Returns true if the object has a property called `propertyName`.
*/
has(propertyName: string): boolean {
return this.obj.has(propertyName);
}
/**
* Returns the number value of the property called `propertyName`.
*
* Throws an error if there is no such property or the property is not a number.
*/
getNumber(propertyName: string): number {
return this.host.parseNumericLiteral(this.getRequiredProperty(propertyName));
}
/**
* Returns the string value of the property called `propertyName`.
*
* Throws an error if there is no such property or the property is not a string.
*/
getString(propertyName: string): string {
return this.host.parseStringLiteral(this.getRequiredProperty(propertyName));
}
/**
* Returns the boolean value of the property called `propertyName`.
*
* Throws an error if there is no such property or the property is not a boolean.
*/
getBoolean(propertyName: string): boolean {
return this.host.parseBooleanLiteral(this.getRequiredProperty(propertyName));
}
/**
* Returns the nested `AstObject` parsed from the property called `propertyName`.
*
* Throws an error if there is no such property or the property is not an object.
*/
getObject(propertyName: string): AstObject<TExpression> {
const expr = this.getRequiredProperty(propertyName);
const obj = this.host.parseObjectLiteral(expr);
return new AstObject(expr, obj, this.host);
}
/**
* Returns an array of `AstValue` objects parsed from the property called `propertyName`.
*
* Throws an error if there is no such property or the property is not an array.
*/
getArray(propertyName: string): AstValue<TExpression>[] {
const arr = this.host.parseArrayLiteral(this.getRequiredProperty(propertyName));
return arr.map(entry => new AstValue(entry, this.host));
}
/**
* Returns a `WrappedNodeExpr` object that wraps the expression at the property called
* `propertyName`.
*
* Throws an error if there is no such property.
*/
getOpaque(propertyName: string): o.WrappedNodeExpr<TExpression> {
return new o.WrappedNodeExpr(this.getRequiredProperty(propertyName));
}
/**
* Returns the raw `TExpression` value of the property called `propertyName`.
*
* Throws an error if there is no such property.
*/
getNode(propertyName: string): TExpression {
return this.getRequiredProperty(propertyName);
}
/**
* Returns an `AstValue` that wraps the value of the property called `propertyName`.
*
* Throws an error if there is no such property.
*/
getValue(propertyName: string): AstValue<TExpression> {
return new AstValue(this.getRequiredProperty(propertyName), this.host);
}
/**
* Converts the AstObject to a raw JavaScript object, mapping each property value (as an
* `AstValue`) to the generic type (`T`) via the `mapper` function.
*/
toLiteral<T>(mapper: (value: AstValue<TExpression>) => T): {[key: string]: T} {
const result: {[key: string]: T} = {};
for (const [key, expression] of this.obj) {
result[key] = mapper(new AstValue(expression, this.host));
}
return result;
}
private getRequiredProperty(propertyName: string): TExpression {
if (!this.obj.has(propertyName)) {
throw new FatalLinkerError(
this.expression, `Expected property '${propertyName}' to be present.`);
}
return this.obj.get(propertyName)!;
}
}
/**
* This helper class wraps an `expression`, exposing methods that use the `host` to give
* access to the underlying value of the wrapped expression.
*/
export class AstValue<TExpression> {
constructor(private expression: TExpression, private host: AstHost<TExpression>) {}
/**
* Is this value a number?
*/
isNumber(): boolean {
return this.host.isNumericLiteral(this.expression);
}
/**
* Parse the number from this value, or error if it is not a number.
*/
getNumber(): number {
return this.host.parseNumericLiteral(this.expression);
}
/**
* Is this value a string?
*/
isString(): boolean {
return this.host.isStringLiteral(this.expression);
}
/**
* Parse the string from this value, or error if it is not a string.
*/
getString(): string {
return this.host.parseStringLiteral(this.expression);
}
/**
* Is this value a boolean?
*/
isBoolean(): boolean {
return this.host.isBooleanLiteral(this.expression);
}
/**
* Parse the boolean from this value, or error if it is not a boolean.
*/
getBoolean(): boolean {
return this.host.parseBooleanLiteral(this.expression);
}
/**
* Is this value an object literal?
*/
isObject(): boolean {
return this.host.isObjectLiteral(this.expression);
}
/**
* Parse this value into an `AstObject`, or error if it is not an object literal.
*/
getObject(): AstObject<TExpression> {
return AstObject.parse(this.expression, this.host);
}
/**
* Is this value an array literal?
*/
isArray(): boolean {
return this.host.isArrayLiteral(this.expression);
}
/**
* Parse this value into an array of `AstValue` objects, or error if it is not an array literal.
*/
getArray(): AstValue<TExpression>[] {
const arr = this.host.parseArrayLiteral(this.expression);
return arr.map(entry => new AstValue(entry, this.host));
}
/**
* Is this value a function expression?
*/
isFunction(): boolean {
return this.host.isFunctionExpression(this.expression);
}
/**
* Extract the return value as an `AstValue` from this value as a function expression, or error if
* it is not a function expression.
*/
getFunctionReturnValue(): AstValue<TExpression> {
return new AstValue(this.host.parseReturnValue(this.expression), this.host);
}
/**
* Return the `TExpression` of this value wrapped in a `WrappedNodeExpr`.
*/
getOpaque(): o.WrappedNodeExpr<TExpression> {
return new o.WrappedNodeExpr(this.expression);
}
/**
* Get the range of the location of this value in the original source.
*/
getRange(): Range {
return this.host.getRange(this.expression);
}
}

View File

@ -0,0 +1,147 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {FatalLinkerError} from '../../fatal_linker_error';
import {AstHost, Range} from '../ast_host';
import {assert} from '../utils';
/**
* This implementation of `AstHost` is able to get information from TypeScript AST nodes.
*
* This host is not actually used at runtime in the current code.
*
* It is implemented here to ensure that the `AstHost` abstraction is not unfairly skewed towards
* the Babel implementation. It could also provide a basis for a 3rd TypeScript compiler plugin to
* do linking in the future.
*/
export class TypeScriptAstHost implements AstHost<ts.Expression> {
getSymbolName(node: ts.Expression): string|null {
if (ts.isIdentifier(node)) {
return node.text;
} else if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) {
return node.name.text;
} else {
return null;
}
}
isStringLiteral = ts.isStringLiteral;
parseStringLiteral(str: ts.Expression): string {
assert(str, this.isStringLiteral, 'a string literal');
return str.text;
}
isNumericLiteral = ts.isNumericLiteral;
parseNumericLiteral(num: ts.Expression): number {
assert(num, this.isNumericLiteral, 'a numeric literal');
return parseInt(num.text);
}
isBooleanLiteral(node: ts.Expression): node is ts.FalseLiteral|ts.TrueLiteral {
return node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword;
}
parseBooleanLiteral(bool: ts.Expression): boolean {
assert(bool, this.isBooleanLiteral, 'a boolean literal');
return bool.kind === ts.SyntaxKind.TrueKeyword;
}
isArrayLiteral = ts.isArrayLiteralExpression;
parseArrayLiteral(array: ts.Expression): ts.Expression[] {
assert(array, this.isArrayLiteral, 'an array literal');
return array.elements.map(element => {
assert(element, isNotEmptyElement, 'element in array not to be empty');
assert(element, isNotSpreadElement, 'element in array not to use spread syntax');
return element;
});
}
isObjectLiteral = ts.isObjectLiteralExpression;
parseObjectLiteral(obj: ts.Expression): Map<string, ts.Expression> {
assert(obj, this.isObjectLiteral, 'an object literal');
const result = new Map<string, ts.Expression>();
for (const property of obj.properties) {
assert(property, ts.isPropertyAssignment, 'a property assignment');
assert(property.name, isPropertyName, 'a property name');
result.set(property.name.text, property.initializer);
}
return result;
}
isFunctionExpression(node: ts.Expression): node is ts.FunctionExpression|ts.ArrowFunction {
return ts.isFunctionExpression(node) || ts.isArrowFunction(node);
}
parseReturnValue(fn: ts.Expression): ts.Expression {
assert(fn, this.isFunctionExpression, 'a function');
if (!ts.isBlock(fn.body)) {
// it is a simple array function expression: `(...) => expr`
return fn.body;
}
// it is a function (arrow or normal) with a body. E.g.:
// * `(...) => { stmt; ... }`
// * `function(...) { stmt; ... }`
if (fn.body.statements.length !== 1) {
throw new FatalLinkerError(
fn.body, 'Unsupported syntax, expected a function body with a single return statement.');
}
const stmt = fn.body.statements[0];
assert(stmt, ts.isReturnStatement, 'a function body with a single return statement');
if (stmt.expression === undefined) {
throw new FatalLinkerError(stmt, 'Unsupported syntax, expected function to return a value.');
}
return stmt.expression;
}
getRange(node: ts.Expression): Range {
const file = node.getSourceFile();
if (file === undefined) {
throw new FatalLinkerError(
node, 'Unable to read range for node - it is missing parent information.');
}
const startPos = node.getStart();
const endPos = node.getEnd();
const {line: startLine, character: startCol} = ts.getLineAndCharacterOfPosition(file, startPos);
return {startLine, startCol, startPos, endPos};
}
}
/**
* Return true if the expression does not represent an empty element in an array literal.
* For example in `[,foo]` the first element is "empty".
*/
function isNotEmptyElement(e: ts.Expression|ts.SpreadElement|
ts.OmittedExpression): e is ts.Expression|ts.SpreadElement {
return !ts.isOmittedExpression(e);
}
/**
* Return true if the expression is not a spread element of an array literal.
* For example in `[x, ...rest]` the `...rest` expression is a spread element.
*/
function isNotSpreadElement(e: ts.Expression|ts.SpreadElement): e is ts.Expression {
return !ts.isSpreadElement(e);
}
/**
* Return true if the expression can be considered a text based property name.
*/
function isPropertyName(e: ts.PropertyName): e is ts.Identifier|ts.StringLiteral|ts.NumericLiteral {
return ts.isIdentifier(e) || ts.isStringLiteral(e) || ts.isNumericLiteral(e);
}

View File

@ -0,0 +1,18 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {FatalLinkerError} from '../fatal_linker_error';
/**
* Assert that the given `node` is of the type guarded by the `predicate` function.
*/
export function assert<T, K extends T>(
node: T, predicate: (node: T) => node is K, expected: string): asserts node is K {
if (!predicate(node)) {
throw new FatalLinkerError(node, `Unsupported syntax, expected ${expected}.`);
}
}

View File

@ -0,0 +1,31 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* An unrecoverable error during linking.
*/
export class FatalLinkerError extends Error {
readonly type = 'FatalLinkerError';
/**
* Create a new FatalLinkerError.
*
* @param node The AST node where the error occurred.
* @param message A description of the error.
*/
constructor(public node: unknown, message: string) {
super(message);
}
}
/**
* Whether the given object `e` is a FatalLinkerError.
*/
export function isFatalLinkerError(e: any): e is FatalLinkerError {
return e && e.type === 'FatalLinkerError';
}

View File

@ -0,0 +1,45 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* This interface represents the lexical scope of a partial declaration in the source code.
*
* For example, if you had the following code:
*
* ```
* function foo() {
* function bar () {
* $ngDeclareDirective({...});
* }
* }
* ```
*
* The `DeclarationScope` of the `$ngDeclareDirective()` call is the body of the `bar()` function.
*
* The `FileLinker` uses this object to identify the lexical scope of any constant statements that
* might be generated by the linking process (i.e. where the `ConstantPool` lives for a set of
* partial linkers).
*/
export interface DeclarationScope<TSharedConstantScope, TExpression> {
/**
* Get a `TSharedConstantScope` object that can be used to reference the lexical scope where any
* shared constant statements would be inserted.
*
* This object is generic because different AST implementations will need different
* `TConstantScope` types to be able to insert shared constant statements. For example in Babel
* this would be a `NodePath` object; in TS it would just be a `Node` object.
*
* If it is not possible to find such a shared scope, then constant statements will be wrapped up
* with their generated linked definition expression, in the form of an IIFE.
*
* @param expression the expression that points to the Angular core framework import.
* @returns a reference to a reference object for where the shared constant statements will be
* inserted, or `null` if it is not possible to have a shared scope.
*/
getConstantScopeRef(expression: TExpression): TSharedConstantScope|null;
}

View File

@ -0,0 +1,48 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool} from '@angular/compiler';
import * as o from '@angular/compiler/src/output/output_ast';
import {LinkerImportGenerator} from '../../linker_import_generator';
import {LinkerEnvironment} from '../linker_environment';
/**
* This class represents (from the point of view of the `FileLinker`) the scope in which
* statements and expressions related to a linked partial declaration will be emitted.
*
* It holds a copy of a `ConstantPool` that is used to capture any constant statements that need to
* be emitted in this context.
*
* This implementation will emit the definition and the constant statements separately.
*/
export class EmitScope<TStatement, TExpression> {
readonly constantPool = new ConstantPool();
constructor(
protected readonly ngImport: TExpression,
protected readonly linkerEnvironment: LinkerEnvironment<TStatement, TExpression>) {}
/**
* Translate the given Output AST definition expression into a generic `TExpression`.
*
* Use a `LinkerImportGenerator` to handle any imports in the definition.
*/
translateDefinition(definition: o.Expression): TExpression {
return this.linkerEnvironment.translator.translateExpression(
definition, new LinkerImportGenerator(this.ngImport));
}
/**
* Return any constant statements that are shared between all uses of this `EmitScope`.
*/
getConstantStatements(): TStatement[] {
const {translator} = this.linkerEnvironment;
const importGenerator = new LinkerImportGenerator(this.ngImport);
return this.constantPool.statements.map(
statement => translator.translateStatement(statement, importGenerator));
}
}

View File

@ -0,0 +1,40 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as o from '@angular/compiler/src/output/output_ast';
import {EmitScope} from './emit_scope';
/**
* This class is a specialization of the `EmitScope` class that is designed for the situation where
* there is no clear shared scope for constant statements. In this case they are bundled with the
* translated definition inside an IIFE.
*/
export class IifeEmitScope<TStatement, TExpression> extends EmitScope<TStatement, TExpression> {
/**
* Translate the given Output AST definition expression into a generic `TExpression`.
*
* Wraps the output from `EmitScope.translateDefinition()` and `EmitScope.getConstantStatements()`
* in an IIFE.
*/
translateDefinition(definition: o.Expression): TExpression {
const {factory} = this.linkerEnvironment;
const constantStatements = super.getConstantStatements();
const returnStatement = factory.createReturnStatement(super.translateDefinition(definition));
const body = factory.createBlock([...constantStatements, returnStatement]);
const fn = factory.createFunctionExpression(/* name */ null, /* args */[], body);
return factory.createCallExpression(fn, /* args */[], /* pure */ false);
}
/**
* It is not valid to call this method, since there will be no shared constant statements - they
* are already emitted in the IIFE alongside the translated definition.
*/
getConstantStatements(): TStatement[] {
throw new Error('BUG - IifeEmitScope should not expose any constant statements');
}
}

View File

@ -0,0 +1,94 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AstObject} from '../ast/ast_value';
import {DeclarationScope} from './declaration_scope';
import {EmitScope} from './emit_scopes/emit_scope';
import {IifeEmitScope} from './emit_scopes/iife_emit_scope';
import {LinkerEnvironment} from './linker_environment';
import {PartialLinkerSelector} from './partial_linkers/partial_linker_selector';
export const NO_STATEMENTS: Readonly<any[]> = [] as const;
/**
* This class is responsible for linking all the partial declarations found in a single file.
*/
export class FileLinker<TConstantScope, TStatement, TExpression> {
private linkerSelector = new PartialLinkerSelector<TStatement, TExpression>();
private emitScopes = new Map<TConstantScope, EmitScope<TStatement, TExpression>>();
constructor(
private linkerEnvironment: LinkerEnvironment<TStatement, TExpression>,
private sourceUrl: string, readonly code: string) {}
/**
* Return true if the given callee name matches a partial declaration that can be linked.
*/
isPartialDeclaration(calleeName: string): boolean {
return this.linkerSelector.supportsDeclaration(calleeName);
}
/**
* Link the metadata extracted from the args of a call to a partial declaration function.
*
* The `declarationScope` is used to determine the scope and strategy of emission of the linked
* definition and any shared constant statements.
*
* @param declarationFn the name of the function used to declare the partial declaration - e.g.
* `$ngDeclareDirective`.
* @param args the arguments passed to the declaration function.
* @param declarationScope the scope that contains this call to the declaration function.
*/
linkPartialDeclaration(
declarationFn: string, args: TExpression[],
declarationScope: DeclarationScope<TConstantScope, TExpression>): TExpression {
if (args.length !== 1) {
throw new Error(
`Invalid function call: It should have only a single object literal argument, but contained ${
args.length}.`);
}
const metaObj = AstObject.parse(args[0], this.linkerEnvironment.host);
const ngImport = metaObj.getNode('ngImport');
const emitScope = this.getEmitScope(ngImport, declarationScope);
const version = metaObj.getNumber('version');
const linker = this.linkerSelector.getLinker(declarationFn, version);
const definition =
linker.linkPartialDeclaration(this.sourceUrl, this.code, emitScope.constantPool, metaObj);
return emitScope.translateDefinition(definition);
}
/**
* Return all the shared constant statements and their associated constant scope references, so
* that they can be inserted into the source code.
*/
getConstantStatements(): {constantScope: TConstantScope, statements: TStatement[]}[] {
const results: {constantScope: TConstantScope, statements: TStatement[]}[] = [];
for (const [constantScope, emitScope] of this.emitScopes.entries()) {
const statements = emitScope.getConstantStatements();
results.push({constantScope, statements});
}
return results;
}
private getEmitScope(
ngImport: TExpression, declarationScope: DeclarationScope<TConstantScope, TExpression>):
EmitScope<TStatement, TExpression> {
const constantScope = declarationScope.getConstantScopeRef(ngImport);
if (constantScope === null) {
// There is no constant scope so we will emit extra statements into the definition IIFE.
return new IifeEmitScope(ngImport, this.linkerEnvironment);
}
if (!this.emitScopes.has(constantScope)) {
this.emitScopes.set(constantScope, new EmitScope(ngImport, this.linkerEnvironment));
}
return this.emitScopes.get(constantScope)!;
}
}

View File

@ -0,0 +1,25 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AstFactory} from '@angular/compiler-cli/src/ngtsc/translator';
import {AstHost} from '../ast/ast_host';
import {DEFAULT_LINKER_OPTIONS, LinkerOptions} from './linker_options';
import {Translator} from './translator';
export class LinkerEnvironment<TStatement, TExpression> {
readonly translator = new Translator<TStatement, TExpression>(this.factory);
private constructor(
readonly host: AstHost<TExpression>, readonly factory: AstFactory<TStatement, TExpression>,
readonly options: LinkerOptions) {}
static create<TStatement, TExpression>(
host: AstHost<TExpression>, factory: AstFactory<TStatement, TExpression>,
options: Partial<LinkerOptions>): LinkerEnvironment<TStatement, TExpression> {
return new LinkerEnvironment(host, factory, {...DEFAULT_LINKER_OPTIONS, ...options});
}
}

View File

@ -0,0 +1,31 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* Options to configure the linking behavior.
*/
export interface LinkerOptions {
/**
* Whether to generate legacy i18n message ids.
* The default is `true`.
*/
enableI18nLegacyMessageIdFormat: boolean;
/**
* Whether to convert all line-endings in ICU expressions to `\n` characters.
* The default is `false`.
*/
i18nNormalizeLineEndingsInICUs: boolean;
}
/**
* The default linker options to use if properties are not provided.
*/
export const DEFAULT_LINKER_OPTIONS: LinkerOptions = {
enableI18nLegacyMessageIdFormat: true,
i18nNormalizeLineEndingsInICUs: false,
};

View File

@ -0,0 +1,25 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool} from '@angular/compiler';
import * as o from '@angular/compiler/src/output/output_ast';
import {AstObject} from '../../ast/ast_value';
import {PartialLinker} from './partial_linker';
/**
* A `PartialLinker` that is designed to process `$ngDeclareComponent()` call expressions.
*/
export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
PartialLinker<TStatement, TExpression> {
linkPartialDeclaration(
sourceUrl: string, code: string, constantPool: ConstantPool,
metaObj: AstObject<TExpression>): o.Expression {
throw new Error('Not implemented.');
}
}

Some files were not shown because too many files have changed in this diff Show More