mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
build: rework breaking changes tooling (#12950)
* Reworks the breaking change tslint rule to remove the logic that checks that there are no expired breaking changes. * Adds a new script that can be used to get an overview of the pending and expired breaking changes.
This commit is contained in:
committed by
Jeremy Elbourn
parent
c618f03a34
commit
0ab720a69d
+2
-1
@@ -22,7 +22,8 @@
|
||||
"deploy": "gulp deploy:devapp",
|
||||
"webdriver-manager": "webdriver-manager",
|
||||
"docs": "gulp docs",
|
||||
"api": "gulp api-docs"
|
||||
"api": "gulp api-docs",
|
||||
"breaking-changes": "gulp breaking-changes"
|
||||
},
|
||||
"version": "7.0.0-beta.0",
|
||||
"requiredAngularVersion": ">=7.0.0-beta.4",
|
||||
|
||||
@@ -16,6 +16,7 @@ createPackageBuildTasks(examplesPackage, ['build-examples-module']);
|
||||
createPackageBuildTasks(momentAdapterPackage);
|
||||
|
||||
import './tasks/aot';
|
||||
import './tasks/breaking-changes';
|
||||
import './tasks/changelog';
|
||||
import './tasks/ci';
|
||||
import './tasks/clean';
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import {task} from 'gulp';
|
||||
import {join, relative} from 'path';
|
||||
import {readFileSync} from 'fs';
|
||||
import {bold, red, green} from 'chalk';
|
||||
import * as ts from 'typescript';
|
||||
import * as tsutils from 'tsutils';
|
||||
import {buildConfig} from '../../package-tools/build-config';
|
||||
|
||||
// Current version from the package.json. Splits it on the dash to ignore `-beta.x` suffixes.
|
||||
const packageVersion = require(join(buildConfig.projectDir, 'package.json')).version.split('-')[0];
|
||||
|
||||
// Regex used to extract versions from a string.
|
||||
const versionRegex = /\d+\.\d+\.\d+/;
|
||||
|
||||
/**
|
||||
* Goes through all of the TypeScript files in the project and puts
|
||||
* together a summary of all of the pending and expired breaking changes.
|
||||
*/
|
||||
task('breaking-changes', () => {
|
||||
const projectDir = buildConfig.projectDir;
|
||||
const configFile = ts.readJsonConfigFile(join(projectDir, 'tsconfig.json'), ts.sys.readFile);
|
||||
const parsedConfig = ts.parseJsonSourceFileConfigFileContent(configFile, ts.sys, projectDir);
|
||||
const summary: {[version: string]: string[]} = {};
|
||||
|
||||
// Go through all the TS files in the project.
|
||||
parsedConfig.fileNames.forEach(fileName => {
|
||||
const sourceFile = ts.createSourceFile(fileName, readFileSync(fileName, 'utf8'),
|
||||
configFile.languageVersion);
|
||||
const lineRanges = tsutils.getLineRanges(sourceFile);
|
||||
|
||||
// Go through each of the comments of the file.
|
||||
tsutils.forEachComment(sourceFile, (file, range) => {
|
||||
const comment = file.substring(range.pos, range.end);
|
||||
const versionMatch = comment.match(versionRegex);
|
||||
|
||||
// Don't do any extra work if the comment doesn't indicate a breaking change.
|
||||
if (!versionMatch || comment.indexOf('@breaking-change') === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a path relative to the project root, in order to make the summary more tidy.
|
||||
// Also replace escaped Windows slashes with regular forward slashes.
|
||||
const pathInProject = relative(projectDir, sourceFile.fileName).replace(/\\/g, '/');
|
||||
const [version] = versionMatch;
|
||||
|
||||
summary[version] = summary[version] || [];
|
||||
summary[version].push(` ${pathInProject}: ${formatMessage(comment, range, lineRanges)}`);
|
||||
});
|
||||
});
|
||||
|
||||
// Go through the summary and log out all of the breaking changes.
|
||||
Object.keys(summary).forEach(version => {
|
||||
const isExpired = hasExpired(packageVersion, version);
|
||||
const status = isExpired ? red('(expired)') : green('(not expired)');
|
||||
const header = bold(`Breaking changes for ${version} ${status}:`);
|
||||
const messages = summary[version].join('\n');
|
||||
|
||||
console.log(isExpired ? red(header) : header);
|
||||
console.log(isExpired ? red(messages) : messages, '\n');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Formats a message to be logged out in the breaking changes summary.
|
||||
* @param comment Contents of the comment that contains the breaking change.
|
||||
* @param commentRange Object containing info on the position of the comment in the file.
|
||||
* @param lines Ranges of the lines of code in the file.
|
||||
*/
|
||||
function formatMessage(comment: string, commentRange: ts.CommentRange, lines: tsutils.LineRange[]) {
|
||||
const lineNumber = lines.findIndex(line => line.pos > commentRange.pos);
|
||||
const messageMatch = comment.match(/@deprecated(.*)|@breaking-change(.*)/);
|
||||
const message = messageMatch ? messageMatch[0] : '';
|
||||
const cleanMessage = message
|
||||
.replace(/[\*\/\r\n]|@[\w-]+/g, '')
|
||||
.replace(versionRegex, '')
|
||||
.trim();
|
||||
|
||||
return `Line ${lineNumber}, ${cleanMessage || 'No message'}`;
|
||||
}
|
||||
|
||||
|
||||
/** Converts a version string into an object. */
|
||||
function parseVersion(version: string) {
|
||||
const [major = 0, minor = 0, patch = 0] = version.split('.').map(segment => parseInt(segment));
|
||||
return {major, minor, patch};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether a version has expired, based on the current version.
|
||||
* @param currentVersion Current version of the package.
|
||||
* @param breakingChange Version that is being checked.
|
||||
*/
|
||||
function hasExpired(currentVersion: string, breakingChange: string) {
|
||||
if (currentVersion === breakingChange) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const current = parseVersion(currentVersion);
|
||||
const target = parseVersion(breakingChange);
|
||||
|
||||
return target.major < current.major ||
|
||||
(target.major === current.major && target.minor < current.minor) ||
|
||||
(
|
||||
target.major === current.major &&
|
||||
target.minor === current.minor &&
|
||||
target.patch < current.patch
|
||||
);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import * as ts from 'typescript';
|
||||
import * as Lint from 'tslint';
|
||||
import * as utils from 'tsutils';
|
||||
import * as path from 'path';
|
||||
|
||||
/** Doc tag that can be used to indicate a breaking change. */
|
||||
const BREAKING_CHANGE = '@breaking-change';
|
||||
|
||||
/** Name of the old doc tag that was being used to indicate a breaking change. */
|
||||
const DELETION_TARGET = '@deletion-target';
|
||||
|
||||
/**
|
||||
* Rule ensuring that breaking changes have not expired.
|
||||
* The current version is taken from the `package.json`.
|
||||
*/
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
// Split it on the dash to ignore `-beta.x` suffixes.
|
||||
const packageVersion = require(path.join(process.cwd(), 'package.json')).version.split('-')[0];
|
||||
|
||||
return this.applyWithFunction(sourceFile, (ctx: Lint.WalkContext<any>) => {
|
||||
utils.forEachComment(ctx.sourceFile, (file, {pos, end}) => {
|
||||
const commentText = file.substring(pos, end);
|
||||
|
||||
// TODO(crisbeto): remove this check once most of the pending
|
||||
// PRs start using `breaking-change`.
|
||||
if (commentText.indexOf(DELETION_TARGET) > -1) {
|
||||
ctx.addFailure(pos, end, `${DELETION_TARGET} has been replaced with ${BREAKING_CHANGE}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasBreakingChange = commentText.indexOf(BREAKING_CHANGE) > -1;
|
||||
|
||||
if (!hasBreakingChange && commentText.indexOf('@deprecated') > -1) {
|
||||
ctx.addFailure(pos, end, `@deprecated marker has to have a ${BREAKING_CHANGE}.`);
|
||||
} if (hasBreakingChange) {
|
||||
const version = commentText.match(/\d+\.\d+\.\d+/);
|
||||
|
||||
if (!version) {
|
||||
ctx.addFailure(pos, end, `${BREAKING_CHANGE} must have a version.`);
|
||||
} else if (this._hasExpired(packageVersion, version[0])) {
|
||||
ctx.addFailure(pos, end, `Breaking change at ${version[0]} is due to be deleted. ` +
|
||||
`Current version is ${packageVersion}.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a version has expired, based on the current version.
|
||||
* @param currentVersion Current version of the package.
|
||||
* @param breakingChange Version that is being checked.
|
||||
*/
|
||||
private _hasExpired(currentVersion: string, breakingChange: string) {
|
||||
if (currentVersion === breakingChange) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const current = this._parseVersion(currentVersion);
|
||||
const target = this._parseVersion(breakingChange);
|
||||
|
||||
return target.major < current.major ||
|
||||
(target.major === current.major && target.minor < current.minor) ||
|
||||
(
|
||||
target.major === current.major &&
|
||||
target.minor === current.minor &&
|
||||
target.patch < current.patch
|
||||
);
|
||||
}
|
||||
|
||||
/** Converts a version string into an object. */
|
||||
private _parseVersion(version: string) {
|
||||
const [major = 0, minor = 0, patch = 0] = version.split('.').map(segment => parseInt(segment));
|
||||
return {major, minor, patch};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as ts from 'typescript';
|
||||
import * as Lint from 'tslint';
|
||||
import * as utils from 'tsutils';
|
||||
|
||||
/** Doc tag that can be used to indicate a breaking change. */
|
||||
const BREAKING_CHANGE = '@breaking-change';
|
||||
|
||||
/** Name of the old doc tag that was being used to indicate a breaking change. */
|
||||
const DELETION_TARGET = '@deletion-target';
|
||||
|
||||
/**
|
||||
* Rule that ensures that comments, indicating a deprecation
|
||||
* or a breaking change, have a valid version.
|
||||
*/
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, (ctx: Lint.WalkContext<any>) => {
|
||||
utils.forEachComment(ctx.sourceFile, (file, {pos, end}) => {
|
||||
const commentText = file.substring(pos, end);
|
||||
|
||||
// TODO(crisbeto): remove this check once most of the pending
|
||||
// PRs start using `breaking-change`.
|
||||
if (commentText.indexOf(DELETION_TARGET) > -1) {
|
||||
ctx.addFailure(pos, end, `${DELETION_TARGET} has been replaced with ${BREAKING_CHANGE}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasBreakingChange = commentText.indexOf(BREAKING_CHANGE) > -1;
|
||||
|
||||
if (!hasBreakingChange && commentText.indexOf('@deprecated') > -1) {
|
||||
ctx.addFailure(pos, end, `@deprecated marker has to have a ${BREAKING_CHANGE}.`);
|
||||
} if (hasBreakingChange && !/\d+\.\d+\.\d+/.test(commentText)) {
|
||||
ctx.addFailure(pos, end, `${BREAKING_CHANGE} must have a version.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,7 @@
|
||||
"no-private-getters": true,
|
||||
"setters-after-getters": true,
|
||||
"rxjs-imports": true,
|
||||
"require-breaking-change-version": true,
|
||||
"no-host-decorator-in-concrete": [
|
||||
true,
|
||||
"HostBinding",
|
||||
|
||||
Reference in New Issue
Block a user