From f45827438e15a50a56982c011c4a5c5b2ba70ea6 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Fri, 16 Oct 2026 03:43:24 +0000 Subject: [PATCH] test(site): prove GuestbookStore with a fake and a real D1 under one suite GuestbookStore was a port with a D1 adapter and no contract. It now ships its in-memory fake beside the service (guestbook-store.memory.ts, a Ref that mirrors the adapter's ids, filters, cap and compare-and-set) and one law suite, tests/features/guestbook/guestbook-store.integration.test.ts, that runs the same fixed histories against the fake and a real local D1 (Miniflare, migrated from the repo's own migrations/*.sql per build): read-after-write, stable repeated reads, commuting different-id writes, newest-first latest that hides Hidden and caps at twenty, stale move refused with the state unchanged, unknown id refused, and a non-enum stored state read back verbatim. Expected values come from each history, never from an adapter. Sabotage: LATEST_ENTRIES 19 fails the D1 cap law; a broken fake compare-and-set fails the fake's stale-move law. miniflare is pinned at the version the lockfile already resolved. Judgment surfaces: apps/site/tsconfig.json includes tests; apps/site/ package.json gains a #guestbook/* import map and two devDependencies. Owner-directed (CONST-W3 declaration). Follows the boundary-testing pack. --- README.md | 4 +- apps/site/package.json | 5 + .../guestbook/guestbook-store.memory.ts | 78 ++++++ .../guestbook/__fixtures__/d1-oracle.ts | 74 +++++ .../guestbook-store.integration.test.ts | 257 ++++++++++++++++++ apps/site/tsconfig.json | 1 + pnpm-lock.yaml | 9 + pnpm-workspace.yaml | 1 + 8 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 apps/site/src/features/guestbook/guestbook-store.memory.ts create mode 100644 apps/site/tests/features/guestbook/__fixtures__/d1-oracle.ts create mode 100644 apps/site/tests/features/guestbook/guestbook-store.integration.test.ts diff --git a/README.md b/README.md index c02e62f2..a6b95180 100644 --- a/README.md +++ b/README.md @@ -118,13 +118,13 @@ The site ships one example feature, a guestbook at `/guestbook`: a pure decision To remove it: -1. Delete `apps/site/src/features/guestbook`. +1. Delete `apps/site/src/features/guestbook` and the guestbook store law suite at `apps/site/tests/features/guestbook`. 2. Delete `apps/site-e2e/tests/features/guestbook`. 3. Delete the route file `apps/site/src/routes/guestbook.tsx`. 4. `apps/site/src/api/site-rpcs.ts`: drop the `GuestbookRpcs` import and make `SiteRpcs` just `HealthRpcs`. 5. `apps/site/src/api/site-rpc-server.ts`: drop the five `../features/guestbook/…` imports and the `GuestbookStoreLive` line; build the context from `SiteDatabaseLive` alone (the health check still probes D1); and delete the `guestbook` record, so the `toLayer` effect returns only `health`. 6. `apps/site/alchemy.run.ts`: drop the `migrations` option from `Cloudflare.D1.Database('Database', …)`. -7. `pnpm-workspace.yaml`: drop the `@systemfsoftware/xstate` line under `catalog:` and its mirror under `overrides:`; `apps/site/package.json`: drop the `@systemfsoftware/xstate` dependency. +7. `pnpm-workspace.yaml`: drop the `@systemfsoftware/xstate` line under `catalog:` and its mirror under `overrides:`; `apps/site/package.json`: drop the `@systemfsoftware/xstate` dependency. Do the same for the guestbook store law suite's dev-only dependencies — `miniflare` and `@systemfsoftware/effect-gherkin-spec` (the `catalog:` line for miniflare, and both entries in `apps/site/package.json`'s `devDependencies`). 8. Run `pnpm install --no-frozen-lockfile` from the shell you already have open. It rewrites `pnpm-lock.yaml` without its xstate entries; `--frozen-lockfile` (pnpm's default when `CI` is set) refuses that rewrite. The devshell builds its pnpm store from `pnpm-lock.yaml`, so it can't start again until the lockfile drops xstate. 9. `flake.nix`: drop the `systemfsoftware-xstate` input, its name in the `outputs` arguments, and its two places in the `sfs-deps` derivation (the copy loop and the `jq -s add` merge); then run `nix flake lock`. 10. Delete this README's guestbook text, from "The site ships one example feature" through the D1 note below. diff --git a/apps/site/package.json b/apps/site/package.json index 45292a45..5f2ccc85 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -1,6 +1,9 @@ { "author": "TODO ", "description": "The one Worker: the starter's TanStack Start site", + "imports": { + "#guestbook/*": "./src/features/guestbook/*" + }, "dependencies": { "@systemfsoftware/effect-cell-types": "catalog:", "@systemfsoftware/xstate": "catalog:", @@ -16,6 +19,7 @@ "@cloudflare/vite-plugin": "catalog:", "@cloudflare/workers-types": "catalog:", "@rolldown/plugin-babel": "catalog:", + "@systemfsoftware/effect-gherkin-spec": "catalog:", "@systemfsoftware/oxlint-config-recommended": "catalog:", "@systemfsoftware/stryker-ignorer-effect-schema-declarations": "catalog:", "@systemfsoftware/stryker-js": "catalog:", @@ -30,6 +34,7 @@ "@vitejs/plugin-react": "catalog:", "alchemy": "catalog:", "babel-plugin-react-compiler": "catalog:", + "miniflare": "catalog:", "oxlint": "catalog:", "typescript": "catalog:", "vite": "catalog:", diff --git a/apps/site/src/features/guestbook/guestbook-store.memory.ts b/apps/site/src/features/guestbook/guestbook-store.memory.ts new file mode 100644 index 00000000..5dbaa960 --- /dev/null +++ b/apps/site/src/features/guestbook/guestbook-store.memory.ts @@ -0,0 +1,78 @@ +import * as Effect from 'effect/Effect' +import * as Layer from 'effect/Layer' +import * as Ref from 'effect/Ref' +import * as S from 'effect/Schema' + +import { GuestbookStore } from './guestbook-store.service' +import { + EntryNotFound, + GuestbookEntries, + GuestbookEntry, + GuestbookRowInvalid, + StoredStateRow, + TransitionConflict, +} from './guestbook.schema' +import type { EntryAccepted } from './sign-guestbook.workflow' + +const LATEST_ENTRIES = 20 + +export interface MemorySeedRow { + readonly id: number + readonly guest: string + readonly message: string + readonly state: string +} + +interface StoredRow { + readonly id: number + readonly guest: string + readonly message: string + readonly state: string +} + +const rowInvalid = (error: S.SchemaError): GuestbookRowInvalid => new GuestbookRowInvalid({ detail: error.message }) + +const visibleInLatest = (row: StoredRow): boolean => row.state === 'Visible' || row.state === 'Flagged' + +const nextId = (rows: ReadonlyArray): number => rows.reduce((max, row) => Math.max(max, row.id), 0) + 1 + +export const layer = (seed: ReadonlyArray = []): Layer.Layer => + Layer.effect( + GuestbookStore, + Effect.gen(function*() { + const rows = yield* Ref.make>(seed) + return { + sign: (entry: S.Codec.Encoded) => + Ref.modify(rows, (all): readonly [StoredRow, ReadonlyArray] => { + const row: StoredRow = { id: nextId(all), guest: entry.guest, message: entry.message, state: 'Visible' } + return [row, [...all, row]] + }).pipe( + Effect.flatMap((row) => S.decodeUnknownEffect(GuestbookEntry)(row).pipe(Effect.mapError(rowInvalid))), + ), + latest: Ref.get(rows).pipe( + Effect.flatMap((all) => + S.decodeUnknownEffect(GuestbookEntries)( + all.filter(visibleInLatest).sort((a, b) => b.id - a.id).slice(0, LATEST_ENTRIES), + ).pipe(Effect.mapError(rowInvalid)) + ), + ), + stateOf: (id) => + Ref.get(rows).pipe( + Effect.map((all) => all.find((row) => row.id === id)), + Effect.flatMap((row) => Effect.mapError(Effect.fromNullishOr(row), () => new EntryNotFound({ id }))), + Effect.flatMap((row) => S.decodeEffect(StoredStateRow)(row).pipe(Effect.mapError(rowInvalid))), + Effect.map(({ state }) => state), + ), + move: (id, from, to) => + Ref.modify( + rows, + (all): readonly [Effect.Effect, ReadonlyArray] => { + const holdsFrom = all.some((stored) => (stored.id === id ? stored.state === from : false)) + return holdsFrom + ? [Effect.succeed(to), all.map((stored) => (stored.id === id ? { ...stored, state: to } : stored))] + : [Effect.fail(new TransitionConflict({ id, from })), all] + }, + ).pipe(Effect.flatten), + } + }), + ) diff --git a/apps/site/tests/features/guestbook/__fixtures__/d1-oracle.ts b/apps/site/tests/features/guestbook/__fixtures__/d1-oracle.ts new file mode 100644 index 00000000..b67c3705 --- /dev/null +++ b/apps/site/tests/features/guestbook/__fixtures__/d1-oracle.ts @@ -0,0 +1,74 @@ +import { readdir, readFile } from 'node:fs/promises' + +import * as Context from 'effect/Context' +import * as Effect from 'effect/Effect' +import * as Layer from 'effect/Layer' +import { convertV4MiniflareOptions, Miniflare } from 'miniflare' + +import { layer as d1Layer } from '#guestbook/d1-guestbook-store.ts' +import type { MemorySeedRow } from '#guestbook/guestbook-store.memory.ts' +import { GuestbookStore } from '#guestbook/guestbook-store.service.ts' + +const migrationsUrl = new URL('../../../../src/features/guestbook/migrations/', import.meta.url) + +const singleLineStatements = (sql: string): ReadonlyArray => + sql.split(';').map((statement) => statement.replace(/\s+/g, ' ').trim()).filter((statement) => statement.length > 0) + +export class GuestbookMigrations extends Context.Service>()( + 'test/GuestbookMigrations', +) {} + +export const migrationsLayer: Layer.Layer = Layer.effect( + GuestbookMigrations, + Effect.gen(function*() { + const names = yield* Effect.tryPromise(() => readdir(migrationsUrl)).pipe(Effect.orDie) + const ordered = names.filter((name) => name.endsWith('.sql')).sort() + const byFile = yield* Effect.forEach( + ordered, + (name) => + Effect.tryPromise(() => readFile(new URL(name, migrationsUrl), 'utf8')).pipe( + Effect.orDie, + Effect.map(singleLineStatements), + ), + ) + return byFile.flat() + }), +) + +export const layer = ( + seed: ReadonlyArray = [], +): Layer.Layer => + Layer.effect( + GuestbookStore, + Effect.gen(function*() { + const miniflare = yield* Effect.acquireRelease( + Effect.sync( + () => + new Miniflare( + convertV4MiniflareOptions({ + script: 'export default { fetch: () => new Response(null, { status: 404 }) }', + modules: true, + compatibilityDate: '2026-10-01', + d1Databases: ['GUESTBOOK'], + }), + ), + ), + (instance) => Effect.promise(() => instance.dispose()), + ) + const database = yield* Effect.tryPromise(() => miniflare.getD1Database('GUESTBOOK')).pipe(Effect.orDie) + const migrations = yield* GuestbookMigrations + for (const statement of migrations) { + yield* Effect.tryPromise(() => database.exec(statement)).pipe(Effect.orDie) + } + for (const row of seed) { + yield* Effect.tryPromise(() => + database + .prepare('INSERT INTO guestbook_entries (id, guest, message, state) VALUES (?1, ?2, ?3, ?4)') + .bind(row.id, row.guest, row.message, row.state) + .run() + ).pipe(Effect.orDie) + } + const built = yield* Layer.build(d1Layer(database)) + return Context.get(built, GuestbookStore) + }), + ) diff --git a/apps/site/tests/features/guestbook/guestbook-store.integration.test.ts b/apps/site/tests/features/guestbook/guestbook-store.integration.test.ts new file mode 100644 index 00000000..a4d93a92 --- /dev/null +++ b/apps/site/tests/features/guestbook/guestbook-store.integration.test.ts @@ -0,0 +1,257 @@ +import { Gherkin, it, makeFeature, pairwiseFor, Then } from '@systemfsoftware/effect-gherkin-spec' +import * as Arr from 'effect/Array' +import * as Effect from 'effect/Effect' +import * as Result from 'effect/Result' +import * as S from 'effect/Schema' + +import * as Memory from '#guestbook/guestbook-store.memory.ts' +import { GuestbookStore } from '#guestbook/guestbook-store.service.ts' +import { EntryId, EntryNotFound, GuestbookEntry, TransitionConflict } from '#guestbook/guestbook.schema.ts' +import type { EntryAccepted } from '#guestbook/sign-guestbook.workflow.ts' +import * as D1Oracle from './__fixtures__/d1-oracle.ts' + +const Feature = makeFeature({ it }) + +const ADA = { guest: 'Ada Lovelace', message: 'The analytical engine would love this guestbook' } as const +const GRACE = { guest: 'Grace Hopper', message: 'Compiled with care' } as const +const ALAN = { guest: 'Alan Turing', message: 'Can machines sign guestbooks?' } as const + +const signOf = (guest: string, message: string): S.Codec.Encoded => ({ + _tag: 'EntryAccepted', + guest, + message, +}) + +const ADA_SIGNS = signOf(ADA.guest, ADA.message) +const GRACE_SIGNS = signOf(GRACE.guest, GRACE.message) +const ALAN_SIGNS = signOf(ALAN.guest, ALAN.message) + +const ENTRY_ONE: EntryId = Result.getOrThrow(S.decodeResult(EntryId)(1)) + +const EXPECTED_ADA: GuestbookEntry = Result.getOrThrow( + S.decodeResult(GuestbookEntry)({ id: 1, guest: ADA.guest, message: ADA.message, state: 'Visible' }), +) +const EXPECTED_GRACE: GuestbookEntry = Result.getOrThrow( + S.decodeResult(GuestbookEntry)({ id: 2, guest: GRACE.guest, message: GRACE.message, state: 'Visible' }), +) +const EXPECTED_ALAN: GuestbookEntry = Result.getOrThrow( + S.decodeResult(GuestbookEntry)({ id: 3, guest: ALAN.guest, message: ALAN.message, state: 'Visible' }), +) + +const EXPECTED_STALE_MOVE = new TransitionConflict({ id: ENTRY_ONE, from: 'Visible' }) +const EXPECTED_NOT_FOUND = new EntryNotFound({ id: ENTRY_ONE }) + +const EXPECTED_LATEST_IDS = [21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2] + +const signedView = (entry: GuestbookEntry) => ({ guest: entry.guest, message: entry.message, state: entry.state }) + +const guestbookView = (entries: ReadonlyArray) => + entries.map(signedView).sort((a, b) => (a.guest < b.guest ? -1 : 1)) + +const EXPECTED_ADA_VIEW = signedView(EXPECTED_ADA) +const EXPECTED_GRACE_VIEW = signedView(EXPECTED_GRACE) + +const BANISHED_SEED: ReadonlyArray = [ + { id: 1, guest: 'Mallory', message: 'Not welcome anywhere', state: 'Banished' }, +] + +const OnBothStores = pairwiseFor( + { a: { name: 'memory', layer: Memory.layer() }, b: { name: 'd1', layer: D1Oracle.layer() } }, + GuestbookStore, +) + +const OnSeededStores = pairwiseFor( + { + a: { name: 'memory', layer: Memory.layer(BANISHED_SEED) }, + b: { name: 'd1', layer: D1Oracle.layer(BANISHED_SEED) }, + }, + GuestbookStore, +) + +Feature('The guestbook store obeys the same laws over the memory fake and over real D1') + .withLayer(D1Oracle.migrationsLayer) + .live('the D1 oracle reads migration files from disk and runs a real Miniflare workerd process') + .body(({ scenario }) => { + scenario( + 'a signed entry reads back exactly as it was written', + Gherkin.Do.pipe( + OnBothStores('a guest signs, and the entry is read back')( + 'readBack', + () => (store) => + Effect.gen(function*() { + const entry = yield* store.sign(ADA_SIGNS) + const state = yield* store.stateOf(ENTRY_ONE) + return { entry, state } + }), + ), + Then('the returned entry is the first entry in Visible, and its stored state agrees')( + ({ readBack }, expect) => + expect({ memory: readBack.a, d1: readBack.b }).toEqual({ + memory: { entry: EXPECTED_ADA, state: 'Visible' }, + d1: { entry: EXPECTED_ADA, state: 'Visible' }, + }), + ), + ), + ) + + scenario( + 'the latest page lists newest entries first and never hidden ones', + Gherkin.Do.pipe( + OnBothStores('three guests sign and the first entry is hidden')( + 'page', + () => (store) => + Effect.gen(function*() { + yield* store.sign(ADA_SIGNS) + yield* store.sign(GRACE_SIGNS) + yield* store.sign(ALAN_SIGNS) + yield* store.move(ENTRY_ONE, 'Visible', 'Hidden') + return yield* store.latest + }), + ), + Then('the page holds the third and second entries, newest first')(({ page }, expect) => + expect({ memory: page.a, d1: page.b }).toEqual({ + memory: [EXPECTED_ALAN, EXPECTED_GRACE], + d1: [EXPECTED_ALAN, EXPECTED_GRACE], + }) + ), + ), + ) + + scenario( + 'reading the latest page again returns the same entries', + Gherkin.Do.pipe( + OnBothStores('a guest signs and the page is read twice')( + 'pages', + () => (store) => + Effect.gen(function*() { + yield* store.sign(ADA_SIGNS) + const first = yield* store.latest + const second = yield* store.latest + return { first, second } + }), + ), + Then('both reads return the signed entry')(({ pages }, expect) => + expect({ memory: pages.a, d1: pages.b }).toEqual({ + memory: { first: [EXPECTED_ADA], second: [EXPECTED_ADA] }, + d1: { first: [EXPECTED_ADA], second: [EXPECTED_ADA] }, + }) + ), + ), + ) + + scenario( + 'entries signed in either order leave the same guestbook', + Gherkin.Do.pipe( + OnBothStores('Ada signs before Grace')( + 'adaFirst', + () => (store) => + Effect.gen(function*() { + yield* store.sign(ADA_SIGNS) + yield* store.sign(GRACE_SIGNS) + return yield* store.latest + }).pipe(Effect.map(guestbookView)), + ), + OnBothStores('Grace signs before Ada')( + 'graceFirst', + () => (store) => + Effect.gen(function*() { + yield* store.sign(GRACE_SIGNS) + yield* store.sign(ADA_SIGNS) + return yield* store.latest + }).pipe(Effect.map(guestbookView)), + ), + Then('either order leaves the same two visible entries')(({ adaFirst, graceFirst }, expect) => + expect({ + adaFirst: { a: adaFirst.a, b: adaFirst.b }, + graceFirst: { a: graceFirst.a, b: graceFirst.b }, + }).toEqual({ + adaFirst: { a: [EXPECTED_ADA_VIEW, EXPECTED_GRACE_VIEW], b: [EXPECTED_ADA_VIEW, EXPECTED_GRACE_VIEW] }, + graceFirst: { a: [EXPECTED_ADA_VIEW, EXPECTED_GRACE_VIEW], b: [EXPECTED_ADA_VIEW, EXPECTED_GRACE_VIEW] }, + }) + ), + ), + ) + + scenario( + 'the latest page holds at most twenty entries', + Gherkin.Do.pipe( + OnBothStores('twenty-one guests sign and the page is read')( + 'ids', + () => (store) => + Effect.gen(function*() { + yield* Effect.forEach(Arr.range(1, 21), (n) => store.sign(signOf(`Signer ${n}`, `Entry ${n}`))) + const page = yield* store.latest + return page.map((entry) => entry.id) + }), + ), + Then('the page holds entries twenty-one through two, newest first')(({ ids }, expect) => + expect({ memory: ids.a, d1: ids.b }).toEqual({ + memory: EXPECTED_LATEST_IDS, + d1: EXPECTED_LATEST_IDS, + }) + ), + ), + ) + + scenario( + 'a move from a stale state is refused and changes nothing', + Gherkin.Do.pipe( + OnBothStores('an entry is flagged, then a stale move is attempted')( + 'outcome', + () => (store) => + Effect.gen(function*() { + yield* store.sign(ADA_SIGNS) + const moved = yield* store.move(ENTRY_ONE, 'Visible', 'Flagged') + const refused = yield* Effect.flip(store.move(ENTRY_ONE, 'Visible', 'Hidden')) + const state = yield* store.stateOf(ENTRY_ONE) + return { moved, refused, state } + }), + ), + Then('the stale move is refused as a transition conflict and the entry stays flagged')( + ({ outcome }, expect) => + expect({ memory: outcome.a, d1: outcome.b }).toEqual({ + memory: { moved: 'Flagged', refused: EXPECTED_STALE_MOVE, state: 'Flagged' }, + d1: { moved: 'Flagged', refused: EXPECTED_STALE_MOVE, state: 'Flagged' }, + }), + ), + ), + ) + + scenario( + 'reading a missing entry reports it is not found', + Gherkin.Do.pipe( + OnBothStores('the state of entry one is read on an empty guestbook')( + 'missing', + () => (store) => Effect.flip(store.stateOf(ENTRY_ONE)), + ), + Then('the read is an entry-not-found refusal')(({ missing }, expect) => + expect({ memory: missing.a, d1: missing.b }).toEqual({ + memory: EXPECTED_NOT_FOUND, + d1: EXPECTED_NOT_FOUND, + }) + ), + ), + ) + + scenario( + 'a stored state outside the lifecycle reads back verbatim', + Gherkin.Do.pipe( + OnSeededStores('a seeded entry holds a state the lifecycle does not know')( + 'stored', + () => (store) => + Effect.gen(function*() { + const state = yield* store.stateOf(ENTRY_ONE) + const page = yield* store.latest + return { state, page } + }), + ), + Then('the raw state comes back verbatim and the entry stays off the latest page')( + ({ stored }, expect) => + expect({ memory: stored.a, d1: stored.b }).toEqual({ + memory: { state: 'Banished', page: [] }, + d1: { state: 'Banished', page: [] }, + }), + ), + ), + ) + }) diff --git a/apps/site/tsconfig.json b/apps/site/tsconfig.json index d3a2ec71..6ecf0b59 100644 --- a/apps/site/tsconfig.json +++ b/apps/site/tsconfig.json @@ -15,6 +15,7 @@ }, "include": [ "src", + "tests", "alchemy.run.ts" ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 954465ce..ab732d7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -224,6 +224,9 @@ catalogs: lint-staged: specifier: 17.6.0 version: 17.6.0 + miniflare: + specifier: 5.20261001.0-alpha + version: 5.20261001.0-alpha oxlint: specifier: 1.82.0 version: 1.82.0 @@ -387,6 +390,9 @@ importers: '@rolldown/plugin-babel': specifier: 'catalog:' version: 0.2.4(@babel/core@8.0.6)(rolldown@1.2.12)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(yaml@2.9.1)) + '@systemfsoftware/effect-gherkin-spec': + specifier: file:../../.sfs-deps/effect-gherkin-spec-7.0.1.tgz + version: file:.sfs-deps/effect-gherkin-spec-7.0.1.tgz(@systemfsoftware/vitest@file:.sfs-deps/vitest-2.0.0.tgz(effect@4.0.1)(redis@6.2.1(@opentelemetry/api@1.9.1))(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(yaml@2.9.1))))(effect@4.0.1)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(yaml@2.9.1))) '@systemfsoftware/oxlint-config-recommended': specifier: file:../../.sfs-deps/oxlint-config-recommended-4.0.0.tgz version: file:.sfs-deps/oxlint-config-recommended-4.0.0.tgz(effect@4.0.1)(oxlint-tsgolint@7.0.2001)(oxlint@1.82.0(oxlint-tsgolint@7.0.2001))(typescript@7.0.2) @@ -429,6 +435,9 @@ importers: babel-plugin-react-compiler: specifier: 'catalog:' version: 1.0.0 + miniflare: + specifier: 'catalog:' + version: 5.20261001.0-alpha(@types/node@24.19.1) oxlint: specifier: 'catalog:' version: 1.82.0(oxlint-tsgolint@7.0.2001) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c9ed1f55..63ef12d3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -63,6 +63,7 @@ catalog: babel-plugin-react-compiler: 1.0.0 effect: 4.0.1 lint-staged: 17.6.0 + miniflare: 5.20261001.0-alpha oxlint: 1.82.0 oxlint-tsgolint: 7.0.2001 playwright: 1.63.0