diff --git a/packages/@best/cli/package.json b/packages/@best/cli/package.json index 64d120ad..9cfe32c5 100644 --- a/packages/@best/cli/package.json +++ b/packages/@best/cli/package.json @@ -24,7 +24,7 @@ "globby": "~9.2.0", "micromatch": "~3.1.10", "rimraf": "~2.6.2", - "simple-git": "~1.92.0", + "simple-git": "~1.113.0", "simple-statistics": "^6.0.1", "yargs": "~11.0.0" } diff --git a/packages/@best/config/package.json b/packages/@best/config/package.json index 6cb65000..9e8e7c3d 100644 --- a/packages/@best/config/package.json +++ b/packages/@best/config/package.json @@ -10,6 +10,7 @@ "dependencies": { "@best/regex-util": "0.7.1", "@best/utils": "0.7.1", - "chalk": "~2.4.2" + "chalk": "~2.4.2", + "simple-git": "~1.113.0" } } diff --git a/packages/@best/config/src/__tests__/git.spec.ts b/packages/@best/config/src/__tests__/git.spec.ts new file mode 100644 index 00000000..9c0d6f27 --- /dev/null +++ b/packages/@best/config/src/__tests__/git.spec.ts @@ -0,0 +1,10 @@ + +import { getGitInfo } from "../utils/git" + +describe('config file resolution', () => { + test('throw if not config is found in the directory', async () => { + const gitInfo = await getGitInfo(process.cwd()); + expect(gitInfo).toBeDefined(); + expect(gitInfo.repo).toBeDefined(); + }); +}); diff --git a/packages/@best/config/src/constants.ts b/packages/@best/config/src/constants.ts deleted file mode 100644 index 611e43b5..00000000 --- a/packages/@best/config/src/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -import path from 'path'; - -export const NODE_MODULES = path.sep + 'node_modules' + path.sep; -export const PACKAGE_JSON = 'package.json'; -export const BEST_CONFIG = 'best.config.js'; diff --git a/packages/@best/config/src/git.ts b/packages/@best/config/src/git.ts deleted file mode 100644 index 64a295ef..00000000 --- a/packages/@best/config/src/git.ts +++ /dev/null @@ -1,103 +0,0 @@ -import childProcess from 'child_process'; - -async function getCurrentHash(cwd: string):Promise { - return new Promise((resolve, reject) => { - const args = ['log', '--pretty=format:%h', '-n', '1']; - const child = childProcess.spawn('git', args, { cwd }); - let stdout = ''; - let stderr = ''; - child.stdout.on('data', data => (stdout += data)); - child.stderr.on('data', data => (stderr += data)); - child.on('error', e => reject(e)); - child.on('close', code => { - if (code === 0) { - resolve(stdout.trim()); - } else { - reject(code + ': ' + stderr); - } - }); - }); -} - -async function getDateOfCurrentHash(cwd: string): Promise { - return new Promise((resolve, reject) => { - const args = ['log', '--pretty=format:%cd', '-n', '1']; - const child = childProcess.spawn('git', args, { cwd }); - let stdout = ''; - let stderr = ''; - child.stdout.on('data', data => (stdout += data)); - child.stderr.on('data', data => (stderr += data)); - child.on('error', e => reject(e)); - child.on('close', code => { - if (code === 0) { - resolve(stdout.trim()); - } else { - reject(code + ': ' + stderr); - } - }); - }) -} - -function hasLocalChanges(cwd: string): Promise { - return new Promise((resolve, reject) => { - const args = ['diff', '--no-ext-diff', '--quiet']; - const child = childProcess.spawn('git', args, { cwd }); - child.on('error', e => reject(e)); - child.on('close', code => resolve(code === 1)); - }); -} - -function getBranch(cwd: string): Promise { - return new Promise((resolve, reject) => { - const args = ['rev-parse', '--abbrev-ref', 'HEAD']; - const child = childProcess.spawn('git', args, { cwd }); - let stdout = ''; - child.stdout.on('data', data => (stdout += data)); - child.on('error', e => reject(e)); - child.on('close', () => resolve(stdout.trim())); - }); -} - -function getRepository(cwd: string): Promise<{ owner: string, repo: string }> { - return new Promise((resolve, reject) => { - const args = ['ls-remote', '--get-url']; - const child = childProcess.spawn('git', args, { cwd }); - let stdout = ''; - let stderr = ''; - child.stdout.on('data', data => (stdout += data)); - child.stderr.on('data', data => (stderr += data)); - child.on('error', e => reject(e)); - child.on('close', code => { - if (code === 0) { - const rawValue = stdout.trim(); - const [owner, repo] = rawValue - .split(':') - .pop()! - .split('.git')[0] - .split('/'); - resolve({ owner, repo }); - } else { - reject(code + ': ' + stderr); - } - }); - }); -} - -interface GitOptions { - rootDir?: string, -} - -export async function addGitInformation(options: GitOptions) { - const cwd = options.rootDir; - if (cwd) { - const [gitCommit, gitCommitDate, gitLocalChanges, gitBranch, gitRepository] = await Promise.all([ - getCurrentHash(cwd), - getDateOfCurrentHash(cwd), - hasLocalChanges(cwd), - getBranch(cwd), - getRepository(cwd), - ]); - - return Object.assign(options, { gitCommit, gitCommitDate, gitLocalChanges, gitBranch, gitRepository }); - } -} diff --git a/packages/@best/config/src/index.ts b/packages/@best/config/src/index.ts index f2ea97db..dc32261c 100644 --- a/packages/@best/config/src/index.ts +++ b/packages/@best/config/src/index.ts @@ -1,230 +1,16 @@ -import fs from 'fs'; -import path from 'path'; -import chalk from 'chalk'; -import { replacePathSepForRegex } from '@best/regex-util'; -import DEFAULT_CONFIG from './defaults'; -import { addGitInformation } from './git'; -import { PACKAGE_JSON, BEST_CONFIG } from './constants'; -import { BestCliOptions } from './types'; +import { resolveConfigPath, readConfigAndSetRootDir, ensureNoDuplicateConfigs } from './utils/resolve-config'; +import { getGitInfo, GitInfo } from './utils/git'; +import { normalizeConfig, normalizeRegexPattern, normalizeRootDirPattern } from './utils/normalize'; +import { BestCliOptions, DefaultProjectOptions, GlobalConfig, ProjectConfig } from './types'; export { BestCliOptions }; -const TARGET_COMMIT = process.env.TARGET_COMMIT; -const BASE_COMMIT = process.env.BASE_COMMIT; -const specialArgs = ['_', '$0', 'h', 'help', 'config']; -const isFile = (filePath:string) => fs.existsSync(filePath) && !fs.lstatSync(filePath).isDirectory(); +function generateProjectConfigs(options: DefaultProjectOptions, isRoot: boolean, gitInfo?: GitInfo): { projectConfig: ProjectConfig, globalConfig: GlobalConfig | undefined } { + let globalConfig: GlobalConfig | undefined = undefined; + let projectConfig: ProjectConfig; -function readConfigAndSetRootDir(configPath: string) { - const isJSON = configPath.endsWith('.json'); - let configObject; - try { - configObject = require(configPath); - } catch (error) { - if (isJSON) { - throw new Error(`Best: Failed to parse config file ${configPath}\n`); - } else { - throw error; - } - } - - if (configPath.endsWith(PACKAGE_JSON)) { - if (!configObject.best) { - throw new Error(`No "best" section has been found in ${configPath}`); - } - - configObject = configObject.best; - } - - if (!configObject) { - throw new Error("Couldn't find any configuration for Best."); - } - - if (configObject.rootDir) { - // We don't touch it if it has an absolute path specified - if (!path.isAbsolute(configObject.rootDir)) { - // otherwise, we'll resolve it relative to the file's __dirname - configObject.rootDir = path.resolve(path.dirname(configPath), configObject.rootDir); - } - } else { - // If rootDir is not there, we'll set it to this file's __dirname - configObject.rootDir = path.dirname(configPath); - } - - return configObject; -} - -function resolveConfigPathByTraversing(pathToResolve: string, initialPath: string, cwd: string): string { - const bestConfig = path.resolve(pathToResolve, BEST_CONFIG); - if (isFile(bestConfig)) { - return bestConfig; - } - - const packageJson = path.resolve(pathToResolve, PACKAGE_JSON); - if (isFile(packageJson)) { - return packageJson; - } - - if (pathToResolve === path.dirname(pathToResolve)) { - throw new Error(`No config found in ${initialPath}`); - } - - // go up a level and try it again - return resolveConfigPathByTraversing(path.dirname(pathToResolve), initialPath, cwd); -} - -function resolveConfigPath(pathToResolve: string, cwd: string) { - const absolutePath = path.isAbsolute(pathToResolve) ? pathToResolve : path.resolve(cwd, pathToResolve); - if (isFile(absolutePath)) { - return absolutePath; - } - - return resolveConfigPathByTraversing(absolutePath, pathToResolve, cwd); -} - -function setFromArgs(initialOptions: any, argsCLI: any) { - const argvToOptions = Object.keys(argsCLI) - .filter(key => argsCLI[key] !== undefined && specialArgs.indexOf(key) === -1) - .reduce((options: any, key: string) => { - switch (key) { - case 'iterations': - options.benchmarkIterations = argsCLI[key]; - break; - case 'compareStats': - options[key] = argsCLI[key].filter(Boolean); - break; - default: - options[key] = argsCLI[key]; - break; - } - return options; - }, {}); - - return Object.assign({}, initialOptions, argvToOptions); -} - -function normalizeRootDir(options: any): any { - // Assert that there *is* a rootDir - if (!options.hasOwnProperty('rootDir')) { - throw new Error(` Configuration option ${chalk.bold('rootDir')} must be specified.`); - } - - options.rootDir = path.normalize(options.rootDir); - return options; -} - -function buildTestPathPattern(argsCLI: any) { - const patterns = []; - if (argsCLI._) { - patterns.push(...argsCLI._); - } - - if (argsCLI.testPathPattern) { - patterns.push(...argsCLI.testPathPattern); - } - - const testPathPattern = patterns.map(replacePathSepForRegex).join('|'); - return testPathPattern; -} - -function normalizeRootDirPattern(str: string, rootDir: string) { - return str.replace(//g, rootDir); -} - -function normalizeUnmockedModulePathPatterns(options: any, key: string) { - return options[key].map((pattern: any) => replacePathSepForRegex(normalizeRootDirPattern(pattern, options.rootDir))); -} - -function normalizeObjectPathPatterns(options: any, rootDir: string) { - return Object.keys(options).reduce((m: any, key) => { - const value = options[key]; - if (typeof value === 'string') { - m[key] = normalizeRootDirPattern(value, rootDir); - } else { - m[key] = value; - } - return m; - }, {}); -} - -function normalizePlugins(plugins: any, { rootDir }: any) { - return plugins.map((plugin: any) => { - if (typeof plugin === 'string') { - return normalizeRootDirPattern(plugin, rootDir); - } else if (Array.isArray(plugin)) { - return [plugin[0], normalizeObjectPathPatterns(plugin[1], rootDir)]; - } - return plugin; - }); -} - -function normalizeRunnerConfig(runnerConfig: any, { runner }: any) { - if (!Array.isArray(runnerConfig)) { - runnerConfig = [runnerConfig]; - } - - const defaultRunners = runnerConfig.filter((c: any) => c.name === undefined || c.name === 'default'); - if (defaultRunners > 1) { - throw new Error('Wrong configuration: More than one default configuration declared'); - } - - const match = runnerConfig.find((c: any) => c.name === runner) || defaultRunners[0] || {}; - - return match; -} - -function normalizeCommits([base, target]: [string, string]) { - base = (base || BASE_COMMIT || ''); - target = (target || TARGET_COMMIT || ''); - return [base.slice(0, 7), target.slice(0, 7)]; -} - -function normalizePattern(names: any) { - if (typeof names === 'string') { - names = names.split(','); - } - if (names instanceof Array) { - names = names.map(name => name.replace(/\*/g, '.*')); - names = new RegExp(`^(${names.join('|')})$`); - } - if (!(names instanceof RegExp)) { - throw new Error(` Names must be provided as a string, array or regular expression.`); - } - return typeof names === 'string' ? new RegExp(names) : names; -} - -function normalize(options: any, argsCLI: any) { - options = normalizeRootDir(setFromArgs(options, argsCLI)); - const newOptions = Object.assign({}, DEFAULT_CONFIG); - Object.keys(options).reduce((newOpts: any, key) => { - let value = newOpts[key]; - switch (key) { - case 'projects': - value = normalizeUnmockedModulePathPatterns(options, key); - break; - case 'plugins': - value = normalizePlugins(options[key], options); - break; - case 'runnerConfig': - value = normalizeRunnerConfig(options[key], options); - break; - case 'compareStats': - value = options[key].length ? normalizeCommits(options[key]) : undefined; - break; - default: - value = options[key]; - } - newOptions[key] = value; - return newOpts; - }, newOptions); - - newOptions.nonFlagArgs = argsCLI._; - newOptions.testPathPattern = buildTestPathPattern(argsCLI); - return newOptions; -} - -function _getConfigs(options: any) { - return { - globalConfig: Object.freeze({ + if (isRoot) { + globalConfig = Object.freeze({ gitIntegration: options.gitIntegration, detectLeaks: options.detectLeaks, compareStats: options.compareStats, @@ -238,126 +24,110 @@ function _getConfigs(options: any) { testNamePattern: options.testNamePattern, testPathPattern: options.testPathPattern, verbose: options.verbose, + gitInfo: gitInfo, gitCommit: options.gitCommit, gitCommitDate: options.gitCommitDate, gitLocalChanges: options.gitLocalChanges, gitBranch: options.gitBranch, gitRepository: options.gitRepository, normalize: options.normalize, - outputMetricPattern: normalizePattern(options.outputMetricNames), + outputMetricPattern: normalizeRegexPattern(options.outputMetricNames), outputTotals: options.outputTotals, outputHistograms: options.outputHistograms, - outputHistogramPattern: normalizePattern(options.outputHistogramNames), + outputHistogramPattern: normalizeRegexPattern(options.outputHistogramNames), histogramQuantileRange: options.histogramQuantileRange, histogramMaxWidth: options.histogramMaxWidth, openPages: options.openPages, - }), - projectConfig: Object.freeze({ - cache: options.cache, - cacheDirectory: options.cacheDirectory, - useHttp: options.useHttp, - cwd: options.cwd, - detectLeaks: options.detectLeaks, - displayName: options.displayName, - globals: options.globals, - moduleDirectories: options.moduleDirectories, - moduleFileExtensions: options.moduleFileExtensions, - moduleLoader: options.moduleLoader, - moduleNameMapper: options.moduleNameMapper, - modulePathIgnorePatterns: options.modulePathIgnorePatterns, - modulePaths: options.modulePaths, - name: options.name, - plugins: options.plugins, - rootDir: options.rootDir, - roots: options.roots, - - projectName: options.projectName, - benchmarkRunner: options.runnerConfig.runner || options.runner, - benchmarkRunnerConfig: options.runnerConfig.config || options.runnerConfig, - benchmarkEnvironment: options.benchmarkEnvironment, - benchmarkEnvironmentOptions: options.benchmarkEnvironmentOptions, - benchmarkMaxDuration: options.benchmarkMaxDuration, - benchmarkMinIterations: options.benchmarkMinIterations, - benchmarkIterations: options.benchmarkIterations, - benchmarkOnClient: options.benchmarkOnClient, - benchmarkOutput: normalizeRootDirPattern(options.benchmarkOutput, options.rootDir), - - testMatch: options.testMatch, - testPathIgnorePatterns: options.testPathIgnorePatterns, - testRegex: options.testRegex, - testURL: options.testURL, - transform: options.transform, - transformIgnorePatterns: options.transformIgnorePatterns, + }); + } + + projectConfig = Object.freeze({ + cache: options.cache, + cacheDirectory: options.cacheDirectory, + useHttp: options.useHttp, + cwd: options.cwd, + detectLeaks: options.detectLeaks, + displayName: options.displayName, + globals: options.globals, + moduleDirectories: options.moduleDirectories, + moduleFileExtensions: options.moduleFileExtensions, + moduleLoader: options.moduleLoader, + moduleNameMapper: options.moduleNameMapper, + modulePathIgnorePatterns: options.modulePathIgnorePatterns, + modulePaths: options.modulePaths, + name: options.name, + plugins: options.plugins, + rootDir: options.rootDir, + roots: options.roots, + + projectName: options.projectName, + benchmarkRunner: options.runnerConfig.runner || options.runner, + benchmarkRunnerConfig: options.runnerConfig.config || options.runnerConfig, + benchmarkEnvironment: options.benchmarkEnvironment, + benchmarkEnvironmentOptions: options.benchmarkEnvironmentOptions, + benchmarkMaxDuration: options.benchmarkMaxDuration, + benchmarkMinIterations: options.benchmarkMinIterations, + benchmarkIterations: options.benchmarkIterations, + benchmarkOnClient: options.benchmarkOnClient, + benchmarkOutput: normalizeRootDirPattern(options.benchmarkOutput, options.rootDir), + + testMatch: options.testMatch, + testPathIgnorePatterns: options.testPathIgnorePatterns, + testRegex: options.testRegex, + testURL: options.testURL, + transform: options.transform, + transformIgnorePatterns: options.transformIgnorePatterns, + + samplesQuantileThreshold: options.samplesQuantileThreshold, + }); - samplesQuantileThreshold: options.samplesQuantileThreshold, - }), - }; + return { globalConfig, projectConfig }; } -const ensureNoDuplicateConfigs = (parsedConfigs: any, projects: string[]) => { - const configPathSet = new Set(); - - for (const { configPath } of parsedConfigs) { - if (configPathSet.has(configPath)) { - let message = 'One or more specified projects share the same config file\n'; - - parsedConfigs.forEach((projectConfig: any, index: number) => { - message = - message + - '\nProject: "' + - projects[index] + - '"\nConfig: "' + - String(projectConfig.configPath) + - '"'; - }); - throw new Error(message); - } - - if (configPath !== null) { - configPathSet.add(configPath); - } - } -}; - -export async function readConfig(argsCLI: any, packageRoot: string, parentConfigPath?: string) { - const customConfigPath = argsCLI.config ? argsCLI.config : packageRoot; - const configPath = resolveConfigPath(customConfigPath, process.cwd()); +export async function readConfig(cliOptions: BestCliOptions, packageRoot: string, parentConfigPath?: string): Promise<{ configPath: string, globalConfig?: GlobalConfig, projectConfig: ProjectConfig }> { + const configPath = resolveConfigPath(cliOptions.config ? cliOptions.config : packageRoot, process.cwd()); const rawOptions = readConfigAndSetRootDir(configPath); - const options = normalize(rawOptions, argsCLI); + const options = normalizeConfig(rawOptions, cliOptions); + let gitConfig; - // We assume all projects are within the same mono-repo - // No need to get version control status again + // If we have a parent Config path, we are in a nested/project best config if (!parentConfigPath) { try { - await addGitInformation(options); + gitConfig = await getGitInfo(options.rootDir); } catch (e) { console.log('[WARN] - Unable to get git information'); /* Unable to get git info */ } } - const { globalConfig, projectConfig } = _getConfigs(options); + const { globalConfig, projectConfig } = generateProjectConfigs(options, !parentConfigPath, gitConfig); return { configPath, globalConfig, projectConfig }; } -export async function getConfigs(projectsFromCLIArgs: string[], argv: BestCliOptions) { - let globalConfig; - let configs: any = []; - let projects = projectsFromCLIArgs; - let configPath: any; +export async function getConfigs(projectsFromCLIArgs: string[], cliOptions: BestCliOptions): Promise<{ globalConfig: GlobalConfig, configs: ProjectConfig[] }> { + let globalConfig: GlobalConfig | undefined; + let configs: ProjectConfig[] = []; + let projects: string[] = []; + let configPath: string; + // We first read the main config if (projectsFromCLIArgs.length === 1) { - const parsedConfig = await readConfig(argv, projects[0]); - configPath = parsedConfig.configPath; + const parsedConfigs = await readConfig(cliOptions, projects[0]); + configPath = parsedConfigs.configPath; + const { globalConfig: parsedGlobalConfig } = parsedConfigs; - if (parsedConfig.globalConfig.projects) { + if (!parsedGlobalConfig) { + throw new Error('Global configuration must exist'); + } + + if (parsedGlobalConfig && parsedGlobalConfig.projects) { // If this was a single project, and its config has `projects` // settings, use that value instead. - projects = parsedConfig.globalConfig.projects; + projects = parsedGlobalConfig.projects; } - globalConfig = parsedConfig.globalConfig; - configs = [parsedConfig.projectConfig]; + globalConfig = parsedGlobalConfig; + configs = [parsedConfigs.projectConfig]; if (globalConfig.projects && globalConfig.projects.length) { // Even though we had one project in CLI args, there might be more @@ -366,18 +136,15 @@ export async function getConfigs(projectsFromCLIArgs: string[], argv: BestCliOpt } } - if (projects.length > 0) { - const parsedConfigs = await Promise.all(projects.map(root => readConfig(argv, root, configPath))); + if (projects.length > 1) { + const parsedConfigs = await Promise.all(projects.map(root => readConfig(cliOptions, root, configPath))); ensureNoDuplicateConfigs(parsedConfigs, projects); configs = parsedConfigs.map(({ projectConfig }) => projectConfig); + globalConfig = parsedConfigs[0].globalConfig; + } - if (!globalConfig) { - globalConfig = parsedConfigs[0].globalConfig; - } - - if (!globalConfig || !configs.length) { - throw new Error('best: No configuration found for any project.'); - } + if (!globalConfig) { + throw new Error('Global configuration not defined'); } return { diff --git a/packages/@best/config/src/types.ts b/packages/@best/config/src/types.ts index fc1a340d..29647095 100644 --- a/packages/@best/config/src/types.ts +++ b/packages/@best/config/src/types.ts @@ -1,4 +1,12 @@ -export interface BestBuildOptions { +export interface RawBestConfig { + [key: string]: any; + rootDir: string; + benchmarkIterations? : number; + compareStats?: string[]; + +} + +export interface DefaultProjectOptions { [key: string]: any, cache: boolean, cacheDirectory: string, @@ -6,7 +14,7 @@ export interface BestBuildOptions { openPages: boolean, moduleDirectories: string[], moduleFileExtensions: string[], - moduleNameMapper: {[moduleName:string]: string }, + moduleNameMapper: { [moduleName:string]: string }, modulePathIgnorePatterns: string[], runner: string, runnerConfig: any, @@ -26,10 +34,11 @@ export interface BestBuildOptions { outputHistogramNames: string, histogramQuantileRange: [number, number], histogramMaxWidth: number, - rootDir?: string + rootDir: string } export interface BestCliOptions { + [key: string]: any, _: string[], help: boolean, clearCache: boolean, @@ -44,3 +53,13 @@ export interface BestCliOptions { iterations: number, compareStats: string[] | undefined }; + + +export interface GlobalConfig { + gitIntegration: boolean; + projects: string[]; +} + +export interface ProjectConfig { + +} diff --git a/packages/@best/config/src/utils/constants.ts b/packages/@best/config/src/utils/constants.ts new file mode 100644 index 00000000..e68d49ca --- /dev/null +++ b/packages/@best/config/src/utils/constants.ts @@ -0,0 +1,2 @@ +export const PACKAGE_JSON = 'package.json'; +export const BEST_CONFIG = 'best.config.js'; diff --git a/packages/@best/config/src/defaults.ts b/packages/@best/config/src/utils/defaults.ts similarity index 92% rename from packages/@best/config/src/defaults.ts rename to packages/@best/config/src/utils/defaults.ts index 88ed1f0e..92e4c050 100644 --- a/packages/@best/config/src/defaults.ts +++ b/packages/@best/config/src/utils/defaults.ts @@ -1,7 +1,7 @@ import { cacheDirectory } from '@best/utils'; -import { BestBuildOptions } from "./types"; +import { DefaultProjectOptions } from "../types"; -const defaultOptions: BestBuildOptions = { +const defaultOptions: DefaultProjectOptions = { cache: true, cacheDirectory: cacheDirectory(), useHttp: true, @@ -44,6 +44,8 @@ const defaultOptions: BestBuildOptions = { // If histograms are shown, make them a limited number of characters wide. histogramMaxWidth: 50, + + rootDir: process.cwd(), }; export default defaultOptions; diff --git a/packages/@best/config/src/utils/git.ts b/packages/@best/config/src/utils/git.ts new file mode 100644 index 00000000..49854350 --- /dev/null +++ b/packages/@best/config/src/utils/git.ts @@ -0,0 +1,61 @@ +import SimpleGit from "simple-git/promise"; + +// TODO: Remove this once the library fixes its types +declare module 'simple-git/promise' { + interface SimpleGit { + listRemote(options: string[]): Promise + } +} + +async function getCurrentHashAndDate(git: SimpleGit.SimpleGit):Promise< { hash: string, date: string } > { + const { latest } = await git.log(); + const date = latest.date; + const hash = latest.hash.slice(0, 7); + return { hash, date }; +} + +async function hasLocalChanges(git: SimpleGit.SimpleGit): Promise { + const diff = await git.diffSummary(); + return diff.files && diff.files.length > 0; +} + +function getBranch(git: SimpleGit.SimpleGit): Promise { + return git.revparse(['--abbrev-ref', 'HEAD']); +} + +async function getRepository(git: SimpleGit.SimpleGit): Promise<{ owner: string, repo: string }> { + const url = await git.listRemote(['--get-url']); + const matches = url.trim().match(/^.+[:\/]([a-zA-Z]+)\/([a-zA-Z]+).git$/); + if (!matches) { + throw new Error('Unable to parse git repo'); + } + + const [, owner, repo] = matches; + return { owner, repo}; +} + +export interface GitInfo { + lastCommit: { hash: string, date: string } + localChanges: boolean, + branch: string, + repo: { + owner: string, + repo: string + } +} + +export async function getGitInfo(baseDir?: string): Promise { + const git = SimpleGit(baseDir); + const isRepo = await git.checkIsRepo(); + + if (isRepo) { + const [lastCommit, localChanges, branch, repo] = await Promise.all([ + getCurrentHashAndDate(git), + hasLocalChanges(git), + getBranch(git), + getRepository(git), + ]); + + return { lastCommit, localChanges, branch, repo }; + } +} diff --git a/packages/@best/config/src/utils/normalize.ts b/packages/@best/config/src/utils/normalize.ts new file mode 100644 index 00000000..2a9a2db7 --- /dev/null +++ b/packages/@best/config/src/utils/normalize.ts @@ -0,0 +1,153 @@ +import path from 'path'; +import chalk from 'chalk'; +import { BestCliOptions, RawBestConfig, DefaultProjectOptions } from '../types'; +import DEFAULT_CONFIG from './defaults'; +import { replacePathSepForRegex } from '@best/regex-util'; + +const TARGET_COMMIT = process.env.TARGET_COMMIT; +const BASE_COMMIT = process.env.BASE_COMMIT; + +function normalizeModulePathPatterns(options: any, key: string) { + return options[key].map((pattern: any) => replacePathSepForRegex(normalizeRootDirPattern(pattern, options.rootDir))); +} + +function normalizeRunnerConfig(runnerConfig: any, { runner }: any) { + if (!Array.isArray(runnerConfig)) { + runnerConfig = [runnerConfig]; + } + + const defaultRunners = runnerConfig.filter((c: any) => c.name === undefined || c.name === 'default'); + if (defaultRunners > 1) { + throw new Error('Wrong configuration: More than one default configuration declared'); + } + + const match = runnerConfig.find((c: any) => c.name === runner) || defaultRunners[0] || {}; + + return match; +} + +function setCliOptionOverrides(initialOptions: RawBestConfig, argsCLI: BestCliOptions): RawBestConfig { + const argvToOptions = Object.keys(argsCLI) + .reduce((options: any, key: string) => { + switch (key) { + case 'iterations': + options.benchmarkIterations = argsCLI[key]; + break; + case 'compareStats': + options.compareStats = argsCLI.compareStats && argsCLI.compareStats.filter(Boolean); + break; + default: + options[key] = argsCLI[key]; + break; + } + return options; + }, {}); + + return Object.assign({}, initialOptions, argvToOptions); +} +function normalizeObjectPathPatterns(options: { [key: string]: any }, rootDir: string) { + return Object.keys(options).reduce((m: any, key) => { + const value = options[key]; + if (typeof value === 'string') { + m[key] = normalizeRootDirPattern(value, rootDir); + } else { + m[key] = value; + } + return m; + }, {}); +} + +function normalizePlugins(plugins: any, { rootDir }: RawBestConfig) { + return plugins.map((plugin: any) => { + if (typeof plugin === 'string') { + return normalizeRootDirPattern(plugin, rootDir); + } else if (Array.isArray(plugin)) { + return [normalizeRootDirPattern(plugin[0], rootDir), normalizeObjectPathPatterns(plugin[1], rootDir)]; + } + return plugin; + }); +} + +function normalizeRootDir(options: any): any { + // Assert that there *is* a rootDir + if (!options.hasOwnProperty('rootDir')) { + throw new Error(` Configuration option ${chalk.bold('rootDir')} must be specified.`); + } + + options.rootDir = path.normalize(options.rootDir); + return options; +} + +function normalizeCommits(commits?: string[]): string[] | undefined { + if (!commits) { + return undefined; + } + + let [base, target] = commits; + base = (base || BASE_COMMIT || ''); + target = (target || TARGET_COMMIT || ''); + return [base.slice(0, 7), target.slice(0, 7)]; +} + +function normalizeTestPathPattern(argsCLI: any) { + const patterns = []; + if (argsCLI._) { + patterns.push(...argsCLI._); + } + + if (argsCLI.testPathPattern) { + patterns.push(...argsCLI.testPathPattern); + } + + const testPathPattern = patterns.map(replacePathSepForRegex).join('|'); + return testPathPattern; +} + +export function normalizeRootDirPattern(str: string, rootDir: string) { + return str.replace(//g, rootDir); +} + +export function normalizeRegexPattern(names: any) { + if (typeof names === 'string') { + names = names.split(','); + } + if (names instanceof Array) { + names = names.map(name => name.replace(/\*/g, '.*')); + names = new RegExp(`^(${names.join('|')})$`); + } + if (!(names instanceof RegExp)) { + throw new Error(` Names must be provided as a string, array or regular expression.`); + } + return typeof names === 'string' ? new RegExp(names) : names; +} + +export function normalizeConfig(options: RawBestConfig, cliOptions: BestCliOptions): DefaultProjectOptions { + options = normalizeRootDir(setCliOptionOverrides(options, cliOptions)); + const defaultProjectOptions: DefaultProjectOptions = { ...DEFAULT_CONFIG }; + + Object.keys(options).reduce((newOpts: any, key) => { + let value = newOpts[key]; + switch (key) { + case 'projects': + value = normalizeModulePathPatterns(options, key); + break; + case 'plugins': + value = normalizePlugins(options[key], options); + break; + case 'runnerConfig': + value = normalizeRunnerConfig(options[key], options); + break; + case 'compareStats': + value = options[key] !== undefined && normalizeCommits(options[key]); + break; + default: + value = options[key]; + } + defaultProjectOptions[key] = value; + return newOpts; + }, defaultProjectOptions); + + defaultProjectOptions.nonFlagArgs = cliOptions._; + defaultProjectOptions.testPathPattern = normalizeTestPathPattern(cliOptions); + return defaultProjectOptions; +} diff --git a/packages/@best/config/src/utils/resolve-config.ts b/packages/@best/config/src/utils/resolve-config.ts new file mode 100644 index 00000000..83d6ab04 --- /dev/null +++ b/packages/@best/config/src/utils/resolve-config.ts @@ -0,0 +1,100 @@ +import fs from 'fs'; +import path from 'path'; +import { PACKAGE_JSON, BEST_CONFIG } from './constants'; +import { RawBestConfig } from '../types'; + +function isFile(filePath:string) { + return fs.existsSync(filePath) && !fs.lstatSync(filePath).isDirectory(); +} + +export function resolveConfigPathByTraversing(pathToResolve: string, initialPath: string, cwd: string): string { + const bestConfig = path.resolve(pathToResolve, BEST_CONFIG); + if (isFile(bestConfig)) { + return bestConfig; + } + + const packageJson = path.resolve(pathToResolve, PACKAGE_JSON); + if (isFile(packageJson)) { + return packageJson; + } + + if (pathToResolve === path.dirname(pathToResolve)) { + throw new Error(`No config found in ${initialPath}`); + } + + // go up a level and try it again + return resolveConfigPathByTraversing(path.dirname(pathToResolve), initialPath, cwd); +} + +export function resolveConfigPath(pathToResolve: string, cwd: string) { + const absolutePath = path.isAbsolute(pathToResolve) ? pathToResolve : path.resolve(cwd, pathToResolve); + if (isFile(absolutePath)) { + return absolutePath; + } + + return resolveConfigPathByTraversing(absolutePath, pathToResolve, cwd); +} + +export function readConfigAndSetRootDir(configPath: string): RawBestConfig { + const isJSON = configPath.endsWith('.json'); + let configObject; + try { + configObject = require(configPath); + } catch (error) { + if (isJSON) { + throw new Error(`Best: Failed to parse config file ${configPath}\n`); + } else { + throw error; + } + } + + if (configPath.endsWith(PACKAGE_JSON)) { + if (!configObject.best) { + throw new Error(`No "best" section has been found in ${configPath}`); + } + + configObject = configObject.best; + } + + if (!configObject) { + throw new Error("Couldn't find any configuration for Best."); + } + + if (configObject.rootDir) { + // We don't touch it if it has an absolute path specified + if (!path.isAbsolute(configObject.rootDir)) { + // otherwise, we'll resolve it relative to the file's __dirname + configObject.rootDir = path.resolve(path.dirname(configPath), configObject.rootDir); + } + } else { + // If rootDir is not there, we'll set it to this file's __dirname + configObject.rootDir = path.dirname(configPath); + } + + return configObject; +} + +export function ensureNoDuplicateConfigs(parsedConfigs: any, projects: string[]) { + const configPathSet = new Set(); + + for (const { configPath } of parsedConfigs) { + if (configPathSet.has(configPath)) { + let message = 'One or more specified projects share the same config file\n'; + + parsedConfigs.forEach((projectConfig: any, index: number) => { + message = + message + + '\nProject: "' + + projects[index] + + '"\nConfig: "' + + String(projectConfig.configPath) + + '"'; + }); + throw new Error(message); + } + + if (configPath !== null) { + configPathSet.add(configPath); + } + } +}; diff --git a/packages/@best/utils/package.json b/packages/@best/utils/package.json index 3ec4d407..92429d1f 100644 --- a/packages/@best/utils/package.json +++ b/packages/@best/utils/package.json @@ -9,7 +9,7 @@ "dependencies": { "chalk": "~2.4.2", "is-ci": "~2.0.0", - "systeminformation": "4.1.6" + "systeminformation": "~4.9.2" }, "devDependencies": { "@types/systeminformation": "^3.23.1" diff --git a/yarn.lock b/yarn.lock index 3336e5c6..64c3f92b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11858,10 +11858,10 @@ systeminformation@3.33.12: resolved "http://npm.lwcjs.org/systeminformation/-/systeminformation-3.33.12/bb96d176a751886fcc4123006e9a2b322e7e1cc8.tgz#bb96d176a751886fcc4123006e9a2b322e7e1cc8" integrity sha512-auKCft3Mc23bhx8P8GhAF+4xWHxMPSBhKRmNwmTxfWrmtwOvhwhU8GmBarYXlp0X52Jwv0QWT988Q9NhlkFecQ== -systeminformation@4.1.6: - version "4.1.6" - resolved "http://npm.lwcjs.org/systeminformation/-/systeminformation-4.1.6/9ee8883178c4977d1910beb3eef6bf250f7fe3b2.tgz#9ee8883178c4977d1910beb3eef6bf250f7fe3b2" - integrity sha512-8Q8wPRvnZX0WRxPVVGN6Q/51GeDBhqruNhFzAjoCPaf4/l0hxTLj8XPjeAME8/Y9I5Y4bdA4fx0QjxvuFhlD8A== +systeminformation@~4.9.2: + version "4.9.2" + resolved "http://npm.lwcjs.org/systeminformation/-/systeminformation-4.9.2/df6e534d660ee438f61dc9cc08f6f826fb599406.tgz#df6e534d660ee438f61dc9cc08f6f826fb599406" + integrity sha512-J3a2Rsdtk+JhJ5cL2ByMLtW5/EZnsZ5gzE573U/Jlu7Ss+cjme9qOVeND5cDKvGcouW3b+hMBtZl5ivNb7HDFw== table@^5.2.3: version "5.4.0"