Compare commits
59 Commits
Author | SHA1 | Date | |
---|---|---|---|
ca7ee794bf | |||
f9f2ba6faf | |||
aea1d211d4 | |||
57a518a36d | |||
29b83189b0 | |||
1d3df7885d | |||
fd06ffa2af | |||
36a1622dd1 | |||
7a91b23cb5 | |||
4b90b6a226 | |||
b13daa4cdf | |||
0c6f026828 | |||
a2520bd267 | |||
b928a209a4 | |||
89e16ed6a5 | |||
1a1f99af37 | |||
df2cd37ed2 | |||
12a71bc6bc | |||
7d270c235a | |||
b0b7248504 | |||
78460c1848 | |||
75b119eafc | |||
64b0ae93f7 | |||
7c0b25f5a6 | |||
07b5df3a19 | |||
e7023726f4 | |||
a9ccd9254c | |||
335f3271d2 | |||
7f93f7ef47 | |||
cf46a87fcd | |||
ad6680f602 | |||
5e287f67af | |||
ecfe6e0609 | |||
df9790dd11 | |||
67cfc4c9bc | |||
a68e623c80 | |||
9e3915ba48 | |||
ba2de61748 | |||
a9a4edebe2 | |||
64f2ffa166 | |||
13020b9cc2 | |||
96b96fba0f | |||
2cbe53a9ba | |||
48755114e5 | |||
a5d5f67be7 | |||
dfb58c44a2 | |||
69948ce919 | |||
3190ccf3b2 | |||
a8ea8173aa | |||
e13a49d1f0 | |||
2f0b8f675a | |||
c2aed033ba | |||
0f8a780b0d | |||
c5bc2e77c8 | |||
079310dc7c | |||
0d2cdf6165 | |||
436dde271f | |||
96891a076f | |||
9ce0067bdf |
@ -4,6 +4,7 @@ import {MergeConfig} from '../dev-infra/pr/merge/config';
|
||||
const commitMessage = {
|
||||
'maxLength': 120,
|
||||
'minBodyLength': 100,
|
||||
'minBodyLengthExcludes': ['docs'],
|
||||
'types': [
|
||||
'build',
|
||||
'ci',
|
||||
@ -56,8 +57,6 @@ const format = {
|
||||
// TODO: burn down format failures and remove aio and integration exceptions.
|
||||
'!aio/**',
|
||||
'!integration/**',
|
||||
// TODO: remove this exclusion as part of IE deprecation.
|
||||
'!shims_for_IE.js',
|
||||
// Both third_party and .yarn are directories containing copied code which should
|
||||
// not be modified.
|
||||
'!third_party/**',
|
||||
|
@ -1036,7 +1036,7 @@ groups:
|
||||
conditions:
|
||||
- *can-be-global-approved
|
||||
- >
|
||||
contains_any_globs(files.exclude("CHANGELOG.md"), [
|
||||
contains_any_globs(files.exclude("CHANGELOG.md").exclude("packages/compiler-cli/**/BUILD.bazel"), [
|
||||
'*',
|
||||
'.circleci/**',
|
||||
'.devcontainer/**',
|
||||
|
@ -24,7 +24,7 @@ filegroup(
|
||||
"//packages/zone.js/dist:zone-testing.js",
|
||||
"//packages/zone.js/dist:task-tracking.js",
|
||||
"//:test-events.js",
|
||||
"//:shims_for_IE.js",
|
||||
"//:third_party/shims_for_IE.js",
|
||||
# Including systemjs because it defines `__eval`, which produces correct stack traces.
|
||||
"@npm//:node_modules/systemjs/dist/system.src.js",
|
||||
"@npm//:node_modules/reflect-metadata/Reflect.js",
|
||||
|
896
CHANGELOG.md
896
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -6,5 +6,5 @@ import { Component } from '@angular/core';
|
||||
templateUrl: './app.component.html'
|
||||
})
|
||||
export class AppComponent {
|
||||
birthday = new Date(1988, 3, 15); // April 15, 1988
|
||||
birthday = new Date(1988, 3, 15); // April 15, 1988 -- since month parameter is zero-based
|
||||
}
|
||||
|
@ -8,5 +8,5 @@ import { Component } from '@angular/core';
|
||||
// #enddocregion hero-birthday-template
|
||||
})
|
||||
export class HeroBirthdayComponent {
|
||||
birthday = new Date(1988, 3, 15); // April 15, 1988
|
||||
birthday = new Date(1988, 3, 15); // April 15, 1988 -- since month parameter is zero-based
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import { Component } from '@angular/core';
|
||||
})
|
||||
// #docregion class
|
||||
export class HeroBirthday2Component {
|
||||
birthday = new Date(1988, 3, 15); // April 15, 1988
|
||||
birthday = new Date(1988, 3, 15); // April 15, 1988 -- since month parameter is zero-based
|
||||
toggle = true; // start with true == shortDate
|
||||
|
||||
get format() { return this.toggle ? 'shortDate' : 'fullDate'; }
|
||||
|
@ -33,7 +33,7 @@ export class HeroesComponent implements OnInit {
|
||||
|
||||
onSelect(hero: Hero): void {
|
||||
this.selectedHero = hero;
|
||||
this.messageService.add(`HeroService: Selected hero id=${hero.id}`);
|
||||
this.messageService.add(`HeroesComponent: Selected hero id=${hero.id}`);
|
||||
}
|
||||
|
||||
// #docregion getHeroes
|
||||
|
@ -197,11 +197,11 @@ Like `EvenBetterLogger`, `HeroService` needs to know if the user is authorized
|
||||
That authorization can change during the course of a single application session,
|
||||
as when you log in a different user.
|
||||
|
||||
Let's say you don't want to inject `UserService` directly into `HeroService`, because you don't want to complicate that service with security-sensitive information.
|
||||
Imagine that you don't want to inject `UserService` directly into `HeroService`, because you don't want to complicate that service with security-sensitive information.
|
||||
`HeroService` won't have direct access to the user information to decide
|
||||
who is authorized and who isn't.
|
||||
|
||||
To resolve this, we give the `HeroService` constructor a boolean flag to control display of secret heroes.
|
||||
To resolve this, give the `HeroService` constructor a boolean flag to control display of secret heroes.
|
||||
|
||||
<code-example path="dependency-injection/src/app/heroes/hero.service.ts" region="internals" header="src/app/heroes/hero.service.ts (excerpt)"></code-example>
|
||||
|
||||
|
@ -119,7 +119,7 @@ The recently-developed [custom elements](https://developer.mozilla.org/en-US/doc
|
||||
|
||||
In browsers that support Custom Elements natively, the specification requires developers use ES2015 classes to define Custom Elements - developers can opt-in to this by setting the `target: "es2015"` property in their project's [TypeScript configuration file](/guide/typescript-configuration). As Custom Element and ES2015 support may not be available in all browsers, developers can instead choose to use a polyfill to support older browsers and ES5 code.
|
||||
|
||||
Use the [Angular CLI](cli) to automatically set up your project with the correct polyfill: `ng add @angular/elements --name=*your_project_name*`.
|
||||
Use the [Angular CLI](cli) to automatically set up your project with the correct polyfill: `ng add @angular/elements --project=*your_project_name*`.
|
||||
- For more information about polyfills, see [polyfill documentation](https://www.webcomponents.org/polyfills).
|
||||
|
||||
- For more information about Angular browser support, see [Browser Support](guide/browser-support).
|
||||
|
@ -495,7 +495,7 @@ for one turn of the browser's JavaScript cycle, which triggers a new change-dete
|
||||
|
||||
#### Write lean hook methods to avoid performance problems
|
||||
|
||||
When you run the *AfterView* sample, notice how frequently Angular calls `AfterViewChecked()`$emdash;often when there are no changes of interest.
|
||||
When you run the *AfterView* sample, notice how frequently Angular calls `AfterViewChecked()`-often when there are no changes of interest.
|
||||
Be very careful about how much logic or computation you put into one of these methods.
|
||||
|
||||
<div class="lightbox">
|
||||
|
@ -112,7 +112,7 @@ Because observables produce values asynchronously, try/catch will not effectivel
|
||||
<code-example>
|
||||
myObservable.subscribe({
|
||||
next(num) { console.log('Next num: ' + num)},
|
||||
error(err) { console.log('Received an errror: ' + err)}
|
||||
error(err) { console.log('Received an error: ' + err)}
|
||||
});
|
||||
</code-example>
|
||||
|
||||
|
@ -101,6 +101,7 @@ The following table provides the status for Angular versions under support.
|
||||
|
||||
Version | Status | Released | Active Ends | LTS Ends
|
||||
------- | ------ | ------------ | ------------ | ------------
|
||||
^10.0.0 | Active | Jun 24, 2020 | Dec 24, 2020 | Dec 24, 2021
|
||||
^9.0.0 | Active | Feb 06, 2020 | Aug 06, 2020 | Aug 06, 2021
|
||||
^8.0.0 | LTS | May 28, 2019 | Nov 28, 2019 | Nov 28, 2020
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Schematics for libraries
|
||||
|
||||
When you create an Angular library, you can provide and package it with schematics that integrate it with the Angular CLI.
|
||||
With your schematics, your users can use `ng add` to install an initial version of your library,
|
||||
With your schematics, your users can use `ng add` to install an initial version of your library,
|
||||
`ng generate` to create artifacts defined in your library, and `ng update` to adjust their project for a new version of your library that introduces breaking changes.
|
||||
|
||||
All three types of schematics can be part of a collection that you package with your library.
|
||||
@ -115,10 +115,10 @@ When you add a schematic to the collection, you have to point to it in the colle
|
||||
<code-example header="projects/my-lib/schematics/my-service/schema.json (Schematic JSON Schema)" path="schematics-for-libraries/projects/my-lib/schematics/my-service/schema.json">
|
||||
</code-example>
|
||||
|
||||
* *id* : A unique id for the schema in the collection.
|
||||
* *title* : A human-readable description of the schema.
|
||||
* *type* : A descriptor for the type provided by the properties.
|
||||
* *properties* : An object that defines the available options for the schematic.
|
||||
* *id*: A unique id for the schema in the collection.
|
||||
* *title*: A human-readable description of the schema.
|
||||
* *type*: A descriptor for the type provided by the properties.
|
||||
* *properties*: An object that defines the available options for the schematic.
|
||||
|
||||
Each option associates key with a type, description, and optional alias.
|
||||
The type defines the shape of the value you expect, and the description is displayed when the user requests usage help for your schematic.
|
||||
@ -130,9 +130,9 @@ When you add a schematic to the collection, you have to point to it in the colle
|
||||
<code-example header="projects/my-lib/schematics/my-service/schema.ts (Schematic Interface)" path="schematics-for-libraries/projects/my-lib/schematics/my-service/schema.ts">
|
||||
</code-example>
|
||||
|
||||
* *name* : The name you want to provide for the created service.
|
||||
* *path* : Overrides the path provided to the schematic. The default path value is based on the current working directory.
|
||||
* *project* : Provides a specific project to run the schematic on. In the schematic, you can provide a default if the option is not provided by the user.
|
||||
* *name*: The name you want to provide for the created service.
|
||||
* *path*: Overrides the path provided to the schematic. The default path value is based on the current working directory.
|
||||
* *project*: Provides a specific project to run the schematic on. In the schematic, you can provide a default if the option is not provided by the user.
|
||||
|
||||
### Add template files
|
||||
|
||||
@ -169,10 +169,9 @@ The Schematics framework provides a file templating system, which supports both
|
||||
The system operates on placeholders defined inside files or paths that loaded in the input `Tree`.
|
||||
It fills these in using values passed into the `Rule`.
|
||||
|
||||
For details of these data structure and syntax, see the [Schematics README](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/schematics/README.md).
|
||||
For details of these data structures and syntax, see the [Schematics README](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/schematics/README.md).
|
||||
|
||||
|
||||
1. Create the main file, `index.ts` and add the source code for your schematic factory function.
|
||||
1. Create the main file `index.ts` and add the source code for your schematic factory function.
|
||||
|
||||
1. First, import the schematics definitions you will need. The Schematics framework offers many utility functions to create and use rules when running a schematic.
|
||||
|
||||
@ -271,7 +270,6 @@ For more information about rules and utility methods, see [Provided Rules](https
|
||||
|
||||
After you build your library and schematics, you can install the schematics collection to run against your project. The steps below show you how to generate a service using the schematic you created above.
|
||||
|
||||
|
||||
### Build your library and schematics
|
||||
|
||||
From the root of your workspace, run the `ng build` command for your library.
|
||||
|
BIN
aio/content/images/bios/ahasall.jpg
Normal file
BIN
aio/content/images/bios/ahasall.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
BIN
aio/content/images/bios/sonukapoor.jpg
Normal file
BIN
aio/content/images/bios/sonukapoor.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
@ -596,6 +596,13 @@
|
||||
"twitter": "devjoost",
|
||||
"bio": "Joost is a Software Engineer from the Netherlands with an interest in open source software who likes to learn something new every day. He works at Blueriq during the day and contributes to Angular in his spare time, by working on the Angular compiler and runtime. He may review your PR even if you never asked for it ;)"
|
||||
},
|
||||
"sonukapoor": {
|
||||
"name": "Sonu Kapoor",
|
||||
"groups": ["Collaborators"],
|
||||
"picture": "sonukapoor.jpg",
|
||||
"website": "https://www.linkedin.com/in/sonu-kapoor/",
|
||||
"bio": "Sonu is a Software Engineer from Toronto, with a high interest in front-end technologies and algorithms."
|
||||
},
|
||||
"jschwarty": {
|
||||
"name": "Justin Schwartzenberger",
|
||||
"picture": "justinschwartzenberger.jpg",
|
||||
@ -815,5 +822,13 @@
|
||||
"website": "https://wellwind.idv.tw/blog/",
|
||||
"bio": "Mike is a full-stack developer, consultant, blogger, instructor, and conference speaker. He has over 10 years of web development experience and passion to share his knowledge.",
|
||||
"groups": ["GDE"]
|
||||
},
|
||||
"ahasall": {
|
||||
"name": "Amadou Sall",
|
||||
"picture": "ahasall.jpg",
|
||||
"groups": ["GDE"],
|
||||
"twitter": "ahasall",
|
||||
"website": "https://www.amadousall.com",
|
||||
"bio": "Amadou is a Frontend Software Engineer from Senegal based in France. He currently works at Air France where he helps developers build better Angular applications. Passionate about web technologies, Amadou is an international speaker, a technical writer, and a Google Developer Expert in Angular."
|
||||
}
|
||||
}
|
||||
|
@ -506,80 +506,6 @@
|
||||
"url": "guide/universal",
|
||||
"title": "Server-side Rendering",
|
||||
"tooltip": "Render HTML server-side with Angular Universal."
|
||||
},
|
||||
{
|
||||
"title": "Upgrading from AngularJS",
|
||||
"tooltip": "Incrementally upgrade an AngularJS application to Angular.",
|
||||
"children": [
|
||||
{
|
||||
"url": "guide/upgrade-setup",
|
||||
"title": "Setup for Upgrading from AngularJS",
|
||||
"tooltip": "Use code from the Angular QuickStart seed as part of upgrading from AngularJS.",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"url": "guide/upgrade",
|
||||
"title": "Upgrading Instructions",
|
||||
"tooltip": "Incrementally upgrade an AngularJS application to Angular."
|
||||
},
|
||||
{
|
||||
"url": "guide/upgrade-performance",
|
||||
"title": "Upgrading for Performance",
|
||||
"tooltip": "Upgrade from AngularJS to Angular in a more flexible way."
|
||||
},
|
||||
{
|
||||
"url": "guide/ajs-quick-reference",
|
||||
"title": "AngularJS-Angular Concepts",
|
||||
"tooltip": "Learn how AngularJS concepts and techniques map to Angular."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Angular Libraries",
|
||||
"tooltip": "Extending Angular with shared libraries.",
|
||||
"children": [
|
||||
{
|
||||
"url": "guide/libraries",
|
||||
"title": "Libraries Overview",
|
||||
"tooltip": "Understand how and when to use or create libraries."
|
||||
},
|
||||
{
|
||||
"url": "guide/using-libraries",
|
||||
"title": "Using Published Libraries",
|
||||
"tooltip": "Integrate published libraries into an app."
|
||||
},
|
||||
{
|
||||
"url": "guide/creating-libraries",
|
||||
"title": "Creating Libraries",
|
||||
"tooltip": "Extend Angular by creating, publishing, and using your own libraries."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Schematics",
|
||||
"tooltip": "Using CLI schematics for code generation.",
|
||||
"children": [
|
||||
{
|
||||
"url": "guide/schematics",
|
||||
"title": "Schematics Overview",
|
||||
"tooltip": "How the CLI uses schematics to generate code."
|
||||
},
|
||||
{
|
||||
"url": "guide/schematics-authoring",
|
||||
"title": "Authoring Schematics",
|
||||
"tooltip": "Understand the structure of a schematic."
|
||||
},
|
||||
{
|
||||
"url": "guide/schematics-for-libraries",
|
||||
"title": "Schematics for Libraries",
|
||||
"tooltip": "Use schematics to integrate your library with the Angular CLI."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "guide/cli-builder",
|
||||
"title": "CLI Builders",
|
||||
"tooltip": "Using builders to customize Angular CLI."
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -1008,6 +934,10 @@
|
||||
}
|
||||
],
|
||||
"docVersions": [
|
||||
{
|
||||
"title": "v9",
|
||||
"url": "https://v9.angular.io/"
|
||||
},
|
||||
{
|
||||
"title": "v8",
|
||||
"url": "https://v8.angular.io/"
|
||||
|
@ -6,7 +6,7 @@
|
||||
In this tutorial, you build your own app from the ground up, providing experience with the typical development process, as well as an introduction to basic app-design concepts, tools, and terminology.
|
||||
|
||||
If you're completely new to Angular, you might want to try the [**Try it now**](start) quick-start app first.
|
||||
It is based on a ready-made partially-completed project, which you can examine and modify in the StacBlitz interactive development environment, where you can see the results in real time.
|
||||
It is based on a ready-made partially-completed project, which you can examine and modify in the StackBlitz interactive development environment, where you can see the results in real time.
|
||||
|
||||
The "Try it" tutorial covers the same major topics—components, template syntax, routing, services, and accessing data via HTTP—in a condensed format, following the most current best practices.
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"hosting": {
|
||||
"target": "aio",
|
||||
"public": "dist",
|
||||
"cleanUrls": true,
|
||||
"redirects": [
|
||||
@ -127,7 +128,7 @@
|
||||
// The below paths are referenced in users projects generated by the CLI
|
||||
{"type": 301, "source": "/config/tsconfig", "destination": "/guide/typescript-configuration"},
|
||||
{"type": 301, "source": "/config/solution-tsconfig", "destination": "https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/#solution-style-tsconfig"},
|
||||
{"type": 301, "source": "/config/app-package-json", "destination": "https://webpack.js.org/configuration/optimization/#optimizationsideeffects"}
|
||||
{"type": 301, "source": "/config/app-package-json", "destination": "/guide/strict-mode#non-local-side-effects-in-applications"}
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
|
@ -123,7 +123,7 @@
|
||||
"cross-spawn": "^5.1.0",
|
||||
"css-selector-parser": "^1.3.0",
|
||||
"dgeni": "^0.4.11",
|
||||
"dgeni-packages": "^0.28.3",
|
||||
"dgeni-packages": "^0.28.4",
|
||||
"entities": "^1.1.1",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-plugin-jasmine": "^2.2.0",
|
||||
@ -175,4 +175,4 @@
|
||||
"xregexp": "^4.0.0",
|
||||
"yargs": "^7.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ else
|
||||
readonly majorVersionStable=${CI_STABLE_BRANCH%%.*}
|
||||
|
||||
# Do not deploy if the major version is not less than the stable branch major version
|
||||
if [[ !( "$majorVersion" -lt "$majorVersionStable" ) ]]; then
|
||||
if (( $majorVersion >= $majorVersionStable )); then
|
||||
echo "Skipping deploy of branch \"$CI_BRANCH\" to firebase."
|
||||
echo "We only deploy archive branches with the major version less than the stable branch: \"$CI_STABLE_BRANCH\""
|
||||
exit 0
|
||||
@ -64,16 +64,27 @@ fi
|
||||
case $deployEnv in
|
||||
next)
|
||||
readonly projectId=aio-staging
|
||||
readonly siteId=$projectId
|
||||
readonly deployedUrl=https://next.angular.io/
|
||||
readonly firebaseToken=$CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN
|
||||
;;
|
||||
stable)
|
||||
readonly projectId=angular-io
|
||||
readonly siteId=$projectId
|
||||
readonly deployedUrl=https://angular.io/
|
||||
readonly firebaseToken=$CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN
|
||||
;;
|
||||
archive)
|
||||
readonly projectId=v${majorVersion}-angular-io
|
||||
# Special case v9-angular-io because its piloting the firebase hosting "multisites" setup
|
||||
# See https://angular-team.atlassian.net/browse/DEV-125 for more info.
|
||||
if [[ "$majorVersion" == "9" ]]; then
|
||||
readonly projectId=aio-staging
|
||||
readonly siteId=v9-angular-io
|
||||
else
|
||||
readonly projectId=v${majorVersion}-angular-io
|
||||
readonly siteId=$projectId
|
||||
fi
|
||||
|
||||
readonly deployedUrl=https://v${majorVersion}.angular.io/
|
||||
readonly firebaseToken=$CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN
|
||||
;;
|
||||
@ -82,6 +93,7 @@ esac
|
||||
echo "Git branch : $CI_BRANCH"
|
||||
echo "Build/deploy mode : $deployEnv"
|
||||
echo "Firebase project : $projectId"
|
||||
echo "Firebase site : $siteId"
|
||||
echo "Deployment URL : $deployedUrl"
|
||||
|
||||
if [[ ${1:-} == "--dry-run" ]]; then
|
||||
@ -92,23 +104,29 @@ fi
|
||||
(
|
||||
cd "`dirname $0`/.."
|
||||
|
||||
# Build the app
|
||||
echo "\n\n\n==== Build the aio app ====\n"
|
||||
yarn build --configuration=$deployEnv --progress=false
|
||||
|
||||
# Include any mode-specific files
|
||||
|
||||
echo "\n\n\n==== Add any mode-specific files into the aio distribution ====\n"
|
||||
cp -rf src/extra-files/$deployEnv/. dist/
|
||||
|
||||
# Set deployedUrl as parameter in the opensearch description
|
||||
|
||||
echo "\n\n\n==== Update opensearch descriptor for aio with the deployedUrl ====\n"
|
||||
# deployedUrl must end with /
|
||||
yarn set-opensearch-url $deployedUrl
|
||||
|
||||
# Check payload size
|
||||
echo "\n\n\n==== Check payload size and upload the numbers to firebase db ====\n"
|
||||
yarn payload-size
|
||||
|
||||
# Deploy to Firebase
|
||||
yarn firebase use "$projectId" --token "$firebaseToken"
|
||||
yarn firebase deploy --message "Commit: $CI_COMMIT" --non-interactive --token "$firebaseToken"
|
||||
|
||||
# Run PWA-score tests
|
||||
echo "\n\n\n==== Deploy aio to firebase hosting ====\n"
|
||||
|
||||
yarn firebase use "${projectId}" --token "$firebaseToken"
|
||||
yarn firebase target:apply hosting aio $siteId --token "$firebaseToken"
|
||||
yarn firebase deploy --only hosting:aio --message "Commit: $CI_COMMIT" --non-interactive --token "$firebaseToken"
|
||||
|
||||
|
||||
echo "\n\n\n==== Run PWA-score tests ====\n"
|
||||
yarn test-pwa-score "$deployedUrl" "$CI_AIO_MIN_PWA_SCORE"
|
||||
)
|
||||
|
@ -68,6 +68,7 @@ function check {
|
||||
expected="Git branch : master
|
||||
Build/deploy mode : next
|
||||
Firebase project : aio-staging
|
||||
Firebase site : aio-staging
|
||||
Deployment URL : https://next.angular.io/"
|
||||
check "$actual" "$expected"
|
||||
)
|
||||
@ -103,6 +104,7 @@ Deployment URL : https://next.angular.io/"
|
||||
expected="Git branch : 4.3.x
|
||||
Build/deploy mode : stable
|
||||
Firebase project : angular-io
|
||||
Firebase site : angular-io
|
||||
Deployment URL : https://angular.io/"
|
||||
check "$actual" "$expected"
|
||||
)
|
||||
@ -139,10 +141,37 @@ Deployment URL : https://angular.io/"
|
||||
expected="Git branch : 2.4.x
|
||||
Build/deploy mode : archive
|
||||
Firebase project : v2-angular-io
|
||||
Firebase site : v2-angular-io
|
||||
Deployment URL : https://v2.angular.io/"
|
||||
check "$actual" "$expected"
|
||||
)
|
||||
|
||||
(
|
||||
echo ===== archive - v9-angular-io multisite special case - deploy success
|
||||
actual=$(
|
||||
export BASH_ENV=/dev/null
|
||||
export CI_REPO_OWNER=angular
|
||||
export CI_REPO_NAME=angular
|
||||
export CI_PULL_REQUEST=false
|
||||
export CI_BRANCH=9.1.x
|
||||
export CI_STABLE_BRANCH=10.0.x
|
||||
export CI_COMMIT=$(git ls-remote origin 9.1.x | cut -c1-40)
|
||||
export CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN=XXXXX
|
||||
$deployToFirebaseDryRun
|
||||
)
|
||||
expected="Git branch : 9.1.x
|
||||
Build/deploy mode : archive
|
||||
Firebase project : aio-staging
|
||||
Firebase site : v9-angular-io
|
||||
Deployment URL : https://v9.angular.io/"
|
||||
# TODO: This test incorrectly expects the Firebase project to be v9-angular-io.
|
||||
# v9-angular-io is a "multisites" project currently within the aio-staging project
|
||||
# This setup is temporary and was created in order to deploy v9.angular.io without
|
||||
# disruptions.
|
||||
# See https://angular-team.atlassian.net/browse/DEV-125 for more info.
|
||||
check "$actual" "$expected"
|
||||
)
|
||||
|
||||
(
|
||||
echo ===== archive - skip deploy - commit not HEAD
|
||||
actual=$(
|
||||
|
52
aio/tests/e2e/src/api-list.e2e-spec.ts
Normal file
52
aio/tests/e2e/src/api-list.e2e-spec.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { by, element } from 'protractor';
|
||||
import { SitePage } from './app.po';
|
||||
|
||||
describe('api-list', () => {
|
||||
const apiSearchInput = element(by.css('aio-api-list .form-search input'));
|
||||
const apiStatusDropdown = element(by.css('aio-api-list aio-select[label="Status:"]'));
|
||||
const apiTypeDropdown = element(by.css('aio-api-list aio-select[label="Type:"]'));
|
||||
let page: SitePage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new SitePage();
|
||||
page.navigateTo('api');
|
||||
});
|
||||
|
||||
it('should find AnimationSequenceMetadata when searching by partial word anima', () => {
|
||||
expect(page.getApiSearchResults()).toContain('HttpEventType');
|
||||
|
||||
apiSearchInput.clear();
|
||||
apiSearchInput.sendKeys('anima');
|
||||
|
||||
expect(page.getApiSearchResults()).not.toContain('HttpEventType');
|
||||
expect(page.getApiSearchResults()).toContain('AnimationSequenceMetadata');
|
||||
});
|
||||
|
||||
it('should find getLocaleDateTimeFormat when searching by partial word date', () => {
|
||||
expect(page.getApiSearchResults()).toContain('formatCurrency');
|
||||
|
||||
apiSearchInput.clear();
|
||||
apiSearchInput.sendKeys('date');
|
||||
|
||||
expect(page.getApiSearchResults()).not.toContain('formatCurrency');
|
||||
expect(page.getApiSearchResults()).toContain('getLocaleDateTimeFormat');
|
||||
});
|
||||
|
||||
it('should find LowerCasePipe when searching for type pipe', () => {
|
||||
expect(page.getApiSearchResults()).toContain('getLocaleDateTimeFormat');
|
||||
|
||||
page.clickDropdownItem(apiTypeDropdown, 'Pipe');
|
||||
|
||||
expect(page.getApiSearchResults()).not.toContain('getLocaleDateTimeFormat');
|
||||
expect(page.getApiSearchResults()).toContain('LowerCasePipe');
|
||||
});
|
||||
|
||||
it('should find ElementRef when searching for status Security Risk', () => {
|
||||
expect(page.getApiSearchResults()).toContain('getLocaleDateTimeFormat');
|
||||
|
||||
page.clickDropdownItem(apiStatusDropdown, 'Security Risk');
|
||||
|
||||
expect(page.getApiSearchResults()).not.toContain('getLocaleDateTimeFormat');
|
||||
expect(page.getApiSearchResults()).toContain('ElementRef');
|
||||
});
|
||||
});
|
@ -83,4 +83,16 @@ export class SitePage {
|
||||
browser.wait(ExpectedConditions.presenceOf(results.first()), 8000);
|
||||
return results.map(link => link && link.getText());
|
||||
}
|
||||
|
||||
getApiSearchResults() {
|
||||
const results = element.all(by.css('aio-api-list .api-item'));
|
||||
browser.wait(ExpectedConditions.presenceOf(results.first()), 2000);
|
||||
return results.map(elem => elem && elem.getText());
|
||||
}
|
||||
|
||||
clickDropdownItem(dropdown: ElementFinder, itemName: string){
|
||||
dropdown.element(by.css('.form-select-button')).click();
|
||||
const menuItem = dropdown.element(by.cssContainingText('.form-select-dropdown li', itemName));
|
||||
menuItem.click();
|
||||
}
|
||||
}
|
||||
|
@ -4467,10 +4467,10 @@ dezalgo@^1.0.0:
|
||||
asap "^2.0.0"
|
||||
wrappy "1"
|
||||
|
||||
dgeni-packages@^0.28.3:
|
||||
version "0.28.3"
|
||||
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.28.3.tgz#2e1e55f341c389b67ebb28933ce1e7e9ad05c49b"
|
||||
integrity sha512-WyVzY3Q4ylfnc2677le5G7a7WqkF88rBSjU9IrAofqro71yzZeWLoEdr/gJY+lJZ0PrDyuRW05pFvIbvX8N0PQ==
|
||||
dgeni-packages@^0.28.4:
|
||||
version "0.28.4"
|
||||
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.28.4.tgz#53a3e6700b8d8f6be168cadcc9fdb36e1d7011d3"
|
||||
integrity sha512-7AUG3pKpWtn69c3v2Mzgh+i5gd+L0AxFfYGWGzBdlJqMlQfaQPQjaS54iYCvnOlK9rXBn9j39yO6EU70gDZuFw==
|
||||
dependencies:
|
||||
canonical-path "^1.0.0"
|
||||
catharsis "^0.8.1"
|
||||
|
@ -25,6 +25,7 @@ def component_benchmark(
|
||||
driver_deps,
|
||||
ng_srcs,
|
||||
ng_deps,
|
||||
ng_assets = [],
|
||||
assets = None,
|
||||
styles = None,
|
||||
entry_point = None,
|
||||
@ -65,6 +66,7 @@ def component_benchmark(
|
||||
driver_deps: Driver's dependencies
|
||||
ng_srcs: All of the ts srcs for the angular app
|
||||
ng_deps: Dependencies for the angular app
|
||||
ng_assets: The static assets for the angular app
|
||||
assets: Static files
|
||||
styles: Stylesheets
|
||||
entry_point: Main entry point for the angular app
|
||||
@ -104,6 +106,7 @@ def component_benchmark(
|
||||
ng_module(
|
||||
name = app_lib,
|
||||
srcs = ng_srcs,
|
||||
assets = ng_assets,
|
||||
# Creates ngFactory and ngSummary to be imported by the app's entry point.
|
||||
generate_ve_shims = True,
|
||||
deps = ng_deps,
|
||||
|
@ -11,6 +11,7 @@ import {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
|
||||
export interface CommitMessageConfig {
|
||||
maxLineLength: number;
|
||||
minBodyLength: number;
|
||||
minBodyLengthTypeExcludes?: string[];
|
||||
types: string[];
|
||||
scopes: string[];
|
||||
}
|
||||
@ -19,7 +20,7 @@ export interface CommitMessageConfig {
|
||||
export function getCommitMessageConfig() {
|
||||
// List of errors encountered validating the config.
|
||||
const errors: string[] = [];
|
||||
// The unvalidated config object.
|
||||
// The non-validated config object.
|
||||
const config: Partial<NgDevConfig<{commitMessage: CommitMessageConfig}>> = getConfig();
|
||||
|
||||
if (config.commitMessage === undefined) {
|
||||
|
@ -10,19 +10,22 @@
|
||||
import * as validateConfig from './config';
|
||||
import {validateCommitMessage} from './validate';
|
||||
|
||||
type CommitMessageConfig = validateConfig.CommitMessageConfig;
|
||||
|
||||
|
||||
// Constants
|
||||
const config = {
|
||||
'commitMessage': {
|
||||
'maxLineLength': 120,
|
||||
'minBodyLength': 0,
|
||||
'types': [
|
||||
const config: {commitMessage: CommitMessageConfig} = {
|
||||
commitMessage: {
|
||||
maxLineLength: 120,
|
||||
minBodyLength: 0,
|
||||
types: [
|
||||
'feat',
|
||||
'fix',
|
||||
'refactor',
|
||||
'release',
|
||||
'style',
|
||||
],
|
||||
'scopes': [
|
||||
scopes: [
|
||||
'common',
|
||||
'compiler',
|
||||
'core',
|
||||
@ -224,5 +227,42 @@ describe('validate-commit-message.js', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('minBodyLength', () => {
|
||||
const minBodyLengthConfig: {commitMessage: CommitMessageConfig} = {
|
||||
commitMessage: {
|
||||
maxLineLength: 120,
|
||||
minBodyLength: 30,
|
||||
minBodyLengthTypeExcludes: ['docs'],
|
||||
types: ['fix', 'docs'],
|
||||
scopes: ['core']
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(validateConfig.getCommitMessageConfig as jasmine.Spy).and.returnValue(minBodyLengthConfig);
|
||||
});
|
||||
|
||||
it('should fail validation if the body is shorter than `minBodyLength`', () => {
|
||||
expect(validateCommitMessage(
|
||||
'fix(core): something\n\n Explanation of the motivation behind this change'))
|
||||
.toBe(VALID);
|
||||
expect(validateCommitMessage('fix(core): something\n\n too short')).toBe(INVALID);
|
||||
expect(lastError).toContain(
|
||||
'The commit message body does not meet the minimum length of 30 characters');
|
||||
expect(validateCommitMessage('fix(core): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(
|
||||
'The commit message body does not meet the minimum length of 30 characters');
|
||||
});
|
||||
|
||||
it('should pass validation if the body is shorter than `minBodyLength` but the commit type is in the `minBodyLengthTypeExclusions` list',
|
||||
() => {
|
||||
expect(validateCommitMessage('docs: just fixing a typo')).toBe(VALID);
|
||||
expect(validateCommitMessage('docs(core): just fixing a typo')).toBe(VALID);
|
||||
expect(validateCommitMessage(
|
||||
'docs(core): just fixing a typo\n\nThis was just a silly typo.'))
|
||||
.toBe(VALID);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -148,7 +148,8 @@ export function validateCommitMessage(
|
||||
// Checking commit body //
|
||||
//////////////////////////
|
||||
|
||||
if (commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
|
||||
if (!config.minBodyLengthTypeExcludes?.includes(commit.type) &&
|
||||
commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
|
||||
printError(`The commit message body does not meet the minimum length of ${
|
||||
config.minBodyLength} characters`);
|
||||
return false;
|
||||
@ -157,7 +158,7 @@ export function validateCommitMessage(
|
||||
const bodyByLine = commit.body.split('\n');
|
||||
if (bodyByLine.some(line => line.length > config.maxLineLength)) {
|
||||
printError(
|
||||
`The commit messsage body contains lines greater than ${config.maxLineLength} characters`);
|
||||
`The commit message body contains lines greater than ${config.maxLineLength} characters`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -63,8 +63,8 @@ export async function discoverNewConflictsForPr(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/** The active github branch when the run began. */
|
||||
const originalBranch = git.getCurrentBranch();
|
||||
/** The active github branch or revision before we performed any Git commands. */
|
||||
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
|
||||
/* Progress bar to indicate progress. */
|
||||
const progressBar = new Bar({format: `[{bar}] ETA: {eta}s | {value}/{total}`});
|
||||
/* PRs which were found to be conflicting. */
|
||||
@ -103,7 +103,7 @@ export async function discoverNewConflictsForPr(
|
||||
const result = exec(`git rebase FETCH_HEAD`);
|
||||
if (result.code) {
|
||||
error('The requested PR currently has conflicts');
|
||||
cleanUpGitState(originalBranch);
|
||||
cleanUpGitState(previousBranchOrRevision);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@ -130,7 +130,7 @@ export async function discoverNewConflictsForPr(
|
||||
info();
|
||||
info(`Result:`);
|
||||
|
||||
cleanUpGitState(originalBranch);
|
||||
cleanUpGitState(previousBranchOrRevision);
|
||||
|
||||
// If no conflicts are found, exit successfully.
|
||||
if (conflicts.length === 0) {
|
||||
@ -147,14 +147,14 @@ export async function discoverNewConflictsForPr(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/** Reset git back to the provided branch. */
|
||||
export function cleanUpGitState(branch: string) {
|
||||
/** Reset git back to the provided branch or revision. */
|
||||
export function cleanUpGitState(previousBranchOrRevision: string) {
|
||||
// Ensure that any outstanding rebases are aborted.
|
||||
exec(`git rebase --abort`);
|
||||
// Ensure that any changes in the current repo state are cleared.
|
||||
exec(`git reset --hard`);
|
||||
// Checkout the original branch from before the run began.
|
||||
exec(`git checkout ${branch}`);
|
||||
exec(`git checkout ${previousBranchOrRevision}`);
|
||||
// Delete the generated branch.
|
||||
exec(`git branch -D ${tempWorkingBranch}`);
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ export class AutosquashMergeStrategy extends MergeStrategy {
|
||||
// is desired, we set the `GIT_SEQUENCE_EDITOR` environment variable to `true` so that
|
||||
// the rebase seems interactive to Git, while it's not interactive to the user.
|
||||
// See: https://github.com/git/git/commit/891d4a0313edc03f7e2ecb96edec5d30dc182294.
|
||||
const branchBeforeRebase = this.git.getCurrentBranch();
|
||||
const branchOrRevisionBeforeRebase = this.git.getCurrentBranchOrRevision();
|
||||
const rebaseEnv =
|
||||
needsCommitMessageFixup ? undefined : {...process.env, GIT_SEQUENCE_EDITOR: 'true'};
|
||||
this.git.run(
|
||||
@ -69,9 +69,9 @@ export class AutosquashMergeStrategy extends MergeStrategy {
|
||||
// Update pull requests commits to reference the pull request. This matches what
|
||||
// Github does when pull requests are merged through the Web UI. The motivation is
|
||||
// that it should be easy to determine which pull request contained a given commit.
|
||||
// **Note**: The filter-branch command relies on the working tree, so we want to make
|
||||
// sure that we are on the initial branch where the merge script has been run.
|
||||
this.git.run(['checkout', '-f', branchBeforeRebase]);
|
||||
// Note: The filter-branch command relies on the working tree, so we want to make sure
|
||||
// that we are on the initial branch or revision where the merge script has been invoked.
|
||||
this.git.run(['checkout', '-f', branchOrRevisionBeforeRebase]);
|
||||
this.git.run(
|
||||
['filter-branch', '-f', '--msg-filter', `${MSG_FILTER_SCRIPT} ${prNumber}`, revisionRange]);
|
||||
|
||||
|
@ -76,14 +76,14 @@ export class PullRequestMergeTask {
|
||||
new GithubApiMergeStrategy(this.git, this.config.githubApiMerge) :
|
||||
new AutosquashMergeStrategy(this.git);
|
||||
|
||||
// Branch that is currently checked out so that we can switch back to it once
|
||||
// the pull request has been merged.
|
||||
let previousBranch: null|string = null;
|
||||
// Branch or revision that is currently checked out so that we can switch back to
|
||||
// it once the pull request has been merged.
|
||||
let previousBranchOrRevision: null|string = null;
|
||||
|
||||
// The following block runs Git commands as child processes. These Git commands can fail.
|
||||
// We want to capture these command errors and return an appropriate merge request status.
|
||||
try {
|
||||
previousBranch = this.git.getCurrentBranch();
|
||||
previousBranchOrRevision = this.git.getCurrentBranchOrRevision();
|
||||
|
||||
// Run preparations for the merge (e.g. fetching branches).
|
||||
await strategy.prepare(pullRequest);
|
||||
@ -96,7 +96,7 @@ export class PullRequestMergeTask {
|
||||
|
||||
// Switch back to the previous branch. We need to do this before deleting the temporary
|
||||
// branches because we cannot delete branches which are currently checked out.
|
||||
this.git.run(['checkout', '-f', previousBranch]);
|
||||
this.git.run(['checkout', '-f', previousBranchOrRevision]);
|
||||
|
||||
await strategy.cleanup(pullRequest);
|
||||
|
||||
@ -112,8 +112,8 @@ export class PullRequestMergeTask {
|
||||
} finally {
|
||||
// Always try to restore the branch if possible. We don't want to leave
|
||||
// the repository in a different state than before.
|
||||
if (previousBranch !== null) {
|
||||
this.git.runGraceful(['checkout', '-f', previousBranch]);
|
||||
if (previousBranchOrRevision !== null) {
|
||||
this.git.runGraceful(['checkout', '-f', previousBranchOrRevision]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,10 +50,10 @@ export async function rebasePr(
|
||||
}
|
||||
|
||||
/**
|
||||
* The branch originally checked out before this method performs any Git
|
||||
* operations that may change the working branch.
|
||||
* The branch or revision originally checked out before this method performed
|
||||
* any Git operations that may change the working branch.
|
||||
*/
|
||||
const originalBranch = git.getCurrentBranch();
|
||||
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
|
||||
/* Get the PR information from Github. */
|
||||
const pr = await getPr(PR_SCHEMA, prNumber, config.github);
|
||||
|
||||
@ -121,7 +121,7 @@ export async function rebasePr(
|
||||
info();
|
||||
info(`To abort the rebase and return to the state of the repository before this command`);
|
||||
info(`run the following command:`);
|
||||
info(` $ git rebase --abort && git reset --hard && git checkout ${originalBranch}`);
|
||||
info(` $ git rebase --abort && git reset --hard && git checkout ${previousBranchOrRevision}`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
info(`Cleaning up git state, and restoring previous state.`);
|
||||
@ -137,7 +137,7 @@ export async function rebasePr(
|
||||
// Ensure that any changes in the current repo state are cleared.
|
||||
git.runGraceful(['reset', '--hard'], {stdio: 'ignore'});
|
||||
// Checkout the original branch from before the run began.
|
||||
git.runGraceful(['checkout', originalBranch], {stdio: 'ignore'});
|
||||
git.runGraceful(['checkout', previousBranchOrRevision], {stdio: 'ignore'});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,9 +130,16 @@ export class GitClient {
|
||||
return this.run(['branch', branchName, '--contains', sha]).stdout !== '';
|
||||
}
|
||||
|
||||
/** Gets the currently checked out branch. */
|
||||
getCurrentBranch(): string {
|
||||
return this.run(['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
|
||||
/** Gets the currently checked out branch or revision. */
|
||||
getCurrentBranchOrRevision(): string {
|
||||
const branchName = this.run(['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
|
||||
// If no branch name could be resolved. i.e. `HEAD` has been returned, then Git
|
||||
// is currently in a detached state. In those cases, we just want to return the
|
||||
// currently checked out revision/SHA.
|
||||
if (branchName === 'HEAD') {
|
||||
return this.run(['rev-parse', 'HEAD']).stdout.trim();
|
||||
}
|
||||
return branchName;
|
||||
}
|
||||
|
||||
/** Gets whether the current Git repository has uncommitted changes. */
|
||||
|
@ -53,40 +53,45 @@ If you modify any part of a public API in one of the supported public packages,
|
||||
The public API guard provides a Bazel target that updates the current status of a given package. If you add to or modify the public API in any way, you must use [yarn](https://yarnpkg.com/) to execute the Bazel target in your terminal shell of choice (a recent version of `bash` is recommended).
|
||||
|
||||
```shell
|
||||
yarn bazel run //tools/public_api_guard:<modified_package>_api.accept
|
||||
yarn bazel run //packages/<modified_package>:<modified_package>_api.accept
|
||||
```
|
||||
|
||||
Using yarn ensures that you are running the correct version of Bazel.
|
||||
(Read more about building Angular with Bazel [here](./BAZEL.md).)
|
||||
|
||||
Here is an example of a Circle CI test failure that resulted from adding a new allowed type to a public property in `forms.d.ts`. Error messages from the API guard use [`git-diff` formatting](https://git-scm.com/docs/git-diff#_combined_diff_format).
|
||||
Here is an example of a Circle CI test failure that resulted from adding a new allowed type to a public property in `core.d.ts`. Error messages from the API guard use [`git-diff` formatting](https://git-scm.com/docs/git-diff#_combined_diff_format).
|
||||
|
||||
```
|
||||
FAIL: //tools/public_api_guard:forms_api (see /home/circleci/.cache/bazel/_bazel_circleci/9ce5c2144ecf75d11717c0aa41e45a8d/execroot/angular/bazel-out/k8-fastbuild/testlogs/tools/public_api_guard/forms_api/test_attempts/attempt_1.log)
|
||||
FAIL: //tools/public_api_guard:forms_api (see /home/circleci/.cache/bazel/_bazel_circleci/9ce5c2144ecf75d11717c0aa41e45a8d/execroot/angular/bazel-out/k8-fastbuild/testlogs/tools/public_api_guard/forms_api/test.log)
|
||||
FAIL: //packages/core:core_api (see /home/circleci/.cache/bazel/_bazel_circleci/9ce5c2144ecf75d11717c0aa41e45a8d/execroot/angular/bazel-out/k8-fastbuild/testlogs/packages/core/core_api/test_attempts/attempt_1.log)
|
||||
INFO: From Action packages/compiler-cli/ngcc/test/fesm5_angular_core.js:
|
||||
[BABEL] Note: The code generator has deoptimised the styling of /b/f/w/bazel-out/k8-fastbuild/bin/packages/core/npm_package/fesm2015/core.js as it exceeds the max of 500KB.
|
||||
FAIL: //packages/core:core_api (see /home/circleci/.cache/bazel/_bazel_circleci/9ce5c2144ecf75d11717c0aa41e45a8d/execroot/angular/bazel-out/k8-fastbuild/testlogs/packages/core/core_api/test.log)
|
||||
|
||||
FAILED: //packages/core:core_api (Summary)
|
||||
/home/circleci/.cache/bazel/_bazel_circleci/9ce5c2144ecf75d11717c0aa41e45a8d/execroot/angular/bazel-out/k8-fastbuild/testlogs/packages/core/core_api/test.log
|
||||
/home/circleci/.cache/bazel/_bazel_circleci/9ce5c2144ecf75d11717c0aa41e45a8d/execroot/angular/bazel-out/k8-fastbuild/testlogs/packages/core/core_api/test_attempts/attempt_1.log
|
||||
INFO: From Testing //packages/core:core_api:
|
||||
==================== Test output for //packages/core:core_api:
|
||||
/b/f/w/bazel-out/k8-fastbuild/bin/packages/core/core_api.sh.runfiles/angular/packages/core/npm_package/core.d.ts(7,1): error: No export declaration found for symbol "ComponentFactory"
|
||||
--- goldens/public-api/core/core.d.ts Golden file
|
||||
+++ goldens/public-api/core/core.d.ts Generated API
|
||||
@@ -563,9 +563,9 @@
|
||||
ngModule: Type<T>;
|
||||
providers?: Provider[];
|
||||
}
|
||||
|
||||
-export declare type NgIterable<T> = Array<T> | Iterable<T>;
|
||||
+export declare type NgIterable<T> = Iterable<T>;
|
||||
|
||||
export declare interface NgModule {
|
||||
bootstrap?: Array<Type<any> | any[]>;
|
||||
declarations?: Array<Type<any> | any[]>;
|
||||
|
||||
FAILED: //tools/public_api_guard:forms_api (Summary)
|
||||
/home/circleci/.cache/bazel/_bazel_circleci/9ce5c2144ecf75d11717c0aa41e45a8d/execroot/angular/bazel-out/k8-fastbuild/testlogs/tools/public_api_guard/forms_api/test.log
|
||||
/home/circleci/.cache/bazel/_bazel_circleci/9ce5c2144ecf75d11717c0aa41e45a8d/execroot/angular/bazel-out/k8-fastbuild/testlogs/tools/public_api_guard/forms_api/test_attempts/attempt_1.log
|
||||
INFO: From Testing //tools/public_api_guard:forms_api:
|
||||
==================== Test output for //tools/public_api_guard:forms_api:
|
||||
--- tools/public_api_guard/forms/forms.d.ts Golden file
|
||||
+++ tools/public_api_guard/forms/forms.d.ts Generated API
|
||||
@@ -4,9 +4,9 @@
|
||||
readonly disabled: boolean;
|
||||
readonly enabled: boolean;
|
||||
readonly errors: ValidationErrors | null;
|
||||
readonly invalid: boolean;
|
||||
- readonly parent: FormGroup | FormArray;
|
||||
+ readonly parent: FormGroup | FormArray | undefined;
|
||||
readonly pending: boolean;
|
||||
readonly pristine: boolean;
|
||||
readonly root: AbstractControl;
|
||||
readonly status: string;
|
||||
|
||||
If you modify a public API, you must accept the new golden file.
|
||||
|
||||
|
||||
To do so, execute the following Bazel target:
|
||||
yarn bazel run //tools/public_api_guard:forms_api.accept
|
||||
yarn bazel run //packages/core:core_api.accept
|
||||
|
||||
```
|
||||
|
2
goldens/public-api/router/router.d.ts
vendored
2
goldens/public-api/router/router.d.ts
vendored
@ -398,7 +398,7 @@ export declare class RouterLinkActive implements OnChanges, OnDestroy, AfterCont
|
||||
routerLinkActiveOptions: {
|
||||
exact: boolean;
|
||||
};
|
||||
constructor(router: Router, element: ElementRef, renderer: Renderer2, link?: RouterLink | undefined, linkWithHref?: RouterLinkWithHref | undefined);
|
||||
constructor(router: Router, element: ElementRef, renderer: Renderer2, cdr: ChangeDetectorRef, link?: RouterLink | undefined, linkWithHref?: RouterLinkWithHref | undefined);
|
||||
ngAfterContentInit(): void;
|
||||
ngOnChanges(changes: SimpleChanges): void;
|
||||
ngOnDestroy(): void;
|
||||
|
@ -12,7 +12,7 @@
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 2987,
|
||||
"main-es2015": 451406,
|
||||
"main-es2015": 450883,
|
||||
"polyfills-es2015": 52630
|
||||
}
|
||||
}
|
||||
@ -21,8 +21,8 @@
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 3097,
|
||||
"main-es2015": 428886,
|
||||
"polyfills-es2015": 52195
|
||||
"main-es2015": 428031,
|
||||
"polyfills-es2015": 52261
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,8 +30,8 @@
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 1485,
|
||||
"main-es2015": 136302,
|
||||
"polyfills-es2015": 37246
|
||||
"main-es2015": 135533,
|
||||
"polyfills-es2015": 37248
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -39,7 +39,7 @@
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 2289,
|
||||
"main-es2015": 246085,
|
||||
"main-es2015": 245488,
|
||||
"polyfills-es2015": 36938,
|
||||
"5-es2015": 751
|
||||
}
|
||||
@ -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": 1209688
|
||||
"bundle": 1209651
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,10 @@ import {
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Injectable,
|
||||
Input,
|
||||
NgModule
|
||||
NgModule,
|
||||
Pipe
|
||||
} from '@angular/core';
|
||||
|
||||
export class NonAngularBaseClass {
|
||||
@ -76,3 +78,17 @@ export class UndecoratedPipeBase {
|
||||
export class WithDirectiveLifecycleHook {
|
||||
ngOnInit() {}
|
||||
}
|
||||
|
||||
// This class is already decorated and should not be migrated. i.e. no TODO
|
||||
// or Angular decorator should be added. `@Injectable` is sufficient.
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
|
||||
// This class is already decorated and should not be migrated. i.e. no TODO
|
||||
// or Angular decorator should be added. `@Injectable` is sufficient.
|
||||
@Pipe({name: 'my-pipe'})
|
||||
export class MyPipe {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
|
@ -4,8 +4,10 @@ import {
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Injectable,
|
||||
Input,
|
||||
NgModule
|
||||
NgModule,
|
||||
Pipe
|
||||
} from '@angular/core';
|
||||
|
||||
export class NonAngularBaseClass {
|
||||
@ -87,3 +89,17 @@ export class UndecoratedPipeBase {
|
||||
export class WithDirectiveLifecycleHook {
|
||||
ngOnInit() {}
|
||||
}
|
||||
|
||||
// This class is already decorated and should not be migrated. i.e. no TODO
|
||||
// or Angular decorator should be added. `@Injectable` is sufficient.
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
|
||||
// This class is already decorated and should not be migrated. i.e. no TODO
|
||||
// or Angular decorator should be added. `@Injectable` is sufficient.
|
||||
@Pipe({name: 'my-pipe'})
|
||||
export class MyPipe {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ module.exports = function(config) {
|
||||
|
||||
// Including systemjs because it defines `__eval`, which produces correct stack traces.
|
||||
'test-events.js',
|
||||
'shims_for_IE.js',
|
||||
'third_party/shims_for_IE.js',
|
||||
'node_modules/systemjs/dist/system.src.js',
|
||||
|
||||
// Serve polyfills necessary for testing the `elements` package.
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "10.0.0",
|
||||
"version": "10.0.2",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
|
@ -27,5 +27,8 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/angular/angular/issues"
|
||||
},
|
||||
"homepage": "https://github.com/angular/angular/tree/master/packages/compiler-cli"
|
||||
"homepage": "https://github.com/angular/angular/tree/master/packages/compiler-cli",
|
||||
"publishConfig": {
|
||||
"registry": "https://wombat-dressing-room.appspot.com"
|
||||
}
|
||||
}
|
||||
|
@ -339,7 +339,9 @@ export class HttpXhrBackend implements HttpBackend {
|
||||
}
|
||||
|
||||
// Finally, abort the in-flight request.
|
||||
xhr.abort();
|
||||
if (xhr.readyState !== xhr.DONE) {
|
||||
xhr.abort();
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -147,6 +147,17 @@ const XSSI_PREFIX = ')]}\'\n';
|
||||
});
|
||||
factory.mock.mockErrorEvent(new Error('blah'));
|
||||
});
|
||||
it('avoids abort a request when fetch operation is completed', done => {
|
||||
const abort = jasmine.createSpy('abort');
|
||||
|
||||
backend.handle(TEST_POST).toPromise().then(() => {
|
||||
expect(abort).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
factory.mock.abort = abort;
|
||||
factory.mock.mockFlush(200, 'OK', 'Done');
|
||||
});
|
||||
describe('progress events', () => {
|
||||
it('are emitted for download progress', done => {
|
||||
backend.handle(TEST_POST.clone({reportProgress: true}))
|
||||
|
@ -31,6 +31,7 @@ ts_library(
|
||||
"//packages/compiler-cli/src/ngtsc/indexer",
|
||||
"//packages/compiler-cli/src/ngtsc/perf",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
"//packages/compiler-cli/src/ngtsc/shims",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck",
|
||||
"@npm//@bazel/typescript",
|
||||
"@npm//@types/node",
|
||||
|
@ -12,7 +12,7 @@ import {ParsedConfiguration} from '../../..';
|
||||
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations';
|
||||
import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles';
|
||||
import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
|
||||
import {absoluteFrom, dirname, FileSystem, LogicalFileSystem, resolve} from '../../../src/ngtsc/file_system';
|
||||
import {absoluteFrom, absoluteFromSourceFile, dirname, FileSystem, LogicalFileSystem, resolve} from '../../../src/ngtsc/file_system';
|
||||
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, PrivateExportAliasingHost, Reexport, ReferenceEmitter} from '../../../src/ngtsc/imports';
|
||||
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry} from '../../../src/ngtsc/metadata';
|
||||
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
|
||||
@ -148,7 +148,8 @@ export class DecorationAnalyzer {
|
||||
*/
|
||||
analyzeProgram(): DecorationAnalyses {
|
||||
for (const sourceFile of this.program.getSourceFiles()) {
|
||||
if (!sourceFile.isDeclarationFile && isWithinPackage(this.packagePath, sourceFile)) {
|
||||
if (!sourceFile.isDeclarationFile &&
|
||||
isWithinPackage(this.packagePath, absoluteFromSourceFile(sourceFile))) {
|
||||
this.compiler.analyzeFile(sourceFile);
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
|
||||
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../../src/ngtsc/file_system';
|
||||
import {MetadataReader} from '../../../src/ngtsc/metadata';
|
||||
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
|
||||
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
|
||||
@ -44,7 +44,7 @@ export class DefaultMigrationHost implements MigrationHost {
|
||||
}
|
||||
|
||||
isInScope(clazz: ClassDeclaration): boolean {
|
||||
return isWithinPackage(this.entryPointPath, clazz.getSourceFile());
|
||||
return isWithinPackage(this.entryPointPath, absoluteFromSourceFile(clazz.getSourceFile()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
|
||||
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../../src/ngtsc/file_system';
|
||||
import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host';
|
||||
import {isWithinPackage} from './util';
|
||||
|
||||
@ -35,7 +35,7 @@ export class SwitchMarkerAnalyzer {
|
||||
analyzeProgram(program: ts.Program): SwitchMarkerAnalyses {
|
||||
const analyzedFiles = new SwitchMarkerAnalyses();
|
||||
program.getSourceFiles()
|
||||
.filter(sourceFile => isWithinPackage(this.packagePath, sourceFile))
|
||||
.filter(sourceFile => isWithinPackage(this.packagePath, absoluteFromSourceFile(sourceFile)))
|
||||
.forEach(sourceFile => {
|
||||
const declarations = this.host.getSwitchableDeclarations(sourceFile);
|
||||
if (declarations.length) {
|
||||
|
@ -5,13 +5,11 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFromSourceFile, AbsoluteFsPath, relative} from '../../../src/ngtsc/file_system';
|
||||
import {AbsoluteFsPath, relative} from '../../../src/ngtsc/file_system';
|
||||
import {DependencyTracker} from '../../../src/ngtsc/incremental/api';
|
||||
|
||||
export function isWithinPackage(packagePath: AbsoluteFsPath, sourceFile: ts.SourceFile): boolean {
|
||||
const relativePath = relative(packagePath, absoluteFromSourceFile(sourceFile));
|
||||
export function isWithinPackage(packagePath: AbsoluteFsPath, filePath: AbsoluteFsPath): boolean {
|
||||
const relativePath = relative(packagePath, filePath);
|
||||
return !relativePath.startsWith('..') && !relativePath.startsWith('node_modules/');
|
||||
}
|
||||
|
||||
|
@ -28,13 +28,13 @@ export class ClusterExecutor implements Executor {
|
||||
|
||||
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, _createCompileFn: CreateCompileFn):
|
||||
Promise<void> {
|
||||
return this.lockFile.lock(() => {
|
||||
return this.lockFile.lock(async () => {
|
||||
this.logger.debug(
|
||||
`Running ngcc on ${this.constructor.name} (using ${this.workerCount} worker processes).`);
|
||||
const master = new ClusterMaster(
|
||||
this.workerCount, this.fileSystem, this.logger, this.fileWriter, this.pkgJsonUpdater,
|
||||
analyzeEntryPoints, this.createTaskCompletedCallback);
|
||||
return master.run();
|
||||
return await master.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import {absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
|
||||
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference} from '../../../src/ngtsc/reflection';
|
||||
import {isWithinPackage} from '../analysis/util';
|
||||
@ -2525,7 +2526,7 @@ function getRootFileOrFail(bundle: BundleProgram): ts.SourceFile {
|
||||
function getNonRootPackageFiles(bundle: BundleProgram): ts.SourceFile[] {
|
||||
const rootFile = bundle.program.getSourceFile(bundle.path);
|
||||
return bundle.program.getSourceFiles().filter(
|
||||
f => (f !== rootFile) && isWithinPackage(bundle.package, f));
|
||||
f => (f !== rootFile) && isWithinPackage(bundle.package, absoluteFromSourceFile(f)));
|
||||
}
|
||||
|
||||
function isTopLevel(node: ts.Node): boolean {
|
||||
|
@ -37,7 +37,11 @@ export class AsyncLocker {
|
||||
*/
|
||||
async lock<T>(fn: () => Promise<T>): Promise<T> {
|
||||
await this.create();
|
||||
return fn().finally(() => this.lockFile.remove());
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
this.lockFile.remove();
|
||||
}
|
||||
}
|
||||
|
||||
protected async create() {
|
||||
|
@ -50,7 +50,7 @@ export function makeEntryPointBundle(
|
||||
const rootDir = entryPoint.packagePath;
|
||||
const options: ts
|
||||
.CompilerOptions = {allowJs: true, maxNodeModuleJsDepth: Infinity, rootDir, ...pathMappings};
|
||||
const srcHost = new NgccSourcesCompilerHost(fs, options, entryPoint.path);
|
||||
const srcHost = new NgccSourcesCompilerHost(fs, options, entryPoint.packagePath);
|
||||
const dtsHost = new NgtscCompilerHost(fs, options);
|
||||
|
||||
// Create the bundle programs, as necessary.
|
||||
@ -63,7 +63,7 @@ export function makeEntryPointBundle(
|
||||
[];
|
||||
const dts = transformDts ? makeBundleProgram(
|
||||
fs, isCore, entryPoint.packagePath, typingsPath, 'r3_symbols.d.ts',
|
||||
options, dtsHost, additionalDtsFiles) :
|
||||
{...options, allowJs: false}, dtsHost, additionalDtsFiles) :
|
||||
null;
|
||||
const isFlatCore = isCore && src.r3SymbolsFile === null;
|
||||
|
||||
|
@ -7,7 +7,8 @@
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
|
||||
import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
|
||||
import {isWithinPackage} from '../analysis/util';
|
||||
import {isRelativePath} from '../utils';
|
||||
|
||||
/**
|
||||
@ -20,7 +21,7 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost {
|
||||
private cache = ts.createModuleResolutionCache(
|
||||
this.getCurrentDirectory(), file => this.getCanonicalFileName(file));
|
||||
|
||||
constructor(fs: FileSystem, options: ts.CompilerOptions, protected entryPointPath: string) {
|
||||
constructor(fs: FileSystem, options: ts.CompilerOptions, protected packagePath: AbsoluteFsPath) {
|
||||
super(fs, options);
|
||||
}
|
||||
|
||||
@ -36,13 +37,24 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost {
|
||||
// file was in the same directory. This is undesirable, as we need to have the actual
|
||||
// JavaScript being present in the program. This logic recognizes this scenario and rewrites
|
||||
// the resolved .d.ts declaration file to its .js counterpart, if it exists.
|
||||
if (resolvedModule !== undefined && resolvedModule.extension === ts.Extension.Dts &&
|
||||
containingFile.endsWith('.js') && isRelativePath(moduleName)) {
|
||||
if (resolvedModule?.extension === ts.Extension.Dts && containingFile.endsWith('.js') &&
|
||||
isRelativePath(moduleName)) {
|
||||
const jsFile = resolvedModule.resolvedFileName.replace(/\.d\.ts$/, '.js');
|
||||
if (this.fileExists(jsFile)) {
|
||||
return {...resolvedModule, resolvedFileName: jsFile, extension: ts.Extension.Js};
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent loading JavaScript source files outside of the package root, which would happen for
|
||||
// packages that don't have .d.ts files. As ngcc should only operate on the .js files
|
||||
// contained within the package, any files outside the package are simply discarded. This does
|
||||
// result in a partial program with error diagnostics, however ngcc won't gather diagnostics
|
||||
// for the program it creates so these diagnostics won't be reported.
|
||||
if (resolvedModule?.extension === ts.Extension.Js &&
|
||||
!isWithinPackage(this.packagePath, this.fs.resolve(resolvedModule.resolvedFileName))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return resolvedModule;
|
||||
});
|
||||
}
|
||||
|
@ -5,7 +5,6 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import {absoluteFrom} from '../../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||
import {isWithinPackage} from '../../src/analysis/util';
|
||||
@ -18,15 +17,13 @@ runInEachFileSystem(() => {
|
||||
|
||||
it('should return true if the source-file is contained in the package', () => {
|
||||
const packagePath = _('/node_modules/test');
|
||||
const file =
|
||||
ts.createSourceFile(_('/node_modules/test/src/index.js'), '', ts.ScriptTarget.ES2015);
|
||||
const file = _('/node_modules/test/src/index.js');
|
||||
expect(isWithinPackage(packagePath, file)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the source-file is not contained in the package', () => {
|
||||
const packagePath = _('/node_modules/test');
|
||||
const file =
|
||||
ts.createSourceFile(_('/node_modules/other/src/index.js'), '', ts.ScriptTarget.ES2015);
|
||||
const file = _('/node_modules/other/src/index.js');
|
||||
expect(isWithinPackage(packagePath, file)).toBe(false);
|
||||
});
|
||||
|
||||
@ -34,13 +31,11 @@ runInEachFileSystem(() => {
|
||||
const packagePath = _('/node_modules/test');
|
||||
|
||||
// An external file inside the package's `node_modules/`.
|
||||
const file1 = ts.createSourceFile(
|
||||
_('/node_modules/test/node_modules/other/src/index.js'), '', ts.ScriptTarget.ES2015);
|
||||
const file1 = _('/node_modules/test/node_modules/other/src/index.js');
|
||||
expect(isWithinPackage(packagePath, file1)).toBe(false);
|
||||
|
||||
// An internal file starting with `node_modules`.
|
||||
const file2 = ts.createSourceFile(
|
||||
_('/node_modules/test/node_modules_optimizer.js'), '', ts.ScriptTarget.ES2015);
|
||||
const file2 = _('/node_modules/test/node_modules_optimizer.js');
|
||||
expect(isWithinPackage(packagePath, file2)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
@ -34,7 +34,7 @@ runInEachFileSystem(() => {
|
||||
|
||||
beforeEach(() => {
|
||||
masterRunSpy = spyOn(ClusterMaster.prototype, 'run')
|
||||
.and.returnValue(Promise.resolve('CusterMaster#run()' as any));
|
||||
.and.returnValue(Promise.resolve('ClusterMaster#run()' as any));
|
||||
createTaskCompletedCallback = jasmine.createSpy('createTaskCompletedCallback');
|
||||
|
||||
mockLogger = new MockLogger();
|
||||
@ -63,7 +63,7 @@ runInEachFileSystem(() => {
|
||||
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
|
||||
|
||||
expect(await executor.execute(analyzeEntryPointsSpy, createCompilerFnSpy))
|
||||
.toBe('CusterMaster#run()' as any);
|
||||
.toBe('ClusterMaster#run()' as any);
|
||||
|
||||
expect(masterRunSpy).toHaveBeenCalledWith();
|
||||
|
||||
@ -78,6 +78,22 @@ runInEachFileSystem(() => {
|
||||
expect(lockFileLog).toEqual(['write()', 'remove()']);
|
||||
});
|
||||
|
||||
it('should call LockFile.write() and LockFile.remove() if analyzeFn fails', async () => {
|
||||
const analyzeEntryPointsSpy =
|
||||
jasmine.createSpy('analyzeEntryPoints').and.throwError('analyze error');
|
||||
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
|
||||
let error = '';
|
||||
try {
|
||||
await executor.execute(analyzeEntryPointsSpy, createCompilerFnSpy);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
expect(analyzeEntryPointsSpy).toHaveBeenCalledWith();
|
||||
expect(createCompilerFnSpy).not.toHaveBeenCalled();
|
||||
expect(error).toEqual('analyze error');
|
||||
expect(lockFileLog).toEqual(['write()', 'remove()']);
|
||||
});
|
||||
|
||||
it('should call LockFile.write() and LockFile.remove() if master runner fails', async () => {
|
||||
const anyFn: () => any = () => undefined;
|
||||
masterRunSpy.and.returnValue(Promise.reject(new Error('master runner error')));
|
||||
|
@ -68,7 +68,7 @@ export function makeTestBundleProgram(
|
||||
const rootDir = fs.dirname(entryPointPath);
|
||||
const options: ts.CompilerOptions =
|
||||
{allowJs: true, maxNodeModuleJsDepth: Infinity, checkJs: false, rootDir, rootDirs: [rootDir]};
|
||||
const host = new NgccSourcesCompilerHost(fs, options, entryPointPath);
|
||||
const host = new NgccSourcesCompilerHost(fs, options, rootDir);
|
||||
return makeBundleProgram(
|
||||
fs, isCore, rootDir, path, 'r3_symbols.js', options, host, additionalFiles);
|
||||
}
|
||||
|
@ -401,6 +401,121 @@ runInEachFileSystem(() => {
|
||||
expect(es5Contents).toContain('ɵngcc0.ɵɵtext(0, "a - b - 3 - 4")');
|
||||
});
|
||||
|
||||
it('should not crash when scanning for ModuleWithProviders needs to evaluate code from an external package',
|
||||
() => {
|
||||
// Regression test for https://github.com/angular/angular/issues/37508
|
||||
// During `ModuleWithProviders` analysis, return statements in methods are evaluated using
|
||||
// the partial evaluator to identify whether they correspond with a `ModuleWithProviders`
|
||||
// function. If an arbitrary method has a return statement that calls into an external
|
||||
// module which doesn't have declaration files, ngcc would attempt to reflect on said
|
||||
// module using the reflection host of the entry-point. This would crash in the case where
|
||||
// e.g. the entry-point is UMD and the external module would be CommonJS, as the UMD
|
||||
// reflection host would throw because it is unable to deal with CommonJS.
|
||||
|
||||
// Setup a non-TS package with CommonJS module format
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _(`/node_modules/identity/package.json`),
|
||||
contents: `{"name": "identity", "main": "./index.js"}`,
|
||||
},
|
||||
{
|
||||
name: _(`/node_modules/identity/index.js`),
|
||||
contents: `
|
||||
function identity(x) { return x; };
|
||||
exports.identity = identity;
|
||||
module.exports = identity;
|
||||
`,
|
||||
},
|
||||
]);
|
||||
|
||||
// Setup an Angular entry-point with UMD module format that references an export of the
|
||||
// CommonJS package.
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/node_modules/test-package/package.json'),
|
||||
contents: '{"name": "test-package", "main": "./index.js", "typings": "./index.d.ts"}'
|
||||
},
|
||||
{
|
||||
name: _('/node_modules/test-package/index.js'),
|
||||
contents: `
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('identity')) :
|
||||
typeof define === 'function' && define.amd ? define('test', ['exports', 'identity'], factory) :
|
||||
(factory(global.test, global.identity));
|
||||
}(this, (function (exports, identity) { 'use strict';
|
||||
function Foo(x) {
|
||||
// The below statement is analyzed for 'ModuleWithProviders', so is evaluated
|
||||
// by ngcc. The reference into the non-TS CommonJS package used to crash ngcc.
|
||||
return identity.identity(x);
|
||||
}
|
||||
exports.Foo = Foo;
|
||||
})));
|
||||
`
|
||||
},
|
||||
{
|
||||
name: _('/node_modules/test-package/index.d.ts'),
|
||||
contents: 'export declare class Foo { static doSomething(x: any): any; }'
|
||||
},
|
||||
{name: _('/node_modules/test-package/index.metadata.json'), contents: 'DUMMY DATA'},
|
||||
]);
|
||||
|
||||
expect(() => mainNgcc({
|
||||
basePath: '/node_modules',
|
||||
targetEntryPointPath: 'test-package',
|
||||
propertiesToConsider: ['main'],
|
||||
}))
|
||||
.not.toThrow();
|
||||
});
|
||||
|
||||
it('should not be able to evaluate code in external packages when no .d.ts files are present',
|
||||
() => {
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _(`/node_modules/external/package.json`),
|
||||
contents: `{"name": "external", "main": "./index.js"}`,
|
||||
},
|
||||
{
|
||||
name: _(`/node_modules/external/index.js`),
|
||||
contents: `
|
||||
export const selector = 'my-selector';
|
||||
`,
|
||||
},
|
||||
]);
|
||||
|
||||
compileIntoApf('test-package', {
|
||||
'/index.ts': `
|
||||
import {NgModule, Component} from '@angular/core';
|
||||
import {selector} from 'external';
|
||||
|
||||
@Component({
|
||||
selector,
|
||||
template: ''
|
||||
})
|
||||
export class FooComponent {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [FooComponent],
|
||||
})
|
||||
export class FooModule {}
|
||||
`,
|
||||
});
|
||||
|
||||
try {
|
||||
mainNgcc({
|
||||
basePath: '/node_modules',
|
||||
targetEntryPointPath: 'test-package',
|
||||
propertiesToConsider: ['esm2015', 'esm5'],
|
||||
});
|
||||
fail('should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.message).toContain(
|
||||
'Failed to compile entry-point test-package (esm2015 as esm2015) due to compilation errors:');
|
||||
expect(e.message).toContain('NG1010');
|
||||
expect(e.message).toContain('selector must be a string');
|
||||
}
|
||||
});
|
||||
|
||||
it('should add ɵfac but not duplicate ɵprov properties on injectables', () => {
|
||||
compileIntoFlatEs2015Package('test-package', {
|
||||
'/index.ts': `
|
||||
|
@ -13,8 +13,12 @@ import {makeEntryPointBundle} from '../../src/packages/entry_point_bundle';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
describe('entry point bundle', () => {
|
||||
let _: typeof absoluteFrom;
|
||||
beforeEach(() => {
|
||||
_ = absoluteFrom;
|
||||
});
|
||||
|
||||
function setupMockFileSystem(): void {
|
||||
const _ = absoluteFrom;
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/node_modules/test/package.json'),
|
||||
@ -210,6 +214,103 @@ runInEachFileSystem(() => {
|
||||
].map(p => absoluteFrom(p).toString())));
|
||||
});
|
||||
|
||||
it('does not include .js files outside of the package when no .d.ts file is available', () => {
|
||||
// Declare main "test" package with "entry" entry-point that imports all sorts of
|
||||
// internal and external modules.
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/node_modules/test/entry/package.json'),
|
||||
contents: `{"name": "test", "main": "./index.js", "typings": "./index.d.ts"}`,
|
||||
},
|
||||
{
|
||||
name: _('/node_modules/test/entry/index.d.ts'),
|
||||
contents: `
|
||||
import 'external-js';
|
||||
import 'external-ts';
|
||||
import 'nested-js';
|
||||
import './local';
|
||||
import '../package';
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: _('/node_modules/test/entry/index.js'),
|
||||
contents: `
|
||||
import 'external-js';
|
||||
import 'external-ts';
|
||||
import 'nested-js';
|
||||
import './local';
|
||||
import '../package';
|
||||
`,
|
||||
},
|
||||
{name: _('/node_modules/test/entry/local.d.ts'), contents: `export {};`},
|
||||
{name: _('/node_modules/test/entry/local.js'), contents: `export {};`},
|
||||
{name: _('/node_modules/test/package.d.ts'), contents: `export {};`},
|
||||
{name: _('/node_modules/test/package.js'), contents: `export {};`},
|
||||
]);
|
||||
|
||||
// Declare "external-js" package outside of the "test" package without .d.ts files, should
|
||||
// not be included in the program.
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/node_modules/external-js/package.json'),
|
||||
contents: `{"name": "external-js", "main": "./index.js"}`,
|
||||
},
|
||||
{name: _('/node_modules/external-js/index.js'), contents: 'export {};'},
|
||||
]);
|
||||
|
||||
// Same as "external-js" but located in a nested node_modules directory, which should also
|
||||
// not be included in the program.
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/node_modules/test/node_modules/nested-js/package.json'),
|
||||
contents: `{"name": "nested-js", "main": "./index.js"}`,
|
||||
},
|
||||
{name: _('/node_modules/test/node_modules/nested-js/index.js'), contents: 'export {}'},
|
||||
]);
|
||||
|
||||
// Declare "external-ts" which does have .d.ts files, so the .d.ts should be
|
||||
// loaded into the program.
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/node_modules/external-ts/package.json'),
|
||||
contents: `{"name": "external-ts", "main": "./index.js", "typings": "./index.d.ts"}`,
|
||||
},
|
||||
{name: _('/node_modules/external-ts/index.d.ts'), contents: 'export {};'},
|
||||
{name: _('/node_modules/external-ts/index.js'), contents: 'export {};'},
|
||||
]);
|
||||
|
||||
const fs = getFileSystem();
|
||||
const entryPoint: EntryPoint = {
|
||||
name: 'test/entry',
|
||||
path: absoluteFrom('/node_modules/test/entry'),
|
||||
packageName: 'test',
|
||||
packagePath: absoluteFrom('/node_modules/test'),
|
||||
packageJson: {name: 'test/entry'},
|
||||
typings: absoluteFrom('/node_modules/test/entry/index.d.ts'),
|
||||
compiledByAngular: true,
|
||||
ignoreMissingDependencies: false,
|
||||
generateDeepReexports: false,
|
||||
};
|
||||
const esm5bundle = makeEntryPointBundle(
|
||||
fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true,
|
||||
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
|
||||
|
||||
expect(esm5bundle.src.program.getSourceFiles().map(sf => _(sf.fileName)))
|
||||
.toEqual(jasmine.arrayWithExactContents([
|
||||
_('/node_modules/test/entry/index.js'),
|
||||
_('/node_modules/test/entry/local.js'),
|
||||
_('/node_modules/test/package.js'),
|
||||
_('/node_modules/external-ts/index.d.ts'),
|
||||
]));
|
||||
expect(esm5bundle.dts!.program.getSourceFiles().map(sf => _(sf.fileName)))
|
||||
.toEqual(jasmine.arrayWithExactContents([
|
||||
_('/node_modules/test/entry/index.d.ts'),
|
||||
_('/node_modules/test/entry/local.d.ts'),
|
||||
_('/node_modules/test/package.d.ts'),
|
||||
_('/node_modules/external-ts/index.d.ts'),
|
||||
]));
|
||||
});
|
||||
|
||||
describe(
|
||||
'including equivalently named, internally imported, src files in the typings program',
|
||||
() => {
|
||||
|
@ -116,7 +116,13 @@ export class NgCompiler {
|
||||
|
||||
const moduleResolutionCache = ts.createModuleResolutionCache(
|
||||
this.adapter.getCurrentDirectory(),
|
||||
fileName => this.adapter.getCanonicalFileName(fileName));
|
||||
// Note: this used to be an arrow-function closure. However, JS engines like v8 have some
|
||||
// strange behaviors with retaining the lexical scope of the closure. Even if this function
|
||||
// doesn't retain a reference to `this`, if other closures in the constructor here reference
|
||||
// `this` internally then a closure created here would retain them. This can cause major
|
||||
// memory leak issues since the `moduleResolutionCache` is a long-lived object and finds its
|
||||
// way into all kinds of places inside TS internal objects.
|
||||
this.adapter.getCanonicalFileName.bind(this.adapter));
|
||||
this.moduleResolver =
|
||||
new ModuleResolver(tsProgram, this.options, this.adapter, moduleResolutionCache);
|
||||
this.resourceManager = new AdapterResourceLoader(adapter, this.options);
|
||||
|
@ -42,20 +42,22 @@ export class NoopIncrementalBuildStrategy implements IncrementalBuildStrategy {
|
||||
* Tracks an `IncrementalDriver` within the strategy itself.
|
||||
*/
|
||||
export class TrackedIncrementalBuildStrategy implements IncrementalBuildStrategy {
|
||||
private previous: IncrementalDriver|null = null;
|
||||
private next: IncrementalDriver|null = null;
|
||||
private driver: IncrementalDriver|null = null;
|
||||
private isSet: boolean = false;
|
||||
|
||||
getIncrementalDriver(): IncrementalDriver|null {
|
||||
return this.next !== null ? this.next : this.previous;
|
||||
return this.driver;
|
||||
}
|
||||
|
||||
setIncrementalDriver(driver: IncrementalDriver): void {
|
||||
this.next = driver;
|
||||
this.driver = driver;
|
||||
this.isSet = true;
|
||||
}
|
||||
|
||||
toNextBuildStrategy(): TrackedIncrementalBuildStrategy {
|
||||
const strategy = new TrackedIncrementalBuildStrategy();
|
||||
strategy.previous = this.next;
|
||||
// Only reuse a driver that was explicitly set via `setIncrementalDriver`.
|
||||
strategy.driver = this.isSet ? this.driver : null;
|
||||
return strategy;
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import {NgCompilerOptions} from './core/api';
|
||||
import {TrackedIncrementalBuildStrategy} from './incremental';
|
||||
import {IndexedComponent} from './indexer';
|
||||
import {NOOP_PERF_RECORDER, PerfRecorder, PerfTracker} from './perf';
|
||||
import {retagAllTsFiles, untagAllTsFiles} from './shims';
|
||||
import {ReusedProgramStrategy} from './typecheck';
|
||||
|
||||
|
||||
@ -68,14 +69,26 @@ export class NgtscProgram implements api.Program {
|
||||
}
|
||||
this.closureCompilerEnabled = !!options.annotateForClosureCompiler;
|
||||
|
||||
const reuseProgram = oldProgram && oldProgram.reuseTsProgram;
|
||||
const reuseProgram = oldProgram?.reuseTsProgram;
|
||||
this.host = NgCompilerHost.wrap(delegateHost, rootNames, options, reuseProgram ?? null);
|
||||
|
||||
if (reuseProgram !== undefined) {
|
||||
// Prior to reusing the old program, restore shim tagging for all its `ts.SourceFile`s.
|
||||
// TypeScript checks the `referencedFiles` of `ts.SourceFile`s for changes when evaluating
|
||||
// incremental reuse of data from the old program, so it's important that these match in order
|
||||
// to get the most benefit out of reuse.
|
||||
retagAllTsFiles(reuseProgram);
|
||||
}
|
||||
|
||||
this.tsProgram = ts.createProgram(this.host.inputFiles, options, this.host, reuseProgram);
|
||||
this.reuseTsProgram = this.tsProgram;
|
||||
|
||||
this.host.postProgramCreationCleanup();
|
||||
|
||||
// Shim tagging has served its purpose, and tags can now be removed from all `ts.SourceFile`s in
|
||||
// the program.
|
||||
untagAllTsFiles(this.tsProgram);
|
||||
|
||||
const reusedProgramStrategy = new ReusedProgramStrategy(
|
||||
this.tsProgram, this.host, this.options, this.host.shimExtensionPrefixes);
|
||||
|
||||
@ -93,6 +106,10 @@ export class NgtscProgram implements api.Program {
|
||||
return this.tsProgram;
|
||||
}
|
||||
|
||||
getReuseTsProgram(): ts.Program {
|
||||
return this.reuseTsProgram;
|
||||
}
|
||||
|
||||
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken|
|
||||
undefined): readonly ts.Diagnostic[] {
|
||||
return this.tsProgram.getOptionsDiagnostics(cancellationToken);
|
||||
@ -248,6 +265,7 @@ export class NgtscProgram implements api.Program {
|
||||
}));
|
||||
this.perfRecorder.stop(fileEmitSpan);
|
||||
}
|
||||
|
||||
this.perfRecorder.stop(emitSpan);
|
||||
|
||||
if (this.perfTracker !== null && this.options.tracePerformance !== undefined) {
|
||||
|
@ -9,7 +9,7 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
export {ShimAdapter} from './src/adapter';
|
||||
export {copyFileShimData, isShim} from './src/expando';
|
||||
export {copyFileShimData, isShim, retagAllTsFiles, retagTsFile, sfExtensionData, untagAllTsFiles, untagTsFile} from './src/expando';
|
||||
export {FactoryGenerator, generatedFactoryTransform} from './src/factory_generator';
|
||||
export {ShimReferenceTagger} from './src/reference_tagger';
|
||||
export {SummaryGenerator} from './src/summary_generator';
|
||||
|
@ -12,7 +12,7 @@ import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '../../file_s
|
||||
import {isDtsPath} from '../../util/src/typescript';
|
||||
import {PerFileShimGenerator, TopLevelShimGenerator} from '../api';
|
||||
|
||||
import {isFileShimSourceFile, isShim, NgExtension, sfExtensionData} from './expando';
|
||||
import {isFileShimSourceFile, isShim, sfExtensionData} from './expando';
|
||||
import {makeShimFileName} from './util';
|
||||
|
||||
interface ShimGeneratorData {
|
||||
|
@ -21,7 +21,16 @@ export const NgExtension = Symbol('NgExtension');
|
||||
export interface NgExtensionData {
|
||||
isTopLevelShim: boolean;
|
||||
fileShim: NgFileShimData|null;
|
||||
|
||||
/**
|
||||
* The contents of the `referencedFiles` array, before modification by a `ShimReferenceTagger`.
|
||||
*/
|
||||
originalReferencedFiles: ReadonlyArray<ts.FileReference>|null;
|
||||
|
||||
/**
|
||||
* The contents of the `referencedFiles` array, after modification by a `ShimReferenceTagger`.
|
||||
*/
|
||||
taggedReferenceFiles: ReadonlyArray<ts.FileReference>|null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -65,6 +74,7 @@ export function sfExtensionData(sf: ts.SourceFile): NgExtensionData {
|
||||
isTopLevelShim: false,
|
||||
fileShim: null,
|
||||
originalReferencedFiles: null,
|
||||
taggedReferenceFiles: null,
|
||||
};
|
||||
extSf[NgExtension] = extension;
|
||||
return extension;
|
||||
@ -110,3 +120,53 @@ export function copyFileShimData(from: ts.SourceFile, to: ts.SourceFile): void {
|
||||
}
|
||||
sfExtensionData(to).fileShim = sfExtensionData(from).fileShim;
|
||||
}
|
||||
|
||||
/**
|
||||
* For those `ts.SourceFile`s in the `program` which have previously been tagged by a
|
||||
* `ShimReferenceTagger`, restore the original `referencedFiles` array that does not have shim tags.
|
||||
*/
|
||||
export function untagAllTsFiles(program: ts.Program): void {
|
||||
for (const sf of program.getSourceFiles()) {
|
||||
untagTsFile(sf);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For those `ts.SourceFile`s in the `program` which have previously been tagged by a
|
||||
* `ShimReferenceTagger`, re-apply the effects of tagging by updating the `referencedFiles` array to
|
||||
* the tagged version produced previously.
|
||||
*/
|
||||
export function retagAllTsFiles(program: ts.Program): void {
|
||||
for (const sf of program.getSourceFiles()) {
|
||||
retagTsFile(sf);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the original `referencedFiles` for the given `ts.SourceFile`.
|
||||
*/
|
||||
export function untagTsFile(sf: ts.SourceFile): void {
|
||||
if (sf.isDeclarationFile || !isExtended(sf)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ext = sfExtensionData(sf);
|
||||
if (ext.originalReferencedFiles !== null) {
|
||||
sf.referencedFiles = ext.originalReferencedFiles as Array<ts.FileReference>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the previously tagged `referencedFiles` to the given `ts.SourceFile`, if it was previously
|
||||
* tagged.
|
||||
*/
|
||||
export function retagTsFile(sf: ts.SourceFile): void {
|
||||
if (sf.isDeclarationFile || !isExtended(sf)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ext = sfExtensionData(sf);
|
||||
if (ext.taggedReferenceFiles !== null) {
|
||||
sf.referencedFiles = ext.taggedReferenceFiles as Array<ts.FileReference>;
|
||||
}
|
||||
}
|
||||
|
@ -8,10 +8,10 @@
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFrom, absoluteFromSourceFile} from '../../file_system';
|
||||
import {absoluteFromSourceFile} from '../../file_system';
|
||||
import {isNonDeclarationTsPath} from '../../util/src/typescript';
|
||||
|
||||
import {isExtended as isExtendedSf, isShim, NgExtension, sfExtensionData} from './expando';
|
||||
import {isShim, sfExtensionData} from './expando';
|
||||
import {makeShimFileName} from './util';
|
||||
|
||||
/**
|
||||
@ -48,8 +48,16 @@ export class ShimReferenceTagger {
|
||||
return;
|
||||
}
|
||||
|
||||
sfExtensionData(sf).originalReferencedFiles = sf.referencedFiles;
|
||||
const referencedFiles = [...sf.referencedFiles];
|
||||
const ext = sfExtensionData(sf);
|
||||
|
||||
// If this file has never been tagged before, capture its `referencedFiles` in the extension
|
||||
// data.
|
||||
if (ext.originalReferencedFiles === null) {
|
||||
ext.originalReferencedFiles = sf.referencedFiles;
|
||||
}
|
||||
|
||||
const referencedFiles = [...ext.originalReferencedFiles];
|
||||
|
||||
|
||||
const sfPath = absoluteFromSourceFile(sf);
|
||||
for (const suffix of this.suffixes) {
|
||||
@ -60,26 +68,16 @@ export class ShimReferenceTagger {
|
||||
});
|
||||
}
|
||||
|
||||
ext.taggedReferenceFiles = referencedFiles;
|
||||
sf.referencedFiles = referencedFiles;
|
||||
this.tagged.add(sf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the original `referencedFiles` values of all tagged `ts.SourceFile`s and disable the
|
||||
* `ShimReferenceTagger`.
|
||||
* Disable the `ShimReferenceTagger` and free memory associated with tracking tagged files.
|
||||
*/
|
||||
finalize(): void {
|
||||
this.enabled = false;
|
||||
for (const sf of this.tagged) {
|
||||
if (!isExtendedSf(sf)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const extensionData = sfExtensionData(sf);
|
||||
if (extensionData.originalReferencedFiles !== null) {
|
||||
sf.referencedFiles = extensionData.originalReferencedFiles! as ts.FileReference[];
|
||||
}
|
||||
}
|
||||
this.tagged.clear();
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import {absoluteFrom as _, AbsoluteFsPath, getSourceFileOrError} from '../../fil
|
||||
import {runInEachFileSystem} from '../../file_system/testing';
|
||||
import {makeProgram} from '../../testing';
|
||||
import {ShimAdapter} from '../src/adapter';
|
||||
import {retagTsFile, untagTsFile} from '../src/expando';
|
||||
import {ShimReferenceTagger} from '../src/reference_tagger';
|
||||
|
||||
import {TestShimGenerator} from './util';
|
||||
@ -67,40 +68,6 @@ runInEachFileSystem(() => {
|
||||
expect(shimSf.referencedFiles).toEqual([]);
|
||||
});
|
||||
|
||||
it('should remove tags during finalization', () => {
|
||||
const tagger = new ShimReferenceTagger(['test1', 'test2']);
|
||||
|
||||
const fileName = _('/file.ts');
|
||||
const sf = makeArbitrarySf(fileName);
|
||||
|
||||
expectReferencedFiles(sf, []);
|
||||
|
||||
tagger.tag(sf);
|
||||
expectReferencedFiles(sf, ['/file.test1.ts', '/file.test2.ts']);
|
||||
|
||||
tagger.finalize();
|
||||
expectReferencedFiles(sf, []);
|
||||
});
|
||||
|
||||
it('should not remove references it did not add during finalization', () => {
|
||||
const tagger = new ShimReferenceTagger(['test1', 'test2']);
|
||||
const fileName = _('/file.ts');
|
||||
const libFileName = _('/lib.d.ts');
|
||||
|
||||
const sf = makeSf(fileName, `
|
||||
/// <reference path="/lib.d.ts" />
|
||||
export const UNIMPORTANT = true;
|
||||
`);
|
||||
|
||||
expectReferencedFiles(sf, [libFileName]);
|
||||
|
||||
tagger.tag(sf);
|
||||
expectReferencedFiles(sf, ['/file.test1.ts', '/file.test2.ts', libFileName]);
|
||||
|
||||
tagger.finalize();
|
||||
expectReferencedFiles(sf, [libFileName]);
|
||||
});
|
||||
|
||||
it('should not tag shims after finalization', () => {
|
||||
const tagger = new ShimReferenceTagger(['test1', 'test2']);
|
||||
tagger.finalize();
|
||||
@ -111,6 +78,56 @@ runInEachFileSystem(() => {
|
||||
tagger.tag(sf);
|
||||
expectReferencedFiles(sf, []);
|
||||
});
|
||||
|
||||
it('should not overwrite original referencedFiles', () => {
|
||||
const tagger = new ShimReferenceTagger(['test']);
|
||||
|
||||
const fileName = _('/file.ts');
|
||||
const sf = makeArbitrarySf(fileName);
|
||||
sf.referencedFiles = [{
|
||||
fileName: _('/other.ts'),
|
||||
pos: 0,
|
||||
end: 0,
|
||||
}];
|
||||
|
||||
tagger.tag(sf);
|
||||
expectReferencedFiles(sf, ['/other.ts', '/file.test.ts']);
|
||||
});
|
||||
|
||||
it('should always tag against the original referencedFiles', () => {
|
||||
const tagger1 = new ShimReferenceTagger(['test1']);
|
||||
const tagger2 = new ShimReferenceTagger(['test2']);
|
||||
|
||||
const fileName = _('/file.ts');
|
||||
const sf = makeArbitrarySf(fileName);
|
||||
|
||||
tagger1.tag(sf);
|
||||
tagger2.tag(sf);
|
||||
expectReferencedFiles(sf, ['/file.test2.ts']);
|
||||
});
|
||||
|
||||
describe('tagging and untagging', () => {
|
||||
it('should be able to untag references and retag them later', () => {
|
||||
const tagger = new ShimReferenceTagger(['test']);
|
||||
|
||||
const fileName = _('/file.ts');
|
||||
const sf = makeArbitrarySf(fileName);
|
||||
sf.referencedFiles = [{
|
||||
fileName: _('/other.ts'),
|
||||
pos: 0,
|
||||
end: 0,
|
||||
}];
|
||||
|
||||
tagger.tag(sf);
|
||||
expectReferencedFiles(sf, ['/other.ts', '/file.test.ts']);
|
||||
|
||||
untagTsFile(sf);
|
||||
expectReferencedFiles(sf, ['/other.ts']);
|
||||
|
||||
retagTsFile(sf);
|
||||
expectReferencedFiles(sf, ['/other.ts', '/file.test.ts']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -5,4 +5,4 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
export {getDeclaration, makeProgram} from './src/utils';
|
||||
export {expectCompleteReuse, getDeclaration, makeProgram} from './src/utils';
|
||||
|
@ -97,6 +97,23 @@ export function walkForDeclaration(name: string, rootNode: ts.Node): ts.Declarat
|
||||
return chosenDecl;
|
||||
}
|
||||
|
||||
const COMPLETE_REUSE_FAILURE_MESSAGE =
|
||||
'The original program was not reused completely, even though no changes should have been made to its structure';
|
||||
|
||||
/**
|
||||
* Extracted from TypeScript's internal enum `StructureIsReused`.
|
||||
*/
|
||||
enum TsStructureIsReused {
|
||||
Not = 0,
|
||||
SafeModules = 1,
|
||||
Completely = 2,
|
||||
}
|
||||
|
||||
export function expectCompleteReuse(oldProgram: ts.Program): void {
|
||||
// Assert complete reuse using TypeScript's private API.
|
||||
expect((oldProgram as any).structureIsReused)
|
||||
.toBe(TsStructureIsReused.Completely, COMPLETE_REUSE_FAILURE_MESSAGE);
|
||||
}
|
||||
|
||||
function bindingNameEquals(node: ts.BindingName, name: string): boolean {
|
||||
if (ts.isIdentifier(node)) {
|
||||
|
@ -13,6 +13,7 @@ import {NgCompilerOptions, UnifiedModulesHost} from './core/api';
|
||||
import {NodeJSFileSystem, setFileSystem} from './file_system';
|
||||
import {PatchedProgramIncrementalBuildStrategy} from './incremental';
|
||||
import {NOOP_PERF_RECORDER} from './perf';
|
||||
import {untagAllTsFiles} from './shims';
|
||||
import {ReusedProgramStrategy} from './typecheck/src/augmented_program';
|
||||
|
||||
// The following is needed to fix a the chicken-and-egg issue where the sync (into g3) script will
|
||||
@ -80,6 +81,9 @@ export class NgTscPlugin implements TscPlugin {
|
||||
wrapHost(
|
||||
host: ts.CompilerHost&UnifiedModulesHost, inputFiles: readonly string[],
|
||||
options: ts.CompilerOptions): PluginCompilerHost {
|
||||
// TODO(alxhub): Eventually the `wrapHost()` API will accept the old `ts.Program` (if one is
|
||||
// available). When it does, its `ts.SourceFile`s need to be re-tagged to enable proper
|
||||
// incremental compilation.
|
||||
this.options = {...this.ngOptions, ...options} as NgCompilerOptions;
|
||||
this.host = NgCompilerHost.wrap(host, inputFiles, this.options, /* oldProgram */ null);
|
||||
return this.host;
|
||||
@ -92,6 +96,8 @@ export class NgTscPlugin implements TscPlugin {
|
||||
if (this.host === null || this.options === null) {
|
||||
throw new Error('Lifecycle error: setupCompilation() before wrapHost().');
|
||||
}
|
||||
this.host.postProgramCreationCleanup();
|
||||
untagAllTsFiles(program);
|
||||
const typeCheckStrategy = new ReusedProgramStrategy(
|
||||
program, this.host, this.options, this.host.shimExtensionPrefixes);
|
||||
this._compiler = new NgCompiler(
|
||||
|
@ -9,6 +9,7 @@
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {AbsoluteFsPath} from '../../file_system';
|
||||
import {retagAllTsFiles, untagAllTsFiles} from '../../shims';
|
||||
|
||||
import {TypeCheckingProgramStrategy, UpdateMode} from './api';
|
||||
import {TypeCheckProgramHost} from './host';
|
||||
@ -26,8 +27,10 @@ export class ReusedProgramStrategy implements TypeCheckingProgramStrategy {
|
||||
*/
|
||||
private sfMap = new Map<string, ts.SourceFile>();
|
||||
|
||||
private program: ts.Program = this.originalProgram;
|
||||
|
||||
constructor(
|
||||
private program: ts.Program, private originalHost: ts.CompilerHost,
|
||||
private originalProgram: ts.Program, private originalHost: ts.CompilerHost,
|
||||
private options: ts.CompilerOptions, private shimExtensionPrefixes: string[]) {}
|
||||
|
||||
getProgram(): ts.Program {
|
||||
@ -35,6 +38,17 @@ export class ReusedProgramStrategy implements TypeCheckingProgramStrategy {
|
||||
}
|
||||
|
||||
updateFiles(contents: Map<AbsoluteFsPath, string>, updateMode: UpdateMode): void {
|
||||
if (contents.size === 0) {
|
||||
// No changes have been requested. Is it safe to skip updating entirely?
|
||||
// If UpdateMode is Incremental, then yes. If UpdateMode is Complete, then it's safe to skip
|
||||
// only if there are no active changes already (that would be cleared by the update).
|
||||
|
||||
if (updateMode !== UpdateMode.Complete || this.sfMap.size === 0) {
|
||||
// No changes would be made to the `ts.Program` anyway, so it's safe to do nothing here.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateMode === UpdateMode.Complete) {
|
||||
this.sfMap.clear();
|
||||
}
|
||||
@ -43,14 +57,25 @@ export class ReusedProgramStrategy implements TypeCheckingProgramStrategy {
|
||||
this.sfMap.set(filePath, ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true));
|
||||
}
|
||||
|
||||
const host =
|
||||
new TypeCheckProgramHost(this.sfMap, this.originalHost, this.shimExtensionPrefixes);
|
||||
const host = new TypeCheckProgramHost(
|
||||
this.sfMap, this.originalProgram, this.originalHost, this.shimExtensionPrefixes);
|
||||
const oldProgram = this.program;
|
||||
|
||||
// Retag the old program's `ts.SourceFile`s with shim tags, to allow TypeScript to reuse the
|
||||
// most data.
|
||||
retagAllTsFiles(oldProgram);
|
||||
|
||||
this.program = ts.createProgram({
|
||||
host,
|
||||
rootNames: this.program.getRootFileNames(),
|
||||
options: this.options,
|
||||
oldProgram: this.program,
|
||||
oldProgram,
|
||||
});
|
||||
host.postProgramCreationCleanup();
|
||||
|
||||
// And untag them afterwards. We explicitly untag both programs here, because the oldProgram
|
||||
// may still be used for emit and needs to not contain tags.
|
||||
untagAllTsFiles(this.program);
|
||||
untagAllTsFiles(oldProgram);
|
||||
}
|
||||
}
|
||||
|
@ -35,8 +35,8 @@ export class TypeCheckProgramHost implements ts.CompilerHost {
|
||||
readonly resolveModuleNames?: ts.CompilerHost['resolveModuleNames'];
|
||||
|
||||
constructor(
|
||||
sfMap: Map<string, ts.SourceFile>, private delegate: ts.CompilerHost,
|
||||
private shimExtensionPrefixes: string[]) {
|
||||
sfMap: Map<string, ts.SourceFile>, private originalProgram: ts.Program,
|
||||
private delegate: ts.CompilerHost, private shimExtensionPrefixes: string[]) {
|
||||
this.sfMap = sfMap;
|
||||
|
||||
if (delegate.getDirectories !== undefined) {
|
||||
@ -52,8 +52,15 @@ export class TypeCheckProgramHost implements ts.CompilerHost {
|
||||
fileName: string, languageVersion: ts.ScriptTarget,
|
||||
onError?: ((message: string) => void)|undefined,
|
||||
shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined {
|
||||
const delegateSf =
|
||||
this.delegate.getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile)!;
|
||||
// Try to use the same `ts.SourceFile` as the original program, if possible. This guarantees
|
||||
// that program reuse will be as efficient as possible.
|
||||
let delegateSf: ts.SourceFile|undefined = this.originalProgram.getSourceFile(fileName);
|
||||
if (delegateSf === undefined) {
|
||||
// Something went wrong and a source file is being requested that's not in the original
|
||||
// program. Just in case, try to retrieve it from the delegate.
|
||||
delegateSf = this.delegate.getSourceFile(
|
||||
fileName, languageVersion, onError, shouldCreateNewSourceFile)!;
|
||||
}
|
||||
if (delegateSf === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ ts_library(
|
||||
"//packages/compiler-cli/src/ngtsc/imports",
|
||||
"//packages/compiler-cli/src/ngtsc/incremental",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
"//packages/compiler-cli/src/ngtsc/shims",
|
||||
"//packages/compiler-cli/src/ngtsc/testing",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck",
|
||||
"//packages/compiler-cli/src/ngtsc/util",
|
||||
|
@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
||||
import {runInEachFileSystem} from '../../file_system/testing';
|
||||
import {sfExtensionData, ShimReferenceTagger} from '../../shims';
|
||||
import {expectCompleteReuse, makeProgram} from '../../testing';
|
||||
import {UpdateMode} from '../src/api';
|
||||
import {ReusedProgramStrategy} from '../src/augmented_program';
|
||||
|
||||
import {createProgramWithNoTemplates} from './test_utils';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
describe('template type-checking program', () => {
|
||||
it('should not be created if no components need to be checked', () => {
|
||||
const {program, templateTypeChecker, programStrategy} = createProgramWithNoTemplates();
|
||||
templateTypeChecker.refresh();
|
||||
// expect() here would create a really long error message, so this is checked manually.
|
||||
if (programStrategy.getProgram() !== program) {
|
||||
fail('Template type-checking created a new ts.Program even though it had no changes.');
|
||||
}
|
||||
});
|
||||
|
||||
it('should have complete reuse if no structural changes are made to shims', () => {
|
||||
const {program, host, options, typecheckPath} = makeSingleFileProgramWithTypecheckShim();
|
||||
const programStrategy = new ReusedProgramStrategy(program, host, options, ['ngtypecheck']);
|
||||
|
||||
// Update /main.ngtypecheck.ts without changing its shape. Verify that the old program was
|
||||
// reused completely.
|
||||
programStrategy.updateFiles(
|
||||
new Map([[typecheckPath, 'export const VERSION = 2;']]), UpdateMode.Complete);
|
||||
|
||||
expectCompleteReuse(program);
|
||||
});
|
||||
|
||||
it('should have complete reuse if no structural changes are made to input files', () => {
|
||||
const {program, host, options, mainPath} = makeSingleFileProgramWithTypecheckShim();
|
||||
const programStrategy = new ReusedProgramStrategy(program, host, options, ['ngtypecheck']);
|
||||
|
||||
// Update /main.ts without changing its shape. Verify that the old program was reused
|
||||
// completely.
|
||||
programStrategy.updateFiles(
|
||||
new Map([[mainPath, 'export const STILL_NOT_A_COMPONENT = true;']]), UpdateMode.Complete);
|
||||
|
||||
expectCompleteReuse(program);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function makeSingleFileProgramWithTypecheckShim(): {
|
||||
program: ts.Program,
|
||||
host: ts.CompilerHost,
|
||||
options: ts.CompilerOptions,
|
||||
mainPath: AbsoluteFsPath,
|
||||
typecheckPath: AbsoluteFsPath,
|
||||
} {
|
||||
const mainPath = absoluteFrom('/main.ts');
|
||||
const typecheckPath = absoluteFrom('/main.ngtypecheck.ts');
|
||||
const {program, host, options} = makeProgram([
|
||||
{
|
||||
name: mainPath,
|
||||
contents: 'export const NOT_A_COMPONENT = true;',
|
||||
},
|
||||
{
|
||||
name: typecheckPath,
|
||||
contents: 'export const VERSION = 1;',
|
||||
}
|
||||
]);
|
||||
|
||||
const sf = getSourceFileOrError(program, mainPath);
|
||||
const typecheckSf = getSourceFileOrError(program, typecheckPath);
|
||||
|
||||
// To ensure this test is validating the correct behavior, the initial conditions of the
|
||||
// input program must be such that:
|
||||
//
|
||||
// 1) /main.ts was previously tagged with a reference to its ngtypecheck shim.
|
||||
// 2) /main.ngtypecheck.ts is marked as a shim itself.
|
||||
|
||||
// Condition 1:
|
||||
const tagger = new ShimReferenceTagger(['ngtypecheck']);
|
||||
tagger.tag(sf);
|
||||
tagger.finalize();
|
||||
|
||||
// Condition 2:
|
||||
sfExtensionData(typecheckSf).fileShim = {
|
||||
extension: 'ngtypecheck',
|
||||
generatedFrom: mainPath,
|
||||
};
|
||||
|
||||
return {program, host, options, mainPath, typecheckPath};
|
||||
}
|
@ -235,10 +235,18 @@ export function tcb(
|
||||
return res.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
export function typecheck(
|
||||
template: string, source: string, declarations: TestDeclaration[] = [],
|
||||
additionalSources: {name: AbsoluteFsPath; contents: string}[] = [],
|
||||
config: Partial<TypeCheckingConfig> = {}, opts: ts.CompilerOptions = {}): ts.Diagnostic[] {
|
||||
export interface TemplateTestEnvironment {
|
||||
sf: ts.SourceFile;
|
||||
program: ts.Program;
|
||||
templateTypeChecker: TemplateTypeChecker;
|
||||
programStrategy: ReusedProgramStrategy;
|
||||
}
|
||||
|
||||
function setupTemplateTypeChecking(
|
||||
source: string, additionalSources: {name: AbsoluteFsPath; contents: string}[],
|
||||
config: Partial<TypeCheckingConfig>, opts: ts.CompilerOptions,
|
||||
makeTypeCheckAdapterFn: (program: ts.Program, sf: ts.SourceFile) =>
|
||||
ProgramTypeCheckAdapter): TemplateTestEnvironment {
|
||||
const typeCheckFilePath = absoluteFrom('/main.ngtypecheck.ts');
|
||||
const files = [
|
||||
typescriptLibDts(),
|
||||
@ -266,48 +274,65 @@ export function typecheck(
|
||||
]);
|
||||
const fullConfig = {...ALL_ENABLED_CONFIG, ...config};
|
||||
|
||||
const templateUrl = 'synthetic.html';
|
||||
const templateFile = new ParseSourceFile(template, templateUrl);
|
||||
const {nodes, errors} = parseTemplate(template, templateUrl);
|
||||
if (errors !== undefined) {
|
||||
throw new Error('Template parse errors: \n' + errors.join('\n'));
|
||||
}
|
||||
|
||||
const {matcher, pipes} = prepareDeclarations(declarations, decl => {
|
||||
let declFile = sf;
|
||||
if (decl.file !== undefined) {
|
||||
declFile = program.getSourceFile(decl.file)!;
|
||||
if (declFile === undefined) {
|
||||
throw new Error(`Unable to locate ${decl.file} for ${decl.type} ${decl.name}`);
|
||||
}
|
||||
}
|
||||
return getClass(declFile, decl.name);
|
||||
});
|
||||
const binder = new R3TargetBinder(matcher);
|
||||
const boundTarget = binder.bind({template: nodes});
|
||||
const clazz = new Reference(getClass(sf, 'TestComponent'));
|
||||
|
||||
const sourceMapping: TemplateSourceMapping = {
|
||||
type: 'external',
|
||||
template,
|
||||
templateUrl,
|
||||
componentClass: clazz.node,
|
||||
// Use the class's name for error mappings.
|
||||
node: clazz.node.name,
|
||||
};
|
||||
|
||||
const checkAdapter = createTypeCheckAdapter((ctx: TypeCheckContext) => {
|
||||
ctx.addTemplate(clazz, boundTarget, pipes, [], sourceMapping, templateFile);
|
||||
});
|
||||
|
||||
const checkAdapter = makeTypeCheckAdapterFn(program, sf);
|
||||
const programStrategy = new ReusedProgramStrategy(program, host, options, []);
|
||||
const templateTypeChecker = new TemplateTypeChecker(
|
||||
program, programStrategy, checkAdapter, fullConfig, emitter, reflectionHost, host,
|
||||
NOOP_INCREMENTAL_BUILD);
|
||||
|
||||
return {program, sf, templateTypeChecker, programStrategy};
|
||||
}
|
||||
|
||||
export function typecheck(
|
||||
template: string, source: string, declarations: TestDeclaration[] = [],
|
||||
additionalSources: {name: AbsoluteFsPath; contents: string}[] = [],
|
||||
config: Partial<TypeCheckingConfig> = {}, opts: ts.CompilerOptions = {}): ts.Diagnostic[] {
|
||||
const {sf, templateTypeChecker} =
|
||||
setupTemplateTypeChecking(source, additionalSources, config, opts, (program, sf) => {
|
||||
const templateUrl = 'synthetic.html';
|
||||
const templateFile = new ParseSourceFile(template, templateUrl);
|
||||
const {nodes, errors} = parseTemplate(template, templateUrl);
|
||||
if (errors !== undefined) {
|
||||
throw new Error('Template parse errors: \n' + errors.join('\n'));
|
||||
}
|
||||
|
||||
const {matcher, pipes} = prepareDeclarations(declarations, decl => {
|
||||
let declFile = sf;
|
||||
if (decl.file !== undefined) {
|
||||
declFile = program.getSourceFile(decl.file)!;
|
||||
if (declFile === undefined) {
|
||||
throw new Error(`Unable to locate ${decl.file} for ${decl.type} ${decl.name}`);
|
||||
}
|
||||
}
|
||||
return getClass(declFile, decl.name);
|
||||
});
|
||||
const binder = new R3TargetBinder(matcher);
|
||||
const boundTarget = binder.bind({template: nodes});
|
||||
const clazz = new Reference(getClass(sf, 'TestComponent'));
|
||||
|
||||
const sourceMapping: TemplateSourceMapping = {
|
||||
type: 'external',
|
||||
template,
|
||||
templateUrl,
|
||||
componentClass: clazz.node,
|
||||
// Use the class's name for error mappings.
|
||||
node: clazz.node.name,
|
||||
};
|
||||
|
||||
return createTypeCheckAdapter((ctx: TypeCheckContext) => {
|
||||
ctx.addTemplate(clazz, boundTarget, pipes, [], sourceMapping, templateFile);
|
||||
});
|
||||
});
|
||||
|
||||
templateTypeChecker.refresh();
|
||||
return templateTypeChecker.getDiagnosticsForFile(sf);
|
||||
}
|
||||
|
||||
export function createProgramWithNoTemplates(): TemplateTestEnvironment {
|
||||
return setupTemplateTypeChecking(
|
||||
'export const NOT_A_COMPONENT = true;', [], {}, {}, () => createTypeCheckAdapter(() => {}));
|
||||
}
|
||||
|
||||
function createTypeCheckAdapter(fn: (ctx: TypeCheckContext) => void): ProgramTypeCheckAdapter {
|
||||
let called = false;
|
||||
return {
|
||||
|
@ -125,10 +125,11 @@ export function resolveModuleName(
|
||||
compilerHost: ts.ModuleResolutionHost&Pick<ts.CompilerHost, 'resolveModuleNames'>,
|
||||
moduleResolutionCache: ts.ModuleResolutionCache|null): ts.ResolvedModule|undefined {
|
||||
if (compilerHost.resolveModuleNames) {
|
||||
// FIXME: Additional parameters are required in TS3.6, but ignored in 3.5.
|
||||
// Remove the any cast once google3 is fully on TS3.6.
|
||||
return (compilerHost as any)
|
||||
.resolveModuleNames([moduleName], containingFile, undefined, undefined, compilerOptions)[0];
|
||||
return compilerHost.resolveModuleNames(
|
||||
[moduleName], containingFile,
|
||||
undefined, // reusedNames
|
||||
undefined, // redirectedReference
|
||||
compilerOptions)[0];
|
||||
} else {
|
||||
return ts
|
||||
.resolveModuleName(
|
||||
|
@ -12,6 +12,7 @@ ts_library(
|
||||
"//packages/compiler-cli/src/ngtsc/file_system/testing",
|
||||
"//packages/compiler-cli/src/ngtsc/indexer",
|
||||
"//packages/compiler-cli/src/ngtsc/routing",
|
||||
"//packages/compiler-cli/src/ngtsc/testing",
|
||||
"//packages/compiler-cli/src/ngtsc/util",
|
||||
"//packages/compiler-cli/test:test_utils",
|
||||
"//packages/compiler-cli/test/helpers",
|
||||
|
@ -128,6 +128,13 @@ export class NgtscTestEnvironment {
|
||||
return this.oldProgram.getTsProgram();
|
||||
}
|
||||
|
||||
getReuseTsProgram(): ts.Program {
|
||||
if (this.oldProgram === null) {
|
||||
throw new Error('No ts.Program has been created yet.');
|
||||
}
|
||||
return (this.oldProgram as NgtscProgram).getReuseTsProgram();
|
||||
}
|
||||
|
||||
/**
|
||||
* Older versions of the CLI do not provide the `CompilerHost.getModifiedResourceFiles()` method.
|
||||
* This results in the `changedResources` set being `null`.
|
||||
|
@ -9,8 +9,9 @@
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ErrorCode, ngErrorCode} from '../../src/ngtsc/diagnostics';
|
||||
import {absoluteFrom as _, getFileSystem} from '../../src/ngtsc/file_system';
|
||||
import {absoluteFrom as _, getFileSystem, getSourceFileOrError} from '../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
|
||||
import {expectCompleteReuse} from '../../src/ngtsc/testing';
|
||||
import {loadStandardTestFiles} from '../helpers/src/mock_file_loading';
|
||||
|
||||
import {NgtscTestEnvironment} from './env';
|
||||
@ -1862,18 +1863,26 @@ export declare class AnimationEvent {
|
||||
expect(env.driveDiagnostics()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not leave references to shims after execution', () => {
|
||||
// This test verifies that proper cleanup is performed for the technique being used to
|
||||
// include shim files in the ts.Program, and that none are left in the referencedFiles of
|
||||
// any ts.SourceFile after compilation.
|
||||
it('should not leave referencedFiles in a tagged state', () => {
|
||||
env.enableMultipleCompilations();
|
||||
|
||||
env.driveMain();
|
||||
for (const sf of env.getTsProgram().getSourceFiles()) {
|
||||
for (const ref of sf.referencedFiles) {
|
||||
expect(ref.fileName).not.toContain('.ngtypecheck.ts');
|
||||
}
|
||||
}
|
||||
const sf = getSourceFileOrError(env.getTsProgram(), _('/test.ts'));
|
||||
expect(sf.referencedFiles.map(ref => ref.fileName)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should allow for complete program reuse during incremental compilations', () => {
|
||||
env.enableMultipleCompilations();
|
||||
|
||||
env.write('other.ts', `export const VERSION = 1;`);
|
||||
|
||||
env.driveMain();
|
||||
const firstProgram = env.getReuseTsProgram();
|
||||
|
||||
env.write('other.ts', `export const VERSION = 2;`);
|
||||
env.driveMain();
|
||||
|
||||
expectCompleteReuse(firstProgram);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -43,20 +43,27 @@ const DIRECTIVE_LIFECYCLE_HOOKS = new Set([
|
||||
const AMBIGUOUS_LIFECYCLE_HOOKS = new Set(['ngOnDestroy']);
|
||||
|
||||
/** Describes how a given class is used in the context of Angular. */
|
||||
enum ClassKind {
|
||||
enum InferredKind {
|
||||
DIRECTIVE,
|
||||
AMBIGUOUS,
|
||||
UNKNOWN,
|
||||
}
|
||||
|
||||
/** Describes possible types of Angular declarations. */
|
||||
enum DeclarationType {
|
||||
DIRECTIVE,
|
||||
COMPONENT,
|
||||
ABSTRACT_DIRECTIVE,
|
||||
PIPE,
|
||||
INJECTABLE,
|
||||
}
|
||||
|
||||
/** Analyzed class declaration. */
|
||||
interface AnalyzedClass {
|
||||
/** Whether the class is decorated with @Directive or @Component. */
|
||||
isDirectiveOrComponent: boolean;
|
||||
/** Whether the class is an abstract directive. */
|
||||
isAbstractDirective: boolean;
|
||||
/** Kind of the given class in terms of Angular. */
|
||||
kind: ClassKind;
|
||||
/** Type of declaration that is determined through an applied decorator. */
|
||||
decoratedType: DeclarationType|null;
|
||||
/** Inferred class kind in terms of Angular. */
|
||||
inferredKind: InferredKind;
|
||||
}
|
||||
|
||||
interface AnalysisFailure {
|
||||
@ -64,6 +71,9 @@ interface AnalysisFailure {
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** TODO message that is added to ambiguous classes using Angular features. */
|
||||
const AMBIGUOUS_CLASS_TODO = 'Add Angular decorator.';
|
||||
|
||||
export class UndecoratedClassesWithDecoratedFieldsTransform {
|
||||
private printer = ts.createPrinter();
|
||||
private importManager = new ImportManager(this.getUpdateRecorder, this.printer);
|
||||
@ -81,10 +91,10 @@ export class UndecoratedClassesWithDecoratedFieldsTransform {
|
||||
* indicating that a given class uses Angular features. https://hackmd.io/vuQfavzfRG6KUCtU7oK_EA
|
||||
*/
|
||||
migrate(sourceFiles: ts.SourceFile[]): AnalysisFailure[] {
|
||||
const {result, ambiguous} = this._findUndecoratedAbstractDirectives(sourceFiles);
|
||||
const {detectedAbstractDirectives, ambiguousClasses} =
|
||||
this._findUndecoratedAbstractDirectives(sourceFiles);
|
||||
|
||||
|
||||
result.forEach(node => {
|
||||
detectedAbstractDirectives.forEach(node => {
|
||||
const sourceFile = node.getSourceFile();
|
||||
const recorder = this.getUpdateRecorder(sourceFile);
|
||||
const directiveExpr =
|
||||
@ -98,12 +108,19 @@ export class UndecoratedClassesWithDecoratedFieldsTransform {
|
||||
// determine whether the class is used as directive, service or pipe. The migration
|
||||
// could potentially determine the type by checking NgModule definitions or inheritance
|
||||
// of other known declarations, but this is out of scope and a TODO/failure is sufficient.
|
||||
return Array.from(ambiguous).reduce((failures, node) => {
|
||||
return Array.from(ambiguousClasses).reduce((failures, node) => {
|
||||
// If the class has been reported as ambiguous before, skip adding a TODO and
|
||||
// printing an error. A class could be visited multiple times when it's part
|
||||
// of multiple build targets in the CLI project.
|
||||
if (this._hasBeenReportedAsAmbiguous(node)) {
|
||||
return failures;
|
||||
}
|
||||
|
||||
const sourceFile = node.getSourceFile();
|
||||
const recorder = this.getUpdateRecorder(sourceFile);
|
||||
|
||||
// Add a TODO to the class that uses Angular features but is not decorated.
|
||||
recorder.addClassTodo(node, `Add Angular decorator.`);
|
||||
recorder.addClassTodo(node, AMBIGUOUS_CLASS_TODO);
|
||||
|
||||
// Add an error for the class that will be printed in the `ng update` output.
|
||||
return failures.concat({
|
||||
@ -125,59 +142,83 @@ export class UndecoratedClassesWithDecoratedFieldsTransform {
|
||||
* directives. Those are ambiguous and could be either Directive, Pipe or service.
|
||||
*/
|
||||
private _findUndecoratedAbstractDirectives(sourceFiles: ts.SourceFile[]) {
|
||||
const result = new Set<ts.ClassDeclaration>();
|
||||
const ambiguousClasses = new Set<ts.ClassDeclaration>();
|
||||
const declarations = new WeakMap<ts.ClassDeclaration, DeclarationType>();
|
||||
const detectedAbstractDirectives = new Set<ts.ClassDeclaration>();
|
||||
const undecoratedClasses = new Set<ts.ClassDeclaration>();
|
||||
const nonAbstractDirectives = new WeakSet<ts.ClassDeclaration>();
|
||||
const abstractDirectives = new WeakSet<ts.ClassDeclaration>();
|
||||
const ambiguous = new Set<ts.ClassDeclaration>();
|
||||
|
||||
const visitNode = (node: ts.Node) => {
|
||||
node.forEachChild(visitNode);
|
||||
if (!ts.isClassDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
const {isDirectiveOrComponent, isAbstractDirective, kind} =
|
||||
this._analyzeClassDeclaration(node);
|
||||
if (isDirectiveOrComponent) {
|
||||
if (isAbstractDirective) {
|
||||
abstractDirectives.add(node);
|
||||
} else {
|
||||
nonAbstractDirectives.add(node);
|
||||
}
|
||||
} else if (kind === ClassKind.DIRECTIVE) {
|
||||
abstractDirectives.add(node);
|
||||
result.add(node);
|
||||
const {inferredKind, decoratedType} = this._analyzeClassDeclaration(node);
|
||||
|
||||
if (decoratedType !== null) {
|
||||
declarations.set(node, decoratedType);
|
||||
return;
|
||||
}
|
||||
|
||||
if (inferredKind === InferredKind.DIRECTIVE) {
|
||||
detectedAbstractDirectives.add(node);
|
||||
} else if (inferredKind === InferredKind.AMBIGUOUS) {
|
||||
ambiguousClasses.add(node);
|
||||
} else {
|
||||
if (kind === ClassKind.AMBIGUOUS) {
|
||||
ambiguous.add(node);
|
||||
}
|
||||
undecoratedClasses.add(node);
|
||||
}
|
||||
};
|
||||
|
||||
sourceFiles.forEach(sourceFile => sourceFile.forEachChild(visitNode));
|
||||
|
||||
// We collected all undecorated class declarations which inherit from abstract directives.
|
||||
// For such abstract directives, the derived classes also need to be migrated.
|
||||
undecoratedClasses.forEach(node => {
|
||||
for (const {node: baseClass} of findBaseClassDeclarations(node, this.typeChecker)) {
|
||||
// If the undecorated class inherits from a non-abstract directive, skip the current
|
||||
// class. We do this because undecorated classes which inherit metadata from non-abstract
|
||||
// directives are handled in the `undecorated-classes-with-di` migration that copies
|
||||
// inherited metadata into an explicit decorator.
|
||||
if (nonAbstractDirectives.has(baseClass)) {
|
||||
break;
|
||||
} else if (abstractDirectives.has(baseClass)) {
|
||||
result.add(node);
|
||||
// In case the undecorated class previously could not be detected as directive,
|
||||
// remove it from the ambiguous set as we now know that it's a guaranteed directive.
|
||||
ambiguous.delete(node);
|
||||
/**
|
||||
* Checks the inheritance of the given set of classes. It removes classes from the
|
||||
* detected abstract directives set when they inherit from a non-abstract Angular
|
||||
* declaration. e.g. an abstract directive can never extend from a component.
|
||||
*
|
||||
* If a class inherits from an abstract directive though, we will migrate them too
|
||||
* as derived classes also need to be decorated. This has been done for a simpler mental
|
||||
* model and reduced complexity in the Angular compiler. See migration plan document.
|
||||
*/
|
||||
const checkInheritanceOfClasses = (classes: Set<ts.ClassDeclaration>) => {
|
||||
classes.forEach(node => {
|
||||
for (const {node: baseClass} of findBaseClassDeclarations(node, this.typeChecker)) {
|
||||
if (!declarations.has(baseClass)) {
|
||||
continue;
|
||||
}
|
||||
// If the undecorated class inherits from an abstract directive, always migrate it.
|
||||
// Derived undecorated classes of abstract directives are always also considered
|
||||
// abstract directives and need to be decorated too. This is necessary as otherwise
|
||||
// the inheritance chain cannot be resolved by the Angular compiler. e.g. when it
|
||||
// flattens directive metadata for type checking. In the other case, we never want
|
||||
// to migrate a class if it extends from a non-abstract Angular declaration. That
|
||||
// is an unsupported pattern as of v9 and was previously handled with the
|
||||
// `undecorated-classes-with-di` migration (which copied the inherited decorator).
|
||||
if (declarations.get(baseClass) === DeclarationType.ABSTRACT_DIRECTIVE) {
|
||||
detectedAbstractDirectives.add(node);
|
||||
} else {
|
||||
detectedAbstractDirectives.delete(node);
|
||||
}
|
||||
ambiguousClasses.delete(node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {result, ambiguous};
|
||||
// Check inheritance of any detected abstract directive. We want to remove
|
||||
// classes that are not eligible abstract directives due to inheritance. i.e.
|
||||
// if a class extends from a component, it cannot be a derived abstract directive.
|
||||
checkInheritanceOfClasses(detectedAbstractDirectives);
|
||||
// Update the class declarations to reflect the detected abstract directives. This is
|
||||
// then used later when we check for undecorated classes that inherit from an abstract
|
||||
// directive and need to be decorated.
|
||||
detectedAbstractDirectives.forEach(
|
||||
n => declarations.set(n, DeclarationType.ABSTRACT_DIRECTIVE));
|
||||
// Check ambiguous and undecorated classes if they inherit from an abstract directive.
|
||||
// If they do, we want to migrate them too. See function definition for more details.
|
||||
checkInheritanceOfClasses(ambiguousClasses);
|
||||
checkInheritanceOfClasses(undecoratedClasses);
|
||||
|
||||
return {detectedAbstractDirectives, ambiguousClasses};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -186,19 +227,30 @@ export class UndecoratedClassesWithDecoratedFieldsTransform {
|
||||
*/
|
||||
private _analyzeClassDeclaration(node: ts.ClassDeclaration): AnalyzedClass {
|
||||
const ngDecorators = node.decorators && getAngularDecorators(this.typeChecker, node.decorators);
|
||||
const kind = this._determineClassKind(node);
|
||||
const inferredKind = this._determineClassKind(node);
|
||||
if (ngDecorators === undefined || ngDecorators.length === 0) {
|
||||
return {isDirectiveOrComponent: false, isAbstractDirective: false, kind};
|
||||
return {decoratedType: null, inferredKind};
|
||||
}
|
||||
const directiveDecorator = ngDecorators.find(({name}) => name === 'Directive');
|
||||
const componentDecorator = ngDecorators.find(({name}) => name === 'Component');
|
||||
const pipeDecorator = ngDecorators.find(({name}) => name === 'Pipe');
|
||||
const injectableDecorator = ngDecorators.find(({name}) => name === 'Injectable');
|
||||
const isAbstractDirective =
|
||||
directiveDecorator !== undefined && this._isAbstractDirective(directiveDecorator);
|
||||
return {
|
||||
isDirectiveOrComponent: !!directiveDecorator || !!componentDecorator,
|
||||
isAbstractDirective,
|
||||
kind,
|
||||
};
|
||||
|
||||
let decoratedType: DeclarationType|null = null;
|
||||
if (isAbstractDirective) {
|
||||
decoratedType = DeclarationType.ABSTRACT_DIRECTIVE;
|
||||
} else if (componentDecorator !== undefined) {
|
||||
decoratedType = DeclarationType.COMPONENT;
|
||||
} else if (directiveDecorator !== undefined) {
|
||||
decoratedType = DeclarationType.DIRECTIVE;
|
||||
} else if (pipeDecorator !== undefined) {
|
||||
decoratedType = DeclarationType.PIPE;
|
||||
} else if (injectableDecorator !== undefined) {
|
||||
decoratedType = DeclarationType.INJECTABLE;
|
||||
}
|
||||
return {decoratedType, inferredKind};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -228,8 +280,8 @@ export class UndecoratedClassesWithDecoratedFieldsTransform {
|
||||
* e.g. lifecycle hooks or decorated members like `@Input` or `@Output` are
|
||||
* considered Angular features..
|
||||
*/
|
||||
private _determineClassKind(node: ts.ClassDeclaration): ClassKind {
|
||||
let usage = ClassKind.UNKNOWN;
|
||||
private _determineClassKind(node: ts.ClassDeclaration): InferredKind {
|
||||
let usage = InferredKind.UNKNOWN;
|
||||
|
||||
for (const member of node.members) {
|
||||
const propertyName = member.name !== undefined ? getPropertyNameText(member.name) : null;
|
||||
@ -237,7 +289,7 @@ export class UndecoratedClassesWithDecoratedFieldsTransform {
|
||||
// If the class declares any of the known directive lifecycle hooks, we can
|
||||
// immediately exit the loop as the class is guaranteed to be a directive.
|
||||
if (propertyName !== null && DIRECTIVE_LIFECYCLE_HOOKS.has(propertyName)) {
|
||||
return ClassKind.DIRECTIVE;
|
||||
return InferredKind.DIRECTIVE;
|
||||
}
|
||||
|
||||
const ngDecorators = member.decorators !== undefined ?
|
||||
@ -245,7 +297,7 @@ export class UndecoratedClassesWithDecoratedFieldsTransform {
|
||||
[];
|
||||
for (const {name} of ngDecorators) {
|
||||
if (DIRECTIVE_FIELD_DECORATORS.has(name)) {
|
||||
return ClassKind.DIRECTIVE;
|
||||
return InferredKind.DIRECTIVE;
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,10 +305,27 @@ export class UndecoratedClassesWithDecoratedFieldsTransform {
|
||||
// the given class is a directive, update the kind and continue looking for other
|
||||
// members that would unveil a more specific kind (i.e. being a directive).
|
||||
if (propertyName !== null && AMBIGUOUS_LIFECYCLE_HOOKS.has(propertyName)) {
|
||||
usage = ClassKind.AMBIGUOUS;
|
||||
usage = InferredKind.AMBIGUOUS;
|
||||
}
|
||||
}
|
||||
|
||||
return usage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given class has been reported as ambiguous in previous
|
||||
* migration run. e.g. when build targets are migrated first, and then test
|
||||
* targets that have an overlap with build source files, the same class
|
||||
* could be detected as ambiguous.
|
||||
*/
|
||||
private _hasBeenReportedAsAmbiguous(node: ts.ClassDeclaration): boolean {
|
||||
const sourceFile = node.getSourceFile();
|
||||
const leadingComments = ts.getLeadingCommentRanges(sourceFile.text, node.pos);
|
||||
if (leadingComments === undefined) {
|
||||
return false;
|
||||
}
|
||||
return leadingComments.some(
|
||||
({kind, pos, end}) => kind === ts.SyntaxKind.SingleLineCommentTrivia &&
|
||||
sourceFile.text.substring(pos, end).includes(`TODO: ${AMBIGUOUS_CLASS_TODO}`));
|
||||
}
|
||||
}
|
||||
|
@ -136,24 +136,36 @@ describe('Google3 undecorated classes with decorated fields TSLint rule', () =>
|
||||
|
||||
it('should not change decorated classes', () => {
|
||||
writeFile('/index.ts', `
|
||||
import { Input, Component, Output, EventEmitter } from '@angular/core';
|
||||
import { Input, Component, Directive, Pipe, Injectable } from '@angular/core';
|
||||
|
||||
@Component({})
|
||||
export class Base {
|
||||
export class MyComp {
|
||||
@Input() isActive: boolean;
|
||||
}
|
||||
|
||||
@Directive({selector: 'dir'})
|
||||
export class MyDir {
|
||||
@Input() isActive: boolean;
|
||||
}
|
||||
|
||||
export class Child extends Base {
|
||||
@Output() clicked = new EventEmitter<void>();
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
|
||||
@Pipe({name: 'my-pipe'})
|
||||
export class MyPipe {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
|
||||
runTSLint(true);
|
||||
const content = getFile('/index.ts');
|
||||
expect(content).toContain(
|
||||
`import { Input, Component, Output, EventEmitter, Directive } from '@angular/core';`);
|
||||
expect(content).toContain(`@Component({})\n export class Base {`);
|
||||
expect(content).toContain(`@Directive()\nexport class Child extends Base {`);
|
||||
expect(content).toMatch(/@Component\({}\)\s+export class MyComp {/);
|
||||
expect(content).toMatch(/@Directive\({selector: 'dir'}\)\s+export class MyDir {/);
|
||||
expect(content).toMatch(/@Injectable\(\)\s+export class MyService {/);
|
||||
expect(content).toMatch(/@Pipe\({name: 'my-pipe'}\)\s+export class MyPipe {/);
|
||||
expect(content).not.toContain('TODO');
|
||||
});
|
||||
|
||||
it('should add @Directive to undecorated classes that have @Output', () => {
|
||||
|
@ -9,6 +9,7 @@
|
||||
/**
|
||||
* Template string function that can be used to dedent the resulting
|
||||
* string literal. The smallest common indentation will be omitted.
|
||||
* Additionally, whitespace in empty lines is removed.
|
||||
*/
|
||||
export function dedent(strings: TemplateStringsArray, ...values: any[]) {
|
||||
let joinedString = '';
|
||||
@ -24,5 +25,7 @@ export function dedent(strings: TemplateStringsArray, ...values: any[]) {
|
||||
|
||||
const minLineIndent = Math.min(...matches.map(el => el.length));
|
||||
const omitMinIndentRegex = new RegExp(`^[ \\t]{${minLineIndent}}`, 'gm');
|
||||
return minLineIndent > 0 ? joinedString.replace(omitMinIndentRegex, '') : joinedString;
|
||||
const omitEmptyLineWhitespaceRegex = /^[ \t]+$/gm;
|
||||
const result = minLineIndent > 0 ? joinedString.replace(omitMinIndentRegex, '') : joinedString;
|
||||
return result.replace(omitEmptyLineWhitespaceRegex, '');
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing';
|
||||
import {HostTree} from '@angular-devkit/schematics';
|
||||
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
|
||||
import * as shx from 'shelljs';
|
||||
import {dedent} from './helpers';
|
||||
|
||||
describe('Undecorated classes with decorated fields migration', () => {
|
||||
let runner: SchematicTestRunner;
|
||||
@ -117,26 +118,253 @@ describe('Undecorated classes with decorated fields migration', () => {
|
||||
expect(tree.readContent('/index.ts')).toContain(`@Directive()\nexport class Base {`);
|
||||
});
|
||||
|
||||
it('should not change decorated classes', async () => {
|
||||
writeFile('/index.ts', `
|
||||
import { Input, Component, Output, EventEmitter } from '@angular/core';
|
||||
it('should not migrate classes decorated with @Component', async () => {
|
||||
writeFile('/index.ts', dedent`
|
||||
import {Input, Component} from '@angular/core';
|
||||
|
||||
@Component({})
|
||||
@Component({selector: 'hello', template: 'hello'})
|
||||
export class Base {
|
||||
@Input() isActive: boolean;
|
||||
}
|
||||
|
||||
export class Child extends Base {
|
||||
@Output() clicked = new EventEmitter<void>();
|
||||
|
||||
@Component({selector: 'hello', template: 'hello'})
|
||||
export class Derived extends Base {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
const content = tree.readContent('/index.ts');
|
||||
expect(content).toContain(
|
||||
`import { Input, Component, Output, EventEmitter, Directive } from '@angular/core';`);
|
||||
expect(content).toContain(`@Component({})\n export class Base {`);
|
||||
expect(content).toContain(`@Directive()\nexport class Child extends Base {`);
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
import {Input, Component} from '@angular/core';
|
||||
|
||||
@Component({selector: 'hello', template: 'hello'})
|
||||
export class Base {
|
||||
@Input() isActive: boolean;
|
||||
}
|
||||
|
||||
@Component({selector: 'hello', template: 'hello'})
|
||||
export class Derived extends Base {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not migrate classes decorated with @Directive', async () => {
|
||||
writeFile('/index.ts', dedent`
|
||||
import {Input, Directive} from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export class Base {
|
||||
@Input() isActive: boolean;
|
||||
}
|
||||
|
||||
@Directive({selector: 'other'})
|
||||
export class Other extends Base {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
import {Input, Directive} from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export class Base {
|
||||
@Input() isActive: boolean;
|
||||
}
|
||||
|
||||
@Directive({selector: 'other'})
|
||||
export class Other extends Base {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not migrate when class inherits from component', async () => {
|
||||
writeFile('/index.ts', dedent`
|
||||
import {Input, Component} from '@angular/core';
|
||||
|
||||
@Component({selector: 'my-comp', template: 'my-comp'})
|
||||
export class MyComp {}
|
||||
|
||||
export class WithDisabled extends MyComp {
|
||||
@Input() disabled: boolean;
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
import {Input, Component} from '@angular/core';
|
||||
|
||||
@Component({selector: 'my-comp', template: 'my-comp'})
|
||||
export class MyComp {}
|
||||
|
||||
export class WithDisabled extends MyComp {
|
||||
@Input() disabled: boolean;
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not migrate when class inherits from pipe', async () => {
|
||||
writeFile('/index.ts', dedent`
|
||||
import {Pipe} from '@angular/core';
|
||||
|
||||
@Pipe({name: 'my-pipe'})
|
||||
export class MyPipe {}
|
||||
|
||||
export class PipeDerived extends MyPipe {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
import {Pipe} from '@angular/core';
|
||||
|
||||
@Pipe({name: 'my-pipe'})
|
||||
export class MyPipe {}
|
||||
|
||||
export class PipeDerived extends MyPipe {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not migrate when class inherits from injectable', async () => {
|
||||
writeFile('/index.ts', dedent`
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class MyService {}
|
||||
|
||||
export class ServiceDerived extends MyService {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class MyService {}
|
||||
|
||||
export class ServiceDerived extends MyService {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not migrate when class inherits from directive', async () => {
|
||||
writeFile('/index.ts', dedent`
|
||||
import {Directive} from '@angular/core';
|
||||
|
||||
@Directive({selector: 'hello'})
|
||||
export class MyDir {}
|
||||
|
||||
export class DirDerived extends MyDir {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
import {Directive} from '@angular/core';
|
||||
|
||||
@Directive({selector: 'hello'})
|
||||
export class MyDir {}
|
||||
|
||||
export class DirDerived extends MyDir {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not add multiple TODOs for ambiguous classes', async () => {
|
||||
writeFile('/angular.json', JSON.stringify({
|
||||
projects: {
|
||||
test: {
|
||||
architect: {
|
||||
build: {options: {tsConfig: './tsconfig.json'}},
|
||||
test: {options: {tsConfig: './tsconfig.json'}},
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
writeFile('/index.ts', dedent`
|
||||
export class MyService {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
// TODO: Add Angular decorator.
|
||||
export class MyService {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not report pipe using `ngOnDestroy` as ambiguous', async () => {
|
||||
writeFile('/index.ts', dedent`
|
||||
import {Pipe} from '@angular/core';
|
||||
|
||||
@Pipe({name: 'my-pipe'})
|
||||
export class MyPipe {
|
||||
ngOnDestroy() {}
|
||||
transform() {}
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
import {Pipe} from '@angular/core';
|
||||
|
||||
@Pipe({name: 'my-pipe'})
|
||||
export class MyPipe {
|
||||
ngOnDestroy() {}
|
||||
transform() {}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not report injectable using `ngOnDestroy` as ambiguous', async () => {
|
||||
writeFile('/index.ts', dedent`
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class MyService {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class MyService {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should add @Directive to undecorated classes that have @Output', async () => {
|
||||
@ -298,6 +526,8 @@ describe('Undecorated classes with decorated fields migration', () => {
|
||||
|
||||
await runMigration();
|
||||
const fileContent = tree.readContent('/index.ts');
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(fileContent).toContain(`import { Input, Directive, NgModule } from '@angular/core';`);
|
||||
expect(fileContent).toMatch(/@Directive\(\)\s+export class Base/);
|
||||
expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedA/);
|
||||
@ -305,6 +535,7 @@ describe('Undecorated classes with decorated fields migration', () => {
|
||||
expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedC/);
|
||||
expect(fileContent).toMatch(/}\s+@Directive\(\{selector: 'my-comp'}\)\s+export class MyComp/);
|
||||
expect(fileContent).toMatch(/}\s+export class MyCompWrapped/);
|
||||
expect(fileContent).not.toContain('TODO: Add Angular decorator');
|
||||
});
|
||||
|
||||
it('should add @Directive to derived undecorated classes of abstract directives', async () => {
|
||||
|
@ -70,8 +70,8 @@ export interface Injectable {
|
||||
* - 'root' : The application-level injector in most apps.
|
||||
* - 'platform' : A special singleton platform injector shared by all
|
||||
* applications on the page.
|
||||
* - 'any' : Provides a unique instance in every module (including lazy modules) that injects the
|
||||
* token.
|
||||
* - 'any' : Provides a unique instance in each lazy loaded module while all eagerly loaded
|
||||
* modules share one instance.
|
||||
*
|
||||
*/
|
||||
providedIn?: Type<any>|'root'|'platform'|'any'|null;
|
||||
|
@ -155,14 +155,6 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
|
||||
|
||||
const rootFlags = this.componentDef.onPush ? LViewFlags.Dirty | LViewFlags.IsRoot :
|
||||
LViewFlags.CheckAlways | LViewFlags.IsRoot;
|
||||
|
||||
// Check whether this Component needs to be isolated from other components, i.e. whether it
|
||||
// should be placed into its own (empty) root context or existing root context should be used.
|
||||
// Note: this is internal-only convention and might change in the future, so it should not be
|
||||
// relied upon externally.
|
||||
const isIsolated = typeof rootSelectorOrNode === 'string' &&
|
||||
/^#root-ng-internal-isolated-\d+/.test(rootSelectorOrNode);
|
||||
|
||||
const rootContext = createRootContext();
|
||||
|
||||
// Create the root view. Uses empty TView and ContentTemplate.
|
||||
@ -232,12 +224,10 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
|
||||
this.componentType, component,
|
||||
createElementRef(viewEngine_ElementRef, tElementNode, rootLView), rootLView, tElementNode);
|
||||
|
||||
if (!rootSelectorOrNode || isIsolated) {
|
||||
// The host element of the internal or isolated root view is attached to the component's host
|
||||
// view node.
|
||||
ngDevMode && assertNodeOfPossibleTypes(rootTView.node, TNodeType.View);
|
||||
rootTView.node!.child = tElementNode;
|
||||
}
|
||||
// The host element of the internal root view is attached to the component's host view node.
|
||||
ngDevMode && assertNodeOfPossibleTypes(rootTView.node, [TNodeType.View]);
|
||||
rootTView.node!.child = tElementNode;
|
||||
|
||||
return componentRef;
|
||||
}
|
||||
}
|
||||
|
@ -99,8 +99,12 @@ let nextNgElementId = 0;
|
||||
export function bloomAdd(
|
||||
injectorIndex: number, tView: TView, type: Type<any>|InjectionToken<any>|string): void {
|
||||
ngDevMode && assertEqual(tView.firstCreatePass, true, 'expected firstCreatePass to be true');
|
||||
let id: number|undefined =
|
||||
typeof type !== 'string' ? (type as any)[NG_ELEMENT_ID] : type.charCodeAt(0) || 0;
|
||||
let id: number|undefined;
|
||||
if (typeof type === 'string') {
|
||||
id = type.charCodeAt(0) || 0;
|
||||
} else if (type.hasOwnProperty(NG_ELEMENT_ID)) {
|
||||
id = (type as any)[NG_ELEMENT_ID];
|
||||
}
|
||||
|
||||
// Set a unique ID on the directive type, so if something tries to inject the directive,
|
||||
// we can easily retrieve the ID and hash it into the bloom bit that should be checked.
|
||||
@ -267,7 +271,7 @@ export function diPublicInInjector(
|
||||
export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): string|null {
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
tNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer);
|
||||
tNode, [TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer]);
|
||||
ngDevMode && assertDefined(tNode, 'expecting tNode');
|
||||
if (attrNameToInject === 'class') {
|
||||
return tNode.classes;
|
||||
@ -584,7 +588,9 @@ export function bloomHashBitOrFactory(token: Type<any>|InjectionToken<any>|strin
|
||||
if (typeof token === 'string') {
|
||||
return token.charCodeAt(0) || 0;
|
||||
}
|
||||
const tokenId: number|undefined = (token as any)[NG_ELEMENT_ID];
|
||||
const tokenId: number|undefined =
|
||||
// First check with `hasOwnProperty` so we don't get an inherited ID.
|
||||
token.hasOwnProperty(NG_ELEMENT_ID) ? (token as any)[NG_ELEMENT_ID] : undefined;
|
||||
// Negative token IDs are used for special objects such as `Injector`
|
||||
return (typeof tokenId === 'number' && tokenId > 0) ? tokenId & BLOOM_MASK : tokenId;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import '../util/ng_i18n_closure_mode';
|
||||
|
||||
import {DEFAULT_LOCALE_ID, getPluralCase} from '../i18n/localization';
|
||||
import {getTemplateContent, SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS} from '../sanitization/html_sanitizer';
|
||||
import {InertBodyHelper} from '../sanitization/inert_body';
|
||||
import {getInertBodyHelper} from '../sanitization/inert_body';
|
||||
import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
|
||||
import {addAllToArray} from '../util/array_utils';
|
||||
import {assertDataInRange, assertDefined, assertEqual} from '../util/assert';
|
||||
@ -1233,7 +1233,7 @@ function icuStart(
|
||||
function parseIcuCase(
|
||||
unsafeHtml: string, parentIndex: number, nestedIcus: IcuExpression[], tIcus: TIcu[],
|
||||
expandoStartIndex: number): IcuCase {
|
||||
const inertBodyHelper = new InertBodyHelper(getDocument());
|
||||
const inertBodyHelper = getInertBodyHelper(getDocument());
|
||||
const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
||||
if (!inertBodyElement) {
|
||||
throw new Error('Unable to generate inert body element');
|
||||
|
@ -9,8 +9,7 @@ import {InjectFlags, InjectionToken, resolveForwardRef} from '../../di';
|
||||
import {ɵɵinject} from '../../di/injector_compatibility';
|
||||
import {Type} from '../../interface/type';
|
||||
import {getOrCreateInjectable, injectAttributeImpl} from '../di';
|
||||
import {TDirectiveHostNode, TNodeType} from '../interfaces/node';
|
||||
import {assertNodeOfPossibleTypes} from '../node_assert';
|
||||
import {TDirectiveHostNode} from '../interfaces/node';
|
||||
import {getLView, getPreviousOrParentTNode} from '../state';
|
||||
|
||||
/**
|
||||
|
@ -128,7 +128,7 @@ function listenerInternal(
|
||||
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer);
|
||||
tNode, [TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer]);
|
||||
|
||||
let processOutputs = true;
|
||||
|
||||
|
@ -15,6 +15,7 @@ import {assertDataInRange, assertDefined, assertDomNode, assertEqual, assertGrea
|
||||
import {createNamedArrayType} from '../../util/named_array_type';
|
||||
import {initNgDevMode} from '../../util/ng_dev_mode';
|
||||
import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../../util/ng_reflect';
|
||||
import {stringify} from '../../util/stringify';
|
||||
import {assertFirstCreatePass, assertLContainer, assertLView} from '../assert';
|
||||
import {attachPatchData} from '../context_discovery';
|
||||
import {getFactoryDef} from '../definition';
|
||||
@ -272,7 +273,7 @@ export function assignTViewNodeToLView(
|
||||
let tNode = tView.node;
|
||||
if (tNode == null) {
|
||||
ngDevMode && tParentNode &&
|
||||
assertNodeOfPossibleTypes(tParentNode, TNodeType.Element, TNodeType.Container);
|
||||
assertNodeOfPossibleTypes(tParentNode, [TNodeType.Element, TNodeType.Container]);
|
||||
tView.node = tNode = createTNode(
|
||||
tView,
|
||||
tParentNode as TElementNode | TContainerNode | null, //
|
||||
@ -794,22 +795,6 @@ export function storeCleanupWithContext(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the cleanup function itself in LView.cleanupInstances.
|
||||
*
|
||||
* This is necessary for functions that are wrapped with their contexts, like in renderer2
|
||||
* listeners.
|
||||
*
|
||||
* On the first template pass, the index of the cleanup function is saved in TView.
|
||||
*/
|
||||
export function storeCleanupFn(tView: TView, lView: LView, cleanupFn: Function): void {
|
||||
getLCleanup(lView).push(cleanupFn);
|
||||
|
||||
if (tView.firstCreatePass) {
|
||||
getTViewCleanup(tView).push(lView[CLEANUP]!.length - 1, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a TNode object from the arguments.
|
||||
*
|
||||
@ -1278,7 +1263,7 @@ function instantiateAllDirectives(
|
||||
const isComponent = isComponentDef(def);
|
||||
|
||||
if (isComponent) {
|
||||
ngDevMode && assertNodeOfPossibleTypes(tNode, TNodeType.Element);
|
||||
ngDevMode && assertNodeOfPossibleTypes(tNode, [TNodeType.Element]);
|
||||
addComponentLogic(lView, tNode as TElementNode, def as ComponentDef<any>);
|
||||
}
|
||||
|
||||
@ -1366,7 +1351,7 @@ function findDirectiveDefMatches(
|
||||
ngDevMode && assertFirstCreatePass(tView);
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
tNode, TNodeType.Element, TNodeType.ElementContainer, TNodeType.Container);
|
||||
tNode, [TNodeType.Element, TNodeType.ElementContainer, TNodeType.Container]);
|
||||
const registry = tView.directiveRegistry;
|
||||
let matches: any[]|null = null;
|
||||
if (registry) {
|
||||
@ -1377,6 +1362,12 @@ function findDirectiveDefMatches(
|
||||
diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, viewData), tView, def.type);
|
||||
|
||||
if (isComponentDef(def)) {
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
tNode, [TNodeType.Element],
|
||||
`"${tNode.tagName}" tags cannot be used as component hosts. ` +
|
||||
`Please use a different tag to activate the ${
|
||||
stringify(def.type)} component.`);
|
||||
if (tNode.flags & TNodeFlags.isComponentHost) throwMultipleComponentError(tNode);
|
||||
markAsComponentHost(tView, tNode);
|
||||
// The component is always stored first with directives after.
|
||||
|
@ -26,12 +26,14 @@ export function assertNodeType(tNode: TNode, type: TNodeType): asserts tNode is
|
||||
assertEqual(tNode.type, type, `should be a ${typeName(type)}`);
|
||||
}
|
||||
|
||||
export function assertNodeOfPossibleTypes(tNode: TNode|null, ...types: TNodeType[]): void {
|
||||
export function assertNodeOfPossibleTypes(
|
||||
tNode: TNode|null, types: TNodeType[], message?: string): void {
|
||||
assertDefined(tNode, 'should be called with a TNode');
|
||||
const found = types.some(type => tNode.type === type);
|
||||
assertEqual(
|
||||
found, true,
|
||||
`Should be one of ${types.map(typeName).join(', ')} but got ${typeName(tNode.type)}`);
|
||||
message ??
|
||||
`Should be one of ${types.map(typeName).join(', ')} but got ${typeName(tNode.type)}`);
|
||||
}
|
||||
|
||||
export function assertNodeNotOfTypes(tNode: TNode, types: TNodeType[], message?: string): void {
|
||||
|
@ -552,7 +552,7 @@ function getRenderParent(tView: TView, tNode: TNode, currentView: LView): REleme
|
||||
} else {
|
||||
// We are inserting a root element of the component view into the component host element and
|
||||
// it should always be eager.
|
||||
ngDevMode && assertNodeOfPossibleTypes(hostTNode, TNodeType.Element);
|
||||
ngDevMode && assertNodeOfPossibleTypes(hostTNode, [TNodeType.Element]);
|
||||
return currentView[HOST];
|
||||
}
|
||||
} else {
|
||||
@ -698,10 +698,10 @@ export function appendChild(
|
||||
*/
|
||||
function getFirstNativeNode(lView: LView, tNode: TNode|null): RNode|null {
|
||||
if (tNode !== null) {
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer,
|
||||
TNodeType.IcuContainer, TNodeType.Projection);
|
||||
ngDevMode && assertNodeOfPossibleTypes(tNode, [
|
||||
TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer, TNodeType.IcuContainer,
|
||||
TNodeType.Projection
|
||||
]);
|
||||
|
||||
const tNodeType = tNode.type;
|
||||
if (tNodeType === TNodeType.Element) {
|
||||
@ -778,10 +778,10 @@ function applyNodes(
|
||||
renderParent: RElement|null, beforeNode: RNode|null, isProjection: boolean) {
|
||||
while (tNode != null) {
|
||||
ngDevMode && assertTNodeForLView(tNode, lView);
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
tNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer,
|
||||
TNodeType.Projection, TNodeType.Projection, TNodeType.IcuContainer);
|
||||
ngDevMode && assertNodeOfPossibleTypes(tNode, [
|
||||
TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer, TNodeType.Projection,
|
||||
TNodeType.IcuContainer
|
||||
]);
|
||||
const rawSlotValue = lView[tNode.index];
|
||||
const tNodeType = tNode.type;
|
||||
if (isProjection) {
|
||||
@ -798,7 +798,7 @@ function applyNodes(
|
||||
applyProjectionRecursive(
|
||||
renderer, action, lView, tNode as TProjectionNode, renderParent, beforeNode);
|
||||
} else {
|
||||
ngDevMode && assertNodeOfPossibleTypes(tNode, TNodeType.Element, TNodeType.Container);
|
||||
ngDevMode && assertNodeOfPossibleTypes(tNode, [TNodeType.Element, TNodeType.Container]);
|
||||
applyToElementOrContainer(action, renderer, renderParent, rawSlotValue, beforeNode);
|
||||
}
|
||||
}
|
||||
|
@ -325,7 +325,7 @@ function createSpecialToken(lView: LView, tNode: TNode, read: any): any {
|
||||
} else if (read === ViewContainerRef) {
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer);
|
||||
tNode, [TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer]);
|
||||
return createContainerRef(
|
||||
ViewContainerRef, ViewEngine_ElementRef,
|
||||
tNode as TElementNode | TContainerNode | TElementContainerNode, lView);
|
||||
|
@ -340,7 +340,7 @@ export function createContainerRef(
|
||||
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
hostTNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer);
|
||||
hostTNode, [TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer]);
|
||||
|
||||
let lContainer: LContainer;
|
||||
const slotValue = hostView[hostTNode.index];
|
||||
|
@ -11,7 +11,7 @@ import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detec
|
||||
import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref';
|
||||
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref';
|
||||
import {assertDefined} from '../util/assert';
|
||||
import {checkNoChangesInRootView, checkNoChangesInternal, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn} from './instructions/shared';
|
||||
import {checkNoChangesInRootView, checkNoChangesInternal, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupWithContext} from './instructions/shared';
|
||||
import {CONTAINER_HEADER_OFFSET} from './interfaces/container';
|
||||
import {TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
|
||||
import {isLContainer} from './interfaces/type_checks';
|
||||
@ -88,7 +88,7 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, viewEngine_Int
|
||||
}
|
||||
|
||||
onDestroy(callback: Function) {
|
||||
storeCleanupFn(this._lView[TVIEW], this._lView, callback);
|
||||
storeCleanupWithContext(this._lView[TVIEW], this._lView, null, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -324,10 +324,10 @@ function collectNativeNodes(
|
||||
tView: TView, lView: LView, tNode: TNode|null, result: any[],
|
||||
isProjection: boolean = false): any[] {
|
||||
while (tNode !== null) {
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
tNode, TNodeType.Element, TNodeType.Container, TNodeType.Projection,
|
||||
TNodeType.ElementContainer, TNodeType.IcuContainer);
|
||||
ngDevMode && assertNodeOfPossibleTypes(tNode, [
|
||||
TNodeType.Element, TNodeType.Container, TNodeType.Projection, TNodeType.ElementContainer,
|
||||
TNodeType.IcuContainer
|
||||
]);
|
||||
|
||||
const lNode = lView[tNode.index];
|
||||
if (lNode !== null) {
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {isDevMode} from '../util/is_dev_mode';
|
||||
import {InertBodyHelper} from './inert_body';
|
||||
import {getInertBodyHelper, InertBodyHelper} from './inert_body';
|
||||
import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer';
|
||||
|
||||
function tagSet(tags: string): {[k: string]: boolean} {
|
||||
@ -245,7 +245,7 @@ let inertBodyHelper: InertBodyHelper;
|
||||
export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
|
||||
let inertBodyElement: HTMLElement|null = null;
|
||||
try {
|
||||
inertBodyHelper = inertBodyHelper || new InertBodyHelper(defaultDoc);
|
||||
inertBodyHelper = inertBodyHelper || getInertBodyHelper(defaultDoc);
|
||||
// Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime).
|
||||
let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : '';
|
||||
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
||||
|
@ -7,89 +7,29 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* This helper class is used to get hold of an inert tree of DOM elements containing dirty HTML
|
||||
* This helper is used to get hold of an inert tree of DOM elements containing dirty HTML
|
||||
* that needs sanitizing.
|
||||
* Depending upon browser support we must use one of three strategies for doing this.
|
||||
* Support: Safari 10.x -> XHR strategy
|
||||
* Support: Firefox -> DomParser strategy
|
||||
* Default: InertDocument strategy
|
||||
* Depending upon browser support we use one of two strategies for doing this.
|
||||
* Default: DOMParser strategy
|
||||
* Fallback: InertDocument strategy
|
||||
*/
|
||||
export class InertBodyHelper {
|
||||
private inertDocument: Document;
|
||||
|
||||
constructor(private defaultDoc: Document) {
|
||||
this.inertDocument = this.defaultDoc.implementation.createHTMLDocument('sanitization-inert');
|
||||
let inertBodyElement = this.inertDocument.body;
|
||||
|
||||
if (inertBodyElement == null) {
|
||||
// usually there should be only one body element in the document, but IE doesn't have any, so
|
||||
// we need to create one.
|
||||
const inertHtml = this.inertDocument.createElement('html');
|
||||
this.inertDocument.appendChild(inertHtml);
|
||||
inertBodyElement = this.inertDocument.createElement('body');
|
||||
inertHtml.appendChild(inertBodyElement);
|
||||
}
|
||||
|
||||
inertBodyElement.innerHTML = '<svg><g onload="this.parentNode.remove()"></g></svg>';
|
||||
if (inertBodyElement.querySelector && !inertBodyElement.querySelector('svg')) {
|
||||
// We just hit the Safari 10.1 bug - which allows JS to run inside the SVG G element
|
||||
// so use the XHR strategy.
|
||||
this.getInertBodyElement = this.getInertBodyElement_XHR;
|
||||
return;
|
||||
}
|
||||
|
||||
inertBodyElement.innerHTML = '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">';
|
||||
if (inertBodyElement.querySelector && inertBodyElement.querySelector('svg img')) {
|
||||
// We just hit the Firefox bug - which prevents the inner img JS from being sanitized
|
||||
// so use the DOMParser strategy, if it is available.
|
||||
// If the DOMParser is not available then we are not in Firefox (Server/WebWorker?) so we
|
||||
// fall through to the default strategy below.
|
||||
if (isDOMParserAvailable()) {
|
||||
this.getInertBodyElement = this.getInertBodyElement_DOMParser;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// None of the bugs were hit so it is safe for us to use the default InertDocument strategy
|
||||
this.getInertBodyElement = this.getInertBodyElement_InertDocument;
|
||||
}
|
||||
export function getInertBodyHelper(defaultDoc: Document): InertBodyHelper {
|
||||
return isDOMParserAvailable() ? new DOMParserHelper() : new InertDocumentHelper(defaultDoc);
|
||||
}
|
||||
|
||||
export interface InertBodyHelper {
|
||||
/**
|
||||
* Get an inert DOM element containing DOM created from the dirty HTML string provided.
|
||||
* The implementation of this is determined in the constructor, when the class is instantiated.
|
||||
*/
|
||||
getInertBodyElement: (html: string) => HTMLElement | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use XHR to create and fill an inert body element (on Safari 10.1)
|
||||
* See
|
||||
* https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449
|
||||
*/
|
||||
private getInertBodyElement_XHR(html: string) {
|
||||
// We add these extra elements to ensure that the rest of the content is parsed as expected
|
||||
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
|
||||
// `<head>` tag.
|
||||
html = '<body><remove></remove>' + html + '</body>';
|
||||
try {
|
||||
html = encodeURI(html);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'document';
|
||||
xhr.open('GET', 'data:text/html;charset=utf-8,' + html, false);
|
||||
xhr.send(undefined);
|
||||
const body: HTMLBodyElement = xhr.response.body;
|
||||
body.removeChild(body.firstChild!);
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use DOMParser to create and fill an inert body element (on Firefox)
|
||||
* See https://github.com/cure53/DOMPurify/releases/tag/0.6.7
|
||||
*
|
||||
*/
|
||||
private getInertBodyElement_DOMParser(html: string) {
|
||||
/**
|
||||
* Uses DOMParser to create and fill an inert body element.
|
||||
* This is the default strategy used in browsers that support it.
|
||||
*/
|
||||
class DOMParserHelper implements InertBodyHelper {
|
||||
getInertBodyElement(html: string): HTMLElement|null {
|
||||
// We add these extra elements to ensure that the rest of the content is parsed as expected
|
||||
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
|
||||
// `<head>` tag.
|
||||
@ -103,14 +43,30 @@ export class InertBodyHelper {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use an HTML5 `template` element, if supported, or an inert body element created via
|
||||
* `createHtmlDocument` to create and fill an inert DOM element.
|
||||
* This is the default sane strategy to use if the browser does not require one of the specialised
|
||||
* strategies above.
|
||||
*/
|
||||
private getInertBodyElement_InertDocument(html: string) {
|
||||
/**
|
||||
* Use an HTML5 `template` element, if supported, or an inert body element created via
|
||||
* `createHtmlDocument` to create and fill an inert DOM element.
|
||||
* This is the fallback strategy if the browser does not support DOMParser.
|
||||
*/
|
||||
class InertDocumentHelper implements InertBodyHelper {
|
||||
private inertDocument: Document;
|
||||
|
||||
constructor(private defaultDoc: Document) {
|
||||
this.inertDocument = this.defaultDoc.implementation.createHTMLDocument('sanitization-inert');
|
||||
|
||||
if (this.inertDocument.body == null) {
|
||||
// usually there should be only one body element in the document, but IE doesn't have any, so
|
||||
// we need to create one.
|
||||
const inertHtml = this.inertDocument.createElement('html');
|
||||
this.inertDocument.appendChild(inertHtml);
|
||||
const inertBodyElement = this.inertDocument.createElement('body');
|
||||
inertHtml.appendChild(inertBodyElement);
|
||||
}
|
||||
}
|
||||
|
||||
getInertBodyElement(html: string): HTMLElement|null {
|
||||
// Prefer using <template> element if supported.
|
||||
const templateEl = this.inertDocument.createElement('template');
|
||||
if ('content' in templateEl) {
|
||||
@ -164,15 +120,15 @@ export class InertBodyHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to determine whether the DOMParser exists in the global context.
|
||||
* The try-catch is because, on some browsers, trying to access this property
|
||||
* on window can actually throw an error.
|
||||
* We need to determine whether the DOMParser exists in the global context and
|
||||
* supports parsing HTML; HTML parsing support is not as wide as other formats, see
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/DOMParser#Browser_compatibility.
|
||||
*
|
||||
* @suppress {uselessCode}
|
||||
*/
|
||||
function isDOMParserAvailable() {
|
||||
export function isDOMParserAvailable() {
|
||||
try {
|
||||
return !!(window as any).DOMParser;
|
||||
return !!new (window as any).DOMParser().parseFromString('', 'text/html');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user