From 13fab62914f96462024bef610027f9278fada701 Mon Sep 17 00:00:00 2001 From: Daniel Steigerwald Date: Tue, 3 Feb 2026 20:38:52 +0100 Subject: [PATCH 01/19] Clarify ESLint 'Unsafe...' troubleshooting --- .github/copilot-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8f95c9470..88c87ef62 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -491,7 +491,7 @@ pnpm test --filter @evolu/common -- -t "yields and returns ok" ## Monorepo TypeScript issues -**ESLint "Unsafe..." errors after changes** - In a monorepo, ESLint may show "Unsafe call", "Unsafe member access", or "Unsafe assignment" errors after modifying packages that other packages depend on. These errors should be ignored. Solution: use VS Code's "Developer: Reload Window" command (Cmd+Shift+P) +**ESLint "Unsafe..." errors after changes** - In a monorepo, ESLint may show "Unsafe call", "Unsafe member access", or "Unsafe assignment" errors after modifying packages that other packages depend on. These errors are caused by stale TypeScript type cache. Solution: run "ESLint: Restart ESLint Server" command (Cmd+Shift+P) ## Git commit messages From 046d374416134d8cbf5941dc676e8e462243fa50 Mon Sep 17 00:00:00 2001 From: Daniel Steigerwald Date: Tue, 3 Feb 2026 22:59:40 +0100 Subject: [PATCH 02/19] Migrate to monorepo Vitest config Introduce a root vitest.config.ts and split package test configs into unit and browser projects (packages/common/vitest.unit.config.ts, packages/common/vitest.browser.config.ts). Remove the old combined common vitest config and migrate per-package coverage/test settings into the central config. Update package.json scripts to run Vitest from the repo root, remove per-package Vitest deps where appropriate, and add browser/playwright-related devDeps at the root. Adjust tsconfigs (add root tsconfig.json and include vitest.*.config.ts in packages/common) and simplify turbo.json task entries. Also add a README link to the Vitest VS Code extension. --- README.md | 1 + bun.lock | 86 ++++++------------------ package.json | 9 ++- packages/common/package.json | 8 --- packages/common/tsconfig.json | 14 +++- packages/common/vitest.browser.config.ts | 52 ++++++++++++++ packages/common/vitest.config.ts | 79 ---------------------- packages/common/vitest.unit.config.ts | 16 +++++ packages/nodejs/package.json | 7 +- packages/nodejs/vitest.config.ts | 10 +-- packages/react-native/package.json | 7 +- packages/react-native/vitest.config.ts | 10 +-- packages/react-web/package.json | 3 +- packages/react/package.json | 3 +- packages/vue/package.json | 3 +- packages/web/package.json | 9 +-- packages/web/vitest.config.ts | 10 +-- tsconfig.json | 7 ++ turbo.json | 8 --- vitest.config.ts | 19 ++++++ 20 files changed, 144 insertions(+), 217 deletions(-) create mode 100644 packages/common/vitest.browser.config.ts delete mode 100644 packages/common/vitest.config.ts create mode 100644 packages/common/vitest.unit.config.ts create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/README.md b/README.md index 57bb03657..fbaf0ae2a 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ Linting Testing - `bun run test` - Run tests +- [Vitest VS Code extension](https://github.com/vitest-dev/vscode) Release diff --git a/bun.lock b/bun.lock index 9fe08654a..7f83613b4 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,9 @@ "@biomejs/biome": "2.3.13", "@changesets/cli": "^2.29.8", "@types/webpack": "^5.28.5", + "@vitest/browser": "^4.0.17", + "@vitest/browser-playwright": "^4.0.17", + "@vitest/coverage-v8": "^4.0.17", "rimraf": "^6.1.2", "turbo": "^2.8.2", "typedoc": "^0.28.16", @@ -93,8 +96,8 @@ "dependencies": { "@angular/core": "^21.1.0", "@angular/platform-browser": "^21.1.0", - "@evolu/common": "latest", - "@evolu/web": "latest", + "@evolu/common": "workspace:*", + "@evolu/web": "workspace:*", }, "devDependencies": { "@analogjs/vite-plugin-angular": "^2.2.2", @@ -112,9 +115,9 @@ "name": "@example/react-electron", "version": "0.0.0", "dependencies": { - "@evolu/common": "latest", - "@evolu/react": "latest", - "@evolu/react-web": "latest", + "@evolu/common": "workspace:*", + "@evolu/react": "workspace:*", + "@evolu/react-web": "workspace:*", "react": "19.2.4", "react-dom": "19.2.4", }, @@ -135,9 +138,9 @@ "version": "1.0.0", "dependencies": { "@blazejkustra/react-native-alert": "^1.0.0", - "@evolu/common": "latest", - "@evolu/react": "latest", - "@evolu/react-native": "latest", + "@evolu/common": "workspace:*", + "@evolu/react": "workspace:*", + "@evolu/react-native": "workspace:*", "@expo/metro-runtime": "^6.1.2", "@expo/vector-icons": "^15.0.3", "abort-signal-polyfill": "^1.0.0", @@ -176,9 +179,9 @@ "name": "@example/react-nextjs", "version": "0.1.0", "dependencies": { - "@evolu/common": "latest", - "@evolu/react": "latest", - "@evolu/react-web": "latest", + "@evolu/common": "workspace:*", + "@evolu/react": "workspace:*", + "@evolu/react-web": "workspace:*", "@tabler/icons-react": "^3.36.1", "clsx": "^2.1.1", "next": "^16.1.3", @@ -200,9 +203,9 @@ "name": "@example/react-vite-pwa", "version": "0.0.0", "dependencies": { - "@evolu/common": "latest", - "@evolu/react": "latest", - "@evolu/react-web": "latest", + "@evolu/common": "workspace:*", + "@evolu/react": "workspace:*", + "@evolu/react-web": "workspace:*", "@tabler/icons-react": "^3.36.1", "clsx": "^2.1.1", "react": "19.2.4", @@ -244,9 +247,9 @@ "name": "@example/vue-vite-pwa", "version": "0.0.0", "dependencies": { - "@evolu/common": "latest", - "@evolu/vue": "latest", - "@evolu/web": "latest", + "@evolu/common": "workspace:*", + "@evolu/vue": "workspace:*", + "@evolu/web": "workspace:*", "vue": "^3.5.26", "workbox-window": "^7.4.0", }, @@ -279,14 +282,10 @@ "@types/node": "^24.10.9", "@types/webpack": "^5.28.5", "@types/ws": "^8.18.1", - "@vitest/browser": "^4.0.17", - "@vitest/browser-playwright": "^4.0.17", - "@vitest/coverage-v8": "^4.0.18", "better-sqlite3": "^12.6.2", "fast-check": "^4.5.3", "playwright": "^1.58.1", "typescript": "^5.9.3", - "vitest": "^4.0.17", "webpack": "^5.104.1", "ws": "^8.19.0", }, @@ -304,9 +303,7 @@ "@types/better-sqlite3": "^7.6.13", "@types/node": "^24.10.9", "@types/ws": "^8.18.1", - "@vitest/coverage-v8": "^4.0.18", "typescript": "^5.9.3", - "vitest": "^4.0.17", }, "peerDependencies": { "@evolu/common": "^7.4.1", @@ -322,7 +319,6 @@ "@types/react-dom": "~19.2.3", "react": "19.2.4", "typescript": "^5.9.3", - "vitest": "^4.0.17", }, "peerDependencies": { "@evolu/common": "^7.4.1", @@ -338,7 +334,6 @@ "@evolu/tsconfig": "workspace:*", "@op-engineering/op-sqlite": "^15.2.2", "@types/react": "~19.2.10", - "@vitest/coverage-v8": "^4.0.18", "expo": "^54.0.31", "expo-secure-store": "~15.0.8", "expo-sqlite": "~16.0.10", @@ -348,7 +343,6 @@ "react-native-sensitive-info": "6.0.0-rc.11", "react-native-svg": "^15.15.1", "typescript": "^5.9.3", - "vitest": "^4.0.17", }, "peerDependencies": { "@evolu/common": "^7.4.1", @@ -386,7 +380,6 @@ "react-dom": "19.2.4", "typescript": "^5.9.3", "user-agent-data-types": "^0.4.2", - "vitest": "^4.0.17", }, "peerDependencies": { "@evolu/common": "^7.4.1", @@ -425,7 +418,6 @@ "@evolu/common": "workspace:*", "@evolu/tsconfig": "workspace:*", "typescript": "^5.9.3", - "vitest": "^4.0.17", }, "peerDependencies": { "@evolu/common": "^7.4.1", @@ -444,12 +436,8 @@ "@evolu/tsconfig": "workspace:*", "@types/sharedworker": "^0.0.211", "@types/web-locks-api": "^0.0.5", - "@vitest/browser": "^4.0.17", - "@vitest/browser-playwright": "^4.0.17", - "@vitest/coverage-v8": "^4.0.18", "typescript": "^5.9.3", "user-agent-data-types": "^0.4.2", - "vitest": "^4.0.17", }, "peerDependencies": { "@evolu/common": "^7.4.1", @@ -3870,40 +3858,6 @@ "@electron/windows-sign/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], - "@example/angular-vite-pwa/@evolu/common": ["@evolu/common@7.4.1", "", { "dependencies": { "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@scure/bip39": "^2.0.0", "kysely": "^0.28.4", "msgpackr": "^1.11.5", "random": "^5.4.1" } }, "sha512-aTSJN9GXvoN5FXo8DRiYdbMVFvBUG6N7Ygu7n5viyauPKBwylyAazZuDcBlQg5MxVv6YrF5c6w2LRG/OR2UIHA=="], - - "@example/angular-vite-pwa/@evolu/web": ["@evolu/web@2.4.0", "", { "dependencies": { "@evolu/sqlite-wasm": "2.2.4", "idb-keyval": "^6.2.2" }, "peerDependencies": { "@evolu/common": "^7.4.0" } }, "sha512-yba/zpcsf5qu4NEup/rx8j+M/7DmF53/EZXKe8NRJefPJxrCxjzteOgUqUMkHQwYDRdH3u81KMoPlhvdBgSFuQ=="], - - "@example/react-electron/@evolu/common": ["@evolu/common@7.4.1", "", { "dependencies": { "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@scure/bip39": "^2.0.0", "kysely": "^0.28.4", "msgpackr": "^1.11.5", "random": "^5.4.1" } }, "sha512-aTSJN9GXvoN5FXo8DRiYdbMVFvBUG6N7Ygu7n5viyauPKBwylyAazZuDcBlQg5MxVv6YrF5c6w2LRG/OR2UIHA=="], - - "@example/react-electron/@evolu/react": ["@evolu/react@10.4.0", "", { "peerDependencies": { "@evolu/common": "^7.4.0", "react": ">=19" } }, "sha512-BLXlLT+/dKSei/JrkLc9Z3fRb48vcXQmaExe0VMdxMzv2h8CyO5l4G635eQtH8F7I40VsnXLCURxNyzKF9DEQw=="], - - "@example/react-electron/@evolu/react-web": ["@evolu/react-web@2.4.0", "", { "peerDependencies": { "@evolu/common": "^7.4.0", "@evolu/web": "^2.4.0", "react": ">=19", "react-dom": ">=19" } }, "sha512-8zf/qQM+JZWykh0eGIAhE3CRFptGB0FwyQa4jp5Ce7yyOkekRMRs5WMZQA9QbRoY/OBRyOcwIP4aUk/tj49VwQ=="], - - "@example/react-expo/@evolu/common": ["@evolu/common@7.4.1", "", { "dependencies": { "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@scure/bip39": "^2.0.0", "kysely": "^0.28.4", "msgpackr": "^1.11.5", "random": "^5.4.1" } }, "sha512-aTSJN9GXvoN5FXo8DRiYdbMVFvBUG6N7Ygu7n5viyauPKBwylyAazZuDcBlQg5MxVv6YrF5c6w2LRG/OR2UIHA=="], - - "@example/react-expo/@evolu/react": ["@evolu/react@10.4.0", "", { "peerDependencies": { "@evolu/common": "^7.4.0", "react": ">=19" } }, "sha512-BLXlLT+/dKSei/JrkLc9Z3fRb48vcXQmaExe0VMdxMzv2h8CyO5l4G635eQtH8F7I40VsnXLCURxNyzKF9DEQw=="], - - "@example/react-expo/@evolu/react-native": ["@evolu/react-native@14.3.0", "", { "peerDependencies": { "@evolu/common": "^7.4.0", "@evolu/react": "^10.4.0", "@op-engineering/op-sqlite": ">=12", "expo": ">=54", "expo-secure-store": ">=15", "expo-sqlite": ">=16", "react-native": ">=0.81", "react-native-nitro-modules": ">=0.31", "react-native-sensitive-info": ">=6", "react-native-svg": ">=15" }, "optionalPeers": ["@op-engineering/op-sqlite", "expo", "expo-secure-store", "expo-sqlite", "react-native-nitro-modules", "react-native-sensitive-info", "react-native-svg"] }, "sha512-/HfypD6F1WrJw2DNcipbDjfFPmy5ebezNNI//GOvy6wCejk/QxPUCNCPksR9akA+UIdqvE1fE9Xct65bBK1e9w=="], - - "@example/react-nextjs/@evolu/common": ["@evolu/common@7.4.1", "", { "dependencies": { "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@scure/bip39": "^2.0.0", "kysely": "^0.28.4", "msgpackr": "^1.11.5", "random": "^5.4.1" } }, "sha512-aTSJN9GXvoN5FXo8DRiYdbMVFvBUG6N7Ygu7n5viyauPKBwylyAazZuDcBlQg5MxVv6YrF5c6w2LRG/OR2UIHA=="], - - "@example/react-nextjs/@evolu/react": ["@evolu/react@10.4.0", "", { "peerDependencies": { "@evolu/common": "^7.4.0", "react": ">=19" } }, "sha512-BLXlLT+/dKSei/JrkLc9Z3fRb48vcXQmaExe0VMdxMzv2h8CyO5l4G635eQtH8F7I40VsnXLCURxNyzKF9DEQw=="], - - "@example/react-nextjs/@evolu/react-web": ["@evolu/react-web@2.4.0", "", { "peerDependencies": { "@evolu/common": "^7.4.0", "@evolu/web": "^2.4.0", "react": ">=19", "react-dom": ">=19" } }, "sha512-8zf/qQM+JZWykh0eGIAhE3CRFptGB0FwyQa4jp5Ce7yyOkekRMRs5WMZQA9QbRoY/OBRyOcwIP4aUk/tj49VwQ=="], - - "@example/react-vite-pwa/@evolu/common": ["@evolu/common@7.4.1", "", { "dependencies": { "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@scure/bip39": "^2.0.0", "kysely": "^0.28.4", "msgpackr": "^1.11.5", "random": "^5.4.1" } }, "sha512-aTSJN9GXvoN5FXo8DRiYdbMVFvBUG6N7Ygu7n5viyauPKBwylyAazZuDcBlQg5MxVv6YrF5c6w2LRG/OR2UIHA=="], - - "@example/react-vite-pwa/@evolu/react": ["@evolu/react@10.4.0", "", { "peerDependencies": { "@evolu/common": "^7.4.0", "react": ">=19" } }, "sha512-BLXlLT+/dKSei/JrkLc9Z3fRb48vcXQmaExe0VMdxMzv2h8CyO5l4G635eQtH8F7I40VsnXLCURxNyzKF9DEQw=="], - - "@example/react-vite-pwa/@evolu/react-web": ["@evolu/react-web@2.4.0", "", { "peerDependencies": { "@evolu/common": "^7.4.0", "@evolu/web": "^2.4.0", "react": ">=19", "react-dom": ">=19" } }, "sha512-8zf/qQM+JZWykh0eGIAhE3CRFptGB0FwyQa4jp5Ce7yyOkekRMRs5WMZQA9QbRoY/OBRyOcwIP4aUk/tj49VwQ=="], - - "@example/vue-vite-pwa/@evolu/common": ["@evolu/common@7.4.1", "", { "dependencies": { "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@scure/bip39": "^2.0.0", "kysely": "^0.28.4", "msgpackr": "^1.11.5", "random": "^5.4.1" } }, "sha512-aTSJN9GXvoN5FXo8DRiYdbMVFvBUG6N7Ygu7n5viyauPKBwylyAazZuDcBlQg5MxVv6YrF5c6w2LRG/OR2UIHA=="], - - "@example/vue-vite-pwa/@evolu/vue": ["@evolu/vue@1.4.0", "", { "peerDependencies": { "@evolu/common": "^7.4.0", "vue": ">=3.5.24" } }, "sha512-ydT8VxDOIXqfC6n6NQxXPsDX+Jvqg/CP+2Tj8hzZgqFO6PZpIBqOX8pS6ceqVCv+9FI4pC+LGyz4X9idrdX87g=="], - - "@example/vue-vite-pwa/@evolu/web": ["@evolu/web@2.4.0", "", { "dependencies": { "@evolu/sqlite-wasm": "2.2.4", "idb-keyval": "^6.2.2" }, "peerDependencies": { "@evolu/common": "^7.4.0" } }, "sha512-yba/zpcsf5qu4NEup/rx8j+M/7DmF53/EZXKe8NRJefPJxrCxjzteOgUqUMkHQwYDRdH3u81KMoPlhvdBgSFuQ=="], - "@expo/cli/picomatch": ["picomatch@3.0.1", "", {}, "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag=="], "@expo/cli/pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], diff --git a/package.json b/package.json index fbfb73316..22101cab9 100755 --- a/package.json +++ b/package.json @@ -15,9 +15,9 @@ "build": "turbo --filter @evolu/* build", "build:web": "bun run build:docs && turbo --filter web build", "build:docs": "typedoc && bun --filter=web run fix:docs", - "test": "turbo run test", - "test:coverage": "turbo run test:coverage", - "test:watch": "turbo run test:watch", + "test": "vitest run", + "test:coverage": "vitest run --coverage", + "test:watch": "vitest", "start": "turbo start", "lint": "biome check", "lint-monorepo": "bunx sherif@latest", @@ -40,6 +40,9 @@ "@biomejs/biome": "2.3.13", "@changesets/cli": "^2.29.8", "@types/webpack": "^5.28.5", + "@vitest/browser": "^4.0.17", + "@vitest/browser-playwright": "^4.0.17", + "@vitest/coverage-v8": "^4.0.17", "rimraf": "^6.1.2", "turbo": "^2.8.2", "typedoc": "^0.28.16", diff --git a/packages/common/package.json b/packages/common/package.json index f42b3cc9d..8e336f9ea 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -45,11 +45,7 @@ "scripts": { "dev": "tsc", "build": "rimraf dist && tsc", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage", "clean": "rimraf .turbo node_modules dist coverage test/tmp test/__screenshots__", - "bench": "vitest bench" }, "dependencies": { @@ -68,14 +64,10 @@ "@types/node": "^24.10.9", "@types/webpack": "^5.28.5", "@types/ws": "^8.18.1", - "@vitest/browser": "^4.0.17", - "@vitest/browser-playwright": "^4.0.17", - "@vitest/coverage-v8": "^4.0.18", "better-sqlite3": "^12.6.2", "fast-check": "^4.5.3", "playwright": "^1.58.1", "typescript": "^5.9.3", - "vitest": "^4.0.17", "webpack": "^5.104.1", "ws": "^8.19.0" }, diff --git a/packages/common/tsconfig.json b/packages/common/tsconfig.json index 1ae925a2a..e9ddf6479 100644 --- a/packages/common/tsconfig.json +++ b/packages/common/tsconfig.json @@ -6,6 +6,14 @@ "allowJs": true, "resolveJsonModule": true }, - "include": ["src", "test", "vitest.config.ts"], - "exclude": ["dist", "node_modules", "test/tmp"] -} + "include": [ + "src", + "test", + "vitest.*.config.ts" + ], + "exclude": [ + "dist", + "node_modules", + "test/tmp" + ] +} \ No newline at end of file diff --git a/packages/common/vitest.browser.config.ts b/packages/common/vitest.browser.config.ts new file mode 100644 index 000000000..eab8a080e --- /dev/null +++ b/packages/common/vitest.browser.config.ts @@ -0,0 +1,52 @@ +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { playwright } from "@vitest/browser-playwright"; +import { defineProject } from "vitest/config"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// Coverage with v8 only works with a single browser instance +const isCoverage = process.argv.includes("--coverage"); + +export default defineProject({ + // Transpile `using`/`await using` for WebKit which doesn't support it yet + esbuild: { supported: { using: false } }, + test: { + snapshotSerializers: [ + resolve(__dirname, "./test/local-first/_uint8ArraySerializer.ts"), + ], + include: ["test/*.test.ts"], + exclude: [ + "test/Sqlite.test.ts", // needs SQLite + "test/TreeShaking.test.ts", // needs esbuild + "test/Identicon.test.ts", // needs canvas + "test/Redacted.test.ts", // uses node:util + ], + name: "browser", + setupFiles: ["./test/_browserSetup.ts"], + browser: { + enabled: true, + provider: playwright(), + headless: true, + // Sequential execution is faster than parallel for some reason. + fileParallelism: false, + commands: { + startWsServer: async () => { + const { createServer } = await import("./test/_globalSetup.js"); + return createServer(); + }, + stopWsServer: async (_, port: number) => { + const { closeServer } = await import("./test/_globalSetup.js"); + await closeServer(port); + }, + }, + instances: isCoverage + ? [{ browser: "chromium" }] + : [ + { browser: "chromium" }, + { browser: "firefox" }, + { browser: "webkit" }, + ], + }, + }, +}); diff --git a/packages/common/vitest.config.ts b/packages/common/vitest.config.ts deleted file mode 100644 index 2b5ed271a..000000000 --- a/packages/common/vitest.config.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { playwright } from "@vitest/browser-playwright"; -import { defineConfig } from "vitest/config"; - -// Coverage with v8 only works with a single browser instance -const isCoverage = process.argv.includes("--coverage"); - -export default defineConfig({ - test: { - exclude: [ - "**/node_modules/**", - "**/dist/**", - "**/coverage/**", - "**/__snapshots__/**", - ], - coverage: { - provider: "v8", - include: ["src/**/*.ts"], - exclude: ["src/**/index.ts"], - reporter: ["text", "html"], - thresholds: { - statements: 70, - branches: 65, - functions: 70, - lines: 70, - }, - }, - projects: [ - { - test: { - snapshotSerializers: ["./test/local-first/_uint8ArraySerializer.ts"], - include: ["test/**/*.test.ts"], - name: "unit", - environment: "node", - setupFiles: ["./test/_nodeSetup.ts"], - }, - }, - { - // Transpile `using`/`await using` for WebKit which doesn't support it yet - esbuild: { supported: { using: false } }, - test: { - snapshotSerializers: ["./test/local-first/_uint8ArraySerializer.ts"], - include: ["test/*.test.ts"], - exclude: [ - "test/Sqlite.test.ts", // needs SQLite - "test/TreeShaking.test.ts", // needs esbuild - "test/Identicon.test.ts", // needs canvas - "test/Redacted.test.ts", // uses node:util - ], - name: "browser", - setupFiles: ["./test/_browserSetup.ts"], - browser: { - enabled: true, - provider: playwright(), - headless: true, - // Sequential execution is faster than parallel for some reason. - fileParallelism: false, - commands: { - startWsServer: async () => { - const { createServer } = await import("./test/_globalSetup.js"); - return createServer(); - }, - stopWsServer: async (_, port: number) => { - const { closeServer } = await import("./test/_globalSetup.js"); - await closeServer(port); - }, - }, - instances: isCoverage - ? [{ browser: "chromium" }] - : [ - { browser: "chromium" }, - { browser: "firefox" }, - { browser: "webkit" }, - ], - }, - }, - }, - ], - }, -}); diff --git a/packages/common/vitest.unit.config.ts b/packages/common/vitest.unit.config.ts new file mode 100644 index 000000000..73d9fdb8f --- /dev/null +++ b/packages/common/vitest.unit.config.ts @@ -0,0 +1,16 @@ +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { defineProject } from "vitest/config"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default defineProject({ + test: { + snapshotSerializers: [ + resolve(__dirname, "./test/local-first/_uint8ArraySerializer.ts"), + ], + include: ["test/**/*.test.ts"], + name: "unit", + environment: "node", + }, +}); diff --git a/packages/nodejs/package.json b/packages/nodejs/package.json index bcbdd3964..c2e0a513c 100644 --- a/packages/nodejs/package.json +++ b/packages/nodejs/package.json @@ -22,9 +22,6 @@ "scripts": { "dev": "tsc", "build": "rimraf dist && tsc", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage", "clean": "rimraf .turbo node_modules dist coverage" }, "dependencies": { @@ -37,9 +34,7 @@ "@types/better-sqlite3": "^7.6.13", "@types/node": "^24.10.9", "@types/ws": "^8.18.1", - "@vitest/coverage-v8": "^4.0.18", - "typescript": "^5.9.3", - "vitest": "^4.0.17" + "typescript": "^5.9.3" }, "peerDependencies": { "@evolu/common": "^7.4.1" diff --git a/packages/nodejs/vitest.config.ts b/packages/nodejs/vitest.config.ts index 8c5405af1..4edb62f48 100644 --- a/packages/nodejs/vitest.config.ts +++ b/packages/nodejs/vitest.config.ts @@ -1,15 +1,9 @@ -import { defineConfig } from "vitest/config"; +import { defineProject } from "vitest/config"; -export default defineConfig({ +export default defineProject({ test: { setupFiles: ["./test/setup.ts"], exclude: ["**/node_modules/**", "**/dist/**"], include: ["test/**/*.test.ts"], - coverage: { - provider: "v8", - include: ["src/**/*.ts"], - exclude: ["src/**/index.ts"], - reporter: ["text", "html"], - }, }, }); diff --git a/packages/react-native/package.json b/packages/react-native/package.json index ecc3fe996..1296b5c76 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -67,9 +67,6 @@ "scripts": { "dev": "tsc", "build": "rimraf dist && tsc", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage", "clean": "rimraf .turbo node_modules dist coverage" }, "devDependencies": { @@ -78,7 +75,6 @@ "@evolu/tsconfig": "workspace:*", "@op-engineering/op-sqlite": "^15.2.2", "@types/react": "~19.2.10", - "@vitest/coverage-v8": "^4.0.18", "expo": "^54.0.31", "expo-secure-store": "~15.0.8", "expo-sqlite": "~16.0.10", @@ -87,8 +83,7 @@ "react-native-nitro-modules": "^0.31.10", "react-native-sensitive-info": "6.0.0-rc.11", "react-native-svg": "^15.15.1", - "typescript": "^5.9.3", - "vitest": "^4.0.17" + "typescript": "^5.9.3" }, "peerDependencies": { "@evolu/common": "^7.4.1", diff --git a/packages/react-native/vitest.config.ts b/packages/react-native/vitest.config.ts index 4705738a3..324ed134b 100644 --- a/packages/react-native/vitest.config.ts +++ b/packages/react-native/vitest.config.ts @@ -1,14 +1,8 @@ -import { defineConfig } from "vitest/config"; +import { defineProject } from "vitest/config"; -export default defineConfig({ +export default defineProject({ test: { exclude: ["**/node_modules/**", "**/dist/**"], include: ["test/**/*.test.ts"], - coverage: { - provider: "v8", - include: ["src/**/*.ts"], - exclude: ["src/**/index.ts"], - reporter: ["text", "html"], - }, }, }); diff --git a/packages/react-web/package.json b/packages/react-web/package.json index 96c38393c..1f50e366b 100644 --- a/packages/react-web/package.json +++ b/packages/react-web/package.json @@ -43,8 +43,7 @@ "react": "19.2.4", "react-dom": "19.2.4", "typescript": "^5.9.3", - "user-agent-data-types": "^0.4.2", - "vitest": "^4.0.17" + "user-agent-data-types": "^0.4.2" }, "peerDependencies": { "@evolu/common": "^7.4.1", diff --git a/packages/react/package.json b/packages/react/package.json index 6a3e5c429..a6b7ca696 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -40,8 +40,7 @@ "@types/react": "~19.2.10", "@types/react-dom": "~19.2.3", "react": "19.2.4", - "typescript": "^5.9.3", - "vitest": "^4.0.17" + "typescript": "^5.9.3" }, "peerDependencies": { "@evolu/common": "^7.4.1", diff --git a/packages/vue/package.json b/packages/vue/package.json index 8cfb8d682..4a0084fc9 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -37,8 +37,7 @@ "devDependencies": { "@evolu/common": "workspace:*", "@evolu/tsconfig": "workspace:*", - "typescript": "^5.9.3", - "vitest": "^4.0.17" + "typescript": "^5.9.3" }, "peerDependencies": { "@evolu/common": "^7.4.1", diff --git a/packages/web/package.json b/packages/web/package.json index c545841ea..ae320baa6 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -30,9 +30,6 @@ "scripts": { "dev": "tsc", "build": "rimraf dist && tsc", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage", "clean": "rimraf .turbo node_modules dist coverage" }, "dependencies": { @@ -44,12 +41,8 @@ "@evolu/tsconfig": "workspace:*", "@types/sharedworker": "^0.0.211", "@types/web-locks-api": "^0.0.5", - "@vitest/browser": "^4.0.17", - "@vitest/browser-playwright": "^4.0.17", - "@vitest/coverage-v8": "^4.0.18", "typescript": "^5.9.3", - "user-agent-data-types": "^0.4.2", - "vitest": "^4.0.17" + "user-agent-data-types": "^0.4.2" }, "peerDependencies": { "@evolu/common": "^7.4.1" diff --git a/packages/web/vitest.config.ts b/packages/web/vitest.config.ts index cad7e8c03..2b5bba32f 100644 --- a/packages/web/vitest.config.ts +++ b/packages/web/vitest.config.ts @@ -1,21 +1,15 @@ import { playwright } from "@vitest/browser-playwright"; -import { defineConfig } from "vitest/config"; +import { defineProject } from "vitest/config"; // Coverage with v8 only works with a single browser instance const isCoverage = process.argv.includes("--coverage"); -export default defineConfig({ +export default defineProject({ // Transpile `using`/`await using` for WebKit which doesn't support it yet esbuild: { supported: { using: false } }, test: { exclude: ["**/node_modules/**", "**/dist/**"], include: ["test/**/*.test.ts"], - coverage: { - provider: "v8", - include: ["src/**/*.ts"], - exclude: ["src/**/index.ts"], - reporter: ["text", "html"], - }, browser: { enabled: true, provider: playwright(), diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..888b5a48d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "./packages/tsconfig/universal-esm.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["vitest.config.ts"] +} diff --git a/turbo.json b/turbo.json index a135d611a..6bf2d0cc0 100644 --- a/turbo.json +++ b/turbo.json @@ -26,14 +26,6 @@ "cache": false, "persistent": true }, - "test": {}, - "test:coverage": { - "outputs": ["coverage/**"] - }, - "test:watch": { - "cache": false, - "persistent": true - }, "clean": { "cache": false }, diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 000000000..701f90146 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + projects: [ + "packages/common/vitest.unit.config.ts", + "packages/common/vitest.browser.config.ts", + "packages/web", + "packages/nodejs", + "packages/react-native", + ], + coverage: { + provider: "v8", + include: ["packages/*/src/**/*.ts"], + exclude: ["packages/*/src/**/index.ts"], + reporter: ["text", "html"], + }, + }, +}); From 9e442be47fb647815cd3f7365112973bcd6baeea Mon Sep 17 00:00:00 2001 From: Daniel Steigerwald Date: Tue, 3 Feb 2026 23:15:26 +0100 Subject: [PATCH 03/19] Expand TDD guidance and add Vitest examples Replace the brief Test-driven development note with an expanded TDD section that explains how to run tests, test file locations, and filtering by test name. Add subsections on test structure, type testing (expectTypeOf), and inline snapshot usage with Vitest code examples. Remove the previous Vitest filtering CLI snippets and some commit-message examples to streamline the guidance and surface more actionable testing patterns for contributors. --- .github/copilot-instructions.md | 92 ++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 88c87ef62..d3590838e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -6,11 +6,6 @@ applyTo: "**/*.{ts,tsx}" Follow these specific conventions and patterns: -## Test-driven development - -- Write a failing test before implementing a new feature or fixing a bug -- Keep test code cleaner than production code — good tests let you refactor production code; nothing protects messy tests - ## Code organization & imports - **Use named imports only** - avoid default exports and namespace imports @@ -433,6 +428,68 @@ const processTask: Task = async (run) => { const result = await sleep("1s")(run); ``` +## Test-driven development + +- Write a failing test before implementing a new feature or fixing a bug +- Run tests using the `runTests` tool with the test file path +- Test files are in `packages/*/test/*.test.ts` +- Use `testNames` parameter to run specific tests by name +- Run related tests after making code changes to verify correctness + +### Test structure + +- Use `describe` blocks to group related tests by feature or function +- Use `test` or `it` for individual test cases (both are equivalent) +- Test names should be descriptive phrases: `"returns true for non-empty array"` +- Use nested `describe` for sub-categories + +```ts +import { describe, expect, expectTypeOf, test } from "vitest"; + +describe("arrayFrom", () => { + test("creates array from iterable", () => { + const result = arrayFrom(new Set([1, 2, 3])); + expect(result).toEqual([1, 2, 3]); + }); + + test("returns input unchanged if already an array", () => { + const input = [1, 2, 3]; + const result = arrayFrom(input); + expect(result).toBe(input); + }); +}); +``` + +### Type testing + +Use `expectTypeOf` from Vitest for compile-time type assertions: + +```ts +import { expectTypeOf } from "vitest"; + +test("returns readonly array", () => { + const result = arrayFrom(2, () => "x"); + expectTypeOf(result).toEqualTypeOf>(); +}); + +test("NonEmptyArray requires at least one element", () => { + const _valid: NonEmptyArray = [1, 2, 3]; + // @ts-expect-error - empty array is not a valid NonEmptyArray + const _invalid: NonEmptyArray = []; +}); +``` + +### Inline snapshots + +Use `toMatchInlineSnapshot` for readable test output directly in the test file: + +```ts +test("Buffer", () => { + const buffer = createBuffer([1, 2, 3]); + expect(buffer.unwrap()).toMatchInlineSnapshot(`uint8:[1,2,3]`); +}); +``` + ## Testing - **Create deps per test** - use `testCreateDeps()` from `@evolu/common` for test isolation @@ -476,19 +533,6 @@ export const testCreateTime = (options?: { }): TestTime => { ... }; ``` -### Vitest filtering (https://vitest.dev/guide/filtering) - -```bash -# Run all tests in a package -pnpm test --filter @evolu/common - -# Run a single file -pnpm test --filter @evolu/common -- Task - -# Run a single test by name (-t flag) -pnpm test --filter @evolu/common -- -t "yields and returns ok" -``` - ## Monorepo TypeScript issues **ESLint "Unsafe..." errors after changes** - In a monorepo, ESLint may show "Unsafe call", "Unsafe member access", or "Unsafe assignment" errors after modifying packages that other packages depend on. These errors are caused by stale TypeScript type cache. Solution: run "ESLint: Restart ESLint Server" command (Cmd+Shift+P) @@ -499,18 +543,6 @@ pnpm test --filter @evolu/common -- -t "yields and returns ok" - **No prefixes** - avoid `feat:`, `fix:`, `feature:` etc. - **Be descriptive** - explain what the change does -```bash -# Good -Add support for custom error formatters -Fix memory leak in WebSocket reconnection -Update schema validation to handle edge cases - -# Avoid -feat: add support for custom error formatters -fix: memory leak in websocket reconnection -Update schema validation to handle edge cases. -``` - ## Changesets - **Write in past tense** - describe what was done, not what will be done From 60ac61b4fb2369cda2f1a4d47cd17cefd31620ac Mon Sep 17 00:00:00 2001 From: Daniel Steigerwald Date: Tue, 3 Feb 2026 23:20:43 +0100 Subject: [PATCH 04/19] Update TreeShaking.test.ts --- packages/common/test/TreeShaking.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/common/test/TreeShaking.test.ts b/packages/common/test/TreeShaking.test.ts index 6fb7b56cd..f8e31c6d8 100644 --- a/packages/common/test/TreeShaking.test.ts +++ b/packages/common/test/TreeShaking.test.ts @@ -1,10 +1,10 @@ import { spawnSync } from "node:child_process"; import { - existsSync, - mkdirSync, - readdirSync, - readFileSync, - rmSync, + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, } from "node:fs"; import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; From dd0b8a680f1fca5703aa7e0e928f53ff0af7d967 Mon Sep 17 00:00:00 2001 From: Daniel Steigerwald Date: Tue, 3 Feb 2026 23:21:06 +0100 Subject: [PATCH 05/19] Use 'observability' term in error comment --- packages/common/src/Error.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/src/Error.ts b/packages/common/src/Error.ts index 1c96a59ab..b4399b58a 100644 --- a/packages/common/src/Error.ts +++ b/packages/common/src/Error.ts @@ -94,7 +94,7 @@ export const createUnknownError = (error: unknown): UnknownError => { * `onunhandledrejection`, `uncaughtException`, etc.). Any error reaching these * handlers is a programming error — all unsafe code should be wrapped with * {@link trySync} or {@link tryAsync}. The `onError` callback exists for - * telemetry and debugging, not error recovery. + * observability, not error recovery. * * Implementations use {@link handleGlobalError} to forward errors. */ From 205bc8c65d88674ef9ab1f39ab11b4215aab5810 Mon Sep 17 00:00:00 2001 From: Daniel Steigerwald Date: Tue, 3 Feb 2026 23:35:19 +0100 Subject: [PATCH 06/19] Rename vitest config to .mts and add scripts tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename vitest.config.ts → vitest.config.mts (switch to .mts/ESM), update tsconfig include to reference vitest.config.mts, and add a new "scripts" test project in the Vitest config to include tests matching scripts/**/*.test.mts. Also simplify package.json "verify" script by removing the generic "pnpm test" step. --- package.json | 2 +- tsconfig.json | 2 +- vitest.config.ts => vitest.config.mts | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) rename vitest.config.ts => vitest.config.mts (79%) diff --git a/package.json b/package.json index 22101cab9..66c9176ac 100755 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "lint": "biome check", "lint-monorepo": "bunx sherif@latest", "format": "biome format --write .", - "verify": "bun run format && bun run build && bun run test && bun run test:coverage && bun run lint && bun run lint-monorepo", + "verify": "bun run format && bun run build && bun run test && bun run test:coverage && bun run lint && bun run lint-monorepo && bunx vitest run scripts/typedoc-plugin-evolu.test.mts", "clean": "turbo clean && rimraf node_modules bun.lock .turbo out", "version": "changeset version", "release": "bun run build && changeset publish", diff --git a/tsconfig.json b/tsconfig.json index 888b5a48d..2b02f3ca7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "noEmit": true }, - "include": ["vitest.config.ts"] + "include": ["vitest.config.mts"] } diff --git a/vitest.config.ts b/vitest.config.mts similarity index 79% rename from vitest.config.ts rename to vitest.config.mts index 701f90146..60c2fe827 100644 --- a/vitest.config.ts +++ b/vitest.config.mts @@ -8,6 +8,12 @@ export default defineConfig({ "packages/web", "packages/nodejs", "packages/react-native", + { + test: { + name: "scripts", + include: ["scripts/**/*.test.mts"], + }, + }, ], coverage: { provider: "v8", From c02e13d95084a448eeedd7f523ed72a3a7b0da4c Mon Sep 17 00:00:00 2001 From: Daniel Steigerwald Date: Tue, 3 Feb 2026 23:36:38 +0100 Subject: [PATCH 07/19] Add createQueryBuilder, replace createQuery usage Introduce a standalone createQueryBuilder(Schema) to build typed Kysely queries without an Evolu instance. The createQuery implementation was moved into packages/common/src/local-first/Schema.ts and exported from index.ts; Evolu.createQuery was removed from the Evolu interface/instance. Updated usages across playgrounds to call createQuery(...) (created via createQueryBuilder) and adjusted types/docs in Query.ts. Added a changeset noting the API migration and guidance to replace evolu.createQuery with createQueryBuilder(Schema). --- .changeset/create-query-builder.md | 16 + .../playgrounds/full/EvoluFullExample.tsx | 13 +- .../minimal/EvoluMinimalExample.tsx | 658 +++++++++--------- .../multitenant/EvoluMultitenantExample.tsx | 5 +- packages/common/src/index.ts | 8 +- packages/common/src/local-first/Evolu.ts | 82 +-- packages/common/src/local-first/Query.ts | 8 +- packages/common/src/local-first/Schema.ts | 44 ++ 8 files changed, 437 insertions(+), 397 deletions(-) create mode 100644 .changeset/create-query-builder.md diff --git a/.changeset/create-query-builder.md b/.changeset/create-query-builder.md new file mode 100644 index 000000000..86868d34a --- /dev/null +++ b/.changeset/create-query-builder.md @@ -0,0 +1,16 @@ +--- +"@evolu/common": major +--- + +Replaced `evolu.createQuery` with standalone `createQueryBuilder` function + +Queries are now created using a standalone `createQueryBuilder` function instead of `evolu.createQuery` method. This enables query creation without an Evolu instance, improving code organization and enabling schema-first development. + +```ts +// Before +const todosQuery = evolu.createQuery((db) => db.selectFrom("todo").selectAll()); + +// After +const createQuery = createQueryBuilder(Schema); +const todosQuery = createQuery((db) => db.selectFrom("todo").selectAll()); +``` diff --git a/apps/web/src/app/(playgrounds)/playgrounds/full/EvoluFullExample.tsx b/apps/web/src/app/(playgrounds)/playgrounds/full/EvoluFullExample.tsx index a89e6ec46..edd8ec328 100644 --- a/apps/web/src/app/(playgrounds)/playgrounds/full/EvoluFullExample.tsx +++ b/apps/web/src/app/(playgrounds)/playgrounds/full/EvoluFullExample.tsx @@ -5,6 +5,7 @@ import { createEvolu, createFormatTypeError, createObjectURL, + createQueryBuilder, FiniteNumber, id, idToIdBytes, @@ -99,6 +100,8 @@ const Schema = { }, }; +const createQuery = createQueryBuilder(Schema); + const deps = createEvoluDeps(); const evolu = createEvolu(deps)(Schema, { @@ -212,7 +215,7 @@ const App: FC = () => { ); }; -const projectsWithTodosQuery = evolu.createQuery( +const projectsWithTodosQuery = createQuery( (db) => db .selectFrom("project") @@ -409,7 +412,7 @@ const HomeTabProjectSectionTodoItem: FC<{ // Demonstrate history tracking. Evolu automatically tracks all changes // in the evolu_history table, making it easy to build audit logs or undo features. - const titleHistoryQuery = evolu.createQuery((db) => + const titleHistoryQuery = createQuery((db) => db .selectFrom("evolu_history") .select(["value", "timestamp"]) @@ -503,7 +506,7 @@ const HomeTabProjectSectionTodoItem: FC<{ ); }; -const projectsQuery = evolu.createQuery((db) => +const projectsQuery = createQuery((db) => db .selectFrom("project") .select(["id", "name", "fooJson"]) @@ -716,7 +719,7 @@ const AccountTab: FC = () => { ); }; -const deletedProjectsQuery = evolu.createQuery((db) => +const deletedProjectsQuery = createQuery((db) => db .selectFrom("project") .select(["id", "name", "updatedAt"]) @@ -728,7 +731,7 @@ const deletedProjectsQuery = evolu.createQuery((db) => type DeletedProjectsRow = typeof deletedProjectsQuery.Row; -const deletedTodosQuery = evolu.createQuery((db) => +const deletedTodosQuery = createQuery((db) => db .selectFrom("todo") .select(["id", "title", "isCompleted", "projectId", "updatedAt"]) diff --git a/apps/web/src/app/(playgrounds)/playgrounds/minimal/EvoluMinimalExample.tsx b/apps/web/src/app/(playgrounds)/playgrounds/minimal/EvoluMinimalExample.tsx index 012f673bb..965e0a77e 100644 --- a/apps/web/src/app/(playgrounds)/playgrounds/minimal/EvoluMinimalExample.tsx +++ b/apps/web/src/app/(playgrounds)/playgrounds/minimal/EvoluMinimalExample.tsx @@ -1,11 +1,9 @@ "use client"; import * as Evolu from "@evolu/common"; -import { createUseEvolu, EvoluProvider, useQuery } from "@evolu/react"; -import { createEvoluDeps } from "@evolu/react-web"; -import { IconEdit, IconTrash } from "@tabler/icons-react"; -import clsx from "clsx"; -import { type FC, Suspense, use, useState } from "react"; +import { createRunner } from "@evolu/web"; + +import type { FC } from "react"; // Primary keys are branded types, preventing accidental use of IDs across // different tables (e.g., a TodoId can't be used where a UserId is expected). @@ -15,7 +13,7 @@ type TodoId = typeof TodoId.Type; // Schema defines database structure with runtime validation. // Column types validate data on insert/update/upsert. -const Schema = { +const _Schema = { todo: { id: TodoId, // Branded type ensuring titles are non-empty and ≤100 chars. @@ -25,66 +23,10 @@ const Schema = { }, }; -const deps = createEvoluDeps(); - -// Create Evolu instance for the React web platform. -const evolu = Evolu.createEvolu(deps)(Schema, { - name: Evolu.SimpleName.orThrow("minimal-example"), - - // TODO: Patri do web deps only? hmm, deps jsou sdilene - // tohle musim pak domyslet, callback? webReloadUrl? uvidime - // tohle rozhodne patri se - // reloadUrl: "/playgrounds/minimal", - - ...(process.env.NODE_ENV === "development" && { - transports: [{ type: "WebSocket", url: "ws://localhost:4000" }], - }), -}); - -// Creates a typed React Hook for accessing Evolu from EvoluProvider context. -// You can also use `evolu` directly, but the hook enables replacing Evolu -// in tests via the EvoluProvider. -const useEvolu = createUseEvolu(evolu); - -/** - * Subscribe to Evolu errors (database, network, sync issues). These should not - * happen in normal operation, so always log them for debugging. Show users a - * friendly error message instead of technical details. - */ -evolu.subscribeError(() => { - const error = evolu.getError(); - if (!error) return; - - alert("🚨 Evolu error occurred! Check the console."); - // eslint-disable-next-line no-console - console.error(error); -}); +// Create a query builder (once per schema). +const createQuery = Evolu.createQueryBuilder(_Schema); -export const EvoluMinimalExample: FC = () => ( -
-
-
-

