diff --git a/.npmignore b/.npmignore index 1335f3d..2e7c7c0 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1,6 @@ .nyc_output/ artifacts/ +docs/ node_modules/ src/ testables/ diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..49f69dc --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1 @@ +# How to contribute diff --git a/docs/issue_template.md b/docs/issue_template.md new file mode 100644 index 0000000..f4e0438 --- /dev/null +++ b/docs/issue_template.md @@ -0,0 +1,3 @@ +# ISSUE + +# FIX SUGGESTION diff --git a/docs/pull_request_template.md b/docs/pull_request_template.md new file mode 100644 index 0000000..08481ab --- /dev/null +++ b/docs/pull_request_template.md @@ -0,0 +1,3 @@ +# What issue does this fix/feature? + +# What tests cover the fix/feature? diff --git a/package.json b/package.json index 2c70cc1..5804ab0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "license": "MIT", "main": "./lib/CLI.js", "typings": "./lib/index.d.ts", - "version": "2.0.0-alpha-5", + "version": "2.0.0-alpha-6", "dependencies": { "chalk": "^2.3.0" }, diff --git a/src/Chest.spec.ts b/src/Chest.spec.ts index 9444af7..d3835d7 100644 --- a/src/Chest.spec.ts +++ b/src/Chest.spec.ts @@ -8,47 +8,13 @@ import { Files, Project, Registry } from './Core' const expect = chai.expect -describe('when using RootProject to load a project', () => { +describe('when loading projects', () => { beforeEach(() => { chai.should() chai.use(chaiAsPromised) }) - it('should load single npm project', async () => { - const directory = Files.join(process.cwd(), 'testables', 'single') - const project = await Chest.project(directory) - const projects = await Chest.projects(project) - expect(projects.length).to.equal(1) - expect(projects[0].name).to.equal('project-single') - expect(projects[0].path).to.equal(directory) - }) - - it('should load yarn workspace project', async () => { - const directory = Files.join(process.cwd(), 'testables', 'workspaces') - const project = await Chest.project(directory) - const projects = await Chest.projects(project) - expect(projects.length).to.equal(2) - expect(projects[0].name).to.equal('simple-package') - expect(projects[1].name).to.equal('simple-project') - expect(projects[0].owner).to.not.equal(undefined) - expect(projects[0].owner).to.not.equal(undefined) - }) - - it('should return static InvalidProject when single project does not exist', () => { - const directory = Files.join(process.cwd(), 'testables', 'nonexistant') - - return Chest.project(directory).then(project => expect(project).to.equal(Project.InvalidProject)) - }) - - it('should throw error when workspace project has no child projects', () => { - const directory = Files.join(process.cwd(), 'testables', 'workspaces-invalid') - return Chest.project(directory).then(async project => { - const projects = await Chest.projects(project) - expect(projects).to.deep.equal([Project.InvalidProject]) - }) - }) - it('should run scripts for single project', () => { const directory = Files.join(process.cwd(), 'testables', 'single') const args = Object.keys(Registry.all()) diff --git a/src/Chest.ts b/src/Chest.ts index 9ab572e..4ee432a 100644 --- a/src/Chest.ts +++ b/src/Chest.ts @@ -6,8 +6,7 @@ export class Chest { private static readonly Log: Log = Logger('chest') public static async run(root: string, ...args: string[]): Promise { - const project = await Chest.project(root) - const projects = await Chest.projects(project) + const project = await Project.load(root) const updaters = Registry.all() Object.keys(updaters).forEach(async name => { @@ -16,52 +15,8 @@ export class Chest { if (updater.type === UpdaterType.Root) { await updater.exec(root) } else { - await Promise.all(projects.map(child => updater.workspace(child))) + await Promise.all(project.children.map(child => updater.workspace(child))) } }) } - - public static async project(root: string): Promise { - const npmfile = path.join(root, 'package.json') - - if (await Files.exists(npmfile) === false) { - Chest.Log.error(new Error(`failed to find ${npmfile} in [${root}]`)) - return Project.InvalidProject - } - - const npm = await Files.json(npmfile) - return new Project(npm.name, root) - } - - public static async projects(owner: Project): Promise { - const project = await Chest.project(owner.path) - const npm = await project.package - this.Log.debug('project', project.name) - - if (npm.private && npm.workspace) { - return npm.workspace.map(workspaceRoot => Chest.workspaces(project, workspaceRoot)) - .reduce(async (previous, current) => (await previous).concat(await current), Promise.resolve([])) - } - - return [project] - } - - private static async workspaces(owner: Project, workspaceRoot: string): Promise { - workspaceRoot = Files.join(owner.path, workspaceRoot.substring(0, workspaceRoot.indexOf('/*'))) - - if (await Files.exists(workspaceRoot) === false) { - Chest.Log.error(new Error(`[workspace] failed to find ${workspaceRoot} in [${owner.name}]`)) - return [Project.InvalidProject] - } - - const projects = await Files.listdirs(workspaceRoot) - - return Promise.all(projects.map(async projectPath => { - const npmfile = path.join(projectPath, 'package.json') - const npm = await Files.json(npmfile) - const project = new Project(npm.name, projectPath, owner) - this.Log.debug('project.workspace', owner.name, '->', project.name) - return project - })) - } } diff --git a/src/Core/Actions/Packages.ts b/src/Core/Actions/Packages.ts index 84173ce..eb997f7 100644 --- a/src/Core/Actions/Packages.ts +++ b/src/Core/Actions/Packages.ts @@ -24,23 +24,25 @@ class Script extends UpdateScript { } public async workspace(project: Project): Promise { - const source = await project.owner.package - const target = await project.package - - target.author = source.author - target.bugs = source.bugs - target.description = source.description - target.homepage = source.homepage - target.license = source.license - target.repository = source.repository - - const filename = path.join(project.path, 'package.json') - - if (this.testing) { - this.log.task('updated package info', filename, JSON.stringify(target, null, 2)) - } else { - await Files.save(filename, target) - this.log.task('updated package info', filename) + if (project.owner) { + const source = await project.owner.package + const target = await project.package + + target.author = source.author + target.bugs = source.bugs + target.description = source.description + target.homepage = source.homepage + target.license = source.license + target.repository = source.repository + + const filename = path.join(project.path, 'package.json') + + if (this.testing) { + this.log.task('workspace', filename, JSON.stringify(target, null, 2)) + } else { + await Files.save(filename, target) + this.log.task('workspace', filename) + } } } } diff --git a/src/Core/Actions/Typings.ts b/src/Core/Actions/Typings.ts index f9cb509..124a43e 100644 --- a/src/Core/Actions/Typings.ts +++ b/src/Core/Actions/Typings.ts @@ -1,4 +1,5 @@ import * as path from 'path' +import { CompilerOptions } from 'typescript' import { Files, Logger, NPM, Project, Registry, Updater, UpdateScript, UpdaterType } from '../index' const ScriptName = Files.extensionless(__filename) @@ -13,6 +14,10 @@ interface Dependency { typings?: string } +interface TsConfig { + compilerOptions: CompilerOptions +} + /* * Updates the "types" property of "tsconfig.json" files by * looking for types from @types. @@ -23,67 +28,49 @@ class Script extends UpdateScript { } public async exec(rootpath: string): Promise { - const tsconfigfile = path.join(rootpath, 'tsconfig.json') - const packagedir = path.join(rootpath, 'node_modules') - this.log.debug('exec', this.name, rootpath, packagedir) - - if (await Files.exists(tsconfigfile) && await Files.exists(packagedir)) { - const packagedirs = await Files.listdirs(packagedir) - const tsconfig = await Files.json(tsconfigfile) + this.log.task('exec', rootpath) + const project = await Project.load(rootpath) - const dependencies = await Promise.all(packagedirs.map(packagedir => { - this.log.debug('dependencies', packagedir) - return this.dependencies(packagedir) - })) - - const typings = dependencies.reduce((previous, current) => previous.concat(current.filter(c => !!c.typings)), []) + if (project === Project.InvalidProject) { + this.log.error(`failed to load any projects at ${rootpath}`) + return + } - tsconfig.compilerOptions.types = typings.map(typing => typing.npmname).sort() + const tsconfig = await project.json('tsconfig.json') + tsconfig.compilerOptions.types = await this.gatherTypeDefinitions(project) - if (this.testing) { - this.log.task('updated types', tsconfigfile, JSON.stringify(tsconfig, null, 2)) - } else { - await Files.save(tsconfigfile, tsconfig) - this.log.task('updated types', tsconfigfile) - } + if (this.testing) { + this.log.task('tsconfig', JSON.stringify(tsconfig, null, 2)) + } else { + await project.save('tsconfig.json', tsconfig) + this.log.task('tsconfig') } } - private async dependencies(packagedir: string): Promise { - const dirname = path.basename(packagedir) + private async gatherTypeDefinitions(project: Project): Promise { + const npm = await project.package + let dependencies: string[] = [] - if (dirname[0] === '@') { - const scopedirs = await Files.listdirs(packagedir) - - return Promise.all(scopedirs - .map(scope => [scope, path.join(scope, 'package.json')]) - .map(async ([scope, scopepath]): Promise => { - const npm = await Files.json(path.join(scope, 'package.json')) - - return { - filename: 'package.json', - filepath: scope, - npmname: npm.name, - scope: dirname, - typings: npm.types || npm.typings || npm.typeScriptVersion ? 'index.d.ts' : undefined, - } - })) + if (npm.dependencies) { + dependencies = dependencies.concat(Object.keys(npm.dependencies)) } - const packagefile = path.join(packagedir, 'package.json') - - if (await Files.exists(packagefile)) { - const npm = await Files.json(packagefile) - - return [{ - filename: 'package.json', - filepath: packagedir, - npmname: npm.name, - typings: npm.types || npm.typings || npm.typeScriptVersion ? 'index.d.ts' : undefined, - }] + if (npm.devDependencies) { + dependencies = dependencies.concat(Object.keys(npm.devDependencies)) } - return [] + const modulesPath = Files.join(project.path, 'node_modules') + + return Promise.all(dependencies.map(async dependency => { + const dependencyPath = Files.join(modulesPath, dependency) + if (await Files.exists(dependencyPath)) { + const npm = await Files.json(dependencyPath) + if (npm.types || npm.typings) { + return dependency + } + } + return '' + })).then(values => values.filter(value => value)) } } diff --git a/src/Core/Files.spec.ts b/src/Core/Files.spec.ts index 259227c..9d17dff 100644 --- a/src/Core/Files.spec.ts +++ b/src/Core/Files.spec.ts @@ -19,6 +19,29 @@ describe('when working with files', () => { chai.use(chaiAsPromise) }) + it('should list directories', async () => { + const directories = await Files.listdirs(Files.join(process.cwd(), 'testables')) + expect(directories.length).to.equal(3) + expect(directories).to.contain(Files.join(process.cwd(), 'testables/single')) + expect(directories).to.contain(Files.join(process.cwd(), 'testables/workspaces')) + expect(directories).to.contain(Files.join(process.cwd(), 'testables/workspaces-invalid')) + }) + + it('should throw error when listing directories', (done) => { + Files.listdirs(Files.join(process.cwd(), 'nonexistant')).catch(() => done()) + }) + + it('should list files', async () => { + const filepaths = await Files.listfiles(Files.join(process.cwd(), 'testables/single')) + expect(filepaths.length).to.equal(2) + expect(filepaths).to.contain(Files.join(process.cwd(), 'testables/single/package.json')) + expect(filepaths).to.contain(Files.join(process.cwd(), 'testables/single/tsconfig.json')) + }) + + it('should throw error when listing files', (done) => { + Files.listfiles(Files.join(process.cwd(), 'nonexistant')).catch(() => done()) + }) + it('should write file', () => { const filename = Files.join(process.cwd(), 'artifacts', 'test.json') return Files.writefile(filename, {}) diff --git a/src/Core/Files.ts b/src/Core/Files.ts index dcc43d3..6201dca 100644 --- a/src/Core/Files.ts +++ b/src/Core/Files.ts @@ -8,6 +8,10 @@ export interface Stat { } class InternalFiles { + public basename(filepath: string): string { + return path.basename(filepath) + } + public exists(filepath: string): Promise { return new Promise((resolve, reject) => { fs.exists(filepath, (exists: boolean) => resolve(exists)) @@ -121,6 +125,7 @@ class InternalFiles { } export interface Files { + basename(filepath: string): string exists(filepath: string): Promise extensionless(filename: string): string join(...args: string[]): string diff --git a/src/Core/Logger.ts b/src/Core/Logger.ts index 50d9f0f..69e1026 100644 --- a/src/Core/Logger.ts +++ b/src/Core/Logger.ts @@ -13,26 +13,30 @@ export function Logger(name: string, category?: string): Log { const cat = category ? `:${category}` : '' const bold = (name: string) => chalk.default.bold(`[${name}${cat}]`) + const log = (...args: any[]) => { + if (['production', 'test', 'testing'].every(env => env !== process.env.NODE_ENV)) { + console.log(...args) + } + } + return { debug: (...args: any[]): void => { - if (process.env.NODE_ENV !== 'production') { - console.log(chalk.default.yellow.inverse(bold(name), ...args)) - } + log(chalk.default.yellow.inverse(bold(name), ...args)) }, error: (...args: any[]): void => { - console.log(chalk.default.red.inverse(bold(name), ...args)) + log(chalk.default.red.inverse(bold(name), ...args)) }, info: (...args: any[]): void => { - console.log(chalk.default.grey.italic(bold(name), ...args)) + log(chalk.default.grey.italic(bold(name), ...args)) }, start: (...args: any[]): void => { - console.log(chalk.default.grey.dim(bold(name), ...args)) + log(chalk.default.grey.dim(bold(name), ...args)) }, done: (...args: any[]): void => { - console.log(chalk.default.grey.dim(bold(name), ...args)) + log(chalk.default.grey.dim(bold(name), ...args)) }, task: (...args: any[]): void => { - console.log(chalk.default.blue(bold(name), ...args)) + log(chalk.default.blue(bold(name), ...args)) } } } diff --git a/src/Core/Project.spec.ts b/src/Core/Project.spec.ts new file mode 100644 index 0000000..c12f9ca --- /dev/null +++ b/src/Core/Project.spec.ts @@ -0,0 +1,35 @@ +import 'mocha' + +import * as chai from 'chai' +import * as chaiAsPromised from 'chai-as-promised' + +import { Files } from './Files' +import { Project } from './Project' +import { Registry } from './Registry' + +const expect = chai.expect + +describe('when loading projects', () => { + + beforeEach(() => { + chai.should() + chai.use(chaiAsPromised) + }) + + it('should load single npm project', async () => { + const directory = Files.join(process.cwd(), 'testables', 'single') + const project = await Project.load(directory) + expect(project.children.length).to.equal(0) + expect(project.name).to.equal('project-single') + expect(project.path).to.equal(directory) + }) + + it('should load yarn workspace project', async () => { + const directory = Files.join(process.cwd(), 'testables', 'workspaces') + const project = await Project.load(directory) + expect(project.children.length).to.equal(2) + expect(project.children[0].owner).to.not.equal(undefined) + expect(project.children[1].owner).to.not.equal(undefined) + }) + +}) diff --git a/src/Core/Project.ts b/src/Core/Project.ts index 5d0a521..4e6a316 100644 --- a/src/Core/Project.ts +++ b/src/Core/Project.ts @@ -1,24 +1,40 @@ import { Files } from './Files' import { NPM } from './Interfaces' +import { Log, Logger } from './Logger' + +interface LernaConfig { + packages?: string[] + useWorkspaces?: boolean + version: string +} export class Project { public static InvalidProject: Project = new Project('invalid', 'invalid') + private readonly _children: Project[] private readonly _name: string - private readonly _owner: Project + private readonly _owner: Project | undefined private readonly _path: string + private readonly log: Log - constructor(name: string, path: string, owner?: Project) { + private constructor(name: string, path: string, owner?: Project) { + this._children = [] this._name = name - this._owner = owner || this + this._owner = owner this._path = path + + this.log = Logger(`project:${this.name}`) + } + + public get children(): Project[] { + return this._children } public get name(): string { return this._name } - public get owner(): Project { + public get owner(): Project | undefined { return this._owner } @@ -29,4 +45,80 @@ export class Project { public get path(): string { return this._path } + + public static async load(rootpath: string): Promise { + const npmfile = Files.join(rootpath, 'package.json') + + if (await Files.exists(npmfile) === false) { + return Project.InvalidProject + } + + const npm = await Files.json(npmfile) + const project = new Project(npm.name, rootpath) + + if (npm.private && npm.workspace) { + const result = await project.loadChildProjects(npm) + return result + } + + return project + } + + public json(filename: string): Promise { + return Files.json(Files.join(this.path, filename)) + } + + public save(filename: string, data: T): Promise { + return Files.save(Files.join(this.path, filename), data) + } + + private async loadChildProjects(npm: NPM): Promise { + const lernafile = Files.join(this.path, 'lerna.json') + if (await Files.exists(lernafile)) { + this.log.debug('lerna-packages', this.path) + return this.loadLernaPackages(lernafile, this) + } + this.log.debug('yarn-workspaces', this.path) + return this.loadYarnWorkspaces(this) + } + + private async loadLernaPackages(filepath: string, project: Project): Promise { + const lerna = await Files.json(filepath) + if (lerna.packages && lerna.useWorkspaces) { + lerna.packages.forEach(async workspace => { + const workspaceName = workspace.substring(0, workspace.indexOf('/*')) + const workspacePath = Files.join(project.path, workspaceName) + const children = await this.loadProjects(workspacePath) + children.forEach(child => this.children.push(child)) + this.log.debug('lerna-package', workspaceName) + }) + } + return this + } + + private async loadYarnWorkspaces(project: Project): Promise { + const npm = await this.package + if (npm.workspace) { + return Promise.all(npm.workspace.map(async workspace => { + const workspaceName = workspace.substring(0, workspace.indexOf('/*')) + const workspacePath = Files.join(project.path, workspaceName) + const children = await this.loadProjects(workspacePath) + children.forEach(child => this.children.push(child)) + this.log.debug('yarn-workspace', workspaceName) + })).then(() => this) + } + return this + } + + private async loadProjects(workspacePath: string): Promise { + const projects = await Files.listdirs(workspacePath) + return Promise.all(projects.map(async projectPath => { + const projectName = Files.basename(projectPath) + const npmfile = Files.join(projectPath, 'package.json') + const npm = await Files.json(npmfile) + const child = new Project(npm.name, projectPath, this) + this.log.debug('added-project', npm.name) + return child + })) + } } diff --git a/src/Core/UpdateScript.ts b/src/Core/UpdateScript.ts index d5b53cf..693798b 100644 --- a/src/Core/UpdateScript.ts +++ b/src/Core/UpdateScript.ts @@ -53,8 +53,17 @@ export abstract class UpdateScript implements Updater { if (error) this.log.error(error) }) - child.stderr.on('data', data => this.args(project, process.stderr, data).map(lines => lines).forEach(args => this.log.error(...args))) - child.stdout.on('data', data => this.args(project, process.stdout, data).map(lines => lines).forEach(args => this.log.task(...args))) + child.stderr.on('data', data => + this.args(project, process.stderr, data) + .map(lines => lines) + .forEach(args => this.log.error(...args)) + ) + + child.stdout.on('data', data => + this.args(project, process.stdout, data) + .map(lines => lines) + .forEach(args => this.log.task(...args)) + ) child.addListener('exit', (code: number, signal: string) => { if (code === 0) {