diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 70862bf..960b582 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -14,4 +14,4 @@ "types": "lib/index.d.ts", "devDependencies": {}, "module": "lib/index.js" -} +} \ No newline at end of file diff --git a/packages/scripts/src/log.ts b/packages/scripts/src/log.ts index ee76989..bcb150d 100644 --- a/packages/scripts/src/log.ts +++ b/packages/scripts/src/log.ts @@ -1,9 +1,9 @@ import {format} from 'util' export function error(message: string, ...values: any[]) { - process.stderr.write(format(message + '\n', ...values)) + process.stderr.write(format(message, ...values) + '\n') } export function info(message: string, ...values: any[]) { - process.stdout.write(format(message + '\n', ...values)) + process.stdout.write(format(message, ...values) + '\n') } diff --git a/packages/scripts/src/scripts/index.ts b/packages/scripts/src/scripts/index.ts index 4238c24..f1e619b 100644 --- a/packages/scripts/src/scripts/index.ts +++ b/packages/scripts/src/scripts/index.ts @@ -3,3 +3,4 @@ export * from './build' export * from './exportDir' export * from './intergen' export * from './syncEsm' +export * from './update' diff --git a/packages/scripts/src/scripts/update.test.ts b/packages/scripts/src/scripts/update.test.ts new file mode 100644 index 0000000..bb465d0 --- /dev/null +++ b/packages/scripts/src/scripts/update.test.ts @@ -0,0 +1,80 @@ +jest.mock('child_process') +jest.mock('fs') +jest.mock('../log') + +import cp from 'child_process' +import * as fs from 'fs' +import {update, IOutdated} from './update' + +describe('update', () => { + + const stringify = (obj: object) => JSON.stringify(obj, null, ' ') + + const readMock = fs.readFileSync as jest.Mock + const writeMock = fs.writeFileSync as jest.Mock + const cpMock = cp.execFileSync as jest.Mock + + let outdated: Record = {} + beforeEach(() => { + outdated = {} + + cpMock.mockClear() + readMock.mockClear() + writeMock.mockClear() + + cpMock.mockImplementation(() => { + const err = new Error('Exit code 1'); + (err as any).stdout = stringify(outdated) + throw err + }) + readMock.mockReturnValue(stringify({ + name: 'test', + dependencies: { + a: '^1.2.3', + }, + devDependencies: { + b: '^3.4.6', + }, + })) + }) + + it('does not change when no changes', async () => { + cpMock.mockReturnValue('{}') + await update('update', '/my/dir') + expect(writeMock.mock.calls.length).toBe(0) + }) + + it('does not change when npm outdated output is empty', async () => { + cpMock.mockReturnValue('') + await update('update', '/my/dir') + expect(writeMock.mock.calls.length).toBe(0) + }) + + it('updates outdated dependencies in package.json', async () => { + outdated = { + a: { + wanted: '1.2.3', + latest: '1.4.0', + location: '', + }, + b: { + wanted: '3.4.6', + latest: '3.4.7', + location: '', + }, + } + await update('update', '/my/dir') + expect(writeMock.mock.calls).toEqual([[ + '/my/dir/package.json', stringify({ + name: 'test', + dependencies: { + a: '^1.4.0', + }, + devDependencies: { + b: '^3.4.7', + }, + }), + ]]) + }) + +}) diff --git a/packages/scripts/src/scripts/update.ts b/packages/scripts/src/scripts/update.ts new file mode 100644 index 0000000..0cc5085 --- /dev/null +++ b/packages/scripts/src/scripts/update.ts @@ -0,0 +1,87 @@ +import * as fs from 'fs' +import * as path from 'path' +import cp from 'child_process' +import {argparse, arg} from '@rondo.dev/argparse' +import {info} from '../log' + +export interface IOutdated { + wanted: string + latest: string + location: string +} + +export interface IPackage { + dependencies?: Record + devDependencies?: Record +} + +function findOutdated(cwd: string): Record { + try { + const result = cp.execFileSync('npm', ['outdated', '--json'], { + cwd, + encoding: 'utf8', + }) + return result === '' ? {} : JSON.parse(result) + } catch (err) { + // npm outdated will exit with code 1 if there are outdated dependencies + return JSON.parse(err.stdout) + } +} + +export async function update(...argv: string[]) { + const {parse} = argparse({ + dirs: arg('string[]', {positional: true, default: ['.'], n: '+'}), + prefix: arg('string', {default: '^'}), + }) + const {dirs, prefix} = parse(argv) + + let updates = 0 + for (const dir of dirs) { + info(dir) + const outdatedByName = findOutdated(dir) + + const pkgFile = path.join(dir, 'package.json') + const pkg: IPackage = JSON.parse(fs.readFileSync(pkgFile, 'utf8')) + let pkgUpdate: IPackage = pkg + + // tslint:disable-next-line + for (const name in outdatedByName) { + const outdated = outdatedByName[name] + pkgUpdate = updateDependency( + pkgUpdate, 'dependencies', name, prefix, outdated) + pkgUpdate = updateDependency( + pkgUpdate, 'devDependencies', name, prefix, outdated) + } + + if (pkgUpdate !== pkg) { + updates += 1 + info('Writing updates...') + fs.writeFileSync(pkgFile, JSON.stringify(pkgUpdate, null, ' ')) + } + } + + if (updates) { + info('Done! Do not forget to run npm install!') + } +} + +function updateDependency( + pkg: IPackage, + key: 'dependencies' | 'devDependencies', + name: string, + prefix: string, + version: IOutdated, +): IPackage { + const deps = pkg[key] + if (!deps || !deps[name] || version.wanted === version.latest) { + return pkg + } + info(' %s.%s %s ==> %s', key, name, version.wanted, version.latest) + return { + ...pkg, + [key]: { + ...deps, + [name]: prefix + version.latest, + }, + } +}