Merge remote-tracking branch 'en/master' into aio

This commit is contained in:
Zhicheng WANG 2020-06-28 11:04:00 +08:00
commit cababcc4ec
45 changed files with 496 additions and 330 deletions

View File

@ -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/**',

View File

@ -24,7 +24,7 @@ filegroup(
"//packages/zone.js/bundles:zone-testing.umd.js",
"//packages/zone.js/bundles:task-tracking.umd.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",

View File

@ -1,3 +1,26 @@
<a name="10.0.1"></a>
## [10.0.1](https://github.com/angular/angular/compare/10.0.0...10.0.1) (2020-06-26)
### Bug Fixes
* **core:** cleanup DOM elements when root view is removed ([#37600](https://github.com/angular/angular/issues/37600)) ([64f2ffa](https://github.com/angular/angular/commit/64f2ffa)), closes [#36449](https://github.com/angular/angular/issues/36449)
* **forms:** change error message ([#37643](https://github.com/angular/angular/issues/37643)) ([c5bc2e7](https://github.com/angular/angular/commit/c5bc2e7))
* **forms:** correct usage of `selectedOptions` ([#37620](https://github.com/angular/angular/issues/37620)) ([dfb58c4](https://github.com/angular/angular/commit/dfb58c4)), closes [#37433](https://github.com/angular/angular/issues/37433)
* **http:** avoid abort a request when fetch operation is completed ([#37367](https://github.com/angular/angular/issues/37367)) ([a5d5f67](https://github.com/angular/angular/commit/a5d5f67))
* **language-service:** reinstate getExternalFiles() ([#37750](https://github.com/angular/angular/issues/37750)) ([ad6680f](https://github.com/angular/angular/commit/ad6680f))
* **migrations:** do not incorrectly add todo for @Injectable or @Pipe ([#37732](https://github.com/angular/angular/issues/37732)) ([13020b9](https://github.com/angular/angular/commit/13020b9)), closes [#37726](https://github.com/angular/angular/issues/37726)
* **router:** `RouterLinkActive` should run CD when setting `isActive` ([#21411](https://github.com/angular/angular/issues/21411)) ([a8ea817](https://github.com/angular/angular/commit/a8ea817)), closes [#15943](https://github.com/angular/angular/issues/15943) [#19934](https://github.com/angular/angular/issues/19934)
* **router:** add null support for RouterLink directive ([#32616](https://github.com/angular/angular/issues/32616)) ([69948ce](https://github.com/angular/angular/commit/69948ce))
* **router:** fix error when calling ParamMap.get function ([#31599](https://github.com/angular/angular/issues/31599)) ([3190ccf](https://github.com/angular/angular/commit/3190ccf))
### Performance Improvements
* **compiler-cli:** fix regressions in incremental program reuse ([#37690](https://github.com/angular/angular/issues/37690)) ([96b96fb](https://github.com/angular/angular/commit/96b96fb))
<a name="10.0.0"></a>
# [10.0.0](https://github.com/angular/angular/compare/10.0.0-rc.6...10.0.0) (2020-06-24)

View File

@ -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
}

View File

@ -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
}

View File

@ -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'; }

View File

@ -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) {

View File

@ -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);
});
});
});
});

View File

@ -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;
}

View File

@ -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}`);
}

View File

@ -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]);

View File

@ -16,9 +16,6 @@ import {isPullRequest, loadAndValidatePullRequest,} from './pull-request';
import {GithubApiMergeStrategy} from './strategies/api-merge';
import {AutosquashMergeStrategy} from './strategies/autosquash-merge';
/** Github OAuth scopes required for the merge task. */
const REQUIRED_SCOPES = ['repo'];
/** Describes the status of a pull request merge. */
export const enum MergeStatus {
UNKNOWN_GIT_ERROR,
@ -56,8 +53,19 @@ export class PullRequestMergeTask {
* @param force Whether non-critical pull request failures should be ignored.
*/
async merge(prNumber: number, force = false): Promise<MergeResult> {
// Assert the authenticated GitClient has access on the required scopes.
const hasOauthScopes = await this.git.hasOauthScopes(...REQUIRED_SCOPES);
// Check whether the given Github token has sufficient permissions for writing
// to the configured repository. If the repository is not private, only the
// reduced `public_repo` OAuth scope is sufficient for performing merges.
const hasOauthScopes = await this.git.hasOauthScopes((scopes, missing) => {
if (!scopes.includes('repo')) {
if (this.config.remote.private) {
missing.push('repo');
} else if (!scopes.includes('public_repo')) {
missing.push('public_repo');
}
}
});
if (hasOauthScopes !== true) {
return {
status: MergeStatus.GITHUB_ERROR,
@ -87,14 +95,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);
@ -107,7 +115,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);
@ -123,8 +131,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]);
}
}
}

