Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions skills/mockapi/assets/templates/mock-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"private": true,
"version": "0.1.0",
"type": "module",
"exports": {
"./browser": "./src/browser.ts",
"./package.json": "./package.json"
},
"scripts": {
"build": "node scripts/build.mjs",
"check": "tsc -p tsconfig.json --noEmit",
Expand All @@ -14,6 +18,7 @@
},
"dependencies": {
"@hono/node-server": "^2.0.4",
"@msw/data": "1.1.6",
"hono": "^4.10.7",
"zod": "^4.3.6"
},
Expand Down
13 changes: 13 additions & 0 deletions skills/mockapi/assets/templates/mock-server/src/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @ts-nocheck -- mockapi template source; stripped during generation
export {
newMockApiDependencies,
type MockApiDependencies,
} from './dependencies.ts'
export {
newBrowserMockStateStore,
type BrowserMockStateStoreOptions,
} from './lib/browserStateStore.ts'
export { seedState } from './generated/mock-admin/state/seed.ts'
export { zMockState } from './generated/mock-admin/contract/zod.gen.ts'
export type { MockState } from './generated/mock-admin/contract/index.ts'
export type { MockStateStore } from './lib/stateStore.ts'
12 changes: 12 additions & 0 deletions skills/mockapi/assets/templates/mock-server/src/dependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// @ts-nocheck -- mockapi template source; stripped during generation
import type { MockStateStore } from './lib/stateStore.ts'

export type MockApiDependencies = {
stateStore: MockStateStore
}

export const newMockApiDependencies = (
stateStore: MockStateStore,
): MockApiDependencies => ({
stateStore,
})

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
// Do not edit by hand.

import type { MockClock, MockState } from '../contract/index.ts'
import type { MockStateRepository } from './repository.ts'
import type { MockStateStore } from '../../../lib/stateStore.ts'

