ci: run typecheck, build & tests on PRs and main (#6)

* ci: run typecheck, build & tests on PRs and main

Add a GitHub Actions workflow (.github/workflows/ci.yml) that installs on
Node 20 (the pinned runtime), builds shared/server/web, typechecks desktop,
and runs the full vitest suite across all packages on every pull request and
push to main. The repo already had 500+ tests but only ever ran them locally.

Also wires up the missing test/typecheck plumbing this exposed:
- web package had 55 test files but no test script — add test/test:watch/typecheck
- add root-level test and typecheck aggregate scripts
- fix a latent web type error the new typecheck surfaced: reference
  vite-plugin-pwa/react types so virtual:pwa-register/react is typed
  (useRegisterSW callback params were implicit any; the tsc half of the web
  build script has been failing, masked by vite build ignoring type errors)

* test: fix two latent failures surfaced by CI on Node 20

- server: exclude dist/** from vitest. Vitest 4's default exclude dropped
  dist/, so after `pnpm build` emits compiled .test.js files, vitest ran those
  stale copies alongside src/*.test.ts and they failed (compiled vi.mock paths
  resolve differently).
- web: polyfill Blob.prototype.stream in the jsdom test setup. jsdom's Blob has
  no .stream() on Node 20 (Node 25 provided one, masking this locally); undici's
  Response constructor calls blob.stream(), so new Response(blob) threw
  'object.stream is not a function'.
This commit is contained in:
TheZwiss
2026-07-10 00:15:41 +02:00
committed by GitHub
parent 0727d5a3b3
commit 628e4dec3e
6 changed files with 99 additions and 1 deletions
+7 -1
View File
@@ -1,7 +1,13 @@
import { defineConfig } from 'vitest/config';
import { defineConfig, configDefaults } from 'vitest/config';
export default defineConfig({
test: {
setupFiles: ['./test/setup-env.ts'],
// Vitest 4's default `exclude` is only node_modules/.git — it no longer
// ignores dist/. Once `pnpm build` (tsc) has emitted the compiled test files
// into dist/, vitest would otherwise run those stale .js copies alongside the
// real src/*.test.ts — and they fail, because compiled vi.mock() paths
// resolve differently than the source. Never run build output as tests.
exclude: [...configDefaults.exclude, 'dist/**'],
},
});
+3
View File
@@ -8,6 +8,9 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"preview": "vite preview",
"e2e:identity-deletion": "playwright test e2e/identity-deletion.spec.ts"
},
+19
View File
@@ -70,6 +70,25 @@ if (typeof ClipboardItem === 'undefined') {
};
}
// jsdom's Blob implementation has no `.stream()` method on Node 20 (Node 25+
// happens to provide one, which is why this only surfaces on the pinned target
// runtime). undici's `Response` constructor extracts a Blob body by calling
// `blob.stream()`, so `new Response(blob)` throws "object.stream is not a
// function" without this. Back it with the blob's own arrayBuffer().
if (typeof Blob !== 'undefined' && typeof Blob.prototype.stream !== 'function') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Blob.prototype as any).stream = function stream(this: Blob): ReadableStream<Uint8Array> {
const blob = this;
return new ReadableStream<Uint8Array>({
async start(controller) {
const buffer = await blob.arrayBuffer();
controller.enqueue(new Uint8Array(buffer));
controller.close();
},
});
};
}
// Patch globalThis.Response to preserve Blob content-type in jsdom.
// jsdom's fetch Response.blob() drops the Blob's MIME type; this shim
// restores it so tests that construct `new Response(blob)` behave correctly.
+1
View File
@@ -1,4 +1,5 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-pwa/react" />
interface ImportMetaEnv {
readonly VITE_FORCE_BOOT_STALL?: string;