diff --git a/packages/scripts/src/Subprocess.test.ts b/packages/scripts/src/Subprocess.test.ts new file mode 100644 index 0000000..64d279e --- /dev/null +++ b/packages/scripts/src/Subprocess.test.ts @@ -0,0 +1,41 @@ +import {StdioOptions} from './Subprocess' +import {Subprocess} from './Subprocess' + +describe('Subprocess', () => { + + describe('constructor', () => { + it('sets stdio to inherit when log true', () => { + const p = new Subprocess('test', [], {}) + expect(p.stdio).toEqual('pipe') + }) + it('sets stdio to ignore when log false', () => { + const p = new Subprocess('test', [], {}, StdioOptions.IGNORE) + expect(p.stdio).toEqual('ignore') + }) + }) + + describe('run', () => { + + // it('rejects on error', async () => { + // const error = await getError( + // new Subprocess('exit 1', environment, StdioOptions.IGNORE).run()) + // expect(error.message).toMatch(/exited with code 1/) + // }) + + // it('logs errors', async () => { + // await getError( + // new Subprocess( + // 'invalid-non-existing-command', + // environment, StdioOptions.PIPE, + // ) + // .run(), + // ) + // }) + + it('resolves on successful invocation', async () => { + await new Subprocess('ls', [], {}, StdioOptions.IGNORE).run() + }) + + }) + +}) diff --git a/packages/scripts/src/Subprocess.ts b/packages/scripts/src/Subprocess.ts new file mode 100644 index 0000000..16e083a --- /dev/null +++ b/packages/scripts/src/Subprocess.ts @@ -0,0 +1,42 @@ +import {spawn} from 'child_process' + +export enum StdioOptions { + PIPE = 'pipe', + INHERIT = 'inherit', + IGNORE = 'ignore', +} + +export class Subprocess { + + constructor( + public readonly command: string, + public readonly args: readonly string[], + public readonly environment: Record, + public readonly stdio: StdioOptions = StdioOptions.PIPE, + ) {} + + async run() { + return new Promise((resolve, reject) => { + process.stderr.write(`> ${this.command} ${this.args.join(' ')}\n`) + const subprocess = spawn(this.command, this.args, { + shell: false, + stdio: this.stdio, + env: this.environment, + }) + + if (this.stdio === StdioOptions.PIPE) { + subprocess.stdout.on('data', data => process.stdout.write(data)) + subprocess.stderr.on('data', data => process.stderr.write(data)) + } + + subprocess.on('close', code => { + if (code === 0) { + resolve() + } else { + reject(new Error(`"${this.command}" exited with code ${code}`)) + } + }) + subprocess.on('error', reject) + }) + } +} diff --git a/packages/scripts/src/commands/build.ts b/packages/scripts/src/commands/build.ts index 614d4d6..61a8628 100644 --- a/packages/scripts/src/commands/build.ts +++ b/packages/scripts/src/commands/build.ts @@ -1,8 +1,5 @@ -import * as childProcess from 'child_process' +import {run} from '../run' export async function build(path: string) { - // TODO fix this - await childProcess.spawn('npx', ['ttsc', '--build', path], { - stdio: 'inherit', - }) + await run('ttsc', ['--build', path]) } diff --git a/packages/scripts/src/commands/index.ts b/packages/scripts/src/commands/index.ts index 6501a0c..ac960ec 100644 --- a/packages/scripts/src/commands/index.ts +++ b/packages/scripts/src/commands/index.ts @@ -1,2 +1,3 @@ export * from './help' export * from './build' +export * from './watch' diff --git a/packages/scripts/src/commands/watch.ts b/packages/scripts/src/commands/watch.ts new file mode 100644 index 0000000..10d2f31 --- /dev/null +++ b/packages/scripts/src/commands/watch.ts @@ -0,0 +1,5 @@ +import {run} from '../run' + +export async function watch(path: string) { + await run('ttsc', ['--build', '--watch', '--preserveWatchOutput', path]) +} diff --git a/packages/scripts/src/index.ts b/packages/scripts/src/index.ts index fe82d88..4046665 100644 --- a/packages/scripts/src/index.ts +++ b/packages/scripts/src/index.ts @@ -5,7 +5,10 @@ import {TCommand} from './TCommand' async function run(...argv: string[]) { const commandName = argv[0] || 'help' if (!(commandName in commands)) { - throw new Error('Command not found:' + commandName) + const c = Object.keys(commands).filter(cmd => !cmd.startsWith('_')) + console.log( + `Available commands:\n\n${c.join('\n')}`) + return } const command = (commands as any)[commandName] as TCommand await command(...argv.slice(1)) @@ -13,4 +16,8 @@ async function run(...argv: string[]) { if (typeof require !== 'undefined' && require.main === module) { run(...process.argv.slice(2)) + .catch(err => { + console.log('> ' + err.message) + process.exit(1) + }) } diff --git a/packages/scripts/src/modules.test.ts b/packages/scripts/src/modules.test.ts new file mode 100644 index 0000000..05cd18f --- /dev/null +++ b/packages/scripts/src/modules.test.ts @@ -0,0 +1,48 @@ +import {getPathVariable, getPathSeparator, findNodeModules} from './modules' +import {resolve} from 'path' +import {platform} from 'os' + +describe('modules', () => { + + describe('getPathSeparator', () => { + it('returns ";" when win32', () => { + expect(getPathSeparator('win32')).toEqual(';') + }) + it('returns ":" otherwise', () => { + expect(getPathSeparator('linux')).toEqual(':') + expect(getPathSeparator('darwin')).toEqual(':') + expect(getPathSeparator('mac')).toEqual(':') + }) + }) + + describe('findNodeModules', () => { + it('should find node_modules/.bin dirs in parent path(s)', () => { + const dirs = findNodeModules() + expect(dirs.length).toBeGreaterThanOrEqual(1) + }) + it('should not fail when path does not exist', () => { + const dirs = findNodeModules('/non/existing/path/bla/123') + expect(dirs).toEqual([]) + }) + }) + + describe('addToPath', () => { + it('does nothing when pathsToAdd is empty', () => { + const paths = getPathVariable([]) + expect(paths).toEqual(process.env.PATH) + }) + it('adds paths to path variable', () => { + const separator = getPathSeparator(platform()) + const paths = getPathVariable(['/a', '/b'], '/c') + expect(paths).toEqual(`/a${separator}/b${separator}/c`) + }) + it('adds node modules paths to path variable by default', () => { + const paths = findNodeModules() + const separator = getPathSeparator(platform()) + expect(paths.length).toBeGreaterThanOrEqual(1) + expect(getPathVariable()) + .toEqual(`${paths.join(separator)}${separator}${process.env.PATH}`) + }) + }) + +}) diff --git a/packages/scripts/src/modules.ts b/packages/scripts/src/modules.ts new file mode 100644 index 0000000..fe47d11 --- /dev/null +++ b/packages/scripts/src/modules.ts @@ -0,0 +1,36 @@ +import * as fs from 'fs' +import * as path from 'path' +import {platform} from 'os' + +export function getPathSeparator(platformValue: string) { + return platformValue === 'win32' ? ';' : ':' +} + +export function findNodeModules(dir: string = process.cwd()): string[] { + let lastPath = '' + const paths = [] + dir = path.resolve(dir) + while (dir !== lastPath) { + const nodeModulesDir = path.join(dir, 'node_modules', '.bin') + if ( + fs.existsSync(nodeModulesDir) + && fs.statSync(nodeModulesDir).isDirectory() + ) { + paths.push(nodeModulesDir) + } + lastPath = dir + dir = path.resolve(dir, '..') + } + return paths +} + +export function getPathVariable( + pathsToAdd: string[] = findNodeModules(), + currentPath = process.env.PATH, +) { + if (!pathsToAdd.length) { + return currentPath + } + const separator = getPathSeparator(platform()) + return `${pathsToAdd.join(separator)}${separator}${currentPath}` +} diff --git a/packages/scripts/src/run.ts b/packages/scripts/src/run.ts new file mode 100644 index 0000000..67fbd00 --- /dev/null +++ b/packages/scripts/src/run.ts @@ -0,0 +1,10 @@ +import {Subprocess} from './Subprocess' +import {getPathVariable} from './modules' + +export async function run(command: string, args: string[]) { + return new Subprocess(command, args, { + ...process.env, + PATH: getPathVariable(), + }) + .run() +}