export class AdminStateService {
constructor(
private readonly stateRepository: MockStateRepository,
private readonly stateStore: MockStateStore,
private readonly operationCount: number,
) {}

Expand All @@ -19,26 +19,26 @@ export class AdminStateService {
}

reset() {
return this.stateRepository.reset()
return this.stateStore.reset()
}

getState() {
return this.stateRepository.snapshot()
return this.stateStore.snapshot()
}

replaceState(state: MockState) {
return this.stateRepository.replace(state)
return this.stateStore.replace(state)
}

snapshot() {
return this.stateRepository.snapshot()
return this.stateStore.snapshot()
}

putSnapshot(state: MockState) {
return this.stateRepository.replace(state)
return this.stateStore.replace(state)
}

setClock(clock: MockClock) {
return this.stateRepository.setClock(clock.now)
return this.stateStore.setClock(clock.now)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// @ts-nocheck -- mockapi template source; stripped during generation
import { clone } from './clone.ts'
import {
newMswDataStateStore,
type MswDataStateStore,
} from './stateStore.ts'
import type { MockState } from '../generated/mock-admin/contract/index.ts'
import { zMockState } from '../generated/mock-admin/contract/zod.gen.ts'
import { seedState } from '../generated/mock-admin/state/seed.ts'

export type BrowserMockStateStoreOptions = {
initialState?: MockState
storageKey: string
}

export const newBrowserMockStateStore = async (
options: BrowserMockStateStoreOptions,
): Promise<MswDataStateStore> => {
const initialState = options.initialState
? clone(options.initialState)
: readPersistedState(options.storageKey) ?? seedState()

return newMswDataStateStore({
initialState,
persist: (state) => persistState(options.storageKey, state),
})
}

const persistState = (storageKey: string, state: MockState) => {
if (typeof window === 'undefined') {
return
}

window.localStorage.setItem(storageKey, JSON.stringify(state))
}

const readPersistedState = (storageKey: string) => {
if (typeof window === 'undefined') {
return null
}

const raw = window.localStorage.getItem(storageKey)

if (!raw) {
return null
}

try {
const result = zMockState.safeParse(JSON.parse(raw))

return result.success ? result.data : null
} catch {
return null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// @ts-nocheck -- mockapi template source; stripped during generation
import {
mkdir,
readFile,
writeFile,
} from 'node:fs/promises'
import path from 'node:path'

import { clone } from './clone.ts'
import {
newMswDataStateStore,
type MswDataStateStore,
} from './stateStore.ts'
import type { MockState } from '../generated/mock-admin/contract/index.ts'
import { zMockState } from '../generated/mock-admin/contract/zod.gen.ts'
import { seedState } from '../generated/mock-admin/state/seed.ts'

export type MockStateOptions = {
initialState?: MockState
stateFile?: string
}

export const newFileMockStateStore = async (
options: MockStateOptions = {},
): Promise<MswDataStateStore> => {
const initialState = options.initialState
? clone(options.initialState)
: (await readPersistedState(options.stateFile)) ?? seedState()

return newMswDataStateStore({
initialState,
persist: (state) => persistState(options.stateFile, state),
})
}

const persistState = async (stateFile: string | undefined, state: MockState) => {
if (!stateFile) {
return
}

await mkdir(path.dirname(stateFile), { recursive: true })
await writeFile(stateFile, `${JSON.stringify(state, null, 2)}\n`, 'utf8')
}

const readPersistedState = async (stateFile: string | undefined) => {
if (!stateFile) {
return null
}

try {
const result = zMockState.safeParse(JSON.parse(await readFile(stateFile, 'utf8')))

return result.success ? result.data : null
} catch (error) {
if (isNodeFileError(error) && error.code === 'ENOENT') {
return null
}

console.warn(`Ignoring invalid mock state at ${stateFile}. Resetting to seed state.`)

return null
}
}

const isNodeFileError = (error: unknown): error is NodeJS.ErrnoException =>
error instanceof Error && 'code' in error
4 changes: 2 additions & 2 deletions skills/mockapi/assets/templates/mock-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { readMockServerConfig } from './config.ts'
import { newMockApiControllers } from './controllers.ts'

const config = readMockServerConfig()
const app = newMockApiApp({
controllers: newMockApiControllers({
const app = await newMockApiApp({
controllers: await newMockApiControllers({
stateFile: config.stateFile,
}),
})
Expand Down
24 changes: 14 additions & 10 deletions skills/mockapi/reference/generate.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,15 @@ nvm-managed Node installations and corepack for pnpm/Yarn.
7. After codegen succeeds, read `reference/mock-server-structure.md`,
`reference/mock-server-examples.md`, `reference/seed-data.md`,
`reference/testing.md`, `reference/mock-server-quality.md`,
`.mockapi/behavior.md`, generated `src/generated/**`,
`.mockapi/behavior.md`, generated `src/generated/**`, `src/dependencies.ts`,
`src/controllers.ts`, and `src/features/**`. Create
feature-local `service.ts` when behavior needs orchestration. Create
feature-local `repository.ts` for every completed feature that owns
non-infrastructure state slices; `idCounters` alone does not need a
repository. Replace generated product seed stub defaults unless
`.mockapi/profile.toml` sets `state.seed = false`. Extend
`MockApiDependencies` in `src/controllers.ts`, instantiate
repositories/services from the shared `MockStateRepository`, and pass
`MockApiDependencies` in `src/dependencies.ts`, instantiate
repositories/services from the shared `MockStateStore`, and pass
dependencies into operation controllers through `deps`. Keep feature seed
data in `src/features/<feature>/seed.ts`; generated
`src/generated/mock-admin/state/seed.ts` will aggregate feature seed
Expand All @@ -151,10 +151,12 @@ nvm-managed Node installations and corepack for pnpm/Yarn.
mutations such as `create`/`update`/`markDeleted`/`restore`, and avoid
service-facing `setAll`, `setItems`, or `replaceAll` APIs.
Do not create a single shared product repository. Services may use
`MockStateRepository` directly for transactions, mock clock helpers,
`MockStateStore` directly for transactions, mock clock helpers,
`idCounters`, and cross-repository coordination only; product state reads and
writes go through feature repositories. Read-only composite features should
consume other feature repositories instead of raw state slices. Keep operation
consume other feature repositories instead of raw state slices. Wrap mutating
workflows in `await stateStore.transaction(async () => ...)` and await
repository mutations inside the transaction. Keep operation
controllers thin; do not put data-access logic there. Replace controller
TODOs from the matching behavior anchors. Use counter IDs with `idCounters`
and `newIdAllocator` by default for create operations; slug-style IDs require
Expand Down Expand Up @@ -202,11 +204,13 @@ incomplete.
- OpenAPI runtime/config files and `src/generated/mock-admin/state/**` may be
overwritten.
- Template scaffolds such as `src/app.ts`, `src/server.ts`,
`src/controllers.ts`, and `src/lib/**` are create-only.
- `src/controllers.ts` is the LLM-owned composition root for DI and route
controller wiring. If the profile API set changes after initial generation,
update `src/app.ts` registrations manually as part of the same LLM-owned
wiring pass.
`src/controllers.ts`, `src/dependencies.ts`, and `src/lib/**` are
create-only.
- `src/dependencies.ts` is the LLM-owned composition root for feature
repositories and services. `src/controllers.ts` is the LLM-owned route
controller aggregation and admin wiring file. If the profile API set changes
after initial generation, update `src/app.ts` registrations manually as part
of the same LLM-owned wiring pass.
- `src/features/**` is reviewable behavior. Generation creates missing
operation controller TODO adapters and create-only seed stubs for owned state
slices. Feature services and repositories are LLM-owned and created only when
Expand Down
Loading