Skip to content

Commit

Permalink
feat: syncing multiple files based on workspaces/packages field (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffijoe committed Oct 8, 2018
1 parent 06ba946 commit 451020e
Show file tree
Hide file tree
Showing 12 changed files with 414 additions and 138 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@
"axios": "^0.18.0",
"chalk": "^2.4.1",
"detect-indent": "^5.0.0",
"ora": "^3.0.0",
"tslib": "^1.9.3"
"glob": "^7.1.3",
"ora": "^3.0.0"
},
"devDependencies": {
"@types/detect-indent": "^5.0.0",
"@types/jest": "^23.3.3",
"@types/ora": "^1.3.4",
"@types/prettier": "^1.13.1",
"@types/prettier": "^1.13.2",
"@types/rimraf": "^2.0.2",
"coveralls": "^3.0.2",
"jest": "^23.6.0",
Expand Down
7 changes: 7 additions & 0 deletions src/__tests__/globber.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createGlobber } from '../globber'

test('returns the current directory as a match', async () => {
const result = await createGlobber().globPackageFiles(process.cwd())
expect(result).toHaveLength(1)
expect(result[0]).toBe(process.cwd() + '/package.json')
})
76 changes: 69 additions & 7 deletions src/__tests__/type-syncer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
IPackageFile
} from '../types'
import { createTypeSyncer } from '../type-syncer'
import { IGlobber } from '../globber'

