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 = { const commitMessage = {
'maxLength': 120, 'maxLength': 120,
'minBodyLength': 100, 'minBodyLength': 100,
'minBodyLengthExcludes': ['docs'],
'types': [ 'types': [
'build', 'build',
'ci', 'ci',
@ -56,8 +57,6 @@ const format = {
// TODO: burn down format failures and remove aio and integration exceptions. // TODO: burn down format failures and remove aio and integration exceptions.
'!aio/**', '!aio/**',
'!integration/**', '!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 // Both third_party and .yarn are directories containing copied code which should
// not be modified. // not be modified.
'!third_party/**', '!third_party/**',

View File

@ -24,7 +24,7 @@ filegroup(
"//packages/zone.js/bundles:zone-testing.umd.js", "//packages/zone.js/bundles:zone-testing.umd.js",
"//packages/zone.js/bundles:task-tracking.umd.js", "//packages/zone.js/bundles:task-tracking.umd.js",
"//:test-events.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. # Including systemjs because it defines `__eval`, which produces correct stack traces.
"@npm//:node_modules/systemjs/dist/system.src.js", "@npm//:node_modules/systemjs/dist/system.src.js",
"@npm//:node_modules/reflect-metadata/Reflect.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> <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) # [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' templateUrl: './app.component.html'
}) })
export class AppComponent { 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 // #enddocregion hero-birthday-template
}) })
export class HeroBirthdayComponent { 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 // #docregion class
export class HeroBirthday2Component { 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 toggle = true; // start with true == shortDate
get format() { return this.toggle ? 'shortDate' : 'fullDate'; } get format() { return this.toggle ? 'shortDate' : 'fullDate'; }

View File

@ -11,6 +11,7 @@ import {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
export interface CommitMessageConfig { export interface CommitMessageConfig {
maxLineLength: number; maxLineLength: number;
minBodyLength: number; minBodyLength: number;
minBodyLengthTypeExcludes?: string[];
types: string[]; types: string[];
scopes: string[]; scopes: string[];
} }
@ -19,7 +20,7 @@ export interface CommitMessageConfig {
export function getCommitMessageConfig() { export function getCommitMessageConfig() {
// List of errors encountered validating the config. // List of errors encountered validating the config.
const errors: string[] = []; const errors: string[] = [];
// The unvalidated config object. // The non-validated config object.
const config: Partial<NgDevConfig<{commitMessage: CommitMessageConfig}>> = getConfig(); const config: Partial<NgDevConfig<{commitMessage: CommitMessageConfig}>> = getConfig();
if (config.commitMessage === undefined) { if (config.commitMessage === undefined) {

View File

@ -10,19 +10,22 @@
import * as validateConfig from './config'; import * as validateConfig from './config';
import {validateCommitMessage} from './validate'; import {validateCommitMessage} from './validate';
type CommitMessageConfig = validateConfig.CommitMessageConfig;
// Constants // Constants
const config = { const config: {commitMessage: CommitMessageConfig} = {
'commitMessage': { commitMessage: {
'maxLineLength': 120, maxLineLength: 120,
'minBodyLength': 0, minBodyLength: 0,
'types': [ types: [
'feat', 'feat',
'fix', 'fix',
'refactor', 'refactor',
'release', 'release',
'style', 'style',
], ],
'scopes': [ scopes: [
'common', 'common',
'compiler', 'compiler',
'core', '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 // // 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 ${ printError(`The commit message body does not meet the minimum length of ${
config.minBodyLength} characters`); config.minBodyLength} characters`);
return false; return false;
@ -157,7 +158,7 @@ export function validateCommitMessage(
const bodyByLine = commit.body.split('\n'); const bodyByLine = commit.body.split('\n');
if (bodyByLine.some(line => line.length > config.maxLineLength)) { if (bodyByLine.some(line => line.length > config.maxLineLength)) {
printError( 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; return false;
} }

View File

@ -63,8 +63,8 @@ export async function discoverNewConflictsForPr(
process.exit(1); process.exit(1);
} }
/** The active github branch when the run began. */ /** The active github branch or revision before we performed any Git commands. */
const originalBranch = git.getCurrentBranch(); const previousBranchOrRevision = git.getCurrentBranchOrRevision();
/* Progress bar to indicate progress. */ /* Progress bar to indicate progress. */
const progressBar = new Bar({format: `[{bar}] ETA: {eta}s | {value}/{total}`}); const progressBar = new Bar({format: `[{bar}] ETA: {eta}s | {value}/{total}`});
/* PRs which were found to be conflicting. */ /* PRs which were found to be conflicting. */
@ -103,7 +103,7 @@ export async function discoverNewConflictsForPr(
const result = exec(`git rebase FETCH_HEAD`); const result = exec(`git rebase FETCH_HEAD`);
if (result.code) { if (result.code) {
error('The requested PR currently has conflicts'); error('The requested PR currently has conflicts');
cleanUpGitState(originalBranch); cleanUpGitState(previousBranchOrRevision);
process.exit(1); process.exit(1);
} }
@ -130,7 +130,7 @@ export async function discoverNewConflictsForPr(
info(); info();
info(`Result:`); info(`Result:`);
cleanUpGitState(originalBranch); cleanUpGitState(previousBranchOrRevision);
// If no conflicts are found, exit successfully. // If no conflicts are found, exit successfully.
if (conflicts.length === 0) { if (conflicts.length === 0) {
@ -147,14 +147,14 @@ export async function discoverNewConflictsForPr(
process.exit(1); process.exit(1);
} }
/** Reset git back to the provided branch. */ /** Reset git back to the provided branch or revision. */
export function cleanUpGitState(branch: string) { export function cleanUpGitState(previousBranchOrRevision: string) {
// Ensure that any outstanding rebases are aborted. // Ensure that any outstanding rebases are aborted.
exec(`git rebase --abort`); exec(`git rebase --abort`);
// Ensure that any changes in the current repo state are cleared. // Ensure that any changes in the current repo state are cleared.
exec(`git reset --hard`); exec(`git reset --hard`);
// Checkout the original branch from before the run began. // Checkout the original branch from before the run began.
exec(`git checkout ${branch}`); exec(`git checkout ${previousBranchOrRevision}`);
// Delete the generated branch. // Delete the generated branch.
exec(`git branch -D ${tempWorkingBranch}`); 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 // 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. // the rebase seems interactive to Git, while it's not interactive to the user.
// See: https://github.com/git/git/commit/891d4a0313edc03f7e2ecb96edec5d30dc182294. // See: https://github.com/git/git/commit/891d4a0313edc03f7e2ecb96edec5d30dc182294.
const branchBeforeRebase = this.git.getCurrentBranch(); const branchOrRevisionBeforeRebase = this.git.getCurrentBranchOrRevision();
const rebaseEnv = const rebaseEnv =
needsCommitMessageFixup ? undefined : {...process.env, GIT_SEQUENCE_EDITOR: 'true'}; needsCommitMessageFixup ? undefined : {...process.env, GIT_SEQUENCE_EDITOR: 'true'};
this.git.run( this.git.run(
@ -69,9 +69,9 @@ export class AutosquashMergeStrategy extends MergeStrategy {
// Update pull requests commits to reference the pull request. This matches what // 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 // 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. // 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 // Note: The filter-branch command relies on the working tree, so we want to make sure
// sure that we are on the initial branch where the merge script has been run. // that we are on the initial branch or revision where the merge script has been invoked.
this.git.run(['checkout', '-f', branchBeforeRebase]); this.git.run(['checkout', '-f', branchOrRevisionBeforeRebase]);
this.git.run( this.git.run(
['filter-branch', '-f', '--msg-filter', `${MSG_FILTER_SCRIPT} ${prNumber}`, revisionRange]); ['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 {GithubApiMergeStrategy} from './strategies/api-merge';
import {AutosquashMergeStrategy} from './strategies/autosquash-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. */ /** Describes the status of a pull request merge. */
export const enum MergeStatus { export const enum MergeStatus {
UNKNOWN_GIT_ERROR, UNKNOWN_GIT_ERROR,
@ -56,8 +53,19 @@ export class PullRequestMergeTask {
* @param force Whether non-critical pull request failures should be ignored. * @param force Whether non-critical pull request failures should be ignored.
*/ */
async merge(prNumber: number, force = false): Promise<MergeResult> { async merge(prNumber: number, force = false): Promise<MergeResult> {
// Assert the authenticated GitClient has access on the required scopes. // Check whether the given Github token has sufficient permissions for writing
const hasOauthScopes = await this.git.hasOauthScopes(...REQUIRED_SCOPES); // 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) { if (hasOauthScopes !== true) {
return { return {
status: MergeStatus.GITHUB_ERROR, status: MergeStatus.GITHUB_ERROR,
@ -87,14 +95,14 @@ export class PullRequestMergeTask {
new GithubApiMergeStrategy(this.git, this.config.githubApiMerge) : new GithubApiMergeStrategy(this.git, this.config.githubApiMerge) :
new AutosquashMergeStrategy(this.git); new AutosquashMergeStrategy(this.git);
// Branch that is currently checked out so that we can switch back to it once // Branch or revision that is currently checked out so that we can switch back to
// the pull request has been merged. // it once the pull request has been merged.
let previousBranch: null|string = null; let previousBranchOrRevision: null|string = null;
// The following block runs Git commands as child processes. These Git commands can fail. // 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. // We want to capture these command errors and return an appropriate merge request status.
try { try {
previousBranch = this.git.getCurrentBranch(); previousBranchOrRevision = this.git.getCurrentBranchOrRevision();
// Run preparations for the merge (e.g. fetching branches). // Run preparations for the merge (e.g. fetching branches).
await strategy.prepare(pullRequest); 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 // 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. // 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); await strategy.cleanup(pullRequest);
@ -123,8 +131,8 @@ export class PullRequestMergeTask {
} finally { } finally {
// Always try to restore the branch if possible. We don't want to leave // Always try to restore the branch if possible. We don't want to leave
// the repository in a different state than before. // the repository in a different state than before.
if (previousBranch !== null) { if (previousBranchOrRevision !== null) {
this.git.runGraceful(['checkout', '-f', previousBranch]); 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 * The branch or revision originally checked out before this method performed
* operations that may change the working branch. * any Git operations that may change the working branch.
*/ */
const originalBranch = git.getCurrentBranch(); const previousBranchOrRevision = git.getCurrentBranchOrRevision();
/* Get the PR information from Github. */ /* Get the PR information from Github. */
const pr = await getPr(PR_SCHEMA, prNumber, config.github); const pr = await getPr(PR_SCHEMA, prNumber, config.github);
@ -121,7 +121,7 @@ export async function rebasePr(
info(); info();
info(`To abort the rebase and return to the state of the repository before this command`); info(`To abort the rebase and return to the state of the repository before this command`);
info(`run the following 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); process.exit(1);
} else { } else {
info(`Cleaning up git state, and restoring previous state.`); 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. // Ensure that any changes in the current repo state are cleared.
git.runGraceful(['reset', '--hard'], {stdio: 'ignore'}); git.runGraceful(['reset', '--hard'], {stdio: 'ignore'});
// Checkout the original branch from before the run began. // 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; name: string;
/** If SSH protocol should be used for git interactions. */ /** If SSH protocol should be used for git interactions. */
useSsh?: boolean; 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}; 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. */ /** Error for failed Git commands. */
export class GitCommandError extends Error { export class GitCommandError extends Error {
constructor(client: GitClient, public args: string[]) { constructor(client: GitClient, public args: string[]) {
@ -119,9 +122,16 @@ export class GitClient {
return this.run(['branch', branchName, '--contains', sha]).stdout !== ''; return this.run(['branch', branchName, '--contains', sha]).stdout !== '';
} }
/** Gets the currently checked out branch. */ /** Gets the currently checked out branch or revision. */
getCurrentBranch(): string { getCurrentBranchOrRevision(): string {
return this.run(['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim(); 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. */ /** 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 * Assert the GitClient instance is using a token with permissions for the all of the
* provided OAuth scopes. * provided OAuth scopes.
*/ */
async hasOauthScopes(...requestedScopes: string[]): Promise<true|{error: string}> { async hasOauthScopes(testFn: OAuthScopeTestFunction): Promise<true|{error: string}> {
const missingScopes: string[] = [];
const scopes = await this.getAuthScopesForToken(); const scopes = await this.getAuthScopesForToken();
requestedScopes.forEach(scope => { const missingScopes: string[] = [];
if (!scopes.includes(scope)) { // Test Github OAuth scopes and collect missing ones.
missingScopes.push(scope); testFn(scopes, missingScopes);
}
});
// If no missing scopes are found, return true to indicate all OAuth Scopes are available. // If no missing scopes are found, return true to indicate all OAuth Scopes are available.
if (missingScopes.length === 0) { if (missingScopes.length === 0) {
return true; 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). 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 ```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. Using yarn ensures that you are running the correct version of Bazel.
(Read more about building Angular with Bazel [here](./BAZEL.md).) (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: //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)
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) 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. If you modify a public API, you must accept the new golden file.
To do so, execute the following Bazel target: 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": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 2987, "runtime-es2015": 2987,
"main-es2015": 451406, "main-es2015": 450883,
"polyfills-es2015": 52630 "polyfills-es2015": 52630
} }
} }
@ -21,7 +21,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 3097, "runtime-es2015": 3097,
"main-es2015": 429710, "main-es2015": 429200,
"polyfills-es2015": 52195 "polyfills-es2015": 52195
} }
} }

View File

@ -30,7 +30,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 1485, "runtime-es2015": 1485,
"main-es2015": 136302, "main-es2015": 135533,
"polyfills-es2015": 37248 "polyfills-es2015": 37248
} }
} }
@ -39,7 +39,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 2289, "runtime-es2015": 2289,
"main-es2015": 246085, "main-es2015": 245488,
"polyfills-es2015": 36938, "polyfills-es2015": 36938,
"5-es2015": 751 "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): 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): (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": "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. // Including systemjs because it defines `__eval`, which produces correct stack traces.
'test-events.js', 'test-events.js',
'shims_for_IE.js', 'third_party/shims_for_IE.js',
'node_modules/systemjs/dist/system.src.js', 'node_modules/systemjs/dist/system.src.js',
// Serve polyfills necessary for testing the `elements` package. // Serve polyfills necessary for testing the `elements` package.

View File

@ -125,10 +125,11 @@ export function resolveModuleName(
compilerHost: ts.ModuleResolutionHost&Pick<ts.CompilerHost, 'resolveModuleNames'>, compilerHost: ts.ModuleResolutionHost&Pick<ts.CompilerHost, 'resolveModuleNames'>,
moduleResolutionCache: ts.ModuleResolutionCache|null): ts.ResolvedModule|undefined { moduleResolutionCache: ts.ModuleResolutionCache|null): ts.ResolvedModule|undefined {
if (compilerHost.resolveModuleNames) { if (compilerHost.resolveModuleNames) {
// FIXME: Additional parameters are required in TS3.6, but ignored in 3.5. return compilerHost.resolveModuleNames(
// Remove the any cast once google3 is fully on TS3.6. [moduleName], containingFile,
return (compilerHost as any) undefined, // reusedNames
.resolveModuleNames([moduleName], containingFile, undefined, undefined, compilerOptions)[0]; undefined, // redirectedReference
compilerOptions)[0];
} else { } else {
return ts return ts
.resolveModuleName( .resolveModuleName(

View File

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

View File

@ -99,8 +99,12 @@ let nextNgElementId = 0;
export function bloomAdd( export function bloomAdd(
injectorIndex: number, tView: TView, type: Type<any>|InjectionToken<any>|string): void { injectorIndex: number, tView: TView, type: Type<any>|InjectionToken<any>|string): void {
ngDevMode && assertEqual(tView.firstCreatePass, true, 'expected firstCreatePass to be true'); ngDevMode && assertEqual(tView.firstCreatePass, true, 'expected firstCreatePass to be true');
let id: number|undefined = let id: number|undefined;
typeof type !== 'string' ? (type as any)[NG_ELEMENT_ID] : type.charCodeAt(0) || 0; 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, // 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. // 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 { export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): string|null {
ngDevMode && ngDevMode &&
assertNodeOfPossibleTypes( assertNodeOfPossibleTypes(
tNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer); tNode, [TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer]);
ngDevMode && assertDefined(tNode, 'expecting tNode'); ngDevMode && assertDefined(tNode, 'expecting tNode');
if (attrNameToInject === 'class') { if (attrNameToInject === 'class') {
return tNode.classes; return tNode.classes;
@ -584,7 +588,9 @@ export function bloomHashBitOrFactory(token: Type<any>|InjectionToken<any>|strin
if (typeof token === 'string') { if (typeof token === 'string') {
return token.charCodeAt(0) || 0; 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` // Negative token IDs are used for special objects such as `Injector`
return (typeof tokenId === 'number' && tokenId > 0) ? tokenId & BLOOM_MASK : tokenId; 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 {DEFAULT_LOCALE_ID, getPluralCase} from '../i18n/localization';
import {getTemplateContent, SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS} from '../sanitization/html_sanitizer'; 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 {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
import {addAllToArray} from '../util/array_utils'; import {addAllToArray} from '../util/array_utils';
import {assertDataInRange, assertDefined, assertEqual} from '../util/assert'; import {assertDataInRange, assertDefined, assertEqual} from '../util/assert';
@ -1233,7 +1233,7 @@ function icuStart(
function parseIcuCase( function parseIcuCase(
unsafeHtml: string, parentIndex: number, nestedIcus: IcuExpression[], tIcus: TIcu[], unsafeHtml: string, parentIndex: number, nestedIcus: IcuExpression[], tIcus: TIcu[],
expandoStartIndex: number): IcuCase { expandoStartIndex: number): IcuCase {
const inertBodyHelper = new InertBodyHelper(getDocument()); const inertBodyHelper = getInertBodyHelper(getDocument());
const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml); const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
if (!inertBodyElement) { if (!inertBodyElement) {
throw new Error('Unable to generate inert body element'); 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 {ɵɵinject} from '../../di/injector_compatibility';
import {Type} from '../../interface/type'; import {Type} from '../../interface/type';
import {getOrCreateInjectable, injectAttributeImpl} from '../di'; import {getOrCreateInjectable, injectAttributeImpl} from '../di';
import {TDirectiveHostNode, TNodeType} from '../interfaces/node'; import {TDirectiveHostNode} from '../interfaces/node';
import {assertNodeOfPossibleTypes} from '../node_assert';
import {getLView, getPreviousOrParentTNode} from '../state'; import {getLView, getPreviousOrParentTNode} from '../state';
/** /**

View File

@ -128,7 +128,7 @@ function listenerInternal(
ngDevMode && ngDevMode &&
assertNodeOfPossibleTypes( assertNodeOfPossibleTypes(
tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer); tNode, [TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer]);
let processOutputs = true; let processOutputs = true;

View File

@ -15,6 +15,7 @@ import {assertDataInRange, assertDefined, assertDomNode, assertEqual, assertGrea
import {createNamedArrayType} from '../../util/named_array_type'; import {createNamedArrayType} from '../../util/named_array_type';
import {initNgDevMode} from '../../util/ng_dev_mode'; import {initNgDevMode} from '../../util/ng_dev_mode';
import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../../util/ng_reflect'; import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../../util/ng_reflect';
import {stringify} from '../../util/stringify';
import {assertFirstCreatePass, assertLContainer, assertLView} from '../assert'; import {assertFirstCreatePass, assertLContainer, assertLView} from '../assert';
import {attachPatchData} from '../context_discovery'; import {attachPatchData} from '../context_discovery';
import {getFactoryDef} from '../definition'; import {getFactoryDef} from '../definition';
@ -272,7 +273,7 @@ export function assignTViewNodeToLView(
let tNode = tView.node; let tNode = tView.node;
if (tNode == null) { if (tNode == null) {
ngDevMode && tParentNode && ngDevMode && tParentNode &&
assertNodeOfPossibleTypes(tParentNode, TNodeType.Element, TNodeType.Container); assertNodeOfPossibleTypes(tParentNode, [TNodeType.Element, TNodeType.Container]);
tView.node = tNode = createTNode( tView.node = tNode = createTNode(
tView, tView,
tParentNode as TElementNode | TContainerNode | null, // 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. * Constructs a TNode object from the arguments.
* *
@ -1278,7 +1263,7 @@ function instantiateAllDirectives(
const isComponent = isComponentDef(def); const isComponent = isComponentDef(def);
if (isComponent) { if (isComponent) {
ngDevMode && assertNodeOfPossibleTypes(tNode, TNodeType.Element); ngDevMode && assertNodeOfPossibleTypes(tNode, [TNodeType.Element]);
addComponentLogic(lView, tNode as TElementNode, def as ComponentDef<any>); addComponentLogic(lView, tNode as TElementNode, def as ComponentDef<any>);
} }
@ -1366,7 +1351,7 @@ function findDirectiveDefMatches(
ngDevMode && assertFirstCreatePass(tView); ngDevMode && assertFirstCreatePass(tView);
ngDevMode && ngDevMode &&
assertNodeOfPossibleTypes( assertNodeOfPossibleTypes(
tNode, TNodeType.Element, TNodeType.ElementContainer, TNodeType.Container); tNode, [TNodeType.Element, TNodeType.ElementContainer, TNodeType.Container]);
const registry = tView.directiveRegistry; const registry = tView.directiveRegistry;
let matches: any[]|null = null; let matches: any[]|null = null;
if (registry) { if (registry) {
@ -1377,6 +1362,12 @@ function findDirectiveDefMatches(
diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, viewData), tView, def.type); diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, viewData), tView, def.type);
if (isComponentDef(def)) { 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); if (tNode.flags & TNodeFlags.isComponentHost) throwMultipleComponentError(tNode);
markAsComponentHost(tView, tNode); markAsComponentHost(tView, tNode);
// The component is always stored first with directives after. // 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)}`); 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'); assertDefined(tNode, 'should be called with a TNode');
const found = types.some(type => tNode.type === type); const found = types.some(type => tNode.type === type);
assertEqual( assertEqual(
found, true, 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 { 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 { } else {
// We are inserting a root element of the component view into the component host element and // We are inserting a root element of the component view into the component host element and
// it should always be eager. // it should always be eager.
ngDevMode && assertNodeOfPossibleTypes(hostTNode, TNodeType.Element); ngDevMode && assertNodeOfPossibleTypes(hostTNode, [TNodeType.Element]);
return currentView[HOST]; return currentView[HOST];
} }
} else { } else {
@ -698,10 +698,10 @@ export function appendChild(
*/ */
function getFirstNativeNode(lView: LView, tNode: TNode|null): RNode|null { function getFirstNativeNode(lView: LView, tNode: TNode|null): RNode|null {
if (tNode !== null) { if (tNode !== null) {
ngDevMode && ngDevMode && assertNodeOfPossibleTypes(tNode, [
assertNodeOfPossibleTypes( TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer, TNodeType.IcuContainer,
tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer, TNodeType.Projection
TNodeType.IcuContainer, TNodeType.Projection); ]);
const tNodeType = tNode.type; const tNodeType = tNode.type;
if (tNodeType === TNodeType.Element) { if (tNodeType === TNodeType.Element) {
@ -778,10 +778,10 @@ function applyNodes(
renderParent: RElement|null, beforeNode: RNode|null, isProjection: boolean) { renderParent: RElement|null, beforeNode: RNode|null, isProjection: boolean) {
while (tNode != null) { while (tNode != null) {
ngDevMode && assertTNodeForLView(tNode, lView); ngDevMode && assertTNodeForLView(tNode, lView);
ngDevMode && ngDevMode && assertNodeOfPossibleTypes(tNode, [
assertNodeOfPossibleTypes( TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer, TNodeType.Projection,
tNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer, TNodeType.IcuContainer
TNodeType.Projection, TNodeType.Projection, TNodeType.IcuContainer); ]);
const rawSlotValue = lView[tNode.index]; const rawSlotValue = lView[tNode.index];
const tNodeType = tNode.type; const tNodeType = tNode.type;
if (isProjection) { if (isProjection) {
@ -798,7 +798,7 @@ function applyNodes(
applyProjectionRecursive( applyProjectionRecursive(
renderer, action, lView, tNode as TProjectionNode, renderParent, beforeNode); renderer, action, lView, tNode as TProjectionNode, renderParent, beforeNode);
} else { } else {
ngDevMode && assertNodeOfPossibleTypes(tNode, TNodeType.Element, TNodeType.Container); ngDevMode && assertNodeOfPossibleTypes(tNode, [TNodeType.Element, TNodeType.Container]);
applyToElementOrContainer(action, renderer, renderParent, rawSlotValue, beforeNode); 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) { } else if (read === ViewContainerRef) {
ngDevMode && ngDevMode &&
assertNodeOfPossibleTypes( assertNodeOfPossibleTypes(
tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer); tNode, [TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer]);
return createContainerRef( return createContainerRef(
ViewContainerRef, ViewEngine_ElementRef, ViewContainerRef, ViewEngine_ElementRef,
tNode as TElementNode | TContainerNode | TElementContainerNode, lView); tNode as TElementNode | TContainerNode | TElementContainerNode, lView);

View File

@ -340,7 +340,7 @@ export function createContainerRef(
ngDevMode && ngDevMode &&
assertNodeOfPossibleTypes( assertNodeOfPossibleTypes(
hostTNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer); hostTNode, [TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer]);
let lContainer: LContainer; let lContainer: LContainer;
const slotValue = hostView[hostTNode.index]; 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 {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref';
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref'; import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref';
import {assertDefined} from '../util/assert'; 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 {CONTAINER_HEADER_OFFSET} from './interfaces/container';
import {TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node'; import {TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
import {isLContainer} from './interfaces/type_checks'; import {isLContainer} from './interfaces/type_checks';
@ -88,7 +88,7 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, viewEngine_Int
} }
onDestroy(callback: Function) { 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[], tView: TView, lView: LView, tNode: TNode|null, result: any[],
isProjection: boolean = false): any[] { isProjection: boolean = false): any[] {
while (tNode !== null) { while (tNode !== null) {
ngDevMode && ngDevMode && assertNodeOfPossibleTypes(tNode, [
assertNodeOfPossibleTypes( TNodeType.Element, TNodeType.Container, TNodeType.Projection, TNodeType.ElementContainer,
tNode, TNodeType.Element, TNodeType.Container, TNodeType.Projection, TNodeType.IcuContainer
TNodeType.ElementContainer, TNodeType.IcuContainer); ]);
const lNode = lView[tNode.index]; const lNode = lView[tNode.index];
if (lNode !== null) { if (lNode !== null) {

View File

@ -7,7 +7,7 @@
*/ */
import {isDevMode} from '../util/is_dev_mode'; import {isDevMode} from '../util/is_dev_mode';
import {InertBodyHelper} from './inert_body'; import {getInertBodyHelper, InertBodyHelper} from './inert_body';
import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer'; import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer';
function tagSet(tags: string): {[k: string]: boolean} { function tagSet(tags: string): {[k: string]: boolean} {
@ -245,7 +245,7 @@ let inertBodyHelper: InertBodyHelper;
export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string { export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
let inertBodyElement: HTMLElement|null = null; let inertBodyElement: HTMLElement|null = null;
try { try {
inertBodyHelper = inertBodyHelper || new InertBodyHelper(defaultDoc); inertBodyHelper = inertBodyHelper || getInertBodyHelper(defaultDoc);
// Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime). // Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime).
let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : ''; let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : '';
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml); 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. * that needs sanitizing.
* Depending upon browser support we must use one of three strategies for doing this. * Depending upon browser support we use one of two strategies for doing this.
* Support: Safari 10.x -> XHR strategy * Default: DOMParser strategy
* Support: Firefox -> DomParser strategy * Fallback: InertDocument strategy
* Default: InertDocument strategy
*/ */
export class InertBodyHelper { export function getInertBodyHelper(defaultDoc: Document): InertBodyHelper {
private inertDocument: Document; return isDOMParserAvailable() ? new DOMParserHelper() : new InertDocumentHelper(defaultDoc);
}
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 interface InertBodyHelper {
/** /**
* Get an inert DOM element containing DOM created from the dirty HTML string provided. * 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; getInertBodyElement: (html: string) => HTMLElement | null;
}
/** /**
* Use XHR to create and fill an inert body element (on Safari 10.1) * Uses DOMParser to create and fill an inert body element.
* See * This is the default strategy used in browsers that support it.
* https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449 */
*/ class DOMParserHelper implements InertBodyHelper {
private getInertBodyElement_XHR(html: string) { 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.
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) {
// We add these extra elements to ensure that the rest of the content is parsed as expected // 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 // e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
// `<head>` tag. // `<head>` tag.
@ -103,14 +43,30 @@ export class InertBodyHelper {
return null; return null;
} }
} }
}
/** /**
* Use an HTML5 `template` element, if supported, or an inert body element created via * Use an HTML5 `template` element, if supported, or an inert body element created via
* `createHtmlDocument` to create and fill an inert DOM element. * `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 * This is the fallback strategy if the browser does not support DOMParser.
* strategies above. */
*/ class InertDocumentHelper implements InertBodyHelper {
private getInertBodyElement_InertDocument(html: string) { 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. // Prefer using <template> element if supported.
const templateEl = this.inertDocument.createElement('template'); const templateEl = this.inertDocument.createElement('template');
if ('content' in templateEl) { if ('content' in templateEl) {
@ -164,15 +120,15 @@ export class InertBodyHelper {
} }
/** /**
* We need to determine whether the DOMParser exists in the global context. * We need to determine whether the DOMParser exists in the global context and
* The try-catch is because, on some browsers, trying to access this property * supports parsing HTML; HTML parsing support is not as wide as other formats, see
* on window can actually throw an error. * https://developer.mozilla.org/en-US/docs/Web/API/DOMParser#Browser_compatibility.
* *
* @suppress {uselessCode} * @suppress {uselessCode}
*/ */
function isDOMParserAvailable() { export function isDOMParserAvailable() {
try { try {
return !!(window as any).DOMParser; return !!new (window as any).DOMParser().parseFromString('', 'text/html');
} catch { } catch {
return false; 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. * 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 */ /* A pattern that matches safe srcset values */
const SAFE_SRCSET_PATTERN = /^(?:(?:https?|file):|[^&:/?#]*(?:[/?#]|$))/gi; 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 {TestBed} from '@angular/core/testing';
import {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser'; import {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers'; 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'; import {domRendererFactory3} from '../../src/render3/interfaces/renderer';
@ -259,6 +259,65 @@ describe('component', () => {
expect(wrapperEls.length).toBe(2); // other elements are preserved 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', () => { it('should use a new ngcontent attribute for child elements created w/ Renderer2', () => {
@Component({ @Component({
selector: 'app-root', selector: 'app-root',

View File

@ -7,9 +7,9 @@
*/ */
import {CommonModule} from '@angular/common'; 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 {ɵ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 {TestBed} from '@angular/core/testing';
import {ivyEnabled, onlyInIvy} from '@angular/private/testing'; import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
import {BehaviorSubject} from 'rxjs'; import {BehaviorSubject} from 'rxjs';
@ -1627,7 +1627,8 @@ describe('di', () => {
TestBed.configureTestingModule({declarations: [MyApp, MyPipe], imports: [CommonModule]}); TestBed.configureTestingModule({declarations: [MyApp, MyPipe], imports: [CommonModule]});
const fixture = TestBed.createComponent(MyApp); const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges(); 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', it('should inject current component ChangeDetectorRef into directives on the same node as components',
@ -1643,7 +1644,7 @@ describe('di', () => {
fixture.detectChanges(); fixture.detectChanges();
const app = fixture.componentInstance; const app = fixture.componentInstance;
const comp = fixture.componentInstance.component; 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 // ChangeDetectorRef is the token, ViewRef has historically been the constructor
expect(app.directive.value).toContain('ViewRef'); expect(app.directive.value).toContain('ViewRef');
@ -1664,7 +1665,7 @@ describe('di', () => {
const fixture = TestBed.createComponent(MyComp); const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges(); fixture.detectChanges();
const comp = fixture.componentInstance; 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 // ChangeDetectorRef is the token, ViewRef has historically been the constructor
expect(comp.directive.value).toContain('ViewRef'); expect(comp.directive.value).toContain('ViewRef');
@ -1692,7 +1693,7 @@ describe('di', () => {
const fixture = TestBed.createComponent(MyApp); const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges(); fixture.detectChanges();
const app = fixture.componentInstance; 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; const comp = fixture.componentInstance.component;
// ChangeDetectorRef is the token, ViewRef has historically been the constructor // ChangeDetectorRef is the token, ViewRef has historically been the constructor
expect(app.directive.value).toContain('ViewRef'); expect(app.directive.value).toContain('ViewRef');
@ -1720,7 +1721,7 @@ describe('di', () => {
const fixture = TestBed.createComponent(MyComp); const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges(); fixture.detectChanges();
const comp = fixture.componentInstance; 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 // ChangeDetectorRef is the token, ViewRef has historically been the constructor
expect(comp.directive.value).toContain('ViewRef'); expect(comp.directive.value).toContain('ViewRef');
@ -1743,7 +1744,7 @@ describe('di', () => {
const fixture = TestBed.createComponent(MyComp); const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges(); fixture.detectChanges();
const comp = fixture.componentInstance; 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 // ChangeDetectorRef is the token, ViewRef has historically been the constructor
expect(comp.directive.value).toContain('ViewRef'); expect(comp.directive.value).toContain('ViewRef');
@ -1773,7 +1774,8 @@ describe('di', () => {
TestBed.configureTestingModule({declarations: [MyApp, MyDirective]}); TestBed.configureTestingModule({declarations: [MyApp, MyDirective]});
const fixture = TestBed.createComponent(MyApp); const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges(); 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'); 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 * 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 {InternalViewRef} from '@angular/core/src/linker/view_ref';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
@ -54,4 +54,22 @@ describe('ViewRef', () => {
fixture.detectChanges(); fixture.detectChanges();
expect(document.body.querySelector('dynamic-cpt')).toBeFalsy(); 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 {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer'; import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer';
import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
{ {
describe('HTML sanitizer', () => { 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 * 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 {$$, $_, isAsciiLetter, isDigit} from '@angular/compiler/src/chars';
import {ATTR, getBindingDescriptor} from './binding_utils'; import {ATTR, getBindingDescriptor} from './binding_utils';
@ -127,72 +127,18 @@ function getBoundedWordSpan(
export function getTemplateCompletions( export function getTemplateCompletions(
templateInfo: ng.AstResult, position: number): ng.CompletionEntry[] { templateInfo: ng.AstResult, position: number): ng.CompletionEntry[] {
let result: ng.CompletionEntry[] = [];
const {htmlAst, template} = templateInfo; 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 templatePosition = position - template.span.start;
const path = getPathToNodeAtPosition(htmlAst, templatePosition); const htmlPath: HtmlAstPath = getPathToNodeAtPosition(htmlAst, templatePosition);
const mostSpecific = path.tail; const mostSpecific = htmlPath.tail;
if (path.empty || !mostSpecific) { const visitor = new HtmlVisitor(templateInfo, htmlPath);
result = elementCompletions(templateInfo); const results: ng.CompletionEntry[] = mostSpecific ?
} else { mostSpecific.visit(visitor, null /* context */) :
const astPosition = templatePosition - mostSpecific.sourceSpan.start.offset; elementCompletions(templateInfo);
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 replacementSpan = getBoundedWordSpan(templateInfo, position, mostSpecific); const replacementSpan = getBoundedWordSpan(templateInfo, position, mostSpecific);
return result.map(entry => { return results.map(entry => {
return { return {
...entry, ...entry,
replacementSpan, 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[] { function attributeCompletions(info: ng.AstResult, path: AstPath<HtmlAst>): ng.CompletionEntry[] {
const attr = path.tail; const attr = path.tail;
const elem = path.parentOf(attr); const elem = path.parentOf(attr);
@ -356,18 +374,6 @@ function elementCompletions(info: ng.AstResult): ng.CompletionEntry[] {
return results; 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 // 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 // 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 // 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 {createLanguageService} from './language_service';
import {TypeScriptServiceHost} from './typescript_host'; 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 { 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: // This plugin could operate under two different modes:
// 1. TS + Angular // 1. TS + Angular
// Plugin augments TS language service to provide additional 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 angularOnly = config ? config.angularOnly === true : false;
const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS); const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS);
const ngLS = createLanguageService(ngLSHost); const ngLS = createLanguageService(ngLSHost);
PROJECT_MAP.set(project, ngLSHost);
function getCompletionsAtPosition( function getCompletionsAtPosition(
fileName: string, position: number, options: tss.GetCompletionsAtPositionOptions|undefined) { 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 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. * Checks whether the program has changed and returns all analyzed modules.
* If program has changed, invalidate all caches and update fileToComponent * If program has changed, invalidate all caches and update fileToComponent

View File

@ -841,6 +841,13 @@ describe('completions', () => {
'trim', '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( function expectContain(

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {create} from '../src/ts_plugin'; import {create, getExternalFiles} from '../src/ts_plugin';
import {CompletionKind} from '../src/types'; import {CompletionKind} from '../src/types';
import {MockTypescriptHost} from './test_utils'; 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`, () => { describe(`with config 'angularOnly = true`, () => {

View File

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