Compare commits
60 Commits
master
...
zone.js-0.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
aaa1d8e2fe | ||
![]() |
dbfb50e9f4 | ||
![]() |
bee44b3359 | ||
![]() |
723a9ff095 | ||
![]() |
e472f5f688 | ||
![]() |
8373b720f3 | ||
![]() |
301513311e | ||
![]() |
64cf087ae5 | ||
![]() |
fec9dcbeb0 | ||
![]() |
e0e5c9f195 | ||
![]() |
cfe424e875 | ||
![]() |
3b9c802dee | ||
![]() |
ca07da4563 | ||
![]() |
81c3e809aa | ||
![]() |
be96510ce9 | ||
![]() |
cb05c0102f | ||
![]() |
5f90b64328 | ||
![]() |
71079ce47e | ||
![]() |
ca798804b2 | ||
![]() |
b071495f92 | ||
![]() |
d5f819ebc1 | ||
![]() |
1388c1761f | ||
![]() |
fb8f4b4d72 | ||
![]() |
f42e6ce917 | ||
![]() |
175c79d1d8 | ||
![]() |
945751e2e8 | ||
![]() |
b769771d60 | ||
![]() |
a80f654af9 | ||
![]() |
aa847cb014 | ||
![]() |
8763d8201c | ||
![]() |
9f7a37b4e9 | ||
![]() |
773f7908c0 | ||
![]() |
f4ced74e3a | ||
![]() |
8366effeec | ||
![]() |
5f2e475abf | ||
![]() |
aa3520eb7d | ||
![]() |
823dd5b341 | ||
![]() |
d6d7caa2a8 | ||
![]() |
dcf7baf3d1 | ||
![]() |
4d17418569 | ||
![]() |
a2e069fdda | ||
![]() |
28534d83ee | ||
![]() |
a6292faa97 | ||
![]() |
e2e5f83869 | ||
![]() |
71138f6004 | ||
![]() |
fa0104017a | ||
![]() |
80b67e02b7 | ||
![]() |
18098d38b8 | ||
![]() |
9514fd9080 | ||
![]() |
2e9fdbde9e | ||
![]() |
df76a2048b | ||
![]() |
3d156162af | ||
![]() |
5dc8d287aa | ||
![]() |
6da9e5851a | ||
![]() |
250e299dc3 | ||
![]() |
8f708b561c | ||
![]() |
e34c33cd46 | ||
![]() |
6d8c73a4d6 | ||
![]() |
6ff28ac944 | ||
![]() |
0de93fd402 |
@ -656,6 +656,18 @@ jobs:
|
||||
- run: yarn tsc -p packages
|
||||
- run: yarn tsc -p modules
|
||||
- run: yarn bazel build //packages/zone.js:npm_package
|
||||
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
|
||||
# specific tests which are reliant on such generated fixtures is not an option as SystemJS
|
||||
# in the Saucelabs legacy job always fetches referenced files, even if the imports would be
|
||||
# guarded by an check to skip in the Saucelabs legacy job. We should be good running such
|
||||
# test in all supported browsers on Saucelabs anyway until this job can be removed.
|
||||
- run:
|
||||
name: Preparing Bazel-generated fixtures required in legacy tests
|
||||
command: |
|
||||
yarn bazel build //packages/core/test:downleveled_es5_fixture
|
||||
# Needed for the ES5 downlevel reflector test in `packages/core/test/reflection`.
|
||||
cp dist/bin/packages/core/test/reflection/es5_downleveled_inheritance_fixture.js \
|
||||
dist/all/@angular/core/test/reflection/es5_downleveled_inheritance_fixture.js
|
||||
- run:
|
||||
# Waiting on ready ensures that we don't run tests too early without Saucelabs not being ready.
|
||||
name: Waiting for Saucelabs tunnel to connect
|
||||
|
@ -7,18 +7,6 @@ export const commitMessage: CommitMessageConfig = {
|
||||
maxLineLength: 120,
|
||||
minBodyLength: 20,
|
||||
minBodyLengthTypeExcludes: ['docs'],
|
||||
types: [
|
||||
'build',
|
||||
'ci',
|
||||
'docs',
|
||||
'feat',
|
||||
'fix',
|
||||
'perf',
|
||||
'refactor',
|
||||
'release',
|
||||
'style',
|
||||
'test',
|
||||
],
|
||||
scopes: [
|
||||
'animations',
|
||||
'bazel',
|
||||
|
@ -509,8 +509,8 @@ groups:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'packages/core/src/i18n/**',
|
||||
'packages/core/src/render3/i18n.ts',
|
||||
'packages/core/src/render3/i18n.md',
|
||||
'packages/core/src/render3/i18n/**',
|
||||
'packages/core/src/render3/instructions/i18n.ts',
|
||||
'packages/core/src/render3/interfaces/i18n.ts',
|
||||
'packages/common/locales/**',
|
||||
'packages/common/src/i18n/**',
|
||||
|
79
CHANGELOG.md
79
CHANGELOG.md
@ -1,3 +1,82 @@
|
||||
<a name="10.1.0-next.6"></a>
|
||||
# 10.1.0-next.6 (2020-08-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** detect DI parameters in JIT mode for downleveled ES2015 classes ([#38463](https://github.com/angular/angular/issues/38463)) ([ca07da4](https://github.com/angular/angular/commit/ca07da4)), closes [#38453](https://github.com/angular/angular/issues/38453)
|
||||
* **core:** move generated i18n statements to the `consts` field of ComponentDef ([#38404](https://github.com/angular/angular/issues/38404)) ([cb05c01](https://github.com/angular/angular/commit/cb05c01))
|
||||
* **localize:** render ICU placeholders in extracted translation files ([#38484](https://github.com/angular/angular/issues/38484)) ([81c3e80](https://github.com/angular/angular/commit/81c3e80))
|
||||
* **ngcc:** detect synthesized delegate constructors for downleveled ES2015 classes ([#38463](https://github.com/angular/angular/issues/38463)) ([3b9c802](https://github.com/angular/angular/commit/3b9c802)), closes [#38453](https://github.com/angular/angular/issues/38453) [#38453](https://github.com/angular/angular/issues/38453)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **compiler-cli:** don't emit template guards when child scope is empty ([#38418](https://github.com/angular/angular/issues/38418)) ([1388c17](https://github.com/angular/angular/commit/1388c17))
|
||||
* **compiler-cli:** only generate directive declarations when used ([#38418](https://github.com/angular/angular/issues/38418)) ([fb8f4b4](https://github.com/angular/angular/commit/fb8f4b4))
|
||||
* **compiler-cli:** only generate type-check code for referenced DOM elements ([#38418](https://github.com/angular/angular/issues/38418)) ([f42e6ce](https://github.com/angular/angular/commit/f42e6ce))
|
||||
|
||||
### Code Refactoring
|
||||
* **router:** export DefaultRouteReuseStrategy to Router public_api ([#31575](https://github.com/angular/angular/issues/31575)) ([ca79880](https://github.com/angular/angular/commit/ca79880))
|
||||
|
||||
|
||||
|
||||
<a name="10.0.10"></a>
|
||||
## 10.0.10 (2020-08-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **common:** Allow scrolling when browser supports scrollTo ([#38468](https://github.com/angular/angular/issues/38468)) ([b32126c](https://github.com/angular/angular/commit/b32126c)), closes [#30630](https://github.com/angular/angular/issues/30630)
|
||||
* **core:** detect DI parameters in JIT mode for downleveled ES2015 classes ([#38500](https://github.com/angular/angular/issues/38500)) ([863acb6](https://github.com/angular/angular/commit/863acb6)), closes [#38453](https://github.com/angular/angular/issues/38453)
|
||||
* **core:** error if CSS custom property in host binding has number in name ([#38432](https://github.com/angular/angular/issues/38432)) ([cb83b8a](https://github.com/angular/angular/commit/cb83b8a)), closes [#37292](https://github.com/angular/angular/issues/37292)
|
||||
* **core:** fix multiple nested views removal from ViewContainerRef ([#38317](https://github.com/angular/angular/issues/38317)) ([d5e09f4](https://github.com/angular/angular/commit/d5e09f4)), closes [#38201](https://github.com/angular/angular/issues/38201)
|
||||
* **ngcc:** detect synthesized delegate constructors for downleveled ES2015 classes ([#38500](https://github.com/angular/angular/issues/38500)) ([f3dd6c2](https://github.com/angular/angular/commit/f3dd6c2)), closes [#38453](https://github.com/angular/angular/issues/38453) [#38453](https://github.com/angular/angular/issues/38453)
|
||||
* **router:** ensure routerLinkActive updates when associated routerLinks change ([#38349](https://github.com/angular/angular/issues/38349)) ([989e8a1](https://github.com/angular/angular/commit/989e8a1)), closes [#18469](https://github.com/angular/angular/issues/18469)
|
||||
|
||||
|
||||
|
||||
<a name="10.1.0-next.5"></a>
|
||||
# 10.1.0-next.5 (2020-08-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler-cli:** avoid creating value expressions for symbols from type-only imports ([#37912](https://github.com/angular/angular/issues/37912)) ([18098d3](https://github.com/angular/angular/commit/18098d3)), closes [#37900](https://github.com/angular/angular/issues/37900)
|
||||
* **compiler-cli:** type-check inputs that include undefined when there's coercion members ([#38273](https://github.com/angular/angular/issues/38273)) ([7525f3a](https://github.com/angular/angular/commit/7525f3a))
|
||||
* **router:** defer loading of wildcard module until needed ([#38348](https://github.com/angular/angular/issues/38348)) ([8f708b5](https://github.com/angular/angular/commit/8f708b5)), closes [#25494](https://github.com/angular/angular/issues/25494)
|
||||
* **router:** restore 'history.state' object for navigations coming from Angular router ([#28108](https://github.com/angular/angular/issues/28108)) ([#28176](https://github.com/angular/angular/issues/28176)) ([df76a20](https://github.com/angular/angular/commit/df76a20))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler-cli:** Add compiler option to report errors when assigning to restricted input fields ([#38249](https://github.com/angular/angular/issues/38249)) ([71138f6](https://github.com/angular/angular/commit/71138f6))
|
||||
* **router:** better warning message when a router outlet has not been instantiated ([#30246](https://github.com/angular/angular/issues/30246)) ([1609815](https://github.com/angular/angular/commit/1609815))
|
||||
|
||||
|
||||
|
||||
<a name="10.0.9"></a>
|
||||
## 10.0.9 (2020-08-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **common:** ensure scrollRestoration is writable ([#30630](https://github.com/angular/angular/issues/30630)) ([#38357](https://github.com/angular/angular/issues/38357)) ([58f4b3a](https://github.com/angular/angular/commit/58f4b3a)), closes [#30629](https://github.com/angular/angular/issues/30629)
|
||||
* **compiler:** evaluate safe navigation expressions in correct binding order ([#37911](https://github.com/angular/angular/issues/37911)) ([f5b9d87](https://github.com/angular/angular/commit/f5b9d87)), closes [#37194](https://github.com/angular/angular/issues/37194)
|
||||
* **compiler-cli:** avoid creating value expressions for symbols from type-only imports ([#38415](https://github.com/angular/angular/issues/38415)) ([ca2b4bc](https://github.com/angular/angular/commit/ca2b4bc)), closes [#37912](https://github.com/angular/angular/issues/37912)
|
||||
* **compiler-cli:** infer quote expressions as any type in type checker ([#37917](https://github.com/angular/angular/issues/37917)) ([5b87c67](https://github.com/angular/angular/commit/5b87c67)), closes [#36568](https://github.com/angular/angular/issues/36568)
|
||||
* **compiler-cli:** mark eager `NgModuleFactory` construction as not side effectful ([#38320](https://github.com/angular/angular/issues/38320)) ([016a41b](https://github.com/angular/angular/commit/016a41b)), closes [#38147](https://github.com/angular/angular/issues/38147)
|
||||
* **compiler-cli:** match wrapHost parameter types within plugin interface ([#38004](https://github.com/angular/angular/issues/38004)) ([df01a82](https://github.com/angular/angular/commit/df01a82))
|
||||
* **compiler-cli:** preserve quotes in class member names ([#38387](https://github.com/angular/angular/issues/38387)) ([c9acb7b](https://github.com/angular/angular/commit/c9acb7b)), closes [#38311](https://github.com/angular/angular/issues/38311)
|
||||
* **core:** prevent NgModule scope being overwritten in JIT compiler ([#37795](https://github.com/angular/angular/issues/37795)) ([3acebdc](https://github.com/angular/angular/commit/3acebdc)), closes [#37105](https://github.com/angular/angular/issues/37105)
|
||||
* **core:** queries not matching string injection tokens ([#38321](https://github.com/angular/angular/issues/38321)) ([32109dc](https://github.com/angular/angular/commit/32109dc)), closes [#38313](https://github.com/angular/angular/issues/38313) [#38315](https://github.com/angular/angular/issues/38315)
|
||||
* **core:** Store the currently selected ICU in `LView` ([#38345](https://github.com/angular/angular/issues/38345)) ([ee5123f](https://github.com/angular/angular/commit/ee5123f))
|
||||
* **platform-server:** remove styles added by ServerStylesHost on destruction ([#38367](https://github.com/angular/angular/issues/38367)) ([7f11149](https://github.com/angular/angular/commit/7f11149))
|
||||
* **router:** prevent calling unsubscribe on undefined subscription in RouterPreloader ([#38344](https://github.com/angular/angular/issues/38344)) ([4151314](https://github.com/angular/angular/commit/4151314))
|
||||
* **service-worker:** fix the chrome debugger syntax highlighter ([#38332](https://github.com/angular/angular/issues/38332)) ([f5d5bac](https://github.com/angular/angular/commit/f5d5bac))
|
||||
|
||||
|
||||
|
||||
<a name="10.1.0-next.4"></a>
|
||||
# 10.1.0-next.4 (2020-08-04)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Location } from '@angular/common';
|
||||
|
||||
@ -11,7 +11,7 @@ import { HeroService } from '../hero.service';
|
||||
styleUrls: [ './hero-detail.component.css' ]
|
||||
})
|
||||
export class HeroDetailComponent implements OnInit {
|
||||
@Input() hero: Hero;
|
||||
hero: Hero;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
|
@ -31,7 +31,7 @@ For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
...
|
||||
|
@ -40,8 +40,7 @@ The top level of the workspace contains workspace-wide configuration files, conf
|
||||
| `package-lock.json` | Provides version information for all packages installed into `node_modules` by the npm client. See [npm documentation](https://docs.npmjs.com/files/package-lock.json) for details. If you use the yarn client, this file will be [yarn.lock](https://yarnpkg.com/lang/en/docs/yarn-lock/) instead. |
|
||||
| `src/` | Source files for the root-level application project. |
|
||||
| `node_modules/` | Provides [npm packages](guide/npm-packages) to the entire workspace. Workspace-wide `node_modules` dependencies are visible to all projects. |
|
||||
| `tsconfig.json` | The `tsconfig.json` file is a ["Solution Style"](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#support-for-solution-style-tsconfigjson-files) TypeScript configuration file. Code editors and TypeScript’s language server use this file to improve development experience. Compilers do not use this file. |
|
||||
| `tsconfig.base.json` | The base [TypeScript](https://www.typescriptlang.org/) configuration for projects in the workspace. All other configuration files inherit from this base file. For more information, see the [Configuration inheritance with extends](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#configuration-inheritance-with-extends) section of the TypeScript documentation.|
|
||||
| `tsconfig.json` | The base [TypeScript](https://www.typescriptlang.org/) configuration for projects in the workspace. All other configuration files inherit from this base file. For more information, see the [Configuration inheritance with extends](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#configuration-inheritance-with-extends) section of the TypeScript documentation.|
|
||||
| `tslint.json` | Default [TSLint](https://palantir.github.io/tslint/) configuration for projects in the workspace. |
|
||||
|
||||
|
||||
@ -103,7 +102,7 @@ Angular components, templates, and styles go here.
|
||||
The application-specific configuration files for the root application reside at the workspace root level.
|
||||
For a multi-project workspace, project-specific configuration files are in the project root, under `projects/project-name/`.
|
||||
|
||||
Project-specific [TypeScript](https://www.typescriptlang.org/) configuration files inherit from the workspace-wide `tsconfig.base.json`, and project-specific [TSLint](https://palantir.github.io/tslint/) configuration files inherit from the workspace-wide `tslint.json`.
|
||||
Project-specific [TypeScript](https://www.typescriptlang.org/) configuration files inherit from the workspace-wide `tsconfig.json`, and project-specific [TSLint](https://palantir.github.io/tslint/) configuration files inherit from the workspace-wide `tslint.json`.
|
||||
|
||||
| APPLICATION-SPECIFIC CONFIG FILES | PURPOSE |
|
||||
| :--------------------- | :------------------------------------------|
|
||||
|
@ -11,11 +11,11 @@ That said, some applications will likely need to apply some manual updates.
|
||||
In version 10, [a few deprecated APIs have been removed](guide/updating-to-version-10#removals) and there are a [few breaking changes](guide/updating-to-version-10#breaking-changes) unrelated to Ivy.
|
||||
If you're seeing errors after updating to version 9, you'll first want to rule those changes out.
|
||||
|
||||
To do so, temporarily [turn off Ivy](guide/ivy#opting-out-of-angular-ivy) in your `tsconfig.base.json` and re-start your app.
|
||||
To do so, temporarily [turn off Ivy](guide/ivy#opting-out-of-angular-ivy) in your `tsconfig.json` and re-start your app.
|
||||
|
||||
If you're still seeing the errors, they are not specific to Ivy. In this case, you may want to consult the [general version 10 guide](guide/updating-to-version-10). If you've opted into any of the new, stricter type-checking settings, you may also want to check out the [template type-checking guide](guide/template-typecheck).
|
||||
|
||||
If the errors are gone, switch back to Ivy by removing the changes to the `tsconfig.base.json` and review the list of expected changes below.
|
||||
If the errors are gone, switch back to Ivy by removing the changes to the `tsconfig.json` and review the list of expected changes below.
|
||||
|
||||
{@a payload-size-debugging}
|
||||
### Payload size debugging
|
||||
|
@ -1,55 +0,0 @@
|
||||
# Solution-style `tsconfig.json` migration
|
||||
|
||||
## What does this migration do?
|
||||
|
||||
This migration adds support to existing projects for TypeScript's new ["solution-style" tsconfig feature](https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/#solution-style-tsconfig).
|
||||
|
||||
Support is added by making two changes:
|
||||
|
||||
1. Renaming the workspace-level `tsconfig.json` to `tsconfig.base.json`.
|
||||
All project [TypeScript configuration files](guide/typescript-configuration) will extend from this base which contains the common options used throughout the workspace.
|
||||
|
||||
2. Adding the solution `tsconfig.json` file at the root of the workspace.
|
||||
This `tsconfig.json` file will only contain references to project-level TypeScript configuration files and is only used by editors/IDEs.
|
||||
|
||||
As an example, the solution `tsconfig.json` for a new project is as follows:
|
||||
```json
|
||||
// This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScript’s language server to improve development experience.
|
||||
// It is not intended to be used to perform a compilation.
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
},
|
||||
{
|
||||
"path": "./e2e/tsconfig.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Why is this migration necessary?
|
||||
|
||||
Solution-style `tsconfig.json` files provide an improved editing experience and fix several long-standing defects when editing files in an IDE.
|
||||
IDEs that leverage the TypeScript language service (for example, [Visual Studio Code](https://code.visualstudio.com)), will only use TypeScript configuration files that are named `tsconfig.json`.
|
||||
In complex projects, there may be more than one compilation unit and each of these units may have different settings and options.
|
||||
|
||||
With the Angular CLI, a project will have application code that will target a browser.
|
||||
It will also have unit tests that should not be included within the built application and that also need additional type information present (`jasmine` in this case).
|
||||
Both parts of the project also share some but not all of the code within the project.
|
||||
As a result, two separate TypeScript configuration files (`tsconfig.app.json` and `tsconfig.spec.json`) are needed to ensure that each part of the application is configured properly and that the right types are used for each part.
|
||||
Also if web workers are used within a project, an additional tsconfig (`tsconfig.worker.json`) is needed.
|
||||
Web workers use similar but incompatible types to the main browser application.
|
||||
This requires the additional configuration file to ensure that the web worker files use the appropriate types and will build successfully.
|
||||
|
||||
While the Angular build system knows about all of these TypeScript configuration files, an IDE using TypeScript's language service does not.
|
||||
Because of this, an IDE will not be able to properly analyze the code from each part of the project and may generate false errors or make suggestions that are incorrect for certain files.
|
||||
By leveraging the new solution-style tsconfig, the IDE can now be aware of the configuration of each part of a project.
|
||||
This allows each file to be treated appropriately based on its tsconfig.
|
||||
IDE features such as error/warning reporting and auto-suggestion will operate more effectively as well.
|
||||
|
||||
The TypeScript 3.9 release [blog post](https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/#solution-style-tsconfig) also contains some additional information regarding this new feature.
|
@ -9,7 +9,7 @@ This process helps ensure that intentional changes to the options are kept in pl
|
||||
|
||||
TypeScript Configuration File(s) | Changed Property | Existing Value | New Value
|
||||
------------- | ------------- | ------------- | ------------- | -------------
|
||||
`<workspace base>/tsconfig.base.json` | `"module"` | `"esnext"` | `"es2020"`
|
||||
`<workspace base>/tsconfig.json` | `"module"` | `"esnext"` | `"es2020"`
|
||||
Used in `browser` builder options (`ng build` for applications) | `"module"` | `"esnext"` | `"es2020"`
|
||||
Used in `ng-packgr` builder options (`ng build` for libraries) | `"module"` | `"esnext"` | `"es2020"`
|
||||
Used in `karma` builder options (`ng test` for applications) | `"module"` | `"esnext"` | `"es2020"`
|
||||
|
@ -2,10 +2,9 @@
|
||||
|
||||
In a single-page app, you change what the user sees by showing or hiding portions of the display that correspond to particular components, rather than going out to the server to get a new page.
|
||||
As users perform application tasks, they need to move between the different [views](guide/glossary#view "Definition of view") that you have defined.
|
||||
To implement this kind of navigation within the single page of your app, you use the Angular **`Router`**.
|
||||
|
||||
To handle the navigation from one [view](guide/glossary#view) to the next, you use the Angular _router_.
|
||||
The router enables navigation by interpreting a browser URL as an instruction to change the view.
|
||||
To handle the navigation from one [view](guide/glossary#view) to the next, you use the Angular **`Router`**.
|
||||
The **`Router`** enables navigation by interpreting a browser URL as an instruction to change the view.
|
||||
|
||||
To explore a sample app featuring the router's primary features, see the <live-example></live-example>.
|
||||
|
||||
|
@ -114,6 +114,7 @@ In case of a false positive like these, there are a few options:
|
||||
|Strictness flag|Effect|
|
||||
|-|-|
|
||||
|`strictInputTypes`|Whether the assignability of a binding expression to the `@Input()` field is checked. Also affects the inference of directive generic types. |
|
||||
|`strictInputAccessModifiers`|Whether access modifiers such as `private`/`protected`/`readonly` are honored when assigning a binding expression to an `@Input()`. If disabled, the access modifiers of the `@Input` are ignored; only the type is checked.|
|
||||
|`strictNullInputTypes`|Whether `strictNullChecks` is honored when checking `@Input()` bindings (per `strictInputTypes`). Turning this off can be useful when using a library that was not built with `strictNullChecks` in mind.|
|
||||
|`strictAttributeTypes`|Whether to check `@Input()` bindings that are made using text attributes (for example, `<mat-tab label="Step 1">` vs `<mat-tab [label]="'Step 1'">`).
|
||||
|`strictSafeNavigationTypes`|Whether the return type of safe navigation operations (for example, `user?.name`) will be correctly inferred based on the type of `user`). If disabled, `user?.name` will be of type `any`.
|
||||
|
@ -18,32 +18,7 @@ that are important to Angular developers, including details about the following
|
||||
## Configuration files
|
||||
|
||||
A given Angular workspace contains several TypeScript configuration files.
|
||||
At the root level, there are two main TypeScript configuration files: a `tsconfig.json` file and a `tsconfig.base.json` file.
|
||||
|
||||
The `tsconfig.json` file is a ["Solution Style"](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#support-for-solution-style-tsconfigjson-files) TypeScript configuration file.
|
||||
Code editors and TypeScript’s language server use this file to improve development experience.
|
||||
Compilers do not use this file.
|
||||
|
||||
The `tsconfig.json` file contains a list of paths to the other TypeScript configuration files used in the workspace.
|
||||
|
||||
<code-example lang="json" header="tsconfig.json" linenums="false">
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
},
|
||||
{
|
||||
"path": "./projects/my-lib/tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
</code-example>
|
||||
|
||||
The `tsconfig.base.json` file specifies the base TypeScript and Angular compiler options that all projects in the workspace inherit.
|
||||
At the root `tsconfig.json` file specifies the base TypeScript and Angular compiler options that all projects in the workspace inherit.
|
||||
|
||||
The TypeScript and Angular have a wide range of options which can be used to configure type-checking features and generated output.
|
||||
For more information, see the [Configuration inheritance with extends](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#configuration-inheritance-with-extends) section of the TypeScript documentation.
|
||||
@ -55,9 +30,9 @@ For details about configuration inheritance, see the [Configuration inheritance
|
||||
|
||||
</div>
|
||||
|
||||
The initial `tsconfig.base.json` for an Angular workspace typically looks like the following example.
|
||||
The initial `tsconfig.json` for an Angular workspace typically looks like the following example.
|
||||
|
||||
<code-example lang="json" header="tsconfig.base.json" linenums="false">
|
||||
<code-example lang="json" header="tsconfig.json" linenums="false">
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
|
@ -48,8 +48,7 @@ src/
|
||||
app/ ... <i>application code</i>
|
||||
app.server.module.ts <i>* server-side application module</i>
|
||||
server.ts <i>* express web server</i>
|
||||
tsconfig.json <i>TypeScript solution style configuration</i>
|
||||
tsconfig.base.json <i>TypeScript base configuration</i>
|
||||
tsconfig.json <i>TypeScript base configuration</i>
|
||||
tsconfig.app.json <i>TypeScript browser application configuration</i>
|
||||
tsconfig.server.json <i>TypeScript server application configuration</i>
|
||||
tsconfig.spec.json <i>TypeScript tests configuration</i>
|
||||
|
@ -77,6 +77,5 @@ Read about the migrations the CLI handles for you automatically:
|
||||
|
||||
* [Migrating missing `@Directive()`/`@Component()` decorators](guide/migration-undecorated-classes)
|
||||
* [Migrating `ModuleWithProviders`](guide/migration-module-with-providers)
|
||||
* [Solution-style `tsconfig.json` migration](guide/migration-solution-style-tsconfig)
|
||||
* [`tslib` direct dependency migration](guide/migration-update-libraries-tslib)
|
||||
* [Update `module` and `target` compiler options migration](guide/migration-update-module-and-target-compiler-options)
|
||||
|
@ -14,12 +14,16 @@ The CLI does not support running Angular itself in a web worker.
|
||||
|
||||
To add a web worker to an existing project, use the Angular CLI `ng generate` command.
|
||||
|
||||
`ng generate web-worker` *location*
|
||||
```bash
|
||||
ng generate web-worker <location>
|
||||
```
|
||||
|
||||
You can add a web worker anywhere in your application.
|
||||
For example, to add a web worker to the root component, `src/app/app.component.ts`, run the following command.
|
||||
|
||||
`ng generate web-worker app`
|
||||
```bash
|
||||
ng generate web-worker app
|
||||
```
|
||||
|
||||
The command performs the following actions.
|
||||
|
||||
|
@ -86,24 +86,6 @@
|
||||
"groups": ["Angular"],
|
||||
"lead": "kara"
|
||||
},
|
||||
"matsko": {
|
||||
"name": "Matias Niemela",
|
||||
"picture": "matias.jpg",
|
||||
"twitter": "yearofmoo",
|
||||
"website": "http://yearofmoo.com",
|
||||
"bio": "Matias Niemela is a fullstack web developer who has been programming & building websites for over 10 years, and a core team member of AngularJS for two years. In the spring of 2015 Matias joined Angular full time at Google. In his free time Matias loves to build complex things and is always up for public speaking, travelling and tweaking his current Vim setup.",
|
||||
"groups": ["Angular"],
|
||||
"lead": "kara"
|
||||
},
|
||||
"kara": {
|
||||
"name": "Kara Erickson",
|
||||
"picture": "kara-erickson.jpg",
|
||||
"twitter": "karaforthewin",
|
||||
"website": "https://github.com/kara",
|
||||
"bio": "Kara is a software engineer on the Angular team at Google and a co-organizer of the Angular-SF Meetup. Prior to Google, she helped build UI components in Angular for guest management systems at OpenTable. She enjoys snacking indiscriminately and probably other things too.",
|
||||
"groups": ["Angular"],
|
||||
"lead": "igorminar"
|
||||
},
|
||||
"pkozlowski-opensource": {
|
||||
"name": "Pawel Kozlowski",
|
||||
"picture": "pawel.jpg",
|
||||
@ -618,26 +600,6 @@
|
||||
"bio": "Justin (aka Schwarty) is a Google Developer Expert in Web Technologies and Angular, the host and maintainer of the weekly AngularAir live video broadcast, educator, writer and content creator. He has Angular courses available on LinkedIn Learning and Pluralsight and loves passing on years of full stack development knowledge to help empower others to find their inner awesomeness!",
|
||||
"groups": ["GDE"]
|
||||
},
|
||||
"dennispbrown": {
|
||||
"name": "Denny Brown",
|
||||
"picture": "denny.jpg",
|
||||
"bio": "Denny is founder of Expert Support, a professional services firm specializing in technical communication, and leads the Angular technical writing team. His lifelong passion has been to reduce the time and effort required to understand complex technical information. Early on, he was Associate Chairman of the Computer Science Department at Stanford, where he taught introductory courses in programming. He also plays old-timers baseball in local leagues and national tournaments.",
|
||||
"groups": ["Angular"],
|
||||
"lead": "aikidave"
|
||||
},
|
||||
"jbogarthyde": {
|
||||
"name": "Judy Bogart",
|
||||
"picture": "judy.png",
|
||||
"groups": ["Angular"],
|
||||
"lead": "dennispbrown"
|
||||
},
|
||||
"rockument69": {
|
||||
"name": "Tony Bove",
|
||||
"picture": "rockument69.jpg",
|
||||
"bio": "Tony is a technical writer with Expert Support. His lifelong passions are helping people use technology, writing fiction, and playing music. When he's not working or playing the harmonica with friends in a bluegrass band, he's swimming and snorkeling on a Kauai beach and playing ball with his Irish Wolfhound. He's worked at home for decades before it became a thing.",
|
||||
"groups": ["Angular"],
|
||||
"lead": "aikidave"
|
||||
},
|
||||
"kapunahelewong": {
|
||||
"name": "Kapunahele Wong",
|
||||
"picture": "kapunahele.jpg",
|
||||
|
@ -858,11 +858,6 @@
|
||||
"title": "Missing @Injectable() Decorators",
|
||||
"tooltip": "Migration to add missing @Injectable() decorators and incomplete provider definitions."
|
||||
},
|
||||
{
|
||||
"url": "guide/migration-solution-style-tsconfig",
|
||||
"title": "Solution-style `tsconfig.json`",
|
||||
"tooltip": "Migration to create a solution-style `tsconfig.json`."
|
||||
},
|
||||
{
|
||||
"url": "guide/migration-update-libraries-tslib",
|
||||
"title": "`tslib` direct dependency",
|
||||
|
@ -385,7 +385,6 @@ next section on [Routing](tutorial/toh-pt5).
|
||||
path="toh-pt4/src/app/heroes/heroes.component.ts">
|
||||
</code-example>
|
||||
|
||||
The browser refreshes and the page displays the list of heroes.
|
||||
Refresh the browser to see the list of heroes, and scroll to the bottom to see the
|
||||
messages from the HeroService. Each time you click a hero, a new message appears to record
|
||||
the selection. Use the "clear" button to clear the message history.
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
|
||||
import { of } from 'rxjs';
|
||||
|
||||
@ -12,20 +12,22 @@ import { LocationService } from 'app/shared/location.service';
|
||||
describe('ContributorListComponent', () => {
|
||||
|
||||
let component: ContributorListComponent;
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let contributorService: TestContributorService;
|
||||
let locationService: TestLocationService;
|
||||
let contributorGroups: ContributorGroup[];
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
ContributorListComponent,
|
||||
{provide: ContributorService, useClass: TestContributorService },
|
||||
{provide: LocationService, useClass: TestLocationService }
|
||||
]);
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{provide: ContributorListComponent, deps: [ContributorService, LocationService] },
|
||||
{provide: ContributorService, useClass: TestContributorService, deps: [] },
|
||||
{provide: LocationService, useClass: TestLocationService, deps: [] }
|
||||
]
|
||||
});
|
||||
|
||||
locationService = injector.get(LocationService);
|
||||
contributorService = injector.get(ContributorService);
|
||||
locationService = injector.get(LocationService) as unknown as TestLocationService;
|
||||
contributorService = injector.get(ContributorService) as unknown as TestContributorService;
|
||||
contributorGroups = contributorService.testContributors;
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
|
||||
import { of } from 'rxjs';
|
||||
|
||||
@ -12,20 +12,22 @@ import { Category } from './resource.model';
|
||||
describe('ResourceListComponent', () => {
|
||||
|
||||
let component: ResourceListComponent;
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let resourceService: TestResourceService;
|
||||
let locationService: TestLocationService;
|
||||
let categories: Category[];
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
ResourceListComponent,
|
||||
{provide: ResourceService, useClass: TestResourceService },
|
||||
{provide: LocationService, useClass: TestLocationService }
|
||||
]);
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{provide: ResourceListComponent, deps: [ResourceService, LocationService] },
|
||||
{provide: ResourceService, useClass: TestResourceService, deps: [] },
|
||||
{provide: LocationService, useClass: TestLocationService, deps: [] }
|
||||
]
|
||||
});
|
||||
|
||||
locationService = injector.get(LocationService);
|
||||
resourceService = injector.get(ResourceService);
|
||||
locationService = injector.get(LocationService) as unknown as TestLocationService;
|
||||
resourceService = injector.get(ResourceService) as unknown as TestResourceService;
|
||||
categories = resourceService.testCategories;
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReflectiveInjector, NgZone } from '@angular/core';
|
||||
import { Injector, NgZone } from '@angular/core';
|
||||
import { fakeAsync, tick } from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { SearchService } from './search.service';
|
||||
@ -6,7 +6,7 @@ import { WebWorkerClient } from 'app/shared/web-worker';
|
||||
|
||||
describe('SearchService', () => {
|
||||
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let service: SearchService;
|
||||
let sendMessageSpy: jasmine.Spy;
|
||||
let mockWorker: WebWorkerClient;
|
||||
@ -16,10 +16,13 @@ describe('SearchService', () => {
|
||||
mockWorker = { sendMessage: sendMessageSpy } as any;
|
||||
spyOn(WebWorkerClient, 'create').and.returnValue(mockWorker);
|
||||
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
SearchService,
|
||||
{ provide: NgZone, useFactory: () => new NgZone({ enableLongStackTrace: false }) }
|
||||
]);
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{ provide: SearchService, deps: [NgZone]},
|
||||
{ provide: NgZone, useFactory: () => new NgZone({ enableLongStackTrace: false }), deps: [] }
|
||||
]
|
||||
});
|
||||
|
||||
service = injector.get(SearchService);
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
import { environment } from 'environments/environment';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { MockLocationService } from 'testing/location.service';
|
||||
@ -15,7 +15,7 @@ describe('Deployment service', () => {
|
||||
it('should get the mode from the `mode` query parameter if available', () => {
|
||||
const injector = getInjector();
|
||||
|
||||
const locationService: MockLocationService = injector.get(LocationService);
|
||||
const locationService = injector.get(LocationService) as unknown as MockLocationService;
|
||||
locationService.search.and.returnValue({ mode: 'bar' });
|
||||
|
||||
const deployment = injector.get(Deployment);
|
||||
@ -25,8 +25,8 @@ describe('Deployment service', () => {
|
||||
});
|
||||
|
||||
function getInjector() {
|
||||
return ReflectiveInjector.resolveAndCreate([
|
||||
Deployment,
|
||||
{ provide: LocationService, useFactory: () => new MockLocationService('') }
|
||||
]);
|
||||
return Injector.create({providers: [
|
||||
{ provide: Deployment, deps: [LocationService] },
|
||||
{ provide: LocationService, useFactory: () => new MockLocationService(''), deps: [] }
|
||||
]});
|
||||
}
|
||||
|
@ -1,18 +1,23 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
|
||||
import { GaService } from 'app/shared/ga.service';
|
||||
import { WindowToken } from 'app/shared/window';
|
||||
|
||||
describe('GaService', () => {
|
||||
let gaService: GaService;
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let gaSpy: jasmine.Spy;
|
||||
let mockWindow: any;
|
||||
|
||||
beforeEach(() => {
|
||||
gaSpy = jasmine.createSpy('ga');
|
||||
mockWindow = { ga: gaSpy };
|
||||
injector = ReflectiveInjector.resolveAndCreate([GaService, { provide: WindowToken, useFactory: () => mockWindow }]);
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{ provide: GaService, deps: [WindowToken] },
|
||||
{ provide: WindowToken, useFactory: () => mockWindow, deps: [] }
|
||||
]});
|
||||
|
||||
gaService = injector.get(GaService);
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
import { Location, LocationStrategy, PlatformLocation } from '@angular/common';
|
||||
import { MockLocationStrategy } from '@angular/common/testing';
|
||||
import { Subject } from 'rxjs';
|
||||
@ -9,26 +9,28 @@ import { LocationService } from './location.service';
|
||||
import { ScrollService } from './scroll.service';
|
||||
|
||||
describe('LocationService', () => {
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let location: MockLocationStrategy;
|
||||
let service: LocationService;
|
||||
let swUpdates: MockSwUpdatesService;
|
||||
let scrollService: MockScrollService;
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
LocationService,
|
||||
Location,
|
||||
{ provide: GaService, useClass: TestGaService },
|
||||
{ provide: LocationStrategy, useClass: MockLocationStrategy },
|
||||
{ provide: PlatformLocation, useClass: MockPlatformLocation },
|
||||
{ provide: SwUpdatesService, useClass: MockSwUpdatesService },
|
||||
{ provide: ScrollService, useClass: MockScrollService }
|
||||
]);
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{ provide: LocationService, deps: [GaService, Location, ScrollService, PlatformLocation, SwUpdatesService] },
|
||||
{ provide: Location, deps: [LocationStrategy, PlatformLocation] },
|
||||
{ provide: GaService, useClass: TestGaService, deps: [] },
|
||||
{ provide: LocationStrategy, useClass: MockLocationStrategy, deps: [] },
|
||||
{ provide: PlatformLocation, useClass: MockPlatformLocation, deps: [] },
|
||||
{ provide: SwUpdatesService, useClass: MockSwUpdatesService, deps: [] },
|
||||
{ provide: ScrollService, useClass: MockScrollService, deps: [] }
|
||||
]
|
||||
});
|
||||
|
||||
location = injector.get(LocationStrategy);
|
||||
service = injector.get(LocationService);
|
||||
swUpdates = injector.get(SwUpdatesService);
|
||||
location = injector.get(LocationStrategy) as unknown as MockLocationStrategy;
|
||||
service = injector.get(LocationService);
|
||||
swUpdates = injector.get(SwUpdatesService) as unknown as MockSwUpdatesService;
|
||||
scrollService = injector.get(ScrollService);
|
||||
});
|
||||
|
||||
@ -380,7 +382,7 @@ describe('LocationService', () => {
|
||||
let platformLocation: MockPlatformLocation;
|
||||
|
||||
beforeEach(() => {
|
||||
platformLocation = injector.get(PlatformLocation);
|
||||
platformLocation = injector.get(PlatformLocation) as unknown as MockPlatformLocation;
|
||||
});
|
||||
|
||||
it('should call replaceState on PlatformLocation', () => {
|
||||
@ -577,7 +579,7 @@ describe('LocationService', () => {
|
||||
let gaLocationChanged: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
const gaService = injector.get(GaService);
|
||||
const gaService = injector.get(GaService) as unknown as TestGaService;
|
||||
gaLocationChanged = gaService.locationChanged;
|
||||
// execute currentPath observable so that gaLocationChanged is called
|
||||
service.currentPath.subscribe();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ErrorHandler, ReflectiveInjector } from '@angular/core';
|
||||
import { ErrorHandler, Injector } from '@angular/core';
|
||||
import { Logger } from './logger.service';
|
||||
|
||||
describe('logger service', () => {
|
||||
@ -10,10 +10,10 @@ describe('logger service', () => {
|
||||
beforeEach(() => {
|
||||
logSpy = spyOn(console, 'log');
|
||||
warnSpy = spyOn(console, 'warn');
|
||||
const injector = ReflectiveInjector.resolveAndCreate([
|
||||
Logger,
|
||||
{ provide: ErrorHandler, useClass: MockErrorHandler }
|
||||
]);
|
||||
const injector = Injector.create({providers: [
|
||||
{ provide: Logger, deps: [ErrorHandler] },
|
||||
{ provide: ErrorHandler, useClass: MockErrorHandler, deps: [] }
|
||||
]});
|
||||
logger = injector.get(Logger);
|
||||
errorHandler = injector.get(ErrorHandler);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ErrorHandler, ReflectiveInjector } from '@angular/core';
|
||||
import { ErrorHandler, Injector } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { WindowToken } from 'app/shared/window';
|
||||
import { AppModule } from 'app/app.module';
|
||||
@ -14,11 +14,12 @@ describe('ReportingErrorHandler service', () => {
|
||||
onerrorSpy = jasmine.createSpy('onerror');
|
||||
superHandler = spyOn(ErrorHandler.prototype, 'handleError');
|
||||
|
||||
const injector = ReflectiveInjector.resolveAndCreate([
|
||||
{ provide: ErrorHandler, useClass: ReportingErrorHandler },
|
||||
{ provide: WindowToken, useFactory: () => ({ onerror: onerrorSpy }) }
|
||||
]);
|
||||
handler = injector.get(ErrorHandler);
|
||||
const injector = Injector.create({providers: [
|
||||
{ provide: ErrorHandler, useClass: ReportingErrorHandler, deps: [WindowToken] },
|
||||
{ provide: WindowToken, useFactory: () => ({ onerror: onerrorSpy }), deps: [] }
|
||||
]});
|
||||
|
||||
handler = injector.get(ErrorHandler) as unknown as ReportingErrorHandler;
|
||||
});
|
||||
|
||||
it('should be registered on the AppModule', () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Injector, ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
import { fakeAsync, tick } from '@angular/core/testing';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
|
||||
@ -151,11 +151,11 @@ describe('ScrollSpyService', () => {
|
||||
let scrollSpyService: ScrollSpyService;
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
injector = Injector.create({providers: [
|
||||
{ provide: DOCUMENT, useValue: { body: {} } },
|
||||
{ provide: ScrollService, useValue: { topOffset: 50 } },
|
||||
ScrollSpyService
|
||||
]);
|
||||
{ provide: ScrollSpyService, deps: [DOCUMENT, ScrollService] }
|
||||
]});
|
||||
|
||||
scrollSpyService = injector.get(ScrollSpyService);
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Location, LocationStrategy, PlatformLocation, ViewportScroller} from '@angular/common';
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {MockLocationStrategy, SpyLocation} from '@angular/common/testing';
|
||||
import {ReflectiveInjector} from '@angular/core';
|
||||
import {Injector} from '@angular/core';
|
||||
import {fakeAsync, tick} from '@angular/core/testing';
|
||||
|
||||
import {ScrollService, topMargin} from './scroll.service';
|
||||
@ -15,7 +15,7 @@ describe('ScrollService', () => {
|
||||
};
|
||||
|
||||
const topOfPageElem = {} as Element;
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let document: MockDocument;
|
||||
let platformLocation: MockPlatformLocation;
|
||||
let scrollService: ScrollService;
|
||||
@ -41,21 +41,25 @@ describe('ScrollService', () => {
|
||||
jasmine.createSpyObj('viewportScroller', ['getScrollPosition', 'scrollToPosition']);
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
{
|
||||
provide: ScrollService,
|
||||
useFactory: createScrollService,
|
||||
deps: [DOCUMENT, PlatformLocation, ViewportScroller, Location],
|
||||
},
|
||||
{provide: Location, useClass: SpyLocation}, {provide: DOCUMENT, useClass: MockDocument},
|
||||
{provide: PlatformLocation, useClass: MockPlatformLocation},
|
||||
{provide: ViewportScroller, useValue: viewportScrollerStub},
|
||||
{provide: LocationStrategy, useClass: MockLocationStrategy}
|
||||
]);
|
||||
injector = Injector.create( {
|
||||
providers: [
|
||||
{
|
||||
provide: ScrollService,
|
||||
useFactory: createScrollService,
|
||||
deps: [DOCUMENT, PlatformLocation, ViewportScroller, Location],
|
||||
},
|
||||
{provide: Location, useClass: SpyLocation, deps: [] },
|
||||
{provide: DOCUMENT, useClass: MockDocument, deps: []},
|
||||
{provide: PlatformLocation, useClass: MockPlatformLocation, deps: []},
|
||||
{provide: ViewportScroller, useValue: viewportScrollerStub},
|
||||
{provide: LocationStrategy, useClass: MockLocationStrategy, deps: []}
|
||||
]
|
||||
});
|
||||
|
||||
platformLocation = injector.get(PlatformLocation);
|
||||
document = injector.get(DOCUMENT);
|
||||
document = injector.get(DOCUMENT) as unknown as MockDocument;
|
||||
scrollService = injector.get(ScrollService);
|
||||
location = injector.get(Location);
|
||||
location = injector.get(Location) as unknown as SpyLocation;
|
||||
|
||||
spyOn(window, 'scrollBy');
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@ -7,7 +7,7 @@ import { ScrollItem, ScrollSpyInfo, ScrollSpyService } from 'app/shared/scroll-s
|
||||
import { TocItem, TocService } from './toc.service';
|
||||
|
||||
describe('TocService', () => {
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let scrollSpyService: MockScrollSpyService;
|
||||
let tocService: TocService;
|
||||
let lastTocList: TocItem[];
|
||||
@ -21,13 +21,14 @@ describe('TocService', () => {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
{ provide: DomSanitizer, useClass: TestDomSanitizer },
|
||||
injector = Injector.create({providers: [
|
||||
{ provide: DomSanitizer, useClass: TestDomSanitizer, deps: [] },
|
||||
{ provide: DOCUMENT, useValue: document },
|
||||
{ provide: ScrollSpyService, useClass: MockScrollSpyService },
|
||||
TocService,
|
||||
]);
|
||||
scrollSpyService = injector.get(ScrollSpyService);
|
||||
{ provide: ScrollSpyService, useClass: MockScrollSpyService, deps: [] },
|
||||
{ provide: TocService, deps: [DOCUMENT, DomSanitizer, ScrollSpyService] },
|
||||
]});
|
||||
|
||||
scrollSpyService = injector.get(ScrollSpyService) as unknown as MockScrollSpyService;
|
||||
tocService = injector.get(TocService);
|
||||
tocService.tocList.subscribe(tocList => lastTocList = tocList);
|
||||
});
|
||||
@ -330,7 +331,7 @@ describe('TocService', () => {
|
||||
});
|
||||
|
||||
it('should have bypassed HTML sanitizing of heading\'s innerHTML ', () => {
|
||||
const domSanitizer: TestDomSanitizer = injector.get(DomSanitizer);
|
||||
const domSanitizer: TestDomSanitizer = injector.get(DomSanitizer) as unknown as TestDomSanitizer;
|
||||
expect(domSanitizer.bypassSecurityTrustHtml)
|
||||
.toHaveBeenCalledWith('Setup to develop <i>locally</i>.');
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ApplicationRef, ReflectiveInjector } from '@angular/core';
|
||||
import { ApplicationRef, Injector } from '@angular/core';
|
||||
import { discardPeriodicTasks, fakeAsync, tick } from '@angular/core/testing';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
import { Subject } from 'rxjs';
|
||||
@ -8,7 +8,7 @@ import { SwUpdatesService } from './sw-updates.service';
|
||||
|
||||
|
||||
describe('SwUpdatesService', () => {
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let appRef: MockApplicationRef;
|
||||
let service: SwUpdatesService;
|
||||
let swu: MockSwUpdate;
|
||||
@ -21,16 +21,16 @@ describe('SwUpdatesService', () => {
|
||||
// run `setup()`/`tearDown()` in `beforeEach()`/`afterEach()` blocks. We use the `run()` helper
|
||||
// to call them inside each test's zone.
|
||||
const setup = (isSwUpdateEnabled: boolean) => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
{ provide: ApplicationRef, useClass: MockApplicationRef },
|
||||
{ provide: Logger, useClass: MockLogger },
|
||||
{ provide: SwUpdate, useFactory: () => new MockSwUpdate(isSwUpdateEnabled) },
|
||||
SwUpdatesService
|
||||
]);
|
||||
injector = Injector.create({providers: [
|
||||
{ provide: ApplicationRef, useClass: MockApplicationRef, deps: [] },
|
||||
{ provide: Logger, useClass: MockLogger, deps: [] },
|
||||
{ provide: SwUpdate, useFactory: () => new MockSwUpdate(isSwUpdateEnabled), deps: [] },
|
||||
{ provide: SwUpdatesService, deps: [ApplicationRef, Logger, SwUpdate] }
|
||||
]});
|
||||
|
||||
appRef = injector.get(ApplicationRef);
|
||||
appRef = injector.get(ApplicationRef) as unknown as MockApplicationRef;
|
||||
service = injector.get(SwUpdatesService);
|
||||
swu = injector.get(SwUpdate);
|
||||
swu = injector.get(SwUpdate) as unknown as MockSwUpdate;
|
||||
checkInterval = (service as any).checkInterval;
|
||||
};
|
||||
const tearDown = () => service.ngOnDestroy();
|
||||
|
@ -12,8 +12,10 @@
|
||||
// If a category becomes empty (e.g. BS and required), then the corresponding job must be commented
|
||||
// out in the CI configuration.
|
||||
var CIconfiguration = {
|
||||
'Chrome': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
'Firefox': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
// Chrome and Firefox run as part of the Bazel browser tests, so we do not run them as
|
||||
// part of the legacy Saucelabs tests.
|
||||
'Chrome': {unitTest: {target: null, required: false}, e2e: {target: null, required: true}},
|
||||
'Firefox': {unitTest: {target: null, required: false}, e2e: {target: null, required: true}},
|
||||
// Set ESR as a not required browser as it fails for Ivy acceptance tests.
|
||||
'FirefoxESR': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
|
||||
// Disabled because using the "beta" channel of Chrome can cause non-deterministic CI results.
|
||||
|
@ -5,7 +5,10 @@ ts_library(
|
||||
name = "commit-message",
|
||||
srcs = [
|
||||
"cli.ts",
|
||||
"commit-message-draft.ts",
|
||||
"config.ts",
|
||||
"parse.ts",
|
||||
"restore-commit-message.ts",
|
||||
"validate.ts",
|
||||
"validate-file.ts",
|
||||
"validate-range.ts",
|
||||
@ -23,9 +26,12 @@ ts_library(
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "validate-test",
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = ["validate.spec.ts"],
|
||||
srcs = [
|
||||
"parse.spec.ts",
|
||||
"validate.spec.ts",
|
||||
],
|
||||
deps = [
|
||||
":commit-message",
|
||||
"//dev-infra/utils",
|
||||
@ -40,7 +46,6 @@ jasmine_node_test(
|
||||
name = "test",
|
||||
bootstrap = ["//tools/testing:node_no_angular_es5"],
|
||||
deps = [
|
||||
":commit-message",
|
||||
":validate-test",
|
||||
"test_lib",
|
||||
],
|
||||
)
|
||||
|
@ -9,6 +9,7 @@ import * as yargs from 'yargs';
|
||||
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {restoreCommitMessage} from './restore-commit-message';
|
||||
import {validateFile} from './validate-file';
|
||||
import {validateCommitRange} from './validate-range';
|
||||
|
||||
@ -16,6 +17,30 @@ import {validateCommitRange} from './validate-range';
|
||||
export function buildCommitMessageParser(localYargs: yargs.Argv) {
|
||||
return localYargs.help()
|
||||
.strict()
|
||||
.command(
|
||||
'restore-commit-message-draft', false,
|
||||
args => {
|
||||
return args.option('file-env-variable', {
|
||||
type: 'string',
|
||||
array: true,
|
||||
conflicts: ['file'],
|
||||
required: true,
|
||||
description:
|
||||
'The key for the environment variable which holds the arguments for the\n' +
|
||||
'prepare-commit-msg hook as described here:\n' +
|
||||
'https://git-scm.com/docs/githooks#_prepare_commit_msg',
|
||||
coerce: arg => {
|
||||
const [file, source] = (process.env[arg] || '').split(' ');
|
||||
if (!file) {
|
||||
throw new Error(`Provided environment variable "${arg}" was not found.`);
|
||||
}
|
||||
return [file, source];
|
||||
},
|
||||
});
|
||||
},
|
||||
args => {
|
||||
restoreCommitMessage(args['file-env-variable'][0], args['file-env-variable'][1] as any);
|
||||
})
|
||||
.command(
|
||||
'pre-commit-validate', 'Validate the most recent commit message', {
|
||||
'file': {
|
||||
@ -38,7 +63,7 @@ export function buildCommitMessageParser(localYargs: yargs.Argv) {
|
||||
}
|
||||
},
|
||||
args => {
|
||||
const file = args.file || args.fileEnvVariable || '.git/COMMIT_EDITMSG';
|
||||
const file = args.file || args['file-env-variable'] || '.git/COMMIT_EDITMSG';
|
||||
validateFile(file);
|
||||
})
|
||||
.command(
|
||||
|
30
dev-infra/commit-message/commit-message-draft.ts
Normal file
30
dev-infra/commit-message/commit-message-draft.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @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 {existsSync, readFileSync, unlinkSync, writeFileSync} from 'fs';
|
||||
|
||||
/** Load the commit message draft from the file system if it exists. */
|
||||
export function loadCommitMessageDraft(basePath: string) {
|
||||
const commitMessageDraftPath = `${basePath}.ngDevSave`;
|
||||
if (existsSync(commitMessageDraftPath)) {
|
||||
return readFileSync(commitMessageDraftPath).toString();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** Remove the commit message draft from the file system. */
|
||||
export function deleteCommitMessageDraft(basePath: string) {
|
||||
const commitMessageDraftPath = `${basePath}.ngDevSave`;
|
||||
if (existsSync(commitMessageDraftPath)) {
|
||||
unlinkSync(commitMessageDraftPath);
|
||||
}
|
||||
}
|
||||
|
||||
/** Save the commit message draft to the file system for later retrieval. */
|
||||
export function saveCommitMessageDraft(basePath: string, commitMessage: string) {
|
||||
writeFileSync(`${basePath}.ngDevSave`, commitMessage);
|
||||
}
|
@ -12,7 +12,6 @@ export interface CommitMessageConfig {
|
||||
maxLineLength: number;
|
||||
minBodyLength: number;
|
||||
minBodyLengthTypeExcludes?: string[];
|
||||
types: string[];
|
||||
scopes: string[];
|
||||
}
|
||||
|
||||
@ -30,3 +29,46 @@ export function getCommitMessageConfig() {
|
||||
assertNoErrors(errors);
|
||||
return config as Required<typeof config>;
|
||||
}
|
||||
|
||||
/** Scope requirement level to be set for each commit type. */
|
||||
export enum ScopeRequirement {
|
||||
Required,
|
||||
Optional,
|
||||
Forbidden,
|
||||
}
|
||||
|
||||
/** A commit type */
|
||||
export interface CommitType {
|
||||
scope: ScopeRequirement;
|
||||
}
|
||||
|
||||
/** The valid commit types for Angular commit messages. */
|
||||
export const COMMIT_TYPES: {[key: string]: CommitType} = {
|
||||
build: {
|
||||
scope: ScopeRequirement.Forbidden,
|
||||
},
|
||||
ci: {
|
||||
scope: ScopeRequirement.Forbidden,
|
||||
},
|
||||
docs: {
|
||||
scope: ScopeRequirement.Optional,
|
||||
},
|
||||
feat: {
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
fix: {
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
perf: {
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
refactor: {
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
release: {
|
||||
scope: ScopeRequirement.Forbidden,
|
||||
},
|
||||
test: {
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
};
|
||||
|
85
dev-infra/commit-message/parse.spec.ts
Normal file
85
dev-infra/commit-message/parse.spec.ts
Normal file
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @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 {parseCommitMessage, ParsedCommitMessage} from './parse';
|
||||
|
||||
|
||||
const commitValues = {
|
||||
prefix: '',
|
||||
type: 'fix',
|
||||
scope: 'changed-area',
|
||||
summary: 'This is a short summary of the change',
|
||||
body: 'This is a longer description of the change Closes #1',
|
||||
};
|
||||
|
||||
function buildCommitMessage(params = {}) {
|
||||
const {prefix, type, scope, summary, body} = {...commitValues, ...params};
|
||||
return `${prefix}${type}${scope ? '(' + scope + ')' : ''}: ${summary}\n\n${body}`;
|
||||
}
|
||||
|
||||
|
||||
describe('commit message parsing:', () => {
|
||||
it('parses the scope', () => {
|
||||
const message = buildCommitMessage();
|
||||
expect(parseCommitMessage(message).scope).toBe(commitValues.scope);
|
||||
});
|
||||
|
||||
it('parses the type', () => {
|
||||
const message = buildCommitMessage();
|
||||
expect(parseCommitMessage(message).type).toBe(commitValues.type);
|
||||
});
|
||||
|
||||
it('parses the header', () => {
|
||||
const message = buildCommitMessage();
|
||||
expect(parseCommitMessage(message).header)
|
||||
.toBe(`${commitValues.type}(${commitValues.scope}): ${commitValues.summary}`);
|
||||
});
|
||||
|
||||
it('parses the body', () => {
|
||||
const message = buildCommitMessage();
|
||||
expect(parseCommitMessage(message).body).toBe(commitValues.body);
|
||||
});
|
||||
|
||||
it('parses the body without Github linking', () => {
|
||||
const body = 'This has linking\nCloses #1';
|
||||
const message = buildCommitMessage({body});
|
||||
expect(parseCommitMessage(message).bodyWithoutLinking).toBe('This has linking\n');
|
||||
});
|
||||
|
||||
it('parses the subject', () => {
|
||||
const message = buildCommitMessage();
|
||||
expect(parseCommitMessage(message).subject).toBe(commitValues.summary);
|
||||
});
|
||||
|
||||
it('identifies if a commit is a fixup', () => {
|
||||
const message1 = buildCommitMessage();
|
||||
expect(parseCommitMessage(message1).isFixup).toBe(false);
|
||||
|
||||
const message2 = buildCommitMessage({prefix: 'fixup! '});
|
||||
expect(parseCommitMessage(message2).isFixup).toBe(true);
|
||||
});
|
||||
|
||||
it('identifies if a commit is a revert', () => {
|
||||
const message1 = buildCommitMessage();
|
||||
expect(parseCommitMessage(message1).isRevert).toBe(false);
|
||||
|
||||
const message2 = buildCommitMessage({prefix: 'revert: '});
|
||||
expect(parseCommitMessage(message2).isRevert).toBe(true);
|
||||
|
||||
const message3 = buildCommitMessage({prefix: 'revert '});
|
||||
expect(parseCommitMessage(message3).isRevert).toBe(true);
|
||||
});
|
||||
|
||||
it('identifies if a commit is a squash', () => {
|
||||
const message1 = buildCommitMessage();
|
||||
expect(parseCommitMessage(message1).isSquash).toBe(false);
|
||||
|
||||
const message2 = buildCommitMessage({prefix: 'squash! '});
|
||||
expect(parseCommitMessage(message2).isSquash).toBe(true);
|
||||
});
|
||||
});
|
73
dev-infra/commit-message/parse.ts
Normal file
73
dev-infra/commit-message/parse.ts
Normal file
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/** A parsed commit message. */
|
||||
export interface ParsedCommitMessage {
|
||||
header: string;
|
||||
body: string;
|
||||
bodyWithoutLinking: string;
|
||||
type: string;
|
||||
scope: string;
|
||||
subject: string;
|
||||
isFixup: boolean;
|
||||
isSquash: boolean;
|
||||
isRevert: boolean;
|
||||
}
|
||||
|
||||
/** Regex determining if a commit is a fixup. */
|
||||
const FIXUP_PREFIX_RE = /^fixup! /i;
|
||||
/** Regex finding all github keyword links. */
|
||||
const GITHUB_LINKING_RE = /((closed?s?)|(fix(es)?(ed)?)|(resolved?s?))\s\#(\d+)/ig;
|
||||
/** Regex determining if a commit is a squash. */
|
||||
const SQUASH_PREFIX_RE = /^squash! /i;
|
||||
/** Regex determining if a commit is a revert. */
|
||||
const REVERT_PREFIX_RE = /^revert:? /i;
|
||||
/** Regex determining the scope of a commit if provided. */
|
||||
const TYPE_SCOPE_RE = /^(\w+)(?:\(([^)]+)\))?\:\s(.+)$/;
|
||||
/** Regex determining the entire header line of the commit. */
|
||||
const COMMIT_HEADER_RE = /^(.*)/i;
|
||||
/** Regex determining the body of the commit. */
|
||||
const COMMIT_BODY_RE = /^.*\n\n([\s\S]*)$/;
|
||||
|
||||
/** Parse a full commit message into its composite parts. */
|
||||
export function parseCommitMessage(commitMsg: string): ParsedCommitMessage {
|
||||
let header = '';
|
||||
let body = '';
|
||||
let bodyWithoutLinking = '';
|
||||
let type = '';
|
||||
let scope = '';
|
||||
let subject = '';
|
||||
|
||||
if (COMMIT_HEADER_RE.test(commitMsg)) {
|
||||
header = COMMIT_HEADER_RE.exec(commitMsg)![1]
|
||||
.replace(FIXUP_PREFIX_RE, '')
|
||||
.replace(SQUASH_PREFIX_RE, '');
|
||||
}
|
||||
if (COMMIT_BODY_RE.test(commitMsg)) {
|
||||
body = COMMIT_BODY_RE.exec(commitMsg)![1];
|
||||
bodyWithoutLinking = body.replace(GITHUB_LINKING_RE, '');
|
||||
}
|
||||
|
||||
if (TYPE_SCOPE_RE.test(header)) {
|
||||
const parsedCommitHeader = TYPE_SCOPE_RE.exec(header)!;
|
||||
type = parsedCommitHeader[1];
|
||||
scope = parsedCommitHeader[2];
|
||||
subject = parsedCommitHeader[3];
|
||||
}
|
||||
return {
|
||||
header,
|
||||
body,
|
||||
bodyWithoutLinking,
|
||||
type,
|
||||
scope,
|
||||
subject,
|
||||
isFixup: FIXUP_PREFIX_RE.test(commitMsg),
|
||||
isSquash: SQUASH_PREFIX_RE.test(commitMsg),
|
||||
isRevert: REVERT_PREFIX_RE.test(commitMsg),
|
||||
};
|
||||
}
|
48
dev-infra/commit-message/restore-commit-message.ts
Normal file
48
dev-infra/commit-message/restore-commit-message.ts
Normal 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 {info} from 'console';
|
||||
import {writeFileSync} from 'fs';
|
||||
|
||||
import {loadCommitMessageDraft} from './commit-message-draft';
|
||||
|
||||
/**
|
||||
* Restore the commit message draft to the git to be used as the default commit message.
|
||||
*
|
||||
* The source provided may be one of the sources described in
|
||||
* https://git-scm.com/docs/githooks#_prepare_commit_msg
|
||||
*/
|
||||
export function restoreCommitMessage(
|
||||
filePath: string, source?: 'message'|'template'|'squash'|'commit') {
|
||||
if (!!source) {
|
||||
info('Skipping commit message restoration attempt');
|
||||
if (source === 'message') {
|
||||
info('A commit message was already provided via the command with a -m or -F flag');
|
||||
}
|
||||
if (source === 'template') {
|
||||
info('A commit message was already provided via the -t flag or config.template setting');
|
||||
}
|
||||
if (source === 'squash') {
|
||||
info('A commit message was already provided as a merge action or via .git/MERGE_MSG');
|
||||
}
|
||||
if (source === 'commit') {
|
||||
info('A commit message was already provided through a revision specified via --fixup, -c,');
|
||||
info('-C or --amend flag');
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
/** A draft of a commit message. */
|
||||
const commitMessage = loadCommitMessageDraft(filePath);
|
||||
|
||||
// If the commit message draft has content, restore it into the provided filepath.
|
||||
if (commitMessage) {
|
||||
writeFileSync(filePath, commitMessage);
|
||||
}
|
||||
// Exit the process
|
||||
process.exit(0);
|
||||
}
|
@ -11,6 +11,7 @@ import {resolve} from 'path';
|
||||
import {getRepoBaseDir} from '../utils/config';
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {deleteCommitMessageDraft, saveCommitMessageDraft} from './commit-message-draft';
|
||||
import {validateCommitMessage} from './validate';
|
||||
|
||||
/** Validate commit message at the provided file path. */
|
||||
@ -18,8 +19,12 @@ export function validateFile(filePath: string) {
|
||||
const commitMessage = readFileSync(resolve(getRepoBaseDir(), filePath), 'utf8');
|
||||
if (validateCommitMessage(commitMessage)) {
|
||||
info('√ Valid commit message');
|
||||
deleteCommitMessageDraft(filePath);
|
||||
return;
|
||||
}
|
||||
// On all invalid commit messages, the commit message should be saved as a draft to be
|
||||
// restored on the next commit attempt.
|
||||
saveCommitMessageDraft(filePath, commitMessage);
|
||||
// If the validation did not return true, exit as a failure.
|
||||
process.exit(1);
|
||||
}
|
||||
|
@ -8,7 +8,8 @@
|
||||
import {info} from '../utils/console';
|
||||
import {exec} from '../utils/shelljs';
|
||||
|
||||
import {parseCommitMessage, validateCommitMessage, ValidateCommitMessageOptions} from './validate';
|
||||
import {parseCommitMessage} from './parse';
|
||||
import {validateCommitMessage, ValidateCommitMessageOptions} from './validate';
|
||||
|
||||
// Whether the provided commit is a fixup commit.
|
||||
const isNonFixup = (m: string) => !parseCommitMessage(m).isFixup;
|
||||
|
@ -18,13 +18,6 @@ const config: {commitMessage: CommitMessageConfig} = {
|
||||
commitMessage: {
|
||||
maxLineLength: 120,
|
||||
minBodyLength: 0,
|
||||
types: [
|
||||
'feat',
|
||||
'fix',
|
||||
'refactor',
|
||||
'release',
|
||||
'style',
|
||||
],
|
||||
scopes: [
|
||||
'common',
|
||||
'compiler',
|
||||
@ -33,7 +26,7 @@ const config: {commitMessage: CommitMessageConfig} = {
|
||||
]
|
||||
}
|
||||
};
|
||||
const TYPES = config.commitMessage.types.join(', ');
|
||||
const TYPES = Object.keys(validateConfig.COMMIT_TYPES).join(', ');
|
||||
const SCOPES = config.commitMessage.scopes.join(', ');
|
||||
const INVALID = false;
|
||||
const VALID = true;
|
||||
@ -47,7 +40,8 @@ describe('validate-commit-message.js', () => {
|
||||
lastError = '';
|
||||
|
||||
spyOn(console, 'error').and.callFake((msg: string) => lastError = msg);
|
||||
spyOn(validateConfig, 'getCommitMessageConfig').and.returnValue(config);
|
||||
spyOn(validateConfig, 'getCommitMessageConfig')
|
||||
.and.returnValue(config as ReturnType<typeof validateConfig.getCommitMessageConfig>);
|
||||
});
|
||||
|
||||
describe('validateMessage()', () => {
|
||||
@ -55,16 +49,16 @@ describe('validate-commit-message.js', () => {
|
||||
expect(validateCommitMessage('feat(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('release(packaging): something')).toBe(VALID);
|
||||
expect(validateCommitMessage('fix(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('fixup! release(packaging): something')).toBe(VALID);
|
||||
expect(validateCommitMessage('fixup! fix(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('squash! release(packaging): something')).toBe(VALID);
|
||||
expect(validateCommitMessage('squash! fix(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('Revert: "release(packaging): something"')).toBe(VALID);
|
||||
expect(validateCommitMessage('Revert: "fix(packaging): something"')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
});
|
||||
|
||||
@ -110,8 +104,8 @@ describe('validate-commit-message.js', () => {
|
||||
expect(validateCommitMessage('feat(bah): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('bah', 'feat(bah): something'));
|
||||
|
||||
expect(validateCommitMessage('style(webworker): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('webworker', 'style(webworker): something'));
|
||||
expect(validateCommitMessage('fix(webworker): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('webworker', 'fix(webworker): something'));
|
||||
|
||||
expect(validateCommitMessage('refactor(security): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('security', 'refactor(security): something'));
|
||||
@ -119,12 +113,12 @@ describe('validate-commit-message.js', () => {
|
||||
expect(validateCommitMessage('refactor(docs): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('docs', 'refactor(docs): something'));
|
||||
|
||||
expect(validateCommitMessage('release(angular): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('angular', 'release(angular): something'));
|
||||
expect(validateCommitMessage('feat(angular): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('angular', 'feat(angular): something'));
|
||||
});
|
||||
|
||||
it('should allow empty scope', () => {
|
||||
expect(validateCommitMessage('fix: blablabla')).toBe(VALID);
|
||||
expect(validateCommitMessage('build: blablabla')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
});
|
||||
|
||||
@ -243,7 +237,6 @@ describe('validate-commit-message.js', () => {
|
||||
maxLineLength: 120,
|
||||
minBodyLength: 30,
|
||||
minBodyLengthTypeExcludes: ['docs'],
|
||||
types: ['fix', 'docs'],
|
||||
scopes: ['core']
|
||||
}
|
||||
};
|
||||
|
@ -7,7 +7,8 @@
|
||||
*/
|
||||
import {error} from '../utils/console';
|
||||
|
||||
import {getCommitMessageConfig} from './config';
|
||||
import {COMMIT_TYPES, getCommitMessageConfig, ScopeRequirement} from './config';
|
||||
import {parseCommitMessage} from './parse';
|
||||
|
||||
/** Options for commit message validation. */
|
||||
export interface ValidateCommitMessageOptions {
|
||||
@ -15,53 +16,9 @@ export interface ValidateCommitMessageOptions {
|
||||
nonFixupCommitHeaders?: string[];
|
||||
}
|
||||
|
||||
const FIXUP_PREFIX_RE = /^fixup! /i;
|
||||
const GITHUB_LINKING_RE = /((closed?s?)|(fix(es)?(ed)?)|(resolved?s?))\s\#(\d+)/ig;
|
||||
const SQUASH_PREFIX_RE = /^squash! /i;
|
||||
const REVERT_PREFIX_RE = /^revert:? /i;
|
||||
const TYPE_SCOPE_RE = /^(\w+)(?:\(([^)]+)\))?\:\s(.+)$/;
|
||||
const COMMIT_HEADER_RE = /^(.*)/i;
|
||||
const COMMIT_BODY_RE = /^.*\n\n([\s\S]*)$/;
|
||||
/** Regex matching a URL for an entire commit body line. */
|
||||
const COMMIT_BODY_URL_LINE_RE = /^https?:\/\/.*$/;
|
||||
|
||||
/** Parse a full commit message into its composite parts. */
|
||||
export function parseCommitMessage(commitMsg: string) {
|
||||
let header = '';
|
||||
let body = '';
|
||||
let bodyWithoutLinking = '';
|
||||
let type = '';
|
||||
let scope = '';
|
||||
let subject = '';
|
||||
|
||||
if (COMMIT_HEADER_RE.test(commitMsg)) {
|
||||
header = COMMIT_HEADER_RE.exec(commitMsg)![1]
|
||||
.replace(FIXUP_PREFIX_RE, '')
|
||||
.replace(SQUASH_PREFIX_RE, '');
|
||||
}
|
||||
if (COMMIT_BODY_RE.test(commitMsg)) {
|
||||
body = COMMIT_BODY_RE.exec(commitMsg)![1];
|
||||
bodyWithoutLinking = body.replace(GITHUB_LINKING_RE, '');
|
||||
}
|
||||
|
||||
if (TYPE_SCOPE_RE.test(header)) {
|
||||
const parsedCommitHeader = TYPE_SCOPE_RE.exec(header)!;
|
||||
type = parsedCommitHeader[1];
|
||||
scope = parsedCommitHeader[2];
|
||||
subject = parsedCommitHeader[3];
|
||||
}
|
||||
return {
|
||||
header,
|
||||
body,
|
||||
bodyWithoutLinking,
|
||||
type,
|
||||
scope,
|
||||
subject,
|
||||
isFixup: FIXUP_PREFIX_RE.test(commitMsg),
|
||||
isSquash: SQUASH_PREFIX_RE.test(commitMsg),
|
||||
isRevert: REVERT_PREFIX_RE.test(commitMsg),
|
||||
};
|
||||
}
|
||||
|
||||
/** Validate a commit message against using the local repo's config. */
|
||||
export function validateCommitMessage(
|
||||
commitMsg: string, options: ValidateCommitMessageOptions = {}) {
|
||||
@ -129,8 +86,26 @@ export function validateCommitMessage(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.types.includes(commit.type)) {
|
||||
printError(`'${commit.type}' is not an allowed type.\n => TYPES: ${config.types.join(', ')}`);
|
||||
|
||||
|
||||
if (COMMIT_TYPES[commit.type] === undefined) {
|
||||
printError(`'${commit.type}' is not an allowed type.\n => TYPES: ${
|
||||
Object.keys(COMMIT_TYPES).join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The scope requirement level for the provided type of the commit message. */
|
||||
const scopeRequirementForType = COMMIT_TYPES[commit.type].scope;
|
||||
|
||||
if (scopeRequirementForType === ScopeRequirement.Forbidden && commit.scope) {
|
||||
printError(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${
|
||||
commit.scope}' was provided.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) {
|
||||
printError(
|
||||
`Scopes are required for commits with type '${commit.type}', but no scope was provided.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -10,12 +10,10 @@ ts_library(
|
||||
deps = [
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/cli-progress",
|
||||
"@npm//@types/inquirer",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/shelljs",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//cli-progress",
|
||||
"@npm//inquirer",
|
||||
"@npm//multimatch",
|
||||
"@npm//shelljs",
|
||||
"@npm//yargs",
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
import * as yargs from 'yargs';
|
||||
|
||||
import {allChangedFilesSince, allFiles} from '../utils/repo-files';
|
||||
import {allChangedFilesSince, allFiles, allStagedFiles} from '../utils/repo-files';
|
||||
|
||||
import {checkFiles, formatFiles} from './format';
|
||||
|
||||
@ -22,22 +22,31 @@ export function buildFormatParser(localYargs: yargs.Argv) {
|
||||
description: 'Run the formatter to check formatting rather than updating code format'
|
||||
})
|
||||
.command(
|
||||
'all', 'Run the formatter on all files in the repository', {},
|
||||
'all', 'Run the formatter on all files in the repository', args => args,
|
||||
({check}) => {
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(allFiles());
|
||||
})
|
||||
.command(
|
||||
'changed [shaOrRef]', 'Run the formatter on files changed since the provided sha/ref', {},
|
||||
'changed [shaOrRef]', 'Run the formatter on files changed since the provided sha/ref',
|
||||
args => args.positional('shaOrRef', {type: 'string'}),
|
||||
({shaOrRef, check}) => {
|
||||
const sha = shaOrRef || 'master';
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(allChangedFilesSince(sha));
|
||||
})
|
||||
.command('files <files..>', 'Run the formatter on provided files', {}, ({check, files}) => {
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(files);
|
||||
});
|
||||
.command(
|
||||
'staged', 'Run the formatter on all staged files', args => args,
|
||||
({check}) => {
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(allStagedFiles());
|
||||
})
|
||||
.command(
|
||||
'files <files..>', 'Run the formatter on provided files',
|
||||
args => args.positional('files', {array: true, type: 'string'}), ({check, files}) => {
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(files!);
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
|
@ -6,9 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {prompt} from 'inquirer';
|
||||
|
||||
import {error, info} from '../utils/console';
|
||||
import {error, info, promptConfirm} from '../utils/console';
|
||||
|
||||
import {runFormatterInParallel} from './run-commands-parallel';
|
||||
|
||||
@ -57,11 +55,7 @@ export async function checkFiles(files: string[]) {
|
||||
// If the command is run in a non-CI environment, prompt to format the files immediately.
|
||||
let runFormatter = false;
|
||||
if (!process.env['CI']) {
|
||||
runFormatter = (await prompt({
|
||||
type: 'confirm',
|
||||
name: 'runFormatter',
|
||||
message: 'Format the files now?',
|
||||
})).runFormatter;
|
||||
runFormatter = await promptConfirm('Format the files now?', true);
|
||||
}
|
||||
|
||||
if (runFormatter) {
|
||||
|
@ -12,18 +12,28 @@ import {error} from '../../utils/console';
|
||||
|
||||
import {discoverNewConflictsForPr} from './index';
|
||||
|
||||
/** The options available to the discover-new-conflicts command via CLI. */
|
||||
export interface DiscoverNewConflictsCommandOptions {
|
||||
date: number;
|
||||
'pr-number': number;
|
||||
}
|
||||
|
||||
/** Builds the discover-new-conflicts pull request command. */
|
||||
export function buildDiscoverNewConflictsCommand(yargs: Argv) {
|
||||
return yargs.option('date', {
|
||||
description: 'Only consider PRs updated since provided date',
|
||||
defaultDescription: '30 days ago',
|
||||
coerce: Date.parse,
|
||||
default: getThirtyDaysAgoDate,
|
||||
});
|
||||
export function buildDiscoverNewConflictsCommand(yargs: Argv):
|
||||
Argv<DiscoverNewConflictsCommandOptions> {
|
||||
return yargs
|
||||
.option('date', {
|
||||
description: 'Only consider PRs updated since provided date',
|
||||
defaultDescription: '30 days ago',
|
||||
coerce: (date) => typeof date === 'number' ? date : Date.parse(date),
|
||||
default: getThirtyDaysAgoDate(),
|
||||
})
|
||||
.positional('pr-number', {demandOption: true, type: 'number'});
|
||||
}
|
||||
|
||||
/** Handles the discover-new-conflicts pull request command. */
|
||||
export async function handleDiscoverNewConflictsCommand({prNumber, date}: Arguments) {
|
||||
export async function handleDiscoverNewConflictsCommand(
|
||||
{'pr-number': prNumber, date}: Arguments<DiscoverNewConflictsCommandOptions>) {
|
||||
// If a provided date is not able to be parsed, yargs provides it as NaN.
|
||||
if (isNaN(date)) {
|
||||
error('Unable to parse the value provided via --date flag');
|
||||
@ -33,11 +43,11 @@ export async function handleDiscoverNewConflictsCommand({prNumber, date}: Argume
|
||||
}
|
||||
|
||||
/** Gets a date object 30 days ago from today. */
|
||||
function getThirtyDaysAgoDate(): Date {
|
||||
function getThirtyDaysAgoDate() {
|
||||
const date = new Date();
|
||||
// Set the hours, minutes and seconds to 0 to only consider date.
|
||||
date.setHours(0, 0, 0, 0);
|
||||
// Set the date to 30 days in the past.
|
||||
date.setDate(date.getDate() - 30);
|
||||
return date;
|
||||
return date.getTime();
|
||||
}
|
||||
|
@ -12,17 +12,26 @@ import {error, red, yellow} from '../../utils/console';
|
||||
|
||||
import {GITHUB_TOKEN_GENERATE_URL, mergePullRequest} from './index';
|
||||
|
||||
/** The options available to the merge command via CLI. */
|
||||
export interface MergeCommandOptions {
|
||||
'github-token'?: string;
|
||||
'pr-number': number;
|
||||
}
|
||||
|
||||
/** Builds the options for the merge command. */
|
||||
export function buildMergeCommand(yargs: Argv) {
|
||||
return yargs.help().strict().option('github-token', {
|
||||
type: 'string',
|
||||
description: 'Github token. If not set, token is retrieved from the environment variables.'
|
||||
});
|
||||
export function buildMergeCommand(yargs: Argv): Argv<MergeCommandOptions> {
|
||||
return yargs.help()
|
||||
.strict()
|
||||
.positional('pr-number', {demandOption: true, type: 'number'})
|
||||
.option('github-token', {
|
||||
type: 'string',
|
||||
description: 'Github token. If not set, token is retrieved from the environment variables.'
|
||||
});
|
||||
}
|
||||
|
||||
/** Handles the merge command. i.e. performs the merge of a specified pull request. */
|
||||
export async function handleMergeCommand(args: Arguments) {
|
||||
const githubToken = args.githubToken || process.env.GITHUB_TOKEN || process.env.TOKEN;
|
||||
export async function handleMergeCommand(args: Arguments<MergeCommandOptions>) {
|
||||
const githubToken = args['github-token'] || process.env.GITHUB_TOKEN || process.env.TOKEN;
|
||||
if (!githubToken) {
|
||||
error(red('No Github token set. Please set the `GITHUB_TOKEN` environment variable.'));
|
||||
error(red('Alternatively, pass the `--github-token` command line flag.'));
|
||||
@ -30,5 +39,5 @@ export async function handleMergeCommand(args: Arguments) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await mergePullRequest(args.prNumber, githubToken);
|
||||
await mergePullRequest(args['pr-number'], githubToken);
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import {PullsListCommitsResponse, PullsMergeParams} from '@octokit/rest';
|
||||
import {prompt} from 'inquirer';
|
||||
|
||||
import {parseCommitMessage} from '../../../commit-message/validate';
|
||||
import {parseCommitMessage} from '../../../commit-message/parse';
|
||||
import {GitClient} from '../../../utils/git';
|
||||
import {GithubApiMergeMethod} from '../config';
|
||||
import {PullRequestFailure} from '../failures';
|
||||
|
@ -15,17 +15,26 @@ import {rebasePr} from './index';
|
||||
/** URL to the Github page where personal access tokens can be generated. */
|
||||
export const GITHUB_TOKEN_GENERATE_URL = `https://github.com/settings/tokens`;
|
||||
|
||||
/** Builds the rebase pull request command. */
|
||||
export function buildRebaseCommand(yargs: Argv) {
|
||||
return yargs.option('github-token', {
|
||||
type: 'string',
|
||||
description: 'Github token. If not set, token is retrieved from the environment variables.'
|
||||
});
|
||||
/** The options available to the rebase command via CLI. */
|
||||
export interface RebaseCommandOptions {
|
||||
'github-token'?: string;
|
||||
prNumber: number;
|
||||
}
|
||||
|
||||
/** Builds the rebase pull request command. */
|
||||
export function buildRebaseCommand(yargs: Argv): Argv<RebaseCommandOptions> {
|
||||
return yargs
|
||||
.option('github-token', {
|
||||
type: 'string',
|
||||
description: 'Github token. If not set, token is retrieved from the environment variables.'
|
||||
})
|
||||
.positional('prNumber', {type: 'number', demandOption: true});
|
||||
}
|
||||
|
||||
|
||||
/** Handles the rebase pull request command. */
|
||||
export async function handleRebaseCommand(args: Arguments) {
|
||||
const githubToken = args.githubToken || process.env.GITHUB_TOKEN || process.env.TOKEN;
|
||||
export async function handleRebaseCommand(args: Arguments<RebaseCommandOptions>) {
|
||||
const githubToken = args['github-token'] || process.env.GITHUB_TOKEN || process.env.TOKEN;
|
||||
if (!githubToken) {
|
||||
error('No Github token set. Please set the `GITHUB_TOKEN` environment variable.');
|
||||
error('Alternatively, pass the `--github-token` command line flag.');
|
||||
|
@ -9,7 +9,7 @@
|
||||
"ts-circular-deps": "./ts-circular-dependencies/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/benchpress": "0.2.0",
|
||||
"@angular/benchpress": "0.2.1",
|
||||
"@octokit/graphql": "<from-root>",
|
||||
"@octokit/types": "<from-root>",
|
||||
"brotli": "<from-root>",
|
||||
|
@ -30,20 +30,19 @@ export function tsCircularDependenciesBuilder(localYargs: yargs.Argv) {
|
||||
{type: 'string', demandOption: true, description: 'Path to the configuration file.'})
|
||||
.option('warnings', {type: 'boolean', description: 'Prints all warnings.'})
|
||||
.command(
|
||||
'check', 'Checks if the circular dependencies have changed.', {},
|
||||
(argv: yargs.Arguments) => {
|
||||
'check', 'Checks if the circular dependencies have changed.', args => args,
|
||||
argv => {
|
||||
const {config: configArg, warnings} = argv;
|
||||
const configPath = isAbsolute(configArg) ? configArg : resolve(configArg);
|
||||
const config = loadTestConfig(configPath);
|
||||
process.exit(main(false, config, warnings));
|
||||
process.exit(main(false, config, !!warnings));
|
||||
})
|
||||
.command(
|
||||
'approve', 'Approves the current circular dependencies.', {}, (argv: yargs.Arguments) => {
|
||||
const {config: configArg, warnings} = argv;
|
||||
const configPath = isAbsolute(configArg) ? configArg : resolve(configArg);
|
||||
const config = loadTestConfig(configPath);
|
||||
process.exit(main(true, config, warnings));
|
||||
});
|
||||
.command('approve', 'Approves the current circular dependencies.', args => args, argv => {
|
||||
const {config: configArg, warnings} = argv;
|
||||
const configPath = isAbsolute(configArg) ? configArg : resolve(configArg);
|
||||
const config = loadTestConfig(configPath);
|
||||
process.exit(main(true, config, !!warnings));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,6 +27,18 @@ export function allChangedFilesSince(sha = 'HEAD') {
|
||||
return Array.from(new Set([...diffFiles, ...untrackedFiles]));
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of all staged files which have been modified.
|
||||
*
|
||||
* Only added, created and modified files are listed as others (deleted, renamed, etc) aren't
|
||||
* changed or available as content to act upon.
|
||||
*/
|
||||
export function allStagedFiles() {
|
||||
return gitOutputAsArray(`git diff --staged --name-only --diff-filter=ACM`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function allFiles() {
|
||||
return gitOutputAsArray(`git ls-files`);
|
||||
}
|
||||
|
@ -1758,9 +1758,10 @@
|
||||
"packages/core/src/render3/index.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/render3/i18n.ts",
|
||||
"packages/core/src/render3/i18n/i18n_apply.ts",
|
||||
"packages/core/src/render3/interfaces/type_checks.ts",
|
||||
"packages/core/src/render3/index.ts"
|
||||
"packages/core/src/render3/index.ts",
|
||||
"packages/core/src/render3/instructions/i18n.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/render3/interfaces/container.ts",
|
||||
|
@ -35,6 +35,7 @@ export interface StrictTemplateOptions {
|
||||
strictContextGenerics?: boolean;
|
||||
strictDomEventTypes?: boolean;
|
||||
strictDomLocalRefTypes?: boolean;
|
||||
strictInputAccessModifiers?: boolean;
|
||||
strictInputTypes?: boolean;
|
||||
strictLiteralTypes?: boolean;
|
||||
strictNullInputTypes?: boolean;
|
||||
|
13
goldens/public-api/router/router.d.ts
vendored
13
goldens/public-api/router/router.d.ts
vendored
@ -51,6 +51,14 @@ export declare class ActivationStart {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export declare abstract class BaseRouteReuseStrategy implements RouteReuseStrategy {
|
||||
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null;
|
||||
shouldAttach(route: ActivatedRouteSnapshot): boolean;
|
||||
shouldDetach(route: ActivatedRouteSnapshot): boolean;
|
||||
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean;
|
||||
store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void;
|
||||
}
|
||||
|
||||
export declare interface CanActivate {
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
|
||||
}
|
||||
@ -370,7 +378,7 @@ export declare class RouterEvent {
|
||||
url: string);
|
||||
}
|
||||
|
||||
export declare class RouterLink {
|
||||
export declare class RouterLink implements OnChanges {
|
||||
fragment: string;
|
||||
preserveFragment: boolean;
|
||||
/** @deprecated */ set preserveQueryParams(value: boolean);
|
||||
@ -386,6 +394,7 @@ export declare class RouterLink {
|
||||
};
|
||||
get urlTree(): UrlTree;
|
||||
constructor(router: Router, route: ActivatedRoute, tabIndex: string, renderer: Renderer2, el: ElementRef);
|
||||
ngOnChanges(changes: SimpleChanges): void;
|
||||
onClick(): boolean;
|
||||
}
|
||||
|
||||
@ -421,7 +430,7 @@ export declare class RouterLinkWithHref implements OnChanges, OnDestroy {
|
||||
target: string;
|
||||
get urlTree(): UrlTree;
|
||||
constructor(router: Router, route: ActivatedRoute, locationStrategy: LocationStrategy);
|
||||
ngOnChanges(changes: {}): any;
|
||||
ngOnChanges(changes: SimpleChanges): any;
|
||||
ngOnDestroy(): any;
|
||||
onClick(button: number, ctrlKey: boolean, metaKey: boolean, shiftKey: boolean): boolean;
|
||||
}
|
||||
|
@ -62,7 +62,7 @@
|
||||
"bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.",
|
||||
"bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338",
|
||||
"bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info",
|
||||
"bundle": 1213130
|
||||
"bundle": 1214857
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,10 @@ INTEGRATION_TESTS = {
|
||||
},
|
||||
"dynamic-compiler": {"tags": ["no-ivy-aot"]},
|
||||
"hello_world__closure": {
|
||||
"commands": "payload_size_tracking",
|
||||
# TODO: Re-enable the payload_size_tracking command:
|
||||
# We should define ngDevMode to false in Closure, but --define only works in the global scope.
|
||||
# With ngDevMode not being set to false, this size tracking test provides little value but a lot of
|
||||
# headache to continue updating the size.
|
||||
"tags": ["no-ivy-aot"],
|
||||
},
|
||||
"hello_world__systemjs_umd": {
|
||||
|
@ -37,6 +37,9 @@ module.exports = function(config) {
|
||||
|
||||
'node_modules/core-js/client/core.js',
|
||||
'node_modules/jasmine-ajax/lib/mock-ajax.js',
|
||||
|
||||
// Dependencies built by Bazel. See `config.yml` for steps running before
|
||||
// the legacy Saucelabs tests run.
|
||||
'dist/bin/packages/zone.js/npm_package/bundles/zone.umd.js',
|
||||
'dist/bin/packages/zone.js/npm_package/bundles/zone-testing.umd.js',
|
||||
'dist/bin/packages/zone.js/npm_package/bundles/task-tracking.umd.js',
|
||||
|
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "10.1.0-next.4",
|
||||
"version": "10.1.0-next.6",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
@ -88,7 +88,7 @@
|
||||
"@types/shelljs": "^0.8.6",
|
||||
"@types/systemjs": "0.19.32",
|
||||
"@types/yaml": "^1.2.0",
|
||||
"@types/yargs": "^11.1.1",
|
||||
"@types/yargs": "^15.0.5",
|
||||
"@webcomponents/custom-elements": "^1.1.0",
|
||||
"angular": "npm:angular@1.7",
|
||||
"angular-1.5": "npm:angular@1.5",
|
||||
@ -153,7 +153,7 @@
|
||||
"typescript": "~3.9.5",
|
||||
"xhr2": "0.2.0",
|
||||
"yaml": "^1.7.2",
|
||||
"yargs": "15.3.0"
|
||||
"yargs": "^15.4.1"
|
||||
},
|
||||
"// 2": "devDependencies are not used under Bazel. Many can be removed after test.sh is deleted.",
|
||||
"devDependencies": {
|
||||
@ -209,7 +209,9 @@
|
||||
"cldr-data-coverage": "full",
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"commit-msg": "yarn -s ng-dev commit-message pre-commit-validate --file-env-variable HUSKY_GIT_PARAMS"
|
||||
"pre-commit": "yarn -s ng-dev format staged",
|
||||
"commit-msg": "yarn -s ng-dev commit-message pre-commit-validate --file-env-variable HUSKY_GIT_PARAMS",
|
||||
"prepare-commit-msg": "yarn -s ng-dev commit-message restore-commit-message-draft --file-env-variable HUSKY_GIT_PARAMS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>) {
|
||||
|
@ -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 {
|
||||
|
@ -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', () => {
|
||||
|
@ -19,9 +19,10 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
alias: 'source',
|
||||
describe:
|
||||
'A path (relative to the working directory) of the `node_modules` folder to process.',
|
||||
default: './node_modules'
|
||||
default: './node_modules',
|
||||
type: 'string',
|
||||
})
|
||||
.option('f', {alias: 'formats', hidden: true, array: true})
|
||||
.option('f', {alias: 'formats', hidden: true, array: true, type: 'string'})
|
||||
.option('p', {
|
||||
alias: 'properties',
|
||||
array: true,
|
||||
@ -29,7 +30,8 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
'An array of names of properties in package.json to compile (e.g. `module` or `main`)\n' +
|
||||
'Each of these properties should hold the path to a bundle-format.\n' +
|
||||
'If provided, only the specified properties are considered for processing.\n' +
|
||||
'If not provided, all the supported format properties (e.g. fesm2015, fesm5, es2015, esm2015, esm5, main, module) in the package.json are considered.'
|
||||
'If not provided, all the supported format properties (e.g. fesm2015, fesm5, es2015, esm2015, esm5, main, module) in the package.json are considered.',
|
||||
type: 'string',
|
||||
})
|
||||
.option('t', {
|
||||
alias: 'target',
|
||||
@ -37,6 +39,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
'A relative path (from the `source` path) to a single entry-point to process (plus its dependencies).\n' +
|
||||
'If this property is provided then `error-on-failed-entry-point` is forced to true.\n' +
|
||||
'This option overrides the `--use-program-dependencies` option.',
|
||||
type: 'string',
|
||||
})
|
||||
.option('use-program-dependencies', {
|
||||
type: 'boolean',
|
||||
@ -47,7 +50,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
.option('first-only', {
|
||||
describe:
|
||||
'If specified then only the first matching package.json property will be compiled.',
|
||||
type: 'boolean'
|
||||
type: 'boolean',
|
||||
})
|
||||
.option('create-ivy-entry-points', {
|
||||
describe:
|
||||
@ -78,6 +81,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
alias: 'loglevel',
|
||||
describe: 'The lowest severity logging message that should be output.',
|
||||
choices: ['debug', 'info', 'warn', 'error'],
|
||||
type: 'string',
|
||||
})
|
||||
.option('invalidate-entry-point-manifest', {
|
||||
describe:
|
||||
@ -105,7 +109,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
.help()
|
||||
.parse(args);
|
||||
|
||||
if (options['f'] && options['f'].length) {
|
||||
if (options.f?.length) {
|
||||
console.error(
|
||||
'The formats option (-f/--formats) has been removed. Consider the properties option (-p/--properties) instead.');
|
||||
process.exit(1);
|
||||
@ -113,12 +117,12 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
|
||||
setFileSystem(new NodeJSFileSystem());
|
||||
|
||||
const baseSourcePath = resolve(options['s'] || './node_modules');
|
||||
const propertiesToConsider: string[] = options['p'];
|
||||
const targetEntryPointPath = options['t'] ? options['t'] : undefined;
|
||||
const baseSourcePath = resolve(options.s || './node_modules');
|
||||
const propertiesToConsider = options.p;
|
||||
const targetEntryPointPath = options.t;
|
||||
const compileAllFormats = !options['first-only'];
|
||||
const createNewEntryPointFormats = options['create-ivy-entry-points'];
|
||||
const logLevel = options['l'] as keyof typeof LogLevel | undefined;
|
||||
const logLevel = options.l as keyof typeof LogLevel | undefined;
|
||||
const enableI18nLegacyMessageIdFormat = options['legacy-message-ids'];
|
||||
const invalidateEntryPointManifest = options['invalidate-entry-point-manifest'];
|
||||
const errorOnFailedEntryPoint = options['error-on-failed-entry-point'];
|
||||
@ -126,7 +130,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
// yargs is not so great at mixed string+boolean types, so we have to test tsconfig against a
|
||||
// string "false" to capture the `tsconfig=false` option.
|
||||
// And we have to convert the option to a string to handle `no-tsconfig`, which will be `false`.
|
||||
const tsConfigPath = `${options['tsconfig']}` === 'false' ? null : options['tsconfig'];
|
||||
const tsConfigPath = `${options.tsconfig}` === 'false' ? null : options.tsconfig;
|
||||
|
||||
const logger = logLevel && new ConsoleLogger(LogLevel[logLevel]);
|
||||
|
||||
@ -138,7 +142,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
createNewEntryPointFormats,
|
||||
logger,
|
||||
enableI18nLegacyMessageIdFormat,
|
||||
async: options['async'],
|
||||
async: options.async,
|
||||
invalidateEntryPointManifest,
|
||||
errorOnFailedEntryPoint,
|
||||
tsConfigPath,
|
||||
|
@ -10,7 +10,7 @@ import * as ts from 'typescript';
|
||||
import {absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
|
||||
|
||||
import {Logger} from '../../../src/ngtsc/logging';
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference} from '../../../src/ngtsc/reflection';
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference, TypeValueReferenceKind, ValueUnavailableKind} from '../../../src/ngtsc/reflection';
|
||||
import {isWithinPackage} from '../analysis/util';
|
||||
import {BundleProgram} from '../packages/bundle_program';
|
||||
import {findAll, getNameText, hasNameIdentifier, isDefined, stripDollarSuffix} from '../utils';
|
||||
@ -1594,7 +1594,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
{decorators: null, typeExpression: null};
|
||||
const nameNode = node.name;
|
||||
|
||||
let typeValueReference: TypeValueReference|null = null;
|
||||
let typeValueReference: TypeValueReference;
|
||||
if (typeExpression !== null) {
|
||||
// `typeExpression` is an expression in a "type" context. Resolve it to a declared value.
|
||||
// Either it's a reference to an imported type, or a type declared locally. Distinguish the
|
||||
@ -1603,7 +1603,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
if (decl !== null && decl.node !== null && decl.viaModule !== null &&
|
||||
isNamedDeclaration(decl.node)) {
|
||||
typeValueReference = {
|
||||
local: false,
|
||||
kind: TypeValueReferenceKind.IMPORTED,
|
||||
valueDeclaration: decl.node,
|
||||
moduleName: decl.viaModule,
|
||||
importedName: decl.node.name.text,
|
||||
@ -1611,11 +1611,16 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
};
|
||||
} else {
|
||||
typeValueReference = {
|
||||
local: true,
|
||||
kind: TypeValueReferenceKind.LOCAL,
|
||||
expression: typeExpression,
|
||||
defaultImportStatement: null,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
typeValueReference = {
|
||||
kind: TypeValueReferenceKind.UNAVAILABLE,
|
||||
reason: {kind: ValueUnavailableKind.MISSING_TYPE},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, KnownDeclaration, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
|
||||
import {getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, hasNameIdentifier} from '../utils';
|
||||
|
||||
import {Esm2015ReflectionHost, getClassDeclarationFromInnerDeclaration, getPropertyValueFromSymbol, isAssignmentStatement, ParamInfo} from './esm2015_host';
|
||||
@ -219,7 +219,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
||||
return Array.from(constructor.parameters);
|
||||
}
|
||||
|
||||
if (isSynthesizedConstructor(constructor)) {
|
||||
if (this.isSynthesizedConstructor(constructor)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -352,6 +352,219 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
||||
const classDeclarationParent = classSymbol.implementation.valueDeclaration.parent;
|
||||
return ts.isBlock(classDeclarationParent) ? Array.from(classDeclarationParent.statements) : [];
|
||||
}
|
||||
|
||||
///////////// Host Private Helpers /////////////
|
||||
|
||||
/**
|
||||
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
|
||||
* in the case no user-defined constructor exists and e.g. property initializers are used.
|
||||
* Those initializers need to be emitted into a constructor in JavaScript, so the TypeScript
|
||||
* compiler generates a synthetic constructor.
|
||||
*
|
||||
* We need to identify such constructors as ngcc needs to be able to tell if a class did
|
||||
* originally have a constructor in the TypeScript source. For ES5, we can not tell an
|
||||
* empty constructor apart from a synthesized constructor, but fortunately that does not
|
||||
* matter for the code generated by ngtsc.
|
||||
*
|
||||
* When a class has a superclass however, a synthesized constructor must not be considered
|
||||
* as a user-defined constructor as that prevents a base factory call from being created by
|
||||
* ngtsc, resulting in a factory function that does not inject the dependencies of the
|
||||
* superclass. Hence, we identify a default synthesized super call in the constructor body,
|
||||
* according to the structure that TypeScript's ES2015 to ES5 transformer generates in
|
||||
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1082-L1098
|
||||
*
|
||||
* Additionally, we handle synthetic delegate constructors that are emitted when TypeScript
|
||||
* downlevel's ES2015 synthetically generated to ES5. These vary slightly from the default
|
||||
* structure mentioned above because the ES2015 output uses a spread operator, for delegating
|
||||
* to the parent constructor, that is preserved through a TypeScript helper in ES5. e.g.
|
||||
*
|
||||
* ```
|
||||
* return _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
* ```
|
||||
*
|
||||
* Such constructs can be still considered as synthetic delegate constructors as they are
|
||||
* the product of a common TypeScript to ES5 synthetic constructor, just being downleveled
|
||||
* to ES5 using `tsc`. See: https://github.com/angular/angular/issues/38453.
|
||||
*
|
||||
*
|
||||
* @param constructor a constructor function to test
|
||||
* @returns true if the constructor appears to have been synthesized
|
||||
*/
|
||||
private isSynthesizedConstructor(constructor: ts.FunctionDeclaration): boolean {
|
||||
if (!constructor.body) return false;
|
||||
|
||||
const firstStatement = constructor.body.statements[0];
|
||||
if (!firstStatement) return false;
|
||||
|
||||
return this.isSynthesizedSuperThisAssignment(firstStatement) ||
|
||||
this.isSynthesizedSuperReturnStatement(firstStatement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies synthesized super calls which pass-through function arguments directly and are
|
||||
* being assigned to a common `_this` variable. The following patterns we intend to match:
|
||||
*
|
||||
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
|
||||
* ```
|
||||
* var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
|
||||
* ```
|
||||
* var _this = _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @param statement a statement that may be a synthesized super call
|
||||
* @returns true if the statement looks like a synthesized super call
|
||||
*/
|
||||
private isSynthesizedSuperThisAssignment(statement: ts.Statement): boolean {
|
||||
if (!ts.isVariableStatement(statement)) return false;
|
||||
|
||||
const variableDeclarations = statement.declarationList.declarations;
|
||||
if (variableDeclarations.length !== 1) return false;
|
||||
|
||||
const variableDeclaration = variableDeclarations[0];
|
||||
if (!ts.isIdentifier(variableDeclaration.name) ||
|
||||
!variableDeclaration.name.text.startsWith('_this'))
|
||||
return false;
|
||||
|
||||
const initializer = variableDeclaration.initializer;
|
||||
if (!initializer) return false;
|
||||
|
||||
return this.isSynthesizedDefaultSuperCall(initializer);
|
||||
}
|
||||
/**
|
||||
* Identifies synthesized super calls which pass-through function arguments directly and
|
||||
* are being returned. The following patterns correspond to synthetic super return calls:
|
||||
*
|
||||
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
|
||||
* ```
|
||||
* return _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
|
||||
* ```
|
||||
* return _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
* ```
|
||||
*
|
||||
* @param statement a statement that may be a synthesized super call
|
||||
* @returns true if the statement looks like a synthesized super call
|
||||
*/
|
||||
private isSynthesizedSuperReturnStatement(statement: ts.Statement): boolean {
|
||||
if (!ts.isReturnStatement(statement)) return false;
|
||||
|
||||
const expression = statement.expression;
|
||||
if (!expression) return false;
|
||||
|
||||
return this.isSynthesizedDefaultSuperCall(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies synthesized super calls which pass-through function arguments directly. The
|
||||
* synthetic delegate super call match the following patterns we intend to match:
|
||||
*
|
||||
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
|
||||
* ```
|
||||
* _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
|
||||
* ```
|
||||
* _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
* ```
|
||||
*
|
||||
* @param expression an expression that may represent a default super call
|
||||
* @returns true if the expression corresponds with the above form
|
||||
*/
|
||||
private isSynthesizedDefaultSuperCall(expression: ts.Expression): boolean {
|
||||
if (!isBinaryExpr(expression, ts.SyntaxKind.BarBarToken)) return false;
|
||||
if (expression.right.kind !== ts.SyntaxKind.ThisKeyword) return false;
|
||||
|
||||
const left = expression.left;
|
||||
if (isBinaryExpr(left, ts.SyntaxKind.AmpersandAmpersandToken)) {
|
||||
return isSuperNotNull(left.left) && this.isSuperApplyCall(left.right);
|
||||
} else {
|
||||
return this.isSuperApplyCall(left);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the expression corresponds to a `super` call passing through
|
||||
* function arguments without any modification. e.g.
|
||||
*
|
||||
* ```
|
||||
* _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* This structure is generated by TypeScript when transforming ES2015 to ES5, see
|
||||
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1148-L1163
|
||||
*
|
||||
* Additionally, we also handle cases where `arguments` are wrapped by a TypeScript spread helper.
|
||||
* This can happen if ES2015 class output contain auto-generated constructors due to class
|
||||
* members. The ES2015 output will be using `super(...arguments)` to delegate to the superclass,
|
||||
* but once downleveled to ES5, the spread operator will be persisted through a TypeScript spread
|
||||
* helper. For example:
|
||||
*
|
||||
* ```
|
||||
* _super.apply(this, __spread(arguments)) || this;
|
||||
* ```
|
||||
*
|
||||
* More details can be found in: https://github.com/angular/angular/issues/38453.
|
||||
*
|
||||
* @param expression an expression that may represent a default super call
|
||||
* @returns true if the expression corresponds with the above form
|
||||
*/
|
||||
private isSuperApplyCall(expression: ts.Expression): boolean {
|
||||
if (!ts.isCallExpression(expression) || expression.arguments.length !== 2) return false;
|
||||
|
||||
const targetFn = expression.expression;
|
||||
if (!ts.isPropertyAccessExpression(targetFn)) return false;
|
||||
if (!isSuperIdentifier(targetFn.expression)) return false;
|
||||
if (targetFn.name.text !== 'apply') return false;
|
||||
|
||||
const thisArgument = expression.arguments[0];
|
||||
if (thisArgument.kind !== ts.SyntaxKind.ThisKeyword) return false;
|
||||
|
||||
const argumentsExpr = expression.arguments[1];
|
||||
|
||||
// If the super is directly invoked with `arguments`, return `true`. This represents the
|
||||
// common TypeScript output where the delegate constructor super call matches the following
|
||||
// pattern: `super.apply(this, arguments)`.
|
||||
if (isArgumentsIdentifier(argumentsExpr)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The other scenario we intend to detect: The `arguments` variable might be wrapped with the
|
||||
// TypeScript spread helper (either through tslib or inlined). This can happen if an explicit
|
||||
// delegate constructor uses `super(...arguments)` in ES2015 and is downleveled to ES5 using
|
||||
// `--downlevelIteration`. The output in such cases would not directly pass the function
|
||||
// `arguments` to the `super` call, but wrap it in a TS spread helper. The output would match
|
||||
// the following pattern: `super.apply(this, tslib.__spread(arguments))`. We check for such
|
||||
// constructs below, but perform the detection of the call expression definition as last as
|
||||
// that is the most expensive operation here.
|
||||
if (!ts.isCallExpression(argumentsExpr) || argumentsExpr.arguments.length !== 1 ||
|
||||
!isArgumentsIdentifier(argumentsExpr.arguments[0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const argumentsCallExpr = argumentsExpr.expression;
|
||||
let argumentsCallDeclaration: Declaration|null = null;
|
||||
|
||||
// The `__spread` helper could be globally available, or accessed through a namespaced
|
||||
// import. Hence we support a property access here as long as it resolves to the actual
|
||||
// known TypeScript spread helper.
|
||||
if (ts.isIdentifier(argumentsCallExpr)) {
|
||||
argumentsCallDeclaration = this.getDeclarationOfIdentifier(argumentsCallExpr);
|
||||
} else if (
|
||||
ts.isPropertyAccessExpression(argumentsCallExpr) &&
|
||||
ts.isIdentifier(argumentsCallExpr.name)) {
|
||||
argumentsCallDeclaration = this.getDeclarationOfIdentifier(argumentsCallExpr.name);
|
||||
}
|
||||
|
||||
return argumentsCallDeclaration !== null &&
|
||||
argumentsCallDeclaration.known === KnownDeclaration.TsHelperSpread;
|
||||
}
|
||||
}
|
||||
|
||||
///////////// Internal Helpers /////////////
|
||||
@ -422,103 +635,8 @@ function reflectArrayElement(element: ts.Expression) {
|
||||
return ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
|
||||
* in the case no user-defined constructor exists and e.g. property initializers are used.
|
||||
* Those initializers need to be emitted into a constructor in JavaScript, so the TypeScript
|
||||
* compiler generates a synthetic constructor.
|
||||
*
|
||||
* We need to identify such constructors as ngcc needs to be able to tell if a class did
|
||||
* originally have a constructor in the TypeScript source. For ES5, we can not tell an
|
||||
* empty constructor apart from a synthesized constructor, but fortunately that does not
|
||||
* matter for the code generated by ngtsc.
|
||||
*
|
||||
* When a class has a superclass however, a synthesized constructor must not be considered
|
||||
* as a user-defined constructor as that prevents a base factory call from being created by
|
||||
* ngtsc, resulting in a factory function that does not inject the dependencies of the
|
||||
* superclass. Hence, we identify a default synthesized super call in the constructor body,
|
||||
* according to the structure that TypeScript's ES2015 to ES5 transformer generates in
|
||||
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1082-L1098
|
||||
*
|
||||
* @param constructor a constructor function to test
|
||||
* @returns true if the constructor appears to have been synthesized
|
||||
*/
|
||||
function isSynthesizedConstructor(constructor: ts.FunctionDeclaration): boolean {
|
||||
if (!constructor.body) return false;
|
||||
|
||||
const firstStatement = constructor.body.statements[0];
|
||||
if (!firstStatement) return false;
|
||||
|
||||
return isSynthesizedSuperThisAssignment(firstStatement) ||
|
||||
isSynthesizedSuperReturnStatement(firstStatement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies a synthesized super call of the form:
|
||||
*
|
||||
* ```
|
||||
* var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* @param statement a statement that may be a synthesized super call
|
||||
* @returns true if the statement looks like a synthesized super call
|
||||
*/
|
||||
function isSynthesizedSuperThisAssignment(statement: ts.Statement): boolean {
|
||||
if (!ts.isVariableStatement(statement)) return false;
|
||||
|
||||
const variableDeclarations = statement.declarationList.declarations;
|
||||
if (variableDeclarations.length !== 1) return false;
|
||||
|
||||
const variableDeclaration = variableDeclarations[0];
|
||||
if (!ts.isIdentifier(variableDeclaration.name) ||
|
||||
!variableDeclaration.name.text.startsWith('_this'))
|
||||
return false;
|
||||
|
||||
const initializer = variableDeclaration.initializer;
|
||||
if (!initializer) return false;
|
||||
|
||||
return isSynthesizedDefaultSuperCall(initializer);
|
||||
}
|
||||
/**
|
||||
* Identifies a synthesized super call of the form:
|
||||
*
|
||||
* ```
|
||||
* return _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* @param statement a statement that may be a synthesized super call
|
||||
* @returns true if the statement looks like a synthesized super call
|
||||
*/
|
||||
function isSynthesizedSuperReturnStatement(statement: ts.Statement): boolean {
|
||||
if (!ts.isReturnStatement(statement)) return false;
|
||||
|
||||
const expression = statement.expression;
|
||||
if (!expression) return false;
|
||||
|
||||
return isSynthesizedDefaultSuperCall(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the expression is of the form:
|
||||
*
|
||||
* ```
|
||||
* _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* This structure is generated by TypeScript when transforming ES2015 to ES5, see
|
||||
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1148-L1163
|
||||
*
|
||||
* @param expression an expression that may represent a default super call
|
||||
* @returns true if the expression corresponds with the above form
|
||||
*/
|
||||
function isSynthesizedDefaultSuperCall(expression: ts.Expression): boolean {
|
||||
if (!isBinaryExpr(expression, ts.SyntaxKind.BarBarToken)) return false;
|
||||
if (expression.right.kind !== ts.SyntaxKind.ThisKeyword) return false;
|
||||
|
||||
const left = expression.left;
|
||||
if (!isBinaryExpr(left, ts.SyntaxKind.AmpersandAmpersandToken)) return false;
|
||||
|
||||
return isSuperNotNull(left.left) && isSuperApplyCall(left.right);
|
||||
function isArgumentsIdentifier(expression: ts.Expression): boolean {
|
||||
return ts.isIdentifier(expression) && expression.text === 'arguments';
|
||||
}
|
||||
|
||||
function isSuperNotNull(expression: ts.Expression): boolean {
|
||||
@ -526,31 +644,6 @@ function isSuperNotNull(expression: ts.Expression): boolean {
|
||||
isSuperIdentifier(expression.left);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the expression is of the form
|
||||
*
|
||||
* ```
|
||||
* _super.apply(this, arguments)
|
||||
* ```
|
||||
*
|
||||
* @param expression an expression that may represent a default super call
|
||||
* @returns true if the expression corresponds with the above form
|
||||
*/
|
||||
function isSuperApplyCall(expression: ts.Expression): boolean {
|
||||
if (!ts.isCallExpression(expression) || expression.arguments.length !== 2) return false;
|
||||
|
||||
const targetFn = expression.expression;
|
||||
if (!ts.isPropertyAccessExpression(targetFn)) return false;
|
||||
if (!isSuperIdentifier(targetFn.expression)) return false;
|
||||
if (targetFn.name.text !== 'apply') return false;
|
||||
|
||||
const thisArgument = expression.arguments[0];
|
||||
if (thisArgument.kind !== ts.SyntaxKind.ThisKeyword) return false;
|
||||
|
||||
const argumentsArgument = expression.arguments[1];
|
||||
return ts.isIdentifier(argumentsArgument) && argumentsArgument.text === 'arguments';
|
||||
}
|
||||
|
||||
function isBinaryExpr(
|
||||
expression: ts.Expression, operator: ts.BinaryOperator): expression is ts.BinaryExpression {
|
||||
return ts.isBinaryExpression(expression) && expression.operatorToken.kind === operator;
|
||||
|
@ -10,7 +10,7 @@ import * as ts from 'typescript';
|
||||
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
|
||||
import {MockLogger} from '../../../src/ngtsc/logging/testing';
|
||||
import {ClassMemberKind, ConcreteDeclaration, CtorParameter, DownleveledEnum, InlineDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost} from '../../../src/ngtsc/reflection';
|
||||
import {ClassMemberKind, ConcreteDeclaration, CtorParameter, DownleveledEnum, InlineDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost, TypeValueReferenceKind} from '../../../src/ngtsc/reflection';
|
||||
import {getDeclaration} from '../../../src/ngtsc/testing';
|
||||
import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
|
||||
import {CommonJsReflectionHost} from '../../src/host/commonjs_host';
|
||||
@ -1456,6 +1456,210 @@ exports.MissingClass2 = MissingClass2;
|
||||
expect(decorators[0].args).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
function getConstructorParameters(
|
||||
constructor: string, mode?: 'inlined'|'inlined_with_suffix'|'imported') {
|
||||
let fileHeader = '';
|
||||
|
||||
switch (mode) {
|
||||
case 'imported':
|
||||
fileHeader = `const tslib = require('tslib');`;
|
||||
break;
|
||||
case 'inlined':
|
||||
fileHeader =
|
||||
`var __spread = (this && this.__spread) || function (...args) { /* ... */ }`;
|
||||
break;
|
||||
case 'inlined_with_suffix':
|
||||
fileHeader =
|
||||
`var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ }`;
|
||||
break;
|
||||
}
|
||||
const file = {
|
||||
name: _('/synthesized_constructors.js'),
|
||||
contents: `
|
||||
${fileHeader}
|
||||
|
||||
var TestClass = /** @class */ (function (_super) {
|
||||
__extends(TestClass, _super);
|
||||
${constructor}
|
||||
return TestClass;
|
||||
}(null));
|
||||
|
||||
exports.TestClass = TestClass;`,
|
||||
};
|
||||
|
||||
loadTestFiles([file]);
|
||||
const bundle = makeTestBundleProgram(file.name);
|
||||
const host =
|
||||
createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle));
|
||||
const classNode =
|
||||
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
|
||||
return host.getConstructorParameters(classNode);
|
||||
}
|
||||
|
||||
describe('TS -> ES5: synthesized constructors', () => {
|
||||
it('recognizes _this assignment from super call', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes super call as return statement', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('handles the case where a unique name was generated for _super or _this', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('does not consider constructors with parameters as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass(arg) {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters!.length).toBe(1);
|
||||
});
|
||||
|
||||
it('does not consider manual super calls as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
return _super.call(this) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters!.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not consider empty constructors as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`function TestClass() {}`);
|
||||
expect(parameters!.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// See: https://github.com/angular/angular/issues/38453.
|
||||
describe('ES2015 -> ES5: synthesized constructors through TSC downleveling', () => {
|
||||
it('recognizes delegate super call using inline spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using inline spread helper with suffix', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread$1(arguments)) || this;
|
||||
}`,
|
||||
'inlined_with_suffix');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
}`,
|
||||
'imported');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
describe('with class member assignment', () => {
|
||||
it('recognizes delegate super call using inline spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using inline spread helper with suffix', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread$1(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'inlined_with_suffix');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'imported');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the case where a unique name was generated for _super or _this', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1.apply(this, __spread(arguments)) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('does not consider constructors with parameters as synthesized', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass(arg) {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters!.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefinitionOfFunction()', () => {
|
||||
@ -1599,7 +1803,7 @@ exports.MissingClass2 = MissingClass2;
|
||||
isNamedVariableDeclaration);
|
||||
const ctrDecorators = host.getConstructorParameters(classNode)!;
|
||||
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference! as {
|
||||
local: true,
|
||||
kind: TypeValueReferenceKind.LOCAL,
|
||||
expression: ts.Identifier,
|
||||
defaultImportStatement: null,
|
||||
}).expression;
|
||||
|
@ -11,7 +11,7 @@ import * as ts from 'typescript';
|
||||
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
|
||||
import {MockLogger} from '../../../src/ngtsc/logging/testing';
|
||||
import {ClassMemberKind, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection';
|
||||
import {ClassMemberKind, isNamedVariableDeclaration, TypeValueReferenceKind} from '../../../src/ngtsc/reflection';
|
||||
import {getDeclaration} from '../../../src/ngtsc/testing';
|
||||
import {loadFakeCore, loadTestFiles, loadTsLib} from '../../../test/helpers';
|
||||
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
|
||||
@ -484,7 +484,7 @@ runInEachFileSystem(() => {
|
||||
isNamedVariableDeclaration);
|
||||
const ctrDecorators = host.getConstructorParameters(classNode)!;
|
||||
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference! as {
|
||||
local: true,
|
||||
kind: TypeValueReferenceKind.LOCAL,
|
||||
expression: ts.Identifier,
|
||||
defaultImportStatement: null,
|
||||
}).expression;
|
||||
|
@ -10,7 +10,7 @@ import * as ts from 'typescript';
|
||||
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
|
||||
import {MockLogger} from '../../../src/ngtsc/logging/testing';
|
||||
import {ClassMemberKind, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection';
|
||||
import {ClassMemberKind, isNamedFunctionDeclaration, isNamedVariableDeclaration, TypeValueReferenceKind} from '../../../src/ngtsc/reflection';
|
||||
import {getDeclaration} from '../../../src/ngtsc/testing';
|
||||
import {loadFakeCore, loadTestFiles, loadTsLib} from '../../../test/helpers';
|
||||
import {getIifeBody} from '../../src/host/esm2015_host';
|
||||
@ -544,7 +544,7 @@ export { AliasedDirective$1 };
|
||||
isNamedVariableDeclaration);
|
||||
const ctrDecorators = host.getConstructorParameters(classNode)!;
|
||||
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference! as {
|
||||
local: true,
|
||||
kind: TypeValueReferenceKind.LOCAL,
|
||||
expression: ts.Identifier,
|
||||
defaultImportStatement: null,
|
||||
}).expression;
|
||||
|
@ -11,7 +11,7 @@ import * as ts from 'typescript';
|
||||
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
|
||||
import {MockLogger} from '../../../src/ngtsc/logging/testing';
|
||||
import {ClassMemberKind, ConcreteDeclaration, CtorParameter, Decorator, DownleveledEnum, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost} from '../../../src/ngtsc/reflection';
|
||||
import {ClassMemberKind, ConcreteDeclaration, CtorParameter, Decorator, DownleveledEnum, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost, TypeValueReferenceKind} from '../../../src/ngtsc/reflection';
|
||||
import {getDeclaration} from '../../../src/ngtsc/testing';
|
||||
import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
|
||||
import {DelegatingReflectionHost} from '../../src/host/delegating_host';
|
||||
@ -1417,86 +1417,236 @@ runInEachFileSystem(() => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('synthesized constructors', () => {
|
||||
function getConstructorParameters(constructor: string) {
|
||||
const file = {
|
||||
name: _('/synthesized_constructors.js'),
|
||||
contents: `
|
||||
function getConstructorParameters(
|
||||
constructor: string,
|
||||
mode?: 'inlined'|'inlined_with_suffix'|'imported'|'imported_namespace') {
|
||||
let fileHeader = '';
|
||||
|
||||
switch (mode) {
|
||||
case 'imported':
|
||||
fileHeader = `import {__spread} from 'tslib';`;
|
||||
break;
|
||||
case 'imported_namespace':
|
||||
fileHeader = `import * as tslib from 'tslib';`;
|
||||
break;
|
||||
case 'inlined':
|
||||
fileHeader =
|
||||
`var __spread = (this && this.__spread) || function (...args) { /* ... */ }`;
|
||||
break;
|
||||
case 'inlined_with_suffix':
|
||||
fileHeader =
|
||||
`var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ }`;
|
||||
break;
|
||||
}
|
||||
|
||||
const file = {
|
||||
name: _('/synthesized_constructors.js'),
|
||||
contents: `
|
||||
${fileHeader}
|
||||
var TestClass = /** @class */ (function (_super) {
|
||||
__extends(TestClass, _super);
|
||||
${constructor}
|
||||
return TestClass;
|
||||
}(null));
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
loadTestFiles([file]);
|
||||
const bundle = makeTestBundleProgram(file.name);
|
||||
const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle));
|
||||
const classNode =
|
||||
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
|
||||
return host.getConstructorParameters(classNode);
|
||||
}
|
||||
loadTestFiles([file]);
|
||||
const bundle = makeTestBundleProgram(file.name);
|
||||
const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle));
|
||||
const classNode =
|
||||
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
|
||||
return host.getConstructorParameters(classNode);
|
||||
}
|
||||
|
||||
describe('TS -> ES5: synthesized constructors', () => {
|
||||
it('recognizes _this assignment from super call', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`);
|
||||
function TestClass() {
|
||||
var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes super call as return statement', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}`);
|
||||
function TestClass() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('handles the case where a unique name was generated for _super or _this', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}`);
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('does not consider constructors with parameters as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass(arg) {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}`);
|
||||
function TestClass(arg) {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters!.length).toBe(1);
|
||||
});
|
||||
|
||||
it('does not consider manual super calls as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
return _super.call(this) || this;
|
||||
}`);
|
||||
function TestClass() {
|
||||
return _super.call(this) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters!.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not consider empty constructors as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
}`);
|
||||
|
||||
const parameters = getConstructorParameters(`function TestClass() {}`);
|
||||
expect(parameters!.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// See: https://github.com/angular/angular/issues/38453.
|
||||
describe('ES2015 -> ES5: synthesized constructors through TSC downleveling', () => {
|
||||
it('recognizes delegate super call using inline spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using inline spread helper with suffix', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread$1(arguments)) || this;
|
||||
}`,
|
||||
'inlined_with_suffix');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'imported');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using namespace imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
}`,
|
||||
'imported_namespace');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
describe('with class member assignment', () => {
|
||||
it('recognizes delegate super call using inline spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using inline spread helper with suffix', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread$1(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'inlined_with_suffix');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'imported');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using namespace imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'imported_namespace');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the case where a unique name was generated for _super or _this', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1.apply(this, __spread(arguments)) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('does not consider constructors with parameters as synthesized', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass(arg) {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters!.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('(returned parameters `decorators.args`)', () => {
|
||||
it('should be an empty array if param decorator has no `args` property', () => {
|
||||
loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]);
|
||||
@ -1670,7 +1820,7 @@ runInEachFileSystem(() => {
|
||||
bundle.program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration);
|
||||
const ctrDecorators = host.getConstructorParameters(classNode)!;
|
||||
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference! as {
|
||||
local: true,
|
||||
kind: TypeValueReferenceKind.LOCAL,
|
||||
expression: ts.Identifier,
|
||||
defaultImportStatement: null,
|
||||
}).expression;
|
||||
|
@ -11,7 +11,7 @@ import * as ts from 'typescript';
|
||||
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
|
||||
import {MockLogger} from '../../../src/ngtsc/logging/testing';
|
||||
import {ClassMemberKind, ConcreteDeclaration, CtorParameter, DownleveledEnum, Import, InlineDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost} from '../../../src/ngtsc/reflection';
|
||||
import {ClassMemberKind, ConcreteDeclaration, CtorParameter, DownleveledEnum, Import, InlineDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost, TypeValueReferenceKind} from '../../../src/ngtsc/reflection';
|
||||
import {getDeclaration} from '../../../src/ngtsc/testing';
|
||||
import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
|
||||
import {DelegatingReflectionHost} from '../../src/host/delegating_host';
|
||||
@ -1564,6 +1564,231 @@ runInEachFileSystem(() => {
|
||||
expect(decorators[0].args).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
function getConstructorParameters(
|
||||
constructor: string, mode: 'inlined'|'inlined_with_suffix'|'imported' = 'imported') {
|
||||
let fileHeaderWithUmd = '';
|
||||
|
||||
switch (mode) {
|
||||
case 'imported':
|
||||
fileHeaderWithUmd = `
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('tslib'))) :
|
||||
typeof define === 'function' && define.amd ? define('test', ['exports', 'tslib'], factory) :
|
||||
(factory(global.test, global.tslib));
|
||||
}(this, (function (exports, tslib) { 'use strict';
|
||||
`;
|
||||
break;
|
||||
case 'inlined':
|
||||
fileHeaderWithUmd = `
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports)) :
|
||||
typeof define === 'function' && define.amd ? define('test', ['exports'], factory) :
|
||||
(factory(global.test));
|
||||
}(this, (function (exports) { 'use strict';
|
||||
|
||||
var __spread = (this && this.__spread) || function (...args) { /* ... */ }
|
||||
`;
|
||||
break;
|
||||
case 'inlined_with_suffix':
|
||||
fileHeaderWithUmd = `
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports)) :
|
||||
typeof define === 'function' && define.amd ? define('test', ['exports'], factory) :
|
||||
(factory(global.test));
|
||||
}(this, (function (exports) { 'use strict';
|
||||
|
||||
var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ }
|
||||
`;
|
||||
break;
|
||||
}
|
||||
|
||||
const file = {
|
||||
name: _('/synthesized_constructors.js'),
|
||||
contents: `
|
||||
${fileHeaderWithUmd}
|
||||
var TestClass = /** @class */ (function (_super) {
|
||||
__extends(TestClass, _super);
|
||||
${constructor}
|
||||
return TestClass;
|
||||
}(null));
|
||||
|
||||
exports.TestClass = TestClass;
|
||||
})));
|
||||
`,
|
||||
};
|
||||
|
||||
loadTestFiles([file]);
|
||||
const bundle = makeTestBundleProgram(file.name);
|
||||
const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle));
|
||||
const classNode =
|
||||
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
|
||||
return host.getConstructorParameters(classNode);
|
||||
}
|
||||
|
||||
describe('TS -> ES5: synthesized constructors', () => {
|
||||
it('recognizes _this assignment from super call', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes super call as return statement', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('handles the case where a unique name was generated for _super or _this', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('does not consider constructors with parameters as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass(arg) {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters!.length).toBe(1);
|
||||
});
|
||||
|
||||
it('does not consider manual super calls as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
return _super.call(this) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters!.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not consider empty constructors as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`function TestClass() {}`);
|
||||
expect(parameters!.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// See: https://github.com/angular/angular/issues/38453.
|
||||
describe('ES2015 -> ES5: synthesized constructors through TSC downleveling', () => {
|
||||
it('recognizes delegate super call using inline spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using inline spread helper with suffix', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread$1(arguments)) || this;
|
||||
}`,
|
||||
'inlined_with_suffix');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, tslib_1.__spread(arguments)) || this;
|
||||
}`,
|
||||
'imported');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
describe('with class member assignment', () => {
|
||||
it('recognizes delegate super call using inline spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using inline spread helper with suffix', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread$1(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'inlined_with_suffix');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, tslib_1.__spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'imported');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the case where a unique name was generated for _super or _this', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1.apply(this, __spread(arguments)) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('does not consider constructors with parameters as synthesized', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass(arg) {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters!.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefinitionOfFunction()', () => {
|
||||
@ -1709,7 +1934,7 @@ runInEachFileSystem(() => {
|
||||
bundle.program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration);
|
||||
const ctrDecorators = host.getConstructorParameters(classNode)!;
|
||||
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference! as {
|
||||
local: true,
|
||||
kind: TypeValueReferenceKind.LOCAL,
|
||||
expression: ts.Identifier,
|
||||
defaultImportStatement: null,
|
||||
}).expression;
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
@ -7,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import {CtorParameter} from '../../../src/ngtsc/reflection';
|
||||
import {CtorParameter, TypeValueReferenceKind} from '../../../src/ngtsc/reflection';
|
||||
|
||||
/**
|
||||
* Check that a given list of `CtorParameter`s has `typeValueReference`s of specific `ts.Identifier`
|
||||
@ -18,19 +17,21 @@ export function expectTypeValueReferencesForParameters(
|
||||
parameters!.forEach((param, idx) => {
|
||||
const expected = expectedParams[idx];
|
||||
if (expected !== null) {
|
||||
if (param.typeValueReference === null) {
|
||||
if (param.typeValueReference.kind === TypeValueReferenceKind.UNAVAILABLE) {
|
||||
fail(`Incorrect typeValueReference generated, expected ${expected}`);
|
||||
} else if (param.typeValueReference.local && fromModule !== null) {
|
||||
} else if (
|
||||
param.typeValueReference.kind === TypeValueReferenceKind.LOCAL && fromModule !== null) {
|
||||
fail(`Incorrect typeValueReference generated, expected non-local`);
|
||||
} else if (!param.typeValueReference.local && fromModule === null) {
|
||||
} else if (
|
||||
param.typeValueReference.kind !== TypeValueReferenceKind.LOCAL && fromModule === null) {
|
||||
fail(`Incorrect typeValueReference generated, expected local`);
|
||||
} else if (param.typeValueReference.local) {
|
||||
} else if (param.typeValueReference.kind === TypeValueReferenceKind.LOCAL) {
|
||||
if (!ts.isIdentifier(param.typeValueReference.expression)) {
|
||||
fail(`Incorrect typeValueReference generated, expected identifer`);
|
||||
fail(`Incorrect typeValueReference generated, expected identifier`);
|
||||
} else {
|
||||
expect(param.typeValueReference.expression.text).toEqual(expected);
|
||||
}
|
||||
} else if (param.typeValueReference !== null) {
|
||||
} else if (param.typeValueReference.kind === TypeValueReferenceKind.IMPORTED) {
|
||||
expect(param.typeValueReference.moduleName).toBe(fromModule!);
|
||||
expect(param.typeValueReference.importedName).toBe(expected);
|
||||
}
|
||||
|
@ -172,7 +172,6 @@ export function readCommandLineAndConfiguration(
|
||||
emitFlags: api.EmitFlags.Default
|
||||
};
|
||||
}
|
||||
const allDiagnostics: Diagnostics = [];
|
||||
const config = readConfiguration(project, cmdConfig.options);
|
||||
const options = {...config.options, ...existingOptions};
|
||||
if (options.locale) {
|
||||
|
@ -15,7 +15,7 @@ import {absoluteFrom, relative} from '../../file_system';
|
||||
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
|
||||
import {DependencyTracker} from '../../incremental/api';
|
||||
import {IndexingContext} from '../../indexer';
|
||||
import {DirectiveMeta, extractDirectiveGuards, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
|
||||
import {DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
|
||||
import {flattenInheritedDirectiveMetadata} from '../../metadata/src/inheritance';
|
||||
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
|
||||
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
|
||||
@ -51,7 +51,7 @@ export interface ComponentAnalysisData {
|
||||
*/
|
||||
meta: Omit<R3ComponentMetadata, ComponentMetadataResolvedFields>;
|
||||
baseClass: Reference<ClassDeclaration>|'dynamic'|null;
|
||||
guards: ReturnType<typeof extractDirectiveGuards>;
|
||||
typeCheckMeta: DirectiveTypeCheckMeta;
|
||||
template: ParsedTemplateWithSource;
|
||||
metadataStmt: Statement|null;
|
||||
|
||||
@ -327,7 +327,7 @@ export class ComponentDecoratorHandler implements
|
||||
i18nUseExternalIds: this.i18nUseExternalIds,
|
||||
relativeContextFilePath,
|
||||
},
|
||||
guards: extractDirectiveGuards(node, this.reflector),
|
||||
typeCheckMeta: extractDirectiveTypeCheckMeta(node, metadata.inputs, this.reflector),
|
||||
metadataStmt: generateSetClassMetadataCall(
|
||||
node, this.reflector, this.defaultImportRecorder, this.isCore,
|
||||
this.annotateForClosureCompiler),
|
||||
@ -356,7 +356,7 @@ export class ComponentDecoratorHandler implements
|
||||
queries: analysis.meta.queries.map(query => query.propertyName),
|
||||
isComponent: true,
|
||||
baseClass: analysis.baseClass,
|
||||
...analysis.guards,
|
||||
...analysis.typeCheckMeta,
|
||||
});
|
||||
|
||||
this.injectableRegistry.registerInjectable(node);
|
||||
|
@ -11,8 +11,8 @@ import * as ts from 'typescript';
|
||||
|
||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||
import {DefaultImportRecorder, Reference} from '../../imports';
|
||||
import {InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
|
||||
import {extractDirectiveGuards} from '../../metadata/src/util';
|
||||
import {DirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
|
||||
import {extractDirectiveTypeCheckMeta} from '../../metadata/src/util';
|
||||
import {DynamicValue, EnumValue, PartialEvaluator} from '../../partial_evaluator';
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, filterToMembersWithDecorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
|
||||
import {LocalModuleScopeRegistry} from '../../scope';
|
||||
@ -35,7 +35,7 @@ const LIFECYCLE_HOOKS = new Set([
|
||||
|
||||
export interface DirectiveHandlerData {
|
||||
baseClass: Reference<ClassDeclaration>|'dynamic'|null;
|
||||
guards: ReturnType<typeof extractDirectiveGuards>;
|
||||
typeCheckMeta: DirectiveTypeCheckMeta;
|
||||
meta: R3DirectiveMetadata;
|
||||
metadataStmt: Statement|null;
|
||||
providersRequiringFactory: Set<Reference<ClassDeclaration>>|null;
|
||||
@ -102,7 +102,7 @@ export class DirectiveDecoratorHandler implements
|
||||
node, this.reflector, this.defaultImportRecorder, this.isCore,
|
||||
this.annotateForClosureCompiler),
|
||||
baseClass: readBaseClass(node, this.reflector, this.evaluator),
|
||||
guards: extractDirectiveGuards(node, this.reflector),
|
||||
typeCheckMeta: extractDirectiveTypeCheckMeta(node, analysis.inputs, this.reflector),
|
||||
providersRequiringFactory
|
||||
}
|
||||
};
|
||||
@ -122,7 +122,7 @@ export class DirectiveDecoratorHandler implements
|
||||
queries: analysis.meta.queries.map(query => query.propertyName),
|
||||
isComponent: false,
|
||||
baseClass: analysis.baseClass,
|
||||
...analysis.guards,
|
||||
...analysis.typeCheckMeta,
|
||||
});
|
||||
|
||||
this.injectableRegistry.registerInjectable(node);
|
||||
|
@ -10,7 +10,7 @@ import {Expression, ExternalExpr, FunctionExpr, Identifiers, InvokeFunctionExpr,
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {DefaultImportRecorder} from '../../imports';
|
||||
import {CtorParameter, Decorator, ReflectionHost} from '../../reflection';
|
||||
import {CtorParameter, Decorator, ReflectionHost, TypeValueReferenceKind} from '../../reflection';
|
||||
|
||||
import {valueReferenceToExpression, wrapFunctionExpressionsInParens} from './util';
|
||||
|
||||
@ -70,8 +70,8 @@ export function generateSetClassMetadataCall(
|
||||
`Duplicate decorated properties found on class '${clazz.name.text}': ` +
|
||||
duplicateDecoratedMemberNames.join(', '));
|
||||
}
|
||||
const decoratedMembers =
|
||||
classMembers.map(member => classMemberToMetadata(member.name, member.decorators!, isCore));
|
||||
const decoratedMembers = classMembers.map(
|
||||
member => classMemberToMetadata(member.nameNode ?? member.name, member.decorators!, isCore));
|
||||
if (decoratedMembers.length > 0) {
|
||||
metaPropDecorators = ts.createObjectLiteral(decoratedMembers);
|
||||
}
|
||||
@ -105,7 +105,7 @@ function ctorParameterToMetadata(
|
||||
isCore: boolean): Expression {
|
||||
// Parameters sometimes have a type that can be referenced. If so, then use it, otherwise
|
||||
// its type is undefined.
|
||||
const type = param.typeValueReference !== null ?
|
||||
const type = param.typeValueReference.kind !== TypeValueReferenceKind.UNAVAILABLE ?
|
||||
valueReferenceToExpression(param.typeValueReference, defaultImportRecorder) :
|
||||
new LiteralExpr(undefined);
|
||||
|
||||
@ -127,7 +127,7 @@ function ctorParameterToMetadata(
|
||||
* Convert a reflected class member to metadata.
|
||||
*/
|
||||
function classMemberToMetadata(
|
||||
name: string, decorators: Decorator[], isCore: boolean): ts.PropertyAssignment {
|
||||
name: ts.PropertyName|string, decorators: Decorator[], isCore: boolean): ts.PropertyAssignment {
|
||||
const ngDecorators = decorators.filter(dec => isAngularDecorator(dec, isCore))
|
||||
.map((decorator: Decorator) => decoratorToMetadata(decorator));
|
||||
const decoratorMeta = ts.createArrayLiteral(ngDecorators);
|
||||
|
@ -12,13 +12,9 @@ import * as ts from 'typescript';
|
||||
import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../diagnostics';
|
||||
import {DefaultImportRecorder, ImportFlags, Reference, ReferenceEmitter} from '../../imports';
|
||||
import {ForeignFunctionResolver, PartialEvaluator} from '../../partial_evaluator';
|
||||
import {ClassDeclaration, CtorParameter, Decorator, Import, isNamedClassDeclaration, ReflectionHost, TypeValueReference} from '../../reflection';
|
||||
import {ClassDeclaration, CtorParameter, Decorator, Import, ImportedTypeValueReference, isNamedClassDeclaration, LocalTypeValueReference, ReflectionHost, TypeValueReference, TypeValueReferenceKind, UnavailableValue, ValueUnavailableKind} from '../../reflection';
|
||||
import {DeclarationData} from '../../scope';
|
||||
|
||||
export enum ConstructorDepErrorKind {
|
||||
NO_SUITABLE_TOKEN,
|
||||
}
|
||||
|
||||
export type ConstructorDeps = {
|
||||
deps: R3DependencyMetadata[];
|
||||
}|{
|
||||
@ -29,7 +25,7 @@ export type ConstructorDeps = {
|
||||
export interface ConstructorDepError {
|
||||
index: number;
|
||||
param: CtorParameter;
|
||||
kind: ConstructorDepErrorKind;
|
||||
reason: UnavailableValue;
|
||||
}
|
||||
|
||||
export function getConstructorDependencies(
|
||||
@ -94,10 +90,14 @@ export function getConstructorDependencies(
|
||||
resolved = R3ResolvedDependencyType.ChangeDetectorRef;
|
||||
}
|
||||
if (token === null) {
|
||||
if (param.typeValueReference.kind !== TypeValueReferenceKind.UNAVAILABLE) {
|
||||
throw new Error(
|
||||
'Illegal state: expected value reference to be unavailable if no token is present');
|
||||
}
|
||||
errors.push({
|
||||
index: idx,
|
||||
kind: ConstructorDepErrorKind.NO_SUITABLE_TOKEN,
|
||||
param,
|
||||
reason: param.typeValueReference.reason,
|
||||
});
|
||||
} else {
|
||||
deps.push({token, attribute, optional, self, skipSelf, host, resolved});
|
||||
@ -118,18 +118,15 @@ export function getConstructorDependencies(
|
||||
* file in which the `TypeValueReference` originated.
|
||||
*/
|
||||
export function valueReferenceToExpression(
|
||||
valueRef: TypeValueReference, defaultImportRecorder: DefaultImportRecorder): Expression;
|
||||
valueRef: LocalTypeValueReference|ImportedTypeValueReference,
|
||||
defaultImportRecorder: DefaultImportRecorder): Expression;
|
||||
export function valueReferenceToExpression(
|
||||
valueRef: null, defaultImportRecorder: DefaultImportRecorder): null;
|
||||
valueRef: TypeValueReference, defaultImportRecorder: DefaultImportRecorder): Expression|null;
|
||||
export function valueReferenceToExpression(
|
||||
valueRef: TypeValueReference|null, defaultImportRecorder: DefaultImportRecorder): Expression|
|
||||
null;
|
||||
export function valueReferenceToExpression(
|
||||
valueRef: TypeValueReference|null, defaultImportRecorder: DefaultImportRecorder): Expression|
|
||||
null {
|
||||
if (valueRef === null) {
|
||||
valueRef: TypeValueReference, defaultImportRecorder: DefaultImportRecorder): Expression|null {
|
||||
if (valueRef.kind === TypeValueReferenceKind.UNAVAILABLE) {
|
||||
return null;
|
||||
} else if (valueRef.local) {
|
||||
} else if (valueRef.kind === TypeValueReferenceKind.LOCAL) {
|
||||
if (defaultImportRecorder !== null && valueRef.defaultImportStatement !== null &&
|
||||
ts.isIdentifier(valueRef.expression)) {
|
||||
defaultImportRecorder.recordImportedIdentifier(
|
||||
@ -137,16 +134,10 @@ export function valueReferenceToExpression(
|
||||
}
|
||||
return new WrappedNodeExpr(valueRef.expression);
|
||||
} else {
|
||||
// TODO(alxhub): this cast is necessary because the g3 typescript version doesn't narrow here.
|
||||
const ref = valueRef as {
|
||||
moduleName: string;
|
||||
importedName: string;
|
||||
nestedPath: string[]|null;
|
||||
};
|
||||
let importExpr: Expression =
|
||||
new ExternalExpr({moduleName: ref.moduleName, name: ref.importedName});
|
||||
if (ref.nestedPath !== null) {
|
||||
for (const property of ref.nestedPath) {
|
||||
new ExternalExpr({moduleName: valueRef.moduleName, name: valueRef.importedName});
|
||||
if (valueRef.nestedPath !== null) {
|
||||
for (const property of valueRef.nestedPath) {
|
||||
importExpr = new ReadPropExpr(importExpr, property);
|
||||
}
|
||||
}
|
||||
@ -195,17 +186,82 @@ export function validateConstructorDependencies(
|
||||
return deps.deps;
|
||||
} else {
|
||||
// TODO(alxhub): this cast is necessary because the g3 typescript version doesn't narrow here.
|
||||
const {param, index} = (deps as {errors: ConstructorDepError[]}).errors[0];
|
||||
// There is at least one error.
|
||||
throw new FatalDiagnosticError(
|
||||
ErrorCode.PARAM_MISSING_TOKEN, param.nameNode,
|
||||
`No suitable injection token for parameter '${param.name || index}' of class '${
|
||||
clazz.name.text}'.\n` +
|
||||
(param.typeNode !== null ? `Found ${param.typeNode.getText()}` :
|
||||
'no type or decorator'));
|
||||
const error = (deps as {errors: ConstructorDepError[]}).errors[0];
|
||||
throw createUnsuitableInjectionTokenError(clazz, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fatal error with diagnostic for an invalid injection token.
|
||||
* @param clazz The class for which the injection token was unavailable.
|
||||
* @param error The reason why no valid injection token is available.
|
||||
*/
|
||||
function createUnsuitableInjectionTokenError(
|
||||
clazz: ClassDeclaration, error: ConstructorDepError): FatalDiagnosticError {
|
||||
const {param, index, reason} = error;
|
||||
let chainMessage: string|undefined = undefined;
|
||||
let hints: ts.DiagnosticRelatedInformation[]|undefined = undefined;
|
||||
switch (reason.kind) {
|
||||
case ValueUnavailableKind.UNSUPPORTED:
|
||||
chainMessage = 'Consider using the @Inject decorator to specify an injection token.';
|
||||
hints = [
|
||||
makeRelatedInformation(reason.typeNode, 'This type is not supported as injection token.'),
|
||||
];
|
||||
break;
|
||||
case ValueUnavailableKind.NO_VALUE_DECLARATION:
|
||||
chainMessage = 'Consider using the @Inject decorator to specify an injection token.';
|
||||
hints = [
|
||||
makeRelatedInformation(
|
||||
reason.typeNode,
|
||||
'This type does not have a value, so it cannot be used as injection token.'),
|
||||
makeRelatedInformation(reason.decl, 'The type is declared here.'),
|
||||
];
|
||||
break;
|
||||
case ValueUnavailableKind.TYPE_ONLY_IMPORT:
|
||||
chainMessage =
|
||||
'Consider changing the type-only import to a regular import, or use the @Inject decorator to specify an injection token.';
|
||||
hints = [
|
||||
makeRelatedInformation(
|
||||
reason.typeNode,
|
||||
'This type is imported using a type-only import, which prevents it from being usable as an injection token.'),
|
||||
makeRelatedInformation(reason.importClause, 'The type-only import occurs here.'),
|
||||
];
|
||||
break;
|
||||
case ValueUnavailableKind.NAMESPACE:
|
||||
chainMessage = 'Consider using the @Inject decorator to specify an injection token.';
|
||||
hints = [
|
||||
makeRelatedInformation(
|
||||
reason.typeNode,
|
||||
'This type corresponds with a namespace, which cannot be used as injection token.'),
|
||||
makeRelatedInformation(reason.importClause, 'The namespace import occurs here.'),
|
||||
];
|
||||
break;
|
||||
case ValueUnavailableKind.UNKNOWN_REFERENCE:
|
||||
chainMessage = 'The type should reference a known declaration.';
|
||||
hints = [makeRelatedInformation(reason.typeNode, 'This type could not be resolved.')];
|
||||
break;
|
||||
case ValueUnavailableKind.MISSING_TYPE:
|
||||
chainMessage =
|
||||
'Consider adding a type to the parameter or use the @Inject decorator to specify an injection token.';
|
||||
break;
|
||||
}
|
||||
|
||||
const chain: ts.DiagnosticMessageChain = {
|
||||
messageText: `No suitable injection token for parameter '${param.name || index}' of class '${
|
||||
clazz.name.text}'.`,
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
code: 0,
|
||||
next: [{
|
||||
messageText: chainMessage,
|
||||
category: ts.DiagnosticCategory.Message,
|
||||
code: 0,
|
||||
}],
|
||||
};
|
||||
|
||||
return new FatalDiagnosticError(ErrorCode.PARAM_MISSING_TOKEN, param.nameNode, chain, hints);
|
||||
}
|
||||
|
||||
export function toR3Reference(
|
||||
valueRef: Reference, typeRef: Reference, valueContext: ts.SourceFile,
|
||||
typeContext: ts.SourceFile, refEmitter: ReferenceEmitter): R3Reference {
|
||||
|
@ -90,6 +90,19 @@ runInEachFileSystem(() => {
|
||||
`);
|
||||
expect(res).toBe('');
|
||||
});
|
||||
|
||||
it('should preserve quotes around class member names', () => {
|
||||
const res = compileAndPrint(`
|
||||
import {Component, Input} from '@angular/core';
|
||||
|
||||
@Component('metadata') class Target {
|
||||
@Input() 'has-dashes-in-name' = 123;
|
||||
@Input() noDashesInName = 456;
|
||||
}
|
||||
`);
|
||||
expect(res).toContain(
|
||||
`{ 'has-dashes-in-name': [{ type: Input }], noDashesInName: [{ type: Input }] })`);
|
||||
});
|
||||
});
|
||||
|
||||
function compileAndPrint(contents: string): string {
|
||||
|
@ -147,6 +147,18 @@ export interface StrictTemplateOptions {
|
||||
*/
|
||||
strictInputTypes?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to check if the input binding attempts to assign to a restricted field (readonly,
|
||||
* private, or protected) on the directive/component.
|
||||
*
|
||||
* Defaults to `false`, even if "fullTemplateTypeCheck", "strictTemplates" and/or
|
||||
* "strictInputTypes" is set. Note that if `strictInputTypes` is not set, or set to `false`, this
|
||||
* flag has no effect.
|
||||
*
|
||||
* Tracking issue for enabling this by default: https://github.com/angular/angular/issues/38400
|
||||
*/
|
||||
strictInputAccessModifiers?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to use strict null types for input bindings for directives.
|
||||
*
|
||||
|
@ -415,6 +415,7 @@ export class NgCompiler {
|
||||
checkQueries: false,
|
||||
checkTemplateBodies: true,
|
||||
checkTypeOfInputBindings: strictTemplates,
|
||||
honorAccessModifiersForInputBindings: false,
|
||||
strictNullInputBindings: strictTemplates,
|
||||
checkTypeOfAttributes: strictTemplates,
|
||||
// Even in full template type-checking mode, DOM binding checks are not quite ready yet.
|
||||
@ -442,6 +443,7 @@ export class NgCompiler {
|
||||
checkTemplateBodies: false,
|
||||
checkTypeOfInputBindings: false,
|
||||
strictNullInputBindings: false,
|
||||
honorAccessModifiersForInputBindings: false,
|
||||
checkTypeOfAttributes: false,
|
||||
checkTypeOfDomBindings: false,
|
||||
checkTypeOfOutputEvents: false,
|
||||
@ -462,6 +464,10 @@ export class NgCompiler {
|
||||
typeCheckingConfig.checkTypeOfInputBindings = this.options.strictInputTypes;
|
||||
typeCheckingConfig.applyTemplateContextGuards = this.options.strictInputTypes;
|
||||
}
|
||||
if (this.options.strictInputAccessModifiers !== undefined) {
|
||||
typeCheckingConfig.honorAccessModifiersForInputBindings =
|
||||
this.options.strictInputAccessModifiers;
|
||||
}
|
||||
if (this.options.strictNullInputTypes !== undefined) {
|
||||
typeCheckingConfig.strictNullInputBindings = this.options.strictNullInputTypes;
|
||||
}
|
||||
|
@ -9,4 +9,4 @@
|
||||
export * from './src/api';
|
||||
export {DtsMetadataReader} from './src/dts';
|
||||
export {CompoundMetadataRegistry, LocalMetadataRegistry, InjectableClassRegistry} from './src/registry';
|
||||
export {extractDirectiveGuards, CompoundMetadataReader} from './src/util';
|
||||
export {extractDirectiveTypeCheckMeta, CompoundMetadataReader} from './src/util';
|
||||
|
@ -32,19 +32,62 @@ export interface NgModuleMeta {
|
||||
rawDeclarations: ts.Expression|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Typing metadata collected for a directive within an NgModule's scope.
|
||||
*/
|
||||
export interface DirectiveTypeCheckMeta {
|
||||
/**
|
||||
* List of static `ngTemplateGuard_xx` members found on the Directive's class.
|
||||
* @see `TemplateGuardMeta`
|
||||
*/
|
||||
ngTemplateGuards: TemplateGuardMeta[];
|
||||
|
||||
/**
|
||||
* Whether the Directive's class has a static ngTemplateContextGuard function.
|
||||
*/
|
||||
hasNgTemplateContextGuard: boolean;
|
||||
|
||||
/**
|
||||
* The set of input fields which have a corresponding static `ngAcceptInputType_` on the
|
||||
* Directive's class. This allows inputs to accept a wider range of types and coerce the input to
|
||||
* a narrower type with a getter/setter. See https://angular.io/guide/template-typecheck.
|
||||
*/
|
||||
coercedInputFields: Set<string>;
|
||||
|
||||
/**
|
||||
* The set of input fields which map to `readonly`, `private`, or `protected` members in the
|
||||
* Directive's class.
|
||||
*/
|
||||
restrictedInputFields: Set<string>;
|
||||
|
||||
/**
|
||||
* The set of input fields which are declared as string literal members in the Directive's class.
|
||||
* We need to track these separately because these fields may not be valid JS identifiers so
|
||||
* we cannot use them with property access expressions when assigning inputs.
|
||||
*/
|
||||
stringLiteralInputFields: Set<string>;
|
||||
|
||||
/**
|
||||
* The set of input fields which do not have corresponding members in the Directive's class.
|
||||
*/
|
||||
undeclaredInputFields: Set<string>;
|
||||
|
||||
/**
|
||||
* Whether the Directive's class is generic, i.e. `class MyDir<T> {...}`.
|
||||
*/
|
||||
isGeneric: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata collected for a directive within an NgModule's scope.
|
||||
*/
|
||||
export interface DirectiveMeta extends T2DirectiveMeta {
|
||||
export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta {
|
||||
ref: Reference<ClassDeclaration>;
|
||||
/**
|
||||
* Unparsed selector of the directive, or null if the directive does not have a selector.
|
||||
*/
|
||||
selector: string|null;
|
||||
queries: string[];
|
||||
ngTemplateGuards: TemplateGuardMeta[];
|
||||
hasNgTemplateContextGuard: boolean;
|
||||
coercedInputFields: Set<string>;
|
||||
|
||||
/**
|
||||
* A `Reference` to the base class for the directive, if one was detected.
|
||||
|
@ -12,7 +12,7 @@ import {Reference} from '../../imports';
|
||||
import {ClassDeclaration, isNamedClassDeclaration, ReflectionHost} from '../../reflection';
|
||||
|
||||
import {DirectiveMeta, MetadataReader, NgModuleMeta, PipeMeta} from './api';
|
||||
import {extractDirectiveGuards, extractReferencesFromType, readStringArrayType, readStringMapType, readStringType} from './util';
|
||||
import {extractDirectiveTypeCheckMeta, extractReferencesFromType, readStringArrayType, readStringMapType, readStringType} from './util';
|
||||
|
||||
/**
|
||||
* A `MetadataReader` that can read metadata from `.d.ts` files, which have static Ivy properties
|
||||
@ -76,16 +76,17 @@ export class DtsMetadataReader implements MetadataReader {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inputs = readStringMapType(def.type.typeArguments[3]);
|
||||
return {
|
||||
ref,
|
||||
name: clazz.name.text,
|
||||
isComponent: def.name === 'ɵcmp',
|
||||
selector: readStringType(def.type.typeArguments[1]),
|
||||
exportAs: readStringArrayType(def.type.typeArguments[2]),
|
||||
inputs: readStringMapType(def.type.typeArguments[3]),
|
||||
inputs,
|
||||
outputs: readStringMapType(def.type.typeArguments[4]),
|
||||
queries: readStringArrayType(def.type.typeArguments[5]),
|
||||
...extractDirectiveGuards(clazz, this.reflector),
|
||||
...extractDirectiveTypeCheckMeta(clazz, inputs, this.reflector),
|
||||
baseClass: readBaseClass(clazz, this.checker, this.reflector),
|
||||
};
|
||||
}
|
||||
|
@ -27,7 +27,10 @@ export function flattenInheritedDirectiveMetadata(
|
||||
|
||||
let inputs: {[key: string]: string|[string, string]} = {};
|
||||
let outputs: {[key: string]: string} = {};
|
||||
let coercedInputFields = new Set<string>();
|
||||
const coercedInputFields = new Set<string>();
|
||||
const undeclaredInputFields = new Set<string>();
|
||||
const restrictedInputFields = new Set<string>();
|
||||
const stringLiteralInputFields = new Set<string>();
|
||||
let isDynamic = false;
|
||||
|
||||
const addMetadata = (meta: DirectiveMeta): void => {
|
||||
@ -48,6 +51,15 @@ export function flattenInheritedDirectiveMetadata(
|
||||
for (const coercedInputField of meta.coercedInputFields) {
|
||||
coercedInputFields.add(coercedInputField);
|
||||
}
|
||||
for (const undeclaredInputField of meta.undeclaredInputFields) {
|
||||
undeclaredInputFields.add(undeclaredInputField);
|
||||
}
|
||||
for (const restrictedInputField of meta.restrictedInputFields) {
|
||||
restrictedInputFields.add(restrictedInputField);
|
||||
}
|
||||
for (const field of meta.stringLiteralInputFields) {
|
||||
stringLiteralInputFields.add(field);
|
||||
}
|
||||
};
|
||||
|
||||
addMetadata(topMeta);
|
||||
@ -57,6 +69,9 @@ export function flattenInheritedDirectiveMetadata(
|
||||
inputs,
|
||||
outputs,
|
||||
coercedInputFields,
|
||||
undeclaredInputFields,
|
||||
restrictedInputFields,
|
||||
stringLiteralInputFields,
|
||||
baseClass: isDynamic ? 'dynamic' : null,
|
||||
};
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import {Reference} from '../../imports';
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, isNamedClassDeclaration, ReflectionHost, reflectTypeEntityToDeclaration} from '../../reflection';
|
||||
import {nodeDebugInfo} from '../../util/src/typescript';
|
||||
|
||||
import {DirectiveMeta, MetadataReader, NgModuleMeta, PipeMeta, TemplateGuardMeta} from './api';
|
||||
import {DirectiveMeta, DirectiveTypeCheckMeta, MetadataReader, NgModuleMeta, PipeMeta, TemplateGuardMeta} from './api';
|
||||
|
||||
export function extractReferencesFromType(
|
||||
checker: ts.TypeChecker, def: ts.TypeNode, ngModuleImportedFrom: string|null,
|
||||
@ -78,13 +78,16 @@ export function readStringArrayType(type: ts.TypeNode): string[] {
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
export function extractDirectiveGuards(node: ClassDeclaration, reflector: ReflectionHost): {
|
||||
ngTemplateGuards: TemplateGuardMeta[],
|
||||
hasNgTemplateContextGuard: boolean,
|
||||
coercedInputFields: Set<string>,
|
||||
} {
|
||||
const staticMembers = reflector.getMembersOfClass(node).filter(member => member.isStatic);
|
||||
/**
|
||||
* Inspects the class' members and extracts the metadata that is used when type-checking templates
|
||||
* that use the directive. This metadata does not contain information from a base class, if any,
|
||||
* making this metadata invariant to changes of inherited classes.
|
||||
*/
|
||||
export function extractDirectiveTypeCheckMeta(
|
||||
node: ClassDeclaration, inputs: {[fieldName: string]: string|[string, string]},
|
||||
reflector: ReflectionHost): DirectiveTypeCheckMeta {
|
||||
const members = reflector.getMembersOfClass(node);
|
||||
const staticMembers = members.filter(member => member.isStatic);
|
||||
const ngTemplateGuards = staticMembers.map(extractTemplateGuard)
|
||||
.filter((guard): guard is TemplateGuardMeta => guard !== null);
|
||||
const hasNgTemplateContextGuard = staticMembers.some(
|
||||
@ -93,7 +96,47 @@ export function extractDirectiveGuards(node: ClassDeclaration, reflector: Reflec
|
||||
const coercedInputFields =
|
||||
new Set(staticMembers.map(extractCoercedInput)
|
||||
.filter((inputName): inputName is string => inputName !== null));
|
||||
return {hasNgTemplateContextGuard, ngTemplateGuards, coercedInputFields};
|
||||
|
||||
const restrictedInputFields = new Set<string>();
|
||||
const stringLiteralInputFields = new Set<string>();
|
||||
const undeclaredInputFields = new Set<string>();
|
||||
|
||||
for (const fieldName of Object.keys(inputs)) {
|
||||
const field = members.find(member => member.name === fieldName);
|
||||
if (field === undefined || field.node === null) {
|
||||
undeclaredInputFields.add(fieldName);
|
||||
continue;
|
||||
}
|
||||
if (isRestricted(field.node)) {
|
||||
restrictedInputFields.add(fieldName);
|
||||
}
|
||||
if (field.nameNode !== null && ts.isStringLiteral(field.nameNode)) {
|
||||
stringLiteralInputFields.add(fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
const arity = reflector.getGenericArityOfClass(node);
|
||||
|
||||
return {
|
||||
hasNgTemplateContextGuard,
|
||||
ngTemplateGuards,
|
||||
coercedInputFields,
|
||||
restrictedInputFields,
|
||||
stringLiteralInputFields,
|
||||
undeclaredInputFields,
|
||||
isGeneric: arity !== null && arity > 0,
|
||||
};
|
||||
}
|
||||
|
||||
function isRestricted(node: ts.Node): boolean {
|
||||
if (node.modifiers === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return node.modifiers.some(
|
||||
modifier => modifier.kind === ts.SyntaxKind.PrivateKeyword ||
|
||||
modifier.kind === ts.SyntaxKind.ProtectedKeyword ||
|
||||
modifier.kind === ts.SyntaxKind.ReadonlyKeyword);
|
||||
}
|
||||
|
||||
function extractTemplateGuard(member: ClassMember): TemplateGuardMeta|null {
|
||||
|
@ -224,25 +224,39 @@ export interface ClassMember {
|
||||
decorators: Decorator[]|null;
|
||||
}
|
||||
|
||||
export const enum TypeValueReferenceKind {
|
||||
LOCAL,
|
||||
IMPORTED,
|
||||
UNAVAILABLE,
|
||||
}
|
||||
|
||||
/**
|
||||
* A reference to a value that originated from a type position.
|
||||
*
|
||||
* For example, a constructor parameter could be declared as `foo: Foo`. A `TypeValueReference`
|
||||
* extracted from this would refer to the value of the class `Foo` (assuming it was actually a
|
||||
* type).
|
||||
*
|
||||
* There are two kinds of such references. A reference with `local: false` refers to a type that was
|
||||
* imported, and gives the symbol `name` and the `moduleName` of the import. Note that this
|
||||
* `moduleName` may be a relative path, and thus is likely only valid within the context of the file
|
||||
* which contained the original type reference.
|
||||
*
|
||||
* A reference with `local: true` refers to any other kind of type via a `ts.Expression` that's
|
||||
* valid within the local file where the type was referenced.
|
||||
* A type reference that refers to any type via a `ts.Expression` that's valid within the local file
|
||||
* where the type was referenced.
|
||||
*/
|
||||
export type TypeValueReference = {
|
||||
local: true; expression: ts.Expression; defaultImportStatement: ts.ImportDeclaration | null;
|
||||
}|{
|
||||
local: false;
|
||||
export interface LocalTypeValueReference {
|
||||
kind: TypeValueReferenceKind.LOCAL;
|
||||
|
||||
/**
|
||||
* The synthesized expression to reference the type in a value position.
|
||||
*/
|
||||
expression: ts.Expression;
|
||||
|
||||
/**
|
||||
* If the type originates from a default import, the import statement is captured here to be able
|
||||
* to track its usages, preventing the import from being elided if it was originally only used in
|
||||
* a type-position. See `DefaultImportTracker` for details.
|
||||
*/
|
||||
defaultImportStatement: ts.ImportDeclaration|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A reference that refers to a type that was imported, and gives the symbol `name` and the
|
||||
* `moduleName` of the import. Note that this `moduleName` may be a relative path, and thus is
|
||||
* likely only valid within the context of the file which contained the original type reference.
|
||||
*/
|
||||
export interface ImportedTypeValueReference {
|
||||
kind: TypeValueReferenceKind.IMPORTED;
|
||||
|
||||
/**
|
||||
* The module specifier from which the `importedName` symbol should be imported.
|
||||
@ -262,7 +276,107 @@ export type TypeValueReference = {
|
||||
nestedPath: string[]|null;
|
||||
|
||||
valueDeclaration: ts.Declaration;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A representation for a type value reference that is used when no value is available. This can
|
||||
* occur due to various reasons, which is indicated in the `reason` field.
|
||||
*/
|
||||
export interface UnavailableTypeValueReference {
|
||||
kind: TypeValueReferenceKind.UNAVAILABLE;
|
||||
|
||||
/**
|
||||
* The reason why no value reference could be determined for a type.
|
||||
*/
|
||||
reason: UnavailableValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* The various reasons why the compiler may be unable to synthesize a value from a type reference.
|
||||
*/
|
||||
export const enum ValueUnavailableKind {
|
||||
/**
|
||||
* No type node was available.
|
||||
*/
|
||||
MISSING_TYPE,
|
||||
|
||||
/**
|
||||
* The type does not have a value declaration, e.g. an interface.
|
||||
*/
|
||||
NO_VALUE_DECLARATION,
|
||||
|
||||
/**
|
||||
* The type is imported using a type-only imports, so it is not suitable to be used in a
|
||||
* value-position.
|
||||
*/
|
||||
TYPE_ONLY_IMPORT,
|
||||
|
||||
/**
|
||||
* The type reference could not be resolved to a declaration.
|
||||
*/
|
||||
UNKNOWN_REFERENCE,
|
||||
|
||||
/**
|
||||
* The type corresponds with a namespace.
|
||||
*/
|
||||
NAMESPACE,
|
||||
|
||||
/**
|
||||
* The type is not supported in the compiler, for example union types.
|
||||
*/
|
||||
UNSUPPORTED,
|
||||
}
|
||||
|
||||
|
||||
export interface UnsupportedType {
|
||||
kind: ValueUnavailableKind.UNSUPPORTED;
|
||||
typeNode: ts.TypeNode;
|
||||
}
|
||||
|
||||
export interface NoValueDeclaration {
|
||||
kind: ValueUnavailableKind.NO_VALUE_DECLARATION;
|
||||
typeNode: ts.TypeNode;
|
||||
decl: ts.Declaration;
|
||||
}
|
||||
|
||||
export interface TypeOnlyImport {
|
||||
kind: ValueUnavailableKind.TYPE_ONLY_IMPORT;
|
||||
typeNode: ts.TypeNode;
|
||||
importClause: ts.ImportClause;
|
||||
}
|
||||
|
||||
export interface NamespaceImport {
|
||||
kind: ValueUnavailableKind.NAMESPACE;
|
||||
typeNode: ts.TypeNode;
|
||||
importClause: ts.ImportClause;
|
||||
}
|
||||
|
||||
export interface UnknownReference {
|
||||
kind: ValueUnavailableKind.UNKNOWN_REFERENCE;
|
||||
typeNode: ts.TypeNode;
|
||||
}
|
||||
|
||||
export interface MissingType {
|
||||
kind: ValueUnavailableKind.MISSING_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* The various reasons why a type node may not be referred to as a value.
|
||||
*/
|
||||
export type UnavailableValue =
|
||||
UnsupportedType|NoValueDeclaration|TypeOnlyImport|NamespaceImport|UnknownReference|MissingType;
|
||||
|
||||
/**
|
||||
* A reference to a value that originated from a type position.
|
||||
*
|
||||
* For example, a constructor parameter could be declared as `foo: Foo`. A `TypeValueReference`
|
||||
* extracted from this would refer to the value of the class `Foo` (assuming it was actually a
|
||||
* type).
|
||||
*
|
||||
* See the individual types for additional information.
|
||||
*/
|
||||
export type TypeValueReference =
|
||||
LocalTypeValueReference|ImportedTypeValueReference|UnavailableTypeValueReference;
|
||||
|
||||
/**
|
||||
* A parameter to a constructor.
|
||||
@ -288,14 +402,10 @@ export interface CtorParameter {
|
||||
* Reference to the value of the parameter's type annotation, if it's possible to refer to the
|
||||
* parameter's type as a value.
|
||||
*
|
||||
* This can either be a reference to a local value, in which case it has `local` set to `true` and
|
||||
* contains a `ts.Expression`, or it's a reference to an imported value, in which case `local` is
|
||||
* set to `false` and the symbol and module name of the imported value are provided instead.
|
||||
*
|
||||
* If the type is not present or cannot be represented as an expression, `typeValueReference` is
|
||||
* `null`.
|
||||
* This can either be a reference to a local value, a reference to an imported value, or no
|
||||
* value if no is present or cannot be represented as an expression.
|
||||
*/
|
||||
typeValueReference: TypeValueReference|null;
|
||||
typeValueReference: TypeValueReference;
|
||||
|
||||
/**
|
||||
* TypeScript `ts.TypeNode` representing the type node found in the type position.
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {TypeValueReference} from './host';
|
||||
import {TypeValueReference, TypeValueReferenceKind, UnavailableTypeValueReference, ValueUnavailableKind} from './host';
|
||||
|
||||
/**
|
||||
* Potentially convert a `ts.TypeNode` to a `TypeValueReference`, which indicates how to use the
|
||||
@ -18,22 +18,26 @@ import {TypeValueReference} from './host';
|
||||
* declaration, or if it is not possible to statically understand.
|
||||
*/
|
||||
export function typeToValue(
|
||||
typeNode: ts.TypeNode|null, checker: ts.TypeChecker): TypeValueReference|null {
|
||||
typeNode: ts.TypeNode|null, checker: ts.TypeChecker): TypeValueReference {
|
||||
// It's not possible to get a value expression if the parameter doesn't even have a type.
|
||||
if (typeNode === null || !ts.isTypeReferenceNode(typeNode)) {
|
||||
return null;
|
||||
if (typeNode === null) {
|
||||
return missingType();
|
||||
}
|
||||
|
||||
if (!ts.isTypeReferenceNode(typeNode)) {
|
||||
return unsupportedType(typeNode);
|
||||
}
|
||||
|
||||
const symbols = resolveTypeSymbols(typeNode, checker);
|
||||
if (symbols === null) {
|
||||
return null;
|
||||
return unknownReference(typeNode);
|
||||
}
|
||||
|
||||
const {local, decl} = symbols;
|
||||
// It's only valid to convert a type reference to a value reference if the type actually
|
||||
// has a value declaration associated with it.
|
||||
if (decl.valueDeclaration === undefined) {
|
||||
return null;
|
||||
return noValueDeclaration(typeNode, decl.declarations[0]);
|
||||
}
|
||||
|
||||
// The type points to a valid value declaration. Rewrite the TypeReference into an
|
||||
@ -47,8 +51,13 @@ export function typeToValue(
|
||||
// This is a default import.
|
||||
// import Foo from 'foo';
|
||||
|
||||
if (firstDecl.isTypeOnly) {
|
||||
// Type-only imports cannot be represented as value.
|
||||
return typeOnlyImport(typeNode, firstDecl);
|
||||
}
|
||||
|
||||
return {
|
||||
local: true,
|
||||
kind: TypeValueReferenceKind.LOCAL,
|
||||
// Copying the name here ensures the generated references will be correctly transformed
|
||||
// along with the import.
|
||||
expression: ts.updateIdentifier(firstDecl.name),
|
||||
@ -60,6 +69,11 @@ export function typeToValue(
|
||||
// or
|
||||
// import {Foo as Bar} from 'foo';
|
||||
|
||||
if (firstDecl.parent.parent.isTypeOnly) {
|
||||
// Type-only imports cannot be represented as value.
|
||||
return typeOnlyImport(typeNode, firstDecl.parent.parent);
|
||||
}
|
||||
|
||||
// Determine the name to import (`Foo`) from the import specifier, as the symbol names of
|
||||
// the imported type could refer to a local alias (like `Bar` in the example above).
|
||||
const importedName = (firstDecl.propertyName || firstDecl.name).text;
|
||||
@ -70,7 +84,7 @@ export function typeToValue(
|
||||
|
||||
const moduleName = extractModuleName(firstDecl.parent.parent.parent);
|
||||
return {
|
||||
local: false,
|
||||
kind: TypeValueReferenceKind.IMPORTED,
|
||||
valueDeclaration: decl.valueDeclaration,
|
||||
moduleName,
|
||||
importedName,
|
||||
@ -80,9 +94,14 @@ export function typeToValue(
|
||||
// The import is a namespace import
|
||||
// import * as Foo from 'foo';
|
||||
|
||||
if (firstDecl.parent.isTypeOnly) {
|
||||
// Type-only imports cannot be represented as value.
|
||||
return typeOnlyImport(typeNode, firstDecl.parent);
|
||||
}
|
||||
|
||||
if (symbols.symbolNames.length === 1) {
|
||||
// The type refers to the namespace itself, which cannot be represented as a value.
|
||||
return null;
|
||||
return namespaceImport(typeNode, firstDecl.parent);
|
||||
}
|
||||
|
||||
// The first symbol name refers to the local name of the namespace, which is is discarded
|
||||
@ -92,7 +111,7 @@ export function typeToValue(
|
||||
|
||||
const moduleName = extractModuleName(firstDecl.parent.parent);
|
||||
return {
|
||||
local: false,
|
||||
kind: TypeValueReferenceKind.IMPORTED,
|
||||
valueDeclaration: decl.valueDeclaration,
|
||||
moduleName,
|
||||
importedName,
|
||||
@ -105,15 +124,60 @@ export function typeToValue(
|
||||
const expression = typeNodeToValueExpr(typeNode);
|
||||
if (expression !== null) {
|
||||
return {
|
||||
local: true,
|
||||
kind: TypeValueReferenceKind.LOCAL,
|
||||
expression,
|
||||
defaultImportStatement: null,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
return unsupportedType(typeNode);
|
||||
}
|
||||
}
|
||||
|
||||
function unsupportedType(typeNode: ts.TypeNode): UnavailableTypeValueReference {
|
||||
return {
|
||||
kind: TypeValueReferenceKind.UNAVAILABLE,
|
||||
reason: {kind: ValueUnavailableKind.UNSUPPORTED, typeNode},
|
||||
};
|
||||
}
|
||||
|
||||
function noValueDeclaration(
|
||||
typeNode: ts.TypeNode, decl: ts.Declaration): UnavailableTypeValueReference {
|
||||
return {
|
||||
kind: TypeValueReferenceKind.UNAVAILABLE,
|
||||
reason: {kind: ValueUnavailableKind.NO_VALUE_DECLARATION, typeNode, decl},
|
||||
};
|
||||
}
|
||||
|
||||
function typeOnlyImport(
|
||||
typeNode: ts.TypeNode, importClause: ts.ImportClause): UnavailableTypeValueReference {
|
||||
return {
|
||||
kind: TypeValueReferenceKind.UNAVAILABLE,
|
||||
reason: {kind: ValueUnavailableKind.TYPE_ONLY_IMPORT, typeNode, importClause},
|
||||
};
|
||||
}
|
||||
|
||||
function unknownReference(typeNode: ts.TypeNode): UnavailableTypeValueReference {
|
||||
return {
|
||||
kind: TypeValueReferenceKind.UNAVAILABLE,
|
||||
reason: {kind: ValueUnavailableKind.UNKNOWN_REFERENCE, typeNode},
|
||||
};
|
||||
}
|
||||
|
||||
function namespaceImport(
|
||||
typeNode: ts.TypeNode, importClause: ts.ImportClause): UnavailableTypeValueReference {
|
||||
return {
|
||||
kind: TypeValueReferenceKind.UNAVAILABLE,
|
||||
reason: {kind: ValueUnavailableKind.NAMESPACE, typeNode, importClause},
|
||||
};
|
||||
}
|
||||
|
||||
function missingType(): UnavailableTypeValueReference {
|
||||
return {
|
||||
kind: TypeValueReferenceKind.UNAVAILABLE,
|
||||
reason: {kind: ValueUnavailableKind.MISSING_TYPE},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to extract a `ts.Expression` that's equivalent to a `ts.TypeNode`, as the two have
|
||||
* different AST shapes but can reference the same symbols.
|
||||
|
@ -68,8 +68,6 @@ export class TypeScriptReflectionHost implements ReflectionHost {
|
||||
|
||||
if (childTypeNodes.length === 1) {
|
||||
typeNode = childTypeNodes[0];
|
||||
} else {
|
||||
typeNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ import * as ts from 'typescript';
|
||||
import {absoluteFrom, getSourceFileOrError} from '../../file_system';
|
||||
import {runInEachFileSystem} from '../../file_system/testing';
|
||||
import {getDeclaration, makeProgram} from '../../testing';
|
||||
import {ClassMember, ClassMemberKind, CtorParameter} from '../src/host';
|
||||
import {ClassMember, ClassMemberKind, CtorParameter, TypeValueReferenceKind} from '../src/host';
|
||||
import {TypeScriptReflectionHost} from '../src/typescript';
|
||||
import {isNamedClassDeclaration} from '../src/util';
|
||||
|
||||
@ -178,7 +178,7 @@ runInEachFileSystem(() => {
|
||||
const args = host.getConstructorParameters(clazz)!;
|
||||
expect(args.length).toBe(1);
|
||||
const param = args[0].typeValueReference;
|
||||
if (param === null || !param.local) {
|
||||
if (param === null || param.kind !== TypeValueReferenceKind.LOCAL) {
|
||||
return fail('Expected local parameter');
|
||||
}
|
||||
expect(param).not.toBeNull();
|
||||
@ -548,17 +548,20 @@ runInEachFileSystem(() => {
|
||||
if (type === undefined) {
|
||||
expect(param.typeValueReference).toBeNull();
|
||||
} else {
|
||||
if (param.typeValueReference === null) {
|
||||
if (param.typeValueReference.kind === TypeValueReferenceKind.UNAVAILABLE) {
|
||||
return fail(`Expected parameter ${name} to have a typeValueReference`);
|
||||
}
|
||||
if (param.typeValueReference.local && typeof type === 'string') {
|
||||
if (param.typeValueReference.kind === TypeValueReferenceKind.LOCAL &&
|
||||
typeof type === 'string') {
|
||||
expect(argExpressionToString(param.typeValueReference.expression)).toEqual(type);
|
||||
} else if (!param.typeValueReference.local && typeof type !== 'string') {
|
||||
} else if (
|
||||
param.typeValueReference.kind === TypeValueReferenceKind.IMPORTED &&
|
||||
typeof type !== 'string') {
|
||||
expect(param.typeValueReference.moduleName).toEqual(type.moduleName);
|
||||
expect(param.typeValueReference.importedName).toEqual(type.name);
|
||||
} else {
|
||||
return fail(`Mismatch between typeValueReference and expected type: ${param.name} / ${
|
||||
param.typeValueReference.local}`);
|
||||
param.typeValueReference.kind}`);
|
||||
}
|
||||
}
|
||||
if (decorator !== undefined) {
|
||||
|
@ -243,6 +243,10 @@ function fakeDirective(ref: Reference<ClassDeclaration>): DirectiveMeta {
|
||||
hasNgTemplateContextGuard: false,
|
||||
ngTemplateGuards: [],
|
||||
coercedInputFields: new Set<string>(),
|
||||
restrictedInputFields: new Set<string>(),
|
||||
stringLiteralInputFields: new Set<string>(),
|
||||
undeclaredInputFields: new Set<string>(),
|
||||
isGeneric: false,
|
||||
baseClass: null,
|
||||
};
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import * as ts from 'typescript';
|
||||
|
||||
import {AbsoluteFsPath} from '../../file_system';
|
||||
import {Reference} from '../../imports';
|
||||
import {TemplateGuardMeta} from '../../metadata';
|
||||
import {DirectiveTypeCheckMeta} from '../../metadata';
|
||||
import {ClassDeclaration} from '../../reflection';
|
||||
|
||||
|
||||
@ -19,12 +19,9 @@ import {ClassDeclaration} from '../../reflection';
|
||||
* Extension of `DirectiveMeta` that includes additional information required to type-check the
|
||||
* usage of a particular directive.
|
||||
*/
|
||||
export interface TypeCheckableDirectiveMeta extends DirectiveMeta {
|
||||
export interface TypeCheckableDirectiveMeta extends DirectiveMeta, DirectiveTypeCheckMeta {
|
||||
ref: Reference<ClassDeclaration>;
|
||||
queries: string[];
|
||||
ngTemplateGuards: TemplateGuardMeta[];
|
||||
coercedInputFields: Set<string>;
|
||||
hasNgTemplateContextGuard: boolean;
|
||||
}
|
||||
|
||||
export type TemplateId = string&{__brand: 'TemplateId'};
|
||||
@ -93,6 +90,14 @@ export interface TypeCheckingConfig {
|
||||
*/
|
||||
checkTypeOfInputBindings: boolean;
|
||||
|
||||
/**
|
||||
* Whether to honor the access modifiers on input bindings for the component/directive.
|
||||
*
|
||||
* If a template binding attempts to assign to an input that is private/protected/readonly,
|
||||
* this will produce errors when enabled but will not when disabled.
|
||||
*/
|
||||
honorAccessModifiersForInputBindings: boolean;
|
||||
|
||||
/**
|
||||
* Whether to use strict null types for input bindings for directives.
|
||||
*
|
||||
|
@ -199,12 +199,12 @@ export class TypeCheckContextImpl implements TypeCheckContext {
|
||||
for (const dir of boundTarget.getUsedDirectives()) {
|
||||
const dirRef = dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
|
||||
const dirNode = dirRef.node;
|
||||
if (requiresInlineTypeCtor(dirNode, this.reflector)) {
|
||||
|
||||
if (dir.isGeneric && requiresInlineTypeCtor(dirNode, this.reflector)) {
|
||||
if (this.inlining === InliningMode.Error) {
|
||||
missingInlines.push(dirNode);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add a type constructor operation for the directive.
|
||||
this.addInlineTypeCtor(fileData, dirNode.getSourceFile(), dirRef, {
|
||||
fnName: 'ngTypeCtor',
|
||||
|
@ -234,8 +234,8 @@ class AstTranslator implements AstVisitor {
|
||||
return node;
|
||||
}
|
||||
|
||||
visitQuote(ast: Quote): never {
|
||||
throw new Error('Method not implemented.');
|
||||
visitQuote(ast: Quote): ts.Expression {
|
||||
return NULL_AS_ANY;
|
||||
}
|
||||
|
||||
visitSafeMethodCall(ast: SafeMethodCall): ts.Expression {
|
||||
|
@ -86,6 +86,21 @@ export function tsDeclareVariable(id: ts.Identifier, type: ts.TypeNode): ts.Vari
|
||||
/* declarationList */[decl]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a `ts.TypeQueryNode` for a coerced input.
|
||||
*
|
||||
* For example: `typeof MatInput.ngAcceptInputType_value`, where MatInput is `typeName` and `value`
|
||||
* is the `coercedInputName`.
|
||||
*
|
||||
* @param typeName The `EntityName` of the Directive where the static coerced input is defined.
|
||||
* @param coercedInputName The field name of the coerced input.
|
||||
*/
|
||||
export function tsCreateTypeQueryForCoercedInput(
|
||||
typeName: ts.EntityName, coercedInputName: string): ts.TypeQueryNode {
|
||||
return ts.createTypeQueryNode(
|
||||
ts.createQualifiedName(typeName, `ngAcceptInputType_${coercedInputName}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `ts.VariableStatement` that initializes a variable with a given expression.
|
||||
*
|
||||
|
@ -19,7 +19,7 @@ import {Environment} from './environment';
|
||||
import {astToTypescript, NULL_AS_ANY} from './expression';
|
||||
import {OutOfBandDiagnosticRecorder} from './oob';
|
||||
import {ExpressionSemanticVisitor} from './template_semantics';
|
||||
import {checkIfClassIsExported, checkIfGenericTypesAreUnbound, tsCallMethod, tsCastToAny, tsCreateElement, tsCreateVariable, tsDeclareVariable} from './ts_util';
|
||||
import {checkIfClassIsExported, checkIfGenericTypesAreUnbound, tsCallMethod, tsCastToAny, tsCreateElement, tsCreateTypeQueryForCoercedInput, tsCreateVariable, tsDeclareVariable} from './ts_util';
|
||||
|
||||
|
||||
|
||||
@ -99,6 +99,13 @@ export function generateTypeCheckBlock(
|
||||
* `ts.Expression` which can be used to reference the operation's result.
|
||||
*/
|
||||
abstract class TcbOp {
|
||||
/**
|
||||
* Set to true if this operation can be considered optional. Optional operations are only executed
|
||||
* when depended upon by other operations, otherwise they are disregarded. This allows for less
|
||||
* code to generate, parse and type-check, overall positively contributing to performance.
|
||||
*/
|
||||
abstract readonly optional: boolean;
|
||||
|
||||
abstract execute(): ts.Expression|null;
|
||||
|
||||
/**
|
||||
@ -125,6 +132,13 @@ class TcbElementOp extends TcbOp {
|
||||
super();
|
||||
}
|
||||
|
||||
get optional() {
|
||||
// The statement generated by this operation is only used for type-inference of the DOM
|
||||
// element's type and won't report diagnostics by itself, so the operation is marked as optional
|
||||
// to avoid generating statements for DOM elements that are never referenced.
|
||||
return true;
|
||||
}
|
||||
|
||||
execute(): ts.Identifier {
|
||||
const id = this.tcb.allocateId();
|
||||
// Add the declaration of the element using document.createElement.
|
||||
@ -148,6 +162,10 @@ class TcbVariableOp extends TcbOp {
|
||||
super();
|
||||
}
|
||||
|
||||
get optional() {
|
||||
return false;
|
||||
}
|
||||
|
||||
execute(): ts.Identifier {
|
||||
// Look for a context variable for the template.
|
||||
const ctx = this.scope.resolve(this.template);
|
||||
@ -176,6 +194,10 @@ class TcbTemplateContextOp extends TcbOp {
|
||||
super();
|
||||
}
|
||||
|
||||
get optional() {
|
||||
return false;
|
||||
}
|
||||
|
||||
execute(): ts.Identifier {
|
||||
// Allocate a template ctx variable and declare it with an 'any' type. The type of this variable
|
||||
// may be narrowed as a result of template guard conditions.
|
||||
@ -197,6 +219,11 @@ class TcbTemplateBodyOp extends TcbOp {
|
||||
constructor(private tcb: Context, private scope: Scope, private template: TmplAstTemplate) {
|
||||
super();
|
||||
}
|
||||
|
||||
get optional() {
|
||||
return false;
|
||||
}
|
||||
|
||||
execute(): null {
|
||||
// An `if` will be constructed, within which the template's children will be type checked. The
|
||||
// `if` is used for two reasons: it creates a new syntactic scope, isolating variables declared
|
||||
@ -277,8 +304,19 @@ class TcbTemplateBodyOp extends TcbOp {
|
||||
// children, as well as tracks bindings within the template.
|
||||
const tmplScope = Scope.forNodes(this.tcb, this.scope, this.template, guard);
|
||||
|
||||
// Render the template's `Scope` into a block.
|
||||
let tmplBlock: ts.Statement = ts.createBlock(tmplScope.render());
|
||||
// Render the template's `Scope` into its statements.
|
||||
const statements = tmplScope.render();
|
||||
if (statements.length === 0) {
|
||||
// As an optimization, don't generate the scope's block if it has no statements. This is
|
||||
// beneficial for templates that contain for example `<span *ngIf="first"></span>`, in which
|
||||
// case there's no need to render the `NgIf` guard expression. This seems like a minor
|
||||
// improvement, however it reduces the number of flow-node antecedents that TypeScript needs
|
||||
// to keep into account for such cases, resulting in an overall reduction of
|
||||
// type-checking time.
|
||||
return null;
|
||||
}
|
||||
|
||||
let tmplBlock: ts.Statement = ts.createBlock(statements);
|
||||
if (guard !== null) {
|
||||
// The scope has a guard that needs to be applied, so wrap the template block into an `if`
|
||||
// statement containing the guard expression.
|
||||
@ -300,6 +338,10 @@ class TcbTextInterpolationOp extends TcbOp {
|
||||
super();
|
||||
}
|
||||
|
||||
get optional() {
|
||||
return false;
|
||||
}
|
||||
|
||||
execute(): null {
|
||||
const expr = tcbExpression(this.binding.value, this.tcb, this.scope);
|
||||
this.scope.addStatement(ts.createExpressionStatement(expr));
|
||||
@ -308,58 +350,240 @@ class TcbTextInterpolationOp extends TcbOp {
|
||||
}
|
||||
|
||||
/**
|
||||
* A `TcbOp` which constructs an instance of a directive with types inferred from its inputs, which
|
||||
* also checks the bindings to the directive in the process.
|
||||
* A `TcbOp` which constructs an instance of a directive _without_ setting any of its inputs. Inputs
|
||||
* are later set in the `TcbDirectiveInputsOp`. Type checking was found to be faster when done in
|
||||
* this way as opposed to `TcbDirectiveCtorOp` which is only necessary when the directive is
|
||||
* generic.
|
||||
*
|
||||
* Executing this operation returns a reference to the directive instance variable with its inferred
|
||||
* type.
|
||||
*/
|
||||
class TcbDirectiveOp extends TcbOp {
|
||||
class TcbDirectiveTypeOp extends TcbOp {
|
||||
constructor(
|
||||
private tcb: Context, private scope: Scope, private node: TmplAstTemplate|TmplAstElement,
|
||||
private dir: TypeCheckableDirectiveMeta) {
|
||||
super();
|
||||
}
|
||||
|
||||
get optional() {
|
||||
// The statement generated by this operation is only used to declare the directive's type and
|
||||
// won't report diagnostics by itself, so the operation is marked as optional to avoid
|
||||
// generating declarations for directives that don't have any inputs/outputs.
|
||||
return true;
|
||||
}
|
||||
|
||||
execute(): ts.Identifier {
|
||||
const id = this.tcb.allocateId();
|
||||
// Process the directive and construct expressions for each of its bindings.
|
||||
const inputs = tcbGetDirectiveInputs(this.node, this.dir, this.tcb, this.scope);
|
||||
|
||||
const type = this.tcb.env.referenceType(this.dir.ref);
|
||||
this.scope.addStatement(tsDeclareVariable(id, type));
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A `TcbOp` which constructs an instance of a directive with types inferred from its inputs. The
|
||||
* inputs themselves are not checked here; checking of inputs is achieved in `TcbDirectiveInputsOp`.
|
||||
* Any errors reported in this statement are ignored, as the type constructor call is only present
|
||||
* for type-inference.
|
||||
*
|
||||
* When a Directive is generic, it is required that the TCB generates the instance using this method
|
||||
* in order to infer the type information correctly.
|
||||
*
|
||||
* Executing this operation returns a reference to the directive instance variable with its inferred
|
||||
* type.
|
||||
*/
|
||||
class TcbDirectiveCtorOp extends TcbOp {
|
||||
constructor(
|
||||
private tcb: Context, private scope: Scope, private node: TmplAstTemplate|TmplAstElement,
|
||||
private dir: TypeCheckableDirectiveMeta) {
|
||||
super();
|
||||
}
|
||||
|
||||
get optional() {
|
||||
// The statement generated by this operation is only used to infer the directive's type and
|
||||
// won't report diagnostics by itself, so the operation is marked as optional.
|
||||
return true;
|
||||
}
|
||||
|
||||
execute(): ts.Identifier {
|
||||
const id = this.tcb.allocateId();
|
||||
|
||||
const genericInputs = new Map<string, TcbDirectiveInput>();
|
||||
|
||||
const inputs = getBoundInputs(this.dir, this.node, this.tcb);
|
||||
for (const input of inputs) {
|
||||
for (const fieldName of input.fieldNames) {
|
||||
// Skip the field if an attribute has already been bound to it; we can't have a duplicate
|
||||
// key in the type constructor call.
|
||||
if (genericInputs.has(fieldName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const expression = translateInput(input.attribute, this.tcb, this.scope);
|
||||
genericInputs.set(fieldName, {
|
||||
type: 'binding',
|
||||
field: fieldName,
|
||||
expression,
|
||||
sourceSpan: input.attribute.sourceSpan
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add unset directive inputs for each of the remaining unset fields.
|
||||
for (const fieldName of Object.keys(this.dir.inputs)) {
|
||||
if (!genericInputs.has(fieldName)) {
|
||||
genericInputs.set(fieldName, {type: 'unset', field: fieldName});
|
||||
}
|
||||
}
|
||||
|
||||
// Call the type constructor of the directive to infer a type, and assign the directive
|
||||
// instance.
|
||||
const typeCtor = tcbCallTypeCtor(this.dir, this.tcb, inputs);
|
||||
addParseSpanInfo(typeCtor, this.node.sourceSpan);
|
||||
const typeCtor = tcbCallTypeCtor(this.dir, this.tcb, Array.from(genericInputs.values()));
|
||||
ignoreDiagnostics(typeCtor);
|
||||
this.scope.addStatement(tsCreateVariable(id, typeCtor));
|
||||
return id;
|
||||
}
|
||||
|
||||
circularFallback(): TcbOp {
|
||||
return new TcbDirectiveCircularFallbackOp(this.tcb, this.scope, this.node, this.dir);
|
||||
return new TcbDirectiveCtorCircularFallbackOp(this.tcb, this.scope, this.node, this.dir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A `TcbOp` which generates code to check input bindings on an element that correspond with the
|
||||
* members of a directive.
|
||||
*
|
||||
* Executing this operation returns nothing.
|
||||
*/
|
||||
class TcbDirectiveInputsOp extends TcbOp {
|
||||
constructor(
|
||||
private tcb: Context, private scope: Scope, private node: TmplAstTemplate|TmplAstElement,
|
||||
private dir: TypeCheckableDirectiveMeta) {
|
||||
super();
|
||||
}
|
||||
|
||||
get optional() {
|
||||
return false;
|
||||
}
|
||||
|
||||
execute(): null {
|
||||
let dirId: ts.Expression|null = null;
|
||||
|
||||
// TODO(joost): report duplicate properties
|
||||
|
||||
const inputs = getBoundInputs(this.dir, this.node, this.tcb);
|
||||
for (const input of inputs) {
|
||||
// For bound inputs, the property is assigned the binding expression.
|
||||
let expr = translateInput(input.attribute, this.tcb, this.scope);
|
||||
if (!this.tcb.env.config.checkTypeOfInputBindings) {
|
||||
// If checking the type of bindings is disabled, cast the resulting expression to 'any'
|
||||
// before the assignment.
|
||||
expr = tsCastToAny(expr);
|
||||
} else if (!this.tcb.env.config.strictNullInputBindings) {
|
||||
// If strict null checks are disabled, erase `null` and `undefined` from the type by
|
||||
// wrapping the expression in a non-null assertion.
|
||||
expr = ts.createNonNullExpression(expr);
|
||||
}
|
||||
|
||||
let assignment: ts.Expression = wrapForDiagnostics(expr);
|
||||
|
||||
for (const fieldName of input.fieldNames) {
|
||||
let target: ts.LeftHandSideExpression;
|
||||
if (this.dir.coercedInputFields.has(fieldName)) {
|
||||
// The input has a coercion declaration which should be used instead of assigning the
|
||||
// expression into the input field directly. To achieve this, a variable is declared
|
||||
// with a type of `typeof Directive.ngAcceptInputType_fieldName` which is then used as
|
||||
// target of the assignment.
|
||||
const dirTypeRef = this.tcb.env.referenceType(this.dir.ref);
|
||||
if (!ts.isTypeReferenceNode(dirTypeRef)) {
|
||||
throw new Error(
|
||||
`Expected TypeReferenceNode from reference to ${this.dir.ref.debugName}`);
|
||||
}
|
||||
|
||||
const id = this.tcb.allocateId();
|
||||
const type = tsCreateTypeQueryForCoercedInput(dirTypeRef.typeName, fieldName);
|
||||
this.scope.addStatement(tsDeclareVariable(id, type));
|
||||
|
||||
target = id;
|
||||
} else if (this.dir.undeclaredInputFields.has(fieldName)) {
|
||||
// If no coercion declaration is present nor is the field declared (i.e. the input is
|
||||
// declared in a `@Directive` or `@Component` decorator's `inputs` property) there is no
|
||||
// assignment target available, so this field is skipped.
|
||||
continue;
|
||||
} else if (
|
||||
!this.tcb.env.config.honorAccessModifiersForInputBindings &&
|
||||
this.dir.restrictedInputFields.has(fieldName)) {
|
||||
// If strict checking of access modifiers is disabled and the field is restricted
|
||||
// (i.e. private/protected/readonly), generate an assignment into a temporary variable
|
||||
// that has the type of the field. This achieves type-checking but circumvents the access
|
||||
// modifiers.
|
||||
if (dirId === null) {
|
||||
dirId = this.scope.resolve(this.node, this.dir);
|
||||
}
|
||||
|
||||
const id = this.tcb.allocateId();
|
||||
const dirTypeRef = this.tcb.env.referenceType(this.dir.ref);
|
||||
if (!ts.isTypeReferenceNode(dirTypeRef)) {
|
||||
throw new Error(
|
||||
`Expected TypeReferenceNode from reference to ${this.dir.ref.debugName}`);
|
||||
}
|
||||
const type = ts.createIndexedAccessTypeNode(
|
||||
ts.createTypeQueryNode(dirId as ts.Identifier),
|
||||
ts.createLiteralTypeNode(ts.createStringLiteral(fieldName)));
|
||||
const temp = tsDeclareVariable(id, type);
|
||||
this.scope.addStatement(temp);
|
||||
target = id;
|
||||
} else {
|
||||
if (dirId === null) {
|
||||
dirId = this.scope.resolve(this.node, this.dir);
|
||||
}
|
||||
|
||||
// To get errors assign directly to the fields on the instance, using property access
|
||||
// when possible. String literal fields may not be valid JS identifiers so we use
|
||||
// literal element access instead for those cases.
|
||||
target = this.dir.stringLiteralInputFields.has(fieldName) ?
|
||||
ts.createElementAccess(dirId, ts.createStringLiteral(fieldName)) :
|
||||
ts.createPropertyAccess(dirId, ts.createIdentifier(fieldName));
|
||||
}
|
||||
|
||||
// Finally the assignment is extended by assigning it into the target expression.
|
||||
assignment = ts.createBinary(target, ts.SyntaxKind.EqualsToken, assignment);
|
||||
}
|
||||
|
||||
addParseSpanInfo(assignment, input.attribute.sourceSpan);
|
||||
this.scope.addStatement(ts.createExpressionStatement(assignment));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A `TcbOp` which is used to generate a fallback expression if the inference of a directive type
|
||||
* via `TcbDirectiveOp` requires a reference to its own type. This can happen using a template
|
||||
* via `TcbDirectiveCtorOp` requires a reference to its own type. This can happen using a template
|
||||
* reference:
|
||||
*
|
||||
* ```html
|
||||
* <some-cmp #ref [prop]="ref.foo"></some-cmp>
|
||||
* ```
|
||||
*
|
||||
* In this case, `TcbDirectiveCircularFallbackOp` will add a second inference of the directive type
|
||||
* to the type-check block, this time calling the directive's type constructor without any input
|
||||
* expressions. This infers the widest possible supertype for the directive, which is used to
|
||||
* In this case, `TcbDirectiveCtorCircularFallbackOp` will add a second inference of the directive
|
||||
* type to the type-check block, this time calling the directive's type constructor without any
|
||||
* input expressions. This infers the widest possible supertype for the directive, which is used to
|
||||
* resolve any recursive references required to infer the real type.
|
||||
*/
|
||||
class TcbDirectiveCircularFallbackOp extends TcbOp {
|
||||
class TcbDirectiveCtorCircularFallbackOp extends TcbOp {
|
||||
constructor(
|
||||
private tcb: Context, private scope: Scope, private node: TmplAstTemplate|TmplAstElement,
|
||||
private dir: TypeCheckableDirectiveMeta) {
|
||||
super();
|
||||
}
|
||||
|
||||
get optional() {
|
||||
return false;
|
||||
}
|
||||
|
||||
execute(): ts.Identifier {
|
||||
const id = this.tcb.allocateId();
|
||||
const typeCtor = this.tcb.env.typeCtorFor(this.dir);
|
||||
@ -387,6 +611,10 @@ class TcbDomSchemaCheckerOp extends TcbOp {
|
||||
super();
|
||||
}
|
||||
|
||||
get optional() {
|
||||
return false;
|
||||
}
|
||||
|
||||
execute(): ts.Expression|null {
|
||||
if (this.checkElement) {
|
||||
this.tcb.domSchemaChecker.checkElement(this.tcb.id, this.element, this.tcb.schemas);
|
||||
@ -443,10 +671,14 @@ class TcbUnclaimedInputsOp extends TcbOp {
|
||||
super();
|
||||
}
|
||||
|
||||
get optional() {
|
||||
return false;
|
||||
}
|
||||
|
||||
execute(): null {
|
||||
// `this.inputs` contains only those bindings not matched by any directive. These bindings go to
|
||||
// the element itself.
|
||||
const elId = this.scope.resolve(this.element);
|
||||
let elId: ts.Expression|null = null;
|
||||
|
||||
// TODO(alxhub): this could be more efficient.
|
||||
for (const binding of this.element.inputs) {
|
||||
@ -468,6 +700,9 @@ class TcbUnclaimedInputsOp extends TcbOp {
|
||||
|
||||
if (this.tcb.env.config.checkTypeOfDomBindings && binding.type === BindingType.Property) {
|
||||
if (binding.name !== 'style' && binding.name !== 'class') {
|
||||
if (elId === null) {
|
||||
elId = this.scope.resolve(this.element);
|
||||
}
|
||||
// A direct binding to a property.
|
||||
const propertyName = ATTR_TO_PROP[binding.name] || binding.name;
|
||||
const prop = ts.createElementAccess(elId, ts.createStringLiteral(propertyName));
|
||||
@ -502,8 +737,13 @@ class TcbDirectiveOutputsOp extends TcbOp {
|
||||
super();
|
||||
}
|
||||
|
||||
get optional() {
|
||||
return false;
|
||||
}
|
||||
|
||||
execute(): null {
|
||||
const dirId = this.scope.resolve(this.node, this.dir);
|
||||
let dirId: ts.Expression|null = null;
|
||||
|
||||
|
||||
// `dir.outputs` is an object map of field names on the directive class to event names.
|
||||
// This is backwards from what's needed to match event handlers - a map of event names to field
|
||||
@ -533,6 +773,9 @@ class TcbDirectiveOutputsOp extends TcbOp {
|
||||
// that has a `subscribe` method that properly carries the `T` into the handler function.
|
||||
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Infer);
|
||||
|
||||
if (dirId === null) {
|
||||
dirId = this.scope.resolve(this.node, this.dir);
|
||||
}
|
||||
const outputField = ts.createElementAccess(dirId, ts.createStringLiteral(field));
|
||||
const outputHelper =
|
||||
ts.createCall(this.tcb.env.declareOutputHelper(), undefined, [outputField]);
|
||||
@ -569,8 +812,12 @@ class TcbUnclaimedOutputsOp extends TcbOp {
|
||||
super();
|
||||
}
|
||||
|
||||
get optional() {
|
||||
return false;
|
||||
}
|
||||
|
||||
execute(): null {
|
||||
const elId = this.scope.resolve(this.element);
|
||||
let elId: ts.Expression|null = null;
|
||||
|
||||
// TODO(alxhub): this could be more efficient.
|
||||
for (const output of this.element.outputs) {
|
||||
@ -595,6 +842,9 @@ class TcbUnclaimedOutputsOp extends TcbOp {
|
||||
// base `Event` type.
|
||||
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Infer);
|
||||
|
||||
if (elId === null) {
|
||||
elId = this.scope.resolve(this.element);
|
||||
}
|
||||
const call = ts.createCall(
|
||||
/* expression */ ts.createPropertyAccess(elId, 'addEventListener'),
|
||||
/* typeArguments */ undefined,
|
||||
@ -694,8 +944,8 @@ class Scope {
|
||||
*/
|
||||
private elementOpMap = new Map<TmplAstElement, number>();
|
||||
/**
|
||||
* A map of maps which tracks the index of `TcbDirectiveOp`s in the `opQueue` for each directive
|
||||
* on a `TmplAstElement` or `TmplAstTemplate` node.
|
||||
* A map of maps which tracks the index of `TcbDirectiveCtorOp`s in the `opQueue` for each
|
||||
* directive on a `TmplAstElement` or `TmplAstTemplate` node.
|
||||
*/
|
||||
private directiveOpMap =
|
||||
new Map<TmplAstElement|TmplAstTemplate, Map<TypeCheckableDirectiveMeta, number>>();
|
||||
@ -811,7 +1061,7 @@ class Scope {
|
||||
*/
|
||||
render(): ts.Statement[] {
|
||||
for (let i = 0; i < this.opQueue.length; i++) {
|
||||
this.executeOp(i);
|
||||
this.executeOp(i, /* skipOptional */ true);
|
||||
}
|
||||
return this.statements;
|
||||
}
|
||||
@ -877,7 +1127,7 @@ class Scope {
|
||||
* Like `executeOp`, but assert that the operation actually returned `ts.Expression`.
|
||||
*/
|
||||
private resolveOp(opIndex: number): ts.Expression {
|
||||
const res = this.executeOp(opIndex);
|
||||
const res = this.executeOp(opIndex, /* skipOptional */ false);
|
||||
if (res === null) {
|
||||
throw new Error(`Error resolving operation, got null`);
|
||||
}
|
||||
@ -891,12 +1141,16 @@ class Scope {
|
||||
* and also protects against a circular dependency from the operation to itself by temporarily
|
||||
* setting the operation's result to a special expression.
|
||||
*/
|
||||
private executeOp(opIndex: number): ts.Expression|null {
|
||||
private executeOp(opIndex: number, skipOptional: boolean): ts.Expression|null {
|
||||
const op = this.opQueue[opIndex];
|
||||
if (!(op instanceof TcbOp)) {
|
||||
return op;
|
||||
}
|
||||
|
||||
if (skipOptional && op.optional) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set the result of the operation in the queue to its circular fallback. If executing this
|
||||
// operation results in a circular dependency, this will prevent an infinite loop and allow for
|
||||
// the resolution of such cycles.
|
||||
@ -957,8 +1211,12 @@ class Scope {
|
||||
|
||||
const dirMap = new Map<TypeCheckableDirectiveMeta, number>();
|
||||
for (const dir of directives) {
|
||||
const dirIndex = this.opQueue.push(new TcbDirectiveOp(this.tcb, this, node, dir)) - 1;
|
||||
const directiveOp = dir.isGeneric ? new TcbDirectiveCtorOp(this.tcb, this, node, dir) :
|
||||
new TcbDirectiveTypeOp(this.tcb, this, node, dir);
|
||||
const dirIndex = this.opQueue.push(directiveOp) - 1;
|
||||
dirMap.set(dir, dirIndex);
|
||||
|
||||
this.opQueue.push(new TcbDirectiveInputsOp(this.tcb, this, node, dir));
|
||||
}
|
||||
this.directiveOpMap.set(node, dirMap);
|
||||
|
||||
@ -1016,6 +1274,11 @@ class Scope {
|
||||
}
|
||||
}
|
||||
|
||||
interface TcbBoundInput {
|
||||
attribute: TmplAstBoundAttribute|TmplAstTextAttribute;
|
||||
fieldNames: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the `ctx` parameter to the top-level TCB function.
|
||||
*
|
||||
@ -1269,53 +1532,13 @@ function tcbCallTypeCtor(
|
||||
/* argumentsArray */[ts.createObjectLiteral(members)]);
|
||||
}
|
||||
|
||||
type TcbDirectiveInput = {
|
||||
type: 'binding'; field: string; expression: ts.Expression; sourceSpan: ParseSourceSpan;
|
||||
}|{
|
||||
type: 'unset';
|
||||
field: string;
|
||||
};
|
||||
function getBoundInputs(
|
||||
directive: TypeCheckableDirectiveMeta, node: TmplAstTemplate|TmplAstElement,
|
||||
tcb: Context): TcbBoundInput[] {
|
||||
const boundInputs: TcbBoundInput[] = [];
|
||||
|
||||
function tcbGetDirectiveInputs(
|
||||
el: TmplAstElement|TmplAstTemplate, dir: TypeCheckableDirectiveMeta, tcb: Context,
|
||||
scope: Scope): TcbDirectiveInput[] {
|
||||
// Only the first binding to a property is written.
|
||||
// TODO(alxhub): produce an error for duplicate bindings to the same property, independently of
|
||||
// this logic.
|
||||
const directiveInputs = new Map<string, TcbDirectiveInput>();
|
||||
// `dir.inputs` is an object map of field names on the directive class to property names.
|
||||
// This is backwards from what's needed to match bindings - a map of properties to field names
|
||||
// is desired. Invert `dir.inputs` into `propMatch` to create this map.
|
||||
const propMatch = new Map<string, string>();
|
||||
const inputs = dir.inputs;
|
||||
Object.keys(inputs).forEach(key => {
|
||||
Array.isArray(inputs[key]) ? propMatch.set(inputs[key][0], key) :
|
||||
propMatch.set(inputs[key] as string, key);
|
||||
});
|
||||
|
||||
el.inputs.forEach(processAttribute);
|
||||
el.attributes.forEach(processAttribute);
|
||||
if (el instanceof TmplAstTemplate) {
|
||||
el.templateAttrs.forEach(processAttribute);
|
||||
}
|
||||
|
||||
// Add unset directive inputs for each of the remaining unset fields.
|
||||
// Note: it's actually important here that `propMatch.values()` isn't used, as there can be
|
||||
// multiple fields which share the same property name and only one of them will be listed as a
|
||||
// value in `propMatch`.
|
||||
for (const field of Object.keys(inputs)) {
|
||||
if (!directiveInputs.has(field)) {
|
||||
directiveInputs.set(field, {type: 'unset', field});
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(directiveInputs.values());
|
||||
|
||||
/**
|
||||
* Add a binding expression to the map for each input/template attribute of the directive that has
|
||||
* a matching binding.
|
||||
*/
|
||||
function processAttribute(attr: TmplAstBoundAttribute|TmplAstTextAttribute): void {
|
||||
const propertyToFieldNames = invertInputs(directive.inputs);
|
||||
const processAttribute = (attr: TmplAstBoundAttribute|TmplAstTextAttribute) => {
|
||||
// Skip non-property bindings.
|
||||
if (attr instanceof TmplAstBoundAttribute && attr.type !== BindingType.Property) {
|
||||
return;
|
||||
@ -1327,34 +1550,92 @@ function tcbGetDirectiveInputs(
|
||||
}
|
||||
|
||||
// Skip the attribute if the directive does not have an input for it.
|
||||
if (!propMatch.has(attr.name)) {
|
||||
if (!propertyToFieldNames.has(attr.name)) {
|
||||
return;
|
||||
}
|
||||
const field = propMatch.get(attr.name)!;
|
||||
const fieldNames = propertyToFieldNames.get(attr.name)!;
|
||||
boundInputs.push({attribute: attr, fieldNames});
|
||||
};
|
||||
|
||||
// Skip the attribute if a previous binding also wrote to it.
|
||||
if (directiveInputs.has(field)) {
|
||||
return;
|
||||
}
|
||||
node.inputs.forEach(processAttribute);
|
||||
node.attributes.forEach(processAttribute);
|
||||
if (node instanceof TmplAstTemplate) {
|
||||
node.templateAttrs.forEach(processAttribute);
|
||||
}
|
||||
|
||||
let expr: ts.Expression;
|
||||
if (attr instanceof TmplAstBoundAttribute) {
|
||||
// Produce an expression representing the value of the binding.
|
||||
expr = tcbExpression(attr.value, tcb, scope);
|
||||
} else {
|
||||
// For regular attributes with a static string value, use the represented string literal.
|
||||
expr = ts.createStringLiteral(attr.value);
|
||||
}
|
||||
return boundInputs;
|
||||
}
|
||||
|
||||
directiveInputs.set(field, {
|
||||
type: 'binding',
|
||||
field: field,
|
||||
expression: expr,
|
||||
sourceSpan: attr.sourceSpan,
|
||||
});
|
||||
/**
|
||||
* Translates the given attribute binding to a `ts.Expression`.
|
||||
*/
|
||||
function translateInput(
|
||||
attr: TmplAstBoundAttribute|TmplAstTextAttribute, tcb: Context, scope: Scope): ts.Expression {
|
||||
if (attr instanceof TmplAstBoundAttribute) {
|
||||
// Produce an expression representing the value of the binding.
|
||||
return tcbExpression(attr.value, tcb, scope);
|
||||
} else {
|
||||
// For regular attributes with a static string value, use the represented string literal.
|
||||
return ts.createStringLiteral(attr.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverts the input-mapping from field-to-property name into property-to-field name, to be able
|
||||
* to match a property in a template with the corresponding field on a directive.
|
||||
*/
|
||||
function invertInputs(inputs: {[fieldName: string]: string|[string, string]}):
|
||||
Map<string, string[]> {
|
||||
const propertyToFieldNames = new Map<string, string[]>();
|
||||
for (const fieldName of Object.keys(inputs)) {
|
||||
const propertyNames = inputs[fieldName];
|
||||
const propertyName = Array.isArray(propertyNames) ? propertyNames[0] : propertyNames;
|
||||
|
||||
if (propertyToFieldNames.has(propertyName)) {
|
||||
propertyToFieldNames.get(propertyName)!.push(fieldName);
|
||||
} else {
|
||||
propertyToFieldNames.set(propertyName, [fieldName]);
|
||||
}
|
||||
}
|
||||
return propertyToFieldNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* An input binding that corresponds with a field of a directive.
|
||||
*/
|
||||
interface TcbDirectiveBoundInput {
|
||||
type: 'binding';
|
||||
|
||||
/**
|
||||
* The name of a field on the directive that is set.
|
||||
*/
|
||||
field: string;
|
||||
|
||||
/**
|
||||
* The `ts.Expression` corresponding with the input binding expression.
|
||||
*/
|
||||
expression: ts.Expression;
|
||||
|
||||
/**
|
||||
* The source span of the full attribute binding.
|
||||
*/
|
||||
sourceSpan: ParseSourceSpan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that a certain field of a directive does not have a corresponding input binding.
|
||||
*/
|
||||
interface TcbDirectiveUnsetInput {
|
||||
type: 'unset';
|
||||
|
||||
/**
|
||||
* The name of a field on the directive for which no input binding is present.
|
||||
*/
|
||||
field: string;
|
||||
}
|
||||
|
||||
type TcbDirectiveInput = TcbDirectiveBoundInput|TcbDirectiveUnsetInput;
|
||||
|
||||
const EVENT_PARAMETER = '$event';
|
||||
|
||||
const enum EventParamType {
|
||||
|
@ -11,6 +11,7 @@ import * as ts from 'typescript';
|
||||
import {ClassDeclaration, ReflectionHost} from '../../reflection';
|
||||
import {TypeCtorMetadata} from '../api';
|
||||
|
||||
import {tsCreateTypeQueryForCoercedInput} from './ts_util';
|
||||
import {TypeParameterEmitter} from './type_parameter_emitter';
|
||||
|
||||
export function generateTypeCtorDeclarationFn(
|
||||
@ -150,9 +151,7 @@ function constructTypeCtorParameter(
|
||||
/* modifiers */ undefined,
|
||||
/* name */ key,
|
||||
/* questionToken */ undefined,
|
||||
/* type */
|
||||
ts.createTypeQueryNode(
|
||||
ts.createQualifiedName(rawType.typeName, `ngAcceptInputType_${key}`)),
|
||||
/* type */ tsCreateTypeQueryForCoercedInput(rawType.typeName, key),
|
||||
/* initializer */ undefined));
|
||||
}
|
||||
}
|
||||
|
@ -227,7 +227,8 @@ runInEachFileSystem(() => {
|
||||
name: 'GuardDir',
|
||||
selector: '[guard]',
|
||||
inputs: {'guard': 'guard'},
|
||||
ngTemplateGuards: [{inputName: 'guard', type: 'binding'}]
|
||||
ngTemplateGuards: [{inputName: 'guard', type: 'binding'}],
|
||||
undeclaredInputFields: ['guard'],
|
||||
}]);
|
||||
|
||||
expect(messages).toEqual([
|
||||
|
@ -158,7 +158,7 @@ describe('type check blocks diagnostics', () => {
|
||||
}];
|
||||
const TEMPLATE = `<my-cmp #a></my-cmp>{{ a || a }}`;
|
||||
expect(tcbWithSpans(TEMPLATE, DIRECTIVES))
|
||||
.toContain('((_t2 /*23,24*/) || (_t2 /*28,29*/) /*23,29*/);');
|
||||
.toContain('((_t1 /*23,24*/) || (_t1 /*28,29*/) /*23,29*/);');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -120,8 +120,9 @@ export function ngForDeclaration(): TestDeclaration {
|
||||
file: absoluteFrom('/ngfor.d.ts'),
|
||||
selector: '[ngForOf]',
|
||||
name: 'NgForOf',
|
||||
inputs: {ngForOf: 'ngForOf'},
|
||||
inputs: {ngForOf: 'ngForOf', ngForTrackBy: 'ngForTrackBy', ngForTemplate: 'ngForTemplate'},
|
||||
hasNgTemplateContextGuard: true,
|
||||
isGeneric: true,
|
||||
};
|
||||
}
|
||||
|
||||
@ -156,6 +157,7 @@ export const ALL_ENABLED_CONFIG: TypeCheckingConfig = {
|
||||
checkQueries: false,
|
||||
checkTemplateBodies: true,
|
||||
checkTypeOfInputBindings: true,
|
||||
honorAccessModifiersForInputBindings: true,
|
||||
strictNullInputBindings: true,
|
||||
checkTypeOfAttributes: true,
|
||||
// Feature is still in development.
|
||||
@ -175,11 +177,13 @@ export const ALL_ENABLED_CONFIG: TypeCheckingConfig = {
|
||||
// Remove 'ref' from TypeCheckableDirectiveMeta and add a 'selector' instead.
|
||||
export type TestDirective = Partial<Pick<
|
||||
TypeCheckableDirectiveMeta,
|
||||
Exclude<keyof TypeCheckableDirectiveMeta, 'ref'|'coercedInputFields'>>>&{
|
||||
selector: string,
|
||||
name: string,
|
||||
file?: AbsoluteFsPath, type: 'directive',
|
||||
coercedInputFields?: string[],
|
||||
Exclude<
|
||||
keyof TypeCheckableDirectiveMeta,
|
||||
'ref'|'coercedInputFields'|'restrictedInputFields'|'stringLiteralInputFields'|
|
||||
'undeclaredInputFields'>>>&{
|
||||
selector: string, name: string, file?: AbsoluteFsPath, type: 'directive',
|
||||
coercedInputFields?: string[], restrictedInputFields?: string[],
|
||||
stringLiteralInputFields?: string[], undeclaredInputFields?: string[], isGeneric?: boolean;
|
||||
};
|
||||
export type TestPipe = {
|
||||
name: string,
|
||||
@ -210,6 +214,7 @@ export function tcb(
|
||||
applyTemplateContextGuards: true,
|
||||
checkQueries: false,
|
||||
checkTypeOfInputBindings: true,
|
||||
honorAccessModifiersForInputBindings: false,
|
||||
strictNullInputBindings: true,
|
||||
checkTypeOfAttributes: true,
|
||||
checkTypeOfDomBindings: false,
|
||||
@ -417,6 +422,10 @@ function prepareDeclarations(
|
||||
isComponent: decl.isComponent || false,
|
||||
ngTemplateGuards: decl.ngTemplateGuards || [],
|
||||
coercedInputFields: new Set<string>(decl.coercedInputFields || []),
|
||||
restrictedInputFields: new Set<string>(decl.restrictedInputFields || []),
|
||||
stringLiteralInputFields: new Set<string>(decl.stringLiteralInputFields || []),
|
||||
undeclaredInputFields: new Set<string>(decl.undeclaredInputFields || []),
|
||||
isGeneric: decl.isGeneric ?? false,
|
||||
outputs: decl.outputs || {},
|
||||
queries: decl.queries || [],
|
||||
};
|
||||
|
@ -42,6 +42,11 @@ describe('type check blocks', () => {
|
||||
.toContain('(((ctx).a) ? ((ctx).b) : (((ctx).c) ? ((ctx).d) : ((ctx).e)))');
|
||||
});
|
||||
|
||||
it('should handle quote expressions as any type', () => {
|
||||
const TEMPLATE = `<span [quote]="sql:expression"></span>`;
|
||||
expect(tcb(TEMPLATE)).toContain('null as any');
|
||||
});
|
||||
|
||||
it('should handle attribute values for directive inputs', () => {
|
||||
const TEMPLATE = `<div dir inputA="value"></div>`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
@ -50,7 +55,7 @@ describe('type check blocks', () => {
|
||||
selector: '[dir]',
|
||||
inputs: {inputA: 'inputA'},
|
||||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('"inputA": ("value")');
|
||||
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t1: DirA = (null!); _t1.inputA = ("value");');
|
||||
});
|
||||
|
||||
it('should handle multiple bindings to the same property', () => {
|
||||
@ -62,8 +67,8 @@ describe('type check blocks', () => {
|
||||
inputs: {inputA: 'inputA'},
|
||||
}];
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain('"inputA": (1)');
|
||||
expect(block).not.toContain('"inputA": (2)');
|
||||
expect(block).toContain('_t1.inputA = (1);');
|
||||
expect(block).toContain('_t1.inputA = (2);');
|
||||
});
|
||||
|
||||
it('should handle empty bindings', () => {
|
||||
@ -74,7 +79,7 @@ describe('type check blocks', () => {
|
||||
selector: '[dir-a]',
|
||||
inputs: {inputA: 'inputA'},
|
||||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('"inputA": (undefined)');
|
||||
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t1.inputA = (undefined);');
|
||||
});
|
||||
|
||||
it('should handle bindings without value', () => {
|
||||
@ -85,7 +90,7 @@ describe('type check blocks', () => {
|
||||
selector: '[dir-a]',
|
||||
inputs: {inputA: 'inputA'},
|
||||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('"inputA": (undefined)');
|
||||
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t1.inputA = (undefined);');
|
||||
});
|
||||
|
||||
it('should handle implicit vars on ng-template', () => {
|
||||
@ -104,20 +109,209 @@ describe('type check blocks', () => {
|
||||
expect(tcb(TEMPLATE)).toContain('var _t2 = _t1.$implicit;');
|
||||
});
|
||||
|
||||
it('should handle missing property bindings', () => {
|
||||
const TEMPLATE = `<div dir [inputA]="foo"></div>`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {
|
||||
fieldA: 'inputA',
|
||||
fieldB: 'inputB',
|
||||
describe('type constructors', () => {
|
||||
it('should handle missing property bindings', () => {
|
||||
const TEMPLATE = `<div dir [inputA]="foo"></div>`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {
|
||||
fieldA: 'inputA',
|
||||
fieldB: 'inputB',
|
||||
},
|
||||
isGeneric: true,
|
||||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t1 = Dir.ngTypeCtor({ "fieldA": (((ctx).foo)), "fieldB": (null as any) });');
|
||||
});
|
||||
|
||||
it('should handle multiple bindings to the same property', () => {
|
||||
const TEMPLATE = `<div dir [inputA]="1" [inputA]="2"></div>`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {
|
||||
fieldA: 'inputA',
|
||||
},
|
||||
isGeneric: true,
|
||||
}];
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain('"fieldA": (1)');
|
||||
expect(block).not.toContain('"fieldA": (2)');
|
||||
});
|
||||
|
||||
|
||||
it('should only apply property bindings to directives', () => {
|
||||
const TEMPLATE = `
|
||||
<div dir [style.color]="'blue'" [class.strong]="false" [attr.enabled]="true"></div>
|
||||
`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {'color': 'color', 'strong': 'strong', 'enabled': 'enabled'},
|
||||
isGeneric: true,
|
||||
}];
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).not.toContain('Dir.ngTypeCtor');
|
||||
expect(block).toContain('"blue"; false; true;');
|
||||
});
|
||||
|
||||
it('should generate a circular directive reference correctly', () => {
|
||||
const TEMPLATE = `
|
||||
<div dir #d="dir" [input]="d"></div>
|
||||
`;
|
||||
const DIRECTIVES: TestDirective[] = [{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
exportAs: ['dir'],
|
||||
inputs: {input: 'input'},
|
||||
isGeneric: true,
|
||||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t2 = Dir.ngTypeCtor((null!)); ' +
|
||||
'var _t1 = Dir.ngTypeCtor({ "input": (_t2) });');
|
||||
});
|
||||
|
||||
it('should generate circular references between two directives correctly', () => {
|
||||
const TEMPLATE = `
|
||||
<div #a="dirA" dir-a [inputA]="b">A</div>
|
||||
<div #b="dirB" dir-b [inputB]="a">B</div>
|
||||
`;
|
||||
const DIRECTIVES: TestDirective[] = [
|
||||
{
|
||||
type: 'directive',
|
||||
name: 'DirA',
|
||||
selector: '[dir-a]',
|
||||
exportAs: ['dirA'],
|
||||
inputs: {inputA: 'inputA'},
|
||||
isGeneric: true,
|
||||
},
|
||||
{
|
||||
type: 'directive',
|
||||
name: 'DirB',
|
||||
selector: '[dir-b]',
|
||||
exportAs: ['dirB'],
|
||||
inputs: {inputB: 'inputB'},
|
||||
isGeneric: true,
|
||||
}
|
||||
];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t3 = DirB.ngTypeCtor((null!)); ' +
|
||||
'var _t2 = DirA.ngTypeCtor({ "inputA": (_t3) }); ' +
|
||||
'var _t1 = DirB.ngTypeCtor({ "inputB": (_t2) });');
|
||||
});
|
||||
|
||||
it('should handle empty bindings', () => {
|
||||
const TEMPLATE = `<div dir-a [inputA]=""></div>`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'DirA',
|
||||
selector: '[dir-a]',
|
||||
inputs: {inputA: 'inputA'},
|
||||
isGeneric: true,
|
||||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('"inputA": (undefined)');
|
||||
});
|
||||
|
||||
it('should handle bindings without value', () => {
|
||||
const TEMPLATE = `<div dir-a [inputA]></div>`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'DirA',
|
||||
selector: '[dir-a]',
|
||||
inputs: {inputA: 'inputA'},
|
||||
isGeneric: true,
|
||||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('"inputA": (undefined)');
|
||||
});
|
||||
|
||||
it('should use coercion types if declared', () => {
|
||||
const TEMPLATE = `<div dir [inputA]="foo"></div>`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {
|
||||
fieldA: 'inputA',
|
||||
},
|
||||
isGeneric: true,
|
||||
coercedInputFields: ['fieldA'],
|
||||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t1: typeof Dir.ngAcceptInputType_fieldA = (null!); ' +
|
||||
'_t1 = (((ctx).foo));');
|
||||
});
|
||||
});
|
||||
|
||||
it('should only generate code for DOM elements that are actually referenced', () => {
|
||||
const TEMPLATE = `
|
||||
<div></div>
|
||||
<button #me (click)="handle(me)"></button>
|
||||
`;
|
||||
const block = tcb(TEMPLATE);
|
||||
expect(block).not.toContain('"div"');
|
||||
expect(block).toContain('var _t1 = document.createElement("button");');
|
||||
expect(block).toContain('(ctx).handle(_t1);');
|
||||
});
|
||||
|
||||
it('should only generate directive declarations that have bindings or are referenced', () => {
|
||||
const TEMPLATE = `
|
||||
<div
|
||||
hasInput [input]="value"
|
||||
hasOutput (output)="handle()"
|
||||
hasReference #ref="ref"
|
||||
noReference
|
||||
noBindings>{{ref.a}}</div>
|
||||
`;
|
||||
const DIRECTIVES: TestDeclaration[] = [
|
||||
{
|
||||
type: 'directive',
|
||||
name: 'HasInput',
|
||||
selector: '[hasInput]',
|
||||
inputs: {input: 'input'},
|
||||
},
|
||||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t2 = Dir.ngTypeCtor({ "fieldA": (((ctx).foo)), "fieldB": (null as any) });');
|
||||
{
|
||||
type: 'directive',
|
||||
name: 'HasOutput',
|
||||
selector: '[hasOutput]',
|
||||
outputs: {output: 'output'},
|
||||
},
|
||||
{
|
||||
type: 'directive',
|
||||
name: 'HasReference',
|
||||
selector: '[hasReference]',
|
||||
exportAs: ['ref'],
|
||||
},
|
||||
{
|
||||
type: 'directive',
|
||||
name: 'NoReference',
|
||||
selector: '[noReference]',
|
||||
exportAs: ['no-ref'],
|
||||
},
|
||||
{
|
||||
type: 'directive',
|
||||
name: 'NoBindings',
|
||||
selector: '[noBindings]',
|
||||
inputs: {unset: 'unset'},
|
||||
},
|
||||
];
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain('var _t1: HasInput = (null!)');
|
||||
expect(block).toContain('_t1.input = (((ctx).value));');
|
||||
expect(block).toContain('var _t2: HasOutput = (null!)');
|
||||
expect(block).toContain('_t2["output"]');
|
||||
expect(block).toContain('var _t3: HasReference = (null!)');
|
||||
expect(block).toContain('(_t3).a');
|
||||
expect(block).not.toContain('NoBindings');
|
||||
expect(block).not.toContain('NoReference');
|
||||
});
|
||||
|
||||
it('should generate a forward element reference correctly', () => {
|
||||
@ -140,9 +334,7 @@ describe('type check blocks', () => {
|
||||
selector: '[dir]',
|
||||
exportAs: ['dir'],
|
||||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t1 = Dir.ngTypeCtor({}); "" + ((_t1).value); var _t2 = document.createElement("div");');
|
||||
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('var _t1: Dir = (null!); "" + ((_t1).value);');
|
||||
});
|
||||
|
||||
it('should handle style and class bindings specially', () => {
|
||||
@ -168,8 +360,10 @@ describe('type check blocks', () => {
|
||||
inputs: {'color': 'color', 'strong': 'strong', 'enabled': 'enabled'},
|
||||
}];
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain(
|
||||
'var _t2 = Dir.ngTypeCtor({ "color": (null as any), "strong": (null as any), "enabled": (null as any) });');
|
||||
expect(block).not.toContain('var _t1: Dir = (null!);');
|
||||
expect(block).not.toContain('"color"');
|
||||
expect(block).not.toContain('"strong"');
|
||||
expect(block).not.toContain('"enabled"');
|
||||
expect(block).toContain('"blue"; false; true;');
|
||||
});
|
||||
|
||||
@ -186,8 +380,8 @@ describe('type check blocks', () => {
|
||||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t3 = Dir.ngTypeCtor((null!)); ' +
|
||||
'var _t2 = Dir.ngTypeCtor({ "input": (_t3) });');
|
||||
'var _t1: Dir = (null!); ' +
|
||||
'_t1.input = (_t1);');
|
||||
});
|
||||
|
||||
it('should generate circular references between two directives correctly', () => {
|
||||
@ -213,9 +407,155 @@ describe('type check blocks', () => {
|
||||
];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t4 = DirA.ngTypeCtor((null!)); ' +
|
||||
'var _t3 = DirB.ngTypeCtor({ "inputA": (_t4) }); ' +
|
||||
'var _t2 = DirA.ngTypeCtor({ "inputA": (_t3) });');
|
||||
'var _t1: DirB = (null!); ' +
|
||||
'var _t2: DirA = (null!); ' +
|
||||
'_t2.inputA = (_t1); ' +
|
||||
'_t1.inputA = (_t2);');
|
||||
});
|
||||
|
||||
it('should handle undeclared properties', () => {
|
||||
const TEMPLATE = `<div dir [inputA]="foo"></div>`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {
|
||||
fieldA: 'inputA',
|
||||
},
|
||||
undeclaredInputFields: ['fieldA']
|
||||
}];
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).not.toContain('var _t1: Dir = (null!);');
|
||||
expect(block).toContain('(((ctx).foo)); ');
|
||||
});
|
||||
|
||||
it('should assign restricted properties to temp variables by default', () => {
|
||||
const TEMPLATE = `<div dir [inputA]="foo"></div>`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {
|
||||
fieldA: 'inputA',
|
||||
},
|
||||
restrictedInputFields: ['fieldA']
|
||||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t1: Dir = (null!); ' +
|
||||
'var _t2: typeof _t1["fieldA"] = (null!); ' +
|
||||
'_t2 = (((ctx).foo)); ');
|
||||
});
|
||||
|
||||
it('should assign properties via element access for field names that are not JS identifiers',
|
||||
() => {
|
||||
const TEMPLATE = `<div dir [inputA]="foo"></div>`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {
|
||||
'some-input.xs': 'inputA',
|
||||
},
|
||||
stringLiteralInputFields: ['some-input.xs'],
|
||||
}];
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain(
|
||||
'var _t1: Dir = (null!); ' +
|
||||
'_t1["some-input.xs"] = (((ctx).foo)); ');
|
||||
});
|
||||
|
||||
it('should handle a single property bound to multiple fields', () => {
|
||||
const TEMPLATE = `<div dir [inputA]="foo"></div>`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {
|
||||
field1: 'inputA',
|
||||
field2: 'inputA',
|
||||
},
|
||||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t1: Dir = (null!); ' +
|
||||
'_t1.field2 = _t1.field1 = (((ctx).foo));');
|
||||
});
|
||||
|
||||
it('should handle a single property bound to multiple fields, where one of them is coerced',
|
||||
() => {
|
||||
const TEMPLATE = `<div dir [inputA]="foo"></div>`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {
|
||||
field1: 'inputA',
|
||||
field2: 'inputA',
|
||||
},
|
||||
coercedInputFields: ['field1'],
|
||||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t1: typeof Dir.ngAcceptInputType_field1 = (null!); ' +
|
||||
'var _t2: Dir = (null!); ' +
|
||||
'_t2.field2 = _t1 = (((ctx).foo));');
|
||||
});
|
||||
|
||||
it('should handle a single property bound to multiple fields, where one of them is undeclared',
|
||||
() => {
|
||||
const TEMPLATE = `<div dir [inputA]="foo"></div>`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {
|
||||
field1: 'inputA',
|
||||
field2: 'inputA',
|
||||
},
|
||||
undeclaredInputFields: ['field1'],
|
||||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t1: Dir = (null!); ' +
|
||||
'_t1.field2 = (((ctx).foo));');
|
||||
});
|
||||
|
||||
it('should use coercion types if declared', () => {
|
||||
const TEMPLATE = `<div dir [inputA]="foo"></div>`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {
|
||||
fieldA: 'inputA',
|
||||
},
|
||||
coercedInputFields: ['fieldA'],
|
||||
}];
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).not.toContain('var _t1: Dir = (null!);');
|
||||
expect(block).toContain(
|
||||
'var _t1: typeof Dir.ngAcceptInputType_fieldA = (null!); ' +
|
||||
'_t1 = (((ctx).foo));');
|
||||
});
|
||||
|
||||
it('should use coercion types if declared, even when backing field is not declared', () => {
|
||||
const TEMPLATE = `<div dir [inputA]="foo"></div>`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {
|
||||
fieldA: 'inputA',
|
||||
},
|
||||
coercedInputFields: ['fieldA'],
|
||||
undeclaredInputFields: ['fieldA'],
|
||||
}];
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).not.toContain('var _t1: Dir = (null!);');
|
||||
expect(block).toContain(
|
||||
'var _t1: typeof Dir.ngAcceptInputType_fieldA = (null!); ' +
|
||||
'_t1 = (((ctx).foo));');
|
||||
});
|
||||
|
||||
it('should handle $any casts', () => {
|
||||
@ -245,7 +585,7 @@ describe('type check blocks', () => {
|
||||
type: 'invocation',
|
||||
}]
|
||||
}];
|
||||
const TEMPLATE = `<div *ngIf="person"></div>`;
|
||||
const TEMPLATE = `<div *ngIf="person">{{person.name}}</div>`;
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain('if (NgIf.ngTemplateGuard_ngIf(_t1, ((ctx).person)))');
|
||||
});
|
||||
@ -261,10 +601,26 @@ describe('type check blocks', () => {
|
||||
type: 'binding',
|
||||
}]
|
||||
}];
|
||||
const TEMPLATE = `<div *ngIf="person !== null"></div>`;
|
||||
const TEMPLATE = `<div *ngIf="person !== null">{{person.name}}</div>`;
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain('if ((((ctx).person)) !== (null))');
|
||||
});
|
||||
|
||||
it('should not emit guards when the child scope is empty', () => {
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'NgIf',
|
||||
selector: '[ngIf]',
|
||||
inputs: {'ngIf': 'ngIf'},
|
||||
ngTemplateGuards: [{
|
||||
inputName: 'ngIf',
|
||||
type: 'invocation',
|
||||
}]
|
||||
}];
|
||||
const TEMPLATE = `<div *ngIf="person">static</div>`;
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).not.toContain('NgIf.ngTemplateGuard_ngIf');
|
||||
});
|
||||
});
|
||||
|
||||
describe('outputs', () => {
|
||||
@ -278,7 +634,7 @@ describe('type check blocks', () => {
|
||||
const TEMPLATE = `<div dir (dirOutput)="foo($event)"></div>`;
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain(
|
||||
'_outputHelper(_t2["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
|
||||
'_outputHelper(_t1["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
|
||||
});
|
||||
|
||||
it('should emit a listener function with AnimationEvent for animation events', () => {
|
||||
@ -325,6 +681,7 @@ describe('type check blocks', () => {
|
||||
checkQueries: false,
|
||||
checkTemplateBodies: true,
|
||||
checkTypeOfInputBindings: true,
|
||||
honorAccessModifiersForInputBindings: false,
|
||||
strictNullInputBindings: true,
|
||||
checkTypeOfAttributes: true,
|
||||
checkTypeOfDomBindings: false,
|
||||
@ -340,7 +697,7 @@ describe('type check blocks', () => {
|
||||
};
|
||||
|
||||
describe('config.applyTemplateContextGuards', () => {
|
||||
const TEMPLATE = `<div *dir></div>`;
|
||||
const TEMPLATE = `<div *dir>{{ value }}</div>`;
|
||||
const GUARD_APPLIED = 'if (Dir.ngTemplateContextGuard(';
|
||||
|
||||
it('should apply template context guards when enabled', () => {
|
||||
@ -374,14 +731,14 @@ describe('type check blocks', () => {
|
||||
|
||||
it('should include null and undefined when enabled', () => {
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain('Dir.ngTypeCtor({ "dirInput": (((ctx).a)) })');
|
||||
expect(block).toContain('_t1.dirInput = (((ctx).a));');
|
||||
expect(block).toContain('((ctx).b);');
|
||||
});
|
||||
it('should use the non-null assertion operator when disabled', () => {
|
||||
const DISABLED_CONFIG:
|
||||
TypeCheckingConfig = {...BASE_CONFIG, strictNullInputBindings: false};
|
||||
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
|
||||
expect(block).toContain('Dir.ngTypeCtor({ "dirInput": (((ctx).a)!) })');
|
||||
expect(block).toContain('_t1.dirInput = (((ctx).a)!);');
|
||||
expect(block).toContain('((ctx).b)!;');
|
||||
});
|
||||
});
|
||||
@ -390,7 +747,7 @@ describe('type check blocks', () => {
|
||||
it('should check types of bindings when enabled', () => {
|
||||
const TEMPLATE = `<div dir [dirInput]="a" [nonDirInput]="b"></div>`;
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain('Dir.ngTypeCtor({ "dirInput": (((ctx).a)) })');
|
||||
expect(block).toContain('_t1.dirInput = (((ctx).a));');
|
||||
expect(block).toContain('((ctx).b);');
|
||||
});
|
||||
|
||||
@ -399,7 +756,7 @@ describe('type check blocks', () => {
|
||||
const DISABLED_CONFIG:
|
||||
TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false};
|
||||
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
|
||||
expect(block).toContain('Dir.ngTypeCtor({ "dirInput": ((((ctx).a) as any)) })');
|
||||
expect(block).toContain('_t1.dirInput = ((((ctx).a) as any));');
|
||||
expect(block).toContain('(((ctx).b) as any);');
|
||||
});
|
||||
|
||||
@ -408,8 +765,7 @@ describe('type check blocks', () => {
|
||||
const DISABLED_CONFIG:
|
||||
TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false};
|
||||
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
|
||||
expect(block).toContain(
|
||||
'Dir.ngTypeCtor({ "dirInput": ((((((ctx).a)) === (((ctx).b))) as any)) })');
|
||||
expect(block).toContain('_t1.dirInput = ((((((ctx).a)) === (((ctx).b))) as any));');
|
||||
});
|
||||
});
|
||||
|
||||
@ -419,9 +775,9 @@ describe('type check blocks', () => {
|
||||
it('should check types of directive outputs when enabled', () => {
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain(
|
||||
'_outputHelper(_t2["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
|
||||
'_outputHelper(_t1["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
|
||||
expect(block).toContain(
|
||||
'_t1.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
|
||||
'_t2.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
|
||||
});
|
||||
it('should not check types of directive outputs when disabled', () => {
|
||||
const DISABLED_CONFIG:
|
||||
@ -456,9 +812,9 @@ describe('type check blocks', () => {
|
||||
it('should check types of DOM events when enabled', () => {
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain(
|
||||
'_outputHelper(_t2["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
|
||||
'_outputHelper(_t1["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
|
||||
expect(block).toContain(
|
||||
'_t1.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
|
||||
'_t2.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
|
||||
});
|
||||
it('should not check types of DOM events when disabled', () => {
|
||||
const DISABLED_CONFIG: TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfDomEvents: false};
|
||||
@ -466,7 +822,7 @@ describe('type check blocks', () => {
|
||||
// Note that directive outputs are still checked, that is controlled by
|
||||
// `checkTypeOfOutputEvents`
|
||||
expect(block).toContain(
|
||||
'_outputHelper(_t2["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
|
||||
'_outputHelper(_t1["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
|
||||
expect(block).toContain('function ($event: any): any { (ctx).foo($event); }');
|
||||
});
|
||||
});
|
||||
@ -502,7 +858,7 @@ describe('type check blocks', () => {
|
||||
|
||||
it('should trace references to a directive when enabled', () => {
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain('(_t2).value');
|
||||
expect(block).toContain('(_t1).value');
|
||||
});
|
||||
|
||||
it('should trace references to an <ng-template> when enabled', () => {
|
||||
@ -529,17 +885,17 @@ describe('type check blocks', () => {
|
||||
|
||||
it('should assign string value to the input when enabled', () => {
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain('"disabled": ("")');
|
||||
expect(block).toContain('"cols": ("3")');
|
||||
expect(block).toContain('"rows": (2)');
|
||||
expect(block).toContain('_t1.disabled = ("");');
|
||||
expect(block).toContain('_t1.cols = ("3");');
|
||||
expect(block).toContain('_t1.rows = (2);');
|
||||
});
|
||||
|
||||
it('should use any for attributes but still check bound attributes when disabled', () => {
|
||||
const DISABLED_CONFIG: TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfAttributes: false};
|
||||
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
|
||||
expect(block).toContain('"disabled": (null as any)');
|
||||
expect(block).toContain('"cols": (null as any)');
|
||||
expect(block).toContain('"rows": (2)');
|
||||
expect(block).not.toContain('"disabled"');
|
||||
expect(block).not.toContain('"cols"');
|
||||
expect(block).toContain('_t1.rows = (2);');
|
||||
});
|
||||
});
|
||||
|
||||
@ -609,5 +965,47 @@ describe('type check blocks', () => {
|
||||
expect(block).toContain('function Test_TCB(ctx: Test<any>)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('config.checkAccessModifiersForInputBindings', () => {
|
||||
const TEMPLATE = `<div dir [inputA]="foo"></div>`;
|
||||
|
||||
it('should assign restricted properties via element access for field names that are not JS identifiers',
|
||||
() => {
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {
|
||||
'some-input.xs': 'inputA',
|
||||
},
|
||||
restrictedInputFields: ['some-input.xs'],
|
||||
stringLiteralInputFields: ['some-input.xs'],
|
||||
}];
|
||||
const enableChecks:
|
||||
TypeCheckingConfig = {...BASE_CONFIG, honorAccessModifiersForInputBindings: true};
|
||||
const block = tcb(TEMPLATE, DIRECTIVES, enableChecks);
|
||||
expect(block).toContain(
|
||||
'var _t1: Dir = (null!); ' +
|
||||
'_t1["some-input.xs"] = (((ctx).foo)); ');
|
||||
});
|
||||
|
||||
it('should assign restricted properties via property access', () => {
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {
|
||||
fieldA: 'inputA',
|
||||
},
|
||||
restrictedInputFields: ['fieldA']
|
||||
}];
|
||||
const enableChecks:
|
||||
TypeCheckingConfig = {...BASE_CONFIG, honorAccessModifiersForInputBindings: true};
|
||||
const block = tcb(TEMPLATE, DIRECTIVES, enableChecks);
|
||||
expect(block).toContain(
|
||||
'var _t1: Dir = (null!); ' +
|
||||
'_t1.fieldA = (((ctx).foo)); ');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user