feat: add configuration for configuration checks

Fixes #13
This commit is contained in:
Karl-Philipp Wulfert
2019-04-18 13:05:46 +02:00
parent 748cea493c
commit da2d7f6f91
6 changed files with 447 additions and 288 deletions

View File

@@ -20,12 +20,15 @@ import {
checkCIConfig,
checkConfigurationFilesAreExtended,
checkContributors,
checkDependencies,
checkLicenses,
checkNeededFiles,
checkNYCConfiguration,
checkScripts,
consoleInfo,
consoleLog,
getConfiguration,
getRules,
} from './common';
// current working directory
@@ -43,18 +46,12 @@ const path = resolve(commander.path);
// check for existing package.json in provided path
if (!existsSync(resolve(path, 'package.json'))) {
throw new Error(`No package.json in "${path}".`);
throw new Error(`No 'package.json' in '${path}'.`);
}
// path to examined package.json
const packageJsonPath = resolve(path, 'package.json');
// whether or not the contents of the package.json were changed
let packageJsonChanged = false;
// whether or not to suggest an overwrite
let suggestOverwrite = false;
// read package.json in provided path
const packageJson = JSON.parse(readFileSync(packageJsonPath).toString());
@@ -64,26 +61,38 @@ if (packageJson.name === '@openstapps/configuration') {
process.exit(0);
}
checkLicenses(packageJson);
// whether or not the contents of the package.json were changed
let packageJsonChanged = false;
// whether or not to suggest an overwrite
let suggestOverwrite = false;
const configuration = getConfiguration(packageJson);
const rules = getRules(configuration);
checkDependencies(rules, packageJson);
checkLicenses(rules, packageJson);
checkConfigurationFilesAreExtended(path);
suggestOverwrite = suggestOverwrite || checkNeededFiles(path, commander.replace);
suggestOverwrite = suggestOverwrite || checkNeededFiles(rules, path, commander.replace);
const checkedNYCConfiguration = checkNYCConfiguration(packageJson, commander.replace);
const checkedNYCConfiguration = checkNYCConfiguration(rules, packageJson, commander.replace);
packageJsonChanged = packageJsonChanged || checkedNYCConfiguration[0];
suggestOverwrite = suggestOverwrite || checkedNYCConfiguration[1];
packageJsonChanged = packageJsonChanged || checkScripts(packageJson, commander.replace);
packageJsonChanged = packageJsonChanged || checkScripts(rules, packageJson, commander.replace);
checkContributors(packageJson);
checkCIConfig(path);
checkCIConfig(rules, path);
if (packageJsonChanged) {
writeFileSync(resolve(path, 'package.json'), JSON.stringify(packageJson, null, 2));
consoleLog(`Changes were written to "${packageJsonPath}".`);
consoleLog(`Changes were written to '${packageJsonPath}'.`);
}
if (suggestOverwrite) {

View File

@@ -2,9 +2,73 @@ import chalk from 'chalk';
import {execSync} from 'child_process';
import {copyFileSync, existsSync, readFileSync} from 'fs';
import {join, resolve, sep} from 'path';
import {satisfies, valid} from 'semver';
import {isDeepStrictEqual} from 'util';
import {parse, stringify} from 'yaml';
import {EXPECTED_CI_CONFIG, EXPECTED_LICENSES, NEEDED_FILES, NYC_CONFIGURATION, SCRIPTS} from './configuration';
/**
* Configuration for the configuration check
*/
export interface Configuration {
/**
* Whether or not the project is meant to be packaged
*/
forPackaging: boolean;
/**
* Whether or not the project has a CLI
*/
hasCli: boolean;
/**
* A list of script names to ignore while checking
*/
ignoreScripts: string[];
/**
* Whether or not the project is meant to be executed server side
*/
serverSide: boolean;
/**
* Whether or not the standard build procedure is meant to be used
*/
standardBuild: boolean;
/**
* Whether or not the standard documentation procedure is meant to be used
*/
standardDocumentation: boolean;
}
/**
* Rules for the configuration check
*/
export interface Rules {
/**
* Expected CI config
*/
ciConfig: any;
/**
* Expected dependencies
*/
dependencies: string[];
/**
* Expected dev dependencies
*/
devDependencies: string[];
/**
* Expected files
*/
files: string[];
/**
* Expected licenses
*/
licenses: string[];
/**
* Expected NYC configuration
*/
nycConfiguration: any;
/**
* Expected scripts
*/
scripts: { [k: string]: string; };
}
/**
* Wrapper for console.info that outputs every argument in cyan
@@ -25,8 +89,13 @@ export function consoleInfo(...args: string[]): void {
*/
export function consoleWarn(...args: string[]): void {
args.forEach((arg) => {
const lines = arg.split('\n');
/* tslint:disable-next-line:no-console */
console.warn('\n' + chalk.red.bold(arg));
console.warn('\n' + chalk.red.bold(lines[0]));
for (const line of lines.slice(1)) {
/* tslint:disable-next-line:no-console */
console.info(line);
}
});
}
@@ -42,6 +111,53 @@ export function consoleLog(...args: string[]): void {
});
}
/**
* Check dependencies are installed
*
* @param rules Rules for check
* @param packageJson package.json to check dependencies in
*/
export function checkDependencies(rules: Rules, packageJson: any): void {
for (const dependency of rules.dependencies) {
const [name, version] = dependency.split(':');
const installedVersion = packageJson.dependencies[name];
if (typeof packageJson.dependencies === 'undefined' || typeof packageJson.dependencies[name] === 'undefined') {
consoleWarn(`Dependency '${name}' is missing.
Please install with 'npm install --save-exact ${name}'.`);
} else if (
typeof version !== 'undefined'
&& valid(version)
&& !satisfies(installedVersion, version)
) {
consoleWarn(
`Version '${installedVersion}' of dependency '${name} does not satisfy constraint '${version}'.`,
);
}
}
for (const devDependency of rules.devDependencies) {
const [name, version] = devDependency.split(':');
const installedVersion = packageJson.dependencies[name];
if (
typeof packageJson.dependencies === 'undefined' || typeof packageJson.dependencies[name] === 'undefined'
&& typeof packageJson.devDependencies === 'undefined' || typeof packageJson.devDependencies[name] === 'undefined'
) {
consoleWarn(`Dev dependency '${name}' is missing.
Please install with 'npm install --save-exact --save-dev ${name}'.`);
} else if (
typeof version !== 'undefined'
&& valid(version)
&& !satisfies(installedVersion, version)
) {
consoleWarn(
`Version '${installedVersion}' of dev dependency '${name} does not satisfy constraint '${version}'.`,
);
}
}
}
/**
* Check that configuration files are extended
*
@@ -67,7 +183,7 @@ export function checkConfigurationFilesAreExtended(path: string): void {
);
if (!configFileExtended) {
consoleWarn(`File "${fileToCheck}" should extend "${expectedPath}"!
consoleWarn(`File '${fileToCheck}' should extend '${expectedPath}'!
Example:
${readFileSync(resolve(__dirname, '..', 'templates', 'template-' + file))}`);
@@ -79,15 +195,16 @@ ${readFileSync(resolve(__dirname, '..', 'templates', 'template-' + file))}`);
/**
* Check needed files
*
* @param rules Rules for check
* @param path Path to files to check
* @param replaceFlag Whether or not to replace files
* @return Whether or not overwrite is suggested
*/
export function checkNeededFiles(path: string, replaceFlag: boolean): boolean {
export function checkNeededFiles(rules: Rules, path: string, replaceFlag: boolean): boolean {
let suggestOverwrite = false;
// copy needed files
NEEDED_FILES.forEach((file) => {
rules.files.forEach((file) => {
let destinationFile = file;
// remove templates directory for destination files
@@ -102,7 +219,7 @@ export function checkNeededFiles(path: string, replaceFlag: boolean): boolean {
// check if file exists or replace flag is set
if (!existsSync(destination) || replaceFlag) {
copyFileSync(source, destination);
consoleInfo(`Copied file "${source}" to "${destination}".`);
consoleInfo(`Copied file '${source}' to '${destination}'.`);
} else if (destinationFile === '.npmignore') {
const npmIgnore = readFileSync(destination).toString();
@@ -143,23 +260,25 @@ https://gitlab.com/openstapps/configuration/issues/11`);
/**
* Check licenses
*
* @param rules Rules for check
* @param packageJson package.json to check license in
*/
export function checkLicenses(packageJson: any): void {
export function checkLicenses(rules: Rules, packageJson: any): void {
// check if license is one of the expected ones
if (EXPECTED_LICENSES.indexOf(packageJson.license) === -1) {
consoleWarn(`License should be one of "${EXPECTED_LICENSES.join(', ')}"!`);
if (rules.licenses.indexOf(packageJson.license) === -1) {
consoleWarn(`License should be one of '${rules.licenses.join(', ')}'!`);
}
}
/**
* Check NYC configuration
*
* @param rules Rules for check
* @param packageJson package.json to check NYC configuration in
* @param replaceFlag Whether or not to replace NYC configuration
* @return Whether or not package.json was changed and if overwrite is suggested
*/
export function checkNYCConfiguration(packageJson: any, replaceFlag: boolean): [boolean, boolean] {
export function checkNYCConfiguration(rules: Rules, packageJson: any, replaceFlag: boolean): [boolean, boolean] {
let packageJsonChanged = false;
let suggestOverwrite = false;
@@ -167,12 +286,12 @@ export function checkNYCConfiguration(packageJson: any, replaceFlag: boolean): [
if (typeof packageJson.devDependencies === 'object' && Object.keys(packageJson.devDependencies).indexOf('nyc') >= 0) {
if (typeof packageJson.nyc === 'undefined' || replaceFlag) {
// add NYC configuration
packageJson.nyc = NYC_CONFIGURATION;
packageJson.nyc = rules.nycConfiguration;
packageJsonChanged = true;
consoleLog(`Added NYC configuration in to 'package.json'.`);
} else if (!isDeepStrictEqual(packageJson.nyc, NYC_CONFIGURATION)) {
} else if (!isDeepStrictEqual(packageJson.nyc, rules.nycConfiguration)) {
consoleInfo(`NYC configuration in 'package.json' differs from the proposed one. Please check manually.`);
suggestOverwrite = true;
@@ -185,11 +304,12 @@ export function checkNYCConfiguration(packageJson: any, replaceFlag: boolean): [
/**
* Check scripts
*
* @param rules Rules for check
* @param packageJson package.json to check scripts in
* @param replaceFlag Whether or not to replace scripts
* @return Whether or not the package.json was changed
*/
export function checkScripts(packageJson: any, replaceFlag: boolean): boolean {
export function checkScripts(rules: Rules, packageJson: any, replaceFlag: boolean): boolean {
let packageJsonChanged = false;
// check if scripts is a map
@@ -199,19 +319,19 @@ export function checkScripts(packageJson: any, replaceFlag: boolean): boolean {
packageJsonChanged = true;
}
Object.keys(SCRIPTS).forEach((scriptName) => {
Object.keys(rules.scripts).forEach((scriptName) => {
const scriptToCheck = packageJson.scripts[scriptName];
// check if script exists
if (typeof scriptToCheck === 'undefined' || replaceFlag) {
packageJson.scripts[scriptName] = SCRIPTS[scriptName];
packageJson.scripts[scriptName] = rules.scripts[scriptName];
packageJsonChanged = true;
consoleInfo(`Added '${scriptName}' script to 'package.json'.`);
} else if (typeof scriptToCheck === 'string' && scriptToCheck !== SCRIPTS[scriptName]) {
} else if (typeof scriptToCheck === 'string' && scriptToCheck !== rules.scripts[scriptName]) {
consoleWarn(`Script '${scriptName}' in 'package.json' should be:
"${SCRIPTS[scriptName].replace('\n', '\\n')}".`);
'${rules.scripts[scriptName].replace('\n', '\\n')}'.`);
}
});
@@ -249,9 +369,10 @@ export function checkContributors(packageJson: any): void {
/**
* Check CI config
*
* @param rules Rules for check
* @param path Path to CI config
*/
export function checkCIConfig(path: string): void {
export function checkCIConfig(rules: Rules, path: string): void {
// check CI config if it exists
const pathToCiConfig = resolve(path, '.gitlab-ci.yml');
if (existsSync(pathToCiConfig)) {
@@ -261,24 +382,201 @@ export function checkCIConfig(path: string): void {
const ciConfig = parse(buffer.toString());
// check entries
for (const entry in EXPECTED_CI_CONFIG) {
if (!EXPECTED_CI_CONFIG.hasOwnProperty(entry)) {
for (const entry in rules.ciConfig) {
if (!rules.ciConfig.hasOwnProperty(entry)) {
continue;
}
if (!isDeepStrictEqual(EXPECTED_CI_CONFIG[entry], ciConfig[entry])) {
consoleWarn(`Entry '${entry}' in ${pathToCiConfig} is incorrect. Expected value is:`);
consoleInfo(stringify((() => {
const completeEntry: any = {};
completeEntry[entry] = EXPECTED_CI_CONFIG[entry];
return completeEntry;
})()));
if (!isDeepStrictEqual(rules.ciConfig[entry], ciConfig[entry])) {
const completeEntry: any = {};
completeEntry[entry] = rules.ciConfig[entry];
consoleWarn(`Entry '${entry}' in '${pathToCiConfig}' is incorrect. Expected value is:
${stringify(completeEntry)}`);
}
}
} catch (error) {
consoleWarn(`Could not parse ${pathToCiConfig} because of '${error.message}'.
Please ensure consistency of CI config manually.`);
consoleInfo(stringify(EXPECTED_CI_CONFIG));
Please ensure consistency of CI config manually.
${stringify(rules.ciConfig)}`);
}
}
}
/**
* Get configuration
*
* @param packageJson package.json to get configuration from
*/
export function getConfiguration(packageJson: any): Configuration {
const defaultConfiguration: Configuration = {
forPackaging: true,
hasCli: true,
ignoreScripts: [],
serverSide: true,
standardBuild: true,
standardDocumentation: true,
};
if (typeof packageJson.openstappsConfiguration !== 'undefined') {
return {
...defaultConfiguration,
...packageJson.openstappsConfiguration,
};
}
return defaultConfiguration;
}
/**
* Get rules for check
*
* @param configuration Configuration for check
*/
export function getRules(configuration: Configuration): Rules {
// expected dependencies
const dependencies: string[] = [];
// expected dev dependencies
const devDependencies = [
'conventional-changelog-cli',
'tslint',
'typescript:^3.4.0',
];
// files that need to be copied
const files = [
'.editorconfig',
join('templates', '.gitignore'),
join('templates', 'tsconfig.json'),
join('templates', 'tslint.json'),
];
// configuration for nyc to add to package.json
const nycConfiguration = {
all: true,
branches: 95,
'check-coverage': true,
exclude: [
'src/test/**/*.spec.ts',
'src/cli.ts',
],
extension: [
'.ts',
],
functions: 95,
include: [
'src',
],
lines: 95,
'per-file': true,
reporter: [
'html',
'text-summary',
],
statements: 95,
};
// expected scripts
const scripts: { [k: string]: string; } = {
/* tslint:disable-next-line:max-line-length */
'changelog': 'conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md && git commit -m \'docs: update changelog\'',
'check-configuration': 'openstapps-configuration',
};
// list of expected licenses
const licenses = [
'AGPL-3.0-only',
'GPL-3.0-only',
];
// expected values in CI config
const ciConfig = {
/* tslint:disable:object-literal-sort-keys */
image: 'registry.gitlab.com/openstapps/projectmanagement/node',
cache: {
key: '${CI_COMMIT_REF_SLUG}',
paths: [
'node_modules',
],
},
audit: {
allow_failure: true,
except: [
'schedules',
],
script: [
'npm audit',
],
stage: 'test',
},
'scheduled-audit': {
only: [
'schedules',
],
script: [
'npm audit',
],
stage: 'test',
},
pages: {
artifacts: {
'paths': [
'public',
],
},
only: [
'/^v[0-9]+\\.[0-9]+\\.[0-9]+$/',
],
script: [
'npm run documentation',
'mv docs public',
],
stage: 'deploy',
},
/* tslint:enable */
};
if (configuration.forPackaging) {
scripts.prepublishOnly = 'npm ci && npm run build';
files.push(
join('templates', '.npmignore'),
);
}
if (configuration.serverSide) {
dependencies.push('@types/node:^10.0.0');
}
if (configuration.standardBuild || configuration.hasCli) {
scripts.build = 'npm run tslint && npm run compile';
scripts.compile = 'rimraf lib && tsc';
devDependencies.push('rimraf');
if (configuration.hasCli) {
devDependencies.push('prepend-file-cli');
scripts.compile += ' && prepend lib/cli.js \'#!/usr/bin/env node\n\'';
}
}
if (configuration.standardDocumentation) {
devDependencies.push('typedoc');
/* tslint:disable-next-line:max-line-length */
scripts.documentation = 'typedoc --includeDeclarations --mode modules --out docs --readme README.md --listInvalidSymbolLinks src';
}
for (const ignoreScript of configuration.ignoreScripts) {
consoleInfo(`Ignoring script '${ignoreScript}'.`);
delete scripts[ignoreScript];
}
return {
ciConfig,
dependencies,
devDependencies,
files,
licenses,
nycConfiguration,
scripts,
};
}

View File

@@ -1,124 +0,0 @@
/*
* Copyright (C) 2018, 2019 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {join} from 'path';
/**
* Files that need to be copied
*/
export const NEEDED_FILES = [
'.editorconfig',
join('templates', '.gitignore'),
join('templates', '.npmignore'),
join('templates', 'tsconfig.json'),
join('templates', 'tslint.json'),
];
/**
* Configuration for nyc to add to package.json
*/
export const NYC_CONFIGURATION = {
all: true,
branches: 95,
'check-coverage': true,
exclude: [
'src/test/**/*.spec.ts',
'src/cli.ts',
],
extension: [
'.ts',
],
functions: 95,
include: [
'src',
],
lines: 95,
'per-file': true,
reporter: [
'html',
'text-summary',
],
statements: 95,
};
/**
* Map of expected scripts
*/
export const SCRIPTS: { [k: string]: string } = {
'build': 'npm run tslint && npm run compile',
/* tslint:disable-next-line:max-line-length */
'changelog': 'conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md && git commit -m \'docs: update changelog\'',
'check-configuration': 'openstapps-configuration',
'compile': 'rimraf lib && tsc && prepend lib/cli.js \'#!/usr/bin/env node\n\'',
/* tslint:disable-next-line:max-line-length */
'documentation': 'typedoc --includeDeclarations --mode modules --out docs --readme README.md --listInvalidSymbolLinks src',
'prepublishOnly': 'npm ci && npm run build',
};
/**
* List of expected licenses
*/
export const EXPECTED_LICENSES = [
'AGPL-3.0-only',
'GPL-3.0-only',
];
/* tslint:disable:object-literal-sort-keys */
/**
* Expected values in CI config
*/
export const EXPECTED_CI_CONFIG: any = {
image: 'registry.gitlab.com/openstapps/projectmanagement/node',
cache: {
key: '${CI_COMMIT_REF_SLUG}',
paths: [
'node_modules',
],
},
audit: {
allow_failure: true,
except: [
'schedules',
],
script: [
'npm audit',
],
stage: 'test',
},
'scheduled-audit': {
only: [
'schedules',
],
script: [
'npm audit',
],
stage: 'test',
},
pages: {
artifacts: {
'paths': [
'public',
],
},
only: [
'/^v[0-9]+\\.[0-9]+\\.[0-9]+$/',
],
script: [
'npm run documentation',
'mv docs public',
],
stage: 'deploy',
},
};
/* tslint:enable */