diff --git a/package.json b/package.json index 34559f8..1bb3e97 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "node": ">=18.19" }, "scripts": { - "test": "xo && c8 ava && tsc ./source/index.d.ts" + "test": "xo && c8 ava && npm run type", + "type": "tsd -t ./source/index.d.ts -f ./source/index.test-d.ts" }, "files": [ "source/**/*.js", @@ -47,11 +48,13 @@ "execa" ], "devDependencies": { + "@types/node": "^22.5.4", "ava": "^6.1.3", "c8": "^10.1.2", "get-node": "^15.0.1", "path-key": "^4.0.0", "tempy": "^3.1.0", + "tsd": "^0.31.2", "typescript": "^5.5.4", "xo": "^0.59.3", "yoctocolors": "^2.1.1" diff --git a/source/index.d.ts b/source/index.d.ts index f04a577..7a1f382 100644 --- a/source/index.d.ts +++ b/source/index.d.ts @@ -1,12 +1,52 @@ -export type Options = { - readonly timeout: number; - readonly signal: AbortSignal; - // Readonly nativeOptions; +import type {ChildProcess, SpawnOptions} from 'node:child_process'; + +type StdioOption = Readonly[number]>; +type StdinOption = StdioOption | {readonly string?: string}; + +export type Options = Omit & Readonly>>; +}>>; + +export type Result = { + stdout: string; + + stderr: string; + + output: string; + + command: string; + + durationMs: number; +}; + +export type SubprocessError = Error & Result & { + exitCode?: number; + + signalName?: string; +}; + +export type Subprocess = Promise & AsyncIterable & { + nodeChildProcess: Promise; + + stdout: AsyncIterable; + + stderr: AsyncIterable; + + pipe(file: string, arguments?: readonly string[], options?: Options): Subprocess; + pipe(file: string, options?: Options): Subprocess; }; -// TODO: Finish this when the API is decided on. -export function nanoSpawn( - command: string, - arguments: readonly string[], - options?: Options -): Promise; +export default function nanoSpawn(file: string, arguments?: readonly string[], options?: Options): Subprocess; +export default function nanoSpawn(file: string, options?: Options): Subprocess; diff --git a/source/index.test-d.ts b/source/index.test-d.ts new file mode 100644 index 0000000..b142536 --- /dev/null +++ b/source/index.test-d.ts @@ -0,0 +1,144 @@ +import type {ChildProcess} from 'node:child_process'; +import { + expectType, + expectAssignable, + expectNotAssignable, + expectError, +} from 'tsd'; +import nanoSpawn, { + type Options, + type Result, + type SubprocessError, + type Subprocess, +} from './index.js'; + +try { + const result = await nanoSpawn('test'); + expectType(result); + expectType(result.stdout); + expectType(result.stderr); + expectType(result.output); + expectType(result.command); + expectType(result.durationMs); + expectNotAssignable(result); + expectError(result.exitCode); + expectError(result.signalName); + expectError(result.other); +} catch (error) { + const subprocessError = error as SubprocessError; + expectType(subprocessError.stdout); + expectType(subprocessError.stderr); + expectType(subprocessError.output); + expectType(subprocessError.command); + expectType(subprocessError.durationMs); + expectAssignable(subprocessError); + expectType(subprocessError.exitCode); + expectType(subprocessError.signalName); + expectError(subprocessError.other); +} + +expectAssignable({} as const); +expectAssignable({argv0: 'test'} as const); +expectNotAssignable({other: 'test'} as const); +expectNotAssignable('test'); + +await nanoSpawn('test', {argv0: 'test'} as const); +expectError(await nanoSpawn('test', {argv0: true} as const)); +await nanoSpawn('test', {preferLocal: true} as const); +expectError(await nanoSpawn('test', {preferLocal: 'true'} as const)); +await nanoSpawn('test', {env: {}} as const); +// eslint-disable-next-line @typescript-eslint/naming-convention +await nanoSpawn('test', {env: {TEST: 'test'}} as const); +expectError(await nanoSpawn('test', {env: true} as const)); +// eslint-disable-next-line @typescript-eslint/naming-convention +expectError(await nanoSpawn('test', {env: {TEST: true}} as const)); +await nanoSpawn('test', {stdin: 'pipe'} as const); +await nanoSpawn('test', {stdin: {string: 'test'} as const} as const); +expectError(await nanoSpawn('test', {stdin: {string: true} as const} as const)); +expectError(await nanoSpawn('test', {stdin: {other: 'test'} as const} as const)); +expectError(await nanoSpawn('test', {stdin: true} as const)); +await nanoSpawn('test', {stdout: 'pipe'} as const); +expectError(await nanoSpawn('test', {stdout: {string: 'test'} as const} as const)); +expectError(await nanoSpawn('test', {stdout: true} as const)); +await nanoSpawn('test', {stderr: 'pipe'} as const); +expectError(await nanoSpawn('test', {stderr: {string: 'test'} as const} as const)); +expectError(await nanoSpawn('test', {stderr: true} as const)); +await nanoSpawn('test', {stdio: ['pipe', 'pipe', 'pipe'] as const} as const); +await nanoSpawn('test', {stdio: [{string: 'test'} as const, 'pipe', 'pipe'] as const} as const); +expectError(await nanoSpawn('test', {stdio: ['pipe', {string: 'test'} as const, 'pipe'] as const} as const)); +expectError(await nanoSpawn('test', {stdio: ['pipe', 'pipe', {string: 'test'} as const] as const} as const)); +expectError(await nanoSpawn('test', {stdio: [{string: true} as const, 'pipe', 'pipe'] as const} as const)); +expectError(await nanoSpawn('test', {stdio: [{other: 'test'} as const, 'pipe', 'pipe'] as const} as const)); +expectError(await nanoSpawn('test', {stdio: [true, true, true] as const} as const)); +await nanoSpawn('test', {stdio: 'pipe'} as const); +expectError(await nanoSpawn('test', {stdio: true} as const)); +expectError(await nanoSpawn('test', {other: 'test'} as const)); + +expectError(await nanoSpawn()); +expectError(await nanoSpawn(true)); +await nanoSpawn('test', [] as const); +await nanoSpawn('test', ['one'] as const); +expectError(await nanoSpawn('test', [true] as const)); +await nanoSpawn('test', {} as const); +expectError(await nanoSpawn('test', true)); +await nanoSpawn('test', ['one'] as const, {} as const); +expectError(await nanoSpawn('test', ['one'] as const, true)); +expectError(await nanoSpawn('test', ['one'] as const, {} as const, true)); + +expectError(await nanoSpawn('test').pipe()); +expectError(await nanoSpawn('test').pipe(true)); +await nanoSpawn('test').pipe('test', [] as const); +await nanoSpawn('test').pipe('test', ['one'] as const); +expectError(await nanoSpawn('test').pipe('test', [true] as const)); +await nanoSpawn('test').pipe('test', {} as const); +expectError(await nanoSpawn('test').pipe('test', true)); +await nanoSpawn('test').pipe('test', ['one'] as const, {} as const); +expectError(await nanoSpawn('test').pipe('test', ['one'] as const, true)); +expectError(await nanoSpawn('test').pipe('test', ['one'] as const, {} as const, true)); + +expectError(await nanoSpawn('test').pipe('test').pipe()); +expectError(await nanoSpawn('test').pipe('test').pipe(true)); +await nanoSpawn('test').pipe('test').pipe('test', [] as const); +await nanoSpawn('test').pipe('test').pipe('test', ['one'] as const); +expectError(await nanoSpawn('test').pipe('test').pipe('test', [true] as const)); +await nanoSpawn('test').pipe('test').pipe('test', {} as const); +expectError(await nanoSpawn('test').pipe('test').pipe('test', true)); +await nanoSpawn('test').pipe('test').pipe('test', ['one'] as const, {} as const); +expectError(await nanoSpawn('test').pipe('test').pipe('test', ['one'] as const, true)); +expectError(await nanoSpawn('test').pipe('test').pipe('test', ['one'] as const, {} as const, true)); + +expectType(nanoSpawn('test').pipe('test')); +expectType(nanoSpawn('test').pipe('test').pipe('test')); +expectType(await nanoSpawn('test').pipe('test')); +expectType(await nanoSpawn('test').pipe('test').pipe('test')); + +for await (const line of nanoSpawn('test')) { + expectType(line); +} + +for await (const line of nanoSpawn('test').pipe('test')) { + expectType(line); +} + +for await (const line of nanoSpawn('test').stdout) { + expectType(line); +} + +for await (const line of nanoSpawn('test').pipe('test').stdout) { + expectType(line); +} + +for await (const line of nanoSpawn('test').stderr) { + expectType(line); +} + +for await (const line of nanoSpawn('test').pipe('test').stderr) { + expectType(line); +} + +const subprocess = nanoSpawn('test'); +expectType(subprocess); + +const nodeChildProcess = await subprocess.nodeChildProcess; +expectType(nodeChildProcess); +expectType(nodeChildProcess.pid); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..09fcfd6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "target": "ES2022", + "strict": true + }, + "files": [ + "source/index.d.ts" + ] +}