View File

@ -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'});
}
}

View File

@ -21,6 +21,8 @@ export interface GitClientConfig {
name: string;
/** If SSH protocol should be used for git interactions. */
useSsh?: boolean;
/** Whether the specified repository is private. */
private?: boolean;
}
/**

View File

@ -21,6 +21,9 @@ type RateLimitResponseWithOAuthScopeHeader = Octokit.Response<Octokit.RateLimitG
headers: {'x-oauth-scopes': string};
};
/** Describes a function that can be used to test for given Github OAuth scopes. */
export type OAuthScopeTestFunction = (scopes: string[], missing: string[]) => void;
/** Error for failed Git commands. */
export class GitCommandError extends Error {
constructor(client: GitClient, public args: string[]) {
@ -119,9 +122,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. */
@ -148,14 +158,11 @@ export class GitClient {
* Assert the GitClient instance is using a token with permissions for the all of the
* provided OAuth scopes.
*/
async hasOauthScopes(...requestedScopes: string[]): Promise<true|{error: string}> {
const missingScopes: string[] = [];
async hasOauthScopes(testFn: OAuthScopeTestFunction): Promise<true|{error: string}> {
const scopes = await this.getAuthScopesForToken();
requestedScopes.forEach(scope => {
if (!scopes.includes(scope)) {
missingScopes.push(scope);
}
});
const missingScopes: string[] = [];
// Test Github OAuth scopes and collect missing ones.
testFn(scopes, missingScopes);
// If no missing scopes are found, return true to indicate all OAuth Scopes are available.
if (missingScopes.length === 0) {
return true;

View File

@ -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
```

View File

@ -12,7 +12,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2987,
"main-es2015": 451406,
"main-es2015": 450883,
"polyfills-es2015": 52630
}
}
@ -21,7 +21,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 3097,
"main-es2015": 429710,
"main-es2015": 429200,
"polyfills-es2015": 52195
}
}

View File

@ -30,7 +30,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1485,
"main-es2015": 136302,
"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": 1209659
}
}
}

View File

@ -43,7 +43,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.

View File

@ -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(

View File

@ -229,7 +229,7 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
createElementRef(viewEngine_ElementRef, tElementNode, rootLView), rootLView, tElementNode);
// The host element of the internal root view is attached to the component's host view node.
ngDevMode && assertNodeOfPossibleTypes(rootTView.node, TNodeType.View);
ngDevMode && assertNodeOfPossibleTypes(rootTView.node, [TNodeType.View]);
rootTView.node!.child = tElementNode;
return componentRef;

View File

@ -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;
}

View File

@ -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');

View File

@ -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';
/**

View File

@ -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;

View File

@ -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.

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -326,7 +326,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);

View File

@ -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];

View File

@ -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) {

View File

@ -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);

View File

@ -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;
}

View File

@ -34,7 +34,7 @@ import {isDevMode} from '../util/is_dev_mode';
*
* This regular expression was taken from the Closure sanitization library.
*/
const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi;
const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file|sms):|[^&:/?#]*(?:[/?#]|$))/gi;
/* A pattern that matches safe srcset values */
const SAFE_SRCSET_PATTERN = /^(?:(?:https?|file):|[^&:/?#]*(?:[/?#]|$))/gi;

View File

@ -11,7 +11,7 @@ import {ApplicationRef, Component, ComponentFactoryResolver, ComponentRef, Eleme
import {TestBed} from '@angular/core/testing';
import {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {onlyInIvy} from '@angular/private/testing';
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
import {domRendererFactory3} from '../../src/render3/interfaces/renderer';
@ -259,6 +259,65 @@ describe('component', () => {
expect(wrapperEls.length).toBe(2); // other elements are preserved
});
describe('invalid host element', () => {
it('should throw when <ng-container> is used as a host element for a Component', () => {
@Component({
selector: 'ng-container',
template: '...',
})
class Comp {
}
@Component({
selector: 'root',
template: '<ng-container></ng-container>',
})
class App {
}
TestBed.configureTestingModule({declarations: [App, Comp]});
if (ivyEnabled) {
expect(() => TestBed.createComponent(App))
.toThrowError(
/"ng-container" tags cannot be used as component hosts. Please use a different tag to activate the Comp component/);
} else {
// In VE there is no special check for the case when `<ng-container>` is used as a host
// element for a Component. VE tries to attach Component's content to a Comment node that
// represents the `<ng-container>` location and this call fails with a
// browser/environment-specific error message, so we just verify that this scenario is
// triggering an error in VE.
expect(() => TestBed.createComponent(App)).toThrow();
}
});
it('should throw when <ng-template> is used as a host element for a Component', () => {
@Component({
selector: 'ng-template',
template: '...',
})
class Comp {
}
@Component({
selector: 'root',
template: '<ng-template></ng-template>',
})
class App {
}
TestBed.configureTestingModule({declarations: [App, Comp]});
if (ivyEnabled) {
expect(() => TestBed.createComponent(App))
.toThrowError(
/"ng-template" tags cannot be used as component hosts. Please use a different tag to activate the Comp component/);
} else {
expect(() => TestBed.createComponent(App))
.toThrowError(
/Components on an embedded template: Comp \("\[ERROR ->\]<ng-template><\/ng-template>"\)/);
}
});
});
it('should use a new ngcontent attribute for child elements created w/ Renderer2', () => {
@Component({
selector: 'app-root',

View File

@ -7,9 +7,9 @@
*/
import {CommonModule} from '@angular/common';
import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, forwardRef, Host, HostBinding, Inject, Injectable, InjectionToken, INJECTOR, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Self, SkipSelf, TemplateRef, ViewChild, ViewContainerRef, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID} from '@angular/core';
import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, forwardRef, Host, HostBinding, Inject, Injectable, InjectionToken, INJECTOR, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Self, SkipSelf, TemplateRef, ViewChild, ViewContainerRef, ViewRef, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID} from '@angular/core';
import {ɵINJECTOR_SCOPE} from '@angular/core/src/core';
import {ViewRef} from '@angular/core/src/render3/view_ref';
import {ViewRef as ViewRefInternal} from '@angular/core/src/render3/view_ref';
import {TestBed} from '@angular/core/testing';
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
import {BehaviorSubject} from 'rxjs';
@ -1627,7 +1627,8 @@ describe('di', () => {
TestBed.configureTestingModule({declarations: [MyApp, MyPipe], imports: [CommonModule]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect((pipeInstance!.cdr as ViewRef<MyApp>).context).toBe(fixture.componentInstance);
expect((pipeInstance!.cdr as ViewRefInternal<MyApp>).context)
.toBe(fixture.componentInstance);
});
it('should inject current component ChangeDetectorRef into directives on the same node as components',
@ -1643,7 +1644,7 @@ describe('di', () => {
fixture.detectChanges();
const app = fixture.componentInstance;
const comp = fixture.componentInstance.component;
expect((comp!.cdr as ViewRef<MyComp>).context).toBe(comp);
expect((comp!.cdr as ViewRefInternal<MyComp>).context).toBe(comp);
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
expect(app.directive.value).toContain('ViewRef');
@ -1664,7 +1665,7 @@ describe('di', () => {
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const comp = fixture.componentInstance;
expect((comp!.cdr as ViewRef<MyComp>).context).toBe(comp);
expect((comp!.cdr as ViewRefInternal<MyComp>).context).toBe(comp);
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
expect(comp.directive.value).toContain('ViewRef');
@ -1692,7 +1693,7 @@ describe('di', () => {
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
const app = fixture.componentInstance;
expect((app!.cdr as ViewRef<MyApp>).context).toBe(app);
expect((app!.cdr as ViewRefInternal<MyApp>).context).toBe(app);
const comp = fixture.componentInstance.component;
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
expect(app.directive.value).toContain('ViewRef');
@ -1720,7 +1721,7 @@ describe('di', () => {
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const comp = fixture.componentInstance;
expect((comp!.cdr as ViewRef<MyComp>).context).toBe(comp);
expect((comp!.cdr as ViewRefInternal<MyComp>).context).toBe(comp);
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
expect(comp.directive.value).toContain('ViewRef');
@ -1743,7 +1744,7 @@ describe('di', () => {
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const comp = fixture.componentInstance;
expect((comp!.cdr as ViewRef<MyComp>).context).toBe(comp);
expect((comp!.cdr as ViewRefInternal<MyComp>).context).toBe(comp);
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
expect(comp.directive.value).toContain('ViewRef');
@ -1773,7 +1774,8 @@ describe('di', () => {
TestBed.configureTestingModule({declarations: [MyApp, MyDirective]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect((dirInstance!.cdr as ViewRef<MyApp>).context).toBe(fixture.componentInstance);
expect((dirInstance!.cdr as ViewRefInternal<MyApp>).context)
.toBe(fixture.componentInstance);
});
});
});
@ -2300,4 +2302,14 @@ describe('di', () => {
expect(fixture.componentInstance.dir.token).toBe('parent');
});
});
it('should not be able to inject ViewRef', () => {
@Component({template: ''})
class App {
constructor(_viewRef: ViewRef) {}
}
TestBed.configureTestingModule({declarations: [App]});
expect(() => TestBed.createComponent(App)).toThrowError(/NullInjectorError/);
});
});

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ApplicationRef, Component, ComponentFactoryResolver, ComponentRef, ElementRef, Injector, NgModule} from '@angular/core';
import {ApplicationRef, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, ElementRef, Injector, NgModule} from '@angular/core';
import {InternalViewRef} from '@angular/core/src/linker/view_ref';
import {TestBed} from '@angular/core/testing';
@ -54,4 +54,22 @@ describe('ViewRef', () => {
fixture.detectChanges();
expect(document.body.querySelector('dynamic-cpt')).toBeFalsy();
});
it('should invoke the onDestroy callback of a view ref', () => {
let called = false;
@Component({template: ''})
class App {
constructor(changeDetectorRef: ChangeDetectorRef) {
(changeDetectorRef as InternalViewRef).onDestroy(() => called = true);
}
}
TestBed.configureTestingModule({declarations: [App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.destroy();
expect(called).toBe(true);
});
});

View File

@ -9,6 +9,7 @@
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer';
import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
{
describe('HTML sanitizer', () => {
@ -229,18 +230,3 @@ import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer';
}
});
}
/**
* 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.
*
* @suppress {uselessCode}
*/
function isDOMParserAvailable() {
try {
return !!(window as any).DOMParser;
} catch (e) {
return false;
}
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteSourceSpan, AST, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, Element, ElementAst, EmptyExpr, ExpressionBinding, getHtmlTagDefinition, HtmlAstPath, Node as HtmlAst, NullTemplateVisitor, ParseSpan, ReferenceAst, TagContentType, TemplateBinding, Text, VariableBinding} from '@angular/compiler';
import {AbsoluteSourceSpan, AST, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, Element, ElementAst, EmptyExpr, ExpressionBinding, getHtmlTagDefinition, HtmlAstPath, Node as HtmlAst, NullTemplateVisitor, ParseSpan, ReferenceAst, TagContentType, TemplateBinding, Text, VariableBinding, Visitor} from '@angular/compiler';
import {$$, $_, isAsciiLetter, isDigit} from '@angular/compiler/src/chars';
import {ATTR, getBindingDescriptor} from './binding_utils';
@ -127,72 +127,18 @@ function getBoundedWordSpan(
export function getTemplateCompletions(
templateInfo: ng.AstResult, position: number): ng.CompletionEntry[] {
let result: ng.CompletionEntry[] = [];
const {htmlAst, template} = templateInfo;
// The templateNode starts at the delimiter character so we add 1 to skip it.
// Calculate the position relative to the start of the template. This is needed
// because spans in HTML AST are relative. Inline template has non-zero start position.
const templatePosition = position - template.span.start;
const path = getPathToNodeAtPosition(htmlAst, templatePosition);
const mostSpecific = path.tail;
if (path.empty || !mostSpecific) {
result = elementCompletions(templateInfo);
} else {
const astPosition = templatePosition - mostSpecific.sourceSpan.start.offset;
mostSpecific.visit(
{
visitElement(ast) {
const startTagSpan = spanOf(ast.sourceSpan);
const tagLen = ast.name.length;
// + 1 for the opening angle bracket
if (templatePosition <= startTagSpan.start + tagLen + 1) {
// If we are in the tag then return the element completions.
result = elementCompletions(templateInfo);
} else if (templatePosition < startTagSpan.end) {
// We are in the attribute section of the element (but not in an attribute).
// Return the attribute completions.
result = attributeCompletionsForElement(templateInfo, ast.name);
}
},
visitAttribute(ast: Attribute) {
// An attribute consists of two parts, LHS="RHS".
// Determine if completions are requested for LHS or RHS
if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) {
// RHS completion
result = attributeValueCompletions(templateInfo, path);
} else {
// LHS completion
result = attributeCompletions(templateInfo, path);
}
},
visitText(ast) {
result = interpolationCompletions(templateInfo, templatePosition);
if (result.length) return result;
const element = path.first(Element);
if (element) {
const definition = getHtmlTagDefinition(element.name);
if (definition.contentType === TagContentType.PARSABLE_DATA) {
result = voidElementAttributeCompletions(templateInfo, path);
if (!result.length) {
// If the element can hold content, show element completions.
result = elementCompletions(templateInfo);
}
}
} else {
// If no element container, implies parsable data so show elements.
result = voidElementAttributeCompletions(templateInfo, path);
if (!result.length) {
result = elementCompletions(templateInfo);
}
}
},
visitComment() {},
visitExpansion() {},
visitExpansionCase() {}
},
null);
}
const htmlPath: HtmlAstPath = getPathToNodeAtPosition(htmlAst, templatePosition);
const mostSpecific = htmlPath.tail;
const visitor = new HtmlVisitor(templateInfo, htmlPath);
const results: ng.CompletionEntry[] = mostSpecific ?
mostSpecific.visit(visitor, null /* context */) :
elementCompletions(templateInfo);
const replacementSpan = getBoundedWordSpan(templateInfo, position, mostSpecific);
return result.map(entry => {
return results.map(entry => {
return {
...entry,
replacementSpan,
@ -200,6 +146,78 @@ export function getTemplateCompletions(
});
}
class HtmlVisitor implements Visitor {
/**
* Position relative to the start of the template.
*/
private readonly relativePosition: number;
constructor(private readonly templateInfo: ng.AstResult, private readonly htmlPath: HtmlAstPath) {
this.relativePosition = htmlPath.position;
}
// Note that every visitor method must explicitly specify return type because
// Visitor returns `any` for all methods.
visitElement(ast: Element): ng.CompletionEntry[] {
const startTagSpan = spanOf(ast.sourceSpan);
const tagLen = ast.name.length;
// + 1 for the opening angle bracket
if (this.relativePosition <= startTagSpan.start + tagLen + 1) {
// If we are in the tag then return the element completions.
return elementCompletions(this.templateInfo);
}
if (this.relativePosition < startTagSpan.end) {
// We are in the attribute section of the element (but not in an attribute).
// Return the attribute completions.
return attributeCompletionsForElement(this.templateInfo, ast.name);
}
return [];
}
visitAttribute(ast: Attribute): ng.CompletionEntry[] {
// An attribute consists of two parts, LHS="RHS".
// Determine if completions are requested for LHS or RHS
if (ast.valueSpan && inSpan(this.relativePosition, spanOf(ast.valueSpan))) {
// RHS completion
return attributeValueCompletions(this.templateInfo, this.htmlPath);
}
// LHS completion
return attributeCompletions(this.templateInfo, this.htmlPath);
}
visitText(): ng.CompletionEntry[] {
const templatePath = findTemplateAstAt(this.templateInfo.templateAst, this.relativePosition);
if (templatePath.tail instanceof BoundTextAst) {
// If we know that this is an interpolation then do not try other scenarios.
const visitor = new ExpressionVisitor(
this.templateInfo, this.relativePosition,
() =>
getExpressionScope(diagnosticInfoFromTemplateInfo(this.templateInfo), templatePath));
templatePath.tail?.visit(visitor, null);
return visitor.results;
}
// TODO(kyliau): Not sure if this check is really needed since we don't have
// any test cases for it.
const element = this.htmlPath.first(Element);
if (element &&
getHtmlTagDefinition(element.name).contentType !== TagContentType.PARSABLE_DATA) {
return [];
}
// This is to account for cases like <h1> <a> text | </h1> where the
// closest element has no closing tag and thus is considered plain text.
const results = voidElementAttributeCompletions(this.templateInfo, this.htmlPath);
if (results.length) {
return results;
}
return elementCompletions(this.templateInfo);
}
visitComment(): ng.CompletionEntry[] {
return [];
}
visitExpansion(): ng.CompletionEntry[] {
return [];
}
visitExpansionCase(): ng.CompletionEntry[] {
return [];
}
}
function attributeCompletions(info: ng.AstResult, path: AstPath<HtmlAst>): ng.CompletionEntry[] {
const attr = path.tail;
const elem = path.parentOf(attr);
@ -356,18 +374,6 @@ function elementCompletions(info: ng.AstResult): ng.CompletionEntry[] {
return results;
}
function interpolationCompletions(info: ng.AstResult, position: number): ng.CompletionEntry[] {
// Look for an interpolation in at the position.
const templatePath = findTemplateAstAt(info.templateAst, position);
if (!templatePath.tail) {
return [];
}
const visitor = new ExpressionVisitor(
info, position, () => getExpressionScope(diagnosticInfoFromTemplateInfo(info), templatePath));
templatePath.tail.visit(visitor, null);
return visitor.results;
}
// There is a special case of HTML where text that contains a unclosed tag is treated as
// text. For exaple '<h1> Some <a text </h1>' produces a text nodes inside of the H1
// element "Some <a text". We, however, want to treat this as if the user was requesting

View File

@ -11,8 +11,30 @@ import * as tss from 'typescript/lib/tsserverlibrary';
import {createLanguageService} from './language_service';
import {TypeScriptServiceHost} from './typescript_host';
// Use a WeakMap to keep track of Project to Host mapping so that when Project
// is deleted Host could be garbage collected.
const PROJECT_MAP = new WeakMap<tss.server.Project, TypeScriptServiceHost>();
/**
* This function is called by tsserver to retrieve the external (non-TS) files
* that should belong to the specified `project`. For Angular, these files are
* external templates. This is called once when the project is loaded, then
* every time when the program is updated.
* @param project Project for which external files should be retrieved.
*/
export function getExternalFiles(project: tss.server.Project): string[] {
if (!project.hasRoots()) {
// During project initialization where there is no root files yet we should
// not do any work.
return [];
}
const ngLsHost = PROJECT_MAP.get(project);
ngLsHost?.getAnalyzedModules();
return ngLsHost?.getExternalTemplates() || [];
}
export function create(info: tss.server.PluginCreateInfo): tss.LanguageService {
const {languageService: tsLS, languageServiceHost: tsLSHost, config} = info;
const {languageService: tsLS, languageServiceHost: tsLSHost, config, project} = info;
// This plugin could operate under two different modes:
// 1. TS + Angular
// Plugin augments TS language service to provide additional Angular
@ -25,6 +47,7 @@ export function create(info: tss.server.PluginCreateInfo): tss.LanguageService {
const angularOnly = config ? config.angularOnly === true : false;
const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS);
const ngLS = createLanguageService(ngLSHost);
PROJECT_MAP.set(project, ngLSHost);
function getCompletionsAtPosition(
fileName: string, position: number, options: tss.GetCompletionsAtPositionOptions|undefined) {

View File

@ -151,6 +151,13 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
return this.resolver.getReflector() as StaticReflector;
}
/**
* Return all known external templates.
*/
getExternalTemplates(): string[] {
return [...this.fileToComponent.keys()];
}
/**
* Checks whether the program has changed and returns all analyzed modules.
* If program has changed, invalidate all caches and update fileToComponent

View File

@ -841,6 +841,13 @@ describe('completions', () => {
'trim',
]);
});
it('should not return any results for unknown symbol', () => {
mockHost.override(TEST_TEMPLATE, '{{ doesnotexist.~{cursor} }}');
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expect(completions).toBeUndefined();
});
});
function expectContain(

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {create} from '../src/ts_plugin';
import {create, getExternalFiles} from '../src/ts_plugin';
import {CompletionKind} from '../src/types';
import {MockTypescriptHost} from './test_utils';
@ -129,6 +129,13 @@ describe('plugin', () => {
},
]);
});
it('should return external templates when getExternalFiles() is called', () => {
const externalTemplates = getExternalFiles(mockProject);
expect(externalTemplates).toEqual([
'/app/test.ng',
]);
});
});
describe(`with config 'angularOnly = true`, () => {

View File

@ -28,7 +28,7 @@ module.exports = function(config) {
// Polyfills.
'node_modules/core-js/client/core.js',
'node_modules/reflect-metadata/Reflect.js',
'shims_for_IE.js',
'third_party/shims_for_IE.js',
// System.js for module loading
'node_modules/systemjs/dist/system-polyfills.js',