create-stitchkit 0.3.2 → 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 +243 -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 +15 -2
- package/template/README.md +51 -6
- package/template/_env.example +10 -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 -13
- package/template/ecosystem.dev.config.cjs +41 -15
- package/template/package.json +5 -4
- package/template/packages/backend/package.json +1 -1
- package/template/packages/backend/scripts/ensure-built.ts +7 -0
- package/template/packages/backend/src/cli.ts +6 -2
- package/template/packages/backend/src/index.ts +23 -7
- package/template/packages/backend/src/surface.ts +6 -1
- package/template/packages/backend/src/transport/errors.ts +4 -2
- package/template/packages/backend/tsconfig.json +1 -1
- 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 -14
- 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 +84 -15
- 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
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,251 @@ All notable changes to **create-stitchkit** are documented here. The scaffolder
|
|
|
4
4
|
has its own version and release line; the Stitchkit range tested by its template
|
|
5
5
|
is declared in the template root catalog.
|
|
6
6
|
|
|
7
|
+
A release that changes the generated project in a way an existing project must
|
|
8
|
+
follow leads its entry with a **`### ⚠️ Breaking changes`** section. What else
|
|
9
|
+
has to happen — including the steps that touch a running machine — is in
|
|
10
|
+
[`UPGRADING.md`](./UPGRADING.md), because a changelog entry carrying an operator
|
|
11
|
+
step is overwritten by the next release.
|
|
12
|
+
|
|
7
13
|
## [Unreleased]
|
|
8
14
|
|
|
15
|
+
## [0.4.0] — 2026-08-25
|
|
16
|
+
|
|
17
|
+
### ⚠️ Breaking changes
|
|
18
|
+
|
|
19
|
+
- **The repository example's browser talks to its OWN origin by default.** The
|
|
20
|
+
example is what gets copied, and it was demonstrating the hard case: the
|
|
21
|
+
browser dialled the API role directly, so the address had to arrive from the
|
|
22
|
+
server at runtime, the API client could not exist until it did, and every call
|
|
23
|
+
site paid for that with a lazy accessor — `repositoryApi().read()` — plus a
|
|
24
|
+
runtime error when something rendered outside `<Providers>`. Somebody copying
|
|
25
|
+
it inherited that whether or not they were cross-origin at all. The body of
|
|
26
|
+
the example is now the default shape: a same-origin `/api/…` path forwarded by
|
|
27
|
+
the web role, and a client that is a module constant.
|
|
28
|
+
`// before: repositoryApi().read()` → `// after: repositoryApi.read()`
|
|
29
|
+
The cross-origin form is not lost — it moved to a named file,
|
|
30
|
+
`packages/frontend/src/lib/api/cross-origin.ts`, with what it costs written
|
|
31
|
+
next to it, and switching to it is one import in `queries.ts`.
|
|
32
|
+
- **`PUBLIC_REALTIME_ORIGIN` — the socket's address has a name of its own.**
|
|
33
|
+
`PUBLIC_API_ORIGIN` used to carry both questions and answer only one: it read
|
|
34
|
+
as a mode switch while in fact nothing but the realtime socket looked at it.
|
|
35
|
+
The two are genuinely different — HTTP can be forwarded by the web role, and a
|
|
36
|
+
WebSocket upgrade cannot survive a route handler — so a deployment can be
|
|
37
|
+
same-origin for HTTP and still have to name the socket's origin. Both are
|
|
38
|
+
optional; a deployment behind one routing layer sets neither.
|
|
39
|
+
`// before: PUBLIC_API_ORIGIN=https://api.example # …which only the socket read` →
|
|
40
|
+
`// after: PUBLIC_REALTIME_ORIGIN=https://api.example`
|
|
41
|
+
- **`app.config.json` is now `project.json` — the project *declaration*.** It no
|
|
42
|
+
longer describes only identity: it states what this repository is, the roles it
|
|
43
|
+
runs, what it builds, what it needs before it starts, the release steps that
|
|
44
|
+
must happen once, and the environment variables a deployment must supply.
|
|
45
|
+
Identity moved under an `identity` key and the file gained a `schemaVersion`,
|
|
46
|
+
so a reader that does not understand the format refuses the project instead of
|
|
47
|
+
interpreting it partially.
|
|
48
|
+
`// before: import { appIdentity } from '@app/config/identity'; appIdentity.name` →
|
|
49
|
+
`// after: import { appDeclaration } from '@app/config/declaration'; appDeclaration.identity.name`
|
|
50
|
+
|
|
51
|
+
A client component imports `@app/config/app-identity` instead — a generated
|
|
52
|
+
module carrying identity alone. Importing the whole declaration from the
|
|
53
|
+
browser would ship role commands, working directories, build artifact paths and
|
|
54
|
+
the migration lockfile in the bundle.
|
|
55
|
+
`// before: import { appIdentity } from '@app/config/identity' // in a 'use client' file` →
|
|
56
|
+
`// after: import { appIdentity } from '@app/config/app-identity'`
|
|
57
|
+
|
|
58
|
+
The rule the file exists to hold: **it must be complete with no machine in
|
|
59
|
+
existence.** Ports, hosts, addresses, machine paths, routing shape and
|
|
60
|
+
supervision policy are named there by variable and never by value — and the
|
|
61
|
+
schema refuses them by shape rather than by review.
|
|
62
|
+
|
|
63
|
+
- **The SEO helpers are async, and `siteOrigin` is gone.** They read the public
|
|
64
|
+
origin from the request instead of a build-time constant, so they cannot be
|
|
65
|
+
constants themselves. A page that calls them must await them — and TypeScript
|
|
66
|
+
will *not* catch it inside an inferred object literal, where a `Promise`
|
|
67
|
+
silently serialises as `{}`.
|
|
68
|
+
`// before: const url = absoluteSiteUrl('/en'); export const siteOrigin` →
|
|
69
|
+
`// after: const url = await absoluteSiteUrl('/en') // and the component becomes async`
|
|
70
|
+
`// before: createPageMetadata('home', locale)` →
|
|
71
|
+
`// after: await createPageMetadata('home', locale)`
|
|
72
|
+
|
|
73
|
+
- **A forwarded host must be claimed before it is believed.** The public origin
|
|
74
|
+
comes from the request, which makes one artifact serve many addresses — and
|
|
75
|
+
would let any caller choose the canonical URL, the sitemap and the OG metadata
|
|
76
|
+
if it were trusted blindly. Set `PUBLIC_WEB_ORIGIN` for a single address, or
|
|
77
|
+
`PUBLIC_WEB_HOSTS` for several; a host outside them is refused. `x-forwarded-proto`
|
|
78
|
+
is narrowed to `http` or `https`.
|
|
79
|
+
`// before: (nothing — any x-forwarded-host was honoured)` →
|
|
80
|
+
`// after: PUBLIC_WEB_HOSTS=app.example,www.app.example`
|
|
81
|
+
|
|
82
|
+
- **Environment variables are declared once, and the declaration lists them as
|
|
83
|
+
`env.variables`.** The server schema, the frontend schema and the tooling
|
|
84
|
+
schema were three overlapping copies that had already diverged.
|
|
85
|
+
`packages/config/src/variables.ts` is now the single declaration; `server.ts`
|
|
86
|
+
and `frontend/src/env.ts` are projections of it, and the declaration's list is
|
|
87
|
+
*derived* from it rather than restated. An overlay may now **tighten** a
|
|
88
|
+
variable, not only add one — the repository example requires `INTERNAL_API_URL`,
|
|
89
|
+
`PUBLIC_API_ORIGIN` and `CORS_ORIGIN` because its frontend dereferences them on
|
|
90
|
+
every render.
|
|
91
|
+
`// before: z.url() repeated in three files; env.required with required:false entries` →
|
|
92
|
+
`// after: applicationVariables.INTERNAL_API_URL, referenced; env.variables`
|
|
93
|
+
|
|
94
|
+
- **`NEXT_PUBLIC_API_URL` and `NEXT_PUBLIC_WEB_URL` are gone.** Anything prefixed
|
|
95
|
+
`NEXT_PUBLIC_` is substituted at BUILD time, so declaring one froze a value of
|
|
96
|
+
the place into the artifact: the built `robots.txt` and `sitemap.xml` carried
|
|
97
|
+
one origin inside their bytes, and the server chunk carried
|
|
98
|
+
`NEXT_PUBLIC_API_URL:"http://…"` as a literal. One build could not serve a
|
|
99
|
+
second address.
|
|
100
|
+
`// before: NEXT_PUBLIC_WEB_URL=https://app.example → baked at build` →
|
|
101
|
+
`// after: no variable; the origin is read from the request`
|
|
102
|
+
**Cost, stated plainly:** `/robots.txt`, `/sitemap.xml` and — because the root
|
|
103
|
+
layout's `generateMetadata` reads the request — the whole `[locale]` segment
|
|
104
|
+
are no longer prerendered as static content. Setting `PUBLIC_WEB_ORIGIN`
|
|
105
|
+
short-circuits the request read and restores static rendering for a deployment
|
|
106
|
+
that serves exactly one address. Answers are built once per address, not once
|
|
107
|
+
per request: `cacheByOrigin` memoises them behind a bounded LRU so a forged
|
|
108
|
+
`Host` cannot grow the cache.
|
|
109
|
+
|
|
110
|
+
- **`CORS_ORIGIN` is optional.** A frontend that reaches the API through its own
|
|
111
|
+
routing layer makes same-origin requests, and requiring an origin there was
|
|
112
|
+
requiring knowledge of the place. Set it only for a genuinely cross-origin
|
|
113
|
+
browser.
|
|
114
|
+
`// before: CORS_ORIGIN=https://app.example # required` →
|
|
115
|
+
`// after: unset unless the browser genuinely lives elsewhere`
|
|
116
|
+
|
|
117
|
+
- **The smoke and e2e addresses are `SMOKE_API_ORIGIN` and `SMOKE_WEB_ORIGIN`.**
|
|
118
|
+
They are legitimately bound to a place — they name the deployment a check dials —
|
|
119
|
+
but must not carry a prefix that makes the build substitute them.
|
|
120
|
+
`// before: NEXT_PUBLIC_API_URL=http://127.0.0.1:3211` →
|
|
121
|
+
`// after: SMOKE_API_ORIGIN=http://127.0.0.1:3211`
|
|
122
|
+
|
|
123
|
+
- **Each role is started by its own PROCESS, in its own directory, and PM2
|
|
124
|
+
process names follow the declared role names.** A role's command is `executable`
|
|
125
|
+
plus `args` in the declaration — argv, never a shell string — and the
|
|
126
|
+
supervision files are rendered from it.
|
|
127
|
+
`// before: <slug>-backend, <slug>-frontend` → `// after: <slug>-api, <slug>-web`
|
|
128
|
+
**Before your first `pm2:prod` on the new files**, remove the old processes, or
|
|
129
|
+
`startOrReload` will start the new pair beside them and both will fight for the
|
|
130
|
+
same ports under `autorestart`:
|
|
131
|
+
```bash
|
|
132
|
+
pm2 delete <slug>-backend <slug>-frontend <slug>-backend-dev <slug>-frontend-dev
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- **The web role reads its bindings itself.** The supervisor no longer builds an
|
|
136
|
+
argv for it; injecting `WEB_PORT` is now sufficient, where before a deployment
|
|
137
|
+
that set it and stopped there got a web role on the wrong port, silently. There
|
|
138
|
+
is no default port in the repository any more — a missing variable fails by
|
|
139
|
+
name.
|
|
140
|
+
`// before: "start": "next start --port ${WEB_PORT:-3210}"` →
|
|
141
|
+
`// after: "start": "bun scripts/serve.ts production"`
|
|
142
|
+
|
|
143
|
+
### Fixed
|
|
144
|
+
|
|
145
|
+
- **A supervised backend now actually drains.** The supervision files started the
|
|
146
|
+
role through a script runner, so the stop signal arrived twice — once from PM2,
|
|
147
|
+
once forwarded by the launcher — and a shutdown chain treats the second signal
|
|
148
|
+
as "force it now". Measured on a real PM2 stop: a declared 15 s grace period
|
|
149
|
+
ended after **1.3 ms** with `outcome: "forced"`, `reason: "signal"`, and the
|
|
150
|
+
only visible trace was a non-zero exit code. The supervisor now execs the role
|
|
151
|
+
itself, and the same stop reports `outcome: "clean"` with exit 0.
|
|
152
|
+
`// before: script: 'bun', args: ['run', 'start'] // an intermediate shape, never released` →
|
|
153
|
+
`// after: script: 'bun', args: ['dist/index.js']`
|
|
154
|
+
A generated project at the previous release ran `script: 'dist/index.js'` with
|
|
155
|
+
`interpreter: 'bun'`, which had the same property; the defect it *did* ship is
|
|
156
|
+
the timeout mismatch below.
|
|
157
|
+
- **Supervision no longer kills the backend mid-shutdown.** The application asked
|
|
158
|
+
for a 30 s drain while PM2 sent `SIGKILL` after `kill_timeout: 15000` in
|
|
159
|
+
production and 10000 in development — so a drain longer than the supervisor's
|
|
160
|
+
patience never finished, every time. The check now covers the **whole**
|
|
161
|
+
termination budget rather than the drain alone: drain floor, plus the force
|
|
162
|
+
window that follows it, plus cleanup. Comparing against the floor alone let
|
|
163
|
+
15 s + 5 s meet a 20 s kill timeout exactly, with no margin at all.
|
|
164
|
+
- **The backend says how its shutdown ended.** `onComplete` logs the outcome, the
|
|
165
|
+
reason, the duration and how many requests completed or were aborted. Without
|
|
166
|
+
it an operator saw a process that vanished and an exit code, and could not tell
|
|
167
|
+
a clean drain from one that was cut short — which is exactly how the defect
|
|
168
|
+
above stayed invisible.
|
|
169
|
+
- **A requested stop of the web role reports success.** Next exits `130` on
|
|
170
|
+
`SIGINT`; the role passed that upward, so every ordinary supervised stop looked
|
|
171
|
+
like a failure. A stop the role was asked to perform now exits `0`, while a code
|
|
172
|
+
from any other cause is still passed on unchanged.
|
|
173
|
+
- **A deployment's environment is no longer overruled by a file.** The production
|
|
174
|
+
supervision file loaded `.env` with `override: true`, so a value injected into
|
|
175
|
+
the process lost to a value in the repository. Bindings come from the place; the
|
|
176
|
+
file fills gaps.
|
|
177
|
+
- **Release steps come from the declaration.** `pm2:prod` hand-carried
|
|
178
|
+
`db:deploy` and a build preflight as a shell string beside the declaration that
|
|
179
|
+
already stated them. It now runs `scripts/release.ts`, which checks every
|
|
180
|
+
artifact `build.artifacts` declares and applies migrations for the engine
|
|
181
|
+
`release.migrations` declares — refusing an engine it has no command for rather
|
|
182
|
+
than skipping the step, because silently not migrating is what leaves a machine
|
|
183
|
+
running against the wrong schema. Both declared migration paths are checked,
|
|
184
|
+
the lockfile included. Development runs the same step.
|
|
185
|
+
- **`bun run test` passes on a fresh scaffold.** A test read `.env` at module
|
|
186
|
+
load, before `env:ensure` had created it, so the second gate the README tells a
|
|
187
|
+
new user to run died with `ENOENT` before a single test executed. Both CI paths
|
|
188
|
+
write `.env` first, so nothing saw it.
|
|
189
|
+
|
|
190
|
+
### Changed
|
|
191
|
+
|
|
192
|
+
- **The generated build declares whether it reads data, and CI proves it does
|
|
193
|
+
not.** Data read while building is a third kind of input — neither code nor a
|
|
194
|
+
binding — and a build that reads it undeclared is a function of whichever
|
|
195
|
+
machine had the database. The template answers it the default way: no route
|
|
196
|
+
can reach a data source at all (`check-authored` refuses the import and now
|
|
197
|
+
says why), so the build is a function of the source alone. The packed lane
|
|
198
|
+
builds against a database address that accepts nothing, which is the only
|
|
199
|
+
check that covers every transitive path at once. A project that genuinely
|
|
200
|
+
needs data at build time declares a frozen export in `build.inputs` and
|
|
201
|
+
`bun scripts/build-inputs.ts` refuses it the moment its digest drifts.
|
|
202
|
+
- **One artifact is now provably portable in CI, within a stated policy.**
|
|
203
|
+
`runtime:smoke` asks the running web role for `/sitemap.xml` and `/robots.txt`
|
|
204
|
+
under two different external addresses and requires two different answers —
|
|
205
|
+
and requires a *third*, unclaimed host to be refused. The first half catches a
|
|
206
|
+
build-time address creeping back in; the second catches the portability
|
|
207
|
+
mechanism turning into an open redirect for metadata.
|
|
208
|
+
- **The drain floor has one home.** The backend passed a literal grace period
|
|
209
|
+
while the declaration stated another — the same two-numbers-in-two-files shape
|
|
210
|
+
that let a 30 s floor meet a 15 s kill timeout. It now reads
|
|
211
|
+
`apiRole.drainFloorMs`, the number a supervisor reads too.
|
|
212
|
+
- **Three files are generated from the declaration** — both supervision files and
|
|
213
|
+
the client-safe identity module — plus the `env.variables` block of the
|
|
214
|
+
declaration itself. `bun run gen:declaration` renders them and the test suite
|
|
215
|
+
refuses a stale copy. The example's declaration is generated from the
|
|
216
|
+
template's in the same way.
|
|
217
|
+
- **The generated application targets Stitchkit `^0.59.0`.** The template's
|
|
218
|
+
catalog pointed at `^0.52.0`, so a project scaffolded today started seven
|
|
219
|
+
minors behind the framework — without neutral client-disconnect handling,
|
|
220
|
+
managed files, composed auth, async operation contracts or the CLI presentation
|
|
221
|
+
policy. Both the packed target lane and the packed HEAD lane pass on the new
|
|
222
|
+
target.
|
|
223
|
+
- **The scaffolder prints the addresses the generated project will actually
|
|
224
|
+
use**, read back from its declaration and example environment rather than
|
|
225
|
+
restated as constants.
|
|
226
|
+
|
|
227
|
+
## [0.3.3] — 2026-08-18
|
|
228
|
+
|
|
229
|
+
### Changed
|
|
230
|
+
|
|
231
|
+
- **Generated applications bind loopback by default.** Both processes listen on
|
|
232
|
+
`BIND_HOST` (default `127.0.0.1`) instead of a hardcoded `0.0.0.0` in the
|
|
233
|
+
backend server and both PM2 configs. Exposing the app to the network is now a
|
|
234
|
+
single conscious opt-in (`BIND_HOST=0.0.0.0` in `.env`) rather than the state
|
|
235
|
+
a forgotten edit leaves behind. Reported from a production deployment of a
|
|
236
|
+
generated app.
|
|
237
|
+
- **The template targets Stitchkit `^0.52.0`** — new applications get
|
|
238
|
+
`implement.declare`, keyed registries and hook-derived scope maps out of the
|
|
239
|
+
box. Purely additive relative to 0.50.
|
|
240
|
+
- **`bun run dev` reports honest URLs and fails fast on occupied ports.** The
|
|
241
|
+
final `Web:`/`API:` lines are rendered from the validated environment instead
|
|
242
|
+
of hardcoded ports, and before starting fresh PM2 processes the script probes
|
|
243
|
+
`API_PORT`/`WEB_PORT` and names the offending variable when a foreign process
|
|
244
|
+
holds one. Reloads of the app's own processes are unaffected.
|
|
245
|
+
- **`start` without a build says what to do.** A missing `dist/index.js` now
|
|
246
|
+
fails with “run `bun run build` first” (also preflighted in `pm2:prod`)
|
|
247
|
+
instead of a bare module-resolution error.
|
|
248
|
+
- **README and AGENTS.md pin the Prisma entry point.** Database commands go
|
|
249
|
+
through the root `bun run db:*` scripts; the `prisma` CLI invoked directly has
|
|
250
|
+
no datasource URL by design.
|
|
251
|
+
|
|
9
252
|
## [0.3.2] — 2026-08-17
|
|
10
253
|
|
|
11
254
|
### Changed
|
package/README.md
CHANGED
|
@@ -16,7 +16,9 @@ complete production UI system.
|
|
|
16
16
|
It uses one conventional `packages/*` namespace: `backend`, `frontend`,
|
|
17
17
|
`config`, `db` and `shared`. The destination name becomes the generated slug;
|
|
18
18
|
`--display-name` sets the human title. Both are recorded once in
|
|
19
|
-
`
|
|
19
|
+
`project.json` — the generated project's **declaration**, the single
|
|
20
|
+
machine-readable statement it makes about itself — and drive package, process,
|
|
21
|
+
transport, UI and SEO identity.
|
|
20
22
|
|
|
21
23
|
The default scaffold is domain-free. To add the runnable repository example:
|
|
22
24
|
|
package/UPGRADING.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Upgrading a generated project
|
|
2
|
+
|
|
3
|
+
How to move a project generated by `create-stitchkit` from one scaffolder
|
|
4
|
+
version to another. It is a different move from upgrading the framework: your
|
|
5
|
+
project is a **copy** of the template, not a dependency on it, so nothing here
|
|
6
|
+
happens by installing anything. What a new scaffolder version brings is a set of
|
|
7
|
+
edits you apply to a tree you already own, plus — and this is the part that bites
|
|
8
|
+
— **operator steps**: things a machine has to be told before the new shape will
|
|
9
|
+
start at all.
|
|
10
|
+
|
|
11
|
+
The framework's own guide is
|
|
12
|
+
[`docs/guide/upgrading.md`](../../docs/guide/upgrading.md); it covers the
|
|
13
|
+
`stitchkit` dependency. This one covers the generated project.
|
|
14
|
+
|
|
15
|
+
## The one rule that makes this work
|
|
16
|
+
|
|
17
|
+
A scaffolder release that changes the generated project in a way an existing
|
|
18
|
+
project must follow leads its [`CHANGELOG.md`](./CHANGELOG.md) entry with a
|
|
19
|
+
**`### ⚠️ Breaking changes`** section (exact heading), each item carrying a
|
|
20
|
+
**before → after** snippet. A version with **no** such section changes nothing
|
|
21
|
+
you are obliged to adopt.
|
|
22
|
+
|
|
23
|
+
The changelog says *what* changed. This file says what else has to happen —
|
|
24
|
+
including the steps that touch a running machine, which have no place in a
|
|
25
|
+
changelog because the next release overwrites the entry that carried them.
|
|
26
|
+
|
|
27
|
+
## Flow
|
|
28
|
+
|
|
29
|
+
1. **Find your project's origin.** The scaffolder version that generated it is
|
|
30
|
+
not recorded in the tree — check the changelog of the version you scaffolded
|
|
31
|
+
with, or diff your tree against a fresh scaffold of your current target.
|
|
32
|
+
2. **Read every `### ⚠️ Breaking changes` in range** in
|
|
33
|
+
[`CHANGELOG.md`](./CHANGELOG.md), from the version above yours up to your
|
|
34
|
+
target.
|
|
35
|
+
3. **Apply the code edits**, then the **`## Released migration: X.Y.Z`** section
|
|
36
|
+
below for the same versions — that is where the operator steps live.
|
|
37
|
+
|
|
38
|
+
> This channel starts at **0.4.0**. Versions below it have breaking changelog
|
|
39
|
+
> entries and no migration section here, because the file did not exist yet;
|
|
40
|
+
> for those, the changelog entry is all there is.
|
|
41
|
+
4. **Verify** with your project's own gates: `bun run check`, `bun run test`,
|
|
42
|
+
`bun run build`, then a real start.
|
|
43
|
+
|
|
44
|
+
## Where a migration section goes while the version has no number
|
|
45
|
+
|
|
46
|
+
Write it here as **`## Unreleased migration: <short slug>`**. The slug matters:
|
|
47
|
+
several may sit side by side, and each belongs to whoever wrote it. Do **not**
|
|
48
|
+
reuse another author's heading — that is how a migration gets overwritten before
|
|
49
|
+
anyone promotes it.
|
|
50
|
+
|
|
51
|
+
At release, the release commit promotes every `Unreleased migration` heading
|
|
52
|
+
into one `## Released migration: X.Y.Z`, each former heading becoming a `###`
|
|
53
|
+
subsection under it — the same move the changelog makes when `[Unreleased]`
|
|
54
|
+
becomes `## [X.Y.Z]`, in the same commit.
|
|
55
|
+
|
|
56
|
+
A release carrying `### ⚠️ Breaking changes` and no matching
|
|
57
|
+
`## Released migration: X.Y.Z` is refused by `bun scripts/release-plan.ts`, in
|
|
58
|
+
`pre-push` and again in the publishing workflow. The check starts at `0.4.0` —
|
|
59
|
+
the first scaffolder release with a migration channel of its own.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Released migration: 0.4.0
|
|
64
|
+
|
|
65
|
+
### the project declares itself
|
|
66
|
+
|
|
67
|
+
Everything in this section is for a project generated **before** the scaffolder
|
|
68
|
+
version that introduces `project.json`.
|
|
69
|
+
|
|
70
|
+
#### The declaration replaces `app.config.json`
|
|
71
|
+
|
|
72
|
+
`app.config.json` said who the project was. `project.json` says what it *is*:
|
|
73
|
+
identity, the roles it runs, what it builds, what it needs before it starts,
|
|
74
|
+
what must happen once on release, and the **names** of the variables a
|
|
75
|
+
deployment supplies. Identity moved under an `identity` key, and the file gained
|
|
76
|
+
a `schemaVersion` so a reader that does not understand the format refuses the
|
|
77
|
+
project instead of interpreting half of it.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// before
|
|
81
|
+
import { appIdentity } from '@app/config/identity';
|
|
82
|
+
appIdentity.name;
|
|
83
|
+
|
|
84
|
+
// after
|
|
85
|
+
import { appDeclaration } from '@app/config/declaration';
|
|
86
|
+
appDeclaration.identity.name;
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
A **client** component imports the generated identity-only module instead —
|
|
90
|
+
importing the whole declaration from the browser ships role commands, working
|
|
91
|
+
directories, artifact paths and the migration lockfile in the bundle:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// before, in a 'use client' file
|
|
95
|
+
import { appIdentity } from '@app/config/identity';
|
|
96
|
+
|
|
97
|
+
// after
|
|
98
|
+
import { appIdentity } from '@app/config/app-identity';
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Operator step: delete the old supervisor processes FIRST
|
|
102
|
+
|
|
103
|
+
PM2 process names now follow the declared role names, so the new supervision
|
|
104
|
+
files start a **new pair beside the old one**. Under `autorestart` both then
|
|
105
|
+
fight for the same ports, and the symptom is an `EADDRINUSE` loop rather than a
|
|
106
|
+
clear error.
|
|
107
|
+
|
|
108
|
+
Before the first `bun run pm2:prod` on the new files:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pm2 delete <slug>-backend <slug>-frontend <slug>-backend-dev <slug>-frontend-dev
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`<slug>` is your project's slug — `identity.slug` in `project.json`. Check what
|
|
115
|
+
is actually registered with `pm2 list` first; nothing here is safe to run blind.
|
|
116
|
+
|
|
117
|
+
#### Operator step: the supervisor's patience must cover the whole shutdown
|
|
118
|
+
|
|
119
|
+
The generated `ecosystem.config.cjs` is now rendered from the declaration, and
|
|
120
|
+
its `kill_timeout` is computed from each role's drain floor plus the force
|
|
121
|
+
window plus cleanup. If you kept a hand-edited supervision file, compare its
|
|
122
|
+
`kill_timeout` against `drainFloorMs` in `project.json`: a timeout shorter than
|
|
123
|
+
the full budget means the drain never finishes, every time, and the only visible
|
|
124
|
+
trace is a non-zero exit code.
|
|
125
|
+
|
|
126
|
+
Regenerate rather than hand-edit:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
bun run gen:declaration
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### Environment: three variables changed meaning
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
# before
|
|
136
|
+
NEXT_PUBLIC_API_URL=https://api.example
|
|
137
|
+
NEXT_PUBLIC_WEB_URL=https://app.example
|
|
138
|
+
|
|
139
|
+
# after — nothing. Both are gone.
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Anything prefixed `NEXT_PUBLIC_` is substituted at **build** time, so declaring
|
|
143
|
+
one froze an address into the artifact: the built `robots.txt` and `sitemap.xml`
|
|
144
|
+
carried one origin in their bytes and one build could not serve a second
|
|
145
|
+
address. The public origin now comes from the request.
|
|
146
|
+
|
|
147
|
+
A forwarded host must be claimed before it is believed. Set **one** of:
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
PUBLIC_WEB_ORIGIN=https://app.example # a single address
|
|
151
|
+
PUBLIC_WEB_HOSTS=app.example,www.app.example # several
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
A host outside them is refused. Setting `PUBLIC_WEB_ORIGIN` also restores static
|
|
155
|
+
rendering for `/robots.txt`, `/sitemap.xml` and the `[locale]` segment, which
|
|
156
|
+
otherwise become request-rendered.
|
|
157
|
+
|
|
158
|
+
The check and e2e addresses were renamed so no build can substitute them:
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
# before
|
|
162
|
+
NEXT_PUBLIC_API_URL=http://127.0.0.1:3211
|
|
163
|
+
# after
|
|
164
|
+
SMOKE_API_ORIGIN=http://127.0.0.1:3211
|
|
165
|
+
SMOKE_WEB_ORIGIN=http://127.0.0.1:3210
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`CORS_ORIGIN` is now optional: a frontend that reaches the API through its own
|
|
169
|
+
routing layer makes same-origin requests, and requiring an origin there was
|
|
170
|
+
requiring knowledge of the place.
|
|
171
|
+
|
|
172
|
+
#### The repository example: calls lose their parentheses
|
|
173
|
+
|
|
174
|
+
Only for a project generated with `--example repository`. The browser now talks
|
|
175
|
+
to its own origin, so the API client is a module constant:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
// before
|
|
179
|
+
repositoryApi().read();
|
|
180
|
+
repositoryUrls().read();
|
|
181
|
+
|
|
182
|
+
// after
|
|
183
|
+
repositoryApi.read();
|
|
184
|
+
repositoryUrls.read();
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The web role forwards `/api/…` to the API role, which needs `INTERNAL_API_URL`
|
|
188
|
+
(already required).
|
|
189
|
+
|
|
190
|
+
**The socket's address is now its own variable.** If you use the realtime
|
|
191
|
+
socket and have no routing layer forwarding `/socket.io`, rename it:
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
# before — read only by the socket, despite the name
|
|
195
|
+
PUBLIC_API_ORIGIN=https://api.example
|
|
196
|
+
# after
|
|
197
|
+
PUBLIC_REALTIME_ORIGIN=https://api.example
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`PUBLIC_API_ORIGIN` still exists and still means HTTP, but it is inert until you
|
|
201
|
+
switch: the import in `packages/frontend/src/lib/api/queries.ts` decides which
|
|
202
|
+
client the browser uses. Both variables are optional; a deployment behind one
|
|
203
|
+
routing layer sets neither, and `CORS_ORIGIN` with them.
|
|
204
|
+
|
|
205
|
+
The cross-origin form moved to `packages/frontend/src/lib/api/cross-origin.ts`
|
|
206
|
+
(previously `lib/api/origin.ts`); `requirePublicApiOrigin` lives there, beside
|
|
207
|
+
`optionalRealtimeOrigin` and `setPublicOrigins`.
|
|
208
|
+
|
|
209
|
+
#### The SEO helpers are async
|
|
210
|
+
|
|
211
|
+
They read the public origin from the request, so they cannot be constants.
|
|
212
|
+
TypeScript will **not** catch a missing `await` inside an inferred object
|
|
213
|
+
literal — a `Promise` serialises there as `{}`.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// before
|
|
217
|
+
const url = absoluteSiteUrl('/en');
|
|
218
|
+
createPageMetadata('home', locale);
|
|
219
|
+
|
|
220
|
+
// after — and the component becomes async
|
|
221
|
+
const url = await absoluteSiteUrl('/en');
|
|
222
|
+
await createPageMetadata('home', locale);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`siteOrigin` is gone.
|