import chalk from 'chalk'; import {execSync} from 'child_process'; import {copyFileSync, existsSync, readFileSync} from 'fs'; import {join, resolve, sep} from 'path'; import {isDeepStrictEqual} from 'util'; import {parse, stringify} from 'yaml'; import {EXPECTED_CI_CONFIG, EXPECTED_LICENSES, NEEDED_FILES, NYC_CONFIGURATION, SCRIPTS} from './configuration'; /** * Wrapper for console.info that outputs every argument in cyan * * @param args Arguments to output */ export function consoleInfo(...args: string[]): void { args.forEach((arg) => { /* tslint:disable-next-line:no-console */ console.info('\n' + chalk.cyan(arg)); }); } /** * Wrapper for console.warn that outputs every argument in red * * @param args Arguments to output */ export function consoleWarn(...args: string[]): void { args.forEach((arg) => { /* tslint:disable-next-line:no-console */ console.warn('\n' + chalk.red.bold(arg)); }); } /** * Wrapper for console.log that outputs every argument in green * * @param args Arguments to output */ export function consoleLog(...args: string[]): void { args.forEach((arg) => { /* tslint:disable-next-line:no-console */ console.log('\n' + chalk.green.bold(arg)); }); } /** * Check that configuration files are extended * * @param path Path, where configuration files are located */ export function checkConfigurationFilesAreExtended(path: string): void { // check if configuration files are extended ['tsconfig.json', 'tslint.json'].forEach((file) => { const fileToCheck = resolve(path, file); const expectedPath = `./node_modules/@openstapps/configuration/${file}`; if (existsSync(fileToCheck)) { const configFile = JSON.parse(readFileSync(fileToCheck).toString()); const configFileExtended = ( typeof configFile.extends === 'string' && configFile.extends === expectedPath ) || ( file === 'tslint.json' && Array.isArray(configFile.extends) && configFile.extends.indexOf(expectedPath) >= 0 ); if (!configFileExtended) { consoleWarn(`File "${fileToCheck}" should extend "${expectedPath}"! Example: ${readFileSync(resolve(__dirname, '..', 'templates', 'template-' + file))}`); } } }); } /** * Check needed files * * @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 { let suggestOverwrite = false; // copy needed files NEEDED_FILES.forEach((file) => { let destinationFile = file; // remove templates directory for destination files if (destinationFile.indexOf('templates') === 0) { destinationFile = destinationFile.split(sep).slice(1).join(sep); file = join('templates', `template-${destinationFile}`); } const source = resolve(__dirname, '..', file); const destination = resolve(path, destinationFile); // check if file exists or replace flag is set if (!existsSync(destination) || replaceFlag) { copyFileSync(source, destination); consoleInfo(`Copied file "${source}" to "${destination}".`); } else if (destinationFile === '.npmignore') { const npmIgnore = readFileSync(destination).toString(); const ignoredPatterns = npmIgnore.split('\n'); let ignoresEverything = false; let unignoresDocs = false; for (const ignoredPattern of ignoredPatterns) { if (ignoredPattern === '/*') { ignoresEverything = true; } if (ignoredPattern === '!docs') { unignoresDocs = true; } } if (!ignoresEverything) { consoleWarn(`'.npmignore' should have '/*' as first pattern to ignore everything.`); suggestOverwrite = true; } if (unignoresDocs) { consoleWarn(`'.npmignore' contains '!docs' and thus the package will contain the documentation. Please double check that this is desired behavior since the docs can become huge: https://gitlab.com/openstapps/configuration/issues/11`); suggestOverwrite = true; } } }); return suggestOverwrite; } /** * Check licenses * * @param packageJson package.json to check license in */ export function checkLicenses(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(', ')}"!`); } } /** * Check NYC configuration * * @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] { let packageJsonChanged = false; let suggestOverwrite = false; // check if nyc is a dependency 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; packageJsonChanged = true; consoleLog(`Added NYC configuration in to 'package.json'.`); } else if (!isDeepStrictEqual(packageJson.nyc, NYC_CONFIGURATION)) { consoleInfo(`NYC configuration in 'package.json' differs from the proposed one. Please check manually.`); suggestOverwrite = true; } } return [packageJsonChanged, suggestOverwrite]; } /** * Check scripts * * @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 { let packageJsonChanged = false; // check if scripts is a map if (typeof packageJson.scripts !== 'object') { packageJson.scripts = {}; packageJsonChanged = true; } Object.keys(SCRIPTS).forEach((scriptName) => { const scriptToCheck = packageJson.scripts[scriptName]; // check if script exists if (typeof scriptToCheck === 'undefined' || replaceFlag) { packageJson.scripts[scriptName] = SCRIPTS[scriptName]; packageJsonChanged = true; consoleInfo(`Added '${scriptName}' script to 'package.json'.`); } else if (typeof scriptToCheck === 'string' && scriptToCheck !== SCRIPTS[scriptName]) { consoleWarn(`Script '${scriptName}' in 'package.json' should be: "${SCRIPTS[scriptName].replace('\n', '\\n')}".`); } }); return packageJsonChanged; } /** * Check contributors * * @param packageJson package.json to check contributors in */ export function checkContributors(packageJson: any): void { const execBuffer = execSync('git log --format=\'%aN\' | sort -u'); for (let author of execBuffer.toString().split('\n')) { author = author.trim(); if (author === '') { continue; } let authorIsAttributed = false; authorIsAttributed = authorIsAttributed || (typeof packageJson.author === 'string' && packageJson.author.indexOf(author) >= 0) || (Array.isArray(packageJson.contributors) && packageJson.contributors.find((contributor: string) => { return contributor.indexOf(author) >= 0; })); if (!authorIsAttributed) { consoleWarn(`'${author}' should be attributed as author or contributor.`); } } } /** * Check CI config * * @param path Path to CI config */ export function checkCIConfig(path: string): void { // check CI config if it exists const pathToCiConfig = resolve(path, '.gitlab-ci.yml'); if (existsSync(pathToCiConfig)) { // read CI config const buffer = readFileSync(pathToCiConfig); try { const ciConfig = parse(buffer.toString()); // check entries for (const entry in EXPECTED_CI_CONFIG) { if (!EXPECTED_CI_CONFIG.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; })())); } } } catch (error) { consoleWarn(`Could not parse ${pathToCiConfig} because of '${error.message}'. Please ensure consistency of CI config manually.`); consoleInfo(stringify(EXPECTED_CI_CONFIG)); } } }