Compare commits

...

10 Commits

Author SHA1 Message Date
a3f3082bf0 release: cut the v10.0.11 release 2020-08-19 09:05:11 -07:00
0570c240e4 docs: Fix typo in the inputs and outputs guide (#38524)
PR Close #38524
2020-08-19 08:27:48 -07:00
398118f708 feat(dev-infra): create a wizard for building commit messages (#38457)
Creates a wizard to walk through creating a commit message in the correct
template for commit messages in Angular repositories.

PR Close #38457
2020-08-18 17:01:18 -07:00
dcc3f6d74d feat(dev-infra): tooling to check out pending PR (#38474)
Creates a tool within ng-dev to checkout a pending PR from the upstream repository.  This automates
an action that many developers on the Angular team need to do periodically in the process of testing
and reviewing incoming PRs.

Example usage:
  ng-dev pr checkout <pr-number>

PR Close #38474
2020-08-18 16:22:52 -07:00
0af95332e9 fix(router): ensure routerLinkActive updates when associated routerLinks change (#38511)
This commit introduces a new subscription in the `routerLinkActive` directive which triggers an update
when any of its associated routerLinks have changes. `RouterLinkActive` not only needs to know when
links are added or removed, but it also needs to know about if a link it already knows about
changes in some way.

Quick note that `from...mergeAll` is used instead of just a simple
`merge` (or `scheduled...mergeAll`) to avoid introducing new rxjs
operators in order to keep bundle size down.

Fixes #18469

PR Close #38511
2020-08-18 10:21:55 -07:00
f9a76a7d06 Revert "fix(router): ensure routerLinkActive updates when associated routerLinks change (#38349)" (#38511)
This reverts commit e0e5c9f195.
Failures in Google tests were detected.

PR Close #38511
2020-08-18 10:21:52 -07:00
832a54ee42 docs(common): Wrong parameter description on TrackBy (#38495)
Track By Function receive the T[index] data, not the node id.
TrackByFunction reference description has the same issue.
PR Close #38495
2020-08-18 10:08:47 -07:00
e8f4294e7f refactor(localize): update yargs and typings for yargs (#38502)
Updating yargs and typings for the updated yargs module.

PR Close #38502
2020-08-18 10:06:32 -07:00
ce448f4341 refactor(ngcc): update yargs and typings for yargs (#38502)
Updating yargs and typings for the updated yargs module.

PR Close #38502
2020-08-18 10:06:29 -07:00
847eaa0fa3 refactor(dev-infra): update yargs and typings for yargs (#38502)
Updating yargs and typings for the updated yargs module.

PR Close #38502
2020-08-18 10:06:26 -07:00
35 changed files with 794 additions and 175 deletions

View File

@ -1,3 +1,13 @@
<a name="10.0.11"></a>
## 10.0.11 (2020-08-19)
### Bug Fixes
* **router:** ensure routerLinkActive updates when associated routerLinks change (resubmit of [#38349](https://github.com/angular/angular/issues/38349)) ([#38511](https://github.com/angular/angular/issues/38511)) ([0af9533](https://github.com/angular/angular/commit/0af9533)), closes [#18469](https://github.com/angular/angular/issues/18469)
<a name="10.0.10"></a>
## 10.0.10 (2020-08-17)

View File

@ -208,7 +208,7 @@ about the event and gives that data to the parent.
The child's template has two controls. The first is an HTML `<input>` with a
[template reference variable](guide/template-reference-variables) , `#newItem`,
where the user types in an item name. Whatever the user types
into the `<input>` gets stored in the `#newItem` variable.
into the `<input>` gets stored in the `value` property of the `#newItem` variable.
<code-example path="inputs-outputs/src/app/item-output/item-output.component.html" region="child-output" header="src/app/item-output/item-output.component.html"></code-example>
@ -218,7 +218,7 @@ an event binding because the part to the left of the equal
sign is in parentheses, `(click)`.
The `(click)` event is bound to the `addNewItem()` method in the child component class which
takes as its argument whatever the value of `#newItem` is.
takes as its argument whatever the value of `#newItem.value` property is.
Now the child component has an `@Output()`
for sending data to the parent and a method for raising an event.

View File

@ -4,6 +4,7 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "commit-message",
srcs = [
"builder.ts",
"cli.ts",
"commit-message-draft.ts",
"config.ts",
@ -12,14 +13,17 @@ ts_library(
"validate.ts",
"validate-file.ts",
"validate-range.ts",
"wizard.ts",
],
module_name = "@angular/dev-infra-private/commit-message",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/utils",
"@npm//@types/inquirer",
"@npm//@types/node",
"@npm//@types/shelljs",
"@npm//@types/yargs",
"@npm//inquirer",
"@npm//shelljs",
"@npm//yargs",
],
@ -29,6 +33,7 @@ ts_library(
name = "test_lib",
testonly = True,
srcs = [
"builder.spec.ts",
"parse.spec.ts",
"validate.spec.ts",
],

View File

@ -0,0 +1,46 @@
/**
* @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 config from '../utils/config';
import * as console from '../utils/console';
import {buildCommitMessage} from './builder';
describe('commit message building:', () => {
beforeEach(() => {
// stub logging calls to prevent noise in test log
spyOn(console, 'info').and.stub();
// provide a configuration for DevInfra when loaded
spyOn(config, 'getConfig').and.returnValue({
commitMessage: {
scopes: ['core'],
}
} as any);
});
it('creates a commit message with a scope', async () => {
buildPromptResponseSpies('fix', 'core', 'This is a summary');
expect(await buildCommitMessage()).toMatch(/^fix\(core\): This is a summary/);
});
it('creates a commit message without a scope', async () => {
buildPromptResponseSpies('build', false, 'This is a summary');
expect(await buildCommitMessage()).toMatch(/^build: This is a summary/);
});
});
/** Create spies to return the mocked selections from prompts. */
function buildPromptResponseSpies(type: string, scope: string|false, summary: string) {
spyOn(console, 'promptAutocomplete')
.and.returnValues(Promise.resolve(type), Promise.resolve(scope));
spyOn(console, 'promptInput').and.returnValue(Promise.resolve(summary));
}

View File

@ -0,0 +1,70 @@
/**
* @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 {ListChoiceOptions} from 'inquirer';
import {info, promptAutocomplete, promptInput} from '../utils/console';
import {COMMIT_TYPES, CommitType, getCommitMessageConfig, ScopeRequirement} from './config';
/** Validate commit message at the provided file path. */
export async function buildCommitMessage() {
// TODO(josephperrott): Add support for skipping wizard with local untracked config file
// TODO(josephperrott): Add default commit message information/commenting into generated messages
info('Just a few questions to start building the commit message!');
/** The commit message type. */
const type = await promptForCommitMessageType();
/** The commit message scope. */
const scope = await promptForCommitMessageScopeForType(type);
/** The commit message summary. */
const summary = await promptForCommitMessageSummary();
return `${type.name}${scope ? '(' + scope + ')' : ''}: ${summary}\n\n`;
}
/** Prompts in the terminal for the commit message's type. */
async function promptForCommitMessageType(): Promise<CommitType> {
info('The type of change in the commit. Allows a reader to know the effect of the change,');
info('whether it brings a new feature, adds additional testing, documents the `project, etc.');
/** List of commit type options for the autocomplete prompt. */
const typeOptions: ListChoiceOptions[] =
Object.values(COMMIT_TYPES).map(({description, name}) => {
return {
name: `${name} - ${description}`,
value: name,
short: name,
};
});
/** The key of a commit message type, selected by the user via prompt. */
const typeName = await promptAutocomplete('Select a type for the commit:', typeOptions);
return COMMIT_TYPES[typeName];
}
/** Prompts in the terminal for the commit message's scope. */
async function promptForCommitMessageScopeForType(type: CommitType): Promise<string|false> {
// If the commit type's scope requirement is forbidden, return early.
if (type.scope === ScopeRequirement.Forbidden) {
info(`Skipping scope selection as the '${type.name}' type does not allow scopes`);
return false;
}
/** Commit message configuration */
const config = getCommitMessageConfig();
info('The area of the repository the changes in this commit most affects.');
return await promptAutocomplete(
'Select a scope for the commit:', config.commitMessage.scopes,
type.scope === ScopeRequirement.Optional ? '<no scope>' : '');
}
/** Prompts in the terminal for the commit message's summary. */
async function promptForCommitMessageSummary(): Promise<string> {
info('Provide a short summary of what the changes in the commit do');
return await promptInput('Provide a short summary of the commit');
}

View File

@ -12,20 +12,23 @@ import {info} from '../utils/console';
import {restoreCommitMessage} from './restore-commit-message';
import {validateFile} from './validate-file';
import {validateCommitRange} from './validate-range';
import {runWizard} from './wizard';
/** Build the parser for the commit-message commands. */
export function buildCommitMessageParser(localYargs: yargs.Argv) {
return localYargs.help()
.strict()
.command(
'restore-commit-message-draft', false, {
'file-env-variable': {
'restore-commit-message-draft', false,
args => {
return args.option('file-env-variable', {
type: 'string',
array: true,
conflicts: ['file'],
required: true,
description:
'The key for the environment variable which holds the arguments for the ' +
'prepare-commit-msg hook as described here: ' +
'The key for the environment variable which holds the arguments for the\n' +
'prepare-commit-msg hook as described here:\n' +
'https://git-scm.com/docs/githooks#_prepare_commit_msg',
coerce: arg => {
const [file, source] = (process.env[arg] || '').split(' ');
@ -34,10 +37,27 @@ export function buildCommitMessageParser(localYargs: yargs.Argv) {
}
return [file, source];
},
}
});
},
args => {
restoreCommitMessage(args.fileEnvVariable[0], args.fileEnvVariable[1]);
restoreCommitMessage(args['file-env-variable'][0], args['file-env-variable'][1] as any);
})
.command(
'wizard <filePath> [source] [commitSha]', '', ((args: any) => {
return args
.positional(
'filePath',
{description: 'The file path to write the generated commit message into'})
.positional('source', {
choices: ['message', 'template', 'merge', 'squash', 'commit'],
description: 'The source of the commit message as described here: ' +
'https://git-scm.com/docs/githooks#_prepare_commit_msg'
})
.positional(
'commitSha', {description: 'The commit sha if source is set to `commit`'});
}),
async (args: any) => {
await runWizard(args);
})
.command(
'pre-commit-validate', 'Validate the most recent commit message', {
@ -61,7 +81,7 @@ export function buildCommitMessageParser(localYargs: yargs.Argv) {
}
},
args => {
const file = args.file || args.fileEnvVariable || '.git/COMMIT_EDITMSG';
const file = args.file || args['file-env-variable'] || '.git/COMMIT_EDITMSG';
validateFile(file);
})
.command(

View File

@ -39,36 +39,56 @@ export enum ScopeRequirement {
/** A commit type */
export interface CommitType {
description: string;
name: string;
scope: ScopeRequirement;
}
/** The valid commit types for Angular commit messages. */
export const COMMIT_TYPES: {[key: string]: CommitType} = {
build: {
name: 'build',
description: 'Changes to local repository build system and tooling',
scope: ScopeRequirement.Forbidden,
},
ci: {
name: 'ci',
description: 'Changes to CI configuration and CI specific tooling',
scope: ScopeRequirement.Forbidden,
},
docs: {
name: 'docs',
description: 'Changes which exclusively affects documentation.',
scope: ScopeRequirement.Optional,
},
feat: {
name: 'feat',
description: 'Creates a new feature',
scope: ScopeRequirement.Required,
},
fix: {
name: 'fix',
description: 'Fixes a previously discovered failure/bug',
scope: ScopeRequirement.Required,
},
perf: {
name: 'perf',
description: 'Improves performance without any change in functionality or API',
scope: ScopeRequirement.Required,
},
refactor: {
name: 'refactor',
description: 'Refactor without any change in functionality or API (includes style changes)',
scope: ScopeRequirement.Required,
},
release: {
name: 'release',
description: 'A release point in the repository',
scope: ScopeRequirement.Forbidden,
},
test: {
name: 'test',
description: 'Improvements or corrections made to the project\'s test suite',
scope: ScopeRequirement.Required,
},
};

View File

@ -0,0 +1,43 @@
/**
* @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 {writeFileSync} from 'fs';
import {info} from '../utils/console';
import {buildCommitMessage} from './builder';
/**
* The source triggering the git commit message creation.
* As described in: https://git-scm.com/docs/githooks#_prepare_commit_msg
*/
export type PrepareCommitMsgHookSource = 'message'|'template'|'merge'|'squash'|'commit';
/** The default commit message used if the wizard does not procude a commit message. */
const defaultCommitMessage = `<type>(<scope>): <summary>
# <Describe the motivation behind this change - explain WHY you are making this change. Wrap all
# lines at 100 characters.>\n\n`;
export async function runWizard(
args: {filePath: string, source?: PrepareCommitMsgHookSource, commitSha?: string}) {
// TODO(josephperrott): Add support for skipping wizard with local untracked config file
if (args.source !== undefined) {
info(`Skipping commit message wizard due because the commit was created via '${
args.source}' source`);
process.exitCode = 0;
return;
}
// Set the default commit message to be updated if the user cancels out of the wizard in progress
writeFileSync(args.filePath, defaultCommitMessage);
/** The generated commit message. */
const commitMessage = await buildCommitMessage();
writeFileSync(args.filePath, commitMessage);
}

View File

@ -22,28 +22,31 @@ export function buildFormatParser(localYargs: yargs.Argv) {
description: 'Run the formatter to check formatting rather than updating code format'
})
.command(
'all', 'Run the formatter on all files in the repository', {},
'all', 'Run the formatter on all files in the repository', args => args,
({check}) => {
const executionCmd = check ? checkFiles : formatFiles;
executionCmd(allFiles());
})
.command(
'changed [shaOrRef]', 'Run the formatter on files changed since the provided sha/ref', {},
'changed [shaOrRef]', 'Run the formatter on files changed since the provided sha/ref',
args => args.positional('shaOrRef', {type: 'string'}),
({shaOrRef, check}) => {
const sha = shaOrRef || 'master';
const executionCmd = check ? checkFiles : formatFiles;
executionCmd(allChangedFilesSince(sha));
})
.command(
'staged', 'Run the formatter on all staged files', {},
'staged', 'Run the formatter on all staged files', args => args,
({check}) => {
const executionCmd = check ? checkFiles : formatFiles;
executionCmd(allStagedFiles());
})
.command('files <files..>', 'Run the formatter on provided files', {}, ({check, files}) => {
const executionCmd = check ? checkFiles : formatFiles;
executionCmd(files);
});
.command(
'files <files..>', 'Run the formatter on provided files',
args => args.positional('files', {array: true, type: 'string'}), ({check, files}) => {
const executionCmd = check ? checkFiles : formatFiles;
executionCmd(files!);
});
}
if (require.main === module) {

View File

@ -6,6 +6,7 @@ ts_library(
module_name = "@angular/dev-infra-private/pr",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/pr/checkout",
"//dev-infra/pr/discover-new-conflicts",
"//dev-infra/pr/merge",
"//dev-infra/pr/rebase",

View File

@ -0,0 +1,13 @@
load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "checkout",
srcs = glob(["*.ts"]),
module_name = "@angular/dev-infra-private/pr/checkout",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/pr/common",
"//dev-infra/utils",
"@npm//@types/yargs",
],
)

View File

@ -0,0 +1,50 @@
/**
* @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 {Arguments, Argv, CommandModule} from 'yargs';
import {error} from '../../utils/console';
import {checkOutPullRequestLocally} from '../common/checkout-pr';
export interface CheckoutOptions {
prNumber: number;
'github-token'?: string;
}
/** URL to the Github page where personal access tokens can be generated. */
export const GITHUB_TOKEN_GENERATE_URL = `https://github.com/settings/tokens`;
/** Builds the checkout pull request command. */
function builder(yargs: Argv) {
return yargs.positional('prNumber', {type: 'number', demandOption: true}).option('github-token', {
type: 'string',
description: 'Github token. If not set, token is retrieved from the environment variables.'
});
}
/** Handles the checkout pull request command. */
async function handler({prNumber, 'github-token': token}: Arguments<CheckoutOptions>) {
const githubToken = token || process.env.GITHUB_TOKEN || process.env.TOKEN;
if (!githubToken) {
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.exitCode = 1;
return;
}
const prCheckoutOptions = {allowIfMaintainerCannotModify: true, branchName: `pr-${prNumber}`};
await checkOutPullRequestLocally(prNumber, githubToken, prCheckoutOptions);
}
/** yargs command module for checking out a PR */
export const CheckoutCommandModule: CommandModule<{}, CheckoutOptions> = {
handler,
builder,
command: 'checkout <pr-number>',
describe: 'Checkout a PR from the upstream repo',
};

View File

@ -8,6 +8,7 @@
import * as yargs from 'yargs';
import {CheckoutCommandModule} from './checkout/cli';
import {buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand} from './discover-new-conflicts/cli';
import {buildMergeCommand, handleMergeCommand} from './merge/cli';
import {buildRebaseCommand, handleRebaseCommand} from './rebase/cli';
@ -24,7 +25,8 @@ export function buildPrParser(localYargs: yargs.Argv) {
buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand)
.command(
'rebase <pr-number>', 'Rebase a pending PR and push the rebased commits back to Github',
buildRebaseCommand, handleRebaseCommand);
buildRebaseCommand, handleRebaseCommand)
.command(CheckoutCommandModule);
}
if (require.main === module) {

View File

@ -0,0 +1,12 @@
load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "common",
srcs = glob(["*.ts"]),
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/utils",
"@npm//@types/node",
"@npm//typed-graphqlify",
],
)

View File

@ -0,0 +1,135 @@
/**
* @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 {types as graphQLTypes} from 'typed-graphqlify';
import {URL} from 'url';
import {info} from '../../utils/console';
import {GitClient} from '../../utils/git';
import {getPr} from '../../utils/github';
/* GraphQL schema for the response body for a pending PR. */
const PR_SCHEMA = {
state: graphQLTypes.string,
maintainerCanModify: graphQLTypes.boolean,
viewerDidAuthor: graphQLTypes.boolean,
headRefOid: graphQLTypes.string,
headRef: {
name: graphQLTypes.string,
repository: {
url: graphQLTypes.string,
nameWithOwner: graphQLTypes.string,
},
},
baseRef: {
name: graphQLTypes.string,
repository: {
url: graphQLTypes.string,
nameWithOwner: graphQLTypes.string,
},
},
};
export class UnexpectedLocalChangesError extends Error {
constructor(m: string) {
super(m);
Object.setPrototypeOf(this, UnexpectedLocalChangesError.prototype);
}
}
export class MaintainerModifyAccessError extends Error {
constructor(m: string) {
super(m);
Object.setPrototypeOf(this, MaintainerModifyAccessError.prototype);
}
}
/** Options for checking out a PR */
export interface PullRequestCheckoutOptions {
/** Whether the PR should be checked out if the maintainer cannot modify. */
allowIfMaintainerCannotModify?: boolean;
}
/**
* Rebase the provided PR onto its merge target branch, and push up the resulting
* commit to the PRs repository.
*/
export async function checkOutPullRequestLocally(
prNumber: number, githubToken: string, opts: PullRequestCheckoutOptions = {}) {
/** Authenticated Git client for git and Github interactions. */
const git = new GitClient(githubToken);
// In order to preserve local changes, checkouts cannot occur if local changes are present in the
// git environment. Checked before retrieving the PR to fail fast.
if (git.hasLocalChanges()) {
throw new UnexpectedLocalChangesError('Unable to checkout PR due to uncommitted changes.');
}
/**
* The branch or revision originally checked out before this method performed
* any Git operations that may change the working branch.
*/
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
/* The PR information from Github. */
const pr = await getPr(PR_SCHEMA, prNumber, git);
/** The branch name of the PR from the repository the PR came from. */
const headRefName = pr.headRef.name;
/** The full ref for the repository and branch the PR came from. */
const fullHeadRef = `${pr.headRef.repository.nameWithOwner}:${headRefName}`;
/** The full URL path of the repository the PR came from with github token as authentication. */
const headRefUrl = addAuthenticationToUrl(pr.headRef.repository.url, githubToken);
// Note: Since we use a detached head for rebasing the PR and therefore do not have
// remote-tracking branches configured, we need to set our expected ref and SHA. This
// allows us to use `--force-with-lease` for the detached head while ensuring that we
// never accidentally override upstream changes that have been pushed in the meanwhile.
// See:
// https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegtltexpectgt
/** Flag for a force push with leage back to upstream. */
const forceWithLeaseFlag = `--force-with-lease=${headRefName}:${pr.headRefOid}`;
// If the PR does not allow maintainers to modify it, exit as the rebased PR cannot
// be pushed up.
if (!pr.maintainerCanModify && !pr.viewerDidAuthor && !opts.allowIfMaintainerCannotModify) {
throw new MaintainerModifyAccessError('PR is not set to allow maintainers to modify the PR');
}
try {
// Fetch the branch at the commit of the PR, and check it out in a detached state.
info(`Checking out PR #${prNumber} from ${fullHeadRef}`);
git.run(['fetch', headRefUrl, headRefName]);
git.run(['checkout', '--detach', 'FETCH_HEAD']);
} catch (e) {
git.checkout(previousBranchOrRevision, true);
throw e;
}
return {
/**
* Pushes the current local branch to the PR on the upstream repository.
*
* @returns true If the command did not fail causing a GitCommandError to be thrown.
* @throws GitCommandError Thrown when the push back to upstream fails.
*/
pushToUpstream: (): true => {
git.run(['push', headRefUrl, `HEAD:${headRefName}`, forceWithLeaseFlag]);
return true;
},
/** Restores the state of the local repository to before the PR checkout occured. */
resetGitState: (): boolean => {
return git.checkout(previousBranchOrRevision, true);
}
};
}
/** Adds the provided token as username to the provided url. */
function addAuthenticationToUrl(urlString: string, token: string) {
const url = new URL(urlString);
url.username = token;
return url.toString();
}

View File

@ -12,18 +12,28 @@ import {error} from '../../utils/console';
import {discoverNewConflictsForPr} from './index';
/** The options available to the discover-new-conflicts command via CLI. */
export interface DiscoverNewConflictsCommandOptions {
date: number;
'pr-number': number;
}
/** Builds the discover-new-conflicts pull request command. */
export function buildDiscoverNewConflictsCommand(yargs: Argv) {
return yargs.option('date', {
description: 'Only consider PRs updated since provided date',
defaultDescription: '30 days ago',
coerce: Date.parse,
default: getThirtyDaysAgoDate,
});
export function buildDiscoverNewConflictsCommand(yargs: Argv):
Argv<DiscoverNewConflictsCommandOptions> {
return yargs
.option('date', {
description: 'Only consider PRs updated since provided date',
defaultDescription: '30 days ago',
coerce: (date) => typeof date === 'number' ? date : Date.parse(date),
default: getThirtyDaysAgoDate(),
})
.positional('pr-number', {demandOption: true, type: 'number'});
}
/** Handles the discover-new-conflicts pull request command. */
export async function handleDiscoverNewConflictsCommand({prNumber, date}: Arguments) {
export async function handleDiscoverNewConflictsCommand(
{'pr-number': prNumber, date}: Arguments<DiscoverNewConflictsCommandOptions>) {
// If a provided date is not able to be parsed, yargs provides it as NaN.
if (isNaN(date)) {
error('Unable to parse the value provided via --date flag');
@ -33,11 +43,11 @@ export async function handleDiscoverNewConflictsCommand({prNumber, date}: Argume
}
/** Gets a date object 30 days ago from today. */
function getThirtyDaysAgoDate(): Date {
function getThirtyDaysAgoDate() {
const date = new Date();
// Set the hours, minutes and seconds to 0 to only consider date.
date.setHours(0, 0, 0, 0);
// Set the date to 30 days in the past.
date.setDate(date.getDate() - 30);
return date;
return date.getTime();
}

View File

@ -72,7 +72,7 @@ export async function discoverNewConflictsForPr(
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);
const allPendingPRs = (await getPendingPrs(PR_SCHEMA, git)).map(processPr);
/** The PR which is being checked against. */
const requestedPr = allPendingPRs.find(pr => pr.number === newPrNumber);
if (requestedPr === undefined) {

View File

@ -12,17 +12,26 @@ import {error, red, yellow} from '../../utils/console';
import {GITHUB_TOKEN_GENERATE_URL, mergePullRequest} from './index';
/** The options available to the merge command via CLI. */
export interface MergeCommandOptions {
'github-token'?: string;
'pr-number': number;
}
/** Builds the options for the merge command. */
export function buildMergeCommand(yargs: Argv) {
return yargs.help().strict().option('github-token', {
type: 'string',
description: 'Github token. If not set, token is retrieved from the environment variables.'
});
export function buildMergeCommand(yargs: Argv): Argv<MergeCommandOptions> {
return yargs.help()
.strict()
.positional('pr-number', {demandOption: true, type: 'number'})
.option('github-token', {
type: 'string',
description: 'Github token. If not set, token is retrieved from the environment variables.'
});
}
/** Handles the merge command. i.e. performs the merge of a specified pull request. */
export async function handleMergeCommand(args: Arguments) {
const githubToken = args.githubToken || process.env.GITHUB_TOKEN || process.env.TOKEN;
export async function handleMergeCommand(args: Arguments<MergeCommandOptions>) {
const githubToken = args['github-token'] || process.env.GITHUB_TOKEN || process.env.TOKEN;
if (!githubToken) {
error(red('No Github token set. Please set the `GITHUB_TOKEN` environment variable.'));
error(red('Alternatively, pass the `--github-token` command line flag.'));
@ -30,5 +39,5 @@ export async function handleMergeCommand(args: Arguments) {
process.exit(1);
}
await mergePullRequest(args.prNumber, githubToken);
await mergePullRequest(args['pr-number'], githubToken);
}

View File

@ -15,17 +15,26 @@ import {rebasePr} from './index';
/** URL to the Github page where personal access tokens can be generated. */
export const GITHUB_TOKEN_GENERATE_URL = `https://github.com/settings/tokens`;
/** Builds the rebase pull request command. */
export function buildRebaseCommand(yargs: Argv) {
return yargs.option('github-token', {
type: 'string',
description: 'Github token. If not set, token is retrieved from the environment variables.'
});
/** The options available to the rebase command via CLI. */
export interface RebaseCommandOptions {
'github-token'?: string;
prNumber: number;
}
/** Builds the rebase pull request command. */
export function buildRebaseCommand(yargs: Argv): Argv<RebaseCommandOptions> {
return yargs
.option('github-token', {
type: 'string',
description: 'Github token. If not set, token is retrieved from the environment variables.'
})
.positional('prNumber', {type: 'number', demandOption: true});
}
/** Handles the rebase pull request command. */
export async function handleRebaseCommand(args: Arguments) {
const githubToken = args.githubToken || process.env.GITHUB_TOKEN || process.env.TOKEN;
export async function handleRebaseCommand(args: Arguments<RebaseCommandOptions>) {
const githubToken = args['github-token'] || process.env.GITHUB_TOKEN || process.env.TOKEN;
if (!githubToken) {
error('No Github token set. Please set the `GITHUB_TOKEN` environment variable.');
error('Alternatively, pass the `--github-token` command line flag.');

View File

@ -55,7 +55,7 @@ export async function rebasePr(
*/
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
/* Get the PR information from Github. */
const pr = await getPr(PR_SCHEMA, prNumber, config.github);
const pr = await getPr(PR_SCHEMA, prNumber, git);
const headRefName = pr.headRef.name;
const baseRefName = pr.baseRef.name;

View File

@ -30,20 +30,19 @@ export function tsCircularDependenciesBuilder(localYargs: yargs.Argv) {
{type: 'string', demandOption: true, description: 'Path to the configuration file.'})
.option('warnings', {type: 'boolean', description: 'Prints all warnings.'})
.command(
'check', 'Checks if the circular dependencies have changed.', {},
(argv: yargs.Arguments) => {
'check', 'Checks if the circular dependencies have changed.', args => args,
argv => {
const {config: configArg, warnings} = argv;
const configPath = isAbsolute(configArg) ? configArg : resolve(configArg);
const config = loadTestConfig(configPath);
process.exit(main(false, config, warnings));
process.exit(main(false, config, !!warnings));
})
.command(
'approve', 'Approves the current circular dependencies.', {}, (argv: yargs.Arguments) => {
const {config: configArg, warnings} = argv;
const configPath = isAbsolute(configArg) ? configArg : resolve(configArg);
const config = loadTestConfig(configPath);
process.exit(main(true, config, warnings));
});
.command('approve', 'Approves the current circular dependencies.', args => args, argv => {
const {config: configArg, warnings} = argv;
const configPath = isAbsolute(configArg) ? configArg : resolve(configArg);
const config = loadTestConfig(configPath);
process.exit(main(true, config, !!warnings));
});
}
/**

View File

@ -17,6 +17,7 @@ ts_library(
"@npm//@types/shelljs",
"@npm//chalk",
"@npm//inquirer",
"@npm//inquirer-autocomplete-prompt",
"@npm//shelljs",
"@npm//tslib",
"@npm//typed-graphqlify",

View File

@ -7,7 +7,8 @@
*/
import chalk from 'chalk';
import {prompt} from 'inquirer';
import {createPromptModule, ListChoiceOptions, prompt} from 'inquirer';
import * as inquirerAutocomplete from 'inquirer-autocomplete-prompt';
/** Reexport of chalk colors for convenient access. */
@ -26,6 +27,52 @@ export async function promptConfirm(message: string, defaultValue = false): Prom
.result;
}
/** Prompts the user to select an option from a filterable autocomplete list. */
export async function promptAutocomplete(
message: string, choices: (string|ListChoiceOptions)[]): Promise<string>;
/**
* Prompts the user to select an option from a filterable autocomplete list, with an option to
* choose no value.
*/
export async function promptAutocomplete(
message: string, choices: (string|ListChoiceOptions)[],
noChoiceText?: string): Promise<string|false>;
export async function promptAutocomplete(
message: string, choices: (string|ListChoiceOptions)[],
noChoiceText?: string): Promise<string|false> {
// Creates a local prompt module with an autocomplete prompt type.
const prompt = createPromptModule({}).registerPrompt('autocomplete', inquirerAutocomplete);
if (noChoiceText) {
choices = [noChoiceText, ...choices];
}
// `prompt` must be cast as `any` as the autocomplete typings are not available.
const result = (await (prompt as any)({
type: 'autocomplete',
name: 'result',
message,
source: (_: any, input: string) => {
if (!input) {
return Promise.resolve(choices);
}
return Promise.resolve(choices.filter(choice => {
if (typeof choice === 'string') {
return choice.includes(input);
}
return choice.name!.includes(input);
}));
}
})).result;
if (result === noChoiceText) {
return false;
}
return result;
}
/** Prompts the user for one line of input. */
export async function promptInput(message: string): Promise<string> {
return (await prompt<{result: string}>({type: 'input', name: 'result', message})).result;
}
/**
* Supported levels for logging functions.
*

View File

@ -26,7 +26,7 @@ export class GithubApiRequestError extends Error {
**/
export class GithubClient extends Octokit {
/** The Github GraphQL (v4) API. */
graqhql: GithubGraphqlClient;
graphql: GithubGraphqlClient;
/** The current user based on checking against the Github API. */
private _currentUser: string|null = null;
@ -42,7 +42,7 @@ export class GithubClient extends Octokit {
});
// Create authenticated graphql client.
this.graqhql = new GithubGraphqlClient(token);
this.graphql = new GithubGraphqlClient(token);
}
/** Retrieve the login of the current user from Github. */
@ -51,7 +51,7 @@ export class GithubClient extends Octokit {
if (this._currentUser !== null) {
return this._currentUser;
}
const result = await this.graqhql.query({
const result = await this.graphql.query({
viewer: {
login: types.string,
}
@ -80,7 +80,7 @@ class GithubGraphqlClient {
// Set the default headers to include authorization with the provided token for all
// graphQL calls.
if (token) {
this.graqhql.defaults({headers: {authorization: `token ${token}`}});
this.graqhql = this.graqhql.defaults({headers: {authorization: `token ${token}`}});
}
}

View File

@ -147,6 +147,25 @@ export class GitClient {
return value.replace(this._githubTokenRegex, '<TOKEN>');
}
/**
* Checks out a requested branch or revision, optionally cleaning the state of the repository
* before attempting the checking. Returns a boolean indicating whether the branch or revision
* was cleanly checked out.
*/
checkout(branchOrRevision: string, cleanState: boolean): boolean {
if (cleanState) {
// Abort any outstanding ams.
this.runGraceful(['am', '--abort'], {stdio: 'ignore'});
// Abort any outstanding cherry-picks.
this.runGraceful(['cherry-pick', '--abort'], {stdio: 'ignore'});
// Abort any outstanding rebases.
this.runGraceful(['rebase', '--abort'], {stdio: 'ignore'});
// Clear any changes in the current repo.
this.runGraceful(['reset', '--hard'], {stdio: 'ignore'});
}
return this.runGraceful(['checkout', branchOrRevision], {stdio: 'ignore'}).status === 0;
}
/**
* Assert the GitClient instance is using a token with permissions for the all of the
* provided OAuth scopes.

View File

@ -6,29 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
import {graphql as unauthenticatedGraphql} from '@octokit/graphql';
import {params, types} from 'typed-graphqlify';
import {params, query as graphqlQuery, types} from 'typed-graphqlify';
import {NgDevConfig} from './config';
/** The configuration required for github interactions. */
type GithubConfig = NgDevConfig['github'];
/**
* Authenticated instance of Github GraphQl API service, relies on a
* personal access token being available in the TOKEN environment variable.
*/
const graphql = unauthenticatedGraphql.defaults({
headers: {
// TODO(josephperrott): Remove reference to TOKEN environment variable as part of larger
// effort to migrate to expecting tokens via GITHUB_ACCESS_TOKEN environment variables.
authorization: `token ${process.env.TOKEN || process.env.GITHUB_ACCESS_TOKEN}`,
}
});
import {GitClient} from './git';
/** Get a PR from github */
export async function getPr<PrSchema>(
prSchema: PrSchema, prNumber: number, {owner, name}: GithubConfig) {
export async function getPr<PrSchema>(prSchema: PrSchema, prNumber: number, git: GitClient) {
/** The owner and name of the repository */
const {owner, name} = git.remoteConfig;
/** The GraphQL query object to get a the PR */
const PR_QUERY = params(
{
$number: 'Int!', // The PR number
@ -41,14 +27,15 @@ export async function getPr<PrSchema>(
})
});
const result =
await graphql(graphqlQuery(PR_QUERY), {number: prNumber, owner, name}) as typeof PR_QUERY;
const result = (await git.github.graphql.query(PR_QUERY, {number: prNumber, owner, name}));
return result.repository.pullRequest;
}
/** Get all pending PRs from github */
export async function getPendingPrs<PrSchema>(prSchema: PrSchema, {owner, name}: GithubConfig) {
// The GraphQL query object to get a page of pending PRs
export async function getPendingPrs<PrSchema>(prSchema: PrSchema, git: GitClient) {
/** The owner and name of the repository */
const {owner, name} = git.remoteConfig;
/** The GraphQL query object to get a page of pending PRs */
const PRS_QUERY = params(
{
$first: 'Int', // How many entries to get with each request
@ -73,36 +60,22 @@ export async function getPendingPrs<PrSchema>(prSchema: PrSchema, {owner, name}:
}),
})
});
const query = graphqlQuery('members', PRS_QUERY);
/**
* Gets the query and queryParams for a specific page of entries.
*/
const queryBuilder = (count: number, cursor?: string) => {
return {
query,
params: {
after: cursor || null,
first: count,
owner,
name,
},
};
};
// The current cursor
/** The current cursor */
let cursor: string|undefined;
// If an additional page of members is expected
/** If an additional page of members is expected */
let hasNextPage = true;
// Array of pending PRs
/** Array of pending PRs */
const prs: Array<PrSchema> = [];
// For each page of the response, get the page and add it to the
// list of PRs
// For each page of the response, get the page and add it to the list of PRs
while (hasNextPage) {
const {query, params} = queryBuilder(100, cursor);
const results = await graphql(query, params) as typeof PRS_QUERY;
const params = {
after: cursor || null,
first: 100,
owner,
name,
};
const results = await git.github.graphql.query(PRS_QUERY, params) as typeof PRS_QUERY;
prs.push(...results.repository.pullRequests.nodes);
hasNextPage = results.repository.pullRequests.pageInfo.hasNextPage;
cursor = results.repository.pullRequests.pageInfo.endCursor;

View File

@ -0,0 +1,17 @@
/**
* @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
*/
// inquirer-autocomplete-prompt doesn't provide types and no types are made available via
// DefinitelyTyped.
declare module "inquirer-autocomplete-prompt" {
import {registerPrompt} from 'inquirer';
let AutocompletePrompt: Parameters<typeof registerPrompt>[1];
export = AutocompletePrompt;
}

View File

@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "10.0.10",
"version": "10.0.11",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",
@ -76,7 +76,7 @@
"@types/diff": "^3.5.1",
"@types/fs-extra": "4.0.2",
"@types/hammerjs": "2.0.35",
"@types/inquirer": "^6.5.0",
"@types/inquirer": "^7.3.0",
"@types/jasmine": "3.5.10",
"@types/jasminewd2": "^2.0.8",
"@types/minimist": "^1.2.0",
@ -87,7 +87,7 @@
"@types/shelljs": "^0.8.6",
"@types/systemjs": "0.19.32",
"@types/yaml": "^1.2.0",
"@types/yargs": "^11.1.1",
"@types/yargs": "^15.0.5",
"@webcomponents/custom-elements": "^1.1.0",
"angular": "npm:angular@1.7",
"angular-1.5": "npm:angular@1.5",
@ -151,7 +151,7 @@
"typescript": "~3.9.5",
"xhr2": "0.2.0",
"yaml": "^1.7.2",
"yargs": "15.3.0"
"yargs": "^15.4.1"
},
"// 2": "devDependencies are not used under Bazel. Many can be removed after test.sh is deleted.",
"devDependencies": {
@ -177,8 +177,9 @@
"glob": "7.1.2",
"gulp": "3.9.1",
"gulp-conventional-changelog": "^2.0.3",
"husky": "^4.2.3",
"inquirer": "^7.1.0",
"husky": "^4.2.5",
"inquirer": "^7.3.3",
"inquirer-autocomplete-prompt": "^1.0.2",
"jpm": "1.3.1",
"karma-browserstack-launcher": "^1.3.0",
"karma-sauce-launcher": "^2.0.2",

View File

@ -155,7 +155,7 @@ export class NgForOf<T, U extends NgIterable<T> = NgIterable<T>> implements DoCh
* rather than the identity of the object itself.
*
* The function receives two inputs,
* the iteration index and the node object ID.
* the iteration index and the associated node data.
*/
@Input()
set ngForTrackBy(fn: TrackByFunction<T>) {

View File

@ -20,9 +20,10 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
alias: 'source',
describe:
'A path (relative to the working directory) of the `node_modules` folder to process.',
default: './node_modules'
default: './node_modules',
type: 'string',
})
.option('f', {alias: 'formats', hidden: true, array: true})
.option('f', {alias: 'formats', hidden: true, array: true, type: 'string'})
.option('p', {
alias: 'properties',
array: true,
@ -30,7 +31,8 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
'An array of names of properties in package.json to compile (e.g. `module` or `main`)\n' +
'Each of these properties should hold the path to a bundle-format.\n' +
'If provided, only the specified properties are considered for processing.\n' +
'If not provided, all the supported format properties (e.g. fesm2015, fesm5, es2015, esm2015, esm5, main, module) in the package.json are considered.'
'If not provided, all the supported format properties (e.g. fesm2015, fesm5, es2015, esm2015, esm5, main, module) in the package.json are considered.',
type: 'string',
})
.option('t', {
alias: 'target',
@ -38,6 +40,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
'A relative path (from the `source` path) to a single entry-point to process (plus its dependencies).\n' +
'If this property is provided then `error-on-failed-entry-point` is forced to true.\n' +
'This option overrides the `--use-program-dependencies` option.',
type: 'string',
})
.option('use-program-dependencies', {
type: 'boolean',
@ -48,7 +51,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
.option('first-only', {
describe:
'If specified then only the first matching package.json property will be compiled.',
type: 'boolean'
type: 'boolean',
})
.option('create-ivy-entry-points', {
describe:
@ -79,6 +82,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
alias: 'loglevel',
describe: 'The lowest severity logging message that should be output.',
choices: ['debug', 'info', 'warn', 'error'],
type: 'string',
})
.option('invalidate-entry-point-manifest', {
describe:
@ -106,7 +110,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
.help()
.parse(args);
if (options['f'] && options['f'].length) {
if (options.f?.length) {
console.error(
'The formats option (-f/--formats) has been removed. Consider the properties option (-p/--properties) instead.');
process.exit(1);
@ -114,12 +118,12 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
setFileSystem(new NodeJSFileSystem());
const baseSourcePath = resolve(options['s'] || './node_modules');
const propertiesToConsider: string[] = options['p'];
const targetEntryPointPath = options['t'] ? options['t'] : undefined;
const baseSourcePath = resolve(options.s || './node_modules');
const propertiesToConsider = options.p;
const targetEntryPointPath = options.t;
const compileAllFormats = !options['first-only'];
const createNewEntryPointFormats = options['create-ivy-entry-points'];
const logLevel = options['l'] as keyof typeof LogLevel | undefined;
const logLevel = options.l as keyof typeof LogLevel | undefined;
const enableI18nLegacyMessageIdFormat = options['legacy-message-ids'];
const invalidateEntryPointManifest = options['invalidate-entry-point-manifest'];
const errorOnFailedEntryPoint = options['error-on-failed-entry-point'];
@ -127,7 +131,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
// yargs is not so great at mixed string+boolean types, so we have to test tsconfig against a
// string "false" to capture the `tsconfig=false` option.
// And we have to convert the option to a string to handle `no-tsconfig`, which will be `false`.
const tsConfigPath = `${options['tsconfig']}` === 'false' ? null : options['tsconfig'];
const tsConfigPath = `${options.tsconfig}` === 'false' ? null : options.tsconfig;
const logger = logLevel && new ConsoleLogger(LogLevel[logLevel]);
@ -139,7 +143,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
createNewEntryPointFormats,
logger,
enableI18nLegacyMessageIdFormat,
async: options['async'],
async: options.async,
invalidateEntryPointManifest,
errorOnFailedEntryPoint,
tsConfigPath,

View File

@ -30,18 +30,21 @@ if (require.main === module) {
required: true,
describe:
'The root path of the files to translate, either absolute or relative to the current working directory. E.g. `dist/en`.',
type: 'string',
})
.option('s', {
alias: 'source',
required: true,
describe:
'A glob pattern indicating what files to translate, relative to the `root` path. E.g. `bundles/**/*`.',
type: 'string',
})
.option('l', {
alias: 'source-locale',
describe:
'The source locale of the application. If this is provided then a copy of the application will be created with no translation but just the `$localize` calls stripped out.',
type: 'string',
})
.option('t', {
@ -54,6 +57,7 @@ if (require.main === module) {
'If you want to merge multiple translation files for each locale, then provide the list of files in an array.\n' +
'Note that the arrays must be in double quotes if you include any whitespace within the array.\n' +
'E.g. `-t "[src/locale/messages.en.xlf, src/locale/messages-2.en.xlf]" [src/locale/messages.fr.xlf,src/locale/messages-2.fr.xlf]`',
type: 'string',
})
.option('target-locales', {
@ -61,6 +65,7 @@ if (require.main === module) {
describe:
'A list of target locales for the translation files, which will override any target locale parsed from the translation file.\n' +
'E.g. "-t en fr de".',
type: 'string',
})
.option('o', {
@ -68,7 +73,8 @@ if (require.main === module) {
required: true,
describe: 'A output path pattern to where the translated files will be written.\n' +
'The path must be either absolute or relative to the current working directory.\n' +
'The marker `{{LOCALE}}` will be replaced with the target locale. E.g. `dist/{{LOCALE}}`.'
'The marker `{{LOCALE}}` will be replaced with the target locale. E.g. `dist/{{LOCALE}}`.',
type: 'string',
})
.option('m', {
@ -76,6 +82,7 @@ if (require.main === module) {
describe: 'How to handle missing translations.',
choices: ['error', 'warning', 'ignore'],
default: 'warning',
type: 'string',
})
.option('d', {
@ -83,6 +90,7 @@ if (require.main === module) {
describe: 'How to handle duplicate translations.',
choices: ['error', 'warning', 'ignore'],
default: 'warning',
type: 'string',
})
.strict()
@ -97,8 +105,8 @@ if (require.main === module) {
const translationFilePaths: (string|string[])[] = convertArraysFromArgs(options['t']);
const outputPathFn = getOutputPathFn(fs.resolve(options['o']));
const diagnostics = new Diagnostics();
const missingTranslation: DiagnosticHandlingStrategy = options['m'];
const duplicateTranslation: DiagnosticHandlingStrategy = options['d'];
const missingTranslation = options['m'] as DiagnosticHandlingStrategy;
const duplicateTranslation = options['d'] as DiagnosticHandlingStrategy;
const sourceLocale: string|undefined = options['l'];
const translationFileLocales: string[] = options['target-locales'] || [];

View File

@ -168,7 +168,7 @@ export class RouterLink implements OnChanges {
private preserve!: boolean;
/** @internal */
onChanges = new Subject<void>();
onChanges = new Subject<RouterLink>();
constructor(
private router: Router, private route: ActivatedRoute,
@ -182,7 +182,7 @@ export class RouterLink implements OnChanges {
ngOnChanges(changes: SimpleChanges) {
// This is subscribed to by `RouterLinkActive` so that it knows to update when there are changes
// to the RouterLinks it's tracking.
this.onChanges.next();
this.onChanges.next(this);
}
/**
@ -309,7 +309,7 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
@HostBinding() href!: string;
/** @internal */
onChanges = new Subject<void>();
onChanges = new Subject<RouterLinkWithHref>();
constructor(
private router: Router, private route: ActivatedRoute,
@ -351,7 +351,7 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
/** @nodoc */
ngOnChanges(changes: SimpleChanges): any {
this.updateTargetUrlAndHref();
this.onChanges.next();
this.onChanges.next(this);
}
/** @nodoc */
ngOnDestroy(): any {

View File

@ -119,8 +119,11 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
[...this.links.toArray(), ...this.linksWithHrefs.toArray(), this.link, this.linkWithHref]
.filter((link): link is RouterLink|RouterLinkWithHref => !!link)
.map(link => link.onChanges);
this.linkInputChangesSubscription =
from(allLinkChanges).pipe(mergeAll()).subscribe(() => this.update());
this.linkInputChangesSubscription = from(allLinkChanges).pipe(mergeAll()).subscribe(link => {
if (this.isActive !== this.isLinkActive(this.router)(link)) {
this.update();
}
});
}
@Input()

View File

@ -3974,7 +3974,7 @@ describe('Integration', () => {
})));
});
describe('routerActiveLink', () => {
describe('routerLinkActive', () => {
it('should set the class when the link is active (a tag)',
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
const fixture = createRoot(router, RootCmp);
@ -4124,6 +4124,29 @@ describe('Integration', () => {
advance(fixture);
expect(paragraph.textContent).toEqual('false');
}));
it('should not trigger change detection when active state has not changed', fakeAsync(() => {
@Component({
template: `<div id="link" routerLinkActive="active" [routerLink]="link"></div>`,
})
class LinkComponent {
link = 'notactive';
}
@Component({template: ''})
class SimpleComponent {
}
TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes([{path: '', component: SimpleComponent}])],
declarations: [LinkComponent, SimpleComponent]
});
const fixture = createRoot(TestBed.inject(Router), LinkComponent);
fixture.componentInstance.link = 'stillnotactive';
fixture.detectChanges(false /** checkNoChanges */);
expect(TestBed.inject(NgZone).hasPendingMicrotasks).toBe(false);
}));
});
describe('lazy loading', () => {

142
yarn.lock
View File

@ -2198,10 +2198,10 @@
resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.35.tgz#7b7c950c7d54593e23bffc8d2b4feba9866a7277"
integrity sha512-4mUIMSZ2U4UOWq1b+iV7XUTE4w+Kr3x+Zb/Qz5ROO6BTZLw2c8/ftjq0aRgluguLs4KRuBnrOy/s389HVn1/zA==
"@types/inquirer@^6.5.0":
version "6.5.0"
resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-6.5.0.tgz#b83b0bf30b88b8be7246d40e51d32fe9d10e09be"
integrity sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==
"@types/inquirer@^7.3.0":
version "7.3.0"
resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-7.3.0.tgz#a1233632ea6249f14eb481dae91138e747b85664"
integrity sha512-wcPs5jTrZYQBzzPlvUEzBcptzO4We2sijSvkBq8oAKRMJoH8PvrmP6QQnxLB5RScNUmRfujxA+ngxD4gk4xe7Q==
dependencies:
"@types/through" "*"
rxjs "^6.4.0"
@ -2347,10 +2347,17 @@
resolved "https://registry.yarnpkg.com/@types/yaml/-/yaml-1.2.0.tgz#4ed577fc4ebbd6b829b28734e56d10c9e6984e09"
integrity sha512-GW8b9qM+ebgW3/zjzPm0I1NxMvLaz/YKT9Ph6tTb+Fkeyzd9yLTvQ6ciQ2MorTRmb/qXmfjMerRpG4LviixaqQ==
"@types/yargs@^11.1.1":
version "11.1.5"
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-11.1.5.tgz#8d71dfe4848ac5d714b75eca3df9cac75a4f8dac"
integrity sha512-1jmXgoIyzxQSm33lYgEXvegtkhloHbed2I0QGlTN66U2F9/ExqJWSCSmaWC0IB/g1tW+IYSp+tDhcZBYB1ZGog==
"@types/yargs-parser@*":
version "15.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==
"@types/yargs@^15.0.5":
version "15.0.5"
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.5.tgz#947e9a6561483bdee9adffc983e91a6902af8b79"
integrity sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==
dependencies:
"@types/yargs-parser" "*"
"@types/yauzl@^2.9.1":
version "2.9.1"
@ -2749,7 +2756,7 @@ ansi-colors@^3.0.0:
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==
ansi-escapes@^3.1.0, ansi-escapes@^3.2.0:
ansi-escapes@^3.0.0, ansi-escapes@^3.1.0, ansi-escapes@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
@ -4015,6 +4022,14 @@ chalk@^3.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^4.0.0, chalk@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
char-spinner@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/char-spinner/-/char-spinner-1.0.1.tgz#e6ea67bd247e107112983b7ab0479ed362800081"
@ -4259,6 +4274,11 @@ cli-width@^2.0.0:
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
cli-width@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
cliui@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
@ -4465,7 +4485,7 @@ compare-semver@^1.0.0:
dependencies:
semver "^5.0.1"
compare-versions@^3.5.1:
compare-versions@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62"
integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==
@ -8049,14 +8069,14 @@ humanize-ms@^1.2.1:
dependencies:
ms "^2.0.0"
husky@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.3.tgz#3b18d2ee5febe99e27f2983500202daffbc3151e"
integrity sha512-VxTsSTRwYveKXN4SaH1/FefRJYCtx+wx04sSVcOpD7N2zjoHxa+cEJ07Qg5NmV3HAK+IRKOyNVpi2YBIVccIfQ==
husky@^4.2.5:
version "4.2.5"
resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.5.tgz#2b4f7622673a71579f901d9885ed448394b5fa36"
integrity sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ==
dependencies:
chalk "^3.0.0"
chalk "^4.0.0"
ci-info "^2.0.0"
compare-versions "^3.5.1"
compare-versions "^3.6.0"
cosmiconfig "^6.0.0"
find-versions "^3.2.0"
opencollective-postinstall "^2.0.2"
@ -8236,7 +8256,17 @@ ini@1.3.5, ini@^1.3.2, ini@^1.3.4, ini@~1.3.0, ini@~1.3.3:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
inquirer@7.1.0, inquirer@^7.1.0:
inquirer-autocomplete-prompt@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/inquirer-autocomplete-prompt/-/inquirer-autocomplete-prompt-1.0.2.tgz#3f2548f73dd12f0a541be055ea9c8c7aedeb42bf"
integrity sha512-vNmAhhrOQwPnUm4B9kz1UB7P98rVF1z8txnjp53r40N0PBCuqoRWqjg3Tl0yz0UkDg7rEUtZ2OZpNc7jnOU9Zw==
dependencies:
ansi-escapes "^3.0.0"
chalk "^2.0.0"
figures "^2.0.0"
run-async "^2.3.0"
inquirer@7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29"
integrity sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==
@ -8255,6 +8285,25 @@ inquirer@7.1.0, inquirer@^7.1.0:
strip-ansi "^6.0.0"
through "^2.3.6"
inquirer@^7.3.3:
version "7.3.3"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==
dependencies:
ansi-escapes "^4.2.1"
chalk "^4.1.0"
cli-cursor "^3.1.0"
cli-width "^3.0.0"
external-editor "^3.0.3"
figures "^3.0.0"
lodash "^4.17.19"
mute-stream "0.0.8"
run-async "^2.4.0"
rxjs "^6.6.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
through "^2.3.6"
inquirer@~6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.3.1.tgz#7a413b5e7950811013a3db491c61d1f3b776e8e7"
@ -9872,6 +9921,11 @@ lodash@^4.0.0, lodash@^4.14.0, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.11,
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
lodash@^4.17.19:
version "4.17.19"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
lodash@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-1.0.2.tgz#8f57560c83b59fc270bd3d561b690043430e2551"
@ -13363,6 +13417,11 @@ run-async@^2.2.0, run-async@^2.4.0:
dependencies:
is-promise "^2.1.0"
run-async@^2.3.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
run-queue@^1.0.0, run-queue@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
@ -13384,6 +13443,13 @@ rxjs@6.5.5:
dependencies:
tslib "^1.9.0"
rxjs@^6.6.0:
version "6.6.2"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2"
integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==
dependencies:
tslib "^1.9.0"
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@ -16291,10 +16357,10 @@ yargs-parser@^15.0.1:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^18.1.0:
version "18.1.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.2.tgz#2f482bea2136dbde0861683abea7756d30b504f1"
integrity sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==
yargs-parser@^18.1.2:
version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
@ -16306,23 +16372,6 @@ yargs-parser@^9.0.2:
dependencies:
camelcase "^4.1.0"
yargs@15.3.0:
version "15.3.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.0.tgz#403af6edc75b3ae04bf66c94202228ba119f0976"
integrity sha512-g/QCnmjgOl1YJjGsnUg2SatC7NUYEiLXJqxNOQU9qSpjzGtGXda9b+OKccr1kLTy8BN9yqEyqfq5lxlwdc13TA==
dependencies:
cliui "^6.0.0"
decamelize "^1.2.0"
find-up "^4.1.0"
get-caller-file "^2.0.1"
require-directory "^2.1.1"
require-main-filename "^2.0.0"
set-blocking "^2.0.0"
string-width "^4.2.0"
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^18.1.0"
yargs@^11.0.0:
version "11.1.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.1.tgz#5052efe3446a4df5ed669c995886cc0f13702766"
@ -16374,6 +16423,23 @@ yargs@^14.2.3:
y18n "^4.0.0"
yargs-parser "^15.0.1"
yargs@^15.4.1:
version "15.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
dependencies:
cliui "^6.0.0"
decamelize "^1.2.0"
find-up "^4.1.0"
get-caller-file "^2.0.1"
require-directory "^2.1.1"
require-main-filename "^2.0.0"
set-blocking "^2.0.0"
string-width "^4.2.0"
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^18.1.2"
yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"