diff --git a/src/cli.ts b/src/cli.ts index e4301960..f4372501 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -12,52 +12,25 @@ * 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 {existsSync, readFileSync, writeFileSync} from 'fs'; +import {resolve} 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)); - }); -} +import { + checkCIConfig, + checkConfigurationFilesAreExtended, + checkContributors, + checkLicenses, + checkNeededFiles, + checkNYCConfiguration, + checkScripts, + consoleInfo, + consoleLog, +} from './common'; +// current working directory const currentWorkingDirectory = cwd(); -let suggestOverwrite = false; - // configure commander commander .version(JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json')).toString()).version) @@ -76,9 +49,12 @@ if (!existsSync(resolve(path, 'package.json'))) { // path to examined package.json const packageJsonPath = resolve(path, 'package.json'); -// wheter or not the contents of the package.json were changed +// 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()); @@ -88,172 +64,22 @@ if (packageJson.name === '@openstapps/configuration') { 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(', ')}"!`); -} +checkLicenses(packageJson); -// check if configuration files are extended -['tsconfig.json', 'tslint.json'].forEach((file) => { - const fileToCheck = resolve(path, file); - const expectedPath = `./node_modules/@openstapps/configuration/${file}`; +checkConfigurationFilesAreExtended(path); - if (existsSync(fileToCheck)) { - const configFile = JSON.parse(readFileSync(fileToCheck).toString()); +suggestOverwrite = suggestOverwrite || checkNeededFiles(path, commander.replace); - const configFileExtended = - (typeof configFile.extends === 'string' && configFile.extends === expectedPath) - || (file === 'tslint.json' && Array.isArray(configFile.extends) && configFile.extends.indexOf(expectedPath) >= 0); +const checkedNYCConfiguration = checkNYCConfiguration(packageJson, commander.replace); - if (!configFileExtended) { - consoleWarn(` -File "${fileToCheck}" should extend "${expectedPath}"! +packageJsonChanged = packageJsonChanged || checkedNYCConfiguration[0]; +suggestOverwrite = suggestOverwrite || checkedNYCConfiguration[1]; -Example: -${readFileSync(resolve(__dirname, '..', 'templates', 'template-' + file))}`); - } - } -}); +packageJsonChanged = packageJsonChanged || checkScripts(packageJson, commander.replace); -// copy needed files -NEEDED_FILES.forEach((file) => { - let destinationFile = file; +checkContributors(packageJson); - // 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)); - } -} +checkCIConfig(path); if (packageJsonChanged) { writeFileSync(resolve(path, 'package.json'), JSON.stringify(packageJson, null, 2)); diff --git a/src/common.ts b/src/common.ts new file mode 100644 index 00000000..276ea620 --- /dev/null +++ b/src/common.ts @@ -0,0 +1,284 @@ +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)); + } + } +}