[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.

chore: update oxfmt config (#32)

* wip

* Update oxfmtrc.json

* chore: update oxfmt config

* chore: extract publint into a separate command

* add changeset

authored by

Roman and committed by
GitHub
(May 7, 2026, 8:51 PM EDT) e3f58337 677b2e0d

+268 -249
+12
.changeset/silly-foxes-protect.md
··· 1 + --- 2 + "@bomb.sh/tools": minor 3 + --- 4 + 5 + Updatess `oxfmt` config: 6 + 7 + - Sets `"singleQuote"` option to `true` 8 + - Adds `"*.json", "*.md", "*.yml", "*.jsonc"` to `"ignorePatterns"` option 9 + 10 + Updates `format` command to include all files by default instead of the `./src` directory 11 + 12 + Extracts `publint` from the `lint` command into a separate command
+4 -1
oxfmtrc.json
··· 1 1 { 2 - "useTabs": true 2 + "$schema": "./node_modules/oxfmt/configuration_schema.json", 3 + "useTabs": true, 4 + "singleQuote": true, 5 + "ignorePatterns": ["*.json", "*.md", "*.yml", "*.jsonc"] 3 6 }
+1
package.json
··· 57 57 "format": "pnpm run bsh format", 58 58 "init": "pnpm run bsh init", 59 59 "lint": "pnpm run bsh lint", 60 + "publint": "pnpm run bsh publint", 60 61 "test": "pnpm run bsh test" 61 62 }, 62 63 "dependencies": {
+6 -5
rules/plugin.js
··· 63 63 const src = context.sourceCode.text; 64 64 let idx = node.start - 1; 65 65 // Walk backwards past whitespace 66 - while (idx >= 0 && (src[idx] === ' ' || src[idx] === '\t' || src[idx] === '\n' || src[idx] === '\r')) { 66 + while ( 67 + idx >= 0 && 68 + (src[idx] === ' ' || src[idx] === '\t' || src[idx] === '\n' || src[idx] === '\r') 69 + ) { 67 70 idx--; 68 71 } 69 72 // Check if preceding non-whitespace ends with */ ··· 82 85 const parent = node.parent; 83 86 if (!parent) return false; 84 87 return ( 85 - parent.type === 'ExportNamedDeclaration' || 86 - parent.type === 'ExportDefaultDeclaration' 88 + parent.type === 'ExportNamedDeclaration' || parent.type === 'ExportDefaultDeclaration' 87 89 ); 88 90 } 89 91 ··· 124 126 const parent = node.parent; 125 127 if (!parent) return false; 126 128 return ( 127 - parent.type === 'ExportNamedDeclaration' || 128 - parent.type === 'ExportDefaultDeclaration' 129 + parent.type === 'ExportNamedDeclaration' || parent.type === 'ExportDefaultDeclaration' 129 130 ); 130 131 } 131 132
+11 -10
src/bin.ts
··· 1 1 #!/usr/bin/env node 2 - import { argv } from "node:process"; 3 - import { build } from "./commands/build.ts"; 4 - import { dev } from "./commands/dev.ts"; 5 - import { format } from "./commands/format.ts"; 6 - import { init } from "./commands/init.ts"; 7 - import { lint } from "./commands/lint.ts"; 8 - import { test } from "./commands/test.ts"; 2 + import { argv } from 'node:process'; 3 + import { build } from './commands/build.ts'; 4 + import { dev } from './commands/dev.ts'; 5 + import { format } from './commands/format.ts'; 6 + import { init } from './commands/init.ts'; 7 + import { lint } from './commands/lint.ts'; 8 + import { publintCommand as publint } from './commands/publint.ts'; 9 + import { test } from './commands/test.ts'; 9 10 10 - const commands = { build, dev, format, init, lint, test }; 11 + const commands = { build, dev, format, init, lint, publint, test }; 11 12 12 13 async function main() { 13 14 const [command, ...args] = argv.slice(2); 14 15 15 16 if (!command) { 16 - console.log(`No command provided. Available commands: ${Object.keys(commands).join(", ")}\n`); 17 + console.log(`No command provided. Available commands: ${Object.keys(commands).join(', ')}\n`); 17 18 return; 18 19 } 19 20 20 21 const run = commands[command as keyof typeof commands]; 21 22 if (!run) { 22 23 console.log( 23 - `Unknown command: ${command}. Available commands: ${Object.keys(commands).join(", ")}`, 24 + `Unknown command: ${command}. Available commands: ${Object.keys(commands).join(', ')}`, 24 25 ); 25 26 return; 26 27 }
+6 -6
src/commands/build.ts
··· 1 - import { parse } from "@bomb.sh/args"; 2 - import { build as tsdown } from "tsdown"; 3 - import type { CommandContext } from "../context.ts"; 1 + import { parse } from '@bomb.sh/args'; 2 + import { build as tsdown } from 'tsdown'; 3 + import type { CommandContext } from '../context.ts'; 4 4 5 5 export async function build(ctx: CommandContext) { 6 6 const args = parse(ctx.args, { 7 - boolean: ["bundle", "dts", "minify"], 7 + boolean: ['bundle', 'dts', 'minify'], 8 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 12 await tsdown({ 13 13 config: false, 14 14 entry, 15 - format: "esm", 15 + format: 'esm', 16 16 sourcemap: true, 17 17 clean: true, 18 18 unbundle: !args.bundle,
+12 -12
src/commands/dev.ts
··· 1 - import { x } from "tinyexec"; 2 - import type { CommandContext } from "../context.ts"; 1 + import { x } from 'tinyexec'; 2 + import type { CommandContext } from '../context.ts'; 3 3 4 4 // standardized `dev` command, shells out to `node --strip-types` 5 5 export async function dev(ctx: CommandContext) { 6 6 const { args } = ctx; 7 - const [file = "./src/index.ts", ...rest] = args; 7 + const [file = './src/index.ts', ...rest] = args; 8 8 // console.clear(); 9 9 console.log( 10 - `node --experimental-transform-types --disable-warning=ExperimentalWarning ${args.join(" ")}`, 10 + `node --experimental-transform-types --disable-warning=ExperimentalWarning ${args.join(' ')}`, 11 11 ); 12 - const stdio = x("node", [ 13 - "--experimental-transform-types", 14 - "--no-warnings", 15 - "--watch-path=./src/", 12 + const stdio = x('node', [ 13 + '--experimental-transform-types', 14 + '--no-warnings', 15 + '--watch-path=./src/', 16 16 file, 17 17 ...rest, 18 18 ]); 19 - console.log("Starting dev server..."); 20 - console.log("Press Ctrl+C to stop the server."); 19 + console.log('Starting dev server...'); 20 + console.log('Press Ctrl+C to stop the server.'); 21 21 22 22 for await (const line of stdio) { 23 - if (line.startsWith("Restarting")) { 23 + if (line.startsWith('Restarting')) { 24 24 console.log(line); 25 25 continue; 26 26 } 27 - if (line.startsWith("Completed")) { 27 + if (line.startsWith('Completed')) { 28 28 console.log(); 29 29 continue; 30 30 }
+6 -6
src/commands/format.ts
··· 1 - import { fileURLToPath } from "node:url"; 2 - import { x } from "tinyexec"; 3 - import type { CommandContext } from "../context.ts"; 4 - import { local } from "../utils.ts"; 1 + import { fileURLToPath } from 'node:url'; 2 + import { x } from 'tinyexec'; 3 + import type { CommandContext } from '../context.ts'; 4 + import { local } from '../utils.ts'; 5 5 6 - const config = fileURLToPath(new URL("../../oxfmtrc.json", import.meta.url)); 6 + const config = fileURLToPath(new URL('../../oxfmtrc.json', import.meta.url)); 7 7 8 8 export async function format(ctx: CommandContext) { 9 - const stdio = x(local("oxfmt"), ["-c", config, "./src", ...ctx.args]); 9 + const stdio = x(local('oxfmt'), ['-c', config, ...ctx.args]); 10 10 11 11 for await (const line of stdio) { 12 12 console.log(line);
+13 -13
src/commands/init.ts
··· 1 - import { readFile, writeFile } from "node:fs/promises"; 2 - import { cwd } from "node:process"; 3 - import { pathToFileURL } from "node:url"; 4 - import { x } from "tinyexec"; 5 - import type { CommandContext } from "../context.ts"; 1 + import { readFile, writeFile } from 'node:fs/promises'; 2 + import { cwd } from 'node:process'; 3 + import { pathToFileURL } from 'node:url'; 4 + import { x } from 'tinyexec'; 5 + import type { CommandContext } from '../context.ts'; 6 6 7 7 export async function init(ctx: CommandContext) { 8 - const [_name = "."] = ctx.args; 8 + const [_name = '.'] = ctx.args; 9 9 const cwdUrl = pathToFileURL(`${cwd()}/`); 10 10 const name = 11 - _name === "." ? new URL("../", cwdUrl).pathname.split("/").filter(Boolean).pop()! : _name; 12 - const dest = new URL("./.temp/", cwdUrl); 13 - for await (const line of x("pnpx", ["giget@latest", "gh:bombshell-dev/template", name])) { 11 + _name === '.' ? new URL('../', cwdUrl).pathname.split('/').filter(Boolean).pop()! : _name; 12 + const dest = new URL('./.temp/', cwdUrl); 13 + for await (const line of x('pnpx', ['giget@latest', 'gh:bombshell-dev/template', name])) { 14 14 console.log(line); 15 15 } 16 16 17 17 const promises: Promise<void>[] = []; 18 - for (const file of ["package.json", "README.md"]) { 18 + for (const file of ['package.json', 'README.md']) { 19 19 promises.push( 20 20 postprocess(new URL(file, dest), (contents) => { 21 - return contents.replaceAll("$name", name); 21 + return contents.replaceAll('$name', name); 22 22 }), 23 23 ); 24 24 } ··· 29 29 file: URL, 30 30 transform: (contents: string) => string | undefined | Promise<string | undefined>, 31 31 ) { 32 - const contents = await readFile(file, { encoding: "utf8" }); 32 + const contents = await readFile(file, { encoding: 'utf8' }); 33 33 const result = await transform(contents); 34 34 if (!result) return; 35 - await writeFile(file, result, { encoding: "utf8" }); 35 + await writeFile(file, result, { encoding: 'utf8' }); 36 36 }
+22 -22
src/commands/lint.test.ts
··· 1 - import { describe, it, expect, beforeEach, afterEach } from "vitest"; 2 - import { createFixture } from "../test-utils/index.ts"; 3 - import { runOxlint, runKnip } from "./lint.ts"; 4 - import { fileURLToPath } from "node:url"; 1 + import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 + import { createFixture } from '../test-utils/index.ts'; 3 + import { runOxlint, runKnip } from './lint.ts'; 4 + import { fileURLToPath } from 'node:url'; 5 5 6 - describe("lint command", () => { 6 + describe('lint command', () => { 7 7 let originalCwd: string; 8 8 let fixture: Awaited<ReturnType<typeof createFixture>>; 9 9 ··· 16 16 if (fixture) await fixture.cleanup(); 17 17 }); 18 18 19 - describe("runOxlint", () => { 20 - it("detects violations in bad code", async () => { 19 + describe('runOxlint', () => { 20 + it('detects violations in bad code', async () => { 21 21 fixture = await createFixture({ 22 22 src: { 23 - "index.ts": "var x = 1;", 23 + 'index.ts': 'var x = 1;', 24 24 }, 25 25 }); 26 26 process.chdir(fileURLToPath(fixture.root)); 27 27 28 - const violations = await runOxlint(["./src"]); 28 + const violations = await runOxlint(['./src']); 29 29 30 30 expect(violations).not.toEqual([]); 31 31 expect(violations).toMatchSnapshot(); 32 32 }); 33 33 34 - it("returns empty array for clean code", async () => { 34 + it('returns empty array for clean code', async () => { 35 35 fixture = await createFixture({ 36 36 src: { 37 - "index.ts": ` 37 + 'index.ts': ` 38 38 export const x = 1; 39 39 `, 40 40 }, 41 41 }); 42 42 process.chdir(fileURLToPath(fixture.root)); 43 43 44 - const violations = await runOxlint(["./src"]); 44 + const violations = await runOxlint(['./src']); 45 45 46 46 expect(violations).toEqual([]); 47 47 }); 48 48 }); 49 49 50 - describe("runKnip", () => { 51 - it("detects unused exports and unused files", async () => { 50 + describe('runKnip', () => { 51 + it('detects unused exports and unused files', async () => { 52 52 fixture = await createFixture({ 53 - "package.json": { 54 - name: "test-pkg", 55 - version: "1.0.0", 56 - type: "module", 57 - exports: "./src/index.ts", 53 + 'package.json': { 54 + name: 'test-pkg', 55 + version: '1.0.0', 56 + type: 'module', 57 + exports: './src/index.ts', 58 58 }, 59 59 src: { 60 - "index.ts": ` 60 + 'index.ts': ` 61 61 import { used } from "./used"; 62 62 console.log(used); 63 63 export const value = 42; 64 64 `, 65 - "used.ts": ` 65 + 'used.ts': ` 66 66 export const used = "used"; 67 67 export const unusedExport = "unusedExport"; 68 68 `, 69 - "unused-file.ts": ` 69 + 'unused-file.ts': ` 70 70 export const unused = "unused"; 71 71 `, 72 72 },
+56 -75
src/commands/lint.ts
··· 1 - import { fileURLToPath } from "node:url"; 2 - import { parse } from "@bomb.sh/args"; 3 - import { publint } from "publint"; 4 - import { x } from "tinyexec"; 5 - import type { JSONReport as KnipJSONReport } from "knip"; 6 - import type { CommandContext } from "../context.ts"; 7 - import { local } from "../utils.ts"; 1 + import { fileURLToPath } from 'node:url'; 2 + import { parse } from '@bomb.sh/args'; 3 + import { x } from 'tinyexec'; 4 + import type { JSONReport as KnipJSONReport } from 'knip'; 5 + import type { CommandContext } from '../context.ts'; 6 + import { local } from '../utils.ts'; 8 7 9 - const oxlintConfig = fileURLToPath(new URL("../../oxlintrc.json", import.meta.url)); 8 + const oxlintConfig = fileURLToPath(new URL('../../oxlintrc.json', import.meta.url)); 10 9 11 10 // -- Types -- 12 11 13 12 interface Violation { 14 - tool: "oxlint" | "publint" | "knip" | "tsc"; 15 - level: "error" | "warning" | "suggestion"; 13 + tool: 'oxlint' | 'publint' | 'knip' | 'tsc'; 14 + level: 'error' | 'warning' | 'suggestion'; 16 15 code: string; 17 16 message: string; 18 17 file?: string; ··· 23 22 // -- Tool Runners -- 24 23 25 24 export async function runOxlint(targets: string[], fix?: boolean): Promise<Violation[]> { 26 - const args = ["-c", oxlintConfig, "--format=json", ...targets]; 27 - if (fix) args.push("--fix"); 28 - const result = await x(local("oxlint"), args, { throwOnError: false }); 25 + const args = ['-c', oxlintConfig, '--format=json', ...targets]; 26 + if (fix) args.push('--fix'); 27 + const result = await x(local('oxlint'), args, { throwOnError: false }); 29 28 const json = JSON.parse(result.stdout); 30 29 return (json.diagnostics ?? []).map( 31 30 (d: { ··· 35 34 filename?: string; 36 35 labels?: Array<{ span?: { line?: number; column?: number } }>; 37 36 }) => ({ 38 - tool: "oxlint" as const, 39 - level: d.severity === "error" ? "error" : "warning", 40 - code: d.code ?? "unknown", 37 + tool: 'oxlint' as const, 38 + level: d.severity === 'error' ? 'error' : 'warning', 39 + code: d.code ?? 'unknown', 41 40 message: d.message, 42 41 file: d.filename, 43 42 line: d.labels?.[0]?.span?.line, ··· 46 45 ); 47 46 } 48 47 49 - async function runPublint(): Promise<Violation[]> { 50 - const result = await publint({ strict: true }); 51 - return result.messages.map((m) => ({ 52 - tool: "publint" as const, 53 - level: m.type === "error" ? "error" : m.type === "warning" ? "warning" : "suggestion", 54 - code: m.code, 55 - message: m.code, 56 - file: "package.json", 57 - line: undefined, 58 - column: undefined, 59 - })); 60 - } 61 - 62 48 export async function runKnip(): Promise<Violation[]> { 63 - const args = ["--no-progress", "--reporter", "json"]; 64 - const result = await x(local("knip"), args, { throwOnError: false }); 49 + const args = ['--no-progress', '--reporter', 'json']; 50 + const result = await x(local('knip'), args, { throwOnError: false }); 65 51 if (!result.stdout.trim()) return []; 66 52 67 53 const json: KnipJSONReport = JSON.parse(result.stdout); ··· 70 56 for (const issue of json.issues) { 71 57 for (const dep of issue.dependencies ?? []) { 72 58 violations.push({ 73 - tool: "knip", 74 - level: "warning", 75 - code: "unused-dependency", 59 + tool: 'knip', 60 + level: 'warning', 61 + code: 'unused-dependency', 76 62 message: `Unused dependency '${dep.name}'`, 77 63 file: issue.file, 78 64 line: dep.line, ··· 81 67 } 82 68 for (const dep of issue.devDependencies ?? []) { 83 69 violations.push({ 84 - tool: "knip", 85 - level: "warning", 86 - code: "unused-devDependency", 70 + tool: 'knip', 71 + level: 'warning', 72 + code: 'unused-devDependency', 87 73 message: `Unused devDependency '${dep.name}'`, 88 74 file: issue.file, 89 75 line: dep.line, ··· 92 78 } 93 79 for (const exp of issue.exports ?? []) { 94 80 violations.push({ 95 - tool: "knip", 96 - level: "warning", 97 - code: "unused-export", 81 + tool: 'knip', 82 + level: 'warning', 83 + code: 'unused-export', 98 84 message: `Unused export '${exp.name}'`, 99 85 file: issue.file, 100 86 line: exp.line, ··· 103 89 } 104 90 for (const t of issue.types ?? []) { 105 91 violations.push({ 106 - tool: "knip", 107 - level: "warning", 108 - code: "unused-type", 92 + tool: 'knip', 93 + level: 'warning', 94 + code: 'unused-type', 109 95 message: `Unused type '${t.name}'`, 110 96 file: issue.file, 111 97 line: t.line, ··· 114 100 } 115 101 for (const file of issue.files ?? []) { 116 102 violations.push({ 117 - tool: "knip", 118 - level: "warning", 119 - code: "unused-file", 103 + tool: 'knip', 104 + level: 'warning', 105 + code: 'unused-file', 120 106 message: `Unused file`, 121 107 file: issue.file, 122 108 line: file.line, ··· 131 117 async function runTypeScript(targets: string[]): Promise<Violation[]> { 132 118 const args = 133 119 targets.length > 0 134 - ? ["--noEmit", "--pretty", "false", ...targets] 135 - : ["--noEmit", "--pretty", "false"]; 136 - const result = await x(local("tsgo"), args, { throwOnError: false }); 120 + ? ['--noEmit', '--pretty', 'false', ...targets] 121 + : ['--noEmit', '--pretty', 'false']; 122 + const result = await x(local('tsgo'), args, { throwOnError: false }); 137 123 const output = result.stdout + result.stderr; 138 124 if (!output.trim()) return []; 139 125 ··· 142 128 let match: RegExpExecArray | null; 143 129 while ((match = re.exec(output)) !== null) { 144 130 violations.push({ 145 - tool: "tsc", 146 - level: match[4] === "error" ? "error" : "warning", 131 + tool: 'tsc', 132 + level: match[4] === 'error' ? 'error' : 'warning', 147 133 code: match[5]!, 148 134 message: match[6]!, 149 135 file: match[1]!, ··· 156 142 157 143 // -- Output -- 158 144 159 - function printViolations(violations: Violation[]) { 145 + export function printViolations(violations: Violation[]) { 160 146 const grouped = new Map<string, Violation[]>(); 161 147 for (const v of violations) { 162 - const key = v.file ?? "(project)"; 148 + const key = v.file ?? '(project)'; 163 149 if (!grouped.has(key)) grouped.set(key, []); 164 150 grouped.get(key)!.push(v); 165 151 } 166 152 167 153 const colors = { 168 - error: "\x1b[31m", 169 - warning: "\x1b[33m", 170 - suggestion: "\x1b[34m", 171 - dim: "\x1b[2m", 172 - reset: "\x1b[0m", 154 + error: '\x1b[31m', 155 + warning: '\x1b[33m', 156 + suggestion: '\x1b[34m', 157 + dim: '\x1b[2m', 158 + reset: '\x1b[0m', 173 159 }; 174 160 175 161 for (const [file, items] of grouped) { 176 162 console.log(`\n${file}`); 177 163 for (const v of items) { 178 - const loc = v.line != null ? ` ${v.line}:${v.column ?? 0}` : " -"; 164 + const loc = v.line != null ? ` ${v.line}:${v.column ?? 0}` : ' -'; 179 165 const color = colors[v.level]; 180 166 const tag = `${v.tool}/${v.code}`; 181 167 console.log( ··· 188 174 for (const v of violations) counts[v.level]++; 189 175 const parts = []; 190 176 if (counts.error) 191 - parts.push(`${colors.error}${counts.error} error${counts.error > 1 ? "s" : ""}${colors.reset}`); 177 + parts.push(`${colors.error}${counts.error} error${counts.error > 1 ? 's' : ''}${colors.reset}`); 192 178 if (counts.warning) 193 179 parts.push( 194 - `${colors.warning}${counts.warning} warning${counts.warning > 1 ? "s" : ""}${colors.reset}`, 180 + `${colors.warning}${counts.warning} warning${counts.warning > 1 ? 's' : ''}${colors.reset}`, 195 181 ); 196 182 if (counts.suggestion) 197 183 parts.push( 198 - `${colors.suggestion}${counts.suggestion} suggestion${counts.suggestion > 1 ? "s" : ""}${colors.reset}`, 184 + `${colors.suggestion}${counts.suggestion} suggestion${counts.suggestion > 1 ? 's' : ''}${colors.reset}`, 199 185 ); 200 186 if (parts.length > 0) { 201 - console.log(`\n${parts.join(", ")}`); 187 + console.log(`\n${parts.join(', ')}`); 202 188 } else { 203 - console.log("\nNo issues found."); 189 + console.log('\nNo issues found.'); 204 190 } 205 191 } 206 192 207 193 // -- Main -- 208 194 209 195 async function collectViolations(targets: string[]): Promise<Violation[]> { 210 - const results = await Promise.allSettled([ 211 - runOxlint(targets), 212 - runPublint(), 213 - runKnip(), 214 - runTypeScript(targets), 215 - ]); 196 + const results = await Promise.allSettled([runOxlint(targets), runKnip(), runTypeScript(targets)]); 216 197 217 198 const violations: Violation[] = []; 218 199 for (const result of results) { 219 - if (result.status === "fulfilled") { 200 + if (result.status === 'fulfilled') { 220 201 violations.push(...result.value); 221 202 } else { 222 203 console.error(result.reason); ··· 227 208 228 209 export async function lint(ctx: CommandContext) { 229 210 const args = parse(ctx.args, { 230 - boolean: ["fix"], 211 + boolean: ['fix'], 231 212 }); 232 - const targets = args._.length > 0 ? args._.map(String) : ["./src"]; 213 + const targets = args._.length > 0 ? args._.map(String) : ['./src']; 233 214 234 215 if (args.fix) { 235 216 await runOxlint(targets, true); ··· 240 221 printViolations(remaining); 241 222 process.exit(1); 242 223 } 243 - console.log("No issues found."); 224 + console.log('No issues found.'); 244 225 return; 245 226 } 246 227 247 228 // Default: report only 248 229 const violations = await collectViolations(targets); 249 230 printViolations(violations); 250 - if (violations.some((v) => v.level === "error")) { 231 + if (violations.some((v) => v.level === 'error')) { 251 232 process.exit(1); 252 233 } 253 234 }
+20
src/commands/publint.ts
··· 1 + import { publint } from 'publint'; 2 + import { printViolations } from './lint.ts'; 3 + 4 + export async function publintCommand() { 5 + const result = await publint({ strict: true }); 6 + const violations = result.messages.map((m) => ({ 7 + tool: 'publint' as const, 8 + level: m.type, 9 + code: m.code, 10 + message: m.code, 11 + file: 'package.json', 12 + line: undefined, 13 + column: undefined, 14 + })); 15 + 16 + printViolations(violations); 17 + if (violations.some((v) => v.level === 'error')) { 18 + process.exit(1); 19 + } 20 + }
+8 -8
src/commands/test.ts
··· 1 - import { existsSync } from "node:fs"; 2 - import { fileURLToPath } from "node:url"; 3 - import { x } from "tinyexec"; 4 - import type { CommandContext } from "../context.ts"; 5 - import { local } from "../utils.ts"; 1 + import { existsSync } from 'node:fs'; 2 + import { fileURLToPath } from 'node:url'; 3 + import { x } from 'tinyexec'; 4 + import type { CommandContext } from '../context.ts'; 5 + import { local } from '../utils.ts'; 6 6 7 7 function resolveConfig(): string { 8 8 // Built output (.mjs) or source (.ts) 9 - for (const ext of [".mjs", ".ts"]) { 9 + for (const ext of ['.mjs', '.ts']) { 10 10 const url = new URL(`../test-utils/vitest.config${ext}`, import.meta.url); 11 11 const path = fileURLToPath(url); 12 12 if (existsSync(path)) return path; 13 13 } 14 - throw new Error("Could not resolve vitest.config file"); 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 20 for await (const line of stdio) { 21 21 console.log(line);
+23 -23
src/test-utils/fixture.test.ts
··· 1 - import { describe, it, expect } from "vitest"; 2 - import { existsSync } from "node:fs"; 3 - import { createFixture } from "./fixture.ts"; 1 + import { describe, it, expect } from 'vitest'; 2 + import { existsSync } from 'node:fs'; 3 + import { createFixture } from './fixture.ts'; 4 4 5 - describe("createFixture", () => { 6 - it("creates files on disk from inline tree", async () => { 5 + describe('createFixture', () => { 6 + it('creates files on disk from inline tree', async () => { 7 7 const fixture = await createFixture({ 8 - "hello.txt": "hello world", 8 + 'hello.txt': 'hello world', 9 9 }); 10 - expect(await fixture.text("hello.txt")).toBe("hello world"); 10 + expect(await fixture.text('hello.txt')).toBe('hello world'); 11 11 }); 12 12 13 - it("creates nested directories from slash-separated keys", async () => { 13 + it('creates nested directories from slash-separated keys', async () => { 14 14 const fixture = await createFixture({ 15 - "src/index.ts": "export const x = 1", 16 - "src/utils/helpers.ts": "export function help() {}", 15 + 'src/index.ts': 'export const x = 1', 16 + 'src/utils/helpers.ts': 'export function help() {}', 17 17 }); 18 - expect(await fixture.isFile("src/index.ts")).toBe(true); 19 - expect(await fixture.isFile("src/utils/helpers.ts")).toBe(true); 18 + expect(await fixture.isFile('src/index.ts')).toBe(true); 19 + expect(await fixture.isFile('src/utils/helpers.ts')).toBe(true); 20 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()); 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 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!"); 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 32 }); 33 33 34 - it("cleanup removes the temp directory", async () => { 35 - const fixture = await createFixture({ "a.txt": "" }); 34 + it('cleanup removes the temp directory', async () => { 35 + const fixture = await createFixture({ 'a.txt': '' }); 36 36 const path = fixture.root; 37 - expect(await fixture.isDirectory(".")).toBe(true); 37 + expect(await fixture.isDirectory('.')).toBe(true); 38 38 await fixture.cleanup(); 39 39 expect(existsSync(path)).toBe(false); 40 40 });
+19 -19
src/test-utils/fixture.ts
··· 1 - import { mkdtemp, symlink as fsSymlink } from "node:fs/promises"; 2 - import { tmpdir } from "node:os"; 3 - import { sep } from "node:path"; 4 - import { fileURLToPath, pathToFileURL } from "node:url"; 5 - import { NodeHfs } from "@humanfs/node"; 6 - import type { HfsImpl } from "@humanfs/types"; 7 - import { expect, onTestFinished } from "vitest"; 1 + import { mkdtemp, symlink as fsSymlink } from 'node:fs/promises'; 2 + import { tmpdir } from 'node:os'; 3 + import { sep } from 'node:path'; 4 + import { fileURLToPath, pathToFileURL } from 'node:url'; 5 + import { NodeHfs } from '@humanfs/node'; 6 + import type { HfsImpl } from '@humanfs/types'; 7 + import { expect, onTestFinished } from 'vitest'; 8 8 9 9 interface ScopedHfsImpl extends Required<HfsImpl> { 10 10 text(file: string | URL): Promise<string | undefined>; ··· 54 54 symlink: (target: string) => SymlinkMarker; 55 55 } 56 56 57 - const SYMLINK = Symbol("symlink"); 57 + const SYMLINK = Symbol('symlink'); 58 58 59 59 /** Opaque marker returned by `ctx.symlink()`. */ 60 60 export interface SymlinkMarker { ··· 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 94 return ( 95 - typeof value === "object" && 95 + typeof value === 'object' && 96 96 value !== null && 97 97 !Buffer.isBuffer(value) && 98 98 !Array.isArray(value) && ··· 148 148 * ``` 149 149 */ 150 150 export async function createFixture(files: FileTree): Promise<Fixture> { 151 - const raw = expect.getState().currentTestName ?? "bsh"; 151 + const raw = expect.getState().currentTestName ?? 'bsh'; 152 152 const prefix = raw 153 153 .toLowerCase() 154 - .replace(/[^a-z0-9]+/g, "-") 155 - .replace(/^-|-$/g, ""); 154 + .replace(/[^a-z0-9]+/g, '-') 155 + .replace(/^-|-$/g, ''); 156 156 const root = new URL(`${prefix}-`, `file://${tmpdir()}/`); 157 157 const path = await mkdtemp(fileURLToPath(root)); 158 158 const base = pathToFileURL(path + sep); 159 159 160 160 const inner = new NodeHfs(); 161 161 const scoped = scopeHfs(inner, base); 162 - const resolve = (...segments: string[]) => new URL(`./${segments.join("/")}`, base); 162 + const resolve = (...segments: string[]) => new URL(`./${segments.join('/')}`, base); 163 163 164 164 const ctx: FileContext = { 165 165 importMeta: { ··· 177 177 178 178 // Nested directory object (not a plain value) 179 179 if ( 180 - typeof raw !== "function" && 180 + typeof raw !== 'function' && 181 181 !Buffer.isBuffer(raw) && 182 182 !Array.isArray(raw) && 183 183 isFileTree(raw) && 184 - !name.includes(".") 184 + !name.includes('.') 185 185 ) { 186 186 await inner.createDirectory(url); 187 187 // Trailing slash so nested entries resolve relative to the dir ··· 190 190 } 191 191 192 192 // Ensure parent directory exists 193 - const parent = new URL("./", url); 193 + const parent = new URL('./', url); 194 194 await inner.createDirectory(parent); 195 195 196 196 // Resolve functions 197 - const content = typeof raw === "function" ? raw(ctx) : raw; 197 + const content = typeof raw === 'function' ? raw(ctx) : raw; 198 198 199 199 // Symlink 200 200 if (isSymlinkMarker(content)) { ··· 209 209 } 210 210 211 211 // JSON auto-serialization for .json files with non-string content 212 - if (name.endsWith(".json") && typeof content !== "string") { 212 + if (name.endsWith('.json') && typeof content !== 'string') { 213 213 await inner.write(url, JSON.stringify(content, null, 2)); 214 214 continue; 215 215 }
+2 -2
src/test-utils/index.ts
··· 1 - export { createFixture } from "./fixture.ts"; 2 - export { createMocks, type Mocks } from "./mock.ts"; 1 + export { createFixture } from './fixture.ts'; 2 + export { createMocks, type Mocks } from './mock.ts';
+13 -13
src/test-utils/mock.test.ts
··· 1 - import { describe, it, expect } from "vitest"; 2 - import { createMocks } from "./mock.ts"; 3 - import { MockReadable, MockWritable } from "./stdio.ts"; 1 + import { describe, it, expect } from 'vitest'; 2 + import { createMocks } from './mock.ts'; 3 + import { MockReadable, MockWritable } from './stdio.ts'; 4 4 5 - describe("createMocks", () => { 6 - it("returns undefined streams when not requested", () => { 5 + describe('createMocks', () => { 6 + it('returns undefined streams when not requested', () => { 7 7 const mocks = createMocks(); 8 8 expect(mocks.input).toBeUndefined(); 9 9 expect(mocks.output).toBeUndefined(); 10 10 }); 11 11 12 - it("creates input stream with `true`", () => { 12 + it('creates input stream with `true`', () => { 13 13 const mocks = createMocks({ input: true }); 14 14 expect(mocks.input).toBeInstanceOf(MockReadable); 15 15 }); 16 16 17 - it("creates output stream with `true`", () => { 17 + it('creates output stream with `true`', () => { 18 18 const mocks = createMocks({ output: true }); 19 19 expect(mocks.output).toBeInstanceOf(MockWritable); 20 20 }); 21 21 22 - it("passes config to output stream", () => { 22 + it('passes config to output stream', () => { 23 23 const mocks = createMocks({ output: { columns: 120, rows: 40, isTTY: true } }); 24 24 expect(mocks.output.columns).toBe(120); 25 25 expect(mocks.output.rows).toBe(40); 26 26 expect(mocks.output.isTTY).toBe(true); 27 27 }); 28 28 29 - it("passes config to input stream", () => { 29 + it('passes config to input stream', () => { 30 30 const mocks = createMocks({ input: { isTTY: true } }); 31 31 expect(mocks.input.isTTY).toBe(true); 32 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"); 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 37 }); 38 38 39 - it("restores env vars after test finishes", async () => { 39 + it('restores env vars after test finishes', async () => { 40 40 // Previous test's onTestFinished should have cleaned up 41 41 expect(process.env.TEST_MOCK_VAR).toBeUndefined(); 42 42 });
+6 -6
src/test-utils/mock.ts
··· 1 - import { onTestFinished, vi } from "vitest"; 2 - import { MockReadable, MockWritable } from "./stdio.ts"; 1 + import { onTestFinished, vi } from 'vitest'; 2 + import { MockReadable, MockWritable } from './stdio.ts'; 3 3 4 4 type InputConfig = true | ConstructorParameters<typeof MockReadable>[0]; 5 5 type OutputConfig = true | ConstructorParameters<typeof MockWritable>[0]; ··· 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 /** ··· 38 38 export function createMocks<O extends CreateMockOptions>(opts?: O): Mocks<O>; 39 39 export function createMocks(opts: CreateMockOptions = {}): Mocks { 40 40 const input = opts.input 41 - ? new MockReadable(typeof opts.input === "object" ? opts.input : undefined) 41 + ? new MockReadable(typeof opts.input === 'object' ? opts.input : undefined) 42 42 : undefined; 43 43 const output = opts.output 44 - ? new MockWritable(typeof opts.output === "object" ? opts.output : undefined) 44 + ? new MockWritable(typeof opts.output === 'object' ? opts.output : undefined) 45 45 : undefined; 46 46 if (opts.env) { 47 47 for (const [key, value] of Object.entries(opts.env)) {
+20 -20
src/test-utils/stdio.test.ts
··· 1 - import { describe, it, expect } from "vitest"; 2 - import { MockReadable, MockWritable } from "./stdio.ts"; 1 + import { describe, it, expect } from 'vitest'; 2 + import { MockReadable, MockWritable } from './stdio.ts'; 3 3 4 - describe("MockReadable", () => { 5 - it("defaults to non-TTY", () => { 4 + describe('MockReadable', () => { 5 + it('defaults to non-TTY', () => { 6 6 const r = new MockReadable(); 7 7 expect(r.isTTY).toBe(false); 8 8 expect(r.isRaw).toBe(false); 9 9 }); 10 10 11 - it("respects isTTY config", () => { 11 + it('respects isTTY config', () => { 12 12 const r = new MockReadable({ isTTY: true }); 13 13 expect(r.isTTY).toBe(true); 14 14 }); 15 15 16 - it("setRawMode enables raw mode", () => { 16 + it('setRawMode enables raw mode', () => { 17 17 const r = new MockReadable(); 18 18 r.setRawMode(); 19 19 expect(r.isRaw).toBe(true); 20 20 }); 21 21 22 - it("pushValue delivers data on read", () => 22 + it('pushValue delivers data on read', () => 23 23 new Promise<void>((resolve) => { 24 24 const r = new MockReadable(); 25 - r.on("data", (chunk) => { 26 - expect(chunk.toString()).toBe("hello"); 25 + r.on('data', (chunk) => { 26 + expect(chunk.toString()).toBe('hello'); 27 27 resolve(); 28 28 }); 29 - r.pushValue("hello"); 29 + r.pushValue('hello'); 30 30 })); 31 31 32 - it("close ends the stream", async () => { 32 + it('close ends the stream', async () => { 33 33 const r = new MockReadable(); 34 34 r.close(); 35 35 ··· 41 41 }); 42 42 }); 43 43 44 - describe("MockWritable", () => { 45 - it("defaults to 80x20 non-TTY", () => { 44 + describe('MockWritable', () => { 45 + it('defaults to 80x20 non-TTY', () => { 46 46 const w = new MockWritable(); 47 47 expect(w.isTTY).toBe(false); 48 48 expect(w.columns).toBe(80); 49 49 expect(w.rows).toBe(20); 50 50 }); 51 51 52 - it("accepts custom config", () => { 52 + it('accepts custom config', () => { 53 53 const w = new MockWritable({ columns: 120, rows: 40, isTTY: true }); 54 54 expect(w.isTTY).toBe(true); 55 55 expect(w.columns).toBe(120); 56 56 expect(w.rows).toBe(40); 57 57 }); 58 58 59 - it("captures written chunks", () => { 59 + it('captures written chunks', () => { 60 60 const w = new MockWritable(); 61 - w.write("hello"); 62 - w.write(" world"); 63 - expect(w.buffer).toEqual(["hello", " world"]); 61 + w.write('hello'); 62 + w.write(' world'); 63 + expect(w.buffer).toEqual(['hello', ' world']); 64 64 }); 65 65 66 - it("resize updates dimensions and emits event", () => { 66 + it('resize updates dimensions and emits event', () => { 67 67 const w = new MockWritable({ columns: 80, rows: 20 }); 68 68 let resized = false; 69 - w.on("resize", () => { 69 + w.on('resize', () => { 70 70 resized = true; 71 71 }); 72 72
+2 -2
src/test-utils/stdio.ts
··· 1 - import { Readable, Writable, type ReadableOptions, type WritableOptions } from "node:stream"; 1 + import { Readable, Writable, type ReadableOptions, type WritableOptions } from 'node:stream'; 2 2 3 3 export class MockReadable extends Readable { 4 4 protected _buffer: unknown[] | null = []; ··· 54 54 public resize(columns: number, rows: number): void { 55 55 this.columns = columns; 56 56 this.rows = rows; 57 - this.emit("resize"); 57 + this.emit('resize'); 58 58 } 59 59 60 60 override _write(
+5 -5
src/test-utils/vitest.config.ts
··· 1 - import { fileURLToPath } from "node:url"; 2 - import { defineConfig } from "vitest/config"; 1 + import { fileURLToPath } from 'node:url'; 2 + import { defineConfig } from 'vitest/config'; 3 3 4 4 export default defineConfig({ 5 5 test: { 6 - exclude: ["dist/**", "node_modules/**"], 6 + exclude: ['dist/**', 'node_modules/**'], 7 7 env: { 8 - FORCE_COLOR: "1", 8 + FORCE_COLOR: '1', 9 9 }, 10 - snapshotSerializers: [fileURLToPath(import.meta.resolve("vitest-ansi-serializer"))], 10 + snapshotSerializers: [fileURLToPath(import.meta.resolve('vitest-ansi-serializer'))], 11 11 }, 12 12 });
+1 -1
src/utils.ts
··· 1 - import { fileURLToPath } from "node:url"; 1 + import { fileURLToPath } from 'node:url'; 2 2 3 3 export function local(file: string) { 4 4 return fileURLToPath(new URL(`../node_modules/.bin/${file}`, import.meta.url));