diff --git a/.changeset/friendly-tools-run.md b/.changeset/friendly-tools-run.md new file mode 100644 index 0000000..ba36420 --- /dev/null +++ b/.changeset/friendly-tools-run.md @@ -0,0 +1,5 @@ +--- +"@changesets/format": patch +--- + +Fix formatter command execution on Windows. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e73b0f..5da28e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,8 +38,8 @@ jobs: annotations: true advanced-security: false - test: - name: Test + checks: + name: Build, lint, and typecheck runs-on: ubuntu-latest timeout-minutes: 10 env: @@ -53,12 +53,6 @@ jobs: - uses: ./.github/actions/ci-setup - # Deno is needed for tests only - - name: Set up Deno - uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 - with: - deno-version: 2.x - - name: Build run: pnpm build @@ -68,6 +62,27 @@ jobs: - name: Typecheck run: pnpm typecheck + test: + name: "Test on ${{ matrix.os }}" + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + fail-fast: false + steps: + - name: Check out repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - uses: ./.github/actions/ci-setup + + - name: Set up Deno + uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 + with: + deno-version: 2.x + - name: Test run: pnpm test @@ -75,7 +90,7 @@ jobs: name: CI OK runs-on: ubuntu-latest if: always() - needs: [lint-workflows, test] + needs: [lint-workflows, checks, test] steps: - name: Exit with error if some jobs are not successful run: exit 1 diff --git a/package.json b/package.json index a4e003b..f21b255 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "release": "changeset publish" }, "dependencies": { - "package-manager-detector": "^1.6.0" + "package-manager-detector": "^1.6.0", + "tinyexec": "^1.2.4" }, "devDependencies": { "@changesets/changelog-github": "^1.0.0-next.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fcbc413..29a4ec3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ importers: package-manager-detector: specifier: ^1.6.0 version: 1.6.0 + tinyexec: + specifier: ^1.2.4 + version: 1.2.4 devDependencies: '@changesets/changelog-github': specifier: ^1.0.0-next.3 @@ -933,10 +936,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.1.2: - resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} - engines: {node: '>=18'} - tinyexec@1.2.4: resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} @@ -1207,7 +1206,7 @@ snapshots: package-manager-detector: 1.6.0 picocolors: 1.1.1 semver: 7.7.4 - tinyexec: 1.1.2 + tinyexec: 1.2.4 transitivePeerDependencies: - '@types/node' @@ -1250,7 +1249,7 @@ snapshots: '@changesets/types': 7.0.0-next.3 '@manypkg/get-packages': 3.1.0 picomatch: 4.0.4 - tinyexec: 1.1.2 + tinyexec: 1.2.4 '@changesets/logger@1.0.0-next.2': dependencies: @@ -1813,8 +1812,6 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.1.2: {} - tinyexec@1.2.4: {} tinyglobby@0.2.17: diff --git a/src/utils.ts b/src/utils.ts index 4384c1b..87aebb8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ -import cp from "node:child_process"; import path from "node:path"; import { resolveCommand, type Agent } from "package-manager-detector"; +import { exec } from "tinyexec"; /** * Traverse upwards from `startDir` to `stopDir` (inclusive), calling `cb` in each directory. If @@ -37,24 +37,8 @@ export async function packageManagerExecute( } export async function spawnProcess(command: string, args: string[], cwd: string): Promise { - return new Promise((resolve, reject) => { - const child = cp.spawn(command, args, { cwd }); - - let stderr = ""; - child.stderr.on("data", (data) => { - stderr += data.toString(); - }); - - child.on("exit", (code) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`${command} exited with code ${code}. Stderr:\n${stderr}`)); - } - }); - - child.on("error", (err) => { - reject(err); - }); + await exec(command, args, { + nodeOptions: { cwd }, + throwOnError: true, }); } diff --git a/tests/format.test.ts b/tests/format.test.ts index 25c60da..5f20dba 100644 --- a/tests/format.test.ts +++ b/tests/format.test.ts @@ -3,28 +3,10 @@ import { expect, test, vi } from "vitest"; import { format, type FormatterName } from "../src/index.ts"; const mocks = vi.hoisted(() => ({ - spawn: vi.fn().mockReturnValue({ - // mock the bare-minimum based on how `src/utils.ts` uses `spawn` - on: (eventName: string, listener: Function) => { - if (eventName === "exit") { - listener(0); // simulate successful exit - } - }, - stderr: { on: () => {} }, - }), + exec: vi.fn().mockResolvedValue({ exitCode: 0, stderr: "", stdout: "" }), })); -vi.mock(import("node:child_process"), async (importOriginal) => { - const mod = await importOriginal(); - return { - ...mod, - spawn: mocks.spawn, - default: { - ...mod.default, - spawn: mocks.spawn, - }, - }; -}); +vi.mock(import("tinyexec"), () => ({ exec: mocks.exec })); const cases: { formatter: FormatterName; expectedCommand: string[] }[] = [ { formatter: "prettier", expectedCommand: ["npx", "prettier", "--write", "file.ts"] }, @@ -42,5 +24,8 @@ test.for(cases)("executes the correct command for $formatter", async (c) => { const result = await format(["file.ts"], { cwd: fixture.path, formatter: c.formatter }); expect(result).toBe(true); const [command, ...args] = c.expectedCommand; - expect(mocks.spawn).toHaveBeenCalledWith(command, args, { cwd: fixture.path }); + expect(mocks.exec).toHaveBeenCalledWith(command, args, { + nodeOptions: { cwd: fixture.path }, + throwOnError: true, + }); });