- Minimal Todo App -

-
- - - {/* - Suspense delivers great UX (no loading flickers) and DX (no loading - states to manage). Highly recommended with Evolu. - */} - - - - - -
-
-); - -// Evolu uses Kysely for type-safe SQL (https://kysely.dev/). -const todosQuery = evolu.createQuery((db) => +const _todosQuery = createQuery((db) => db // Type-safe SQL: try autocomplete for table and column names. .selectFrom("todo") @@ -100,257 +42,343 @@ const todosQuery = evolu.createQuery((db) => .orderBy("createdAt"), ); -// Extract the row type from the query for type-safe component props. -type TodosRow = typeof todosQuery.Row; - -const Todos: FC = () => { - // useQuery returns live data - component re-renders when data changes. - const todos = useQuery(todosQuery); - const { insert } = useEvolu(); - const [newTodoTitle, setNewTodoTitle] = useState(""); - - const addTodo = () => { - const result = insert( - "todo", - { - title: newTodoTitle.trim(), - }, - { - onComplete: () => { - setNewTodoTitle(""); - }, - }, - ); - - if (!result.ok) { - alert(formatTypeError(result.error)); - } - }; - - return ( -
-
    - {todos.map((todo) => ( - - ))} -
- -
- { - setNewTodoTitle(e.target.value); - }} - onKeyDown={(e) => { - if (e.key === "Enter") addTodo(); - }} - placeholder="Add a new todo..." - className="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6" - /> -
-
- ); -}; +// vytvorit deps +// const deps = createEvoluDeps() +// const run = createRunner(deps) // muze mrdnout vlastni konzoli +// const evolu = run(createEvolu(...)) -const TodoItem: FC<{ - row: TodosRow; -}> = ({ row: { id, title, isCompleted } }) => { - const { update } = useEvolu(); - - const handleToggleCompletedClick = () => { - update("todo", { - id, - isCompleted: Evolu.booleanToSqliteBoolean(!isCompleted), - }); - }; - - const handleRenameClick = () => { - const newTitle = window.prompt("Edit todo", title); - if (newTitle == null) return; - - const result = update("todo", { id, title: newTitle }); - if (!result.ok) { - alert(formatTypeError(result.error)); - } - }; - - const handleDeleteClick = () => { - update("todo", { - id, - // Soft delete with isDeleted flag (CRDT-friendly, preserves sync history). - isDeleted: Evolu.sqliteTrue, - }); - }; - - return ( -
  • - -
    - - -
    -
  • - ); -}; +// ok, uz vim, nad cim jsem dumal, jak se to bude volat a predavat -const OwnerActions: FC = () => { - const evolu = useEvolu(); - const appOwner = use(evolu.appOwner); - - const [showMnemonic, setShowMnemonic] = useState(false); - - // Restore owner from mnemonic to sync data across devices. - const handleRestoreAppOwnerClick = () => { - const mnemonic = window.prompt("Enter your mnemonic to restore your data:"); - if (mnemonic == null) return; - - const result = Evolu.Mnemonic.from(mnemonic.trim()); - if (!result.ok) { - alert(formatTypeError(result.error)); - return; - } - - // void evolu.restoreAppOwner(result.value); - }; - - const handleResetAppOwnerClick = () => { - if (confirm("Are you sure? This will delete all your local data.")) { - // void evolu.resetAppOwner(); - } - }; - - const handleDownloadDatabaseClick = () => { - void evolu.exportDatabase().then((data) => { - using objectUrl = Evolu.createObjectURL( - new Blob([data], { type: "application/x-sqlite3" }), - ); - - const link = document.createElement("a"); - link.href = objectUrl.url; - link.download = `${evolu.name}.sqlite3`; - link.click(); - }); - }; - - return ( -
    -

    Account

    -

    - Todos are stored in local SQLite. When you sync across devices, your - data is end-to-end encrypted using your mnemonic. -

    - -
    -