/* * 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 . */ import chalk from 'chalk'; import {execSync} from 'child_process'; import * as commander from 'commander'; import {copyFileSync, existsSync, readFileSync, writeFileSync} from 'fs'; import {join, resolve, sep} from 'path'; import {cwd} from 'process'; import {isDeepStrictEqual} from 'util'; import {parse, stringify} from 'yaml'; import {EXPECTED_CI_CONFIG, EXPECTED_LICENSES, NEEDED_FILES, NYC_CONFIGURATION, SCRIPTS} from './configuration'; /* tslint:disable:no-console */ /** * Wrapper for console.info that outputs every argument in cyan * @param args */ function consoleInfo(...args: string[]): void { args.forEach((arg) => { console.info('\n' + chalk.cyan(arg)); }); } /** * Wrapper for console.warn that outputs every argument in red * @param args */ function consoleWarn(...args: string[]): void { args.forEach((arg) => { console.warn('\n' + chalk.red.bold(arg)); }); } /** * Wrapper for console.log that outputs every argument in green * @param args */ function consoleLog(...args: string[]): void { args.forEach((arg) => { console.log('\n' + chalk.green.bold(arg)); }); } const currentWorkingDirectory = cwd(); let suggestOverwrite = false; // configure commander commander .version(JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json')).toString()).version) .option('-p, --path ', `Path of project to add files to (${currentWorkingDirectory})`, currentWorkingDirectory) .option('-r, --replace', 'Whether to replace existing files or not', false) .parse(process.argv); // make path absolute 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}".`); } // path to examined package.json const packageJsonPath = resolve(path, 'package.json'); // wheter or not the contents of the package.json were changed let packageJsonChanged = false; // read package.json in provided path const packageJson = JSON.parse(readFileSync(packageJsonPath).toString()); // check if provided path is this package if (packageJson.name === '@openstapps/configuration') { consoleInfo('I\'m not going to check myself!'); process.exit(0); } // 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 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))}`); } } }); // 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) || commander.replace) { 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; } } }); // check if nyc is a dependency if (typeof packageJson.devDependencies === 'object' && Object.keys(packageJson.devDependencies).indexOf('nyc') >= 0) { if (typeof packageJson.nyc === 'undefined' || commander.replace) { // add NYC configuration packageJson.nyc = NYC_CONFIGURATION; packageJsonChanged = true; consoleLog(`Added NYC configuration in "${packageJsonPath}".`); } else if (!isDeepStrictEqual(packageJson.nyc, NYC_CONFIGURATION)) { consoleInfo(`NYC configuration in '${packageJsonPath}' differs from the proposed one. Please check manually...`); suggestOverwrite = true; } } // 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' || commander.replace) { packageJson.scripts[scriptName] = SCRIPTS[scriptName]; packageJsonChanged = true; consoleInfo(`Added '${scriptName}' script to '${packageJsonPath}'.`); } else if (typeof scriptToCheck === 'string' && scriptToCheck !== SCRIPTS[scriptName]) { consoleWarn(`NPM script '${scriptName}' should be "${SCRIPTS[scriptName].replace('\n', '\\n')}".`); } }); 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 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)); } } if (packageJsonChanged) { writeFileSync(resolve(path, 'package.json'), JSON.stringify(packageJson, null, 2)); consoleLog(`Changes were written to "${packageJsonPath}".`); } if (suggestOverwrite) { consoleInfo(`You should consider to overwrite your configuration files and check for intended derivations: npm run check-configuration -- -r`); } consoleLog(`Done checking the configuration in '${path}'.`);