Merge remote-tracking branch 'en/master' into aio
This commit is contained in:
commit
cababcc4ec
|
@ -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/**',
|
||||
|
|
|
@ -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",
|
||||
|
|
23
CHANGELOG.md
23
CHANGELOG.md
|
@ -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)
|
||||
|
||||
|
|
|
@ -6,5 +6,5 @@ import { Component } from '@angular/core';
|
|||
templateUrl: './app.component.html'
|
||||
})
|
||||
export class AppComponent {
|
||||
birthday = new Date(1988, 3, 15); // April 15, 1988
|
||||
birthday = new Date(1988, 3, 15); // April 15, 1988 -- since month parameter is zero-based
|
||||
}
|
||||
|
|
|
@ -8,5 +8,5 @@ import { Component } from '@angular/core';
|
|||
// #enddocregion hero-birthday-template
|
||||
})
|
||||
export class HeroBirthdayComponent {
|
||||
birthday = new Date(1988, 3, 15); // April 15, 1988
|
||||
birthday = new Date(1988, 3, 15); // April 15, 1988 -- since month parameter is zero-based
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { Component } from '@angular/core';
|
|||
})
|
||||
// #docregion class
|
||||
export class HeroBirthday2Component {
|
||||
birthday = new Date(1988, 3, 15); // April 15, 1988
|
||||
birthday = new Date(1988, 3, 15); // April 15, 1988 -- since month parameter is zero-based
|
||||
toggle = true; // start with true == shortDate
|
||||
|
||||
get format() { return this.toggle ? 'shortDate' : 'fullDate'; }
|
||||
|
|
|
@ -11,6 +11,7 @@ import {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
|
|||
export interface CommitMessageConfig {
|
||||
maxLineLength: number;
|
||||
minBodyLength: number;
|
||||
minBodyLengthTypeExcludes?: string[];
|
||||
types: string[];
|
||||
scopes: string[];
|
||||
}
|
||||
|
@ -19,7 +20,7 @@ export interface CommitMessageConfig {
|
|||
export function getCommitMessageConfig() {
|
||||
// List of errors encountered validating the config.
|
||||
const errors: string[] = [];
|
||||
// The unvalidated config object.
|
||||
// The non-validated config object.
|
||||
const config: Partial<NgDevConfig<{commitMessage: CommitMessageConfig}>> = getConfig();
|
||||
|
||||
if (config.commitMessage === undefined) {
|
||||
|
|
|
@ -10,19 +10,22 @@
|
|||
import * as validateConfig from './config';
|
||||
import {validateCommitMessage} from './validate';
|
||||
|
||||
type CommitMessageConfig = validateConfig.CommitMessageConfig;
|
||||
|
||||
|
||||
// Constants
|
||||
const config = {
|
||||
'commitMessage': {
|
||||
'maxLineLength': 120,
|
||||
'minBodyLength': 0,
|
||||
'types': [
|
||||
const config: {commitMessage: CommitMessageConfig} = {
|
||||
commitMessage: {
|
||||
maxLineLength: 120,
|
||||
minBodyLength: 0,
|
||||
types: [
|
||||
'feat',
|
||||
'fix',
|
||||
'refactor',
|
||||
'release',
|
||||
'style',
|
||||
],
|
||||
'scopes': [
|
||||
scopes: [
|
||||
'common',
|
||||
'compiler',
|
||||
'core',
|
||||
|
@ -224,5 +227,42 @@ describe('validate-commit-message.js', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('minBodyLength', () => {
|
||||
const minBodyLengthConfig: {commitMessage: CommitMessageConfig} = {
|
||||
commitMessage: {
|
||||
maxLineLength: 120,
|
||||
minBodyLength: 30,
|
||||
minBodyLengthTypeExcludes: ['docs'],
|
||||
types: ['fix', 'docs'],
|
||||
scopes: ['core']
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(validateConfig.getCommitMessageConfig as jasmine.Spy).and.returnValue(minBodyLengthConfig);
|
||||
});
|
||||
|
||||
it('should fail validation if the body is shorter than `minBodyLength`', () => {
|
||||
expect(validateCommitMessage(
|
||||
'fix(core): something\n\n Explanation of the motivation behind this change'))
|
||||
.toBe(VALID);
|
||||
expect(validateCommitMessage('fix(core): something\n\n too short')).toBe(INVALID);
|
||||
expect(lastError).toContain(
|
||||
'The commit message body does not meet the minimum length of 30 characters');
|
||||
expect(validateCommitMessage('fix(core): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(
|
||||
'The commit message body does not meet the minimum length of 30 characters');
|
||||
});
|
||||
|
||||
it('should pass validation if the body is shorter than `minBodyLength` but the commit type is in the `minBodyLengthTypeExclusions` list',
|
||||
() => {
|
||||
expect(validateCommitMessage('docs: just fixing a typo')).toBe(VALID);
|
||||
expect(validateCommitMessage('docs(core): just fixing a typo')).toBe(VALID);
|
||||
expect(validateCommitMessage(
|
||||
'docs(core): just fixing a typo\n\nThis was just a silly typo.'))
|
||||
.toBe(VALID);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -148,7 +148,8 @@ export function validateCommitMessage(
|
|||
// Checking commit body //
|
||||
//////////////////////////
|
||||
|
||||
if (commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
|
||||
if (!config.minBodyLengthTypeExcludes?.includes(commit.type) &&
|
||||
commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
|
||||
printError(`The commit message body does not meet the minimum length of ${
|
||||
config.minBodyLength} characters`);
|
||||
return false;
|
||||
|
@ -157,7 +158,7 @@ export function validateCommitMessage(
|
|||
const bodyByLine = commit.body.split('\n');
|
||||
if (bodyByLine.some(line => line.length > config.maxLineLength)) {
|
||||
printError(
|
||||
`The commit messsage body contains lines greater than ${config.maxLineLength} characters`);
|
||||
`The commit message body contains lines greater than ${config.maxLineLength} characters`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -63,8 +63,8 @@ export async function discoverNewConflictsForPr(
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
/** The active github branch when the run began. */
|
||||
const originalBranch = git.getCurrentBranch();
|
||||
/** The active github branch or revision before we performed any Git commands. */
|
||||
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
|
||||
/* Progress bar to indicate progress. */
|
||||
const progressBar = new Bar({format: `[{bar}] ETA: {eta}s | {value}/{total}`});
|
||||
/* PRs which were found to be conflicting. */
|
||||
|
@ -103,7 +103,7 @@ export async function discoverNewConflictsForPr(
|
|||
const result = exec(`git rebase FETCH_HEAD`);
|
||||
if (result.code) {
|
||||
error('The requested PR currently has conflicts');
|
||||
cleanUpGitState(originalBranch);
|
||||
cleanUpGitState(previousBranchOrRevision);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
@ -130,7 +130,7 @@ export async function discoverNewConflictsForPr(
|
|||
info();
|
||||
info(`Result:`);
|
||||
|
||||
cleanUpGitState(originalBranch);
|
||||
cleanUpGitState(previousBranchOrRevision);
|
||||
|
||||
// If no conflicts are found, exit successfully.
|
||||
if (conflicts.length === 0) {
|
||||
|
@ -147,14 +147,14 @@ export async function discoverNewConflictsForPr(
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
/** Reset git back to the provided branch. */
|
||||
export function cleanUpGitState(branch: string) {
|
||||
/** Reset git back to the provided branch or revision. */
|
||||
export function cleanUpGitState(previousBranchOrRevision: string) {
|
||||
// Ensure that any outstanding rebases are aborted.
|
||||
exec(`git rebase --abort`);
|
||||
// Ensure that any changes in the current repo state are cleared.
|
||||
exec(`git reset --hard`);
|
||||
// Checkout the original branch from before the run began.
|
||||
exec(`git checkout ${branch}`);
|
||||
exec(`git checkout ${previousBranchOrRevision}`);
|
||||
// Delete the generated branch.
|
||||
exec(`git branch -D ${tempWorkingBranch}`);
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ export class AutosquashMergeStrategy extends MergeStrategy {
|
|||
// is desired, we set the `GIT_SEQUENCE_EDITOR` environment variable to `true` so that
|
||||
// the rebase seems interactive to Git, while it's not interactive to the user.
|
||||
// See: https://github.com/git/git/commit/891d4a0313edc03f7e2ecb96edec5d30dc182294.
|
||||
const branchBeforeRebase = this.git.getCurrentBranch();
|
||||
const branchOrRevisionBeforeRebase = this.git.getCurrentBranchOrRevision();
|
||||
const rebaseEnv =
|
||||
needsCommitMessageFixup ? undefined : {...process.env, GIT_SEQUENCE_EDITOR: 'true'};
|
||||
this.git.run(
|
||||
|
@ -69,9 +69,9 @@ export class AutosquashMergeStrategy extends MergeStrategy {
|
|||
// Update pull requests commits to reference the pull request. This matches what
|
||||
// Github does when pull requests are merged through the Web UI. The motivation is
|
||||
// that it should be easy to determine which pull request contained a given commit.
|
||||
// **Note**: The filter-branch command relies on the working tree, so we want to make
|
||||
// sure that we are on the initial branch where the merge script has been run.
|
||||
this.git.run(['checkout', '-f', branchBeforeRebase]);
|
||||
// Note: The filter-branch command relies on the working tree, so we want to make sure
|
||||
// that we are on the initial branch or revision where the merge script has been invoked.
|
||||
this.git.run(['checkout', '-f', branchOrRevisionBeforeRebase]);
|
||||
this.git.run(
|
||||
['filter-branch', '-f', '--msg-filter', `${MSG_FILTER_SCRIPT} ${prNumber}`, revisionRange]);
|
||||
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
||||
```
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -99,8 +99,12 @@ let nextNgElementId = 0;
|
|||
export function bloomAdd(
|
||||
injectorIndex: number, tView: TView, type: Type<any>|InjectionToken<any>|string): void {
|
||||
ngDevMode && assertEqual(tView.firstCreatePass, true, 'expected firstCreatePass to be true');
|
||||
let id: number|undefined =
|
||||
typeof type !== 'string' ? (type as any)[NG_ELEMENT_ID] : type.charCodeAt(0) || 0;
|
||||
let id: number|undefined;
|
||||
if (typeof type === 'string') {
|
||||
id = type.charCodeAt(0) || 0;
|
||||
} else if (type.hasOwnProperty(NG_ELEMENT_ID)) {
|
||||
id = (type as any)[NG_ELEMENT_ID];
|
||||
}
|
||||
|
||||
// Set a unique ID on the directive type, so if something tries to inject the directive,
|
||||
// we can easily retrieve the ID and hash it into the bloom bit that should be checked.
|
||||
|
@ -267,7 +271,7 @@ export function diPublicInInjector(
|
|||
export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): string|null {
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
tNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer);
|
||||
tNode, [TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer]);
|
||||
ngDevMode && assertDefined(tNode, 'expecting tNode');
|
||||
if (attrNameToInject === 'class') {
|
||||
return tNode.classes;
|
||||
|
@ -584,7 +588,9 @@ export function bloomHashBitOrFactory(token: Type<any>|InjectionToken<any>|strin
|
|||
if (typeof token === 'string') {
|
||||
return token.charCodeAt(0) || 0;
|
||||
}
|
||||
const tokenId: number|undefined = (token as any)[NG_ELEMENT_ID];
|
||||
const tokenId: number|undefined =
|
||||
// First check with `hasOwnProperty` so we don't get an inherited ID.
|
||||
token.hasOwnProperty(NG_ELEMENT_ID) ? (token as any)[NG_ELEMENT_ID] : undefined;
|
||||
// Negative token IDs are used for special objects such as `Injector`
|
||||
return (typeof tokenId === 'number' && tokenId > 0) ? tokenId & BLOOM_MASK : tokenId;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import '../util/ng_i18n_closure_mode';
|
|||
|
||||
import {DEFAULT_LOCALE_ID, getPluralCase} from '../i18n/localization';
|
||||
import {getTemplateContent, SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS} from '../sanitization/html_sanitizer';
|
||||
import {InertBodyHelper} from '../sanitization/inert_body';
|
||||
import {getInertBodyHelper} from '../sanitization/inert_body';
|
||||
import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
|
||||
import {addAllToArray} from '../util/array_utils';
|
||||
import {assertDataInRange, assertDefined, assertEqual} from '../util/assert';
|
||||
|
@ -1233,7 +1233,7 @@ function icuStart(
|
|||
function parseIcuCase(
|
||||
unsafeHtml: string, parentIndex: number, nestedIcus: IcuExpression[], tIcus: TIcu[],
|
||||
expandoStartIndex: number): IcuCase {
|
||||
const inertBodyHelper = new InertBodyHelper(getDocument());
|
||||
const inertBodyHelper = getInertBodyHelper(getDocument());
|
||||
const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
||||
if (!inertBodyElement) {
|
||||
throw new Error('Unable to generate inert body element');
|
||||
|
|
|
@ -9,8 +9,7 @@ import {InjectFlags, InjectionToken, resolveForwardRef} from '../../di';
|
|||
import {ɵɵinject} from '../../di/injector_compatibility';
|
||||
import {Type} from '../../interface/type';
|
||||
import {getOrCreateInjectable, injectAttributeImpl} from '../di';
|
||||
import {TDirectiveHostNode, TNodeType} from '../interfaces/node';
|
||||
import {assertNodeOfPossibleTypes} from '../node_assert';
|
||||
import {TDirectiveHostNode} from '../interfaces/node';
|
||||
import {getLView, getPreviousOrParentTNode} from '../state';
|
||||
|
||||
/**
|
||||
|
|
|
@ -128,7 +128,7 @@ function listenerInternal(
|
|||
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer);
|
||||
tNode, [TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer]);
|
||||
|
||||
let processOutputs = true;
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import {assertDataInRange, assertDefined, assertDomNode, assertEqual, assertGrea
|
|||
import {createNamedArrayType} from '../../util/named_array_type';
|
||||
import {initNgDevMode} from '../../util/ng_dev_mode';
|
||||
import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../../util/ng_reflect';
|
||||
import {stringify} from '../../util/stringify';
|
||||
import {assertFirstCreatePass, assertLContainer, assertLView} from '../assert';
|
||||
import {attachPatchData} from '../context_discovery';
|
||||
import {getFactoryDef} from '../definition';
|
||||
|
@ -272,7 +273,7 @@ export function assignTViewNodeToLView(
|
|||
let tNode = tView.node;
|
||||
if (tNode == null) {
|
||||
ngDevMode && tParentNode &&
|
||||
assertNodeOfPossibleTypes(tParentNode, TNodeType.Element, TNodeType.Container);
|
||||
assertNodeOfPossibleTypes(tParentNode, [TNodeType.Element, TNodeType.Container]);
|
||||
tView.node = tNode = createTNode(
|
||||
tView,
|
||||
tParentNode as TElementNode | TContainerNode | null, //
|
||||
|
@ -794,22 +795,6 @@ export function storeCleanupWithContext(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the cleanup function itself in LView.cleanupInstances.
|
||||
*
|
||||
* This is necessary for functions that are wrapped with their contexts, like in renderer2
|
||||
* listeners.
|
||||
*
|
||||
* On the first template pass, the index of the cleanup function is saved in TView.
|
||||
*/
|
||||
export function storeCleanupFn(tView: TView, lView: LView, cleanupFn: Function): void {
|
||||
getLCleanup(lView).push(cleanupFn);
|
||||
|
||||
if (tView.firstCreatePass) {
|
||||
getTViewCleanup(tView).push(lView[CLEANUP]!.length - 1, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a TNode object from the arguments.
|
||||
*
|
||||
|
@ -1278,7 +1263,7 @@ function instantiateAllDirectives(
|
|||
const isComponent = isComponentDef(def);
|
||||
|
||||
if (isComponent) {
|
||||
ngDevMode && assertNodeOfPossibleTypes(tNode, TNodeType.Element);
|
||||
ngDevMode && assertNodeOfPossibleTypes(tNode, [TNodeType.Element]);
|
||||
addComponentLogic(lView, tNode as TElementNode, def as ComponentDef<any>);
|
||||
}
|
||||
|
||||
|
@ -1366,7 +1351,7 @@ function findDirectiveDefMatches(
|
|||
ngDevMode && assertFirstCreatePass(tView);
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
tNode, TNodeType.Element, TNodeType.ElementContainer, TNodeType.Container);
|
||||
tNode, [TNodeType.Element, TNodeType.ElementContainer, TNodeType.Container]);
|
||||
const registry = tView.directiveRegistry;
|
||||
let matches: any[]|null = null;
|
||||
if (registry) {
|
||||
|
@ -1377,6 +1362,12 @@ function findDirectiveDefMatches(
|
|||
diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, viewData), tView, def.type);
|
||||
|
||||
if (isComponentDef(def)) {
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
tNode, [TNodeType.Element],
|
||||
`"${tNode.tagName}" tags cannot be used as component hosts. ` +
|
||||
`Please use a different tag to activate the ${
|
||||
stringify(def.type)} component.`);
|
||||
if (tNode.flags & TNodeFlags.isComponentHost) throwMultipleComponentError(tNode);
|
||||
markAsComponentHost(tView, tNode);
|
||||
// The component is always stored first with directives after.
|
||||
|
|
|
@ -26,12 +26,14 @@ export function assertNodeType(tNode: TNode, type: TNodeType): asserts tNode is
|
|||
assertEqual(tNode.type, type, `should be a ${typeName(type)}`);
|
||||
}
|
||||
|
||||
export function assertNodeOfPossibleTypes(tNode: TNode|null, ...types: TNodeType[]): void {
|
||||
export function assertNodeOfPossibleTypes(
|
||||
tNode: TNode|null, types: TNodeType[], message?: string): void {
|
||||
assertDefined(tNode, 'should be called with a TNode');
|
||||
const found = types.some(type => tNode.type === type);
|
||||
assertEqual(
|
||||
found, true,
|
||||
`Should be one of ${types.map(typeName).join(', ')} but got ${typeName(tNode.type)}`);
|
||||
message ??
|
||||
`Should be one of ${types.map(typeName).join(', ')} but got ${typeName(tNode.type)}`);
|
||||
}
|
||||
|
||||
export function assertNodeNotOfTypes(tNode: TNode, types: TNodeType[], message?: string): void {
|
||||
|
|
|
@ -552,7 +552,7 @@ function getRenderParent(tView: TView, tNode: TNode, currentView: LView): REleme
|
|||
} else {
|
||||
// We are inserting a root element of the component view into the component host element and
|
||||
// it should always be eager.
|
||||
ngDevMode && assertNodeOfPossibleTypes(hostTNode, TNodeType.Element);
|
||||
ngDevMode && assertNodeOfPossibleTypes(hostTNode, [TNodeType.Element]);
|
||||
return currentView[HOST];
|
||||
}
|
||||
} else {
|
||||
|
@ -698,10 +698,10 @@ export function appendChild(
|
|||
*/
|
||||
function getFirstNativeNode(lView: LView, tNode: TNode|null): RNode|null {
|
||||
if (tNode !== null) {
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer,
|
||||
TNodeType.IcuContainer, TNodeType.Projection);
|
||||
ngDevMode && assertNodeOfPossibleTypes(tNode, [
|
||||
TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer, TNodeType.IcuContainer,
|
||||
TNodeType.Projection
|
||||
]);
|
||||
|
||||
const tNodeType = tNode.type;
|
||||
if (tNodeType === TNodeType.Element) {
|
||||
|
@ -778,10 +778,10 @@ function applyNodes(
|
|||
renderParent: RElement|null, beforeNode: RNode|null, isProjection: boolean) {
|
||||
while (tNode != null) {
|
||||
ngDevMode && assertTNodeForLView(tNode, lView);
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
tNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer,
|
||||
TNodeType.Projection, TNodeType.Projection, TNodeType.IcuContainer);
|
||||
ngDevMode && assertNodeOfPossibleTypes(tNode, [
|
||||
TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer, TNodeType.Projection,
|
||||
TNodeType.IcuContainer
|
||||
]);
|
||||
const rawSlotValue = lView[tNode.index];
|
||||
const tNodeType = tNode.type;
|
||||
if (isProjection) {
|
||||
|
@ -798,7 +798,7 @@ function applyNodes(
|
|||
applyProjectionRecursive(
|
||||
renderer, action, lView, tNode as TProjectionNode, renderParent, beforeNode);
|
||||
} else {
|
||||
ngDevMode && assertNodeOfPossibleTypes(tNode, TNodeType.Element, TNodeType.Container);
|
||||
ngDevMode && assertNodeOfPossibleTypes(tNode, [TNodeType.Element, TNodeType.Container]);
|
||||
applyToElementOrContainer(action, renderer, renderParent, rawSlotValue, beforeNode);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -340,7 +340,7 @@ export function createContainerRef(
|
|||
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
hostTNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer);
|
||||
hostTNode, [TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer]);
|
||||
|
||||
let lContainer: LContainer;
|
||||
const slotValue = hostView[hostTNode.index];
|
||||
|
|
|
@ -11,7 +11,7 @@ import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detec
|
|||
import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref';
|
||||
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref';
|
||||
import {assertDefined} from '../util/assert';
|
||||
import {checkNoChangesInRootView, checkNoChangesInternal, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn} from './instructions/shared';
|
||||
import {checkNoChangesInRootView, checkNoChangesInternal, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupWithContext} from './instructions/shared';
|
||||
import {CONTAINER_HEADER_OFFSET} from './interfaces/container';
|
||||
import {TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
|
||||
import {isLContainer} from './interfaces/type_checks';
|
||||
|
@ -88,7 +88,7 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, viewEngine_Int
|
|||
}
|
||||
|
||||
onDestroy(callback: Function) {
|
||||
storeCleanupFn(this._lView[TVIEW], this._lView, callback);
|
||||
storeCleanupWithContext(this._lView[TVIEW], this._lView, null, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -324,10 +324,10 @@ function collectNativeNodes(
|
|||
tView: TView, lView: LView, tNode: TNode|null, result: any[],
|
||||
isProjection: boolean = false): any[] {
|
||||
while (tNode !== null) {
|
||||
ngDevMode &&
|
||||
assertNodeOfPossibleTypes(
|
||||
tNode, TNodeType.Element, TNodeType.Container, TNodeType.Projection,
|
||||
TNodeType.ElementContainer, TNodeType.IcuContainer);
|
||||
ngDevMode && assertNodeOfPossibleTypes(tNode, [
|
||||
TNodeType.Element, TNodeType.Container, TNodeType.Projection, TNodeType.ElementContainer,
|
||||
TNodeType.IcuContainer
|
||||
]);
|
||||
|
||||
const lNode = lView[tNode.index];
|
||||
if (lNode !== null) {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import {isDevMode} from '../util/is_dev_mode';
|
||||
import {InertBodyHelper} from './inert_body';
|
||||
import {getInertBodyHelper, InertBodyHelper} from './inert_body';
|
||||
import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer';
|
||||
|
||||
function tagSet(tags: string): {[k: string]: boolean} {
|
||||
|
@ -245,7 +245,7 @@ let inertBodyHelper: InertBodyHelper;
|
|||
export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
|
||||
let inertBodyElement: HTMLElement|null = null;
|
||||
try {
|
||||
inertBodyHelper = inertBodyHelper || new InertBodyHelper(defaultDoc);
|
||||
inertBodyHelper = inertBodyHelper || getInertBodyHelper(defaultDoc);
|
||||
// Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime).
|
||||
let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : '';
|
||||
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
||||
|
|
|
@ -7,89 +7,29 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* This helper class is used to get hold of an inert tree of DOM elements containing dirty HTML
|
||||
* This helper is used to get hold of an inert tree of DOM elements containing dirty HTML
|
||||
* that needs sanitizing.
|
||||
* Depending upon browser support we must use one of three strategies for doing this.
|
||||
* Support: Safari 10.x -> XHR strategy
|
||||
* Support: Firefox -> DomParser strategy
|
||||
* Default: InertDocument strategy
|
||||
* Depending upon browser support we use one of two strategies for doing this.
|
||||
* Default: DOMParser strategy
|
||||
* Fallback: InertDocument strategy
|
||||
*/
|
||||
export class InertBodyHelper {
|
||||
private inertDocument: Document;
|
||||
|
||||
constructor(private defaultDoc: Document) {
|
||||
this.inertDocument = this.defaultDoc.implementation.createHTMLDocument('sanitization-inert');
|
||||
let inertBodyElement = this.inertDocument.body;
|
||||
|
||||
if (inertBodyElement == null) {
|
||||
// usually there should be only one body element in the document, but IE doesn't have any, so
|
||||
// we need to create one.
|
||||
const inertHtml = this.inertDocument.createElement('html');
|
||||
this.inertDocument.appendChild(inertHtml);
|
||||
inertBodyElement = this.inertDocument.createElement('body');
|
||||
inertHtml.appendChild(inertBodyElement);
|
||||
}
|
||||
|
||||
inertBodyElement.innerHTML = '<svg><g onload="this.parentNode.remove()"></g></svg>';
|
||||
if (inertBodyElement.querySelector && !inertBodyElement.querySelector('svg')) {
|
||||
// We just hit the Safari 10.1 bug - which allows JS to run inside the SVG G element
|
||||
// so use the XHR strategy.
|
||||
this.getInertBodyElement = this.getInertBodyElement_XHR;
|
||||
return;
|
||||
}
|
||||
|
||||
inertBodyElement.innerHTML = '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">';
|
||||
if (inertBodyElement.querySelector && inertBodyElement.querySelector('svg img')) {
|
||||
// We just hit the Firefox bug - which prevents the inner img JS from being sanitized
|
||||
// so use the DOMParser strategy, if it is available.
|
||||
// If the DOMParser is not available then we are not in Firefox (Server/WebWorker?) so we
|
||||
// fall through to the default strategy below.
|
||||
if (isDOMParserAvailable()) {
|
||||
this.getInertBodyElement = this.getInertBodyElement_DOMParser;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// None of the bugs were hit so it is safe for us to use the default InertDocument strategy
|
||||
this.getInertBodyElement = this.getInertBodyElement_InertDocument;
|
||||
}
|
||||
export function getInertBodyHelper(defaultDoc: Document): InertBodyHelper {
|
||||
return isDOMParserAvailable() ? new DOMParserHelper() : new InertDocumentHelper(defaultDoc);
|
||||
}
|
||||
|
||||
export interface InertBodyHelper {
|
||||
/**
|
||||
* Get an inert DOM element containing DOM created from the dirty HTML string provided.
|
||||
* The implementation of this is determined in the constructor, when the class is instantiated.
|
||||
*/
|
||||
getInertBodyElement: (html: string) => HTMLElement | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use XHR to create and fill an inert body element (on Safari 10.1)
|
||||
* See
|
||||
* https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449
|
||||
*/
|
||||
private getInertBodyElement_XHR(html: string) {
|
||||
// We add these extra elements to ensure that the rest of the content is parsed as expected
|
||||
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
|
||||
// `<head>` tag.
|
||||
html = '<body><remove></remove>' + html + '</body>';
|
||||
try {
|
||||
html = encodeURI(html);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'document';
|
||||
xhr.open('GET', 'data:text/html;charset=utf-8,' + html, false);
|
||||
xhr.send(undefined);
|
||||
const body: HTMLBodyElement = xhr.response.body;
|
||||
body.removeChild(body.firstChild!);
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use DOMParser to create and fill an inert body element (on Firefox)
|
||||
* See https://github.com/cure53/DOMPurify/releases/tag/0.6.7
|
||||
*
|
||||
*/
|
||||
private getInertBodyElement_DOMParser(html: string) {
|
||||
/**
|
||||
* Uses DOMParser to create and fill an inert body element.
|
||||
* This is the default strategy used in browsers that support it.
|
||||
*/
|
||||
class DOMParserHelper implements InertBodyHelper {
|
||||
getInertBodyElement(html: string): HTMLElement|null {
|
||||
// We add these extra elements to ensure that the rest of the content is parsed as expected
|
||||
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
|
||||
// `<head>` tag.
|
||||
|
@ -103,14 +43,30 @@ export class InertBodyHelper {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use an HTML5 `template` element, if supported, or an inert body element created via
|
||||
* `createHtmlDocument` to create and fill an inert DOM element.
|
||||
* This is the default sane strategy to use if the browser does not require one of the specialised
|
||||
* strategies above.
|
||||
*/
|
||||
private getInertBodyElement_InertDocument(html: string) {
|
||||
/**
|
||||
* Use an HTML5 `template` element, if supported, or an inert body element created via
|
||||
* `createHtmlDocument` to create and fill an inert DOM element.
|
||||
* This is the fallback strategy if the browser does not support DOMParser.
|
||||
*/
|
||||
class InertDocumentHelper implements InertBodyHelper {
|
||||
private inertDocument: Document;
|
||||
|
||||
constructor(private defaultDoc: Document) {
|
||||
this.inertDocument = this.defaultDoc.implementation.createHTMLDocument('sanitization-inert');
|
||||
|
||||
if (this.inertDocument.body == null) {
|
||||
// usually there should be only one body element in the document, but IE doesn't have any, so
|
||||
// we need to create one.
|
||||
const inertHtml = this.inertDocument.createElement('html');
|
||||
this.inertDocument.appendChild(inertHtml);
|
||||
const inertBodyElement = this.inertDocument.createElement('body');
|
||||
inertHtml.appendChild(inertBodyElement);
|
||||
}
|
||||
}
|
||||
|
||||
getInertBodyElement(html: string): HTMLElement|null {
|
||||
// Prefer using <template> element if supported.
|
||||
const templateEl = this.inertDocument.createElement('template');
|
||||
if ('content' in templateEl) {
|
||||
|
@ -164,15 +120,15 @@ export class InertBodyHelper {
|
|||
}
|
||||
|
||||
/**
|
||||
* We need to determine whether the DOMParser exists in the global context.
|
||||
* The try-catch is because, on some browsers, trying to access this property
|
||||
* on window can actually throw an error.
|
||||
* We need to determine whether the DOMParser exists in the global context and
|
||||
* supports parsing HTML; HTML parsing support is not as wide as other formats, see
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/DOMParser#Browser_compatibility.
|
||||
*
|
||||
* @suppress {uselessCode}
|
||||
*/
|
||||
function isDOMParserAvailable() {
|
||||
export function isDOMParserAvailable() {
|
||||
try {
|
||||
return !!(window as any).DOMParser;
|
||||
return !!new (window as any).DOMParser().parseFromString('', 'text/html');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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/);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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`, () => {
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue