diff --git a/build.ts b/build.ts index 33a5c8c..20aea01 100644 --- a/build.ts +++ b/build.ts @@ -1,14 +1,17 @@ import * as chokidar from 'chokidar'; import * as path from 'path'; import * as prettier from 'prettier'; -import { spawnPromise, writeFilePromise } from './utils'; +import { FLOW_FILENAME, spawnAsync, TYPESCRIPT_FILENAME, writeFileAsync } from './utils'; const ROOT_DIR = __dirname; -const TYPESCRIPT_FILENAME = 'index.d.ts'; -const FLOW_FILENAME = 'index.js.flow'; const TEST_FILENAME = 'typecheck.ts'; -if (process.argv.indexOf('--watch') !== -1) { +if (process.argv.includes('--start')) { + trigger().catch(e => { + console.error(e); + process.exit(1); + }); +} else if (process.argv.includes('--watch')) { trigger() .catch(e => { console.error(e); @@ -27,20 +30,15 @@ if (process.argv.indexOf('--watch') !== -1) { ); }); }); -} else { - trigger().catch(e => { - console.error(e); - process.exit(1); - }); } -async function trigger() { +export default async function trigger() { console.info('Generating...'); const output = await create(); console.info('Formatting...'); const [flow, typescript] = await Promise.all([format(output.flow, 'flow'), format(output.typescript, 'typescript')]); console.info(`Writing files...`); - await Promise.all([writeFilePromise(FLOW_FILENAME, flow), writeFilePromise(TYPESCRIPT_FILENAME, typescript)]); + await Promise.all([writeFileAsync(FLOW_FILENAME, flow), writeFileAsync(TYPESCRIPT_FILENAME, typescript)]); console.info('Type checking...'); await typecheck(); } @@ -72,15 +70,12 @@ async function format(output: string, parser: prettier.BuiltInParserName) { function typecheck() { return Promise.all([ - spawnPromise( + spawnAsync( path.join(ROOT_DIR, `node_modules/.bin/${process.platform === 'win32' ? 'tsc.cmd' : 'tsc'}`), path.join(ROOT_DIR, TYPESCRIPT_FILENAME), path.join(ROOT_DIR, TEST_FILENAME), '--noEmit', ), - spawnPromise( - path.join(ROOT_DIR, `node_modules/.bin/${process.platform === 'win32' ? 'flow.cmd' : 'flow'}`), - 'check', - ), + spawnAsync(path.join(ROOT_DIR, `node_modules/.bin/${process.platform === 'win32' ? 'flow.cmd' : 'flow'}`), 'check'), ]); } diff --git a/package.json b/package.json index bdfa5cd..17e48b1 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ }, "scripts": { "update": "ts-node update.ts", - "build": "ts-node build.ts", + "build": "ts-node build.ts --start", "watch": "ts-node build.ts --watch", "lint": "tslint --exclude node_modules/**/* --exclude **/*.d.ts --fix **/*.ts", "pretty": "prettier --write build.ts **/*.{ts,js,json}", diff --git a/update.ts b/update.ts index 6530edd..ee84351 100644 --- a/update.ts +++ b/update.ts @@ -1,14 +1,15 @@ +import build from './build'; // @ts-ignore import * as packageJson from './package.json'; -import { getJson, spawnPromise, writeFilePromise } from './utils'; +import { FLOW_FILENAME, getJsonAsync, questionAsync, spawnAsync, TYPESCRIPT_FILENAME, writeFileAsync } from './utils'; (async () => { - if ((await spawnPromise('git', 'status', '--porcelain')) !== '') { + if ((await spawnAsync('git', 'status', '--porcelain')) !== '') { console.error('Your working directory needs to be clean!'); process.exit(1); } - console.info('Checks for updates...'); + console.info('Check for updates...'); const MDN_DATA = 'mdn-data'; const MDN_COMPAT = 'mdn-browser-compat-data'; @@ -17,12 +18,12 @@ import { getJson, spawnPromise, writeFilePromise } from './utils'; const [mdnCompatRepo, currentMdnCompatCommit] = packageJson.devDependencies[MDN_COMPAT].split('#'); const [mdnDataMaster, mdnCompatMaster] = [ - await getJson({ + await getJsonAsync({ hostname: 'api.github.com', path: '/repos/mdn/data/branches/master', headers: { 'User-Agent': 'NodeJS' }, }), - await getJson({ + await getJsonAsync({ hostname: 'api.github.com', path: '/repos/mdn/browser-compat-data/branches/master', headers: { 'User-Agent': 'NodeJS' }, @@ -33,44 +34,93 @@ import { getJson, spawnPromise, writeFilePromise } from './utils'; const latestMdnCompatCommit = mdnCompatMaster.commit.sha; if (latestMdnDataCommit !== currentMdnDataCommit || latestMdnCompatCommit !== currentMdnCompatCommit) { - console.info('Update found, upgrading and building...'); + console.info('Update found!'); + console.info('Upgrading...'); packageJson.devDependencies[MDN_DATA] = `${mdnDataRepo}#${latestMdnDataCommit}`; packageJson.devDependencies[MDN_COMPAT] = `${mdnCompatRepo}#${latestMdnCompatCommit}`; - await writeFilePromise('./package.json', JSON.stringify(packageJson, null, 2) + '\n'); + await writeFileAsync('./package.json', JSON.stringify(packageJson, null, 2) + '\n'); + await upgrade(); try { - await spawnPromise('yarn.cmd', '--silent', '--no-progress'); + await build(); } catch (e) { - console.error(e); - process.exit(1); + throw new Error(e); } const [indexDtsDiff, indexFlowDiff] = [ - await spawnPromise('git', '--no-pager', 'diff', 'index.d.ts'), - await spawnPromise('git', '--no-pager', 'diff', 'index.js.flow'), + await spawnAsync('git', '--no-pager', 'diff', '--color', TYPESCRIPT_FILENAME), + await spawnAsync('git', '--no-pager', 'diff', '--color', FLOW_FILENAME), ]; if (indexDtsDiff !== '' || indexFlowDiff !== '') { - await spawnPromise('git', 'commit', '-am', 'Bump MDN'); + console.info("Changes detected! Here's the diff:"); + console.info(indexDtsDiff); + console.info(indexFlowDiff); - const [major, minor, patch] = packageJson.version.split('.'); - const version = `${major}.${minor}.${Number(patch) + 1}`; + const doPrepare = await questionAsync('Do you want to prepare a release for this? (y/n) '); - packageJson.version = version; - await writeFilePromise('./package.json', JSON.stringify(packageJson, null, 2) + '\n'); - await spawnPromise('git', 'commit', '-am', `v${version}`); - await spawnPromise('git', 'tag', `v${version}`); + if (doPrepare === 'y') { + await spawnAsync('git', 'commit', '-am', 'Bump MDN'); - console.info('Changes detected! The changes are committed and tagged. You just need to:'); - console.info('- `git push origin HEAD --tags`'); - console.info('- `npm publish`'); + const [major, minor, patch] = packageJson.version.split('.'); + const version = `${major}.${minor}.${Number(patch) + 1}`; + + const tag = `v${version}`; + + packageJson.version = version; + await writeFileAsync('./package.json', JSON.stringify(packageJson, null, 2) + '\n'); + await spawnAsync('git', 'commit', '-am', tag); + await spawnAsync('git', 'tag', tag); + + console.info(`The changes are committed and tagged with: ${tag}`); + + const doPush = await questionAsync('Do you want to push now? (y/n) '); + + if (doPush === 'y') { + console.info('Pushing...'); + await spawnAsync('git', 'push', 'origin', 'HEAD', '--tags'); + } + } else { + console.info('Maybe next time!'); + console.info('Resetting...'); + await reset(); + console.info('Downgrading...'); + await upgrade(); + } } else { - console.info('No changes detected, resetting...'); - await spawnPromise('git', 'reset', '--hard'); + console.info('No changes detected!'); + console.info('Resetting...'); + await reset(); + console.info('Downgrading...'); + await upgrade(); } + + process.exit(0); } else { console.info('Nothing to update!'); } })(); + +async function reset() { + try { + await spawnAsync('git', 'reset', '--hard'); + } catch (e) { + throw new Error(e); + } +} + +async function upgrade() { + try { + await spawnAsync( + process.platform === 'win32' ? 'yarn.cmd' : 'yarn', + { stdio: 'inherit' }, + '--silent', + '--no-progress', + '--ignore-scripts', + ); + } catch (e) { + throw new Error(e); + } +} diff --git a/utils.ts b/utils.ts index 6ca98ae..9387932 100644 --- a/utils.ts +++ b/utils.ts @@ -1,10 +1,13 @@ -import { spawn } from 'child_process'; +import { spawn, SpawnOptions } from 'child_process'; import { writeFile } from 'fs'; import { get, RequestOptions } from 'https'; +import { createInterface } from 'readline'; -const ROOT_DIR = __dirname; +export const ROOT_DIR = __dirname; +export const TYPESCRIPT_FILENAME = 'index.d.ts'; +export const FLOW_FILENAME = 'index.js.flow'; -export function writeFilePromise(filename: string, content: string) { +export function writeFileAsync(filename: string, content: string) { return new Promise((resolve, reject) => { writeFile(filename, content, 'utf-8', error => { if (error) { @@ -16,19 +19,34 @@ export function writeFilePromise(filename: string, content: string) { }); } -export function spawnPromise(command: string, ...args: string[]) { +export function spawnAsync(command: string, optionsOrArg: SpawnOptions | string, ...args: string[]): Promise { + let options: SpawnOptions | undefined; + + if (typeof optionsOrArg === 'string') { + args.unshift(optionsOrArg); + } else { + options = optionsOrArg; + } + return new Promise((resolve, reject) => { const cp = spawn(command, args, { cwd: ROOT_DIR, + ...options, }); - let data = ''; - cp.stdout.on('data', chunk => (data += chunk)); - cp.on('close', code => (code === 0 ? resolve(data) : reject(data))); + + if (cp.stdout) { + let data = ''; + cp.stdout.on('data', chunk => (data += chunk)); + cp.on('close', code => (code === 0 ? resolve(data) : reject(data))); + } else { + cp.on('close', code => (code === 0 ? resolve() : reject())); + } + cp.on('error', reject); }); } -export function getJson(url: RequestOptions): Promise { +export function getJsonAsync(url: RequestOptions): Promise { return new Promise((resolve, reject) => { const req = get(url, res => { let data = ''; @@ -46,3 +64,11 @@ export function getJson(url: RequestOptions): Promise { req.end(); }); } + +const readline = createInterface(process.stdin, process.stdout); + +export function questionAsync(message: string): Promise { + return new Promise(resolve => { + readline.question(message, resolve); + }); +}