[READ-ONLY] Mirror of https://github.com/bombshell-dev/tools. Internal CLI to standardize tooling across all Bombshell projects
0

Configure Feed

Select the types of activity you want to include in your feed.

[ci] format

authored by

Nate Moore and committed by
bombshell-bot
(Mar 27, 2026, 1:24 AM UTC) 7c8c389f ffba54eb

+372 -372
+14 -14
src/commands/build.ts
··· 3 3 import type { CommandContext } from "../context.ts"; 4 4 5 5 export async function build(ctx: CommandContext) { 6 - const args = parse(ctx.args, { 7 - boolean: ["bundle", "dts", "minify"], 8 - }); 6 + const args = parse(ctx.args, { 7 + boolean: ["bundle", "dts", "minify"], 8 + }); 9 9 10 - const entry = args._.length > 0 ? args._.map(String) : ["src/**/*.ts", "!src/**/*.test.ts"]; 10 + const entry = args._.length > 0 ? args._.map(String) : ["src/**/*.ts", "!src/**/*.test.ts"]; 11 11 12 - await tsdown({ 13 - config: false, 14 - entry, 15 - format: "esm", 16 - sourcemap: true, 17 - clean: true, 18 - unbundle: !args.bundle, 19 - dts: args.dts, 20 - minify: args.minify, 21 - }); 12 + await tsdown({ 13 + config: false, 14 + entry, 15 + format: "esm", 16 + sourcemap: true, 17 + clean: true, 18 + unbundle: !args.bundle, 19 + dts: args.dts, 20 + minify: args.minify, 21 + }); 22 22 }
+31 -31
src/commands/test-utils/fixture.test.ts
··· 3 3 import { createFixture } from "./fixture.ts"; 4 4 5 5 describe("createFixture", () => { 6 - it("creates files on disk from inline tree", async () => { 7 - const fixture = await createFixture({ 8 - "hello.txt": "hello world", 9 - }); 10 - expect(await fixture.text("hello.txt")).toBe("hello world"); 11 - }); 6 + it("creates files on disk from inline tree", async () => { 7 + const fixture = await createFixture({ 8 + "hello.txt": "hello world", 9 + }); 10 + expect(await fixture.text("hello.txt")).toBe("hello world"); 11 + }); 12 12 13 - it("creates nested directories from slash-separated keys", async () => { 14 - const fixture = await createFixture({ 15 - "src/index.ts": "export const x = 1", 16 - "src/utils/helpers.ts": "export function help() {}", 17 - }); 18 - expect(await fixture.isFile("src/index.ts")).toBe(true); 19 - expect(await fixture.isFile("src/utils/helpers.ts")).toBe(true); 20 - }); 13 + it("creates nested directories from slash-separated keys", async () => { 14 + const fixture = await createFixture({ 15 + "src/index.ts": "export const x = 1", 16 + "src/utils/helpers.ts": "export function help() {}", 17 + }); 18 + expect(await fixture.isFile("src/index.ts")).toBe(true); 19 + expect(await fixture.isFile("src/utils/helpers.ts")).toBe(true); 20 + }); 21 21 22 - it("resolve returns absolute path within fixture root", async () => { 23 - const fixture = await createFixture({ "a.txt": "" }); 24 - expect(fixture.resolve("a.txt").toString()).toContain(fixture.root.toString()); 25 - }); 22 + it("resolve returns absolute path within fixture root", async () => { 23 + const fixture = await createFixture({ "a.txt": "" }); 24 + expect(fixture.resolve("a.txt").toString()).toContain(fixture.root.toString()); 25 + }); 26 26 27 - it("text reads the actual file", async () => { 28 - const fixture = await createFixture({ "a.txt": "Empty" }); 29 - expect(await fixture.text("a.txt")).toEqual("Empty"); 30 - await fixture.write("a.txt", "Hello world!"); 31 - expect(await fixture.text("a.txt")).toEqual("Hello world!"); 32 - }); 27 + it("text reads the actual file", async () => { 28 + const fixture = await createFixture({ "a.txt": "Empty" }); 29 + expect(await fixture.text("a.txt")).toEqual("Empty"); 30 + await fixture.write("a.txt", "Hello world!"); 31 + expect(await fixture.text("a.txt")).toEqual("Hello world!"); 32 + }); 33 33 34 - it("cleanup removes the temp directory", async () => { 35 - const fixture = await createFixture({ "a.txt": "" }); 36 - const path = fixture.root; 37 - expect(await fixture.isDirectory(fixture.root)).toBe(true); 38 - await fixture.cleanup(); 39 - expect(existsSync(path)).toBe(false); 40 - }); 34 + it("cleanup removes the temp directory", async () => { 35 + const fixture = await createFixture({ "a.txt": "" }); 36 + const path = fixture.root; 37 + expect(await fixture.isDirectory(fixture.root)).toBe(true); 38 + await fixture.cleanup(); 39 + expect(existsSync(path)).toBe(false); 40 + }); 41 41 });
+140 -140
src/commands/test-utils/fixture.ts
··· 7 7 import { expect, onTestFinished } from "vitest"; 8 8 9 9 interface ScopedHfsImpl extends Required<HfsImpl> { 10 - text(file: string | URL): Promise<string | undefined>; 11 - json(file: string | URL): Promise<unknown | undefined>; 10 + text(file: string | URL): Promise<string | undefined>; 11 + json(file: string | URL): Promise<unknown | undefined>; 12 12 } 13 13 14 14 /** ··· 17 17 * Includes all `hfs` methods — paths are resolved relative to the fixture root. 18 18 */ 19 19 export interface Fixture extends ScopedHfsImpl { 20 - /** The fixture root as a `file://` URL. */ 21 - root: URL; 22 - /** Resolve a relative path within the fixture root. */ 23 - resolve: (...segments: string[]) => URL; 24 - /** Delete the fixture directory. Also runs automatically via `onTestFinished`. */ 25 - cleanup: () => Promise<void>; 20 + /** The fixture root as a `file://` URL. */ 21 + root: URL; 22 + /** Resolve a relative path within the fixture root. */ 23 + resolve: (...segments: string[]) => URL; 24 + /** Delete the fixture directory. Also runs automatically via `onTestFinished`. */ 25 + cleanup: () => Promise<void>; 26 26 } 27 27 28 28 /** Context passed to dynamic file content functions. */ 29 29 export interface FileContext { 30 - /** 31 - * Metadata about the fixture root, analogous to `import.meta`. 32 - * 33 - * - `url` — the fixture root as a `file://` URL string 34 - * - `filename` — absolute filesystem path to the fixture root 35 - * - `dirname` — same as `filename` (root is a directory) 36 - * - `resolve(path)` — resolve a relative path against the fixture root 37 - */ 38 - importMeta: { 39 - url: string; 40 - filename: string; 41 - dirname: string; 42 - resolve: (path: string) => string; 43 - }; 44 - /** 45 - * Create a symbolic link to `target`. 46 - * 47 - * Returns a `SymlinkMarker` — the fixture will create the symlink on disk. 48 - * 49 - * @example 50 - * ```ts 51 - * { 'link.txt': ({ symlink }) => symlink('./target.txt') } 52 - * ``` 53 - */ 54 - symlink: (target: string) => SymlinkMarker; 30 + /** 31 + * Metadata about the fixture root, analogous to `import.meta`. 32 + * 33 + * - `url` — the fixture root as a `file://` URL string 34 + * - `filename` — absolute filesystem path to the fixture root 35 + * - `dirname` — same as `filename` (root is a directory) 36 + * - `resolve(path)` — resolve a relative path against the fixture root 37 + */ 38 + importMeta: { 39 + url: string; 40 + filename: string; 41 + dirname: string; 42 + resolve: (path: string) => string; 43 + }; 44 + /** 45 + * Create a symbolic link to `target`. 46 + * 47 + * Returns a `SymlinkMarker` — the fixture will create the symlink on disk. 48 + * 49 + * @example 50 + * ```ts 51 + * { 'link.txt': ({ symlink }) => symlink('./target.txt') } 52 + * ``` 53 + */ 54 + symlink: (target: string) => SymlinkMarker; 55 55 } 56 56 57 57 const SYMLINK = Symbol("symlink"); 58 58 59 59 /** Opaque marker returned by `ctx.symlink()`. */ 60 60 export interface SymlinkMarker { 61 - [SYMLINK]: true; 62 - target: string; 61 + [SYMLINK]: true; 62 + target: string; 63 63 } 64 64 65 65 /** ··· 74 74 * | Function | `({ importMeta, symlink }) => symlink('./target')` | 75 75 */ 76 76 export type FileTreeValue = 77 - | string 78 - | Buffer 79 - | Record<string, unknown> 80 - | unknown[] 81 - | FileTree 82 - | ((ctx: FileContext) => string | Buffer | SymlinkMarker); 77 + | string 78 + | Buffer 79 + | Record<string, unknown> 80 + | unknown[] 81 + | FileTree 82 + | ((ctx: FileContext) => string | Buffer | SymlinkMarker); 83 83 84 84 /** A recursive tree of files and directories. */ 85 85 export interface FileTree { 86 - [key: string]: FileTreeValue; 86 + [key: string]: FileTreeValue; 87 87 } 88 88 89 89 function isSymlinkMarker(value: unknown): value is SymlinkMarker { 90 - return typeof value === "object" && value !== null && SYMLINK in value; 90 + return typeof value === "object" && value !== null && SYMLINK in value; 91 91 } 92 92 93 93 function isFileTree(value: unknown): value is FileTree { 94 - return ( 95 - typeof value === "object" && 96 - value !== null && 97 - !Buffer.isBuffer(value) && 98 - !Array.isArray(value) && 99 - !isSymlinkMarker(value) 100 - ); 94 + return ( 95 + typeof value === "object" && 96 + value !== null && 97 + !Buffer.isBuffer(value) && 98 + !Array.isArray(value) && 99 + !isSymlinkMarker(value) 100 + ); 101 101 } 102 102 103 103 function scopeHfs(inner: NodeHfs, base: URL): ScopedHfsImpl { 104 - const r = (p: string | URL) => new URL(`./${p}`, base); 105 - const r2 = (a: string | URL, b: string | URL) => [r(a), r(b)] as const; 104 + const r = (p: string | URL) => new URL(`./${p}`, base); 105 + const r2 = (a: string | URL, b: string | URL) => [r(a), r(b)] as const; 106 106 107 - return { 108 - text: (p: string | URL) => inner.text(r(p)), 109 - json: (p: string | URL) => inner.json(r(p)), 110 - bytes: (p) => inner.bytes(r(p)), 111 - write: (p, c) => inner.write(r(p), c), 112 - append: (p, c) => inner.append(r(p), c), 113 - isFile: (p) => inner.isFile(r(p)), 114 - isDirectory: (p) => inner.isDirectory(r(p)), 115 - createDirectory: (p) => inner.createDirectory(r(p)), 116 - delete: (p) => inner.delete(r(p)), 117 - deleteAll: (p) => inner.deleteAll(r(p)), 118 - list: (p) => inner.list(r(p)), 119 - size: (p) => inner.size(r(p)), 120 - lastModified: (p) => inner.lastModified(r(p)), 121 - copy: (s, d) => inner.copy(...r2(s, d)), 122 - copyAll: (s, d) => inner.copyAll(...r2(s, d)), 123 - move: (s, d) => inner.move(...r2(s, d)), 124 - moveAll: (s, d) => inner.moveAll(...r2(s, d)), 125 - }; 107 + return { 108 + text: (p: string | URL) => inner.text(r(p)), 109 + json: (p: string | URL) => inner.json(r(p)), 110 + bytes: (p) => inner.bytes(r(p)), 111 + write: (p, c) => inner.write(r(p), c), 112 + append: (p, c) => inner.append(r(p), c), 113 + isFile: (p) => inner.isFile(r(p)), 114 + isDirectory: (p) => inner.isDirectory(r(p)), 115 + createDirectory: (p) => inner.createDirectory(r(p)), 116 + delete: (p) => inner.delete(r(p)), 117 + deleteAll: (p) => inner.deleteAll(r(p)), 118 + list: (p) => inner.list(r(p)), 119 + size: (p) => inner.size(r(p)), 120 + lastModified: (p) => inner.lastModified(r(p)), 121 + copy: (s, d) => inner.copy(...r2(s, d)), 122 + copyAll: (s, d) => inner.copyAll(...r2(s, d)), 123 + move: (s, d) => inner.move(...r2(s, d)), 124 + moveAll: (s, d) => inner.moveAll(...r2(s, d)), 125 + }; 126 126 } 127 127 128 128 /** ··· 148 148 * ``` 149 149 */ 150 150 export async function createFixture(files: FileTree): Promise<Fixture> { 151 - const raw = expect.getState().currentTestName ?? "bsh"; 152 - const prefix = raw 153 - .toLowerCase() 154 - .replace(/[^a-z0-9]+/g, "-") 155 - .replace(/^-|-$/g, ""); 156 - const root = new URL(`${prefix}-`, `file://${tmpdir()}/`); 157 - const path = await mkdtemp(fileURLToPath(root)); 158 - const base = pathToFileURL(path + sep); 151 + const raw = expect.getState().currentTestName ?? "bsh"; 152 + const prefix = raw 153 + .toLowerCase() 154 + .replace(/[^a-z0-9]+/g, "-") 155 + .replace(/^-|-$/g, ""); 156 + const root = new URL(`${prefix}-`, `file://${tmpdir()}/`); 157 + const path = await mkdtemp(fileURLToPath(root)); 158 + const base = pathToFileURL(path + sep); 159 159 160 - const inner = new NodeHfs(); 161 - const scoped = scopeHfs(inner, base); 162 - const resolve = (...segments: string[]) => new URL(`./${segments.join("/")}`, base); 160 + const inner = new NodeHfs(); 161 + const scoped = scopeHfs(inner, base); 162 + const resolve = (...segments: string[]) => new URL(`./${segments.join("/")}`, base); 163 163 164 - const ctx: FileContext = { 165 - importMeta: { 166 - url: base.toString(), 167 - filename: fileURLToPath(base), 168 - dirname: fileURLToPath(base), 169 - resolve: (p: string) => new URL(`./${p}`, base).toString(), 170 - }, 171 - symlink: (target: string): SymlinkMarker => ({ [SYMLINK]: true, target }), 172 - }; 164 + const ctx: FileContext = { 165 + importMeta: { 166 + url: base.toString(), 167 + filename: fileURLToPath(base), 168 + dirname: fileURLToPath(base), 169 + resolve: (p: string) => new URL(`./${p}`, base).toString(), 170 + }, 171 + symlink: (target: string): SymlinkMarker => ({ [SYMLINK]: true, target }), 172 + }; 173 173 174 - async function writeTree(tree: FileTree, dir: URL): Promise<void> { 175 - for (const [name, raw] of Object.entries(tree)) { 176 - const url = new URL(name, dir); 174 + async function writeTree(tree: FileTree, dir: URL): Promise<void> { 175 + for (const [name, raw] of Object.entries(tree)) { 176 + const url = new URL(name, dir); 177 177 178 - // Nested directory object (not a plain value) 179 - if ( 180 - typeof raw !== "function" && 181 - !Buffer.isBuffer(raw) && 182 - !Array.isArray(raw) && 183 - isFileTree(raw) && 184 - !name.includes(".") 185 - ) { 186 - await inner.createDirectory(url); 187 - // Trailing slash so nested entries resolve relative to the dir 188 - await writeTree(raw, new URL(`${url}/`)); 189 - continue; 190 - } 178 + // Nested directory object (not a plain value) 179 + if ( 180 + typeof raw !== "function" && 181 + !Buffer.isBuffer(raw) && 182 + !Array.isArray(raw) && 183 + isFileTree(raw) && 184 + !name.includes(".") 185 + ) { 186 + await inner.createDirectory(url); 187 + // Trailing slash so nested entries resolve relative to the dir 188 + await writeTree(raw, new URL(`${url}/`)); 189 + continue; 190 + } 191 191 192 - // Ensure parent directory exists 193 - const parent = new URL("./", url); 194 - await inner.createDirectory(parent); 192 + // Ensure parent directory exists 193 + const parent = new URL("./", url); 194 + await inner.createDirectory(parent); 195 195 196 - // Resolve functions 197 - const content = typeof raw === "function" ? raw(ctx) : raw; 196 + // Resolve functions 197 + const content = typeof raw === "function" ? raw(ctx) : raw; 198 198 199 - // Symlink 200 - if (isSymlinkMarker(content)) { 201 - await fsSymlink(content.target, url); 202 - continue; 203 - } 199 + // Symlink 200 + if (isSymlinkMarker(content)) { 201 + await fsSymlink(content.target, url); 202 + continue; 203 + } 204 204 205 - // Buffer 206 - if (Buffer.isBuffer(content)) { 207 - await inner.write(url, content); 208 - continue; 209 - } 205 + // Buffer 206 + if (Buffer.isBuffer(content)) { 207 + await inner.write(url, content); 208 + continue; 209 + } 210 210 211 - // JSON auto-serialization for .json files with non-string content 212 - if (name.endsWith(".json") && typeof content !== "string") { 213 - await inner.write(url, JSON.stringify(content, null, 2)); 214 - continue; 215 - } 211 + // JSON auto-serialization for .json files with non-string content 212 + if (name.endsWith(".json") && typeof content !== "string") { 213 + await inner.write(url, JSON.stringify(content, null, 2)); 214 + continue; 215 + } 216 216 217 - // String content 218 - await inner.write(url, content as string); 219 - } 220 - } 217 + // String content 218 + await inner.write(url, content as string); 219 + } 220 + } 221 221 222 - await writeTree(files, base); 222 + await writeTree(files, base); 223 223 224 - const cleanup = () => inner.deleteAll(path).then(() => undefined); 225 - onTestFinished(cleanup); 224 + const cleanup = () => inner.deleteAll(path).then(() => undefined); 225 + onTestFinished(cleanup); 226 226 227 - return { 228 - root: base, 229 - resolve, 230 - cleanup, 231 - ...scoped, 232 - }; 227 + return { 228 + root: base, 229 + resolve, 230 + cleanup, 231 + ...scoped, 232 + }; 233 233 }
+31 -31
src/commands/test-utils/mock.test.ts
··· 3 3 import { MockReadable, MockWritable } from "./stdio.ts"; 4 4 5 5 describe("createMocks", () => { 6 - it("returns undefined streams when not requested", () => { 7 - const mocks = createMocks(); 8 - expect(mocks.input).toBeUndefined(); 9 - expect(mocks.output).toBeUndefined(); 10 - }); 6 + it("returns undefined streams when not requested", () => { 7 + const mocks = createMocks(); 8 + expect(mocks.input).toBeUndefined(); 9 + expect(mocks.output).toBeUndefined(); 10 + }); 11 11 12 - it("creates input stream with `true`", () => { 13 - const mocks = createMocks({ input: true }); 14 - expect(mocks.input).toBeInstanceOf(MockReadable); 15 - }); 12 + it("creates input stream with `true`", () => { 13 + const mocks = createMocks({ input: true }); 14 + expect(mocks.input).toBeInstanceOf(MockReadable); 15 + }); 16 16 17 - it("creates output stream with `true`", () => { 18 - const mocks = createMocks({ output: true }); 19 - expect(mocks.output).toBeInstanceOf(MockWritable); 20 - }); 17 + it("creates output stream with `true`", () => { 18 + const mocks = createMocks({ output: true }); 19 + expect(mocks.output).toBeInstanceOf(MockWritable); 20 + }); 21 21 22 - it("passes config to output stream", () => { 23 - const mocks = createMocks({ output: { columns: 120, rows: 40, isTTY: true } }); 24 - expect(mocks.output.columns).toBe(120); 25 - expect(mocks.output.rows).toBe(40); 26 - expect(mocks.output.isTTY).toBe(true); 27 - }); 22 + it("passes config to output stream", () => { 23 + const mocks = createMocks({ output: { columns: 120, rows: 40, isTTY: true } }); 24 + expect(mocks.output.columns).toBe(120); 25 + expect(mocks.output.rows).toBe(40); 26 + expect(mocks.output.isTTY).toBe(true); 27 + }); 28 28 29 - it("passes config to input stream", () => { 30 - const mocks = createMocks({ input: { isTTY: true } }); 31 - expect(mocks.input.isTTY).toBe(true); 32 - }); 29 + it("passes config to input stream", () => { 30 + const mocks = createMocks({ input: { isTTY: true } }); 31 + expect(mocks.input.isTTY).toBe(true); 32 + }); 33 33 34 - it("stubs env vars for duration of test", () => { 35 - createMocks({ env: { TEST_MOCK_VAR: "hello" } }); 36 - expect(process.env.TEST_MOCK_VAR).toBe("hello"); 37 - }); 34 + it("stubs env vars for duration of test", () => { 35 + createMocks({ env: { TEST_MOCK_VAR: "hello" } }); 36 + expect(process.env.TEST_MOCK_VAR).toBe("hello"); 37 + }); 38 38 39 - it("restores env vars after test finishes", async () => { 40 - // Previous test's onTestFinished should have cleaned up 41 - expect(process.env.TEST_MOCK_VAR).toBeUndefined(); 42 - }); 39 + it("restores env vars after test finishes", async () => { 40 + // Previous test's onTestFinished should have cleaned up 41 + expect(process.env.TEST_MOCK_VAR).toBeUndefined(); 42 + }); 43 43 });
+24 -24
src/commands/test-utils/mock.ts
··· 5 5 type OutputConfig = true | ConstructorParameters<typeof MockWritable>[0]; 6 6 7 7 export interface CreateMockOptions { 8 - /** Environment variables to set for the duration of the test. */ 9 - env?: Record<string, string | undefined>; 10 - /** Pass `true` for defaults, or a config object. Omit to skip. */ 11 - input?: InputConfig; 12 - /** Pass `true` for defaults, or a config object for columns/rows/isTTY. */ 13 - output?: OutputConfig; 8 + /** Environment variables to set for the duration of the test. */ 9 + env?: Record<string, string | undefined>; 10 + /** Pass `true` for defaults, or a config object. Omit to skip. */ 11 + input?: InputConfig; 12 + /** Pass `true` for defaults, or a config object for columns/rows/isTTY. */ 13 + output?: OutputConfig; 14 14 } 15 15 16 16 export type Mocks<O extends CreateMockOptions = CreateMockOptions> = { 17 - input: O["input"] extends InputConfig ? MockReadable : undefined; 18 - output: O["output"] extends OutputConfig ? MockWritable : undefined; 17 + input: O["input"] extends InputConfig ? MockReadable : undefined; 18 + output: O["output"] extends OutputConfig ? MockWritable : undefined; 19 19 }; 20 20 21 21 /** ··· 37 37 */ 38 38 export function createMocks<O extends CreateMockOptions>(opts?: O): Mocks<O>; 39 39 export function createMocks(opts: CreateMockOptions = {}): Mocks { 40 - const input = opts.input 41 - ? new MockReadable(typeof opts.input === "object" ? opts.input : undefined) 42 - : undefined; 43 - const output = opts.output 44 - ? new MockWritable(typeof opts.output === "object" ? opts.output : undefined) 45 - : undefined; 46 - if (opts.env) { 47 - for (const [key, value] of Object.entries(opts.env)) { 48 - vi.stubEnv(key, value); 49 - } 50 - } 40 + const input = opts.input 41 + ? new MockReadable(typeof opts.input === "object" ? opts.input : undefined) 42 + : undefined; 43 + const output = opts.output 44 + ? new MockWritable(typeof opts.output === "object" ? opts.output : undefined) 45 + : undefined; 46 + if (opts.env) { 47 + for (const [key, value] of Object.entries(opts.env)) { 48 + vi.stubEnv(key, value); 49 + } 50 + } 51 51 52 - onTestFinished(() => { 53 - vi.unstubAllEnvs(); 54 - vi.restoreAllMocks(); 55 - }); 52 + onTestFinished(() => { 53 + vi.unstubAllEnvs(); 54 + vi.restoreAllMocks(); 55 + }); 56 56 57 - return { input, output } as Mocks; 57 + return { input, output } as Mocks; 58 58 }
+61 -61
src/commands/test-utils/stdio.test.ts
··· 2 2 import { MockReadable, MockWritable } from "./stdio.ts"; 3 3 4 4 describe("MockReadable", () => { 5 - it("defaults to non-TTY", () => { 6 - const r = new MockReadable(); 7 - expect(r.isTTY).toBe(false); 8 - expect(r.isRaw).toBe(false); 9 - }); 5 + it("defaults to non-TTY", () => { 6 + const r = new MockReadable(); 7 + expect(r.isTTY).toBe(false); 8 + expect(r.isRaw).toBe(false); 9 + }); 10 10 11 - it("respects isTTY config", () => { 12 - const r = new MockReadable({ isTTY: true }); 13 - expect(r.isTTY).toBe(true); 14 - }); 11 + it("respects isTTY config", () => { 12 + const r = new MockReadable({ isTTY: true }); 13 + expect(r.isTTY).toBe(true); 14 + }); 15 15 16 - it("setRawMode enables raw mode", () => { 17 - const r = new MockReadable(); 18 - r.setRawMode(); 19 - expect(r.isRaw).toBe(true); 20 - }); 16 + it("setRawMode enables raw mode", () => { 17 + const r = new MockReadable(); 18 + r.setRawMode(); 19 + expect(r.isRaw).toBe(true); 20 + }); 21 21 22 - it("pushValue delivers data on read", () => 23 - new Promise<void>((resolve) => { 24 - const r = new MockReadable(); 25 - r.on("data", (chunk) => { 26 - expect(chunk.toString()).toBe("hello"); 27 - resolve(); 28 - }); 29 - r.pushValue("hello"); 30 - })); 22 + it("pushValue delivers data on read", () => 23 + new Promise<void>((resolve) => { 24 + const r = new MockReadable(); 25 + r.on("data", (chunk) => { 26 + expect(chunk.toString()).toBe("hello"); 27 + resolve(); 28 + }); 29 + r.pushValue("hello"); 30 + })); 31 31 32 - it("close ends the stream", async () => { 33 - const r = new MockReadable(); 34 - r.close(); 32 + it("close ends the stream", async () => { 33 + const r = new MockReadable(); 34 + r.close(); 35 35 36 - const chunks: string[] = []; 37 - for await (const chunk of r) { 38 - chunks.push(chunk.toString()); 39 - } 40 - expect(chunks).toEqual([]); 41 - }); 36 + const chunks: string[] = []; 37 + for await (const chunk of r) { 38 + chunks.push(chunk.toString()); 39 + } 40 + expect(chunks).toEqual([]); 41 + }); 42 42 }); 43 43 44 44 describe("MockWritable", () => { 45 - it("defaults to 80x20 non-TTY", () => { 46 - const w = new MockWritable(); 47 - expect(w.isTTY).toBe(false); 48 - expect(w.columns).toBe(80); 49 - expect(w.rows).toBe(20); 50 - }); 45 + it("defaults to 80x20 non-TTY", () => { 46 + const w = new MockWritable(); 47 + expect(w.isTTY).toBe(false); 48 + expect(w.columns).toBe(80); 49 + expect(w.rows).toBe(20); 50 + }); 51 51 52 - it("accepts custom config", () => { 53 - const w = new MockWritable({ columns: 120, rows: 40, isTTY: true }); 54 - expect(w.isTTY).toBe(true); 55 - expect(w.columns).toBe(120); 56 - expect(w.rows).toBe(40); 57 - }); 52 + it("accepts custom config", () => { 53 + const w = new MockWritable({ columns: 120, rows: 40, isTTY: true }); 54 + expect(w.isTTY).toBe(true); 55 + expect(w.columns).toBe(120); 56 + expect(w.rows).toBe(40); 57 + }); 58 58 59 - it("captures written chunks", () => { 60 - const w = new MockWritable(); 61 - w.write("hello"); 62 - w.write(" world"); 63 - expect(w.buffer).toEqual(["hello", " world"]); 64 - }); 59 + it("captures written chunks", () => { 60 + const w = new MockWritable(); 61 + w.write("hello"); 62 + w.write(" world"); 63 + expect(w.buffer).toEqual(["hello", " world"]); 64 + }); 65 65 66 - it("resize updates dimensions and emits event", () => { 67 - const w = new MockWritable({ columns: 80, rows: 20 }); 68 - let resized = false; 69 - w.on("resize", () => { 70 - resized = true; 71 - }); 66 + it("resize updates dimensions and emits event", () => { 67 + const w = new MockWritable({ columns: 80, rows: 20 }); 68 + let resized = false; 69 + w.on("resize", () => { 70 + resized = true; 71 + }); 72 72 73 - w.resize(120, 40); 73 + w.resize(120, 40); 74 74 75 - expect(w.columns).toBe(120); 76 - expect(w.rows).toBe(40); 77 - expect(resized).toBe(true); 78 - }); 75 + expect(w.columns).toBe(120); 76 + expect(w.rows).toBe(40); 77 + expect(resized).toBe(true); 78 + }); 79 79 });
+52 -52
src/commands/test-utils/stdio.ts
··· 1 1 import { Readable, Writable, type ReadableOptions, type WritableOptions } from "node:stream"; 2 2 3 3 export class MockReadable extends Readable { 4 - protected _buffer: unknown[] | null = []; 5 - public isTTY = false; 6 - public isRaw = false; 7 - public setRawMode() { 8 - this.isRaw = true; 9 - } 4 + protected _buffer: unknown[] | null = []; 5 + public isTTY = false; 6 + public isRaw = false; 7 + public setRawMode() { 8 + this.isRaw = true; 9 + } 10 10 11 - constructor(config?: { isTTY?: boolean }, opts?: ReadableOptions) { 12 - super(opts); 13 - this.isTTY = config?.isTTY ?? false; 14 - } 11 + constructor(config?: { isTTY?: boolean }, opts?: ReadableOptions) { 12 + super(opts); 13 + this.isTTY = config?.isTTY ?? false; 14 + } 15 15 16 - override _read() { 17 - if (this._buffer === null) { 18 - this.push(null); 19 - return; 20 - } 16 + override _read() { 17 + if (this._buffer === null) { 18 + this.push(null); 19 + return; 20 + } 21 21 22 - for (const val of this._buffer) { 23 - this.push(val); 24 - } 22 + for (const val of this._buffer) { 23 + this.push(val); 24 + } 25 25 26 - this._buffer = []; 27 - } 26 + this._buffer = []; 27 + } 28 28 29 - pushValue(val: unknown): void { 30 - this._buffer?.push(val); 31 - } 29 + pushValue(val: unknown): void { 30 + this._buffer?.push(val); 31 + } 32 32 33 - close(): void { 34 - this._buffer = null; 35 - } 33 + close(): void { 34 + this._buffer = null; 35 + } 36 36 } 37 37 38 38 export class MockWritable extends Writable { 39 - public buffer: string[] = []; 40 - public isTTY = false; 41 - public columns = 80; 42 - public rows = 20; 39 + public buffer: string[] = []; 40 + public isTTY = false; 41 + public columns = 80; 42 + public rows = 20; 43 43 44 - constructor( 45 - config?: { columns?: number; rows?: number; isTTY?: boolean }, 46 - opts?: WritableOptions, 47 - ) { 48 - super(opts); 49 - this.isTTY = config?.isTTY ?? false; 50 - this.columns = config?.columns ?? 80; 51 - this.rows = config?.rows ?? 20; 52 - } 44 + constructor( 45 + config?: { columns?: number; rows?: number; isTTY?: boolean }, 46 + opts?: WritableOptions, 47 + ) { 48 + super(opts); 49 + this.isTTY = config?.isTTY ?? false; 50 + this.columns = config?.columns ?? 80; 51 + this.rows = config?.rows ?? 20; 52 + } 53 53 54 - public resize(columns: number, rows: number): void { 55 - this.columns = columns; 56 - this.rows = rows; 57 - this.emit("resize"); 58 - } 54 + public resize(columns: number, rows: number): void { 55 + this.columns = columns; 56 + this.rows = rows; 57 + this.emit("resize"); 58 + } 59 59 60 - override _write( 61 - chunk: any, 62 - _encoding: BufferEncoding, 63 - callback: (error?: Error | null | undefined) => void, 64 - ): void { 65 - this.buffer.push(chunk.toString()); 66 - callback(); 67 - } 60 + override _write( 61 + chunk: any, 62 + _encoding: BufferEncoding, 63 + callback: (error?: Error | null | undefined) => void, 64 + ): void { 65 + this.buffer.push(chunk.toString()); 66 + callback(); 67 + } 68 68 }
+8 -8
src/commands/test-utils/vitest.config.ts
··· 1 - import { defineConfig } from 'vitest/config'; 1 + import { defineConfig } from "vitest/config"; 2 2 3 3 export default defineConfig({ 4 - test: { 5 - exclude: ['dist/**', 'node_modules/**'], 6 - env: { 7 - FORCE_COLOR: '1', 8 - }, 9 - snapshotSerializers: ['vitest-ansi-serializer'], 10 - }, 4 + test: { 5 + exclude: ["dist/**", "node_modules/**"], 6 + env: { 7 + FORCE_COLOR: "1", 8 + }, 9 + snapshotSerializers: ["vitest-ansi-serializer"], 10 + }, 11 11 });
+11 -11
src/commands/test.ts
··· 5 5 import { local } from "../utils.ts"; 6 6 7 7 function resolveConfig(): string { 8 - // Built output (.mjs) or source (.ts) 9 - for (const ext of [".mjs", ".ts"]) { 10 - const url = new URL(`./test-utils/vitest.config${ext}`, import.meta.url); 11 - const path = fileURLToPath(url); 12 - if (existsSync(path)) return path; 13 - } 14 - throw new Error("Could not resolve vitest.config file"); 8 + // Built output (.mjs) or source (.ts) 9 + for (const ext of [".mjs", ".ts"]) { 10 + const url = new URL(`./test-utils/vitest.config${ext}`, import.meta.url); 11 + const path = fileURLToPath(url); 12 + if (existsSync(path)) return path; 13 + } 14 + throw new Error("Could not resolve vitest.config file"); 15 15 } 16 16 17 17 export async function test(ctx: CommandContext) { 18 - const stdio = x(local("vitest"), ["run", "--config", resolveConfig(), ...ctx.args]); 18 + const stdio = x(local("vitest"), ["run", "--config", resolveConfig(), ...ctx.args]); 19 19 20 - for await (const line of stdio) { 21 - console.log(line); 22 - } 20 + for await (const line of stdio) { 21 + console.log(line); 22 + } 23 23 }