diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a347f9da..0e65c386a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: [main] pull_request: branches: [main] + types: [opened, synchronize, reopened, labeled] workflow_call: jobs: @@ -23,10 +24,13 @@ jobs: - run: pnpm check test: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + # Windows runs on main pushes and PRs labeled "test: windows" + os: '${{ (github.event_name == ''push'' || contains(github.event.pull_request.labels.*.name, ''test: windows'')) && fromJSON(''["blacksmith-4vcpu-ubuntu-2404","windows-latest"]'') || fromJSON(''["blacksmith-4vcpu-ubuntu-2404"]'') }}' + suite: [unit, core-markdown, metaframeworks, pages-react-vue, pages-svelte-solid] include: - suite: unit script: test:unit @@ -52,6 +56,19 @@ jobs: - run: pnpm build - run: pnpm exec playwright install --with-deps chromium - if: matrix.suite == 'core-markdown' || matrix.suite == 'metaframeworks' || startsWith(matrix.suite, 'pages-') + if: matrix.suite != 'unit' && !contains(matrix.os, 'windows') + + - run: pnpm exec playwright install chromium + if: matrix.suite != 'unit' && contains(matrix.os, 'windows') - run: pnpm ${{ matrix.script }}${{ matrix.retry && format(' --retry {0}', matrix.retry) || '' }} + + done: + runs-on: ubuntu-latest + if: always() + needs: + - lint-and-check + - test + steps: + - run: exit 1 + if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} diff --git a/package.json b/package.json index d2e62c12e..6f093b41d 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ "test": "vitest --run" }, "devDependencies": { + "@types/cross-spawn": "^6.0.6", "@typescript/native-preview": "catalog:", + "cross-spawn": "^7.0.6", "husky": "^9.1.7", "lint-staged": "^16.4.0", "oxfmt": "^0.42.0", diff --git a/packages/md/test/integration/markdown-blog.test.ts b/packages/md/test/integration/markdown-blog.test.ts index 5e26a2f54..e34300363 100644 --- a/packages/md/test/integration/markdown-blog.test.ts +++ b/packages/md/test/integration/markdown-blog.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, beforeAll, afterAll } from "vitest"; -import { execSync, spawn } from "node:child_process"; +import { execSync } from "node:child_process"; +import spawn from "cross-spawn"; import { join } from "node:path"; import { chromium, type Browser, type Page } from "playwright"; import { getPort, waitForServer, killChild } from "../../../../test/integration-helpers"; diff --git a/packages/react/test/integration/react-pages-client.test.ts b/packages/react/test/integration/react-pages-client.test.ts index f72261523..820a6f3ea 100644 --- a/packages/react/test/integration/react-pages-client.test.ts +++ b/packages/react/test/integration/react-pages-client.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, beforeAll, afterAll } from "vitest"; -import { execSync, spawn } from "node:child_process"; +import { execSync } from "node:child_process"; +import spawn from "cross-spawn"; import { join } from "node:path"; import { readFileSync, writeFileSync } from "node:fs"; import { chromium, type Browser, type Page } from "playwright"; diff --git a/packages/solid/test/integration/solid-pages-playground.test.ts b/packages/solid/test/integration/solid-pages-playground.test.ts index 5f5ebb150..df2177541 100644 --- a/packages/solid/test/integration/solid-pages-playground.test.ts +++ b/packages/solid/test/integration/solid-pages-playground.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { execSync, spawn } from "node:child_process"; +import { execSync } from "node:child_process"; +import spawn from "cross-spawn"; import { join } from "node:path"; import { readFileSync, writeFileSync } from "node:fs"; import { chromium, type Browser, type Page } from "playwright"; diff --git a/packages/svelte/test/integration/svelte-pages-client.test.ts b/packages/svelte/test/integration/svelte-pages-client.test.ts index a207401ba..e128c9807 100644 --- a/packages/svelte/test/integration/svelte-pages-client.test.ts +++ b/packages/svelte/test/integration/svelte-pages-client.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, beforeAll, afterAll } from "vitest"; -import { execSync, spawn } from "node:child_process"; +import { execSync } from "node:child_process"; +import spawn from "cross-spawn"; import { join } from "node:path"; import { readFileSync, writeFileSync } from "node:fs"; import { chromium, type Browser, type Page } from "playwright"; diff --git a/packages/void/test/integration/astro.test.ts b/packages/void/test/integration/astro.test.ts index 1989ea024..4fb4858d7 100644 --- a/packages/void/test/integration/astro.test.ts +++ b/packages/void/test/integration/astro.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { execSync, spawn } from "node:child_process"; +import { execSync } from "node:child_process"; +import spawn from "cross-spawn"; import { existsSync, readFileSync, rmSync } from "node:fs"; import { join } from "node:path"; import { getPort, waitForServer, killChild } from "../../../../test/integration-helpers"; diff --git a/packages/void/test/integration/kitchen-sink-build.test.ts b/packages/void/test/integration/kitchen-sink-build.test.ts index 00a15db39..60e6d6821 100644 --- a/packages/void/test/integration/kitchen-sink-build.test.ts +++ b/packages/void/test/integration/kitchen-sink-build.test.ts @@ -1,7 +1,8 @@ import { beforeAll, describe, expect, it } from "vitest"; -import { spawnSync } from "node:child_process"; +import spawn from "cross-spawn"; import { existsSync, readFileSync, readdirSync, rmSync } from "node:fs"; import { join } from "node:path"; +import { toSlash } from "../../src/shared/utils"; const ROOT = join(import.meta.dirname, "..", "..", "..", ".."); const PLAYGROUND = "playground/kitchen-sink"; @@ -15,7 +16,7 @@ describe("playground/kitchen-sink build", () => { rmSync(join(pg, "dist"), { recursive: true, force: true }); rmSync(join(pg, ".void"), { recursive: true, force: true }); - const result = spawnSync("pnpm", ["--dir", PLAYGROUND, "build"], { + const result = spawn.sync("pnpm", ["--dir", PLAYGROUND, "build"], { cwd: ROOT, encoding: "utf8", maxBuffer: 10 * 1024 * 1024, @@ -51,7 +52,7 @@ describe("playground/kitchen-sink build", () => { function collectFiles(dir: string): string[] { const files: string[] = []; for (const entry of readdirSync(dir, { withFileTypes: true })) { - const entryPath = join(dir, entry.name); + const entryPath = toSlash(join(dir, entry.name)); if (entry.isDirectory()) { files.push(...collectFiles(entryPath)); } else { diff --git a/packages/void/test/integration/kitchen-sink.test.ts b/packages/void/test/integration/kitchen-sink.test.ts index 2b9619fb4..351095e31 100644 --- a/packages/void/test/integration/kitchen-sink.test.ts +++ b/packages/void/test/integration/kitchen-sink.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, beforeAll, afterAll } from "vitest"; -import { execSync, spawn } from "node:child_process"; +import { execSync } from "node:child_process"; +import spawn from "cross-spawn"; import { join } from "node:path"; import { rmSync } from "node:fs"; import { chromium, type Browser, type Page } from "playwright"; diff --git a/packages/void/test/integration/named-layouts.test.ts b/packages/void/test/integration/named-layouts.test.ts index a7ab1c76e..b9a802402 100644 --- a/packages/void/test/integration/named-layouts.test.ts +++ b/packages/void/test/integration/named-layouts.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, beforeAll, afterAll } from "vitest"; -import { spawn } from "node:child_process"; +import spawn from "cross-spawn"; import { join } from "node:path"; import { getPort, waitForServer, killChild } from "../../../../test/integration-helpers"; diff --git a/packages/void/test/integration/node-target.test.ts b/packages/void/test/integration/node-target.test.ts index fec409d8e..d7645fd3b 100644 --- a/packages/void/test/integration/node-target.test.ts +++ b/packages/void/test/integration/node-target.test.ts @@ -1,10 +1,11 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { execSync, spawn } from "node:child_process"; +import { execSync } from "node:child_process"; +import spawn from "cross-spawn"; import { existsSync, readFileSync, rmSync } from "node:fs"; import { join } from "node:path"; import { getPort, waitForServer, killChild } from "../../../../test/integration-helpers"; -const ROOT = new URL("../../../..", import.meta.url).pathname; +const ROOT = join(import.meta.dirname, "..", "..", "..", ".."); let DEV_PORT: number; let DEV_BASE: string; let PROD_PORT: number; diff --git a/packages/void/test/integration/nuxt.test.ts b/packages/void/test/integration/nuxt.test.ts index 408aa127b..409aa7b72 100644 --- a/packages/void/test/integration/nuxt.test.ts +++ b/packages/void/test/integration/nuxt.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { execSync, spawn } from "node:child_process"; +import { execSync } from "node:child_process"; +import spawn from "cross-spawn"; import { existsSync, readFileSync, rmSync } from "node:fs"; import { join } from "node:path"; import { getPort, waitForServer, killChild } from "../../../../test/integration-helpers"; diff --git a/packages/void/test/integration/pg-test.test.ts b/packages/void/test/integration/pg-test.test.ts index 1f8c571a3..a9999ced4 100644 --- a/packages/void/test/integration/pg-test.test.ts +++ b/packages/void/test/integration/pg-test.test.ts @@ -1,6 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { readFileSync, writeFileSync, unlinkSync } from "node:fs"; -import { spawn } from "node:child_process"; +import spawn from "cross-spawn"; import { join } from "node:path"; import { once } from "node:events"; import { PGlite } from "@electric-sql/pglite"; diff --git a/packages/void/test/integration/react-router.test.ts b/packages/void/test/integration/react-router.test.ts index 41cf9561d..0b0b22384 100644 --- a/packages/void/test/integration/react-router.test.ts +++ b/packages/void/test/integration/react-router.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { execSync, spawn } from "node:child_process"; +import { execSync } from "node:child_process"; +import spawn from "cross-spawn"; import { existsSync, readFileSync, rmSync } from "node:fs"; import { join } from "node:path"; import { getPort, waitForServer, killChild } from "../../../../test/integration-helpers"; diff --git a/packages/void/test/integration/ssr-playground.test.ts b/packages/void/test/integration/ssr-playground.test.ts index e0355d3e2..23570e219 100644 --- a/packages/void/test/integration/ssr-playground.test.ts +++ b/packages/void/test/integration/ssr-playground.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { execSync, spawn } from "node:child_process"; +import { execSync } from "node:child_process"; +import spawn from "cross-spawn"; import { existsSync, readFileSync, rmSync } from "node:fs"; import { join } from "node:path"; import { getPort, waitForServer, killChild } from "../../../../test/integration-helpers"; diff --git a/packages/void/test/integration/sveltekit.test.ts b/packages/void/test/integration/sveltekit.test.ts index 14276e368..f77840987 100644 --- a/packages/void/test/integration/sveltekit.test.ts +++ b/packages/void/test/integration/sveltekit.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { execSync, spawn } from "node:child_process"; +import { execSync } from "node:child_process"; +import spawn from "cross-spawn"; import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { getPort, waitForServer, killChild } from "../../../../test/integration-helpers"; diff --git a/packages/void/test/integration/tanstack-start.test.ts b/packages/void/test/integration/tanstack-start.test.ts index c3df6298e..db17784df 100644 --- a/packages/void/test/integration/tanstack-start.test.ts +++ b/packages/void/test/integration/tanstack-start.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { execSync, spawn } from "node:child_process"; +import { execSync } from "node:child_process"; +import spawn from "cross-spawn"; import { existsSync, readFileSync, rmSync } from "node:fs"; import { join } from "node:path"; import { getPort, waitForServer, killChild } from "../../../../test/integration-helpers"; diff --git a/packages/void/test/integration/vinext-app.test.ts b/packages/void/test/integration/vinext-app.test.ts index 57c6e317a..e59d3c2aa 100644 --- a/packages/void/test/integration/vinext-app.test.ts +++ b/packages/void/test/integration/vinext-app.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { execSync, spawn } from "node:child_process"; +import { execSync } from "node:child_process"; +import spawn from "cross-spawn"; import { existsSync, readFileSync, rmSync } from "node:fs"; import { join } from "node:path"; import { getPort, waitForServer, killChild } from "../../../../test/integration-helpers"; diff --git a/packages/void/test/integration/vinext-pages.test.ts b/packages/void/test/integration/vinext-pages.test.ts index a63fe7bc1..e05ca8f40 100644 --- a/packages/void/test/integration/vinext-pages.test.ts +++ b/packages/void/test/integration/vinext-pages.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { execSync, spawn } from "node:child_process"; +import { execSync } from "node:child_process"; +import spawn from "cross-spawn"; import { existsSync, readFileSync, rmSync } from "node:fs"; import { join } from "node:path"; import { getPort, waitForServer, killChild } from "../../../../test/integration-helpers"; diff --git a/packages/void/test/integration/websockets-playground.test.ts b/packages/void/test/integration/websockets-playground.test.ts index 0c0007864..d3bc5cc30 100644 --- a/packages/void/test/integration/websockets-playground.test.ts +++ b/packages/void/test/integration/websockets-playground.test.ts @@ -1,5 +1,5 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { spawn } from "node:child_process"; +import spawn from "cross-spawn"; import { join } from "node:path"; import { chromium, type Browser, type Page } from "playwright"; import { getPort, killChild, waitForServer } from "../../../../test/integration-helpers"; diff --git a/packages/vue/test/integration/vue-pages-client.test.ts b/packages/vue/test/integration/vue-pages-client.test.ts index d0c30a7e7..8d17a53b0 100644 --- a/packages/vue/test/integration/vue-pages-client.test.ts +++ b/packages/vue/test/integration/vue-pages-client.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, beforeAll, afterAll } from "vitest"; -import { execSync, spawn } from "node:child_process"; +import { execSync } from "node:child_process"; +import spawn from "cross-spawn"; import { join } from "node:path"; import { readFileSync, writeFileSync } from "node:fs"; import { chromium, type Browser, type Page } from "playwright"; @@ -764,6 +765,9 @@ describe("vue-pages client (dev)", () => { () => document.querySelector('[data-testid="count"]')?.textContent === "Count: 1", ); + // Wait a bit for reactive state to stabilize before HMR (Windows can be slower) + await new Promise((r) => setTimeout(r, 200)); + // Set a window marker to detect full page reload await page.evaluate(() => { (window as any).__hmr_marker = true; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b2617e90..9b83c10e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,9 +33,15 @@ importers: .: devDependencies: + '@types/cross-spawn': + specifier: ^6.0.6 + version: 6.0.6 '@typescript/native-preview': specifier: 'catalog:' version: 7.0.0-dev.20260325.1 + cross-spawn: + specifier: ^7.0.6 + version: 7.0.6 husky: specifier: ^9.1.7 version: 9.1.7 @@ -3673,6 +3679,9 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/cross-spawn@6.0.6': + resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} + '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} @@ -10689,6 +10698,10 @@ snapshots: '@types/cookie@0.6.0': {} + '@types/cross-spawn@6.0.6': + dependencies: + '@types/node': 25.5.0 + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 diff --git a/test/integration-helpers.ts b/test/integration-helpers.ts index 725f33ac9..916c39eb0 100644 --- a/test/integration-helpers.ts +++ b/test/integration-helpers.ts @@ -1,6 +1,6 @@ import { createServer, type Server } from "node:net"; import { once } from "node:events"; -import type { spawn } from "node:child_process"; +import { execSync, type spawn } from "node:child_process"; /** * Ports held by getPort() until waitForServer() releases them. @@ -63,7 +63,18 @@ export async function waitForServer( export async function killChild(child: ReturnType): Promise { if (child.exitCode === null && child.signalCode === null) { - child.kill("SIGTERM"); + if (process.platform === "win32" && child.pid) { + // On Windows, SIGTERM doesn't propagate to child processes. + // Use taskkill /T to kill the entire process tree (e.g. workerd.exe). + // See: https://github.com/voidzero-dev/void/actions/runs/23939152784/job/69826006981?pr=63 + try { + execSync(`taskkill /pid ${child.pid} /T /F`, { stdio: "ignore" }); + } catch { + // Process may have already exited + } + } else { + child.kill("SIGTERM"); + } await Promise.race([once(child, "exit"), new Promise((resolve) => setTimeout(resolve, 5_000))]); if (child.exitCode === null && child.signalCode === null) { child.kill("SIGKILL");