Compare commits
47 Commits
Author | SHA1 | Date | |
---|---|---|---|
1f0c1f3ff2 | |||
a86a41210f | |||
20c44a6240 | |||
0feb560136 | |||
f4d06445a3 | |||
6c1ab479ee | |||
f7997256fc | |||
dc9da17f37 | |||
99c41d6db0 | |||
1aae94a421 | |||
f0f319b1d6 | |||
454e073918 | |||
76d64331bd | |||
6855396449 | |||
0b96908225 | |||
65f52f3761 | |||
9ce55d08f8 | |||
765c1c5b17 | |||
ccfa3426f7 | |||
83379fa0bb | |||
2ffd44ad96 | |||
4a84842bbb | |||
daa715a1ca | |||
31bce80771 | |||
711e4d4cc2 | |||
fdc5941fe7 | |||
ae10a541bf | |||
b797913d10 | |||
1465372a0e | |||
a33cb2d39b | |||
1202d17b4c | |||
d837828317 | |||
dc210bc75b | |||
75ead94ed5 | |||
6ac0042aef | |||
3eee53603c | |||
5c94fa97c1 | |||
468f4a3f9e | |||
a2393bef02 | |||
5bd1c7bbf7 | |||
19313f7dad | |||
4e2500278c | |||
2c4cfa6f2c | |||
c8f0c3b637 | |||
cf4883240b | |||
6f3157fe6d | |||
12d8af50dd |
@ -1,5 +1,3 @@
|
||||
import {exec} from 'shelljs';
|
||||
|
||||
import {MergeConfig} from './dev-infra/pr/merge/config';
|
||||
|
||||
// The configuration for `ng-dev commit-message` commands.
|
||||
@ -82,33 +80,11 @@ const github = {
|
||||
name: 'angular',
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the name of the current patch branch. The patch branch is determined by
|
||||
* looking for upstream branches that follow the format of `{major}.{minor}.x`.
|
||||
*/
|
||||
const getPatchBranchName = (): string => {
|
||||
const branches =
|
||||
exec(
|
||||
`git ls-remote --heads https://github.com/${github.owner}/${github.name}.git`,
|
||||
{silent: true})
|
||||
.trim()
|
||||
.split('\n');
|
||||
|
||||
for (let i = branches.length - 1; i >= 0; i--) {
|
||||
const branchName = branches[i];
|
||||
const matches = branchName.match(/refs\/heads\/([0-9]+\.[0-9]+\.x)/);
|
||||
if (matches !== null) {
|
||||
return matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
throw Error('Could not determine patch branch name.');
|
||||
};
|
||||
|
||||
// Configuration for the `ng-dev pr merge` command. The command can be used
|
||||
// for merging upstream pull requests into branches based on a PR target label.
|
||||
const merge = () => {
|
||||
const patchBranch = getPatchBranchName();
|
||||
// TODO: resume dynamically determining patch branch
|
||||
const patch = '10.0.x';
|
||||
const config: MergeConfig = {
|
||||
githubApiMerge: false,
|
||||
claSignedLabel: 'cla: yes',
|
||||
@ -121,18 +97,18 @@ const merge = () => {
|
||||
},
|
||||
{
|
||||
pattern: 'PR target: patch-only',
|
||||
branches: [patchBranch],
|
||||
branches: [patch],
|
||||
},
|
||||
{
|
||||
pattern: 'PR target: master & patch',
|
||||
branches: ['master', patchBranch],
|
||||
branches: ['master', patch],
|
||||
},
|
||||
],
|
||||
requiredBaseCommits: {
|
||||
// PRs that target either `master` or the patch branch, need to be rebased
|
||||
// on top of the latest commit message validation fix.
|
||||
'master': '4341743b4a6d7e23c6f944aa9e34166b701369a1',
|
||||
[patchBranch]: '2a53f471592f424538802907aca1f60f1177a86d'
|
||||
[patch]: '2a53f471592f424538802907aca1f60f1177a86d'
|
||||
},
|
||||
};
|
||||
return config;
|
||||
|
@ -568,7 +568,7 @@ groups:
|
||||
- *can-be-global-approved
|
||||
- *can-be-global-docs-approved
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
contains_any_globs(files.exclude('packages/compiler-cli/**'), [
|
||||
'**/testing/**',
|
||||
'aio/content/guide/testing.md',
|
||||
'aio/content/examples/testing/**',
|
||||
|
46
CHANGELOG.md
46
CHANGELOG.md
@ -1,3 +1,49 @@
|
||||
<a name="9.1.12"></a>
|
||||
## [9.1.12](https://github.com/angular/angular/compare/9.1.11...9.1.12) (2020-07-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** infinite loop if injectable using inheritance has a custom decorator ([6c1ab47](https://github.com/angular/angular/commit/6c1ab47)), closes [#35733](https://github.com/angular/angular/issues/35733)
|
||||
|
||||
|
||||
|
||||
<a name="9.1.11"></a>
|
||||
## [9.1.11](https://github.com/angular/angular/compare/9.1.10...9.1.11) (2020-06-10)
|
||||
|
||||
### Reverts
|
||||
|
||||
* **elements:** fire custom element output events during component initialization ([dc9da17](https://github.com/angular/angular/commit/dc9da17))
|
||||
|
||||
|
||||
<a name="9.1.10"></a>
|
||||
## [9.1.10](https://github.com/angular/angular/compare/9.1.9...9.1.10) (2020-06-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **elements:** fire custom element output events during component initialization ([454e073](https://github.com/angular/angular/commit/454e073)), closes [/github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/create-custom-element.ts#L167-L170](https://github.com//github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/create-custom-element.ts/issues/L167-L170) [/github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/create-custom-element.ts#L164](https://github.com//github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/create-custom-element.ts/issues/L164) [/github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/component-factory-strategy.ts#L158](https://github.com//github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/component-factory-strategy.ts/issues/L158) [#36141](https://github.com/angular/angular/issues/36141)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **ngcc:** cache parsed tsconfig between runs ([1aae94a](https://github.com/angular/angular/commit/1aae94a)), closes [#37417](https://github.com/angular/angular/issues/37417) [#36882](https://github.com/angular/angular/issues/36882)
|
||||
|
||||
|
||||
|
||||
<a name="9.1.9"></a>
|
||||
## [9.1.9](https://github.com/angular/angular/compare/9.1.8...9.1.9) (2020-05-20)
|
||||
|
||||
This release contains a re-submit of the following 3 commits with fixes for TS 3.8.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **elements:** capture input properties set before upgrading the element ([#36114](https://github.com/angular/angular/issues/36114)) ([#37226](https://github.com/angular/angular/issues/37226)) ([a33cb2d](https://github.com/angular/angular/commit/a33cb2d)), closes [#30848](https://github.com/angular/angular/issues/30848) [#31416](https://github.com/angular/angular/issues/31416)
|
||||
* **elements:** correctly handle getting/setting properties before connecting the element ([#36114](https://github.com/angular/angular/issues/36114)) ([#37226](https://github.com/angular/angular/issues/37226)) ([6ac0042](https://github.com/angular/angular/commit/6ac0042)), closes [/github.com/angular/angular/pull/31416/files#r300326698](https://github.com//github.com/angular/angular/pull/31416/files/issues/r300326698)
|
||||
* **elements:** do not break when the constructor of an Angular Element is not called ([#36114](https://github.com/angular/angular/issues/36114)) ([#37226](https://github.com/angular/angular/issues/37226)) ([1465372](https://github.com/angular/angular/commit/1465372))
|
||||
|
||||
|
||||
|
||||
<a name="9.1.8"></a>
|
||||
## [9.1.8](https://github.com/angular/angular/compare/9.1.6...9.1.8) (2020-05-20)
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"hosting": {
|
||||
"target": "aio",
|
||||
"public": "dist",
|
||||
"cleanUrls": true,
|
||||
"redirects": [
|
||||
|
@ -33,7 +33,7 @@ else
|
||||
readonly majorVersionStable=${CI_STABLE_BRANCH%%.*}
|
||||
|
||||
# Do not deploy if the major version is not less than the stable branch major version
|
||||
if [[ !( "$majorVersion" < "$majorVersionStable" ) ]]; then
|
||||
if (( $majorVersion >= $majorVersionStable )); then
|
||||
echo "Skipping deploy of branch \"$CI_BRANCH\" to firebase."
|
||||
echo "We only deploy archive branches with the major version less than the stable branch: \"$CI_STABLE_BRANCH\""
|
||||
exit 0
|
||||
@ -64,16 +64,27 @@ fi
|
||||
case $deployEnv in
|
||||
next)
|
||||
readonly projectId=aio-staging
|
||||
readonly siteId=$projectId
|
||||
readonly deployedUrl=https://next.angular.io/
|
||||
readonly firebaseToken=$CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN
|
||||
;;
|
||||
stable)
|
||||
readonly projectId=angular-io
|
||||
readonly siteId=$projectId
|
||||
readonly deployedUrl=https://angular.io/
|
||||
readonly firebaseToken=$CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN
|
||||
;;
|
||||
archive)
|
||||
readonly projectId=v${majorVersion}-angular-io
|
||||
# Special case v9-angular-io because its piloting the firebase hosting "multisites" setup
|
||||
# See https://angular-team.atlassian.net/browse/DEV-125 for more info.
|
||||
if [[ "$majorVersion" == "9" ]]; then
|
||||
readonly projectId=aio-staging
|
||||
readonly siteId=v9-angular-io
|
||||
else
|
||||
readonly projectId=v${majorVersion}-angular-io
|
||||
readonly siteId=$projectId
|
||||
fi
|
||||
|
||||
readonly deployedUrl=https://v${majorVersion}.angular.io/
|
||||
readonly firebaseToken=$CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN
|
||||
;;
|
||||
@ -82,6 +93,7 @@ esac
|
||||
echo "Git branch : $CI_BRANCH"
|
||||
echo "Build/deploy mode : $deployEnv"
|
||||
echo "Firebase project : $projectId"
|
||||
echo "Firebase site : $siteId"
|
||||
echo "Deployment URL : $deployedUrl"
|
||||
|
||||
if [[ ${1:-} == "--dry-run" ]]; then
|
||||
@ -92,23 +104,29 @@ fi
|
||||
(
|
||||
cd "`dirname $0`/.."
|
||||
|
||||
# Build the app
|
||||
echo "\n\n\n==== Build the aio app ====\n"
|
||||
yarn build --configuration=$deployEnv --progress=false
|
||||
|
||||
# Include any mode-specific files
|
||||
|
||||
echo "\n\n\n==== Add any mode-specific files into the aio distribution ====\n"
|
||||
cp -rf src/extra-files/$deployEnv/. dist/
|
||||
|
||||
# Set deployedUrl as parameter in the opensearch description
|
||||
|
||||
echo "\n\n\n==== Update opensearch descriptor for aio with the deployedUrl ====\n"
|
||||
# deployedUrl must end with /
|
||||
yarn set-opensearch-url $deployedUrl
|
||||
|
||||
# Check payload size
|
||||
echo "\n\n\n==== Check payload size and upload the numbers to firebase db ====\n"
|
||||
yarn payload-size
|
||||
|
||||
# Deploy to Firebase
|
||||
yarn firebase use "$projectId" --token "$firebaseToken"
|
||||
yarn firebase deploy --message "Commit: $CI_COMMIT" --non-interactive --token "$firebaseToken"
|
||||
|
||||
# Run PWA-score tests
|
||||
echo "\n\n\n==== Deploy aio to firebase hosting ====\n"
|
||||
|
||||
yarn firebase use "${projectId}" --token "$firebaseToken"
|
||||
yarn firebase target:apply hosting aio $siteId --token "$firebaseToken"
|
||||
yarn firebase deploy --only hosting:aio --message "Commit: $CI_COMMIT" --non-interactive --token "$firebaseToken"
|
||||
|
||||
|
||||
echo "\n\n\n==== Run PWA-score tests ====\n"
|
||||
yarn test-pwa-score "$deployedUrl" "$CI_AIO_MIN_PWA_SCORE"
|
||||
)
|
||||
|
@ -68,6 +68,7 @@ function check {
|
||||
expected="Git branch : master
|
||||
Build/deploy mode : next
|
||||
Firebase project : aio-staging
|
||||
Firebase site : aio-staging
|
||||
Deployment URL : https://next.angular.io/"
|
||||
check "$actual" "$expected"
|
||||
)
|
||||
@ -103,6 +104,7 @@ Deployment URL : https://next.angular.io/"
|
||||
expected="Git branch : 4.3.x
|
||||
Build/deploy mode : stable
|
||||
Firebase project : angular-io
|
||||
Firebase site : angular-io
|
||||
Deployment URL : https://angular.io/"
|
||||
check "$actual" "$expected"
|
||||
)
|
||||
@ -139,10 +141,37 @@ Deployment URL : https://angular.io/"
|
||||
expected="Git branch : 2.4.x
|
||||
Build/deploy mode : archive
|
||||
Firebase project : v2-angular-io
|
||||
Firebase site : v2-angular-io
|
||||
Deployment URL : https://v2.angular.io/"
|
||||
check "$actual" "$expected"
|
||||
)
|
||||
|
||||
(
|
||||
echo ===== archive - v9-angular-io multisite special case - deploy success
|
||||
actual=$(
|
||||
export BASH_ENV=/dev/null
|
||||
export CI_REPO_OWNER=angular
|
||||
export CI_REPO_NAME=angular
|
||||
export CI_PULL_REQUEST=false
|
||||
export CI_BRANCH=9.1.x
|
||||
export CI_STABLE_BRANCH=10.0.x
|
||||
export CI_COMMIT=$(git ls-remote origin 9.1.x | cut -c1-40)
|
||||
export CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN=XXXXX
|
||||
$deployToFirebaseDryRun
|
||||
)
|
||||
expected="Git branch : 9.1.x
|
||||
Build/deploy mode : archive
|
||||
Firebase project : aio-staging
|
||||
Firebase site : v9-angular-io
|
||||
Deployment URL : https://v9.angular.io/"
|
||||
# TODO: This test incorrectly expects the Firebase project to be v9-angular-io.
|
||||
# v9-angular-io is a "multisites" project currently within the aio-staging project
|
||||
# This setup is temporary and was created in order to deploy v9.angular.io without
|
||||
# disruptions.
|
||||
# See https://angular-team.atlassian.net/browse/DEV-125 for more info.
|
||||
check "$actual" "$expected"
|
||||
)
|
||||
|
||||
(
|
||||
echo ===== archive - skip deploy - commit not HEAD
|
||||
actual=$(
|
||||
|
@ -5,13 +5,9 @@
|
||||
</div>
|
||||
|
||||
<mat-toolbar color="primary" class="app-toolbar no-print" [class.transitioning]="isTransitioning">
|
||||
<mat-toolbar-row class="notification-container">
|
||||
<aio-notification notificationId="survey-march-2020" expirationDate="2020-04-15" [dismissOnContentClick]="true" (dismissed)="notificationDismissed()">
|
||||
<a href="https://goo.gle/angular-survey-2020">
|
||||
<mat-icon class="icon" svgIcon="insert_comment" aria-label="Announcement"></mat-icon>
|
||||
<span class="message">Help Angular by taking a <b>1 minute survey</b>!</span>
|
||||
<span class="action-button">Go to survey</span>
|
||||
</a>
|
||||
<mat-toolbar-row class="notification-container blm-message">
|
||||
<aio-notification notificationId="blm-2020" expirationDate="2022-04-15" [dismissOnContentClick]="true" (dismissed)="notificationDismissed()">
|
||||
#BlackLivesMatter
|
||||
</aio-notification>
|
||||
</mat-toolbar-row>
|
||||
<mat-toolbar-row>
|
||||
|
@ -183,8 +183,8 @@ section#intro {
|
||||
|
||||
// ANGULAR LINE
|
||||
.background-sky {
|
||||
background-color: $blue;
|
||||
background: $bluegradient;
|
||||
background-color: $black;
|
||||
background: $black;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
|
@ -20,9 +20,16 @@ mat-toolbar.mat-toolbar {
|
||||
}
|
||||
}
|
||||
|
||||
.blm-message {
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
background: #2d2d2d;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
// HOME PAGE OVERRIDE: TOPNAV TOOLBAR
|
||||
aio-shell.page-home mat-toolbar.mat-toolbar {
|
||||
background-color: $blue;
|
||||
background-color: $black;
|
||||
|
||||
@media (min-width: 481px) {
|
||||
&:not(.transitioning) {
|
||||
|
@ -30,7 +30,16 @@ var CIconfiguration = {
|
||||
'Android7': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
'Android8': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
'Android9': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
'Android10': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
// Disable Android 10 tests due to infrastructure failure.
|
||||
// ex:
|
||||
// Chrome Mobile 74.0.3729 (Android 0.0.0) ERROR:
|
||||
// Error: XHR error loading
|
||||
// http://angular-ci.local:9876/base/node_modules/rxjs/internal/operators/zip.js
|
||||
//
|
||||
// Error loading http://angular-ci.local:9876/base/node_modules/rxjs/internal/operators/zip.js as
|
||||
// "../internal/operators/zip" from
|
||||
// http://angular-ci.local:9876/base/node_modules/rxjs/operators/index.js
|
||||
'Android10': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
|
||||
'Safari12': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
|
||||
'Safari13': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
|
||||
'iOS10': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
|
||||
|
@ -32,6 +32,7 @@ ts_library(
|
||||
"@npm//@types/events",
|
||||
"@npm//@types/jasmine",
|
||||
"@npm//@types/node",
|
||||
"@npm//inquirer",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -6,6 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as yargs from 'yargs';
|
||||
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {validateFile} from './validate-file';
|
||||
import {validateCommitRange} from './validate-range';
|
||||
|
||||
@ -51,10 +54,10 @@ export function buildCommitMessageParser(localYargs: yargs.Argv) {
|
||||
// If on CI, and not pull request number is provided, assume the branch
|
||||
// being run on is an upstream branch.
|
||||
if (process.env['CI'] && process.env['CI_PULL_REQUEST'] === 'false') {
|
||||
console.info(
|
||||
`Since valid commit messages are enforced by PR linting on CI, we do not\n` +
|
||||
`need to validate commit messages on CI runs on upstream branches.\n\n` +
|
||||
`Skipping check of provided commit range`);
|
||||
info(`Since valid commit messages are enforced by PR linting on CI, we do not`);
|
||||
info(`need to validate commit messages on CI runs on upstream branches.`);
|
||||
info();
|
||||
info(`Skipping check of provided commit range`);
|
||||
return;
|
||||
}
|
||||
validateCommitRange(argv.range);
|
||||
|
@ -9,6 +9,7 @@ import {readFileSync} from 'fs';
|
||||
import {resolve} from 'path';
|
||||
|
||||
import {getRepoBaseDir} from '../utils/config';
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {validateCommitMessage} from './validate';
|
||||
|
||||
@ -16,7 +17,7 @@ import {validateCommitMessage} from './validate';
|
||||
export function validateFile(filePath: string) {
|
||||
const commitMessage = readFileSync(resolve(getRepoBaseDir(), filePath), 'utf8');
|
||||
if (validateCommitMessage(commitMessage)) {
|
||||
console.info('√ Valid commit message');
|
||||
info('√ Valid commit message');
|
||||
return;
|
||||
}
|
||||
// If the validation did not return true, exit as a failure.
|
||||
|
@ -6,6 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {exec} from 'shelljs';
|
||||
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {parseCommitMessage, validateCommitMessage, ValidateCommitMessageOptions} from './validate';
|
||||
|
||||
// Whether the provided commit is a fixup commit.
|
||||
@ -31,7 +34,7 @@ export function validateCommitRange(range: string) {
|
||||
// Separate the commits from a single string into individual commits
|
||||
const commits = result.split(randomValueSeparator).map(l => l.trim()).filter(line => !!line);
|
||||
|
||||
console.info(`Examining ${commits.length} commit(s) in the provided range: ${range}`);
|
||||
info(`Examining ${commits.length} commit(s) in the provided range: ${range}`);
|
||||
|
||||
// Check each commit in the commit range. Commits are allowed to be fixup commits for other
|
||||
// commits in the provided commit range.
|
||||
@ -46,7 +49,7 @@ export function validateCommitRange(range: string) {
|
||||
});
|
||||
|
||||
if (allCommitsInRangeValid) {
|
||||
console.info('√ All commit messages in range valid.');
|
||||
info('√ All commit messages in range valid.');
|
||||
} else {
|
||||
// Exit with a non-zero exit code if invalid commit messages have
|
||||
// been discovered.
|
||||
|
@ -5,6 +5,8 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {error} from '../utils/console';
|
||||
|
||||
import {getCommitMessageConfig} from './config';
|
||||
|
||||
/** Options for commit message validation. */
|
||||
@ -62,8 +64,8 @@ export function parseCommitMessage(commitMsg: string) {
|
||||
/** Validate a commit message against using the local repo's config. */
|
||||
export function validateCommitMessage(
|
||||
commitMsg: string, options: ValidateCommitMessageOptions = {}) {
|
||||
function error(errorMessage: string) {
|
||||
console.error(
|
||||
function printError(errorMessage: string) {
|
||||
error(
|
||||
`INVALID COMMIT MSG: \n` +
|
||||
`${'─'.repeat(40)}\n` +
|
||||
`${commitMsg}\n` +
|
||||
@ -91,7 +93,7 @@ export function validateCommitMessage(
|
||||
// the git history anyway, unless the options provided to not allow squash commits.
|
||||
if (commit.isSquash) {
|
||||
if (options.disallowSquash) {
|
||||
error('The commit must be manually squashed into the target commit');
|
||||
printError('The commit must be manually squashed into the target commit');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -104,7 +106,7 @@ export function validateCommitMessage(
|
||||
// check.
|
||||
if (commit.isFixup) {
|
||||
if (options.nonFixupCommitHeaders && !options.nonFixupCommitHeaders.includes(commit.header)) {
|
||||
error(
|
||||
printError(
|
||||
'Unable to find match for fixup commit among prior commits: ' +
|
||||
(options.nonFixupCommitHeaders.map(x => `\n ${x}`).join('') || '-'));
|
||||
return false;
|
||||
@ -117,22 +119,23 @@ export function validateCommitMessage(
|
||||
// Checking commit header //
|
||||
////////////////////////////
|
||||
if (commit.header.length > config.maxLineLength) {
|
||||
error(`The commit message header is longer than ${config.maxLineLength} characters`);
|
||||
printError(`The commit message header is longer than ${config.maxLineLength} characters`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!commit.type) {
|
||||
error(`The commit message header does not match the expected format.`);
|
||||
printError(`The commit message header does not match the expected format.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.types.includes(commit.type)) {
|
||||
error(`'${commit.type}' is not an allowed type.\n => TYPES: ${config.types.join(', ')}`);
|
||||
printError(`'${commit.type}' is not an allowed type.\n => TYPES: ${config.types.join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (commit.scope && !config.scopes.includes(commit.scope)) {
|
||||
error(`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
|
||||
printError(
|
||||
`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -146,14 +149,14 @@ export function validateCommitMessage(
|
||||
//////////////////////////
|
||||
|
||||
if (commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
|
||||
error(`The commit message body does not meet the minimum length of ${
|
||||
printError(`The commit message body does not meet the minimum length of ${
|
||||
config.minBodyLength} characters`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bodyByLine = commit.body.split('\n');
|
||||
if (bodyByLine.some(line => line.length > config.maxLineLength)) {
|
||||
error(
|
||||
printError(
|
||||
`The commit messsage body contains lines greater than ${config.maxLineLength} characters`);
|
||||
return false;
|
||||
}
|
||||
|
@ -34,35 +34,9 @@ export function buildFormatParser(localYargs: yargs.Argv) {
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(allChangedFilesSince(sha));
|
||||
})
|
||||
.command(
|
||||
'files <files..>', 'Run the formatter on provided files', {},
|
||||
({check, files}) => {
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(files);
|
||||
})
|
||||
// TODO(josephperrott): remove this hidden command after deprecation period.
|
||||
.command('deprecation-warning [originalCommand]', false, {}, ({originalCommand}) => {
|
||||
console.warn(`\`yarn ${
|
||||
originalCommand}\` is deprecated in favor of running the formatter via ng-dev`);
|
||||
console.warn();
|
||||
console.warn(`As a replacement of \`yarn ${originalCommand}\`, run:`);
|
||||
switch (originalCommand) {
|
||||
case 'bazel:format':
|
||||
case 'bazel:lint-fix':
|
||||
console.warn(` yarn ng-dev format all`);
|
||||
break;
|
||||
case 'bazel:lint':
|
||||
console.warn(` yarn ng-dev format all --check`);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Error: Unrecognized previous command.`);
|
||||
}
|
||||
console.warn();
|
||||
console.warn(`You can find more usage information by running:`);
|
||||
console.warn(` yarn ng-dev format --help`);
|
||||
console.warn();
|
||||
console.warn(`For more on the rationale and effects of this deprecation visit:`);
|
||||
console.warn(` https://github.com/angular/angular/pull/36842#issue-410321447`);
|
||||
.command('files <files..>', 'Run the formatter on provided files', {}, ({check, files}) => {
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(files);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,9 @@
|
||||
*/
|
||||
|
||||
import {prompt} from 'inquirer';
|
||||
|
||||
import {error, info} from '../utils/console';
|
||||
|
||||
import {runFormatterInParallel} from './run-commands-parallel';
|
||||
|
||||
/**
|
||||
@ -17,16 +20,16 @@ export async function formatFiles(files: string[]) {
|
||||
let failures = await runFormatterInParallel(files, 'format');
|
||||
|
||||
if (failures === false) {
|
||||
console.info('No files matched for formatting.');
|
||||
info('No files matched for formatting.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// The process should exit as a failure if any of the files failed to format.
|
||||
if (failures.length !== 0) {
|
||||
console.error(`Formatting failed, see errors above for more information.`);
|
||||
error(`Formatting failed, see errors above for more information.`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.info(`√ Formatting complete.`);
|
||||
info(`√ Formatting complete.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@ -38,18 +41,18 @@ export async function checkFiles(files: string[]) {
|
||||
const failures = await runFormatterInParallel(files, 'check');
|
||||
|
||||
if (failures === false) {
|
||||
console.info('No files matched for formatting check.');
|
||||
info('No files matched for formatting check.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
// Provide output expressing which files are failing formatting.
|
||||
console.group('\nThe following files are out of format:');
|
||||
info.group('\nThe following files are out of format:');
|
||||
for (const file of failures) {
|
||||
console.info(` - ${file}`);
|
||||
info(` - ${file}`);
|
||||
}
|
||||
console.groupEnd();
|
||||
console.info();
|
||||
info.groupEnd();
|
||||
info();
|
||||
|
||||
// If the command is run in a non-CI environment, prompt to format the files immediately.
|
||||
let runFormatter = false;
|
||||
@ -67,13 +70,13 @@ export async function checkFiles(files: string[]) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
// Inform user how to format files in the future.
|
||||
console.info();
|
||||
console.info(`To format the failing file run the following command:`);
|
||||
console.info(` yarn ng-dev format files ${failures.join(' ')}`);
|
||||
info();
|
||||
info(`To format the failing file run the following command:`);
|
||||
info(` yarn ng-dev format files ${failures.join(' ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.info('√ All files correctly formatted.');
|
||||
info('√ All files correctly formatted.');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
import {join} from 'path';
|
||||
|
||||
import {getRepoBaseDir} from '../../utils/config';
|
||||
import {error} from '../../utils/console';
|
||||
|
||||
import {Formatter} from './base-formatter';
|
||||
|
||||
@ -35,9 +36,9 @@ export class Buildifier extends Formatter {
|
||||
callback:
|
||||
(file: string, code: number, _: string, stderr: string) => {
|
||||
if (code !== 0) {
|
||||
console.error(`Error running buildifier on: ${file}`);
|
||||
console.error(stderr);
|
||||
console.error();
|
||||
error(`Error running buildifier on: ${file}`);
|
||||
error(stderr);
|
||||
error();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -9,6 +9,7 @@
|
||||
import {join} from 'path';
|
||||
|
||||
import {getRepoBaseDir} from '../../utils/config';
|
||||
import {error} from '../../utils/console';
|
||||
|
||||
import {Formatter} from './base-formatter';
|
||||
|
||||
@ -35,9 +36,9 @@ export class ClangFormat extends Formatter {
|
||||
callback:
|
||||
(file: string, code: number, _: string, stderr: string) => {
|
||||
if (code !== 0) {
|
||||
console.error(`Error running clang-format on: ${file}`);
|
||||
console.error(stderr);
|
||||
console.error();
|
||||
error(`Error running clang-format on: ${file}`);
|
||||
error(stderr);
|
||||
error();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -11,6 +11,8 @@ import * as multimatch from 'multimatch';
|
||||
import {cpus} from 'os';
|
||||
import {exec} from 'shelljs';
|
||||
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {Formatter, FormatterAction, getActiveFormatters} from './formatters';
|
||||
|
||||
const AVAILABLE_THREADS = Math.max(cpus().length - 1, 1);
|
||||
@ -47,10 +49,10 @@ export function runFormatterInParallel(allFiles: string[], action: FormatterActi
|
||||
|
||||
switch (action) {
|
||||
case 'format':
|
||||
console.info(`Formatting ${pendingCommands.length} file(s)`);
|
||||
info(`Formatting ${pendingCommands.length} file(s)`);
|
||||
break;
|
||||
case 'check':
|
||||
console.info(`Checking format of ${pendingCommands.length} file(s)`);
|
||||
info(`Checking format of ${pendingCommands.length} file(s)`);
|
||||
break;
|
||||
default:
|
||||
throw Error(`Invalid format action "${action}": allowed actions are "format" and "check"`);
|
||||
|
@ -1,5 +1,7 @@
|
||||
import {Arguments, Argv} from 'yargs';
|
||||
|
||||
import {error} from '../../utils/console';
|
||||
|
||||
import {discoverNewConflictsForPr} from './index';
|
||||
|
||||
/** Builds the discover-new-conflicts pull request command. */
|
||||
@ -16,7 +18,7 @@ export function buildDiscoverNewConflictsCommand(yargs: Argv) {
|
||||
export async function handleDiscoverNewConflictsCommand({prNumber, date}: Arguments) {
|
||||
// If a provided date is not able to be parsed, yargs provides it as NaN.
|
||||
if (isNaN(date)) {
|
||||
console.error('Unable to parse the value provided via --date flag');
|
||||
error('Unable to parse the value provided via --date flag');
|
||||
process.exit(1);
|
||||
}
|
||||
await discoverNewConflictsForPr(prNumber, date);
|
||||
|
@ -10,6 +10,7 @@ import {Bar} from 'cli-progress';
|
||||
import {types as graphQLTypes} from 'typed-graphqlify';
|
||||
|
||||
import {getConfig, NgDevConfig} from '../../utils/config';
|
||||
import {error, info} from '../../utils/console';
|
||||
import {getCurrentBranch, hasLocalChanges} from '../../utils/git';
|
||||
import {getPendingPrs} from '../../utils/github';
|
||||
import {exec} from '../../utils/shelljs';
|
||||
@ -57,7 +58,7 @@ export async function discoverNewConflictsForPr(
|
||||
// If there are any local changes in the current repository state, the
|
||||
// check cannot run as it needs to move between branches.
|
||||
if (hasLocalChanges()) {
|
||||
console.error('Cannot run with local changes. Please make sure there are no local changes.');
|
||||
error('Cannot run with local changes. Please make sure there are no local changes.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@ -68,15 +69,15 @@ export async function discoverNewConflictsForPr(
|
||||
/* PRs which were found to be conflicting. */
|
||||
const conflicts: Array<PullRequest> = [];
|
||||
|
||||
console.info(`Requesting pending PRs from Github`);
|
||||
info(`Requesting pending PRs from Github`);
|
||||
/** List of PRs from github currently known as mergable. */
|
||||
const allPendingPRs = (await getPendingPrs(PR_SCHEMA, config.github)).map(processPr);
|
||||
/** The PR which is being checked against. */
|
||||
const requestedPr = allPendingPRs.find(pr => pr.number === newPrNumber);
|
||||
if (requestedPr === undefined) {
|
||||
console.error(
|
||||
error(
|
||||
`The request PR, #${newPrNumber} was not found as a pending PR on github, please confirm`);
|
||||
console.error(`the PR number is correct and is an open PR`);
|
||||
error(`the PR number is correct and is an open PR`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@ -89,8 +90,8 @@ export async function discoverNewConflictsForPr(
|
||||
// PRs updated after the provided date
|
||||
pr.updatedAt >= updatedAfter);
|
||||
});
|
||||
console.info(`Retrieved ${allPendingPRs.length} total pending PRs`);
|
||||
console.info(`Checking ${pendingPrs.length} PRs for conflicts after a merge of #${newPrNumber}`);
|
||||
info(`Retrieved ${allPendingPRs.length} total pending PRs`);
|
||||
info(`Checking ${pendingPrs.length} PRs for conflicts after a merge of #${newPrNumber}`);
|
||||
|
||||
// Fetch and checkout the PR being checked.
|
||||
exec(`git fetch ${requestedPr.headRef.repository.url} ${requestedPr.headRef.name}`);
|
||||
@ -100,7 +101,7 @@ export async function discoverNewConflictsForPr(
|
||||
exec(`git fetch ${requestedPr.baseRef.repository.url} ${requestedPr.baseRef.name}`);
|
||||
const result = exec(`git rebase FETCH_HEAD`);
|
||||
if (result.code) {
|
||||
console.error('The requested PR currently has conflicts');
|
||||
error('The requested PR currently has conflicts');
|
||||
cleanUpGitState(originalBranch);
|
||||
process.exit(1);
|
||||
}
|
||||
@ -125,21 +126,23 @@ export async function discoverNewConflictsForPr(
|
||||
}
|
||||
// End the progress bar as all PRs have been processed.
|
||||
progressBar.stop();
|
||||
console.info(`\nResult:`);
|
||||
info();
|
||||
info(`Result:`);
|
||||
|
||||
cleanUpGitState(originalBranch);
|
||||
|
||||
// If no conflicts are found, exit successfully.
|
||||
if (conflicts.length === 0) {
|
||||
console.info(`No new conflicting PRs found after #${newPrNumber} merging`);
|
||||
info(`No new conflicting PRs found after #${newPrNumber} merging`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Inform about discovered conflicts, exit with failure.
|
||||
console.error(`${conflicts.length} PR(s) which conflict(s) after #${newPrNumber} merges:`);
|
||||
error.group(`${conflicts.length} PR(s) which conflict(s) after #${newPrNumber} merges:`);
|
||||
for (const pr of conflicts) {
|
||||
console.error(` - ${pr.number}: ${pr.title}`);
|
||||
error(` - ${pr.number}: ${pr.title}`);
|
||||
}
|
||||
error.groupEnd();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ ts_library(
|
||||
"@npm//@octokit/rest",
|
||||
"@npm//@types/inquirer",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/semver",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//chalk",
|
||||
],
|
||||
|
@ -6,8 +6,10 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import {Arguments, Argv} from 'yargs';
|
||||
|
||||
import {error, red, yellow} from '../../utils/console';
|
||||
|
||||
import {GITHUB_TOKEN_GENERATE_URL, mergePullRequest} from './index';
|
||||
|
||||
/** Builds the options for the merge command. */
|
||||
@ -22,10 +24,9 @@ export function buildMergeCommand(yargs: Argv) {
|
||||
export async function handleMergeCommand(args: Arguments) {
|
||||
const githubToken = args.githubToken || process.env.GITHUB_TOKEN || process.env.TOKEN;
|
||||
if (!githubToken) {
|
||||
console.error(
|
||||
chalk.red('No Github token set. Please set the `GITHUB_TOKEN` environment variable.'));
|
||||
console.error(chalk.red('Alternatively, pass the `--github-token` command line flag.'));
|
||||
console.error(chalk.yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`));
|
||||
error(red('No Github token set. Please set the `GITHUB_TOKEN` environment variable.'));
|
||||
error(red('Alternatively, pass the `--github-token` command line flag.'));
|
||||
error(yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
68
dev-infra/pr/merge/determine-merge-branches.ts
Normal file
68
dev-infra/pr/merge/determine-merge-branches.ts
Normal file
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as semver from 'semver';
|
||||
import {exec} from '../../utils/shelljs';
|
||||
|
||||
/**
|
||||
* Helper function that can be used to determine merge branches based on a given
|
||||
* project version. The function determines merge branches primarily through the
|
||||
* specified version, but falls back to consulting the NPM registry when needed.
|
||||
*
|
||||
* Consulting the NPM registry for determining the patch branch may slow down merging,
|
||||
* so whenever possible, the branches are determined statically based on the current
|
||||
* version. In some cases, consulting the NPM registry is inevitable because for major
|
||||
* pre-releases, we cannot determine the latest stable minor version from the current
|
||||
* pre-release version.
|
||||
*/
|
||||
export function determineMergeBranches(
|
||||
currentVersion: string, npmPackageName: string): {minor: string, patch: string} {
|
||||
const projectVersion = semver.parse(currentVersion);
|
||||
if (projectVersion === null) {
|
||||
throw Error('Cannot parse version set in project "package.json" file.');
|
||||
}
|
||||
const {major, minor, patch, prerelease} = projectVersion;
|
||||
const isMajor = minor === 0 && patch === 0;
|
||||
const isMinor = minor !== 0 && patch === 0;
|
||||
|
||||
// If there is no prerelease, then we compute patch and minor branches based
|
||||
// on the current version major and minor.
|
||||
if (prerelease.length === 0) {
|
||||
return {minor: `${major}.x`, patch: `${major}.${minor}.x`};
|
||||
}
|
||||
|
||||
// If current version is set to a minor prerelease, we can compute the merge branches
|
||||
// statically. e.g. if we are set to `9.3.0-next.0`, then our merge branches should
|
||||
// be set to `9.x` and `9.2.x`.
|
||||
if (isMinor) {
|
||||
return {minor: `${major}.x`, patch: `${major}.${minor - 1}.x`};
|
||||
} else if (!isMajor) {
|
||||
throw Error('Unexpected version. Cannot have prerelease for patch version.');
|
||||
}
|
||||
|
||||
// If we are set to a major prerelease, we cannot statically determine the stable patch
|
||||
// branch (as the latest minor segment is unknown). We determine it by looking in the NPM
|
||||
// registry for the latest stable release that will tell us about the current minor segment.
|
||||
// e.g. if the current major is `v10.0.0-next.0`, then we need to look for the latest release.
|
||||
// Let's say this is `v9.2.6`. Our patch branch will then be called `9.2.x`.
|
||||
const latestVersion = exec(`yarn -s info ${npmPackageName} dist-tags.latest`).trim();
|
||||
if (!latestVersion) {
|
||||
throw Error('Could not determine version of latest release.');
|
||||
}
|
||||
const expectedMajor = major - 1;
|
||||
const parsedLatestVersion = semver.parse(latestVersion);
|
||||
if (parsedLatestVersion === null) {
|
||||
throw Error(`Could not parse latest version from NPM registry: ${latestVersion}`);
|
||||
} else if (parsedLatestVersion.major !== expectedMajor) {
|
||||
throw Error(
|
||||
`Expected latest release to have major version: v${expectedMajor}, ` +
|
||||
`but got: v${latestVersion}`);
|
||||
}
|
||||
|
||||
return {patch: `${expectedMajor}.${parsedLatestVersion.minor}.x`, minor: `${expectedMajor}.x`};
|
||||
}
|
@ -8,6 +8,9 @@
|
||||
|
||||
import * as Octokit from '@octokit/rest';
|
||||
import {spawnSync, SpawnSyncOptions, SpawnSyncReturns} from 'child_process';
|
||||
|
||||
import {info} from '../../utils/console';
|
||||
|
||||
import {MergeConfigWithRemote} from './config';
|
||||
|
||||
/** Error for failed Github API requests. */
|
||||
@ -74,7 +77,7 @@ export class GitClient {
|
||||
// To improve the debugging experience in case something fails, we print all executed
|
||||
// Git commands. Note that we do not want to print the token if is contained in the
|
||||
// command. It's common to share errors with others if the tool failed.
|
||||
console.info('Executing: git', this.omitGithubTokenFromMessage(args.join(' ')));
|
||||
info('Executing: git', this.omitGithubTokenFromMessage(args.join(' ')));
|
||||
|
||||
const result = spawnSync('git', args, {
|
||||
cwd: this._projectRoot,
|
||||
|
@ -6,10 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
|
||||
import {getRepoBaseDir} from '../../utils/config';
|
||||
import {promptConfirm} from '../../utils/console';
|
||||
import {error, green, info, promptConfirm, red, yellow} from '../../utils/console';
|
||||
|
||||
import {loadAndValidateConfig, MergeConfigWithRemote} from './config';
|
||||
import {GithubApiRequestError} from './git';
|
||||
@ -40,8 +39,8 @@ export async function mergePullRequest(
|
||||
if (config === undefined) {
|
||||
const {config: _config, errors} = loadAndValidateConfig();
|
||||
if (errors) {
|
||||
console.error(chalk.red('Invalid configuration:'));
|
||||
errors.forEach(desc => console.error(chalk.yellow(` - ${desc}`)));
|
||||
error(red('Invalid configuration:'));
|
||||
errors.forEach(desc => error(yellow(` - ${desc}`)));
|
||||
process.exit(1);
|
||||
}
|
||||
config = _config!;
|
||||
@ -65,9 +64,9 @@ export async function mergePullRequest(
|
||||
// Catch errors to the Github API for invalid requests. We want to
|
||||
// exit the script with a better explanation of the error.
|
||||
if (e instanceof GithubApiRequestError && e.status === 401) {
|
||||
console.error(chalk.red('Github API request failed. ' + e.message));
|
||||
console.error(chalk.yellow('Please ensure that your provided token is valid.'));
|
||||
console.error(chalk.yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`));
|
||||
error(red('Github API request failed. ' + e.message));
|
||||
error(yellow('Please ensure that your provided token is valid.'));
|
||||
error(yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`));
|
||||
process.exit(1);
|
||||
}
|
||||
throw e;
|
||||
@ -99,25 +98,25 @@ export async function mergePullRequest(
|
||||
|
||||
switch (status) {
|
||||
case MergeStatus.SUCCESS:
|
||||
console.info(chalk.green(`Successfully merged the pull request: ${prNumber}`));
|
||||
info(green(`Successfully merged the pull request: ${prNumber}`));
|
||||
return true;
|
||||
case MergeStatus.DIRTY_WORKING_DIR:
|
||||
console.error(chalk.red(
|
||||
`Local working repository not clean. Please make sure there are ` +
|
||||
`no uncommitted changes.`));
|
||||
error(
|
||||
red(`Local working repository not clean. Please make sure there are ` +
|
||||
`no uncommitted changes.`));
|
||||
return false;
|
||||
case MergeStatus.UNKNOWN_GIT_ERROR:
|
||||
console.error(chalk.red(
|
||||
'An unknown Git error has been thrown. Please check the output ' +
|
||||
'above for details.'));
|
||||
error(
|
||||
red('An unknown Git error has been thrown. Please check the output ' +
|
||||
'above for details.'));
|
||||
return false;
|
||||
case MergeStatus.FAILED:
|
||||
console.error(chalk.yellow(`Could not merge the specified pull request.`));
|
||||
console.error(chalk.red(failure!.message));
|
||||
error(yellow(`Could not merge the specified pull request.`));
|
||||
error(red(failure!.message));
|
||||
if (canForciblyMerge && !disableForceMergePrompt) {
|
||||
console.info();
|
||||
console.info(chalk.yellow('The pull request above failed due to non-critical errors.'));
|
||||
console.info(chalk.yellow(`This error can be forcibly ignored if desired.`));
|
||||
info();
|
||||
info(yellow('The pull request above failed due to non-critical errors.'));
|
||||
info(yellow(`This error can be forcibly ignored if desired.`));
|
||||
return await promptAndPerformForceMerge();
|
||||
}
|
||||
return false;
|
||||
|
@ -8,6 +8,8 @@
|
||||
|
||||
import {Arguments, Argv} from 'yargs';
|
||||
|
||||
import {error} from '../../utils/console';
|
||||
|
||||
import {rebasePr} from './index';
|
||||
|
||||
/** URL to the Github page where personal access tokens can be generated. */
|
||||
@ -25,9 +27,9 @@ export function buildRebaseCommand(yargs: Argv) {
|
||||
export async function handleRebaseCommand(args: Arguments) {
|
||||
const githubToken = args.githubToken || process.env.GITHUB_TOKEN || process.env.TOKEN;
|
||||
if (!githubToken) {
|
||||
console.error('No Github token set. Please set the `GITHUB_TOKEN` environment variable.');
|
||||
console.error('Alternatively, pass the `--github-token` command line flag.');
|
||||
console.error(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`);
|
||||
error('No Github token set. Please set the `GITHUB_TOKEN` environment variable.');
|
||||
error('Alternatively, pass the `--github-token` command line flag.');
|
||||
error(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
@ -6,12 +6,11 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {prompt} from 'inquirer';
|
||||
import {types as graphQLTypes} from 'typed-graphqlify';
|
||||
import {URL} from 'url';
|
||||
|
||||
import {getConfig, NgDevConfig} from '../../utils/config';
|
||||
import {promptConfirm} from '../../utils/console';
|
||||
import {error, info, promptConfirm} from '../../utils/console';
|
||||
import {getCurrentBranch, hasLocalChanges} from '../../utils/git';
|
||||
import {getPr} from '../../utils/github';
|
||||
import {exec} from '../../utils/shelljs';
|
||||
@ -45,7 +44,7 @@ export async function rebasePr(
|
||||
prNumber: number, githubToken: string, config: Pick<NgDevConfig, 'github'> = getConfig()) {
|
||||
// TODO: Rely on a common assertNoLocalChanges function.
|
||||
if (hasLocalChanges()) {
|
||||
console.error('Cannot perform rebase of PR with local changes.');
|
||||
error('Cannot perform rebase of PR with local changes.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@ -65,7 +64,7 @@ export async function rebasePr(
|
||||
// If the PR does not allow maintainers to modify it, exit as the rebased PR cannot
|
||||
// be pushed up.
|
||||
if (!pr.maintainerCanModify && !pr.viewerDidAuthor) {
|
||||
console.error(
|
||||
error(
|
||||
`Cannot rebase as you did not author the PR and the PR does not allow maintainers` +
|
||||
`to modify the PR`);
|
||||
process.exit(1);
|
||||
@ -73,51 +72,48 @@ export async function rebasePr(
|
||||
|
||||
try {
|
||||
// Fetch the branch at the commit of the PR, and check it out in a detached state.
|
||||
console.info(`Checking out PR #${prNumber} from ${fullHeadRef}`);
|
||||
info(`Checking out PR #${prNumber} from ${fullHeadRef}`);
|
||||
exec(`git fetch ${headRefUrl} ${pr.headRef.name}`);
|
||||
exec(`git checkout --detach FETCH_HEAD`);
|
||||
|
||||
// Fetch the PRs target branch and rebase onto it.
|
||||
console.info(`Fetching ${fullBaseRef} to rebase #${prNumber} on`);
|
||||
info(`Fetching ${fullBaseRef} to rebase #${prNumber} on`);
|
||||
exec(`git fetch ${baseRefUrl} ${pr.baseRef.name}`);
|
||||
console.info(`Attempting to rebase PR #${prNumber} on ${fullBaseRef}`);
|
||||
info(`Attempting to rebase PR #${prNumber} on ${fullBaseRef}`);
|
||||
const rebaseResult = exec(`git rebase FETCH_HEAD`);
|
||||
|
||||
// If the rebase was clean, push the rebased PR up to the authors fork.
|
||||
if (rebaseResult.code === 0) {
|
||||
console.info(`Rebase was able to complete automatically without conflicts`);
|
||||
console.info(`Pushing rebased PR #${prNumber} to ${fullHeadRef}`);
|
||||
info(`Rebase was able to complete automatically without conflicts`);
|
||||
info(`Pushing rebased PR #${prNumber} to ${fullHeadRef}`);
|
||||
exec(`git push ${baseRefUrl} HEAD:${pr.baseRef.name} --force-with-lease`);
|
||||
console.info(`Rebased and updated PR #${prNumber}`);
|
||||
info(`Rebased and updated PR #${prNumber}`);
|
||||
cleanUpGitState();
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
error(err.message);
|
||||
cleanUpGitState();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// On automatic rebase failures, prompt to choose if the rebase should be continued
|
||||
// manually or aborted now.
|
||||
console.info(`Rebase was unable to complete automatically without conflicts.`);
|
||||
info(`Rebase was unable to complete automatically without conflicts.`);
|
||||
// If the command is run in a non-CI environment, prompt to format the files immediately.
|
||||
const continueRebase =
|
||||
process.env['CI'] === undefined && await promptConfirm('Manually complete rebase?');
|
||||
|
||||
if (continueRebase) {
|
||||
console.info(
|
||||
`After manually completing rebase, run the following command to update PR #${prNumber}:`);
|
||||
console.info(
|
||||
` $ git push ${pr.baseRef.repository.url} HEAD:${pr.baseRef.name} --force-with-lease`);
|
||||
console.info();
|
||||
console.info(
|
||||
`To abort the rebase and return to the state of the repository before this command`);
|
||||
console.info(`run the following command:`);
|
||||
console.info(` $ git rebase --abort && git reset --hard && git checkout ${originalBranch}`);
|
||||
info(`After manually completing rebase, run the following command to update PR #${prNumber}:`);
|
||||
info(` $ git push ${pr.baseRef.repository.url} HEAD:${pr.baseRef.name} --force-with-lease`);
|
||||
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}`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.info(`Cleaning up git state, and restoring previous state.`);
|
||||
info(`Cleaning up git state, and restoring previous state.`);
|
||||
}
|
||||
|
||||
cleanUpGitState();
|
||||
|
@ -5,6 +5,8 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {error} from '../utils/console';
|
||||
import {convertConditionToFunction} from './condition_evaluator';
|
||||
import {PullApproveGroupConfig} from './parse-yaml';
|
||||
|
||||
@ -58,9 +60,11 @@ export class PullApproveGroup {
|
||||
matchedFiles: new Set(),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Could not parse condition in group: ${this.groupName}`);
|
||||
console.error(` - ${expression}`);
|
||||
console.error(`Error:`, e.message, e.stack);
|
||||
error(`Could not parse condition in group: ${this.groupName}`);
|
||||
error(` - ${expression}`);
|
||||
error(`Error:`);
|
||||
error(e.message);
|
||||
error(e.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@ -84,7 +88,7 @@ export class PullApproveGroup {
|
||||
`From the [${this.groupName}] group:\n` +
|
||||
` - ${expression}` +
|
||||
`\n\n${e.message} ${e.stack}\n\n`;
|
||||
console.error(errMessage);
|
||||
error(errMessage);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
@ -6,18 +6,19 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {info} from '../utils/console';
|
||||
import {PullApproveGroupResult} from './group';
|
||||
|
||||
/** Create logs for each pullapprove group result. */
|
||||
export function logGroup(group: PullApproveGroupResult, matched = true) {
|
||||
const conditions = matched ? group.matchedConditions : group.unmatchedConditions;
|
||||
console.groupCollapsed(`[${group.groupName}]`);
|
||||
info.group(`[${group.groupName}]`);
|
||||
if (conditions.length) {
|
||||
conditions.forEach(matcher => {
|
||||
const count = matcher.matchedFiles.size;
|
||||
console.info(`${count} ${count === 1 ? 'match' : 'matches'} - ${matcher.expression}`)
|
||||
info(`${count} ${count === 1 ? 'match' : 'matches'} - ${matcher.expression}`);
|
||||
});
|
||||
console.groupEnd();
|
||||
info.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,7 +31,7 @@ export function logHeader(...params: string[]) {
|
||||
const rightSpace = fillWidth - leftSpace - headerText.length;
|
||||
const fill = (count: number, content: string) => content.repeat(count);
|
||||
|
||||
console.info(`┌${fill(fillWidth, '─')}┐`);
|
||||
console.info(`│${fill(leftSpace, ' ')}${headerText}${fill(rightSpace, ' ')}│`);
|
||||
console.info(`└${fill(fillWidth, '─')}┘`);
|
||||
info(`┌${fill(fillWidth, '─')}┐`);
|
||||
info(`│${fill(leftSpace, ' ')}${headerText}${fill(rightSpace, ' ')}│`);
|
||||
info(`└${fill(fillWidth, '─')}┘`);
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import * as path from 'path';
|
||||
import {cd, exec, set} from 'shelljs';
|
||||
|
||||
import {getRepoBaseDir} from '../utils/config';
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {PullApproveGroup} from './group';
|
||||
import {logGroup, logHeader} from './logging';
|
||||
@ -67,38 +68,39 @@ export function verify(verbose = false) {
|
||||
*/
|
||||
logHeader('Overall Result');
|
||||
if (verificationSucceeded) {
|
||||
console.info('PullApprove verification succeeded!');
|
||||
info('PullApprove verification succeeded!');
|
||||
} else {
|
||||
console.info(`PullApprove verification failed.\n`);
|
||||
console.info(`Please update '.pullapprove.yml' to ensure that all necessary`);
|
||||
console.info(`files/directories have owners and all patterns that appear in`);
|
||||
console.info(`the file correspond to actual files/directories in the repo.`);
|
||||
info(`PullApprove verification failed.`);
|
||||
info();
|
||||
info(`Please update '.pullapprove.yml' to ensure that all necessary`);
|
||||
info(`files/directories have owners and all patterns that appear in`);
|
||||
info(`the file correspond to actual files/directories in the repo.`);
|
||||
}
|
||||
/**
|
||||
* File by file Summary
|
||||
*/
|
||||
logHeader('PullApprove results by file');
|
||||
console.groupCollapsed(`Matched Files (${matchedFiles.length} files)`);
|
||||
verbose && matchedFiles.forEach(file => console.info(file));
|
||||
console.groupEnd();
|
||||
console.groupCollapsed(`Unmatched Files (${unmatchedFiles.length} files)`);
|
||||
unmatchedFiles.forEach(file => console.info(file));
|
||||
console.groupEnd();
|
||||
info.group(`Matched Files (${matchedFiles.length} files)`);
|
||||
verbose && matchedFiles.forEach(file => info(file));
|
||||
info.groupEnd();
|
||||
info.group(`Unmatched Files (${unmatchedFiles.length} files)`);
|
||||
unmatchedFiles.forEach(file => info(file));
|
||||
info.groupEnd();
|
||||
/**
|
||||
* Group by group Summary
|
||||
*/
|
||||
logHeader('PullApprove results by group');
|
||||
console.groupCollapsed(`Groups skipped (${groupsSkipped.length} groups)`);
|
||||
verbose && groupsSkipped.forEach(group => console.info(`${group.groupName}`));
|
||||
console.groupEnd();
|
||||
info.group(`Groups skipped (${groupsSkipped.length} groups)`);
|
||||
verbose && groupsSkipped.forEach(group => info(`${group.groupName}`));
|
||||
info.groupEnd();
|
||||
const matchedGroups = resultsByGroup.filter(group => !group.unmatchedCount);
|
||||
console.groupCollapsed(`Matched conditions by Group (${matchedGroups.length} groups)`);
|
||||
info.group(`Matched conditions by Group (${matchedGroups.length} groups)`);
|
||||
verbose && matchedGroups.forEach(group => logGroup(group));
|
||||
console.groupEnd();
|
||||
info.groupEnd();
|
||||
const unmatchedGroups = resultsByGroup.filter(group => group.unmatchedCount);
|
||||
console.groupCollapsed(`Unmatched conditions by Group (${unmatchedGroups.length} groups)`);
|
||||
info.group(`Unmatched conditions by Group (${unmatchedGroups.length} groups)`);
|
||||
unmatchedGroups.forEach(group => logGroup(group, false));
|
||||
console.groupEnd();
|
||||
info.groupEnd();
|
||||
|
||||
// Provide correct exit code based on verification success.
|
||||
process.exit(verificationSucceeded ? 0 : 1);
|
||||
|
@ -8,6 +8,7 @@ ts_library(
|
||||
module_name = "@angular/dev-infra-private/release",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/shelljs",
|
||||
"@npm//@types/yargs",
|
||||
|
@ -8,6 +8,8 @@
|
||||
|
||||
import {exec as _exec} from 'shelljs';
|
||||
|
||||
import {info} from '../utils/console';
|
||||
|
||||
/**
|
||||
* Log the environment variables expected by bazel for stamping.
|
||||
*
|
||||
|
@ -16,6 +16,7 @@
|
||||
"inquirer": "<from-root>",
|
||||
"minimatch": "<from-root>",
|
||||
"multimatch": "<from-root>",
|
||||
"semver": "<from-root>",
|
||||
"shelljs": "<from-root>",
|
||||
"typed-graphqlify": "<from-root>",
|
||||
"yaml": "<from-root>",
|
||||
|
@ -8,6 +8,8 @@
|
||||
|
||||
import {dirname, isAbsolute, resolve} from 'path';
|
||||
|
||||
import {error} from '../utils/console';
|
||||
|
||||
import {ModuleResolver} from './analyzer';
|
||||
|
||||
|
||||
@ -52,8 +54,8 @@ export function loadTestConfig(configPath: string): CircularDependenciesTestConf
|
||||
}
|
||||
return config;
|
||||
} catch (e) {
|
||||
console.error('Could not load test configuration file at: ' + configPath);
|
||||
console.error(`Failed with: ${e.message}`);
|
||||
error('Could not load test configuration file at: ' + configPath);
|
||||
error(`Failed with: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,8 @@ import {sync as globSync} from 'glob';
|
||||
import {isAbsolute, relative, resolve} from 'path';
|
||||
import * as ts from 'typescript';
|
||||
import * as yargs from 'yargs';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import {green, info, error, red, yellow} from '../utils/console';
|
||||
|
||||
import {Analyzer, ReferenceChain} from './analyzer';
|
||||
import {compareGoldens, convertReferenceChainToGolden, Golden} from './golden';
|
||||
@ -66,15 +67,14 @@ export function main(
|
||||
|
||||
const actual = convertReferenceChainToGolden(cycles, baseDir);
|
||||
|
||||
console.info(
|
||||
chalk.green(` Current number of cycles: ${chalk.yellow(cycles.length.toString())}`));
|
||||
info(green(` Current number of cycles: ${yellow(cycles.length.toString())}`));
|
||||
|
||||
if (approve) {
|
||||
writeFileSync(goldenFile, JSON.stringify(actual, null, 2));
|
||||
console.info(chalk.green('✅ Updated golden file.'));
|
||||
info(green('✅ Updated golden file.'));
|
||||
return 0;
|
||||
} else if (!existsSync(goldenFile)) {
|
||||
console.error(chalk.red(`❌ Could not find golden file: ${goldenFile}`));
|
||||
error(red(`❌ Could not find golden file: ${goldenFile}`));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@ -84,17 +84,15 @@ export function main(
|
||||
// it's common that third-party modules are not resolved/visited. Also generated files
|
||||
// from the View Engine compiler (i.e. factories, summaries) cannot be resolved.
|
||||
if (printWarnings && warningsCount !== 0) {
|
||||
console.info(chalk.yellow('⚠ The following imports could not be resolved:'));
|
||||
Array.from(analyzer.unresolvedModules)
|
||||
.sort()
|
||||
.forEach(specifier => console.info(` • ${specifier}`));
|
||||
info(yellow('⚠ The following imports could not be resolved:'));
|
||||
Array.from(analyzer.unresolvedModules).sort().forEach(specifier => info(` • ${specifier}`));
|
||||
analyzer.unresolvedFiles.forEach((value, key) => {
|
||||
console.info(` • ${getRelativePath(baseDir, key)}`);
|
||||
value.sort().forEach(specifier => console.info(` ${specifier}`));
|
||||
info(` • ${getRelativePath(baseDir, key)}`);
|
||||
value.sort().forEach(specifier => info(` ${specifier}`));
|
||||
});
|
||||
} else {
|
||||
console.info(chalk.yellow(`⚠ ${warningsCount} imports could not be resolved.`));
|
||||
console.info(chalk.yellow(` Please rerun with "--warnings" to inspect unresolved imports.`));
|
||||
info(yellow(`⚠ ${warningsCount} imports could not be resolved.`));
|
||||
info(yellow(` Please rerun with "--warnings" to inspect unresolved imports.`));
|
||||
}
|
||||
|
||||
const expected: Golden = JSON.parse(readFileSync(goldenFile, 'utf8'));
|
||||
@ -102,25 +100,24 @@ export function main(
|
||||
const isMatching = fixedCircularDeps.length === 0 && newCircularDeps.length === 0;
|
||||
|
||||
if (isMatching) {
|
||||
console.info(chalk.green('✅ Golden matches current circular dependencies.'));
|
||||
info(green('✅ Golden matches current circular dependencies.'));
|
||||
return 0;
|
||||
}
|
||||
|
||||
console.error(chalk.red('❌ Golden does not match current circular dependencies.'));
|
||||
error(red('❌ Golden does not match current circular dependencies.'));
|
||||
if (newCircularDeps.length !== 0) {
|
||||
console.error(chalk.yellow(` New circular dependencies which are not allowed:`));
|
||||
newCircularDeps.forEach(c => console.error(` • ${convertReferenceChainToString(c)}`));
|
||||
console.error();
|
||||
error(yellow(` New circular dependencies which are not allowed:`));
|
||||
newCircularDeps.forEach(c => error(` • ${convertReferenceChainToString(c)}`));
|
||||
error();
|
||||
}
|
||||
if (fixedCircularDeps.length !== 0) {
|
||||
console.error(
|
||||
chalk.yellow(` Fixed circular dependencies that need to be removed from the golden:`));
|
||||
fixedCircularDeps.forEach(c => console.error(` • ${convertReferenceChainToString(c)}`));
|
||||
console.error();
|
||||
error(yellow(` Fixed circular dependencies that need to be removed from the golden:`));
|
||||
fixedCircularDeps.forEach(c => error(` • ${convertReferenceChainToString(c)}`));
|
||||
error();
|
||||
if (approveCommand) {
|
||||
console.info(chalk.yellow(` Please approve the new golden with: ${approveCommand}`));
|
||||
info(yellow(` Please approve the new golden with: ${approveCommand}`));
|
||||
} else {
|
||||
console.info(chalk.yellow(
|
||||
info(yellow(
|
||||
` Please update the golden. The following command can be ` +
|
||||
`run: yarn ts-circular-deps approve ${getRelativePath(process.cwd(), goldenFile)}.`));
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ ts_library(
|
||||
"@npm//@types/inquirer",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/shelljs",
|
||||
"@npm//chalk",
|
||||
"@npm//shelljs",
|
||||
"@npm//tslib",
|
||||
"@npm//typed-graphqlify",
|
||||
|
@ -7,8 +7,10 @@
|
||||
*/
|
||||
|
||||
import {existsSync} from 'fs';
|
||||
import {join} from 'path';
|
||||
import {dirname, join} from 'path';
|
||||
import {exec} from 'shelljs';
|
||||
|
||||
import {error} from './console';
|
||||
import {isTsNodeAvailable} from './ts-node';
|
||||
|
||||
/**
|
||||
@ -83,14 +85,19 @@ function readConfigFile(configPath: string): object {
|
||||
// version of the given configuration seems to exist, set up `ts-node` if available.
|
||||
if (require.extensions['.ts'] === undefined && existsSync(`${configPath}.ts`) &&
|
||||
isTsNodeAvailable()) {
|
||||
require('ts-node').register({skipProject: true, transpileOnly: true});
|
||||
// Ensure the module target is set to `commonjs`. This is necessary because the
|
||||
// dev-infra tool runs in NodeJS which does not support ES modules by default.
|
||||
// Additionally, set the `dir` option to the directory that contains the configuration
|
||||
// file. This allows for custom compiler options (such as `--strict`).
|
||||
require('ts-node').register(
|
||||
{dir: dirname(configPath), transpileOnly: true, compilerOptions: {module: 'commonjs'}});
|
||||
}
|
||||
|
||||
try {
|
||||
return require(configPath)
|
||||
return require(configPath);
|
||||
} catch (e) {
|
||||
console.error('Could not read configuration file.');
|
||||
console.error(e);
|
||||
error('Could not read configuration file.');
|
||||
error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@ -103,9 +110,9 @@ export function assertNoErrors(errors: string[]) {
|
||||
if (errors.length == 0) {
|
||||
return;
|
||||
}
|
||||
console.error(`Errors discovered while loading configuration file:`);
|
||||
for (const error of errors) {
|
||||
console.error(` - ${error}`);
|
||||
error(`Errors discovered while loading configuration file:`);
|
||||
for (const err of errors) {
|
||||
error(` - ${err}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
@ -6,8 +6,15 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import {prompt} from 'inquirer';
|
||||
|
||||
|
||||
/** Reexport of chalk colors for convenient access. */
|
||||
export const red: typeof chalk = chalk.red;
|
||||
export const green: typeof chalk = chalk.green;
|
||||
export const yellow: typeof chalk = chalk.yellow;
|
||||
|
||||
/** Prompts the user with a confirmation question and a specified message. */
|
||||
export async function promptConfirm(message: string, defaultValue = false): Promise<boolean> {
|
||||
return (await prompt<{result: boolean}>({
|
||||
@ -18,3 +25,86 @@ export async function promptConfirm(message: string, defaultValue = false): Prom
|
||||
}))
|
||||
.result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported levels for logging functions.
|
||||
*
|
||||
* Levels are mapped to numbers to represent a hierarchy of logging levels.
|
||||
*/
|
||||
export enum LOG_LEVELS {
|
||||
SILENT = 0,
|
||||
ERROR = 1,
|
||||
WARN = 2,
|
||||
LOG = 3,
|
||||
INFO = 4,
|
||||
DEBUG = 5,
|
||||
}
|
||||
|
||||
/** Default log level for the tool. */
|
||||
export const DEFAULT_LOG_LEVEL = LOG_LEVELS.INFO;
|
||||
|
||||
/** Write to the console for at INFO logging level */
|
||||
export const info = buildLogLevelFunction(() => console.info, LOG_LEVELS.INFO);
|
||||
|
||||
/** Write to the console for at ERROR logging level */
|
||||
export const error = buildLogLevelFunction(() => console.error, LOG_LEVELS.ERROR);
|
||||
|
||||
/** Write to the console for at DEBUG logging level */
|
||||
export const debug = buildLogLevelFunction(() => console.debug, LOG_LEVELS.DEBUG);
|
||||
|
||||
/** Write to the console for at LOG logging level */
|
||||
// tslint:disable-next-line: no-console
|
||||
export const log = buildLogLevelFunction(() => console.log, LOG_LEVELS.LOG);
|
||||
|
||||
/** Write to the console for at WARN logging level */
|
||||
export const warn = buildLogLevelFunction(() => console.warn, LOG_LEVELS.WARN);
|
||||
|
||||
/** Build an instance of a logging function for the provided level. */
|
||||
function buildLogLevelFunction(loadCommand: () => Function, level: LOG_LEVELS) {
|
||||
/** Write to stdout for the LOG_LEVEL. */
|
||||
const loggingFunction = (...text: string[]) => {
|
||||
runConsoleCommand(loadCommand, level, ...text);
|
||||
};
|
||||
|
||||
/** Start a group at the LOG_LEVEL, optionally starting it as collapsed. */
|
||||
loggingFunction.group = (text: string, collapsed = false) => {
|
||||
const command = collapsed ? console.groupCollapsed : console.group;
|
||||
runConsoleCommand(() => command, level, text);
|
||||
};
|
||||
|
||||
/** End the group at the LOG_LEVEL. */
|
||||
loggingFunction.groupEnd = () => {
|
||||
runConsoleCommand(() => console.groupEnd, level);
|
||||
};
|
||||
|
||||
return loggingFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the console command provided, if the environments logging level greater than the
|
||||
* provided logging level.
|
||||
*
|
||||
* The loadCommand takes in a function which is called to retrieve the console.* function
|
||||
* to allow for jasmine spies to still work in testing. Without this method of retrieval
|
||||
* the console.* function, the function is saved into the closure of the created logging
|
||||
* function before jasmine can spy.
|
||||
*/
|
||||
function runConsoleCommand(loadCommand: () => Function, logLevel: LOG_LEVELS, ...text: string[]) {
|
||||
if (getLogLevel() >= logLevel) {
|
||||
loadCommand()(...text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the log level from environment variables, if the value found
|
||||
* based on the LOG_LEVEL environment variable is undefined, return the default
|
||||
* logging level.
|
||||
*/
|
||||
function getLogLevel() {
|
||||
const logLevelEnvValue: any = (process.env[`LOG_LEVEL`] || '').toUpperCase();
|
||||
const logLevel = LOG_LEVELS[logLevelEnvValue];
|
||||
if (logLevel === undefined) {
|
||||
return DEFAULT_LOG_LEVEL;
|
||||
}
|
||||
return logLevel;
|
||||
}
|
||||
|
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "9.1.8",
|
||||
"version": "9.1.12",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
@ -16,9 +16,11 @@
|
||||
"url": "https://github.com/angular/angular.git"
|
||||
},
|
||||
"scripts": {
|
||||
"bazel:format": "yarn -s ng-dev format deprecation-warning bazel:format",
|
||||
"bazel:lint": "yarn -s ng-dev format deprecation-warning bazel:lint",
|
||||
"bazel:lint-fix": "yarn -s ng-dev format deprecation-warning bazel:lint-fix",
|
||||
"/": "",
|
||||
"// 1": "Many developer of our checks/scripts/tools have moved to our ng-dev tool",
|
||||
"// 2": "Find the usage you are looking for with:",
|
||||
"// 3": "yarn ng-dev --help",
|
||||
"/ ": "",
|
||||
"preinstall": "node tools/yarn/check-yarn.js",
|
||||
"postinstall": "node scripts/webdriver-manager-update.js && node --preserve-symlinks --preserve-symlinks-main ./tools/postinstall-patches.js",
|
||||
"check-env": "gulp check-env",
|
||||
|
@ -12,7 +12,7 @@ import {AsyncNgccOptions, NgccOptions, SyncNgccOptions} from './src/ngcc_options
|
||||
|
||||
export {ConsoleLogger} from './src/logging/console_logger';
|
||||
export {Logger, LogLevel} from './src/logging/logger';
|
||||
export {AsyncNgccOptions, NgccOptions, SyncNgccOptions} from './src/ngcc_options';
|
||||
export {AsyncNgccOptions, clearTsConfigCache, NgccOptions, SyncNgccOptions} from './src/ngcc_options';
|
||||
export {PathMappings} from './src/path_mappings';
|
||||
|
||||
export function process(options: AsyncNgccOptions): Promise<void>;
|
||||
|
@ -156,7 +156,7 @@ export function getSharedSetup(options: NgccOptions): SharedSetup&RequiredNgccOp
|
||||
const absBasePath = absoluteFrom(options.basePath);
|
||||
const projectPath = fileSystem.dirname(absBasePath);
|
||||
const tsConfig =
|
||||
options.tsConfigPath !== null ? readConfiguration(options.tsConfigPath || projectPath) : null;
|
||||
options.tsConfigPath !== null ? getTsConfig(options.tsConfigPath || projectPath) : null;
|
||||
|
||||
let {
|
||||
basePath,
|
||||
@ -200,3 +200,28 @@ export function getSharedSetup(options: NgccOptions): SharedSetup&RequiredNgccOp
|
||||
new InPlaceFileWriter(fileSystem, logger, errorOnFailedEntryPoint),
|
||||
};
|
||||
}
|
||||
|
||||
let tsConfigCache: ParsedConfiguration|null = null;
|
||||
let tsConfigPathCache: string|null = null;
|
||||
|
||||
/**
|
||||
* Get the parsed configuration object for the given `tsConfigPath`.
|
||||
*
|
||||
* This function will cache the previous parsed configuration object to avoid unnecessary processing
|
||||
* of the tsconfig.json in the case that it is requested repeatedly.
|
||||
*
|
||||
* This makes the assumption, which is true as of writing, that the contents of tsconfig.json and
|
||||
* its dependencies will not change during the life of the process running ngcc.
|
||||
*/
|
||||
function getTsConfig(tsConfigPath: string): ParsedConfiguration|null {
|
||||
if (tsConfigPath !== tsConfigPathCache) {
|
||||
tsConfigPathCache = tsConfigPath;
|
||||
tsConfigCache = readConfiguration(tsConfigPath);
|
||||
}
|
||||
return tsConfigCache;
|
||||
}
|
||||
|
||||
export function clearTsConfigCache() {
|
||||
tsConfigPathCache = null;
|
||||
tsConfigCache = null;
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import {Folder, MockFileSystem, runInEachFileSystem, TestFile} from '../../../sr
|
||||
import {loadStandardTestFiles, loadTestFiles} from '../../../test/helpers';
|
||||
import {getLockFilePath} from '../../src/locking/lock_file';
|
||||
import {mainNgcc} from '../../src/main';
|
||||
import {clearTsConfigCache} from '../../src/ngcc_options';
|
||||
import {hasBeenProcessed, markAsProcessed} from '../../src/packages/build_marker';
|
||||
import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point';
|
||||
import {EntryPointManifestFile} from '../../src/packages/entry_point_manifest';
|
||||
@ -41,6 +42,10 @@ runInEachFileSystem(() => {
|
||||
spyOn(os, 'cpus').and.returnValue([{model: 'Mock CPU'} as any]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearTsConfigCache();
|
||||
});
|
||||
|
||||
it('should run ngcc without errors for esm2015', () => {
|
||||
expect(() => mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']}))
|
||||
.not.toThrow();
|
||||
|
78
packages/compiler-cli/ngcc/test/ngcc_options_spec.ts
Normal file
78
packages/compiler-cli/ngcc/test/ngcc_options_spec.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
||||
|
||||
import {clearTsConfigCache, getSharedSetup, NgccOptions} from '../src/ngcc_options';
|
||||
|
||||
import {MockLogger} from './helpers/mock_logger';
|
||||
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
let fs: FileSystem;
|
||||
let _abs: typeof absoluteFrom;
|
||||
let projectPath: AbsoluteFsPath;
|
||||
|
||||
beforeEach(() => {
|
||||
fs = getFileSystem();
|
||||
_abs = absoluteFrom;
|
||||
projectPath = _abs('/project');
|
||||
});
|
||||
|
||||
describe('getSharedSetup()', () => {
|
||||
let pathToProjectTsConfig: AbsoluteFsPath;
|
||||
let pathToCustomTsConfig: AbsoluteFsPath;
|
||||
|
||||
beforeEach(() => {
|
||||
clearTsConfigCache();
|
||||
pathToProjectTsConfig = fs.resolve(projectPath, 'tsconfig.json');
|
||||
fs.ensureDir(fs.dirname(pathToProjectTsConfig));
|
||||
fs.writeFile(pathToProjectTsConfig, '{"files": ["src/index.ts"]}');
|
||||
pathToCustomTsConfig = _abs('/path/to/tsconfig.json');
|
||||
fs.ensureDir(fs.dirname(pathToCustomTsConfig));
|
||||
fs.writeFile(pathToCustomTsConfig, '{"files": ["custom/index.ts"]}');
|
||||
});
|
||||
|
||||
it('should load the tsconfig.json at the project root if tsConfigPath is `undefined`', () => {
|
||||
const setup = getSharedSetup({...createOptions()});
|
||||
expect(setup.tsConfigPath).toBeUndefined();
|
||||
expect(setup.tsConfig?.rootNames).toEqual([fs.resolve(projectPath, 'src/index.ts')]);
|
||||
});
|
||||
|
||||
it('should load a specific tsconfig.json if tsConfigPath is a string', () => {
|
||||
const setup = getSharedSetup({...createOptions(), tsConfigPath: pathToCustomTsConfig});
|
||||
expect(setup.tsConfigPath).toEqual(pathToCustomTsConfig);
|
||||
expect(setup.tsConfig?.rootNames).toEqual([_abs('/path/to/custom/index.ts')]);
|
||||
});
|
||||
|
||||
it('should not load a tsconfig.json if tsConfigPath is `null`', () => {
|
||||
const setup = getSharedSetup({...createOptions(), tsConfigPath: null});
|
||||
expect(setup.tsConfigPath).toBe(null);
|
||||
expect(setup.tsConfig).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This function creates an object that contains the minimal required properties for NgccOptions.
|
||||
*/
|
||||
function createOptions(): NgccOptions {
|
||||
return {
|
||||
async: false,
|
||||
basePath: fs.resolve(projectPath, 'node_modules'),
|
||||
propertiesToConsider: ['es2015'],
|
||||
compileAllFormats: false,
|
||||
createNewEntryPointFormats: false,
|
||||
logger: new MockLogger(),
|
||||
fileSystem: getFileSystem(),
|
||||
errorOnFailedEntryPoint: true,
|
||||
enableI18nLegacyMessageIdFormat: true,
|
||||
invalidateEntryPointManifest: false,
|
||||
};
|
||||
}
|
||||
});
|
@ -657,16 +657,31 @@ export function ɵɵgetFactoryOf<T>(type: Type<any>): FactoryFn<T>|null {
|
||||
*/
|
||||
export function ɵɵgetInheritedFactory<T>(type: Type<any>): (type: Type<T>) => T {
|
||||
return noSideEffects(() => {
|
||||
const proto = Object.getPrototypeOf(type.prototype).constructor as Type<any>;
|
||||
const factory = (proto as any)[NG_FACTORY_DEF] || ɵɵgetFactoryOf<T>(proto);
|
||||
if (factory !== null) {
|
||||
return factory;
|
||||
} else {
|
||||
// There is no factory defined. Either this was improper usage of inheritance
|
||||
// (no Angular decorator on the superclass) or there is no constructor at all
|
||||
// in the inheritance chain. Since the two cases cannot be distinguished, the
|
||||
// latter has to be assumed.
|
||||
return (t) => new t();
|
||||
const ownConstructor = type.prototype.constructor;
|
||||
const ownFactory = ownConstructor[NG_FACTORY_DEF] || ɵɵgetFactoryOf(ownConstructor);
|
||||
const objectPrototype = Object.prototype;
|
||||
let parent = Object.getPrototypeOf(type.prototype).constructor;
|
||||
|
||||
// Go up the prototype until we hit `Object`.
|
||||
while (parent && parent !== objectPrototype) {
|
||||
const factory = parent[NG_FACTORY_DEF] || ɵɵgetFactoryOf(parent);
|
||||
|
||||
// If we hit something that has a factory and the factory isn't the same as the type,
|
||||
// we've found the inherited factory. Note the check that the factory isn't the type's
|
||||
// own factory is redundant in most cases, but if the user has custom decorators on the
|
||||
// class, this lookup will start one level down in the prototype chain, causing us to
|
||||
// find the own factory first and potentially triggering an infinite loop downstream.
|
||||
if (factory && factory !== ownFactory) {
|
||||
return factory;
|
||||
}
|
||||
|
||||
parent = Object.getPrototypeOf(parent);
|
||||
}
|
||||
|
||||
// There is no factory defined. Either this was improper usage of inheritance
|
||||
// (no Angular decorator on the superclass) or there is no constructor at all
|
||||
// in the inheritance chain. Since the two cases cannot be distinguished, the
|
||||
// latter has to be assumed.
|
||||
return t => new t();
|
||||
});
|
||||
}
|
||||
|
@ -80,5 +80,7 @@ To profile, append `_profile` to the target name and attach a debugger via chrom
|
||||
To interactively edit/rerun benchmarks use `ibazel` instead of `bazel`.
|
||||
|
||||
To debug
|
||||
- Follow the directions in `profile_in_browser.html`
|
||||
OR
|
||||
- `yarn bazel build --config=ivy //packages/core/test/render3/perf:noop_change_detection`
|
||||
- `node --inspect-brk bazel-out/darwin-fastbuild/bin/packages/core/test/render3/perf/noop_change_detection.min_debug.es2015.js`
|
@ -8,7 +8,11 @@
|
||||
<ol>
|
||||
<li>Build the benchmark using <tt>yarn bazel build //packages/core/test/render3/perf:${BENCHMARK}.min_debug.es2015.js --config=ivy</tt></li>
|
||||
<li>Open this file using the <tt>file://</tt> protocol and add <tt>?benchmark=BENCHMARK</tt> to the URL.</li>
|
||||
<li>Open debug console for details</li>
|
||||
<li>
|
||||
Note: You should likely run this in an incognito browser with the "no-turbo-inlining" flag.<br />
|
||||
On Chrome, the command would be <code>/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome -incognito --js-flags="--no-turbo-inlining"</code>
|
||||
</li>
|
||||
<li>Open debug console for details. Benchmark profiles are available in the "JavaScript Profiler" tab of Chrome DevTools.</li>
|
||||
</ol>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -6,10 +6,10 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Component as _Component, ComponentFactoryResolver, ElementRef, Injectable as _Injectable, InjectFlags, InjectionToken, InjectorType, Provider, RendererFactory2, ViewContainerRef, ɵNgModuleDef as NgModuleDef, ɵɵdefineInjectable, ɵɵdefineInjector, ɵɵinject} from '../../src/core';
|
||||
import {Component as _Component, ComponentFactoryResolver, ElementRef, Injectable as _Injectable, InjectFlags, InjectionToken, InjectorType, Provider, RendererFactory2, Type, ViewContainerRef, ɵNgModuleDef as NgModuleDef, ɵɵdefineInjectable, ɵɵdefineInjector, ɵɵinject} from '../../src/core';
|
||||
import {forwardRef} from '../../src/di/forward_ref';
|
||||
import {createInjector} from '../../src/di/r3_injector';
|
||||
import {injectComponentFactoryResolver, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵProvidersFeature, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index';
|
||||
import {injectComponentFactoryResolver, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵgetInheritedFactory, ɵɵProvidersFeature, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index';
|
||||
import {RenderFlags} from '../../src/render3/interfaces/definition';
|
||||
import {NgModuleFactory} from '../../src/render3/ng_module_ref';
|
||||
import {getInjector} from '../../src/render3/util/discovery_utils';
|
||||
@ -1282,7 +1282,126 @@ describe('providers', () => {
|
||||
expect(injector.get(Some).location).toEqual('From app component');
|
||||
});
|
||||
});
|
||||
|
||||
// Note: these tests check the behavior of `getInheritedFactory` specifically.
|
||||
// Since `getInheritedFactory` is only generated in AOT, the tests can't be
|
||||
// ported directly to TestBed while running in JIT mode.
|
||||
describe('getInheritedFactory on class with custom decorator', () => {
|
||||
function addFoo() {
|
||||
return (constructor: Type<any>): any => {
|
||||
const decoratedClass = class Extender extends constructor { foo = 'bar'; };
|
||||
|
||||
// On IE10 child classes don't inherit static properties by default. If we detect
|
||||
// such a case, try to account for it so the tests are consistent between browsers.
|
||||
if (Object.getPrototypeOf(decoratedClass) !== constructor) {
|
||||
decoratedClass.prototype = constructor.prototype;
|
||||
}
|
||||
|
||||
return decoratedClass;
|
||||
};
|
||||
}
|
||||
|
||||
it('should find the correct factories if a parent class has a custom decorator', () => {
|
||||
class GrandParent {
|
||||
static ɵfac = function GrandParent_Factory() {};
|
||||
}
|
||||
|
||||
@addFoo()
|
||||
class Parent extends GrandParent {
|
||||
static ɵfac = function Parent_Factory() {};
|
||||
}
|
||||
|
||||
class Child extends Parent {
|
||||
static ɵfac = function Child_Factory() {};
|
||||
}
|
||||
|
||||
expect(ɵɵgetInheritedFactory(Child).name).toBe('Parent_Factory');
|
||||
expect(ɵɵgetInheritedFactory(Parent).name).toBe('GrandParent_Factory');
|
||||
expect(ɵɵgetInheritedFactory(GrandParent).name).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should find the correct factories if a child class has a custom decorator', () => {
|
||||
class GrandParent {
|
||||
static ɵfac = function GrandParent_Factory() {};
|
||||
}
|
||||
|
||||
class Parent extends GrandParent {
|
||||
static ɵfac = function Parent_Factory() {};
|
||||
}
|
||||
|
||||
@addFoo()
|
||||
class Child extends Parent {
|
||||
static ɵfac = function Child_Factory() {};
|
||||
}
|
||||
|
||||
expect(ɵɵgetInheritedFactory(Child).name).toBe('Parent_Factory');
|
||||
expect(ɵɵgetInheritedFactory(Parent).name).toBe('GrandParent_Factory');
|
||||
expect(ɵɵgetInheritedFactory(GrandParent).name).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should find the correct factories if a grandparent class has a custom decorator', () => {
|
||||
@addFoo()
|
||||
class GrandParent {
|
||||
static ɵfac = function GrandParent_Factory() {};
|
||||
}
|
||||
|
||||
class Parent extends GrandParent {
|
||||
static ɵfac = function Parent_Factory() {};
|
||||
}
|
||||
|
||||
class Child extends Parent {
|
||||
static ɵfac = function Child_Factory() {};
|
||||
}
|
||||
|
||||
expect(ɵɵgetInheritedFactory(Child).name).toBe('Parent_Factory');
|
||||
expect(ɵɵgetInheritedFactory(Parent).name).toBe('GrandParent_Factory');
|
||||
expect(ɵɵgetInheritedFactory(GrandParent).name).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should find the correct factories if all classes have a custom decorator', () => {
|
||||
@addFoo()
|
||||
class GrandParent {
|
||||
static ɵfac = function GrandParent_Factory() {};
|
||||
}
|
||||
|
||||
@addFoo()
|
||||
class Parent extends GrandParent {
|
||||
static ɵfac = function Parent_Factory() {};
|
||||
}
|
||||
|
||||
@addFoo()
|
||||
class Child extends Parent {
|
||||
static ɵfac = function Child_Factory() {};
|
||||
}
|
||||
|
||||
expect(ɵɵgetInheritedFactory(Child).name).toBe('Parent_Factory');
|
||||
expect(ɵɵgetInheritedFactory(Parent).name).toBe('GrandParent_Factory');
|
||||
expect(ɵɵgetInheritedFactory(GrandParent).name).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should find the correct factories if parent and grandparent classes have a custom decorator',
|
||||
() => {
|
||||
@addFoo()
|
||||
class GrandParent {
|
||||
static ɵfac = function GrandParent_Factory() {};
|
||||
}
|
||||
|
||||
@addFoo()
|
||||
class Parent extends GrandParent {
|
||||
static ɵfac = function Parent_Factory() {};
|
||||
}
|
||||
|
||||
class Child extends Parent {
|
||||
static ɵfac = function Child_Factory() {};
|
||||
}
|
||||
|
||||
expect(ɵɵgetInheritedFactory(Child).name).toBe('Parent_Factory');
|
||||
expect(ɵɵgetInheritedFactory(Parent).name).toBe('GrandParent_Factory');
|
||||
expect(ɵɵgetInheritedFactory(GrandParent).name).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
interface ComponentTest {
|
||||
providers?: Provider[];
|
||||
viewProviders?: Provider[];
|
||||
|
@ -48,8 +48,7 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
events!: Observable<NgElementStrategyEvent>;
|
||||
|
||||
/** Reference to the component that was created on connect. */
|
||||
// TODO(issue/24571): remove '!'.
|
||||
private componentRef!: ComponentRef<any>|null;
|
||||
private componentRef: ComponentRef<any>|null = null;
|
||||
|
||||
/** Changes that have been made to the component ref since the last time onChanges was called. */
|
||||
private inputChanges: SimpleChanges|null = null;
|
||||
@ -86,7 +85,7 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.componentRef) {
|
||||
if (this.componentRef === null) {
|
||||
this.initializeComponent(element);
|
||||
}
|
||||
}
|
||||
@ -97,15 +96,15 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
*/
|
||||
disconnect() {
|
||||
// Return if there is no componentRef or the component is already scheduled for destruction
|
||||
if (!this.componentRef || this.scheduledDestroyFn !== null) {
|
||||
if (this.componentRef === null || this.scheduledDestroyFn !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule the component to be destroyed after a small timeout in case it is being
|
||||
// moved elsewhere in the DOM
|
||||
this.scheduledDestroyFn = scheduler.schedule(() => {
|
||||
if (this.componentRef) {
|
||||
this.componentRef!.destroy();
|
||||
if (this.componentRef !== null) {
|
||||
this.componentRef.destroy();
|
||||
this.componentRef = null;
|
||||
}
|
||||
}, DESTROY_DELAY);
|
||||
@ -116,11 +115,11 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
* retrieved from the cached initialization values.
|
||||
*/
|
||||
getInputValue(property: string): any {
|
||||
if (!this.componentRef) {
|
||||
if (this.componentRef === null) {
|
||||
return this.initialInputValues.get(property);
|
||||
}
|
||||
|
||||
return (this.componentRef.instance as any)[property];
|
||||
return this.componentRef.instance[property];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -128,7 +127,7 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
* cached and set when the component is created.
|
||||
*/
|
||||
setInputValue(property: string, value: any): void {
|
||||
if (!this.componentRef) {
|
||||
if (this.componentRef === null) {
|
||||
this.initialInputValues.set(property, value);
|
||||
return;
|
||||
}
|
||||
@ -142,7 +141,7 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
}
|
||||
|
||||
this.recordInputChange(property, value);
|
||||
(this.componentRef.instance as any)[property] = value;
|
||||
this.componentRef.instance[property] = value;
|
||||
this.scheduleDetectChanges();
|
||||
}
|
||||
|
||||
@ -156,11 +155,10 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
extractProjectableNodes(element, this.componentFactory.ngContentSelectors);
|
||||
this.componentRef = this.componentFactory.create(childInjector, projectableNodes, element);
|
||||
|
||||
this.implementsOnChanges =
|
||||
isFunction((this.componentRef.instance as any as OnChanges).ngOnChanges);
|
||||
this.implementsOnChanges = isFunction((this.componentRef.instance as OnChanges).ngOnChanges);
|
||||
|
||||
this.initializeInputs();
|
||||
this.initializeOutputs();
|
||||
this.initializeOutputs(this.componentRef);
|
||||
|
||||
this.detectChanges();
|
||||
|
||||
@ -188,17 +186,17 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
}
|
||||
|
||||
/** Sets up listeners for the component's outputs so that the events stream emits the events. */
|
||||
protected initializeOutputs(): void {
|
||||
protected initializeOutputs(componentRef: ComponentRef<any>): void {
|
||||
const eventEmitters = this.componentFactory.outputs.map(({propName, templateName}) => {
|
||||
const emitter = (this.componentRef!.instance as any)[propName] as EventEmitter<any>;
|
||||
return emitter.pipe(map((value: any) => ({name: templateName, value})));
|
||||
const emitter: EventEmitter<any> = componentRef.instance[propName];
|
||||
return emitter.pipe(map(value => ({name: templateName, value})));
|
||||
});
|
||||
|
||||
this.events = merge(...eventEmitters);
|
||||
}
|
||||
|
||||
/** Calls ngOnChanges with all the inputs that have changed since the last call. */
|
||||
protected callNgOnChanges(): void {
|
||||
protected callNgOnChanges(componentRef: ComponentRef<any>): void {
|
||||
if (!this.implementsOnChanges || this.inputChanges === null) {
|
||||
return;
|
||||
}
|
||||
@ -207,7 +205,7 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
// during ngOnChanges.
|
||||
const inputChanges = this.inputChanges;
|
||||
this.inputChanges = null;
|
||||
(this.componentRef!.instance as any as OnChanges).ngOnChanges(inputChanges);
|
||||
(componentRef.instance as OnChanges).ngOnChanges(inputChanges);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -230,7 +228,8 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
*/
|
||||
protected recordInputChange(property: string, currentValue: any): void {
|
||||
// Do not record the change if the component does not implement `OnChanges`.
|
||||
if (this.componentRef && !this.implementsOnChanges) {
|
||||
// (We can only determine that after the component has been instantiated.)
|
||||
if (this.componentRef !== null && !this.implementsOnChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -255,11 +254,11 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
|
||||
/** Runs change detection on the component. */
|
||||
protected detectChanges(): void {
|
||||
if (!this.componentRef) {
|
||||
if (this.componentRef === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.callNgOnChanges();
|
||||
this.componentRef!.changeDetectorRef.detectChanges();
|
||||
this.callNgOnChanges(this.componentRef);
|
||||
this.componentRef.changeDetectorRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
@ -209,12 +209,6 @@ export function createCustomElement<P>(
|
||||
}
|
||||
}
|
||||
|
||||
// TypeScript 3.9+ defines getters/setters as configurable but non-enumerable properties (in
|
||||
// compliance with the spec). This breaks emulated inheritance in ES5 on environments that do not
|
||||
// natively support `Object.setPrototypeOf()` (such as IE 9-10).
|
||||
// Update the property descriptor of `NgElementImpl#ngElementStrategy` to make it enumerable.
|
||||
Object.defineProperty(NgElementImpl.prototype, 'ngElementStrategy', {enumerable: true});
|
||||
|
||||
// Add getters and setters to the prototype for each property input.
|
||||
defineInputGettersSetters(inputs, NgElementImpl.prototype);
|
||||
|
||||
|
@ -24,7 +24,7 @@ const semver = require('semver');
|
||||
|
||||
// Ignore commits that have specific patterns in commit message, it's ok for these commits to be
|
||||
// present only in one branch. Ignoring them reduced the "noise" in the final output.
|
||||
const ignorePatterns = [
|
||||
const ignoreCommitPatterns = [
|
||||
'release:',
|
||||
'docs: release notes',
|
||||
// These commits are created to update cli command docs sources with the most recent sha (stored
|
||||
@ -34,6 +34,13 @@ const ignorePatterns = [
|
||||
'build(docs-infra): upgrade cli command docs sources',
|
||||
];
|
||||
|
||||
// Ignore feature commits that have specific patterns in commit message, it's ok for these commits
|
||||
// to be present in patch branch.
|
||||
const ignoreFeatureCheckPatterns = [
|
||||
// It is ok and in fact desirable for dev-infra features to be on the patch branch.
|
||||
'feat(dev-infra):'
|
||||
];
|
||||
|
||||
// String to be displayed as a version for initial commits in a branch
|
||||
// (before first release from that branch).
|
||||
const initialVersion = 'initial';
|
||||
@ -59,6 +66,11 @@ function maybeExtractReleaseVersion(commit) {
|
||||
return matches ? matches[1] || matches[2] : null;
|
||||
}
|
||||
|
||||
// Checks whether commit message matches any patterns in ignore list.
|
||||
function shouldIgnoreCommit(commitMessage, ignorePatterns) {
|
||||
return ignorePatterns.some(pattern => commitMessage.indexOf(pattern) > -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param rawGitCommits
|
||||
* @returns {Map<string, [string, string]>} - Map of commit message to [commit info, version]
|
||||
@ -67,12 +79,12 @@ function collectCommitsAsMap(rawGitCommits) {
|
||||
const commits = toArray(rawGitCommits);
|
||||
const commitsMap = new Map();
|
||||
let version = initialVersion;
|
||||
commits.reverse().forEach((item) => {
|
||||
const skip = ignorePatterns.some(pattern => item.indexOf(pattern) > -1);
|
||||
commits.reverse().forEach((commit) => {
|
||||
const ignore = shouldIgnoreCommit(commit, ignoreCommitPatterns);
|
||||
// Keep track of the current version while going though the list of commits, so that we can use
|
||||
// this information in the output (i.e. display a version when a commit was introduced).
|
||||
version = maybeExtractReleaseVersion(item) || version;
|
||||
if (!skip) {
|
||||
version = maybeExtractReleaseVersion(commit) || version;
|
||||
if (!ignore) {
|
||||
// Extract original commit description from commit message, so that we can find matching
|
||||
// commit in other commit range. For example, for the following commit message:
|
||||
//
|
||||
@ -80,8 +92,8 @@ function collectCommitsAsMap(rawGitCommits) {
|
||||
//
|
||||
// we extract only "feat: update the locale files" part and use it as a key, since commit SHA
|
||||
// and PR number may be different for the same commit in master and patch branches.
|
||||
const key = item.slice(11).replace(/\(\#\d+\)/g, '').trim();
|
||||
commitsMap.set(key, [item, version]);
|
||||
const key = commit.slice(11).replace(/\(\#\d+\)/g, '').trim();
|
||||
commitsMap.set(key, [commit, version]);
|
||||
}
|
||||
});
|
||||
return commitsMap;
|
||||
@ -113,7 +125,7 @@ function diff(mapA, mapB) {
|
||||
*/
|
||||
function listFeatures(commitsMap) {
|
||||
return Array.from(commitsMap.keys()).reduce((result, key) => {
|
||||
if (key.startsWith('feat')) {
|
||||
if (key.startsWith('feat') && !shouldIgnoreCommit(key, ignoreFeatureCheckPatterns)) {
|
||||
const value = commitsMap.get(key);
|
||||
result.push(getCommitInfoAsString(value[1], value[0]));
|
||||
}
|
||||
|
Reference in New Issue
Block a user