create-stitchkit 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/UPGRADING.md +66 -0
- package/examples/repository/packages/backend/src/surface.snapshot.json +32 -0
- package/examples/repository/packages/backend/src/surface.ts +49 -2
- package/examples/repository/packages/frontend/src/app/[locale]/starter-page.tsx +7 -0
- package/examples/repository/packages/shared/src/index.ts +3 -0
- package/examples/repository/project.json +10 -0
- package/package.json +10 -10
- package/template/.data/board.sqlite +0 -0
- package/template/biome.json +1 -1
- package/template/bun.lock +194 -179
- package/template/e2e/starter.spec.ts +7 -1
- package/template/package.json +14 -8
- package/template/packages/backend/package.json +5 -5
- package/template/packages/backend/src/index.ts +10 -2
- package/template/packages/backend/src/lib/board.ts +99 -0
- package/template/packages/backend/src/lib/live.ts +73 -0
- package/template/packages/backend/src/surface.snapshot.json +32 -0
- package/template/packages/backend/src/surface.ts +53 -3
- package/template/packages/backend/src/transport/board-service.ts +18 -0
- package/template/packages/config/package.json +3 -3
- package/template/packages/config/src/variables.ts +16 -0
- package/template/packages/db/package.json +3 -3
- package/template/packages/frontend/package.json +15 -15
- package/template/packages/frontend/src/app/[locale]/page.tsx +2 -0
- package/template/packages/frontend/src/app/[locale]/starter-page.tsx +9 -1
- package/template/packages/frontend/src/features/board/board-live.ts +91 -0
- package/template/packages/frontend/src/features/board/board-panel.tsx +112 -0
- package/template/packages/frontend/src/lib/query-client.test.ts +6 -0
- package/template/packages/frontend/src/lib/query-client.ts +25 -17
- package/template/packages/shared/package.json +2 -2
- package/template/packages/shared/src/contracts/board.ts +54 -0
- package/template/packages/shared/src/contracts/live.ts +20 -0
- package/template/packages/shared/src/index.ts +3 -0
- package/template/packages/shared/src/schemas/board.ts +41 -0
- package/template/project.json +10 -0
- package/templates/agent/biome.json +1 -1
- package/templates/agent/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,62 @@ step is overwritten by the next release.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.6.1] — 2026-09-20
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **The generated frontend carries the canonical React Query runtime shape.**
|
|
20
|
+
`lib/query-client.ts` owns request-local SSR identity, one browser singleton,
|
|
21
|
+
pending dehydration, application `staleTime` and the retry policy a released
|
|
22
|
+
core already lets it state — one retry for queries, none for mutations — with
|
|
23
|
+
a test that keeps it there. It does not depend on an unreleased Stitchkit
|
|
24
|
+
export; `UPGRADING.md` records the one-step cutover to
|
|
25
|
+
`createQueryClientFactory` after the matching core release is available.
|
|
26
|
+
- **The generated workspace now targets Stitchkit `^0.90.5` and current stable
|
|
27
|
+
dependencies.** Its root and agent lockfiles were regenerated independently while
|
|
28
|
+
preserving the single `catalog.stitchkit` source and workspace `catalog:` links.
|
|
29
|
+
|
|
30
|
+
## [0.6.0] — 2026-09-02
|
|
31
|
+
|
|
32
|
+
The generated project stops being an empty frame with a to-do at the bottom of
|
|
33
|
+
the page. It ships **one vertical feature**, from schema to transport to UI —
|
|
34
|
+
and it was chosen so that every live primitive Stitchkit gained in 0.75–0.76 is
|
|
35
|
+
there because the feature needs it, not to demonstrate anything.
|
|
36
|
+
|
|
37
|
+
The demonstration is the second browser tab: post from it, and the first one
|
|
38
|
+
updates without asking. Nothing on that page polls, and nothing refetches after
|
|
39
|
+
a write.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **A live board.** `board.list` is a watched read and `board.post` writes to it.
|
|
44
|
+
Together they use, and only where they are needed:
|
|
45
|
+
- `defineEvents` — one topic, `board.changed`, declared in `shared` because the
|
|
46
|
+
server announces it and the browser subscribes to it;
|
|
47
|
+
- `defineKeyspace` / `openKeyspace` over SQLite — notes are authoritative in
|
|
48
|
+
memory and durable behind it, so a read in a handler needs no `await` and a
|
|
49
|
+
restart loses nothing;
|
|
50
|
+
- `createWatchHub` / `createWatchClient` — every browser asking the same
|
|
51
|
+
question is **one read** on the server, re-run when the topic says the answer
|
|
52
|
+
may have changed;
|
|
53
|
+
- `createTrustFence` — installed on **both** lanes when `TRUSTED_HOSTS` is set,
|
|
54
|
+
because the Socket.IO lane never reaches a lifecycle hook.
|
|
55
|
+
|
|
56
|
+
The comments say which of these to reach for and, more usefully, when not to:
|
|
57
|
+
a keyspace is for a small bounded set the process wants synchronously, and the
|
|
58
|
+
moment a thing wants queries, relations or unbounded growth it is a database
|
|
59
|
+
row and Prisma is already there for it.
|
|
60
|
+
|
|
61
|
+
- **Two environment variables**, both declared in the one place the project
|
|
62
|
+
declares variables: `BOARD_STORE_PATH` (defaulted, and the directory is created
|
|
63
|
+
by the application rather than by whoever deploys it) and `TRUSTED_HOSTS`
|
|
64
|
+
(unset means no fence, which is honest for a laptop and wrong for anything a
|
|
65
|
+
network can reach — a fence cannot invent the names it should answer to).
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- The template now targets `stitchkit` `^0.76.1`, up from `^0.71.0`.
|
|
70
|
+
|
|
15
71
|
## [0.5.1] — 2026-09-01
|
|
16
72
|
|
|
17
73
|
Three findings from someone setting up a new application on the starter from
|
package/UPGRADING.md
CHANGED
|
@@ -60,6 +60,72 @@ the first scaffolder release with a migration channel of its own.
|
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
+
## Released migration: 0.6.1
|
|
64
|
+
|
|
65
|
+
### Canonical query client factory
|
|
66
|
+
|
|
67
|
+
This migration is additive, but it has a dependency order: first upgrade to a
|
|
68
|
+
Stitchkit release that exports `createQueryClientFactory` from
|
|
69
|
+
`stitchkit/react`. Then replace the local QueryClient construction with:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { cache } from 'react';
|
|
73
|
+
import { createQueryClientFactory } from 'stitchkit/react';
|
|
74
|
+
|
|
75
|
+
export const getQueryClient = createQueryClientFactory({
|
|
76
|
+
serverCache: cache,
|
|
77
|
+
queryClient: {
|
|
78
|
+
defaultOptions: { queries: { staleTime: 30_000 } },
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Keep project-specific mutation toasts or cache configuration in the factory
|
|
84
|
+
options. Do not copy the framework retry predicate back into the application.
|
|
85
|
+
|
|
86
|
+
## Released migration: 0.6.0
|
|
87
|
+
|
|
88
|
+
The scaffolder gained a vertical feature. Adopting it in a project you already
|
|
89
|
+
own is optional — nothing breaks if you skip it — but two things are **operator
|
|
90
|
+
steps**, and skipping those with the feature adopted means the API will not
|
|
91
|
+
start.
|
|
92
|
+
|
|
93
|
+
### 1. The store directory has to be writable
|
|
94
|
+
|
|
95
|
+
`BOARD_STORE_PATH` defaults to `.data/board.sqlite`, relative to the API role's
|
|
96
|
+
working directory. The application creates the directory itself; what it cannot
|
|
97
|
+
do is make a read-only volume writable.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# on the machine, as the user the API runs as
|
|
101
|
+
test -w "$(dirname "${BOARD_STORE_PATH:-.data/board.sqlite}")" || echo "not writable"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
If the role runs from a read-only image, point `BOARD_STORE_PATH` at a mounted
|
|
105
|
+
volume instead.
|
|
106
|
+
|
|
107
|
+
### 2. Decide about the trust fence, on purpose
|
|
108
|
+
|
|
109
|
+
`TRUSTED_HOSTS` is unset by default, and unset means **no fence**. That is
|
|
110
|
+
correct on a laptop and wrong on anything a network reaches — but a fence cannot
|
|
111
|
+
guess which names your deployment answers to, so it refuses to invent them.
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# every authority this deployment answers on, comma separated
|
|
115
|
+
TRUSTED_HOSTS=app.internal,app.internal:5181
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Set it and the fence is installed on both lanes: HTTP before routing, and the
|
|
119
|
+
realtime handshake, which never reaches a lifecycle hook on either runtime. If
|
|
120
|
+
your browser lives on another origin you already declared it as `CORS_ORIGIN`,
|
|
121
|
+
and the fence reads that one rather than asking you a second time.
|
|
122
|
+
|
|
123
|
+
### 3. Nothing else
|
|
124
|
+
|
|
125
|
+
The rest of the feature is code you either copy or do not. The framework range
|
|
126
|
+
moved to `^0.76.1`; if you upgrade the dependency without taking the feature,
|
|
127
|
+
[the framework's own guide](../../docs/guide/upgrading.md) is the one to follow.
|
|
128
|
+
|
|
63
129
|
## Released migration: 0.5.0
|
|
64
130
|
|
|
65
131
|
### the approval policy names a tool that no longer exists
|
|
@@ -1,4 +1,36 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"service": "board",
|
|
4
|
+
"action": "list",
|
|
5
|
+
"scope": "public",
|
|
6
|
+
"hasInput": false,
|
|
7
|
+
"hasOutput": true,
|
|
8
|
+
"inputShape": null,
|
|
9
|
+
"outputShape": "ce49fe198eb3412f",
|
|
10
|
+
"http": [
|
|
11
|
+
{
|
|
12
|
+
"method": "GET",
|
|
13
|
+
"path": "/api/board"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"tools": {}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"service": "board",
|
|
20
|
+
"action": "post",
|
|
21
|
+
"scope": "public",
|
|
22
|
+
"hasInput": true,
|
|
23
|
+
"hasOutput": true,
|
|
24
|
+
"inputShape": "b1d8eb61c62cafed",
|
|
25
|
+
"outputShape": "ce49fe198eb3412f",
|
|
26
|
+
"http": [
|
|
27
|
+
{
|
|
28
|
+
"method": "POST",
|
|
29
|
+
"path": "/api/board"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"tools": {}
|
|
33
|
+
},
|
|
2
34
|
{
|
|
3
35
|
"service": "repository",
|
|
4
36
|
"action": "read",
|
|
@@ -1,16 +1,63 @@
|
|
|
1
1
|
import { env } from '@app/config';
|
|
2
2
|
import { repositoryRealtimeContract } from '@app/shared';
|
|
3
|
-
import {
|
|
3
|
+
import type { TrustFence } from 'stitchkit/server';
|
|
4
|
+
import { bindRealtimeServer, createSocketIOServer, createTrustFence } from 'stitchkit/server';
|
|
5
|
+
import { type BoardRuntime, openBoard } from './lib/board';
|
|
6
|
+
import { bindLive } from './lib/live';
|
|
7
|
+
import { createBoardService } from './transport/board-service';
|
|
4
8
|
import { createRepositoryService } from './transport/repository-service';
|
|
5
9
|
import { createSystemService } from './transport/system-service';
|
|
6
10
|
|
|
11
|
+
/**
|
|
12
|
+
* The fence this deployment answers behind, or nothing.
|
|
13
|
+
*
|
|
14
|
+
* `trustedHosts` has no default and cannot have one: a fence compares the
|
|
15
|
+
* authority a request addressed against a list, and it cannot invent the list.
|
|
16
|
+
* Unset means no fence — honest for a checkout on a laptop, wrong for anything
|
|
17
|
+
* a network can reach.
|
|
18
|
+
*/
|
|
19
|
+
function createFence(): TrustFence | undefined {
|
|
20
|
+
if (!env.TRUSTED_HOSTS) return undefined;
|
|
21
|
+
const browserOrigin = env.CORS_ORIGIN ? [new URL(env.CORS_ORIGIN).host] : [];
|
|
22
|
+
return createTrustFence({
|
|
23
|
+
trustedHosts: env.TRUSTED_HOSTS.split(',').map((entry) => entry.trim()),
|
|
24
|
+
...(browserOrigin.length > 0 && { trustedOrigins: browserOrigin }),
|
|
25
|
+
onRefused: (refusal) => {
|
|
26
|
+
console.warn(
|
|
27
|
+
`Refused a ${refusal.lane} request: ${refusal.reason} (host ${refusal.host ?? 'absent'})`,
|
|
28
|
+
);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
7
33
|
export async function createSurface() {
|
|
34
|
+
const board: BoardRuntime = await openBoard();
|
|
35
|
+
const fence = createFence();
|
|
36
|
+
|
|
8
37
|
const socket = await createSocketIOServer({
|
|
9
38
|
cors: { origin: env.CORS_ORIGIN ?? [] },
|
|
39
|
+
// The socket's own admission point. `/socket.io/*` never reaches a lifecycle
|
|
40
|
+
// hook on either runtime, so a fence installed only in `hooks` would leave
|
|
41
|
+
// open the lane this app pushes its live data over.
|
|
42
|
+
...(fence && { allowRequest: fence.allowRequest }),
|
|
10
43
|
});
|
|
44
|
+
|
|
45
|
+
// Two bindings over one socket, deliberately. This example's own contract and
|
|
46
|
+
// the live one are separate declarations with separate event names, and
|
|
47
|
+
// merging them into a single registry here would mean editing the merge every
|
|
48
|
+
// time either side gains a topic.
|
|
11
49
|
const realtime = bindRealtimeServer(repositoryRealtimeContract, socket);
|
|
50
|
+
const live = bindLive(board, socket);
|
|
51
|
+
|
|
12
52
|
const repositoryService = createRepositoryService((snapshot) =>
|
|
13
53
|
realtime.emit('repository:refreshed', snapshot),
|
|
14
54
|
);
|
|
15
|
-
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
socket,
|
|
58
|
+
fence,
|
|
59
|
+
board,
|
|
60
|
+
hub: live.hub,
|
|
61
|
+
services: [createSystemService(), createBoardService(board), repositoryService],
|
|
62
|
+
};
|
|
16
63
|
}
|
|
@@ -4,6 +4,7 @@ import { BrandMark } from '@/components/brand-mark';
|
|
|
4
4
|
import { RepositorySummary } from '@/components/repository-summary';
|
|
5
5
|
import { LanguageSwitcher, ThemeToggle } from '@/components/system-controls';
|
|
6
6
|
import { buttonVariants } from '@/components/ui';
|
|
7
|
+
import { BoardPanel } from '@/features/board/board-panel';
|
|
7
8
|
import type { AppLocale } from '@/i18n/locales';
|
|
8
9
|
import { Link } from '@/i18n/navigation';
|
|
9
10
|
import { absoluteSiteUrl } from '@/lib/seo/metadata';
|
|
@@ -15,6 +16,7 @@ interface StarterPageProps {
|
|
|
15
16
|
applicationDescription: string;
|
|
16
17
|
heroTitle: string;
|
|
17
18
|
catalogueLabel: string;
|
|
19
|
+
realtimeOrigin?: string;
|
|
18
20
|
locale: AppLocale;
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -62,6 +64,7 @@ export async function StarterPage({
|
|
|
62
64
|
applicationDescription,
|
|
63
65
|
heroTitle,
|
|
64
66
|
catalogueLabel,
|
|
67
|
+
realtimeOrigin,
|
|
65
68
|
locale,
|
|
66
69
|
}: StarterPageProps) {
|
|
67
70
|
const homeSeo = getSeoPage('home', locale);
|
|
@@ -140,6 +143,10 @@ export async function StarterPage({
|
|
|
140
143
|
</div>
|
|
141
144
|
</div>
|
|
142
145
|
|
|
146
|
+
<div className='mt-6 w-full max-w-xl'>
|
|
147
|
+
<BoardPanel realtimeOrigin={realtimeOrigin} />
|
|
148
|
+
</div>
|
|
149
|
+
|
|
143
150
|
<div className='mt-5 w-full'>
|
|
144
151
|
<RepositorySummary />
|
|
145
152
|
</div>
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
export * from './contracts/board';
|
|
2
|
+
export * from './contracts/live';
|
|
1
3
|
export * from './contracts/repository';
|
|
2
4
|
export * from './contracts/system';
|
|
3
5
|
export * from './realtime/repository';
|
|
6
|
+
export * from './schemas/board';
|
|
4
7
|
export * from './schemas/repository';
|
|
5
8
|
export * from './schemas/system';
|
|
@@ -105,6 +105,11 @@
|
|
|
105
105
|
"shape": "string",
|
|
106
106
|
"required": false
|
|
107
107
|
},
|
|
108
|
+
{
|
|
109
|
+
"name": "BOARD_STORE_PATH",
|
|
110
|
+
"shape": "string",
|
|
111
|
+
"required": false
|
|
112
|
+
},
|
|
108
113
|
{
|
|
109
114
|
"name": "CORS_ORIGIN",
|
|
110
115
|
"shape": "url",
|
|
@@ -179,6 +184,11 @@
|
|
|
179
184
|
"shape": "url",
|
|
180
185
|
"required": false
|
|
181
186
|
},
|
|
187
|
+
{
|
|
188
|
+
"name": "TRUSTED_HOSTS",
|
|
189
|
+
"shape": "string",
|
|
190
|
+
"required": false
|
|
191
|
+
},
|
|
182
192
|
{
|
|
183
193
|
"name": "WEB_PORT",
|
|
184
194
|
"shape": "integer",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-stitchkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Create a production-shaped Stitchkit application",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Max Listov <maxlistov@gmail.com>",
|
|
@@ -73,17 +73,17 @@
|
|
|
73
73
|
"prepublishOnly": "bun run check && bun run test && bun run build"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"zod": "^4.
|
|
76
|
+
"zod": "^4.6.5"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@opentui/core": "^0.5.
|
|
80
|
-
"@opentui/react": "^0.5.
|
|
81
|
-
"@openrouter/ai-sdk-provider": "^3.
|
|
82
|
-
"@types/bun": "^1.4.
|
|
83
|
-
"@types/react": "^19.
|
|
84
|
-
"ai": "^7.0.
|
|
85
|
-
"react": "^19.
|
|
86
|
-
"stitchkit": "0.
|
|
79
|
+
"@opentui/core": "^0.5.11",
|
|
80
|
+
"@opentui/react": "^0.5.11",
|
|
81
|
+
"@openrouter/ai-sdk-provider": "^3.1.0",
|
|
82
|
+
"@types/bun": "^1.4.2",
|
|
83
|
+
"@types/react": "^19.3.0",
|
|
84
|
+
"ai": "^7.0.107",
|
|
85
|
+
"react": "^19.3.0",
|
|
86
|
+
"stitchkit": "0.90.5",
|
|
87
87
|
"typescript": "^7.0.2"
|
|
88
88
|
},
|
|
89
89
|
"engines": {
|
|
Binary file
|
package/template/biome.json
CHANGED