From 3d297d3068e1791853c950762e342c5c601c9766 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 6 May 2026 18:31:55 +0100 Subject: [PATCH 1/5] wip --- oxfmtrc.json | 7 ++++++- src/commands/format.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/oxfmtrc.json b/oxfmtrc.json index c959087..f0b5cbe 100644 --- a/oxfmtrc.json +++ b/oxfmtrc.json @@ -1,3 +1,8 @@ { - "useTabs": true + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "useTabs": true, + "singleQuote": true, + "printWidth": 100, + "trailingComma": "es5", + "ignorePatterns": ["*.json", "*.md"] } diff --git a/src/commands/format.ts b/src/commands/format.ts index 6e1b04c..d1be452 100644 --- a/src/commands/format.ts +++ b/src/commands/format.ts @@ -1,12 +1,12 @@ -import { fileURLToPath } from "node:url"; -import { x } from "tinyexec"; -import type { CommandContext } from "../context.ts"; -import { local } from "../utils.ts"; +import { fileURLToPath } from 'node:url'; +import { x } from 'tinyexec'; +import type { CommandContext } from '../context.ts'; +import { local } from '../utils.ts'; -const config = fileURLToPath(new URL("../../oxfmtrc.json", import.meta.url)); +const config = fileURLToPath(new URL('../../oxfmtrc.json', import.meta.url)); export async function format(ctx: CommandContext) { - const stdio = x(local("oxfmt"), ["-c", config, "./src", ...ctx.args]); + const stdio = x(local('oxfmt'), ['-c', config, ...ctx.args]); for await (const line of stdio) { console.log(line); From f1f34d9cb4707aab50bc7dbadfb61530f60328bf Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 6 May 2026 18:32:40 +0100 Subject: [PATCH 2/5] Update oxfmtrc.json --- oxfmtrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oxfmtrc.json b/oxfmtrc.json index f0b5cbe..15cd216 100644 --- a/oxfmtrc.json +++ b/oxfmtrc.json @@ -4,5 +4,5 @@ "singleQuote": true, "printWidth": 100, "trailingComma": "es5", - "ignorePatterns": ["*.json", "*.md"] + "ignorePatterns": ["*.json", "*.md", "*.yml", "*.jsonc"] } From 175116f5635a63ad164c4bf5d695c92c2c591247 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 6 May 2026 18:39:09 +0100 Subject: [PATCH 3/5] chore: update oxfmt config --- oxfmtrc.json | 2 - rules/plugin.js | 11 +-- src/bin.ts | 18 ++--- src/commands/build.ts | 12 ++-- src/commands/dev.ts | 24 +++---- src/commands/init.ts | 26 +++---- src/commands/lint.test.ts | 44 ++++++------ src/commands/lint.ts | 116 ++++++++++++++++---------------- src/commands/test.ts | 16 ++--- src/test-utils/fixture.test.ts | 46 ++++++------- src/test-utils/fixture.ts | 38 +++++------ src/test-utils/index.ts | 4 +- src/test-utils/mock.test.ts | 26 +++---- src/test-utils/mock.ts | 12 ++-- src/test-utils/stdio.test.ts | 40 +++++------ src/test-utils/stdio.ts | 4 +- src/test-utils/vitest.config.ts | 10 +-- src/utils.ts | 2 +- 18 files changed, 225 insertions(+), 226 deletions(-) diff --git a/oxfmtrc.json b/oxfmtrc.json index 15cd216..fecb5d3 100644 --- a/oxfmtrc.json +++ b/oxfmtrc.json @@ -2,7 +2,5 @@ "$schema": "./node_modules/oxfmt/configuration_schema.json", "useTabs": true, "singleQuote": true, - "printWidth": 100, - "trailingComma": "es5", "ignorePatterns": ["*.json", "*.md", "*.yml", "*.jsonc"] } diff --git a/rules/plugin.js b/rules/plugin.js index e761719..fedf0fb 100644 --- a/rules/plugin.js +++ b/rules/plugin.js @@ -63,7 +63,10 @@ const plugin = { const src = context.sourceCode.text; let idx = node.start - 1; // Walk backwards past whitespace - while (idx >= 0 && (src[idx] === ' ' || src[idx] === '\t' || src[idx] === '\n' || src[idx] === '\r')) { + while ( + idx >= 0 && + (src[idx] === ' ' || src[idx] === '\t' || src[idx] === '\n' || src[idx] === '\r') + ) { idx--; } // Check if preceding non-whitespace ends with */ @@ -82,8 +85,7 @@ const plugin = { const parent = node.parent; if (!parent) return false; return ( - parent.type === 'ExportNamedDeclaration' || - parent.type === 'ExportDefaultDeclaration' + parent.type === 'ExportNamedDeclaration' || parent.type === 'ExportDefaultDeclaration' ); } @@ -124,8 +126,7 @@ const plugin = { const parent = node.parent; if (!parent) return false; return ( - parent.type === 'ExportNamedDeclaration' || - parent.type === 'ExportDefaultDeclaration' + parent.type === 'ExportNamedDeclaration' || parent.type === 'ExportDefaultDeclaration' ); } diff --git a/src/bin.ts b/src/bin.ts index b1d8ebb..9606e1c 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,11 +1,11 @@ #!/usr/bin/env node -import { argv } from "node:process"; -import { build } from "./commands/build.ts"; -import { dev } from "./commands/dev.ts"; -import { format } from "./commands/format.ts"; -import { init } from "./commands/init.ts"; -import { lint } from "./commands/lint.ts"; -import { test } from "./commands/test.ts"; +import { argv } from 'node:process'; +import { build } from './commands/build.ts'; +import { dev } from './commands/dev.ts'; +import { format } from './commands/format.ts'; +import { init } from './commands/init.ts'; +import { lint } from './commands/lint.ts'; +import { test } from './commands/test.ts'; const commands = { build, dev, format, init, lint, test }; @@ -13,14 +13,14 @@ async function main() { const [command, ...args] = argv.slice(2); if (!command) { - console.log(`No command provided. Available commands: ${Object.keys(commands).join(", ")}\n`); + console.log(`No command provided. Available commands: ${Object.keys(commands).join(', ')}\n`); return; } const run = commands[command as keyof typeof commands]; if (!run) { console.log( - `Unknown command: ${command}. Available commands: ${Object.keys(commands).join(", ")}`, + `Unknown command: ${command}. Available commands: ${Object.keys(commands).join(', ')}`, ); return; } diff --git a/src/commands/build.ts b/src/commands/build.ts index bf26b15..5f3883e 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -1,18 +1,18 @@ -import { parse } from "@bomb.sh/args"; -import { build as tsdown } from "tsdown"; -import type { CommandContext } from "../context.ts"; +import { parse } from '@bomb.sh/args'; +import { build as tsdown } from 'tsdown'; +import type { CommandContext } from '../context.ts'; export async function build(ctx: CommandContext) { const args = parse(ctx.args, { - boolean: ["bundle", "dts", "minify"], + boolean: ['bundle', 'dts', 'minify'], }); - const entry = args._.length > 0 ? args._.map(String) : ["src/**/*.ts", "!src/**/*.test.ts"]; + const entry = args._.length > 0 ? args._.map(String) : ['src/**/*.ts', '!src/**/*.test.ts']; await tsdown({ config: false, entry, - format: "esm", + format: 'esm', sourcemap: true, clean: true, unbundle: !args.bundle, diff --git a/src/commands/dev.ts b/src/commands/dev.ts index c627789..d137e0c 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -1,30 +1,30 @@ -import { x } from "tinyexec"; -import type { CommandContext } from "../context.ts"; +import { x } from 'tinyexec'; +import type { CommandContext } from '../context.ts'; // standardized `dev` command, shells out to `node --strip-types` export async function dev(ctx: CommandContext) { const { args } = ctx; - const [file = "./src/index.ts", ...rest] = args; + const [file = './src/index.ts', ...rest] = args; // console.clear(); console.log( - `node --experimental-transform-types --disable-warning=ExperimentalWarning ${args.join(" ")}`, + `node --experimental-transform-types --disable-warning=ExperimentalWarning ${args.join(' ')}`, ); - const stdio = x("node", [ - "--experimental-transform-types", - "--no-warnings", - "--watch-path=./src/", + const stdio = x('node', [ + '--experimental-transform-types', + '--no-warnings', + '--watch-path=./src/', file, ...rest, ]); - console.log("Starting dev server..."); - console.log("Press Ctrl+C to stop the server."); + console.log('Starting dev server...'); + console.log('Press Ctrl+C to stop the server.'); for await (const line of stdio) { - if (line.startsWith("Restarting")) { + if (line.startsWith('Restarting')) { console.log(line); continue; } - if (line.startsWith("Completed")) { + if (line.startsWith('Completed')) { console.log(); continue; } diff --git a/src/commands/init.ts b/src/commands/init.ts index be4734d..d183f9e 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,24 +1,24 @@ -import { readFile, writeFile } from "node:fs/promises"; -import { cwd } from "node:process"; -import { pathToFileURL } from "node:url"; -import { x } from "tinyexec"; -import type { CommandContext } from "../context.ts"; +import { readFile, writeFile } from 'node:fs/promises'; +import { cwd } from 'node:process'; +import { pathToFileURL } from 'node:url'; +import { x } from 'tinyexec'; +import type { CommandContext } from '../context.ts'; export async function init(ctx: CommandContext) { - const [_name = "."] = ctx.args; + const [_name = '.'] = ctx.args; const cwdUrl = pathToFileURL(`${cwd()}/`); const name = - _name === "." ? new URL("../", cwdUrl).pathname.split("/").filter(Boolean).pop()! : _name; - const dest = new URL("./.temp/", cwdUrl); - for await (const line of x("pnpx", ["giget@latest", "gh:bombshell-dev/template", name])) { + _name === '.' ? new URL('../', cwdUrl).pathname.split('/').filter(Boolean).pop()! : _name; + const dest = new URL('./.temp/', cwdUrl); + for await (const line of x('pnpx', ['giget@latest', 'gh:bombshell-dev/template', name])) { console.log(line); } const promises: Promise[] = []; - for (const file of ["package.json", "README.md"]) { + for (const file of ['package.json', 'README.md']) { promises.push( postprocess(new URL(file, dest), (contents) => { - return contents.replaceAll("$name", name); + return contents.replaceAll('$name', name); }), ); } @@ -29,8 +29,8 @@ async function postprocess( file: URL, transform: (contents: string) => string | undefined | Promise, ) { - const contents = await readFile(file, { encoding: "utf8" }); + const contents = await readFile(file, { encoding: 'utf8' }); const result = await transform(contents); if (!result) return; - await writeFile(file, result, { encoding: "utf8" }); + await writeFile(file, result, { encoding: 'utf8' }); } diff --git a/src/commands/lint.test.ts b/src/commands/lint.test.ts index 7b6b9f0..f442245 100644 --- a/src/commands/lint.test.ts +++ b/src/commands/lint.test.ts @@ -1,9 +1,9 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { createFixture } from "../test-utils/index.ts"; -import { runOxlint, runKnip } from "./lint.ts"; -import { fileURLToPath } from "node:url"; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { createFixture } from '../test-utils/index.ts'; +import { runOxlint, runKnip } from './lint.ts'; +import { fileURLToPath } from 'node:url'; -describe("lint command", () => { +describe('lint command', () => { let originalCwd: string; let fixture: Awaited>; @@ -16,57 +16,57 @@ describe("lint command", () => { if (fixture) await fixture.cleanup(); }); - describe("runOxlint", () => { - it("detects violations in bad code", async () => { + describe('runOxlint', () => { + it('detects violations in bad code', async () => { fixture = await createFixture({ src: { - "index.ts": "var x = 1;", + 'index.ts': 'var x = 1;', }, }); process.chdir(fileURLToPath(fixture.root)); - const violations = await runOxlint(["./src"]); + const violations = await runOxlint(['./src']); expect(violations).not.toEqual([]); expect(violations).toMatchSnapshot(); }); - it("returns empty array for clean code", async () => { + it('returns empty array for clean code', async () => { fixture = await createFixture({ src: { - "index.ts": ` + 'index.ts': ` export const x = 1; `, }, }); process.chdir(fileURLToPath(fixture.root)); - const violations = await runOxlint(["./src"]); + const violations = await runOxlint(['./src']); expect(violations).toEqual([]); }); }); - describe("runKnip", () => { - it("detects unused exports and unused files", async () => { + describe('runKnip', () => { + it('detects unused exports and unused files', async () => { fixture = await createFixture({ - "package.json": { - name: "test-pkg", - version: "1.0.0", - type: "module", - exports: "./src/index.ts", + 'package.json': { + name: 'test-pkg', + version: '1.0.0', + type: 'module', + exports: './src/index.ts', }, src: { - "index.ts": ` + 'index.ts': ` import { used } from "./used"; console.log(used); export const value = 42; `, - "used.ts": ` + 'used.ts': ` export const used = "used"; export const unusedExport = "unusedExport"; `, - "unused-file.ts": ` + 'unused-file.ts': ` export const unused = "unused"; `, }, diff --git a/src/commands/lint.ts b/src/commands/lint.ts index 9232a39..cba79e5 100644 --- a/src/commands/lint.ts +++ b/src/commands/lint.ts @@ -1,18 +1,18 @@ -import { fileURLToPath } from "node:url"; -import { parse } from "@bomb.sh/args"; -import { publint } from "publint"; -import { x } from "tinyexec"; -import type { JSONReport as KnipJSONReport } from "knip"; -import type { CommandContext } from "../context.ts"; -import { local } from "../utils.ts"; +import { fileURLToPath } from 'node:url'; +import { parse } from '@bomb.sh/args'; +import { publint } from 'publint'; +import { x } from 'tinyexec'; +import type { JSONReport as KnipJSONReport } from 'knip'; +import type { CommandContext } from '../context.ts'; +import { local } from '../utils.ts'; -const oxlintConfig = fileURLToPath(new URL("../../oxlintrc.json", import.meta.url)); +const oxlintConfig = fileURLToPath(new URL('../../oxlintrc.json', import.meta.url)); // -- Types -- interface Violation { - tool: "oxlint" | "publint" | "knip" | "tsc"; - level: "error" | "warning" | "suggestion"; + tool: 'oxlint' | 'publint' | 'knip' | 'tsc'; + level: 'error' | 'warning' | 'suggestion'; code: string; message: string; file?: string; @@ -23,9 +23,9 @@ interface Violation { // -- Tool Runners -- export async function runOxlint(targets: string[], fix?: boolean): Promise { - const args = ["-c", oxlintConfig, "--format=json", ...targets]; - if (fix) args.push("--fix"); - const result = await x(local("oxlint"), args, { throwOnError: false }); + const args = ['-c', oxlintConfig, '--format=json', ...targets]; + if (fix) args.push('--fix'); + const result = await x(local('oxlint'), args, { throwOnError: false }); const json = JSON.parse(result.stdout); return (json.diagnostics ?? []).map( (d: { @@ -35,9 +35,9 @@ export async function runOxlint(targets: string[], fix?: boolean): Promise; }) => ({ - tool: "oxlint" as const, - level: d.severity === "error" ? "error" : "warning", - code: d.code ?? "unknown", + tool: 'oxlint' as const, + level: d.severity === 'error' ? 'error' : 'warning', + code: d.code ?? 'unknown', message: d.message, file: d.filename, line: d.labels?.[0]?.span?.line, @@ -49,19 +49,19 @@ export async function runOxlint(targets: string[], fix?: boolean): Promise { const result = await publint({ strict: true }); return result.messages.map((m) => ({ - tool: "publint" as const, - level: m.type === "error" ? "error" : m.type === "warning" ? "warning" : "suggestion", + tool: 'publint' as const, + level: m.type === 'error' ? 'error' : m.type === 'warning' ? 'warning' : 'suggestion', code: m.code, message: m.code, - file: "package.json", + file: 'package.json', line: undefined, column: undefined, })); } export async function runKnip(): Promise { - const args = ["--no-progress", "--reporter", "json"]; - const result = await x(local("knip"), args, { throwOnError: false }); + const args = ['--no-progress', '--reporter', 'json']; + const result = await x(local('knip'), args, { throwOnError: false }); if (!result.stdout.trim()) return []; const json: KnipJSONReport = JSON.parse(result.stdout); @@ -70,9 +70,9 @@ export async function runKnip(): Promise { for (const issue of json.issues) { for (const dep of issue.dependencies ?? []) { violations.push({ - tool: "knip", - level: "warning", - code: "unused-dependency", + tool: 'knip', + level: 'warning', + code: 'unused-dependency', message: `Unused dependency '${dep.name}'`, file: issue.file, line: dep.line, @@ -81,9 +81,9 @@ export async function runKnip(): Promise { } for (const dep of issue.devDependencies ?? []) { violations.push({ - tool: "knip", - level: "warning", - code: "unused-devDependency", + tool: 'knip', + level: 'warning', + code: 'unused-devDependency', message: `Unused devDependency '${dep.name}'`, file: issue.file, line: dep.line, @@ -92,9 +92,9 @@ export async function runKnip(): Promise { } for (const exp of issue.exports ?? []) { violations.push({ - tool: "knip", - level: "warning", - code: "unused-export", + tool: 'knip', + level: 'warning', + code: 'unused-export', message: `Unused export '${exp.name}'`, file: issue.file, line: exp.line, @@ -103,9 +103,9 @@ export async function runKnip(): Promise { } for (const t of issue.types ?? []) { violations.push({ - tool: "knip", - level: "warning", - code: "unused-type", + tool: 'knip', + level: 'warning', + code: 'unused-type', message: `Unused type '${t.name}'`, file: issue.file, line: t.line, @@ -114,9 +114,9 @@ export async function runKnip(): Promise { } for (const file of issue.files ?? []) { violations.push({ - tool: "knip", - level: "warning", - code: "unused-file", + tool: 'knip', + level: 'warning', + code: 'unused-file', message: `Unused file`, file: issue.file, line: file.line, @@ -131,9 +131,9 @@ export async function runKnip(): Promise { async function runTypeScript(targets: string[]): Promise { const args = targets.length > 0 - ? ["--noEmit", "--pretty", "false", ...targets] - : ["--noEmit", "--pretty", "false"]; - const result = await x(local("tsgo"), args, { throwOnError: false }); + ? ['--noEmit', '--pretty', 'false', ...targets] + : ['--noEmit', '--pretty', 'false']; + const result = await x(local('tsgo'), args, { throwOnError: false }); const output = result.stdout + result.stderr; if (!output.trim()) return []; @@ -142,8 +142,8 @@ async function runTypeScript(targets: string[]): Promise { let match: RegExpExecArray | null; while ((match = re.exec(output)) !== null) { violations.push({ - tool: "tsc", - level: match[4] === "error" ? "error" : "warning", + tool: 'tsc', + level: match[4] === 'error' ? 'error' : 'warning', code: match[5]!, message: match[6]!, file: match[1]!, @@ -159,23 +159,23 @@ async function runTypeScript(targets: string[]): Promise { function printViolations(violations: Violation[]) { const grouped = new Map(); for (const v of violations) { - const key = v.file ?? "(project)"; + const key = v.file ?? '(project)'; if (!grouped.has(key)) grouped.set(key, []); grouped.get(key)!.push(v); } const colors = { - error: "\x1b[31m", - warning: "\x1b[33m", - suggestion: "\x1b[34m", - dim: "\x1b[2m", - reset: "\x1b[0m", + error: '\x1b[31m', + warning: '\x1b[33m', + suggestion: '\x1b[34m', + dim: '\x1b[2m', + reset: '\x1b[0m', }; for (const [file, items] of grouped) { console.log(`\n${file}`); for (const v of items) { - const loc = v.line != null ? ` ${v.line}:${v.column ?? 0}` : " -"; + const loc = v.line != null ? ` ${v.line}:${v.column ?? 0}` : ' -'; const color = colors[v.level]; const tag = `${v.tool}/${v.code}`; console.log( @@ -188,19 +188,19 @@ function printViolations(violations: Violation[]) { for (const v of violations) counts[v.level]++; const parts = []; if (counts.error) - parts.push(`${colors.error}${counts.error} error${counts.error > 1 ? "s" : ""}${colors.reset}`); + parts.push(`${colors.error}${counts.error} error${counts.error > 1 ? 's' : ''}${colors.reset}`); if (counts.warning) parts.push( - `${colors.warning}${counts.warning} warning${counts.warning > 1 ? "s" : ""}${colors.reset}`, + `${colors.warning}${counts.warning} warning${counts.warning > 1 ? 's' : ''}${colors.reset}`, ); if (counts.suggestion) parts.push( - `${colors.suggestion}${counts.suggestion} suggestion${counts.suggestion > 1 ? "s" : ""}${colors.reset}`, + `${colors.suggestion}${counts.suggestion} suggestion${counts.suggestion > 1 ? 's' : ''}${colors.reset}`, ); if (parts.length > 0) { - console.log(`\n${parts.join(", ")}`); + console.log(`\n${parts.join(', ')}`); } else { - console.log("\nNo issues found."); + console.log('\nNo issues found.'); } } @@ -216,7 +216,7 @@ async function collectViolations(targets: string[]): Promise { const violations: Violation[] = []; for (const result of results) { - if (result.status === "fulfilled") { + if (result.status === 'fulfilled') { violations.push(...result.value); } else { console.error(result.reason); @@ -227,9 +227,9 @@ async function collectViolations(targets: string[]): Promise { export async function lint(ctx: CommandContext) { const args = parse(ctx.args, { - boolean: ["fix"], + boolean: ['fix'], }); - const targets = args._.length > 0 ? args._.map(String) : ["./src"]; + const targets = args._.length > 0 ? args._.map(String) : ['./src']; if (args.fix) { await runOxlint(targets, true); @@ -240,14 +240,14 @@ export async function lint(ctx: CommandContext) { printViolations(remaining); process.exit(1); } - console.log("No issues found."); + console.log('No issues found.'); return; } // Default: report only const violations = await collectViolations(targets); printViolations(violations); - if (violations.some((v) => v.level === "error")) { + if (violations.some((v) => v.level === 'error')) { process.exit(1); } } diff --git a/src/commands/test.ts b/src/commands/test.ts index 7b29af3..08554ae 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -1,21 +1,21 @@ -import { existsSync } from "node:fs"; -import { fileURLToPath } from "node:url"; -import { x } from "tinyexec"; -import type { CommandContext } from "../context.ts"; -import { local } from "../utils.ts"; +import { existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { x } from 'tinyexec'; +import type { CommandContext } from '../context.ts'; +import { local } from '../utils.ts'; function resolveConfig(): string { // Built output (.mjs) or source (.ts) - for (const ext of [".mjs", ".ts"]) { + for (const ext of ['.mjs', '.ts']) { const url = new URL(`../test-utils/vitest.config${ext}`, import.meta.url); const path = fileURLToPath(url); if (existsSync(path)) return path; } - throw new Error("Could not resolve vitest.config file"); + throw new Error('Could not resolve vitest.config file'); } export async function test(ctx: CommandContext) { - const stdio = x(local("vitest"), ["run", "--config", resolveConfig(), ...ctx.args]); + const stdio = x(local('vitest'), ['run', '--config', resolveConfig(), ...ctx.args]); for await (const line of stdio) { console.log(line); diff --git a/src/test-utils/fixture.test.ts b/src/test-utils/fixture.test.ts index d5fe73f..b71bdcc 100644 --- a/src/test-utils/fixture.test.ts +++ b/src/test-utils/fixture.test.ts @@ -1,40 +1,40 @@ -import { describe, it, expect } from "vitest"; -import { existsSync } from "node:fs"; -import { createFixture } from "./fixture.ts"; +import { describe, it, expect } from 'vitest'; +import { existsSync } from 'node:fs'; +import { createFixture } from './fixture.ts'; -describe("createFixture", () => { - it("creates files on disk from inline tree", async () => { +describe('createFixture', () => { + it('creates files on disk from inline tree', async () => { const fixture = await createFixture({ - "hello.txt": "hello world", + 'hello.txt': 'hello world', }); - expect(await fixture.text("hello.txt")).toBe("hello world"); + expect(await fixture.text('hello.txt')).toBe('hello world'); }); - it("creates nested directories from slash-separated keys", async () => { + it('creates nested directories from slash-separated keys', async () => { const fixture = await createFixture({ - "src/index.ts": "export const x = 1", - "src/utils/helpers.ts": "export function help() {}", + 'src/index.ts': 'export const x = 1', + 'src/utils/helpers.ts': 'export function help() {}', }); - expect(await fixture.isFile("src/index.ts")).toBe(true); - expect(await fixture.isFile("src/utils/helpers.ts")).toBe(true); + expect(await fixture.isFile('src/index.ts')).toBe(true); + expect(await fixture.isFile('src/utils/helpers.ts')).toBe(true); }); - it("resolve returns absolute path within fixture root", async () => { - const fixture = await createFixture({ "a.txt": "" }); - expect(fixture.resolve("a.txt").toString()).toContain(fixture.root.toString()); + it('resolve returns absolute path within fixture root', async () => { + const fixture = await createFixture({ 'a.txt': '' }); + expect(fixture.resolve('a.txt').toString()).toContain(fixture.root.toString()); }); - it("text reads the actual file", async () => { - const fixture = await createFixture({ "a.txt": "Empty" }); - expect(await fixture.text("a.txt")).toEqual("Empty"); - await fixture.write("a.txt", "Hello world!"); - expect(await fixture.text("a.txt")).toEqual("Hello world!"); + it('text reads the actual file', async () => { + const fixture = await createFixture({ 'a.txt': 'Empty' }); + expect(await fixture.text('a.txt')).toEqual('Empty'); + await fixture.write('a.txt', 'Hello world!'); + expect(await fixture.text('a.txt')).toEqual('Hello world!'); }); - it("cleanup removes the temp directory", async () => { - const fixture = await createFixture({ "a.txt": "" }); + it('cleanup removes the temp directory', async () => { + const fixture = await createFixture({ 'a.txt': '' }); const path = fixture.root; - expect(await fixture.isDirectory(".")).toBe(true); + expect(await fixture.isDirectory('.')).toBe(true); await fixture.cleanup(); expect(existsSync(path)).toBe(false); }); diff --git a/src/test-utils/fixture.ts b/src/test-utils/fixture.ts index 2f51e73..bf4fc41 100644 --- a/src/test-utils/fixture.ts +++ b/src/test-utils/fixture.ts @@ -1,10 +1,10 @@ -import { mkdtemp, symlink as fsSymlink } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { sep } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; -import { NodeHfs } from "@humanfs/node"; -import type { HfsImpl } from "@humanfs/types"; -import { expect, onTestFinished } from "vitest"; +import { mkdtemp, symlink as fsSymlink } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { sep } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { NodeHfs } from '@humanfs/node'; +import type { HfsImpl } from '@humanfs/types'; +import { expect, onTestFinished } from 'vitest'; interface ScopedHfsImpl extends Required { text(file: string | URL): Promise; @@ -54,7 +54,7 @@ export interface FileContext { symlink: (target: string) => SymlinkMarker; } -const SYMLINK = Symbol("symlink"); +const SYMLINK = Symbol('symlink'); /** Opaque marker returned by `ctx.symlink()`. */ export interface SymlinkMarker { @@ -87,12 +87,12 @@ export interface FileTree { } function isSymlinkMarker(value: unknown): value is SymlinkMarker { - return typeof value === "object" && value !== null && SYMLINK in value; + return typeof value === 'object' && value !== null && SYMLINK in value; } function isFileTree(value: unknown): value is FileTree { return ( - typeof value === "object" && + typeof value === 'object' && value !== null && !Buffer.isBuffer(value) && !Array.isArray(value) && @@ -148,18 +148,18 @@ function scopeHfs(inner: NodeHfs, base: URL): ScopedHfsImpl { * ``` */ export async function createFixture(files: FileTree): Promise { - const raw = expect.getState().currentTestName ?? "bsh"; + const raw = expect.getState().currentTestName ?? 'bsh'; const prefix = raw .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-|-$/g, ""); + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); const root = new URL(`${prefix}-`, `file://${tmpdir()}/`); const path = await mkdtemp(fileURLToPath(root)); const base = pathToFileURL(path + sep); const inner = new NodeHfs(); const scoped = scopeHfs(inner, base); - const resolve = (...segments: string[]) => new URL(`./${segments.join("/")}`, base); + const resolve = (...segments: string[]) => new URL(`./${segments.join('/')}`, base); const ctx: FileContext = { importMeta: { @@ -177,11 +177,11 @@ export async function createFixture(files: FileTree): Promise { // Nested directory object (not a plain value) if ( - typeof raw !== "function" && + typeof raw !== 'function' && !Buffer.isBuffer(raw) && !Array.isArray(raw) && isFileTree(raw) && - !name.includes(".") + !name.includes('.') ) { await inner.createDirectory(url); // Trailing slash so nested entries resolve relative to the dir @@ -190,11 +190,11 @@ export async function createFixture(files: FileTree): Promise { } // Ensure parent directory exists - const parent = new URL("./", url); + const parent = new URL('./', url); await inner.createDirectory(parent); // Resolve functions - const content = typeof raw === "function" ? raw(ctx) : raw; + const content = typeof raw === 'function' ? raw(ctx) : raw; // Symlink if (isSymlinkMarker(content)) { @@ -209,7 +209,7 @@ export async function createFixture(files: FileTree): Promise { } // JSON auto-serialization for .json files with non-string content - if (name.endsWith(".json") && typeof content !== "string") { + if (name.endsWith('.json') && typeof content !== 'string') { await inner.write(url, JSON.stringify(content, null, 2)); continue; } diff --git a/src/test-utils/index.ts b/src/test-utils/index.ts index b8f5612..34d26a5 100644 --- a/src/test-utils/index.ts +++ b/src/test-utils/index.ts @@ -1,2 +1,2 @@ -export { createFixture } from "./fixture.ts"; -export { createMocks, type Mocks } from "./mock.ts"; +export { createFixture } from './fixture.ts'; +export { createMocks, type Mocks } from './mock.ts'; diff --git a/src/test-utils/mock.test.ts b/src/test-utils/mock.test.ts index 25b9ea6..0c3d0e5 100644 --- a/src/test-utils/mock.test.ts +++ b/src/test-utils/mock.test.ts @@ -1,42 +1,42 @@ -import { describe, it, expect } from "vitest"; -import { createMocks } from "./mock.ts"; -import { MockReadable, MockWritable } from "./stdio.ts"; +import { describe, it, expect } from 'vitest'; +import { createMocks } from './mock.ts'; +import { MockReadable, MockWritable } from './stdio.ts'; -describe("createMocks", () => { - it("returns undefined streams when not requested", () => { +describe('createMocks', () => { + it('returns undefined streams when not requested', () => { const mocks = createMocks(); expect(mocks.input).toBeUndefined(); expect(mocks.output).toBeUndefined(); }); - it("creates input stream with `true`", () => { + it('creates input stream with `true`', () => { const mocks = createMocks({ input: true }); expect(mocks.input).toBeInstanceOf(MockReadable); }); - it("creates output stream with `true`", () => { + it('creates output stream with `true`', () => { const mocks = createMocks({ output: true }); expect(mocks.output).toBeInstanceOf(MockWritable); }); - it("passes config to output stream", () => { + it('passes config to output stream', () => { const mocks = createMocks({ output: { columns: 120, rows: 40, isTTY: true } }); expect(mocks.output.columns).toBe(120); expect(mocks.output.rows).toBe(40); expect(mocks.output.isTTY).toBe(true); }); - it("passes config to input stream", () => { + it('passes config to input stream', () => { const mocks = createMocks({ input: { isTTY: true } }); expect(mocks.input.isTTY).toBe(true); }); - it("stubs env vars for duration of test", () => { - createMocks({ env: { TEST_MOCK_VAR: "hello" } }); - expect(process.env.TEST_MOCK_VAR).toBe("hello"); + it('stubs env vars for duration of test', () => { + createMocks({ env: { TEST_MOCK_VAR: 'hello' } }); + expect(process.env.TEST_MOCK_VAR).toBe('hello'); }); - it("restores env vars after test finishes", async () => { + it('restores env vars after test finishes', async () => { // Previous test's onTestFinished should have cleaned up expect(process.env.TEST_MOCK_VAR).toBeUndefined(); }); diff --git a/src/test-utils/mock.ts b/src/test-utils/mock.ts index a83e07d..4bb8357 100644 --- a/src/test-utils/mock.ts +++ b/src/test-utils/mock.ts @@ -1,5 +1,5 @@ -import { onTestFinished, vi } from "vitest"; -import { MockReadable, MockWritable } from "./stdio.ts"; +import { onTestFinished, vi } from 'vitest'; +import { MockReadable, MockWritable } from './stdio.ts'; type InputConfig = true | ConstructorParameters[0]; type OutputConfig = true | ConstructorParameters[0]; @@ -14,8 +14,8 @@ export interface CreateMockOptions { } export type Mocks = { - input: O["input"] extends InputConfig ? MockReadable : undefined; - output: O["output"] extends OutputConfig ? MockWritable : undefined; + input: O['input'] extends InputConfig ? MockReadable : undefined; + output: O['output'] extends OutputConfig ? MockWritable : undefined; }; /** @@ -38,10 +38,10 @@ export type Mocks = { export function createMocks(opts?: O): Mocks; export function createMocks(opts: CreateMockOptions = {}): Mocks { const input = opts.input - ? new MockReadable(typeof opts.input === "object" ? opts.input : undefined) + ? new MockReadable(typeof opts.input === 'object' ? opts.input : undefined) : undefined; const output = opts.output - ? new MockWritable(typeof opts.output === "object" ? opts.output : undefined) + ? new MockWritable(typeof opts.output === 'object' ? opts.output : undefined) : undefined; if (opts.env) { for (const [key, value] of Object.entries(opts.env)) { diff --git a/src/test-utils/stdio.test.ts b/src/test-utils/stdio.test.ts index 233b287..d34a121 100644 --- a/src/test-utils/stdio.test.ts +++ b/src/test-utils/stdio.test.ts @@ -1,35 +1,35 @@ -import { describe, it, expect } from "vitest"; -import { MockReadable, MockWritable } from "./stdio.ts"; +import { describe, it, expect } from 'vitest'; +import { MockReadable, MockWritable } from './stdio.ts'; -describe("MockReadable", () => { - it("defaults to non-TTY", () => { +describe('MockReadable', () => { + it('defaults to non-TTY', () => { const r = new MockReadable(); expect(r.isTTY).toBe(false); expect(r.isRaw).toBe(false); }); - it("respects isTTY config", () => { + it('respects isTTY config', () => { const r = new MockReadable({ isTTY: true }); expect(r.isTTY).toBe(true); }); - it("setRawMode enables raw mode", () => { + it('setRawMode enables raw mode', () => { const r = new MockReadable(); r.setRawMode(); expect(r.isRaw).toBe(true); }); - it("pushValue delivers data on read", () => + it('pushValue delivers data on read', () => new Promise((resolve) => { const r = new MockReadable(); - r.on("data", (chunk) => { - expect(chunk.toString()).toBe("hello"); + r.on('data', (chunk) => { + expect(chunk.toString()).toBe('hello'); resolve(); }); - r.pushValue("hello"); + r.pushValue('hello'); })); - it("close ends the stream", async () => { + it('close ends the stream', async () => { const r = new MockReadable(); r.close(); @@ -41,32 +41,32 @@ describe("MockReadable", () => { }); }); -describe("MockWritable", () => { - it("defaults to 80x20 non-TTY", () => { +describe('MockWritable', () => { + it('defaults to 80x20 non-TTY', () => { const w = new MockWritable(); expect(w.isTTY).toBe(false); expect(w.columns).toBe(80); expect(w.rows).toBe(20); }); - it("accepts custom config", () => { + it('accepts custom config', () => { const w = new MockWritable({ columns: 120, rows: 40, isTTY: true }); expect(w.isTTY).toBe(true); expect(w.columns).toBe(120); expect(w.rows).toBe(40); }); - it("captures written chunks", () => { + it('captures written chunks', () => { const w = new MockWritable(); - w.write("hello"); - w.write(" world"); - expect(w.buffer).toEqual(["hello", " world"]); + w.write('hello'); + w.write(' world'); + expect(w.buffer).toEqual(['hello', ' world']); }); - it("resize updates dimensions and emits event", () => { + it('resize updates dimensions and emits event', () => { const w = new MockWritable({ columns: 80, rows: 20 }); let resized = false; - w.on("resize", () => { + w.on('resize', () => { resized = true; }); diff --git a/src/test-utils/stdio.ts b/src/test-utils/stdio.ts index fe7ab56..72c5f7c 100644 --- a/src/test-utils/stdio.ts +++ b/src/test-utils/stdio.ts @@ -1,4 +1,4 @@ -import { Readable, Writable, type ReadableOptions, type WritableOptions } from "node:stream"; +import { Readable, Writable, type ReadableOptions, type WritableOptions } from 'node:stream'; export class MockReadable extends Readable { protected _buffer: unknown[] | null = []; @@ -54,7 +54,7 @@ export class MockWritable extends Writable { public resize(columns: number, rows: number): void { this.columns = columns; this.rows = rows; - this.emit("resize"); + this.emit('resize'); } override _write( diff --git a/src/test-utils/vitest.config.ts b/src/test-utils/vitest.config.ts index 878d783..ffcbf9e 100644 --- a/src/test-utils/vitest.config.ts +++ b/src/test-utils/vitest.config.ts @@ -1,12 +1,12 @@ -import { fileURLToPath } from "node:url"; -import { defineConfig } from "vitest/config"; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - exclude: ["dist/**", "node_modules/**"], + exclude: ['dist/**', 'node_modules/**'], env: { - FORCE_COLOR: "1", + FORCE_COLOR: '1', }, - snapshotSerializers: [fileURLToPath(import.meta.resolve("vitest-ansi-serializer"))], + snapshotSerializers: [fileURLToPath(import.meta.resolve('vitest-ansi-serializer'))], }, }); diff --git a/src/utils.ts b/src/utils.ts index c86eacc..f02e0ec 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { fileURLToPath } from "node:url"; +import { fileURLToPath } from 'node:url'; export function local(file: string) { return fileURLToPath(new URL(`../node_modules/.bin/${file}`, import.meta.url)); From 400d677a493f425a4805a7cb82e4ba774c1209c4 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 6 May 2026 20:03:56 +0100 Subject: [PATCH 4/5] chore: extract publint into a separate command --- package.json | 1 + src/bin.ts | 3 ++- src/commands/lint.ts | 23 ++--------------------- src/commands/publint.ts | 20 ++++++++++++++++++++ 4 files changed, 25 insertions(+), 22 deletions(-) create mode 100644 src/commands/publint.ts diff --git a/package.json b/package.json index 7db8761..cfbea79 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "format": "pnpm run bsh format", "init": "pnpm run bsh init", "lint": "pnpm run bsh lint", + "publint": "pnpm run bsh publint", "test": "pnpm run bsh test" }, "dependencies": { diff --git a/src/bin.ts b/src/bin.ts index 9606e1c..0081e9f 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -5,9 +5,10 @@ import { dev } from './commands/dev.ts'; import { format } from './commands/format.ts'; import { init } from './commands/init.ts'; import { lint } from './commands/lint.ts'; +import { publintCommand as publint } from './commands/publint.ts'; import { test } from './commands/test.ts'; -const commands = { build, dev, format, init, lint, test }; +const commands = { build, dev, format, init, lint, publint, test }; async function main() { const [command, ...args] = argv.slice(2); diff --git a/src/commands/lint.ts b/src/commands/lint.ts index cba79e5..8a6f73f 100644 --- a/src/commands/lint.ts +++ b/src/commands/lint.ts @@ -1,6 +1,5 @@ import { fileURLToPath } from 'node:url'; import { parse } from '@bomb.sh/args'; -import { publint } from 'publint'; import { x } from 'tinyexec'; import type { JSONReport as KnipJSONReport } from 'knip'; import type { CommandContext } from '../context.ts'; @@ -46,19 +45,6 @@ export async function runOxlint(targets: string[], fix?: boolean): Promise { - const result = await publint({ strict: true }); - return result.messages.map((m) => ({ - tool: 'publint' as const, - level: m.type === 'error' ? 'error' : m.type === 'warning' ? 'warning' : 'suggestion', - code: m.code, - message: m.code, - file: 'package.json', - line: undefined, - column: undefined, - })); -} - export async function runKnip(): Promise { const args = ['--no-progress', '--reporter', 'json']; const result = await x(local('knip'), args, { throwOnError: false }); @@ -156,7 +142,7 @@ async function runTypeScript(targets: string[]): Promise { // -- Output -- -function printViolations(violations: Violation[]) { +export function printViolations(violations: Violation[]) { const grouped = new Map(); for (const v of violations) { const key = v.file ?? '(project)'; @@ -207,12 +193,7 @@ function printViolations(violations: Violation[]) { // -- Main -- async function collectViolations(targets: string[]): Promise { - const results = await Promise.allSettled([ - runOxlint(targets), - runPublint(), - runKnip(), - runTypeScript(targets), - ]); + const results = await Promise.allSettled([runOxlint(targets), runKnip(), runTypeScript(targets)]); const violations: Violation[] = []; for (const result of results) { diff --git a/src/commands/publint.ts b/src/commands/publint.ts new file mode 100644 index 0000000..2db73c6 --- /dev/null +++ b/src/commands/publint.ts @@ -0,0 +1,20 @@ +import { publint } from 'publint'; +import { printViolations } from './lint.ts'; + +export async function publintCommand() { + const result = await publint({ strict: true }); + const violations = result.messages.map((m) => ({ + tool: 'publint' as const, + level: m.type, + code: m.code, + message: m.code, + file: 'package.json', + line: undefined, + column: undefined, + })); + + printViolations(violations); + if (violations.some((v) => v.level === 'error')) { + process.exit(1); + } +} From 48ff773e07c6f0ed6be42773f4614004526d3397 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 6 May 2026 20:19:59 +0100 Subject: [PATCH 5/5] add changeset --- .changeset/silly-foxes-protect.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/silly-foxes-protect.md diff --git a/.changeset/silly-foxes-protect.md b/.changeset/silly-foxes-protect.md new file mode 100644 index 0000000..08f248a --- /dev/null +++ b/.changeset/silly-foxes-protect.md @@ -0,0 +1,12 @@ +--- +"@bomb.sh/tools": minor +--- + +Updatess `oxfmt` config: + +- Sets `"singleQuote"` option to `true` +- Adds `"*.json", "*.md", "*.yml", "*.jsonc"` to `"ignorePatterns"` option + +Updates `format` command to include all files by default instead of the `./src` directory + +Extracts `publint` from the `lint` command into a separate command