Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.nyc_output/
artifacts/
docs/
node_modules/
src/
testables/
Expand Down
1 change: 1 addition & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# How to contribute
3 changes: 3 additions & 0 deletions docs/issue_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ISSUE

# FIX SUGGESTION
3 changes: 3 additions & 0 deletions docs/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# What issue does this fix/feature?

# What tests cover the fix/feature?
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
36 changes: 1 addition & 35 deletions src/Chest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
49 changes: 2 additions & 47 deletions src/Chest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ export class Chest {
private static readonly Log: Log = Logger('chest')

public static async run(root: string, ...args: string[]): Promise<void> {
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 => {
Expand All @@ -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<Project> {
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<NPM>(npmfile)
return new Project(npm.name, root)
}

public static async projects(owner: Project): Promise<Project[]> {
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<Project[]> {
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<NPM>(npmfile)
const project = new Project(npm.name, projectPath, owner)
this.Log.debug('project.workspace', owner.name, '->', project.name)
return project
}))
}
}
36 changes: 19 additions & 17 deletions src/Core/Actions/Packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,25 @@ class Script extends UpdateScript {
}

public async workspace(project: Project): Promise<void> {
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)
}
}
}
}
Expand Down
87 changes: 37 additions & 50 deletions src/Core/Actions/Typings.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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.
Expand All @@ -23,67 +28,49 @@ class Script extends UpdateScript {
}

public async exec(rootpath: string): Promise<void> {
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<any>(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>('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<Dependency[]> {
const dirname = path.basename(packagedir)
private async gatherTypeDefinitions(project: Project): Promise<string[]> {
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<Dependency> => {
const npm = await Files.json<NPM>(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<NPM>(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<NPM>(dependencyPath)
if (npm.types || npm.typings) {
return dependency
}
}
return ''
})).then(values => values.filter(value => value))
}
}

Expand Down
23 changes: 23 additions & 0 deletions src/Core/Files.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {})
Expand Down
5 changes: 5 additions & 0 deletions src/Core/Files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export interface Stat {
}

class InternalFiles {
public basename(filepath: string): string {
return path.basename(filepath)
}

public exists(filepath: string): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
fs.exists(filepath, (exists: boolean) => resolve(exists))
Expand Down Expand Up @@ -121,6 +125,7 @@ class InternalFiles {
}

export interface Files {
basename(filepath: string): string
exists(filepath: string): Promise<boolean>
extensionless(filename: string): string
join(...args: string[]): string
Expand Down
20 changes: 12 additions & 8 deletions src/Core/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}
Loading