feat(dev-infra): introduce validators for ng-dev config loading (#37049)
Introduces infrastructure to validate configuration of the ng-dev command at run time. Allowing for errors to be returned to the user running the command. PR Close #37049
This commit is contained in:
parent
45f4a47286
commit
14c0ec97d8
|
@ -5,9 +5,27 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
|
||||||
|
|
||||||
export interface CommitMessageConfig {
|
export interface CommitMessageConfig {
|
||||||
maxLineLength: number;
|
maxLineLength: number;
|
||||||
minBodyLength: number;
|
minBodyLength: number;
|
||||||
types: string[];
|
types: string[];
|
||||||
scopes: string[];
|
scopes: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Retrieve and validate the config as `CommitMessageConfig`. */
|
||||||
|
export function getCommitMessageConfig() {
|
||||||
|
// List of errors encountered validating the config.
|
||||||
|
const errors: string[] = [];
|
||||||
|
// The unvalidated config object.
|
||||||
|
const config: Partial<NgDevConfig<{commitMessage: CommitMessageConfig}>> = getConfig();
|
||||||
|
|
||||||
|
if (config.commitMessage === undefined) {
|
||||||
|
errors.push(`No configuration defined for "commitMessage"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNoErrors(errors);
|
||||||
|
return config as Required<typeof config>;
|
||||||
|
}
|
||||||
|
|
|
@ -7,11 +7,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Imports
|
// Imports
|
||||||
import * as utilConfig from '../utils/config';
|
import * as validateConfig from './config';
|
||||||
|
|
||||||
import {validateCommitMessage} from './validate';
|
import {validateCommitMessage} from './validate';
|
||||||
|
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const config = {
|
const config = {
|
||||||
'commitMessage': {
|
'commitMessage': {
|
||||||
|
@ -46,7 +44,7 @@ describe('validate-commit-message.js', () => {
|
||||||
lastError = '';
|
lastError = '';
|
||||||
|
|
||||||
spyOn(console, 'error').and.callFake((msg: string) => lastError = msg);
|
spyOn(console, 'error').and.callFake((msg: string) => lastError = msg);
|
||||||
spyOn(utilConfig, 'getAngularDevConfig').and.returnValue(config);
|
spyOn(validateConfig, 'getCommitMessageConfig').and.returnValue(config);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('validateMessage()', () => {
|
describe('validateMessage()', () => {
|
||||||
|
|
|
@ -5,8 +5,7 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {getAngularDevConfig} from '../utils/config';
|
import {getCommitMessageConfig} from './config';
|
||||||
import {CommitMessageConfig} from './config';
|
|
||||||
|
|
||||||
/** Options for commit message validation. */
|
/** Options for commit message validation. */
|
||||||
export interface ValidateCommitMessageOptions {
|
export interface ValidateCommitMessageOptions {
|
||||||
|
@ -76,7 +75,7 @@ export function validateCommitMessage(
|
||||||
`<type>(<scope>): <subject>\n\n<body>`);
|
`<type>(<scope>): <subject>\n\n<body>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = getAngularDevConfig<'commitMessage', CommitMessageConfig>().commitMessage;
|
const config = getCommitMessageConfig().commitMessage;
|
||||||
const commit = parseCommitMessage(commitMsg);
|
const commit = parseCommitMessage(commitMsg);
|
||||||
|
|
||||||
////////////////////////////////////
|
////////////////////////////////////
|
||||||
|
|
|
@ -6,8 +6,46 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface FormatConfig {
|
import {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
|
||||||
[keyof: string]: boolean|{
|
|
||||||
|
interface Formatter {
|
||||||
matchers: string[];
|
matchers: string[];
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export interface FormatConfig {
|
||||||
|
[keyof: string]: boolean|Formatter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retrieve and validate the config as `FormatConfig`. */
|
||||||
|
export function getFormatConfig() {
|
||||||
|
// List of errors encountered validating the config.
|
||||||
|
const errors: string[] = [];
|
||||||
|
// The unvalidated config object.
|
||||||
|
const config: Partial<NgDevConfig<{format: FormatConfig}>> = getConfig();
|
||||||
|
|
||||||
|
if (config.format === undefined) {
|
||||||
|
errors.push(`No configuration defined for "format"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(config.format!)) {
|
||||||
|
switch (typeof value) {
|
||||||
|
case 'boolean':
|
||||||
|
break;
|
||||||
|
case 'object':
|
||||||
|
checkFormatterConfig(key, value, errors);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errors.push(`"format.${key}" is not a boolean or Formatter object`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNoErrors(errors);
|
||||||
|
return config as Required<typeof config>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Validate an individual Formatter config. */
|
||||||
|
function checkFormatterConfig(key: string, config: Partial<Formatter>, errors: string[]) {
|
||||||
|
if (config.matchers === undefined) {
|
||||||
|
errors.push(`Missing "format.${key}.matchers" value`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {getAngularDevConfig} from '../../utils/config';
|
import {getFormatConfig} from '../config';
|
||||||
import {FormatConfig} from '../config';
|
|
||||||
|
|
||||||
import {Buildifier} from './buildifier';
|
import {Buildifier} from './buildifier';
|
||||||
import {ClangFormat} from './clang-format';
|
import {ClangFormat} from './clang-format';
|
||||||
|
@ -16,11 +15,7 @@ import {ClangFormat} from './clang-format';
|
||||||
* Get all defined formatters which are active based on the current loaded config.
|
* Get all defined formatters which are active based on the current loaded config.
|
||||||
*/
|
*/
|
||||||
export function getActiveFormatters() {
|
export function getActiveFormatters() {
|
||||||
let config = {};
|
const config = getFormatConfig().format;
|
||||||
try {
|
|
||||||
config = getAngularDevConfig<'format', FormatConfig>().format || {};
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
return [new Buildifier(config), new ClangFormat(config)].filter(
|
return [new Buildifier(config), new ClangFormat(config)].filter(
|
||||||
formatter => formatter.isEnabled());
|
formatter => formatter.isEnabled());
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,7 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||||
|
|
||||||
ts_library(
|
ts_library(
|
||||||
name = "utils",
|
name = "utils",
|
||||||
srcs = [
|
srcs = glob(["*.ts"]),
|
||||||
"config.ts",
|
|
||||||
"repo-files.ts",
|
|
||||||
],
|
|
||||||
module_name = "@angular/dev-infra-private/utils",
|
module_name = "@angular/dev-infra-private/utils",
|
||||||
visibility = ["//dev-infra:__subpackages__"],
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
deps = [
|
deps = [
|
||||||
|
|
|
@ -9,13 +9,64 @@
|
||||||
import {join} from 'path';
|
import {join} from 'path';
|
||||||
import {exec} from 'shelljs';
|
import {exec} from 'shelljs';
|
||||||
|
|
||||||
|
/** The common configuration for ng-dev. */
|
||||||
|
type CommonConfig = {
|
||||||
|
// TODO: add common configuration
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration for the specific ng-dev command, providing both the common
|
||||||
|
* ng-dev config as well as the specific config of a subcommand.
|
||||||
|
*/
|
||||||
|
export type NgDevConfig<T = {}> = CommonConfig&T;
|
||||||
|
|
||||||
// The filename expected for creating the ng-dev config, without the file
|
// The filename expected for creating the ng-dev config, without the file
|
||||||
// extension to allow either a typescript or javascript file to be used.
|
// extension to allow either a typescript or javascript file to be used.
|
||||||
const CONFIG_FILE_NAME = '.ng-dev-config';
|
const CONFIG_FILE_NAME = '.ng-dev-config';
|
||||||
|
|
||||||
|
/** The configuration for ng-dev. */
|
||||||
|
let CONFIG: {}|null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the path of the directory for the repository base.
|
* Get the configuration from the file system, returning the already loaded copy if it
|
||||||
|
* is defined.
|
||||||
*/
|
*/
|
||||||
|
export function getConfig(): NgDevConfig {
|
||||||
|
// If the global config is not defined, load it from the file system.
|
||||||
|
if (CONFIG === null) {
|
||||||
|
// The full path to the configuration file.
|
||||||
|
const configPath = join(getRepoBaseDir(), CONFIG_FILE_NAME);
|
||||||
|
// Set the global config object to a clone of the configuration loaded through default exports
|
||||||
|
// from the config file.
|
||||||
|
CONFIG = {...require(configPath)};
|
||||||
|
}
|
||||||
|
// Return a clone of the global config to ensure that a new instance of the config is returned
|
||||||
|
// each time, preventing unexpected effects of modifications to the config object.
|
||||||
|
return validateCommonConfig({...CONFIG});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Validate the common configuration has been met for the ng-dev command. */
|
||||||
|
function validateCommonConfig(config: NgDevConfig<CommonConfig>) {
|
||||||
|
// TODO: add validation for the common configuration
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts the provided array of error messages is empty. If any errors are in the array,
|
||||||
|
* logs the errors and exit the process as a failure.
|
||||||
|
*/
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the path of the directory for the repository base. */
|
||||||
export function getRepoBaseDir() {
|
export function getRepoBaseDir() {
|
||||||
const baseRepoDir = exec(`git rev-parse --show-toplevel`, {silent: true});
|
const baseRepoDir = exec(`git rev-parse --show-toplevel`, {silent: true});
|
||||||
if (baseRepoDir.code) {
|
if (baseRepoDir.code) {
|
||||||
|
@ -26,26 +77,3 @@ export function getRepoBaseDir() {
|
||||||
}
|
}
|
||||||
return baseRepoDir.trim();
|
return baseRepoDir.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the configuration from the .ng-dev-config.js file.
|
|
||||||
*/
|
|
||||||
export function getAngularDevConfig<K, T>(supressError = false): DevInfraConfig<K, T> {
|
|
||||||
const configPath = join(getRepoBaseDir(), CONFIG_FILE_NAME);
|
|
||||||
try {
|
|
||||||
return require(configPath) as DevInfraConfig<K, T>;
|
|
||||||
} catch (err) {
|
|
||||||
if (!supressError) {
|
|
||||||
throw Error(`Unable to load config file at:\n ${configPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {} as DevInfraConfig<K, T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface exressing the expected structure of the DevInfraConfig.
|
|
||||||
* Allows for providing a typing for a part of the config to read.
|
|
||||||
*/
|
|
||||||
export interface DevInfraConfig<K, T> {
|
|
||||||
[K: string]: T;
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue