diff --git a/.ng-dev/config.ts b/.ng-dev/config.ts index 6350332d5e..342b3f64f7 100644 --- a/.ng-dev/config.ts +++ b/.ng-dev/config.ts @@ -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/**', diff --git a/BUILD.bazel b/BUILD.bazel index 48e84cc328..73e67bea63 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -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", diff --git a/CHANGELOG.md b/CHANGELOG.md index f7f4739d4d..531ca66972 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ + +## [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)) + + + # [10.0.0](https://github.com/angular/angular/compare/10.0.0-rc.6...10.0.0) (2020-06-24) diff --git a/aio/content/examples/pipes/src/app/app.component.ts b/aio/content/examples/pipes/src/app/app.component.ts index 2b739ed2a0..5b71cc53c7 100644 --- a/aio/content/examples/pipes/src/app/app.component.ts +++ b/aio/content/examples/pipes/src/app/app.component.ts @@ -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 } diff --git a/aio/content/examples/pipes/src/app/hero-birthday1.component.ts b/aio/content/examples/pipes/src/app/hero-birthday1.component.ts index d51914550f..4b475e80d6 100644 --- a/aio/content/examples/pipes/src/app/hero-birthday1.component.ts +++ b/aio/content/examples/pipes/src/app/hero-birthday1.component.ts @@ -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 } diff --git a/aio/content/examples/pipes/src/app/hero-birthday2.component.ts b/aio/content/examples/pipes/src/app/hero-birthday2.component.ts index ce71c2ab1e..f8ee3ca911 100644 --- a/aio/content/examples/pipes/src/app/hero-birthday2.component.ts +++ b/aio/content/examples/pipes/src/app/hero-birthday2.component.ts @@ -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'; } diff --git a/dev-infra/commit-message/config.ts b/dev-infra/commit-message/config.ts index 7a84edc488..2d9739cecf 100644 --- a/dev-infra/commit-message/config.ts +++ b/dev-infra/commit-message/config.ts @@ -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> = getConfig(); if (config.commitMessage === undefined) { diff --git a/dev-infra/commit-message/validate.spec.ts b/dev-infra/commit-message/validate.spec.ts index e745fa7158..ad1bf5b1e9 100644 --- a/dev-infra/commit-message/validate.spec.ts +++ b/dev-infra/commit-message/validate.spec.ts @@ -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); + }); + }); }); }); diff --git a/dev-infra/commit-message/validate.ts b/dev-infra/commit-message/validate.ts index 6148f6dfe2..a9b01f1c4d 100644 --- a/dev-infra/commit-message/validate.ts +++ b/dev-infra/commit-message/validate.ts @@ -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; } diff --git a/dev-infra/pr/discover-new-conflicts/index.ts b/dev-infra/pr/discover-new-conflicts/index.ts index daa1873092..790d809438 100644 --- a/dev-infra/pr/discover-new-conflicts/index.ts +++ b/dev-infra/pr/discover-new-conflicts/index.ts @@ -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}`); } diff --git a/dev-infra/pr/merge/strategies/autosquash-merge.ts b/dev-infra/pr/merge/strategies/autosquash-merge.ts index 198eda3daf..e304ed98bd 100644 --- a/dev-infra/pr/merge/strategies/autosquash-merge.ts +++ b/dev-infra/pr/merge/strategies/autosquash-merge.ts @@ -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]); diff --git a/dev-infra/pr/merge/task.ts b/dev-infra/pr/merge/task.ts index e02815cbc0..7b9a76ead2 100644 --- a/dev-infra/pr/merge/task.ts +++ b/dev-infra/pr/merge/task.ts @@ -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 { - // 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]); } } } diff --git a/dev-infra/pr/rebase/index.ts b/dev-infra/pr/rebase/index.ts index 0b9fa9fe77..8c0af93324 100644 --- a/dev-infra/pr/rebase/index.ts +++ b/dev-infra/pr/rebase/index.ts @@ -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'}); } } diff --git a/dev-infra/utils/config.ts b/dev-infra/utils/config.ts index bcf6a82d60..bd08ee68e4 100644 --- a/dev-infra/utils/config.ts +++ b/dev-infra/utils/config.ts @@ -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; } /** diff --git a/dev-infra/utils/git/index.ts b/dev-infra/utils/git/index.ts index dbfb50c180..88c626dc20 100644 --- a/dev-infra/utils/git/index.ts +++ b/dev-infra/utils/git/index.ts @@ -21,6 +21,9 @@ type RateLimitResponseWithOAuthScopeHeader = Octokit.Response 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 { - const missingScopes: string[] = []; + async hasOauthScopes(testFn: OAuthScopeTestFunction): Promise { 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; diff --git a/docs/PUBLIC_API.md b/docs/PUBLIC_API.md index be65edd1df..dc592c3788 100644 --- a/docs/PUBLIC_API.md +++ b/docs/PUBLIC_API.md @@ -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:_api.accept +yarn bazel run //packages/:_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; + providers?: Provider[]; + } + +-export declare type NgIterable = Array | Iterable; ++export declare type NgIterable = Iterable; + + export declare interface NgModule { + bootstrap?: Array | any[]>; + declarations?: Array | 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 + ``` diff --git a/goldens/size-tracking/aio-payloads.json b/goldens/size-tracking/aio-payloads.json index 3189fe1905..b2733035ab 100755 --- a/goldens/size-tracking/aio-payloads.json +++ b/goldens/size-tracking/aio-payloads.json @@ -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 } } diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index bc40e78b62..cf8bdc7286 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -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 } } } diff --git a/karma-js.conf.js b/karma-js.conf.js index ce31b84a83..bdab6ea463 100644 --- a/karma-js.conf.js +++ b/karma-js.conf.js @@ -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. diff --git a/packages/compiler-cli/src/ngtsc/util/src/typescript.ts b/packages/compiler-cli/src/ngtsc/util/src/typescript.ts index ee64a577ec..13a90d3a3e 100644 --- a/packages/compiler-cli/src/ngtsc/util/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/util/src/typescript.ts @@ -125,10 +125,11 @@ export function resolveModuleName( compilerHost: ts.ModuleResolutionHost&Pick, 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( diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index 58a22cd490..e12a2d854c 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -229,7 +229,7 @@ export class ComponentFactory extends viewEngine_ComponentFactory { 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; diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 0ce15a35ad..5aee59cdd6 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -99,8 +99,12 @@ let nextNgElementId = 0; export function bloomAdd( injectorIndex: number, tView: TView, type: Type|InjectionToken|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|InjectionToken|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; } diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts index 6c0c5ec7b1..1377482c80 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -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'); diff --git a/packages/core/src/render3/instructions/di.ts b/packages/core/src/render3/instructions/di.ts index ce4110fe21..075ed38037 100644 --- a/packages/core/src/render3/instructions/di.ts +++ b/packages/core/src/render3/instructions/di.ts @@ -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'; /** diff --git a/packages/core/src/render3/instructions/listener.ts b/packages/core/src/render3/instructions/listener.ts index 0ed0540288..3a8daffc70 100644 --- a/packages/core/src/render3/instructions/listener.ts +++ b/packages/core/src/render3/instructions/listener.ts @@ -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; diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 1ca8a714d7..0afae0d5e6 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -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); } @@ -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. diff --git a/packages/core/src/render3/node_assert.ts b/packages/core/src/render3/node_assert.ts index 895c52397d..59714e88df 100644 --- a/packages/core/src/render3/node_assert.ts +++ b/packages/core/src/render3/node_assert.ts @@ -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 { diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 4717bb70fd..c7ce0d2542 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -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); } } diff --git a/packages/core/src/render3/query.ts b/packages/core/src/render3/query.ts index 0e73f530db..63eaf2bd7d 100644 --- a/packages/core/src/render3/query.ts +++ b/packages/core/src/render3/query.ts @@ -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); diff --git a/packages/core/src/render3/view_engine_compatibility.ts b/packages/core/src/render3/view_engine_compatibility.ts index f39721f7e4..42e69cea5a 100644 --- a/packages/core/src/render3/view_engine_compatibility.ts +++ b/packages/core/src/render3/view_engine_compatibility.ts @@ -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]; diff --git a/packages/core/src/render3/view_ref.ts b/packages/core/src/render3/view_ref.ts index 4da248498a..f4df71d91d 100644 --- a/packages/core/src/render3/view_ref.ts +++ b/packages/core/src/render3/view_ref.ts @@ -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 implements viewEngine_EmbeddedViewRef, 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) { diff --git a/packages/core/src/sanitization/html_sanitizer.ts b/packages/core/src/sanitization/html_sanitizer.ts index 4f79d98927..ab7bbbe836 100644 --- a/packages/core/src/sanitization/html_sanitizer.ts +++ b/packages/core/src/sanitization/html_sanitizer.ts @@ -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); diff --git a/packages/core/src/sanitization/inert_body.ts b/packages/core/src/sanitization/inert_body.ts index 46f76eb8f4..62c6598df8 100644 --- a/packages/core/src/sanitization/inert_body.ts +++ b/packages/core/src/sanitization/inert_body.ts @@ -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 = ''; - 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 = '

'; - 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 `` do not get hoisted to the - // `` tag. - html = '' + html + ''; - 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 `` do not get hoisted to the // `` 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