const typedefs: ITypeDefinition[] = [
{
Expand Down Expand Up @@ -35,7 +36,7 @@ const typedefs: ITypeDefinition[] = [
]

function buildSyncer() {
const packageFile: IPackageFile = {
const rootPackageFile: IPackageFile = {
name: 'consumer',
dependencies: {
package1: '^1.0.0',
Expand All @@ -53,6 +54,22 @@ function buildSyncer() {
'@myorg/package7': '^1.0.0',
package8: '~1.0.0',
package9: '1.0.0'
},
packages: ['packages/*'],
workspaces: ['packages/*']
}

const package1File: IPackageFile = {
name: 'package-1',
dependencies: {
package1: '^1.0.0'
}
}

const package2File: IPackageFile = {
name: 'package-1',
dependencies: {
package3: '^1.0.0'
}
}

Expand All @@ -61,24 +78,50 @@ function buildSyncer() {
getLatestTypingsVersion: jest.fn(() => Promise.resolve('1.0.0'))
}
const packageService: IPackageJSONService = {
readPackageFile: jest.fn(() => Promise.resolve(packageFile)),
readPackageFile: jest.fn(async (filepath: string) => {
switch (filepath) {
case 'package.json':
return rootPackageFile
case 'packages/package-1/package.json':
return package1File
case 'packages/package-2/package.json':
return package2File
default:
throw new Error('What?!')
}
}),
writePackageFile: jest.fn(() => Promise.resolve())
}

const globber: IGlobber = {
globPackageFiles: jest.fn(async pattern => {
switch (pattern) {
case 'packages/*':
return [
'packages/package-1/package.json',
'packages/package-2/package.json'
]
default:
return []
}
})
}

return {
typedefSource,
packageService,
packageFile,
syncer: createTypeSyncer(packageService, typedefSource)
rootPackageFile,
syncer: createTypeSyncer(packageService, typedefSource, globber)
}
}

describe('type syncer', () => {
it('adds new packages to the package.json', async () => {
const { syncer, packageService } = buildSyncer()
const result = await syncer.sync('package.json')
const writtenPackage = (packageService.writePackageFile as jest.Mock<any>)
.mock.calls[0][1] as IPackageFile
const writtenPackage = (packageService.writePackageFile as jest.Mock<
any
>).mock.calls.find(c => c[0] === 'package.json')![1] as IPackageFile
expect(writtenPackage.devDependencies).toEqual({
'@types/package1': '^1.0.0',
'@types/package3': '^1.0.0',
Expand All @@ -90,14 +133,33 @@ describe('type syncer', () => {
package4: '^1.0.0',
package5: '^1.0.0'
})
expect(result.newTypings.map(x => x.typingsName).sort()).toEqual([
expect(result.syncedFiles).toHaveLength(3)

expect(result.syncedFiles[0].filePath).toEqual('package.json')
expect(
result.syncedFiles[0].newTypings.map(x => x.typingsName).sort()
).toEqual([
'myorg__package7',
'package1',
'package3',
'package5',
'package8',
'package9'
])

expect(result.syncedFiles[1].filePath).toEqual(
'packages/package-1/package.json'
)
expect(
result.syncedFiles[1].newTypings.map(x => x.typingsName).sort()
).toEqual(['package1'])

expect(result.syncedFiles[2].filePath).toEqual(
'packages/package-2/package.json'
)
expect(
result.syncedFiles[2].newTypings.map(x => x.typingsName).sort()
).toEqual(['package3'])
})

it('does not write packages if options.dry is specified', async () => {
Expand Down
36 changes: 35 additions & 1 deletion src/__tests__/util.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { uniq, filterMap, mergeObjects, orderObject, promisify } from '../util'
import {
uniq,
filterMap,
mergeObjects,
orderObject,
promisify,
memoizeAsync
} from '../util'

describe('util', () => {
describe('uniq', () => {
Expand Down Expand Up @@ -66,4 +73,31 @@ describe('util', () => {
expect(err.message).toBe('oh shit')
})
})

describe('memoizeAsync', () => {
it('memoizes promises', async () => {
let i = 0

const m = memoizeAsync((k: string) =>
Promise.resolve(k + (++i).toString())
)
expect([await m('hello'), await m('hello')]).toEqual(['hello1', 'hello1'])
expect([await m('goodbye'), await m('goodbye')]).toEqual([
'goodbye2',
'goodbye2'
])
})

it('removes entry on fail', async () => {
let i = 0

const m = memoizeAsync((k: string) =>
Promise.reject(new Error(k + (++i).toString()))
)
expect([
await m('hello').catch(err => err.message),
await m('hello').catch(err => err.message)
]).toEqual(['hello1', 'hello2'])
})
})
})
79 changes: 60 additions & 19 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createContainer, InjectionMode, asFunction } from 'awilix'
import { createTypeSyncer } from './type-syncer'
import { ITypeSyncer, ITypeDefinition } from './types'
import { createContainer, InjectionMode, asFunction, asValue } from 'awilix'
import chalk from 'chalk'
import * as C from './cli-util'
import { ITypeSyncer, ITypeDefinition, ISyncedFile } from './types'
import { createTypeSyncer } from './type-syncer'
import { createTypeDefinitionSource } from './type-definition-source'
import { createPackageJSONFileService } from './package-json-file-service'
import { createGlobber } from './globber'

/**
* Starts the TypeSync CLI.
Expand All @@ -15,26 +16,31 @@ export async function startCli() {
const container = createContainer({
injectionMode: InjectionMode.CLASSIC
}).register({
typeDefinitionSource: asFunction(createTypeDefinitionSource),
packageJSONService: asFunction(createPackageJSONFileService),
typeDefinitionSource: asFunction(createTypeDefinitionSource).singleton(),
packageJSONService: asFunction(createPackageJSONFileService).singleton(),
globber: asFunction(createGlobber).singleton(),
typeSyncer: asFunction(createTypeSyncer)
})
await _runCli(container.resolve<ITypeSyncer>('typeSyncer'))
await run(container.resolve<ITypeSyncer>('typeSyncer'))
} catch (err) {
C.error(err)
process.exit(1)
process.exitCode = 1
}
}

async function _runCli(syncer: ITypeSyncer) {
/**
* Actual CLI runner. Uses the `syncer` instance to sync.
* @param syncer
*/
async function run(syncer: ITypeSyncer) {
const { args, flags } = C.parseArguments(process.argv.slice(2))
const [filePath = 'package.json'] = args
if (flags.help) {
printHelp()
return
}

C.log(`TypeSync v${chalk.white(require('../package.json').version)}`)
C.log(chalk`TypeSync v{white ${require('../package.json').version}}`)
if (flags.dry) {
C.log('—— DRY RUN — will not modify file ——')
}
Expand All @@ -43,25 +49,60 @@ async function _runCli(syncer: ITypeSyncer) {
() => syncer.sync(filePath, { dry: flags.dry })
)

const formattedTypings = result.newTypings.map(formatPackageName).join('\n')
const syncedFilesOutput = result.syncedFiles
.map(renderSyncedFile)
.join('\n\n')
const totalNewTypings = result.syncedFiles
.map(f => f.newTypings.length)
.reduce((accum, next) => accum + next, 0)
C.success(
result.newTypings.length === 0
totalNewTypings === 0
? `No new typings added, looks like you're all synced up!`
: (chalk as any)`${
result.newTypings.length
} typings added:\n${formattedTypings}\n\n✨ Go ahead and run {green npm install} or {green yarn} to install the packages that were added.`
: chalk`${totalNewTypings.toString()} new typings added.\n\n${syncedFilesOutput}\n\n✨ Go ahead and run {green npm install} or {green yarn} to install the packages that were added.`
)
}

function formatPackageName(t: ITypeDefinition) {
return `${chalk.bold.green('+')} ${chalk.gray('@types/')}${chalk.bold.blue(
t.typingsName
)}`
/**
* Renders a type definition.
* @param typeDef
* @param isLast
*/
function renderTypeDef(typeDef: ITypeDefinition, isLast: boolean) {
const treeNode = isLast ? '└─' : '├─'
return chalk`${treeNode} {gray @types/}{bold.blue ${typeDef.typingsName}}`
}

/**
* Renders a synced file.
*
* @param file
*/
function renderSyncedFile(file: ISyncedFile) {
const badge =
file.newTypings.length === 0
? chalk`{blue.bold (no new typings added)}`
: chalk`{green.bold (${file.newTypings.length.toString()} new typings added)}`
const title = chalk`📦 ${file.package.name} {gray.italic — ${
file.filePath
}} ${badge}`
const nl = '\n'
return (
title +
nl +
file.newTypings
.map(t =>
renderTypeDef(t, file.newTypings[file.newTypings.length - 1] === t)
)
.join(nl)
)
}

/**
* Prints the help text.
*/
function printHelp() {
console.log(
(chalk as any)`
chalk`
{blue.bold typesync} - adds missing TypeScript definitions to package.json
Options
Expand Down
55 changes: 0 additions & 55 deletions src/fakes.ts

This file was deleted.

Loading

0 comments on commit 451020e

Please sign in to comment.