create-stitchkit 0.3.3 → 0.4.0
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 +218 -0
- package/README.md +3 -1
- package/UPGRADING.md +225 -0
- package/dist/cli.js +233 -41
- package/examples/repository/_env.example.append +21 -0
- package/examples/repository/packages/backend/src/domain/repository/github-cache.ts +2 -2
- package/examples/repository/packages/backend/src/surface.ts +1 -1
- package/examples/repository/packages/config/src/features.ts +17 -0
- package/examples/repository/packages/frontend/src/app/[locale]/page.tsx +4 -4
- package/examples/repository/packages/frontend/src/app/[locale]/starter-page.tsx +2 -2
- package/examples/repository/packages/frontend/src/app/api/[...path]/route.ts +66 -0
- package/examples/repository/packages/frontend/src/lib/api/client.ts +17 -12
- package/examples/repository/packages/frontend/src/lib/api/cross-origin.ts +87 -0
- package/examples/repository/packages/frontend/src/lib/api/place.ts +26 -0
- package/examples/repository/packages/frontend/src/lib/api/queries.ts +3 -0
- package/examples/repository/packages/frontend/src/lib/api/server-client.ts +11 -0
- package/examples/repository/packages/frontend/src/lib/realtime/repository.ts +44 -12
- package/examples/repository/packages/frontend/src/providers/client-providers.tsx +30 -0
- package/examples/repository/packages/frontend/src/providers/index.tsx +17 -12
- package/examples/repository/packages/frontend/src/providers/realtime.tsx +6 -4
- package/examples/repository/project.json +189 -0
- package/examples/repository/scripts/runtime-smoke.ts +25 -5
- package/package.json +9 -1
- package/template/AGENTS.md +12 -2
- package/template/README.md +43 -6
- package/template/_env.example +8 -4
- package/template/biome.json +5 -1
- package/template/bun.lock +2 -2
- package/template/e2e/starter.spec.ts +5 -7
- package/template/ecosystem.config.cjs +42 -19
- package/template/ecosystem.dev.config.cjs +41 -21
- package/template/package.json +5 -4
- package/template/packages/backend/src/cli.ts +6 -2
- package/template/packages/backend/src/index.ts +21 -5
- package/template/packages/backend/src/surface.ts +6 -1
- package/template/packages/backend/src/transport/errors.ts +4 -2
- package/template/packages/config/package.json +3 -1
- package/template/packages/config/src/app-identity.generated.ts +20 -0
- package/template/packages/config/src/declaration.ts +30 -0
- package/template/packages/config/src/project-declaration.generated.ts +611 -0
- package/template/packages/config/src/server.ts +8 -17
- package/template/packages/config/src/variables.ts +89 -0
- package/template/packages/frontend/next.config.ts +3 -2
- package/template/packages/frontend/package.json +2 -2
- package/template/packages/frontend/scripts/serve.ts +70 -0
- package/template/packages/frontend/src/app/[locale]/layout.tsx +9 -8
- package/template/packages/frontend/src/app/[locale]/page.tsx +2 -2
- package/template/packages/frontend/src/app/[locale]/starter-page.tsx +2 -2
- package/template/packages/frontend/src/app/[locale]/ui/[story]/page.tsx +1 -1
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/landing-showcase.tsx +1 -1
- package/template/packages/frontend/src/app/robots.ts +4 -2
- package/template/packages/frontend/src/app/sitemap.ts +7 -19
- package/template/packages/frontend/src/env.ts +27 -8
- package/template/packages/frontend/src/lib/seo/cache-by-origin.test.ts +68 -0
- package/template/packages/frontend/src/lib/seo/cache-by-origin.ts +40 -0
- package/template/packages/frontend/src/lib/seo/metadata.ts +68 -11
- package/template/packages/frontend/src/lib/seo/pages.ts +2 -2
- package/template/packages/frontend/src/lib/seo/request-origin.ts +89 -0
- package/template/packages/frontend/src/theme/config.ts +1 -1
- package/template/packages/frontend/tsconfig.json +10 -3
- package/template/playwright.config.ts +1 -1
- package/template/project.json +169 -0
- package/template/scripts/build-inputs.test.ts +69 -0
- package/template/scripts/build-inputs.ts +57 -0
- package/template/scripts/check-authored.ts +18 -2
- package/template/scripts/declaration.test.ts +206 -0
- package/template/scripts/declaration.ts +268 -0
- package/template/scripts/dev.ts +41 -20
- package/template/scripts/local-env.test.ts +2 -2
- package/template/scripts/local-env.ts +3 -3
- package/template/scripts/release-steps.test.ts +87 -0
- package/template/scripts/release-steps.ts +108 -0
- package/template/scripts/release.ts +30 -0
- package/template/scripts/runtime-smoke.ts +7 -4
- package/template/scripts/serve-mode.test.ts +36 -0
- package/template/scripts/supervision-signal.test.ts +94 -0
- package/template/scripts/tooling-env.ts +5 -2
- package/template/scripts/web-surface-smoke.ts +70 -0
- package/template/app.config.json +0 -9
- package/template/packages/config/src/identity.ts +0 -18
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createRepositoryApi } from './client';
|
|
2
|
+
import { internalApiUrl } from './place';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Server-side API client. Separate module on purpose: it reads the SERVER
|
|
6
|
+
* environment, and importing that from a module the browser bundle also pulls
|
|
7
|
+
* in would drag server configuration into the client graph.
|
|
8
|
+
*/
|
|
9
|
+
export function createServerRepositoryApi() {
|
|
10
|
+
return createRepositoryApi(internalApiUrl());
|
|
11
|
+
}
|
|
@@ -1,20 +1,52 @@
|
|
|
1
1
|
import { repositoryRealtimeContract } from '@app/shared';
|
|
2
2
|
import { createRealtimeClient } from 'stitchkit';
|
|
3
3
|
import { createCacheBridge } from 'stitchkit/react';
|
|
4
|
-
import {
|
|
4
|
+
import { optionalRealtimeOrigin } from '@/lib/api/cross-origin';
|
|
5
5
|
import { useRepository } from '@/lib/api/queries';
|
|
6
6
|
import { getQueryClient } from '@/lib/query-client';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The one thing a route handler cannot proxy.
|
|
10
|
+
*
|
|
11
|
+
* A WebSocket upgrade does not survive a Next route handler, which is why this
|
|
12
|
+
* has a variable of its own — `PUBLIC_REALTIME_ORIGIN` — rather than sharing
|
|
13
|
+
* the HTTP one. A deployment can be same-origin for HTTP and still have to name
|
|
14
|
+
* the socket's address: two roles on two ports with no routing layer in front
|
|
15
|
+
* of them is exactly that. Unset picks the page's own origin, where a routing
|
|
16
|
+
* layer forwards `/socket.io`.
|
|
17
|
+
*/
|
|
18
|
+
function buildSocket() {
|
|
19
|
+
// `window.location.origin` is not a value baked in anywhere: it is what this
|
|
20
|
+
// browser actually dialled, read at connect time, exactly like the server
|
|
21
|
+
// reads the request's origin. This function only ever runs in an effect.
|
|
22
|
+
const url = optionalRealtimeOrigin() ?? window.location.origin;
|
|
23
|
+
return createRealtimeClient(repositoryRealtimeContract, { url });
|
|
24
|
+
}
|
|
11
25
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
function buildBridge() {
|
|
27
|
+
return createCacheBridge({
|
|
28
|
+
socket: repositorySocket(),
|
|
29
|
+
queryClient: getQueryClient,
|
|
30
|
+
handlers: {
|
|
31
|
+
'repository:refreshed': (snapshot, { queryClient }) => {
|
|
32
|
+
queryClient.setQueryData(useRepository.getKey(), snapshot);
|
|
33
|
+
},
|
|
18
34
|
},
|
|
19
|
-
}
|
|
20
|
-
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Built on FIRST USE: the socket needs whatever origin the server supplied,
|
|
39
|
+
// and that arrives at runtime rather than from the build. See
|
|
40
|
+
// `lib/api/cross-origin.ts`.
|
|
41
|
+
let socket: ReturnType<typeof buildSocket> | undefined;
|
|
42
|
+
let bridge: ReturnType<typeof buildBridge> | undefined;
|
|
43
|
+
|
|
44
|
+
export function repositorySocket(): ReturnType<typeof buildSocket> {
|
|
45
|
+
socket ??= buildSocket();
|
|
46
|
+
return socket;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function repositoryBridge(): ReturnType<typeof buildBridge> {
|
|
50
|
+
bridge ??= buildBridge();
|
|
51
|
+
return bridge;
|
|
52
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
import { Toaster } from '@/components/ui/toaster';
|
|
6
|
+
import { type PublicOrigins, setPublicOrigins } from '@/lib/api/cross-origin';
|
|
7
|
+
import { ReactQueryProvider } from './react-query';
|
|
8
|
+
import { RealtimeProvider } from './realtime';
|
|
9
|
+
|
|
10
|
+
export function ClientProviders({
|
|
11
|
+
origins,
|
|
12
|
+
children,
|
|
13
|
+
}: {
|
|
14
|
+
origins: PublicOrigins;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}) {
|
|
17
|
+
// Recorded before any child renders, so the realtime socket can be built
|
|
18
|
+
// lazily on first use. Idempotent: the same values every render. `undefined`
|
|
19
|
+
// means this deployment serves both roles on one origin.
|
|
20
|
+
setPublicOrigins(origins);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<ReactQueryProvider>
|
|
24
|
+
<RealtimeProvider>
|
|
25
|
+
<TooltipProvider delayDuration={250}>{children}</TooltipProvider>
|
|
26
|
+
</RealtimeProvider>
|
|
27
|
+
<Toaster />
|
|
28
|
+
</ReactQueryProvider>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
|
4
1
|
import type { ReactNode } from 'react';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { RealtimeProvider } from './realtime';
|
|
2
|
+
import { publicApiOrigin, publicRealtimeOrigin } from '@/lib/api/place';
|
|
3
|
+
import { ClientProviders } from './client-providers';
|
|
8
4
|
|
|
5
|
+
/**
|
|
6
|
+
* A SERVER component on purpose.
|
|
7
|
+
*
|
|
8
|
+
* The browser's data calls are same-origin and need no address at all. What
|
|
9
|
+
* still can is the realtime socket, which no route handler can proxy: when this
|
|
10
|
+
* deployment runs the two roles on two origins, the socket's address is read
|
|
11
|
+
* HERE, per request, and handed down. Reading it at request time rather than at
|
|
12
|
+
* build time is what lets one artifact serve every address it is routed to; a
|
|
13
|
+
* `NEXT_PUBLIC_` variable would have frozen one into the bundle.
|
|
14
|
+
*
|
|
15
|
+
* `undefined` for both is the normal answer for a single-origin deployment.
|
|
16
|
+
*/
|
|
9
17
|
export function Providers({ children }: { children: ReactNode }) {
|
|
10
18
|
return (
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
</RealtimeProvider>
|
|
15
|
-
<Toaster />
|
|
16
|
-
</ReactQueryProvider>
|
|
19
|
+
<ClientProviders origins={{ api: publicApiOrigin(), realtime: publicRealtimeOrigin() }}>
|
|
20
|
+
{children}
|
|
21
|
+
</ClientProviders>
|
|
17
22
|
);
|
|
18
23
|
}
|
|
@@ -5,11 +5,13 @@ import { repositoryBridge, repositorySocket } from '@/lib/realtime/repository';
|
|
|
5
5
|
|
|
6
6
|
export function RealtimeProvider({ children }: { children: ReactNode }) {
|
|
7
7
|
useEffect(() => {
|
|
8
|
-
repositorySocket
|
|
9
|
-
repositoryBridge
|
|
8
|
+
const socket = repositorySocket();
|
|
9
|
+
const bridge = repositoryBridge();
|
|
10
|
+
socket.connect();
|
|
11
|
+
bridge.connect();
|
|
10
12
|
return () => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
bridge.disconnect();
|
|
14
|
+
socket.disconnect();
|
|
13
15
|
};
|
|
14
16
|
}, []);
|
|
15
17
|
return children;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"kind": "application",
|
|
4
|
+
"identity": {
|
|
5
|
+
"slug": "stitchkit-starter",
|
|
6
|
+
"name": "Stitchkit Starter",
|
|
7
|
+
"version": "0.1.0",
|
|
8
|
+
"description": {
|
|
9
|
+
"en": "Stitchkit Starter is a production application built with Stitchkit.",
|
|
10
|
+
"ru": "Stitchkit Starter — production-приложение на Stitchkit."
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"roles": [
|
|
14
|
+
{
|
|
15
|
+
"name": "api",
|
|
16
|
+
"workingDirectory": "packages/backend",
|
|
17
|
+
"commands": {
|
|
18
|
+
"development": {
|
|
19
|
+
"executable": "bun",
|
|
20
|
+
"args": [
|
|
21
|
+
"--watch",
|
|
22
|
+
"src/index.ts"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"production": {
|
|
26
|
+
"executable": "bun",
|
|
27
|
+
"args": [
|
|
28
|
+
"dist/index.js"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"listener": {
|
|
33
|
+
"portVariable": "API_PORT",
|
|
34
|
+
"bindVariable": "BIND_HOST",
|
|
35
|
+
"readinessPath": "/health"
|
|
36
|
+
},
|
|
37
|
+
"drainFloorMs": 15000
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "web",
|
|
41
|
+
"workingDirectory": "packages/frontend",
|
|
42
|
+
"commands": {
|
|
43
|
+
"development": {
|
|
44
|
+
"executable": "bun",
|
|
45
|
+
"args": [
|
|
46
|
+
"scripts/serve.ts",
|
|
47
|
+
"development"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"production": {
|
|
51
|
+
"executable": "bun",
|
|
52
|
+
"args": [
|
|
53
|
+
"scripts/serve.ts",
|
|
54
|
+
"production"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"listener": {
|
|
59
|
+
"portVariable": "WEB_PORT",
|
|
60
|
+
"bindVariable": "BIND_HOST",
|
|
61
|
+
"readinessPath": "/"
|
|
62
|
+
},
|
|
63
|
+
"drainFloorMs": 5000
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"build": {
|
|
67
|
+
"command": {
|
|
68
|
+
"executable": "bun",
|
|
69
|
+
"args": [
|
|
70
|
+
"run",
|
|
71
|
+
"build"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"artifacts": [
|
|
75
|
+
"packages/backend/dist",
|
|
76
|
+
"packages/frontend/.next",
|
|
77
|
+
"packages/db/src/generated"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
"requires": [
|
|
81
|
+
{
|
|
82
|
+
"name": "postgres",
|
|
83
|
+
"phases": [
|
|
84
|
+
"release",
|
|
85
|
+
"start"
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"release": {
|
|
90
|
+
"migrations": {
|
|
91
|
+
"engine": "prisma",
|
|
92
|
+
"root": "packages/db/migrations",
|
|
93
|
+
"lockfile": "packages/db/migrations/migration_lock.toml"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"env": {
|
|
97
|
+
"variables": [
|
|
98
|
+
{
|
|
99
|
+
"name": "API_PORT",
|
|
100
|
+
"shape": "integer",
|
|
101
|
+
"required": true
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "BIND_HOST",
|
|
105
|
+
"shape": "string",
|
|
106
|
+
"required": false
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "CORS_ORIGIN",
|
|
110
|
+
"shape": "url",
|
|
111
|
+
"required": false
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"name": "DATABASE_URL",
|
|
115
|
+
"shape": "url",
|
|
116
|
+
"required": true
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "GITHUB_API_URL",
|
|
120
|
+
"shape": "url",
|
|
121
|
+
"required": false
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"name": "GITHUB_CACHE_TTL_SECONDS",
|
|
125
|
+
"shape": "integer",
|
|
126
|
+
"required": false
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"name": "GITHUB_REPOSITORY",
|
|
130
|
+
"shape": "string",
|
|
131
|
+
"required": true
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"name": "GITHUB_TOKEN",
|
|
135
|
+
"shape": "string",
|
|
136
|
+
"required": false
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"name": "INTERNAL_API_URL",
|
|
140
|
+
"shape": "url",
|
|
141
|
+
"required": true
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"name": "LOG_FORMAT",
|
|
145
|
+
"shape": "enum",
|
|
146
|
+
"required": false,
|
|
147
|
+
"members": [
|
|
148
|
+
"pretty",
|
|
149
|
+
"json"
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"name": "NODE_ENV",
|
|
154
|
+
"shape": "enum",
|
|
155
|
+
"required": false,
|
|
156
|
+
"members": [
|
|
157
|
+
"development",
|
|
158
|
+
"test",
|
|
159
|
+
"production"
|
|
160
|
+
]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"name": "PUBLIC_API_ORIGIN",
|
|
164
|
+
"shape": "url",
|
|
165
|
+
"required": false
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"name": "PUBLIC_REALTIME_ORIGIN",
|
|
169
|
+
"shape": "url",
|
|
170
|
+
"required": false
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"name": "PUBLIC_WEB_HOSTS",
|
|
174
|
+
"shape": "string",
|
|
175
|
+
"required": false
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"name": "PUBLIC_WEB_ORIGIN",
|
|
179
|
+
"shape": "url",
|
|
180
|
+
"required": false
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "WEB_PORT",
|
|
184
|
+
"shape": "integer",
|
|
185
|
+
"required": true
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -3,10 +3,10 @@ import { createRealtimeClient, defineRealtimeContract } from 'stitchkit';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { defineSurfaceProbe, runSurfaceConformance } from './surface-conformance';
|
|
5
5
|
import { loadToolingEnv } from './tooling-env';
|
|
6
|
-
import { assertPublicWebSurface } from './web-surface-smoke';
|
|
6
|
+
import { assertArtifactIsPlacementFree, assertPublicWebSurface } from './web-surface-smoke';
|
|
7
7
|
|
|
8
8
|
const toolingEnv = loadToolingEnv();
|
|
9
|
-
const apiOrigin = toolingEnv.
|
|
9
|
+
const apiOrigin = toolingEnv.SMOKE_API_ORIGIN;
|
|
10
10
|
|
|
11
11
|
async function json(path: string, init?: RequestInit): Promise<unknown> {
|
|
12
12
|
const response = await fetch(`${apiOrigin}${path}`, init);
|
|
@@ -155,7 +155,7 @@ await runSurfaceConformance({
|
|
|
155
155
|
});
|
|
156
156
|
if (
|
|
157
157
|
corsResponse.headers.get('access-control-allow-origin') !==
|
|
158
|
-
new URL(toolingEnv.
|
|
158
|
+
new URL(toolingEnv.SMOKE_WEB_ORIGIN).origin
|
|
159
159
|
) {
|
|
160
160
|
throw new Error('API CORS origin differs from the configured web origin');
|
|
161
161
|
}
|
|
@@ -167,6 +167,26 @@ await runSurfaceConformance({
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
{
|
|
171
|
+
// THE DEFAULT PATH, end to end: the browser's own origin answers `/api/…`
|
|
172
|
+
// because the web role forwards it. This is what makes the example's client a
|
|
173
|
+
// module constant, so it is checked rather than described.
|
|
174
|
+
const proxied = await fetch(`${toolingEnv.SMOKE_WEB_ORIGIN}/api/repository`);
|
|
175
|
+
if (!proxied.ok) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`The web role did not forward /api/repository (${proxied.status}) — the same-origin default is broken`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const sameOrigin = RepositorySnapshotSchema.parse(await proxied.json());
|
|
181
|
+
const direct = RepositorySnapshotSchema.parse(await json('/api/repository/'));
|
|
182
|
+
if (sameOrigin.fullName !== direct.fullName) {
|
|
183
|
+
throw new Error("the forwarded answer differs from the API role's own");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await assertPublicWebSurface(toolingEnv.SMOKE_WEB_ORIGIN);
|
|
188
|
+
await assertArtifactIsPlacementFree(toolingEnv.SMOKE_WEB_ORIGIN);
|
|
171
189
|
|
|
172
|
-
console.log(
|
|
190
|
+
console.log(
|
|
191
|
+
'Runtime HTTP (same-origin and direct), OpenAPI, Socket.IO, MCP and public web smoke passed',
|
|
192
|
+
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-stitchkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Create a production-shaped Stitchkit application",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Max Listov <maxlistov@gmail.com>",
|
|
@@ -27,13 +27,21 @@
|
|
|
27
27
|
"!template/**/src/generated/**",
|
|
28
28
|
"!template/**/*.log",
|
|
29
29
|
"!template/**/*.tsbuildinfo",
|
|
30
|
+
"!template/**/coverage/**",
|
|
31
|
+
"!examples/**/.env",
|
|
30
32
|
"!examples/**/node_modules/**",
|
|
31
33
|
"!examples/**/.next/**",
|
|
32
34
|
"!examples/**/dist/**",
|
|
35
|
+
"!examples/**/coverage/**",
|
|
36
|
+
"!examples/**/playwright-report/**",
|
|
37
|
+
"!examples/**/test-results/**",
|
|
38
|
+
"!examples/**/next-env.d.ts",
|
|
39
|
+
"!examples/**/src/generated/**",
|
|
33
40
|
"!examples/**/*.log",
|
|
34
41
|
"!examples/**/*.tsbuildinfo",
|
|
35
42
|
"README.md",
|
|
36
43
|
"CHANGELOG.md",
|
|
44
|
+
"UPGRADING.md",
|
|
37
45
|
"LICENSE"
|
|
38
46
|
],
|
|
39
47
|
"publishConfig": {
|
package/template/AGENTS.md
CHANGED
|
@@ -12,8 +12,18 @@ framework source repository.
|
|
|
12
12
|
owns application policy. Routes remain thin transport boundaries.
|
|
13
13
|
- `packages/frontend` owns Next.js pages, typed clients, query/mutation hooks and
|
|
14
14
|
cache reactions.
|
|
15
|
-
- `packages/config/src/
|
|
16
|
-
`
|
|
15
|
+
- `packages/config/src/variables.ts` is the only declaration of an environment
|
|
16
|
+
variable. `server.ts` and `frontend/src/env.ts` are projections of it, and
|
|
17
|
+
the declaration's `env.variables` is derived from it — never restated.
|
|
18
|
+
- `project.json` is the only place this project describes itself. It is written
|
|
19
|
+
by machine — the scaffolder stamps the identity, `bun run gen:declaration`
|
|
20
|
+
derives `env.variables` — so the formatter leaves it alone and
|
|
21
|
+
`scripts/declaration.test.ts` is what checks it.
|
|
22
|
+
- Three files are generated from it and must not be hand-edited:
|
|
23
|
+
`ecosystem.config.cjs`, `ecosystem.dev.config.cjs` and the `env.variables`
|
|
24
|
+
block of `project.json`. Run `bun run gen:declaration` after changing a role. It holds
|
|
25
|
+
nothing that differs between two deployments; those are named there by
|
|
26
|
+
variable and supplied by the place.
|
|
17
27
|
|
|
18
28
|
Dependencies point inward: frontend/backend → shared; backend → db/config.
|
|
19
29
|
Shared never imports an application runtime package.
|
package/template/README.md
CHANGED
|
@@ -2,9 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
Production-shaped application generated by `create-stitchkit`.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
This project describes itself in [`project.json`](project.json) — its
|
|
6
|
+
**declaration**: identity, the roles it runs, what it builds, what it needs
|
|
7
|
+
before it starts, and the environment variables a deployment must supply.
|
|
8
|
+
|
|
9
|
+
The declaration is true **with no machine in existence**. A field you cannot
|
|
10
|
+
fill in without knowing where the code will run is a *binding*, not a
|
|
11
|
+
declaration: ports, hosts, addresses, machine paths and supervision policy are
|
|
12
|
+
named there by variable and never by value, and the schema has nowhere to put
|
|
13
|
+
them. Change the slug, display name, version or description there and package
|
|
14
|
+
names, process names, MCP/OpenAPI identity, UI copy and SEO follow.
|
|
15
|
+
|
|
16
|
+
Three files are **generated** from it — `ecosystem.config.cjs`,
|
|
17
|
+
`ecosystem.dev.config.cjs` and the `env.variables` block of the declaration
|
|
18
|
+
itself. Run `bun run gen:declaration` after changing a role; the test suite
|
|
19
|
+
refuses a stale copy.
|
|
8
20
|
|
|
9
21
|
## Start
|
|
10
22
|
|
|
@@ -35,6 +47,27 @@ Configure it with `GITHUB_REPOSITORY`; `GITHUB_TOKEN` is optional.
|
|
|
35
47
|
Repository visibility then demonstrates the canonical Prisma enum → shared Zod schema
|
|
36
48
|
→ HTTP/UI path without duplicating its allowed values.
|
|
37
49
|
|
|
50
|
+
**How the browser reaches the API, and what each shape costs.** By default it
|
|
51
|
+
does not reach it at all: the browser calls its own origin (`/api/…`), the web
|
|
52
|
+
role forwards to the API role (`packages/frontend/src/app/api/[...path]/route.ts`),
|
|
53
|
+
and the client is a plain module constant — no address to wait for, no lazy
|
|
54
|
+
accessor, nothing to order relative to `<Providers>`. A single-origin deployment
|
|
55
|
+
supplies only `INTERNAL_API_URL`.
|
|
56
|
+
|
|
57
|
+
Two things can pull a deployment out of that, and they are separate questions,
|
|
58
|
+
so they have separate variables. **`PUBLIC_REALTIME_ORIGIN`** is the socket: a
|
|
59
|
+
WebSocket upgrade does not survive the route handler that forwards `/api`, so
|
|
60
|
+
two roles on two ports with nothing in front of them must name the socket's
|
|
61
|
+
origin even though their HTTP is already same-origin. Behind one routing layer
|
|
62
|
+
that forwards `/socket.io`, leave it unset. **`PUBLIC_API_ORIGIN`** is HTTP, for
|
|
63
|
+
a frontend that genuinely dials the API role itself — and setting it changes
|
|
64
|
+
nothing on its own: switching is one import in
|
|
65
|
+
`packages/frontend/src/lib/api/queries.ts`, documented in
|
|
66
|
+
`packages/frontend/src/lib/api/cross-origin.ts`. That variant costs a
|
|
67
|
+
server-delivered address, a client built on first use (hence the parentheses),
|
|
68
|
+
an ordering requirement relative to `<Providers>`, and `CORS_ORIGIN` on the API
|
|
69
|
+
role.
|
|
70
|
+
|
|
38
71
|
The `/en/ui` catalogue is isolated under
|
|
39
72
|
`packages/frontend/src/app/[locale]/ui` and can be removed as one directory;
|
|
40
73
|
reusable primitives remain in `components/ui`. It renders the complete
|
|
@@ -63,13 +96,17 @@ bun run e2e
|
|
|
63
96
|
Provide a production `.env`, then:
|
|
64
97
|
|
|
65
98
|
```bash
|
|
66
|
-
bun run db:deploy
|
|
67
99
|
bun run build
|
|
68
100
|
bun run pm2:prod
|
|
69
101
|
```
|
|
70
102
|
|
|
71
|
-
|
|
72
|
-
|
|
103
|
+
`pm2:prod` is the release: it checks every artifact `build.artifacts` declares,
|
|
104
|
+
applies the migrations `release.migrations` declares, and only then starts the
|
|
105
|
+
roles from the generated supervision file. There is no separate `db:deploy`
|
|
106
|
+
step to remember — the declaration already says the migrations exist, and one
|
|
107
|
+
place saying it is the point.
|
|
108
|
+
|
|
109
|
+
The roles are separate processes and can also be deployed independently.
|
|
73
110
|
|
|
74
111
|
PostgreSQL is external infrastructure in development and production. The
|
|
75
112
|
application owns its schema and migrations; the environment owns the database
|
package/template/_env.example
CHANGED
|
@@ -4,8 +4,12 @@ DATABASE_URL=postgresql://USER:PASSWORD@127.0.0.1:5432/stitchkit_starter
|
|
|
4
4
|
BIND_HOST=127.0.0.1
|
|
5
5
|
API_PORT=3211
|
|
6
6
|
WEB_PORT=3210
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
SMOKE_API_ORIGIN=http://127.0.0.1:3211
|
|
8
|
+
SMOKE_WEB_ORIGIN=http://127.0.0.1:3210
|
|
9
|
+
# Hosts this deployment answers for, comma-separated. One built artifact can
|
|
10
|
+
# serve several addresses; a forwarded host outside this list is refused rather
|
|
11
|
+
# than believed. Leave unset and set PUBLIC_WEB_ORIGIN instead for a single one.
|
|
12
|
+
PUBLIC_WEB_HOSTS=127.0.0.1:3210
|
|
10
13
|
LOG_FORMAT=pretty
|
|
11
|
-
CORS_ORIGIN
|
|
14
|
+
# CORS_ORIGIN is only needed for a genuinely cross-origin browser.
|
|
15
|
+
# CORS_ORIGIN=http://127.0.0.1:3210
|
package/template/biome.json
CHANGED
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
"!!playwright-report",
|
|
10
10
|
"!!test-results",
|
|
11
11
|
"!!packages/frontend/next-env.d.ts",
|
|
12
|
-
"!!packages/frontend/public"
|
|
12
|
+
"!!packages/frontend/public",
|
|
13
|
+
"!!project.json",
|
|
14
|
+
"!!ecosystem.config.cjs",
|
|
15
|
+
"!!ecosystem.dev.config.cjs",
|
|
16
|
+
"!!packages/config/src/app-identity.generated.ts"
|
|
13
17
|
]
|
|
14
18
|
},
|
|
15
19
|
"formatter": {
|
package/template/bun.lock
CHANGED
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
},
|
|
139
139
|
},
|
|
140
140
|
"catalog": {
|
|
141
|
-
"stitchkit": "^0.
|
|
141
|
+
"stitchkit": "^0.60.0",
|
|
142
142
|
},
|
|
143
143
|
"packages": {
|
|
144
144
|
"@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.46", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.25", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LIAO6kAG8fpXQb9L0iwPk1FIbXftvqnyC56v5NEAzeWTeL8fUsy/Hx86VPBTWEDFdwbVprjWifJOAqS6AOj3mA=="],
|
|
@@ -1117,7 +1117,7 @@
|
|
|
1117
1117
|
|
|
1118
1118
|
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
|
1119
1119
|
|
|
1120
|
-
"stitchkit": ["stitchkit@0.
|
|
1120
|
+
"stitchkit": ["stitchkit@0.60.0", "", { "dependencies": { "ky": "^2.0.2" }, "peerDependencies": { "@modelcontextprotocol/ext-apps": "^1.7.2", "@modelcontextprotocol/server": "^2.0.0", "@openrouter/ai-sdk-provider": "^3.0.0", "@opentelemetry/api": "^1.9.0", "@socket.io/bun-engine": "^0.1.1", "@socket.io/component-emitter": "^3.1.2", "@tanstack/react-query": ">=5", "@types/bun": "^1.3.14", "ai": "^7.0.0", "grammy": "^1.45.1", "react": ">=18", "react-query-kit": "^3.3.3", "socket.io": "^4.8.3", "socket.io-client": "^4.8.3", "srvx": "^0.12.5", "zod": "^4.4.3" }, "optionalPeers": ["@modelcontextprotocol/ext-apps", "@modelcontextprotocol/server", "@openrouter/ai-sdk-provider", "@opentelemetry/api", "@socket.io/bun-engine", "@socket.io/component-emitter", "@tanstack/react-query", "@types/bun", "ai", "grammy", "react", "react-query-kit", "socket.io", "socket.io-client", "srvx"] }, "sha512-bP4HUHYG4/0WfF1oFGsRddvu+jHpCNyvL/lPNhWHofb6QrcL0KY1nna72lqg9g59xZKhNMLd8mzRCJkLKvghFg=="],
|
|
1121
1121
|
|
|
1122
1122
|
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
|
1123
1123
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { appDeclaration } from '@app/config/declaration';
|
|
2
2
|
import { systemContract } from '@app/shared';
|
|
3
3
|
import AxeBuilder from '@axe-core/playwright';
|
|
4
4
|
import { expect, test } from '@playwright/test';
|
|
@@ -21,7 +21,7 @@ test('calls the live backend through the typed contract client', async () => {
|
|
|
21
21
|
const client = createClient(
|
|
22
22
|
systemContract,
|
|
23
23
|
createHttpClient({
|
|
24
|
-
baseUrl: `${toolingEnv.
|
|
24
|
+
baseUrl: `${toolingEnv.SMOKE_API_ORIGIN}/api`,
|
|
25
25
|
credentials: 'omit',
|
|
26
26
|
}),
|
|
27
27
|
);
|
|
@@ -31,7 +31,7 @@ test('calls the live backend through the typed contract client', async () => {
|
|
|
31
31
|
|
|
32
32
|
test('publishes complete page metadata', async ({ page }) => {
|
|
33
33
|
await page.goto('/en/ui/themes');
|
|
34
|
-
await expect(page).toHaveTitle(`Theme system · ${
|
|
34
|
+
await expect(page).toHaveTitle(`Theme system · ${appDeclaration.identity.name}`);
|
|
35
35
|
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute(
|
|
36
36
|
'href',
|
|
37
37
|
/\/en\/ui\/themes$/,
|
|
@@ -42,9 +42,7 @@ test('publishes complete page metadata', async ({ page }) => {
|
|
|
42
42
|
);
|
|
43
43
|
|
|
44
44
|
const imageUrl = await page.locator('meta[property="og:image"]').getAttribute('content');
|
|
45
|
-
expect(imageUrl).toBe(
|
|
46
|
-
new URL('/api/og/en/themes', toolingEnv.NEXT_PUBLIC_WEB_URL).toString(),
|
|
47
|
-
);
|
|
45
|
+
expect(imageUrl).toBe(new URL('/api/og/en/themes', toolingEnv.SMOKE_WEB_ORIGIN).toString());
|
|
48
46
|
});
|
|
49
47
|
|
|
50
48
|
test('switches catalogue sections and component tabs', async ({ page }) => {
|
|
@@ -217,7 +215,7 @@ test('provides a server-first synchronized theme system', async ({
|
|
|
217
215
|
await page.getByRole('button', { name: 'System', exact: true }).click();
|
|
218
216
|
await expect(page.getByTestId('theme-state-selected')).toContainText('system');
|
|
219
217
|
const themeCookie = (await context.cookies()).find(
|
|
220
|
-
(cookie) => cookie.name === `${
|
|
218
|
+
(cookie) => cookie.name === `${appDeclaration.identity.slug}-theme`,
|
|
221
219
|
);
|
|
222
220
|
expect(themeCookie?.value).toBe('system');
|
|
223
221
|
await page.reload();
|