diff --git a/scripts/generate-docs-content.ts b/scripts/generate-docs-content.ts index 1fb3954ec..84ea93551 100644 --- a/scripts/generate-docs-content.ts +++ b/scripts/generate-docs-content.ts @@ -1,10 +1,10 @@ -import { execFileSync } from "node:child_process"; import { readFileSync, writeFileSync } from "node:fs"; import { join, relative, sep } from "node:path"; import { globbySync } from "globby"; import { renderHookEventsMatrix } from "./hook-events-table.js"; +import { repoRoot, runOxfmt } from "./run-oxfmt.js"; /** * Embed the canonical `docs/**\/*.md` hierarchy into a generated TypeScript @@ -12,21 +12,9 @@ import { renderHookEventsMatrix } from "./hook-events-table.js"; * (npm dist bundles and bun-compiled binaries alike). The generated file is * committed; CI fails on drift via `pnpm run check:docs-content`. */ -const repoRoot = join(import.meta.dirname, ".."); const docsRoot = join(repoRoot, "docs"); const outputPath = join(repoRoot, "src", "generated", "docs-content.ts"); -// Normalize to the repo's formatter so the drift check compares stable output. -// npx is npx.cmd on Windows; a shell resolves it. stderr is inherited so a -// formatter failure stays diagnosable. -const runOxfmt = (path: string): void => { - execFileSync("npx", ["oxfmt", relative(repoRoot, path)], { - cwd: repoRoot, - stdio: ["ignore", "ignore", "inherit"], - shell: process.platform === "win32", - }); -}; - // Regenerate the derived hook-event matrix inside file-formats.md before // embedding, so a stale committed table fails the docs-content drift check // (`check:docs-content` diffs this file as well as the embed). Written @@ -34,7 +22,7 @@ const runOxfmt = (path: string): void => { // owns the final column layout, mirroring generate-supported-tools-tables.ts. const hookMatrixPath = join(docsRoot, "reference", "file-formats.md"); writeFileSync(hookMatrixPath, renderHookEventsMatrix(readFileSync(hookMatrixPath, "utf8")), "utf8"); -runOxfmt(hookMatrixPath); +runOxfmt([hookMatrixPath]); const filePaths = globbySync("**/*.md", { cwd: docsRoot, @@ -68,6 +56,6 @@ const lines: string[] = [ ]; writeFileSync(outputPath, lines.join("\n"), "utf8"); -runOxfmt(outputPath); +runOxfmt([outputPath]); // oxlint-disable-next-line no-console console.log(`Embedded ${entries.length} docs into ${relative(repoRoot, outputPath)}`); diff --git a/scripts/generate-json-schema.ts b/scripts/generate-json-schema.ts index 83e0e9e01..51f4ee206 100644 --- a/scripts/generate-json-schema.ts +++ b/scripts/generate-json-schema.ts @@ -1,4 +1,3 @@ -import { execFileSync } from "node:child_process"; import { writeFileSync } from "node:fs"; import { join } from "node:path"; @@ -13,6 +12,7 @@ import { } from "../src/constants/rulesync-paths.js"; import { RulesyncMcpFileSchema } from "../src/features/mcp/rulesync-mcp.js"; import { RulesyncPermissionsFileSchema } from "../src/types/permissions.js"; +import { runOxfmt } from "./run-oxfmt.js"; type SchemaMeta = { $id: string; @@ -70,4 +70,4 @@ generateSchema( ); // Format generated schema files with oxfmt for consistent formatting -execFileSync("npx", ["oxfmt", outputPath, mcpOutputPath, permissionsOutputPath]); +runOxfmt([outputPath, mcpOutputPath, permissionsOutputPath]); diff --git a/scripts/generate-supported-tools-tables.ts b/scripts/generate-supported-tools-tables.ts index ada549dcf..5b2fac375 100644 --- a/scripts/generate-supported-tools-tables.ts +++ b/scripts/generate-supported-tools-tables.ts @@ -1,4 +1,3 @@ -import { execFileSync } from "node:child_process"; import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; @@ -7,6 +6,7 @@ import { TOOL_DISPLAY, type ToolDisplayEntry } from "../src/types/tool-display.j import { ALL_TOOL_TARGETS, type ToolTarget } from "../src/types/tool-targets.js"; import { formatError } from "../src/utils/error.js"; import { replaceBetweenMarkers } from "./markdown-markers.js"; +import { runOxfmt } from "./run-oxfmt.js"; const FEATURES = [ "rules", @@ -145,7 +145,7 @@ const main = (): void => { // run it here. Freshness is checked in CI by running this script then // `git diff` (see the `check:supported-tools` package script) — the same // approach as the gitignore generator, avoiding an oxfmt-vs-generator conflict. - execFileSync("pnpm", ["exec", "oxfmt", ...targets.map((t) => t.path)], { stdio: "inherit" }); + runOxfmt(targets.map((t) => t.path)); }; main(); diff --git a/scripts/run-oxfmt.test.ts b/scripts/run-oxfmt.test.ts new file mode 100644 index 000000000..ca6c411ea --- /dev/null +++ b/scripts/run-oxfmt.test.ts @@ -0,0 +1,58 @@ +import { execFileSync } from "node:child_process"; +import { join } from "node:path"; + +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { repoRoot, runOxfmt } from "./run-oxfmt.js"; + +vi.mock("node:child_process", () => ({ + execFileSync: vi.fn(), +})); + +const execFileSyncMock = vi.mocked(execFileSync); + +describe("runOxfmt", () => { + afterEach(() => { + execFileSyncMock.mockClear(); + vi.unstubAllGlobals(); + }); + + it("does not spawn oxfmt when no paths are given, so it cannot format the whole repo", () => { + runOxfmt([]); + + expect(execFileSyncMock).not.toHaveBeenCalled(); + }); + + it("passes repo-root-relative paths and runs from the repo root", () => { + runOxfmt([ + join(repoRoot, "README.md"), + join(repoRoot, "docs", "reference", "supported-tools.md"), + ]); + + expect(execFileSyncMock).toHaveBeenCalledTimes(1); + const [command, args, options] = execFileSyncMock.mock.calls[0]!; + expect(command).toBe("npx"); + expect(args).toEqual([ + "--no-install", + "oxfmt", + "README.md", + join("docs", "reference", "supported-tools.md"), + ]); + expect(options?.cwd).toBe(repoRoot); + }); + + it("resolves npx through a shell only on Windows", () => { + const platform = process.platform; + + Object.defineProperty(process, "platform", { value: "win32", configurable: true }); + try { + runOxfmt([join(repoRoot, "README.md")]); + } finally { + Object.defineProperty(process, "platform", { value: platform, configurable: true }); + } + expect(execFileSyncMock.mock.calls[0]?.[2]?.shell).toBe(true); + + runOxfmt([join(repoRoot, "README.md")]); + expect(execFileSyncMock.mock.calls[1]?.[2]?.shell).toBe(false); + }); +}); diff --git a/scripts/run-oxfmt.ts b/scripts/run-oxfmt.ts new file mode 100644 index 000000000..b216a1fca --- /dev/null +++ b/scripts/run-oxfmt.ts @@ -0,0 +1,25 @@ +import { execFileSync } from "node:child_process"; +import { join, relative } from "node:path"; + +export const repoRoot = join(import.meta.dirname, ".."); + +/** + * Format generated files with the repo's formatter so drift checks compare + * stable output. Shared by the generator scripts (docs content, supported-tools + * tables, JSON schemas) so a fix here reaches every call site. + * + * npx is npx.cmd on Windows; a shell resolves it. `--no-install` keeps npx from + * silently fetching a different oxfmt version when the pinned devDependency is + * missing, which would surface as an unexplained drift-check failure. stderr is + * inherited so a formatter failure stays diagnosable. + */ +export const runOxfmt = (paths: string[]): void => { + if (paths.length === 0) { + return; + } + execFileSync("npx", ["--no-install", "oxfmt", ...paths.map((path) => relative(repoRoot, path))], { + cwd: repoRoot, + stdio: ["ignore", "ignore", "inherit"], + shell: process.platform === "win32", + }); +};