create-stitchkit 0.3.3 → 0.4.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 +347 -0
- package/README.md +3 -1
- package/UPGRADING.md +342 -0
- package/dist/cli.js +238 -42
- 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 +38 -6
- package/package.json +12 -2
- package/template/AGENTS.md +23 -3
- package/template/README.md +83 -8
- package/template/_env.example +16 -4
- package/template/_gitignore +1 -0
- package/template/biome.json +6 -2
- package/template/bun.lock +115 -98
- 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 +12 -10
- package/template/packages/backend/package.json +2 -2
- package/template/packages/backend/src/cleanup.ts +121 -0
- package/template/packages/backend/src/cli.ts +6 -2
- package/template/packages/backend/src/index.ts +33 -8
- 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 +6 -2
- 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/server.ts +8 -17
- package/template/packages/config/src/shutdown.ts +20 -0
- package/template/packages/config/src/variables.ts +89 -0
- package/template/packages/db/package.json +2 -2
- package/template/packages/frontend/next.config.ts +3 -2
- package/template/packages/frontend/package.json +13 -13
- 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/components/ui/toaster.tsx +4 -1
- 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 +1 -1
- 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/packages/shared/package.json +1 -1
- package/template/playwright.config.ts +1 -1
- package/template/project.json +169 -0
- package/template/scripts/acceptance-database.test.ts +73 -0
- package/template/scripts/acceptance-database.ts +92 -0
- package/template/scripts/acceptance-local.ts +144 -0
- package/template/scripts/build-inputs.test.ts +69 -0
- package/template/scripts/build-inputs.ts +58 -0
- package/template/scripts/build-stamp.test.ts +151 -0
- package/template/scripts/build-stamp.ts +169 -0
- package/template/scripts/check-authored.ts +18 -2
- package/template/scripts/client-boundary.test.ts +117 -0
- package/template/scripts/client-boundary.ts +148 -0
- package/template/scripts/declaration.test.ts +206 -0
- package/template/scripts/declaration.ts +271 -0
- package/template/scripts/deployment-preflight.ts +41 -0
- package/template/scripts/dev.ts +43 -20
- package/template/scripts/local-env.test.ts +2 -2
- package/template/scripts/local-env.ts +9 -3
- package/template/scripts/readiness.ts +92 -0
- package/template/scripts/release-steps.test.ts +87 -0
- package/template/scripts/release-steps.ts +112 -0
- package/template/scripts/release.ts +38 -0
- package/template/scripts/runtime-smoke.test.ts +178 -0
- package/template/scripts/runtime-smoke.ts +21 -5
- package/template/scripts/serve-mode.test.ts +36 -0
- package/template/scripts/shutdown-budget.fixture.ts +29 -0
- package/template/scripts/shutdown-budget.test.ts +164 -0
- package/template/scripts/supervision-signal.test.ts +94 -0
- package/template/scripts/surface-conformance.ts +8 -1
- package/template/scripts/tooling-env.ts +35 -3
- package/template/scripts/web-surface-smoke.ts +183 -2
- package/template/app.config.json +0 -9
- package/template/packages/config/src/identity.ts +0 -18
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,355 @@ 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.1] — 2026-08-25
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **The `Gates` list can be run top to bottom, and it never deploys.** It
|
|
20
|
+
presented five commands in a row, two of which check a *running* deployment —
|
|
21
|
+
with nothing in the list that starts one, so on a fresh project `bun run
|
|
22
|
+
runtime:smoke` failed with a connection reset from inside a check. The answer
|
|
23
|
+
is not a deploy command: `bun run pm2:prod` applies the declared migrations
|
|
24
|
+
and reloads the PM2 daemon the developer is running, so a gate list carrying
|
|
25
|
+
it quietly means "deploy". The runtime gates now run under `bun run
|
|
26
|
+
acceptance:local`, which creates a deployment of its own — separate
|
|
27
|
+
`PM2_HOME`, ephemeral ports, its own public-host allowlist, its own
|
|
28
|
+
database — and destroys it by naming the declared roles. Neither guide says
|
|
29
|
+
`pm2 delete all` any more: it empties whichever daemon it is pointed at,
|
|
30
|
+
including applications that have nothing to do with the project.
|
|
31
|
+
- **The acceptance gate writes only to a database of its own.** It inherited
|
|
32
|
+
`DATABASE_URL`, and the runtime gates write — the repository example's smoke
|
|
33
|
+
posts `/api/repository/refresh` twice, which upserts. So a command a developer
|
|
34
|
+
is told to run before handing work off wrote rows into whatever `.env` named.
|
|
35
|
+
It runs against `ACCEPTANCE_DATABASE_URL`, applies the declared migrations
|
|
36
|
+
there, and refuses to start when that variable is unset or reuses the
|
|
37
|
+
deployment's database **name** — with the line to paste in the refusal. The
|
|
38
|
+
name alone decides, because a hostname is not proof of a different server:
|
|
39
|
+
`localhost` and `127.0.0.1` are one, and so are two DNS names for the same
|
|
40
|
+
PostgreSQL. The deployment's URL is not in the harness's child environment at
|
|
41
|
+
all.
|
|
42
|
+
- **A release will not start an artifact that is not this source's.**
|
|
43
|
+
`assertBuildArtifacts()` checked that the declared paths exist, and existence
|
|
44
|
+
is not freshness: `pm2:prod` run without a build applied this source's
|
|
45
|
+
migrations and started the previous source's `dist` and `.next`, moving the
|
|
46
|
+
schema ahead of the code in the one direction a code rollback does not undo.
|
|
47
|
+
`bun run build` now leaves a digest of the source it read, and the release
|
|
48
|
+
refuses when the tree no longer hashes to it — a digest rather than a
|
|
49
|
+
timestamp, because a checkout rewrites every mtime and a formatter rewrites
|
|
50
|
+
some for no change at all. Everything is source unless something names it
|
|
51
|
+
otherwise, and the something is the **declaration**: the digest skips
|
|
52
|
+
`build.artifacts`, `node_modules`/`.git`, and runtime state. It no longer
|
|
53
|
+
skips by kind — every `.md`, every test file, every directory called
|
|
54
|
+
`generated` — because a project that imports MDX or keeps checked-in source in
|
|
55
|
+
a directory of that name would have changed its content, kept its digest, and
|
|
56
|
+
been told a stale artifact was current. `.env` stays out on purpose: a binding
|
|
57
|
+
is not an input to this build, and hashing it would refuse a correct artifact
|
|
58
|
+
whenever a deployment edited its own environment.
|
|
59
|
+
- **The shutdown budget is an upper bound again.** `terminationBudgetMs` adds a
|
|
60
|
+
fixed cleanup allowance to the drain floor and refuses a supervision policy
|
|
61
|
+
that allows less — but the closes that run after the drain (the MCP session,
|
|
62
|
+
the database pool) had no deadline of their own, so a hung close ran past the
|
|
63
|
+
very kill timeout the budget had approved and turned an orderly shutdown into
|
|
64
|
+
the SIGKILL that runs no cleanup at all. The role's cleanup now shares one
|
|
65
|
+
bounded budget with the generator, from one constant both read, and names
|
|
66
|
+
whatever it stopped waiting for — and then **ends the process**. Setting
|
|
67
|
+
`process.exitCode` only decides the code a process reports when it exits, and
|
|
68
|
+
a step that ran out of time is usually still holding the handle that stops it
|
|
69
|
+
from exiting; a close that *threw* is reported with its cause and is no longer
|
|
70
|
+
counted as a clean shutdown. The shared deadline is measured with
|
|
71
|
+
`performance.now()` and the clock can no longer be injected: a wall clock
|
|
72
|
+
stepped backwards widens the very upper bound the supervisor's kill timeout
|
|
73
|
+
was derived from, and a frozen injected clock hands every step a full budget.
|
|
74
|
+
- **The project declaration stays out of the browser bundle.**
|
|
75
|
+
`lib/seo/pages.ts` imported `appDeclaration` for one field, and a client
|
|
76
|
+
component imports that module — so role commands, working directories,
|
|
77
|
+
artifact and migration paths and every environment variable name travelled
|
|
78
|
+
into the client graph along with the Zod parser. It reads `appIdentity` now,
|
|
79
|
+
and a test walks every `'use client'` graph and fails if one reaches the
|
|
80
|
+
declaration — by resolving each specifier to a file rather than matching a
|
|
81
|
+
string, so a barrel re-export, a relative path into the config package and a
|
|
82
|
+
double-quoted import are all caught.
|
|
83
|
+
- **A duplicated host is one address.** The portability check counted the
|
|
84
|
+
entries of `PUBLIC_WEB_HOSTS` without deduplicating them, so the same host
|
|
85
|
+
written twice passed as two addresses and the proof compared the deployment
|
|
86
|
+
with itself.
|
|
87
|
+
- **Every dial in the runtime smoke is bounded.** An endpoint that accepts the
|
|
88
|
+
connection and never answers used to hang the gate with no output and no
|
|
89
|
+
deadline instead of failing it.
|
|
90
|
+
- **A build output cannot be published inside the template.** What the scaffolder
|
|
91
|
+
copies and what npm publishes were two lists that had to agree, and only one of
|
|
92
|
+
them was consulted when a name was excluded. The exclusions are data now, and a
|
|
93
|
+
test fails until the package manifest carries the same negation.
|
|
94
|
+
- **A role bound to an IPv6 address gets a readiness URL that parses.**
|
|
95
|
+
`BIND_HOST=::1` produced `http://::1:3211/health`, which is not an address
|
|
96
|
+
with a port and which `fetch` refuses — so the wait failed on the spelling
|
|
97
|
+
rather than on the role. The literal is bracketed.
|
|
98
|
+
- **`bun run dev` and `bun run pm2:prod` report the roles as running only once
|
|
99
|
+
they answer.** A supervisor returns at the spawn, seconds before a role
|
|
100
|
+
listens, so both printed their address at a moment when nothing was there and
|
|
101
|
+
every command after them raced the application they had just started. Both now
|
|
102
|
+
wait on each role's declared `readinessPath`.
|
|
103
|
+
- **`runtime:smoke` asks the deployment on the addresses it claims.** The
|
|
104
|
+
portability check carried two fixture hosts of its own, so it only ever passed
|
|
105
|
+
where somebody had put those exact names into `PUBLIC_WEB_HOSTS` — which only
|
|
106
|
+
the packed lane had. Everyone else got a bare 500 from a policy working as
|
|
107
|
+
designed. It now reads `PUBLIC_WEB_HOSTS`, and a deployment with too few
|
|
108
|
+
addresses to compare is told which line to add instead of being refused a
|
|
109
|
+
request.
|
|
110
|
+
- **A closed deployment is diagnosed, not reset.** `runtime:smoke` says what is
|
|
111
|
+
not listening and which command starts it, before the first check runs.
|
|
112
|
+
- **The theme value the toaster reads narrows again.** `@wrksz/themes` 1.2
|
|
113
|
+
changed `useThemeValue`'s type parameter to describe the map rather than the
|
|
114
|
+
value, and it now infers `const` — so naming the value union at the call site
|
|
115
|
+
widened the result to every member of `string` and the generated project
|
|
116
|
+
stopped type-checking. The call site lets inference do it.
|
|
117
|
+
- **A green `runtime:smoke` prints one line.** The MCP surface check asked the
|
|
118
|
+
server to list tools even when it advertised no tool capability, which made
|
|
119
|
+
the vendor client log a debug warning on every successful run. It reads the
|
|
120
|
+
advertised capability instead.
|
|
121
|
+
|
|
122
|
+
### Changed
|
|
123
|
+
|
|
124
|
+
- **Every dependency of the generated project is on its latest release.**
|
|
125
|
+
Next 16.3.2, `next-intl` 4.13.7, `@tanstack/react-query` 5.102.3,
|
|
126
|
+
`@tanstack/react-table` 9.1.2, `framer-motion` 13.1.1, `@wrksz/themes` 1.2.0,
|
|
127
|
+
`shiki` 4.4.3, `sonner` 2.0.8, `ai` 7.0.78, `pg` 8.23.0, Playwright 1.62.1,
|
|
128
|
+
Biome 2.5.10 and the `@types/*` that go with them. No major crossed; the
|
|
129
|
+
Stitchkit range is unchanged and still declared once, in the catalog.
|
|
130
|
+
- **The generated project imports the declaration schema instead of mirroring
|
|
131
|
+
it.** `packages/config/src/declaration.ts` now reads
|
|
132
|
+
`parseProjectDeclaration` from `stitchkit/declaration`, and the 611-line
|
|
133
|
+
generated copy — `packages/config/src/project-declaration.generated.ts` — is
|
|
134
|
+
gone with the script that maintained it. The copy existed only because the
|
|
135
|
+
entrypoint was not on npm yet; the template's catalog targets `^0.60.0`,
|
|
136
|
+
which publishes it, so "one schema, three readers" is now literally true.
|
|
137
|
+
Adopting it is one import and one deletion — see
|
|
138
|
+
[`UPGRADING.md`](./UPGRADING.md).
|
|
139
|
+
- **`scripts/local-env.ts` reads the identity module, not the declaration.** It
|
|
140
|
+
needs one slug, and a project scaffolded with `--no-install` renders its
|
|
141
|
+
`.env` before anything is installed — a script that reaches for the
|
|
142
|
+
framework's schema to read a name cannot run in that window.
|
|
143
|
+
|
|
144
|
+
## [0.4.0] — 2026-08-25
|
|
145
|
+
|
|
146
|
+
### ⚠️ Breaking changes
|
|
147
|
+
|
|
148
|
+
- **The repository example's browser talks to its OWN origin by default.** The
|
|
149
|
+
example is what gets copied, and it was demonstrating the hard case: the
|
|
150
|
+
browser dialled the API role directly, so the address had to arrive from the
|
|
151
|
+
server at runtime, the API client could not exist until it did, and every call
|
|
152
|
+
site paid for that with a lazy accessor — `repositoryApi().read()` — plus a
|
|
153
|
+
runtime error when something rendered outside `<Providers>`. Somebody copying
|
|
154
|
+
it inherited that whether or not they were cross-origin at all. The body of
|
|
155
|
+
the example is now the default shape: a same-origin `/api/…` path forwarded by
|
|
156
|
+
the web role, and a client that is a module constant.
|
|
157
|
+
`// before: repositoryApi().read()` → `// after: repositoryApi.read()`
|
|
158
|
+
The cross-origin form is not lost — it moved to a named file,
|
|
159
|
+
`packages/frontend/src/lib/api/cross-origin.ts`, with what it costs written
|
|
160
|
+
next to it, and switching to it is one import in `queries.ts`.
|
|
161
|
+
- **`PUBLIC_REALTIME_ORIGIN` — the socket's address has a name of its own.**
|
|
162
|
+
`PUBLIC_API_ORIGIN` used to carry both questions and answer only one: it read
|
|
163
|
+
as a mode switch while in fact nothing but the realtime socket looked at it.
|
|
164
|
+
The two are genuinely different — HTTP can be forwarded by the web role, and a
|
|
165
|
+
WebSocket upgrade cannot survive a route handler — so a deployment can be
|
|
166
|
+
same-origin for HTTP and still have to name the socket's origin. Both are
|
|
167
|
+
optional; a deployment behind one routing layer sets neither.
|
|
168
|
+
`// before: PUBLIC_API_ORIGIN=https://api.example # …which only the socket read` →
|
|
169
|
+
`// after: PUBLIC_REALTIME_ORIGIN=https://api.example`
|
|
170
|
+
- **`app.config.json` is now `project.json` — the project *declaration*.** It no
|
|
171
|
+
longer describes only identity: it states what this repository is, the roles it
|
|
172
|
+
runs, what it builds, what it needs before it starts, the release steps that
|
|
173
|
+
must happen once, and the environment variables a deployment must supply.
|
|
174
|
+
Identity moved under an `identity` key and the file gained a `schemaVersion`,
|
|
175
|
+
so a reader that does not understand the format refuses the project instead of
|
|
176
|
+
interpreting it partially.
|
|
177
|
+
`// before: import { appIdentity } from '@app/config/identity'; appIdentity.name` →
|
|
178
|
+
`// after: import { appDeclaration } from '@app/config/declaration'; appDeclaration.identity.name`
|
|
179
|
+
|
|
180
|
+
A client component imports `@app/config/app-identity` instead — a generated
|
|
181
|
+
module carrying identity alone. Importing the whole declaration from the
|
|
182
|
+
browser would ship role commands, working directories, build artifact paths and
|
|
183
|
+
the migration lockfile in the bundle.
|
|
184
|
+
`// before: import { appIdentity } from '@app/config/identity' // in a 'use client' file` →
|
|
185
|
+
`// after: import { appIdentity } from '@app/config/app-identity'`
|
|
186
|
+
|
|
187
|
+
The rule the file exists to hold: **it must be complete with no machine in
|
|
188
|
+
existence.** Ports, hosts, addresses, machine paths, routing shape and
|
|
189
|
+
supervision policy are named there by variable and never by value — and the
|
|
190
|
+
schema refuses them by shape rather than by review.
|
|
191
|
+
|
|
192
|
+
- **The SEO helpers are async, and `siteOrigin` is gone.** They read the public
|
|
193
|
+
origin from the request instead of a build-time constant, so they cannot be
|
|
194
|
+
constants themselves. A page that calls them must await them — and TypeScript
|
|
195
|
+
will *not* catch it inside an inferred object literal, where a `Promise`
|
|
196
|
+
silently serialises as `{}`.
|
|
197
|
+
`// before: const url = absoluteSiteUrl('/en'); export const siteOrigin` →
|
|
198
|
+
`// after: const url = await absoluteSiteUrl('/en') // and the component becomes async`
|
|
199
|
+
`// before: createPageMetadata('home', locale)` →
|
|
200
|
+
`// after: await createPageMetadata('home', locale)`
|
|
201
|
+
|
|
202
|
+
- **A forwarded host must be claimed before it is believed.** The public origin
|
|
203
|
+
comes from the request, which makes one artifact serve many addresses — and
|
|
204
|
+
would let any caller choose the canonical URL, the sitemap and the OG metadata
|
|
205
|
+
if it were trusted blindly. Set `PUBLIC_WEB_ORIGIN` for a single address, or
|
|
206
|
+
`PUBLIC_WEB_HOSTS` for several; a host outside them is refused. `x-forwarded-proto`
|
|
207
|
+
is narrowed to `http` or `https`.
|
|
208
|
+
`// before: (nothing — any x-forwarded-host was honoured)` →
|
|
209
|
+
`// after: PUBLIC_WEB_HOSTS=app.example,www.app.example`
|
|
210
|
+
|
|
211
|
+
- **Environment variables are declared once, and the declaration lists them as
|
|
212
|
+
`env.variables`.** The server schema, the frontend schema and the tooling
|
|
213
|
+
schema were three overlapping copies that had already diverged.
|
|
214
|
+
`packages/config/src/variables.ts` is now the single declaration; `server.ts`
|
|
215
|
+
and `frontend/src/env.ts` are projections of it, and the declaration's list is
|
|
216
|
+
*derived* from it rather than restated. An overlay may now **tighten** a
|
|
217
|
+
variable, not only add one — the repository example requires `INTERNAL_API_URL`,
|
|
218
|
+
`PUBLIC_API_ORIGIN` and `CORS_ORIGIN` because its frontend dereferences them on
|
|
219
|
+
every render.
|
|
220
|
+
`// before: z.url() repeated in three files; env.required with required:false entries` →
|
|
221
|
+
`// after: applicationVariables.INTERNAL_API_URL, referenced; env.variables`
|
|
222
|
+
|
|
223
|
+
- **`NEXT_PUBLIC_API_URL` and `NEXT_PUBLIC_WEB_URL` are gone.** Anything prefixed
|
|
224
|
+
`NEXT_PUBLIC_` is substituted at BUILD time, so declaring one froze a value of
|
|
225
|
+
the place into the artifact: the built `robots.txt` and `sitemap.xml` carried
|
|
226
|
+
one origin inside their bytes, and the server chunk carried
|
|
227
|
+
`NEXT_PUBLIC_API_URL:"http://…"` as a literal. One build could not serve a
|
|
228
|
+
second address.
|
|
229
|
+
`// before: NEXT_PUBLIC_WEB_URL=https://app.example → baked at build` →
|
|
230
|
+
`// after: no variable; the origin is read from the request`
|
|
231
|
+
**Cost, stated plainly:** `/robots.txt`, `/sitemap.xml` and — because the root
|
|
232
|
+
layout's `generateMetadata` reads the request — the whole `[locale]` segment
|
|
233
|
+
are no longer prerendered as static content. Setting `PUBLIC_WEB_ORIGIN`
|
|
234
|
+
short-circuits the request read and restores static rendering for a deployment
|
|
235
|
+
that serves exactly one address. Answers are built once per address, not once
|
|
236
|
+
per request: `cacheByOrigin` memoises them behind a bounded LRU so a forged
|
|
237
|
+
`Host` cannot grow the cache.
|
|
238
|
+
|
|
239
|
+
- **`CORS_ORIGIN` is optional.** A frontend that reaches the API through its own
|
|
240
|
+
routing layer makes same-origin requests, and requiring an origin there was
|
|
241
|
+
requiring knowledge of the place. Set it only for a genuinely cross-origin
|
|
242
|
+
browser.
|
|
243
|
+
`// before: CORS_ORIGIN=https://app.example # required` →
|
|
244
|
+
`// after: unset unless the browser genuinely lives elsewhere`
|
|
245
|
+
|
|
246
|
+
- **The smoke and e2e addresses are `SMOKE_API_ORIGIN` and `SMOKE_WEB_ORIGIN`.**
|
|
247
|
+
They are legitimately bound to a place — they name the deployment a check dials —
|
|
248
|
+
but must not carry a prefix that makes the build substitute them.
|
|
249
|
+
`// before: NEXT_PUBLIC_API_URL=http://127.0.0.1:3211` →
|
|
250
|
+
`// after: SMOKE_API_ORIGIN=http://127.0.0.1:3211`
|
|
251
|
+
|
|
252
|
+
- **Each role is started by its own PROCESS, in its own directory, and PM2
|
|
253
|
+
process names follow the declared role names.** A role's command is `executable`
|
|
254
|
+
plus `args` in the declaration — argv, never a shell string — and the
|
|
255
|
+
supervision files are rendered from it.
|
|
256
|
+
`// before: <slug>-backend, <slug>-frontend` → `// after: <slug>-api, <slug>-web`
|
|
257
|
+
**Before your first `pm2:prod` on the new files**, remove the old processes, or
|
|
258
|
+
`startOrReload` will start the new pair beside them and both will fight for the
|
|
259
|
+
same ports under `autorestart`:
|
|
260
|
+
```bash
|
|
261
|
+
pm2 delete <slug>-backend <slug>-frontend <slug>-backend-dev <slug>-frontend-dev
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
- **The web role reads its bindings itself.** The supervisor no longer builds an
|
|
265
|
+
argv for it; injecting `WEB_PORT` is now sufficient, where before a deployment
|
|
266
|
+
that set it and stopped there got a web role on the wrong port, silently. There
|
|
267
|
+
is no default port in the repository any more — a missing variable fails by
|
|
268
|
+
name.
|
|
269
|
+
`// before: "start": "next start --port ${WEB_PORT:-3210}"` →
|
|
270
|
+
`// after: "start": "bun scripts/serve.ts production"`
|
|
271
|
+
|
|
272
|
+
### Fixed
|
|
273
|
+
|
|
274
|
+
- **A supervised backend now actually drains.** The supervision files started the
|
|
275
|
+
role through a script runner, so the stop signal arrived twice — once from PM2,
|
|
276
|
+
once forwarded by the launcher — and a shutdown chain treats the second signal
|
|
277
|
+
as "force it now". Measured on a real PM2 stop: a declared 15 s grace period
|
|
278
|
+
ended after **1.3 ms** with `outcome: "forced"`, `reason: "signal"`, and the
|
|
279
|
+
only visible trace was a non-zero exit code. The supervisor now execs the role
|
|
280
|
+
itself, and the same stop reports `outcome: "clean"` with exit 0.
|
|
281
|
+
`// before: script: 'bun', args: ['run', 'start'] // an intermediate shape, never released` →
|
|
282
|
+
`// after: script: 'bun', args: ['dist/index.js']`
|
|
283
|
+
A generated project at the previous release ran `script: 'dist/index.js'` with
|
|
284
|
+
`interpreter: 'bun'`, which had the same property; the defect it *did* ship is
|
|
285
|
+
the timeout mismatch below.
|
|
286
|
+
- **Supervision no longer kills the backend mid-shutdown.** The application asked
|
|
287
|
+
for a 30 s drain while PM2 sent `SIGKILL` after `kill_timeout: 15000` in
|
|
288
|
+
production and 10000 in development — so a drain longer than the supervisor's
|
|
289
|
+
patience never finished, every time. The check now covers the **whole**
|
|
290
|
+
termination budget rather than the drain alone: drain floor, plus the force
|
|
291
|
+
window that follows it, plus cleanup. Comparing against the floor alone let
|
|
292
|
+
15 s + 5 s meet a 20 s kill timeout exactly, with no margin at all.
|
|
293
|
+
- **The backend says how its shutdown ended.** `onComplete` logs the outcome, the
|
|
294
|
+
reason, the duration and how many requests completed or were aborted. Without
|
|
295
|
+
it an operator saw a process that vanished and an exit code, and could not tell
|
|
296
|
+
a clean drain from one that was cut short — which is exactly how the defect
|
|
297
|
+
above stayed invisible.
|
|
298
|
+
- **A requested stop of the web role reports success.** Next exits `130` on
|
|
299
|
+
`SIGINT`; the role passed that upward, so every ordinary supervised stop looked
|
|
300
|
+
like a failure. A stop the role was asked to perform now exits `0`, while a code
|
|
301
|
+
from any other cause is still passed on unchanged.
|
|
302
|
+
- **A deployment's environment is no longer overruled by a file.** The production
|
|
303
|
+
supervision file loaded `.env` with `override: true`, so a value injected into
|
|
304
|
+
the process lost to a value in the repository. Bindings come from the place; the
|
|
305
|
+
file fills gaps.
|
|
306
|
+
- **Release steps come from the declaration.** `pm2:prod` hand-carried
|
|
307
|
+
`db:deploy` and a build preflight as a shell string beside the declaration that
|
|
308
|
+
already stated them. It now runs `scripts/release.ts`, which checks every
|
|
309
|
+
artifact `build.artifacts` declares and applies migrations for the engine
|
|
310
|
+
`release.migrations` declares — refusing an engine it has no command for rather
|
|
311
|
+
than skipping the step, because silently not migrating is what leaves a machine
|
|
312
|
+
running against the wrong schema. Both declared migration paths are checked,
|
|
313
|
+
the lockfile included. Development runs the same step.
|
|
314
|
+
- **`bun run test` passes on a fresh scaffold.** A test read `.env` at module
|
|
315
|
+
load, before `env:ensure` had created it, so the second gate the README tells a
|
|
316
|
+
new user to run died with `ENOENT` before a single test executed. Both CI paths
|
|
317
|
+
write `.env` first, so nothing saw it.
|
|
318
|
+
|
|
319
|
+
### Changed
|
|
320
|
+
|
|
321
|
+
- **The generated build declares whether it reads data, and CI proves it does
|
|
322
|
+
not.** Data read while building is a third kind of input — neither code nor a
|
|
323
|
+
binding — and a build that reads it undeclared is a function of whichever
|
|
324
|
+
machine had the database. The template answers it the default way: no route
|
|
325
|
+
can reach a data source at all (`check-authored` refuses the import and now
|
|
326
|
+
says why), so the build is a function of the source alone. The packed lane
|
|
327
|
+
builds against a database address that accepts nothing, which is the only
|
|
328
|
+
check that covers every transitive path at once. A project that genuinely
|
|
329
|
+
needs data at build time declares a frozen export in `build.inputs` and
|
|
330
|
+
`bun scripts/build-inputs.ts` refuses it the moment its digest drifts.
|
|
331
|
+
- **One artifact is now provably portable in CI, within a stated policy.**
|
|
332
|
+
`runtime:smoke` asks the running web role for `/sitemap.xml` and `/robots.txt`
|
|
333
|
+
under two different external addresses and requires two different answers —
|
|
334
|
+
and requires a *third*, unclaimed host to be refused. The first half catches a
|
|
335
|
+
build-time address creeping back in; the second catches the portability
|
|
336
|
+
mechanism turning into an open redirect for metadata.
|
|
337
|
+
- **The drain floor has one home.** The backend passed a literal grace period
|
|
338
|
+
while the declaration stated another — the same two-numbers-in-two-files shape
|
|
339
|
+
that let a 30 s floor meet a 15 s kill timeout. It now reads
|
|
340
|
+
`apiRole.drainFloorMs`, the number a supervisor reads too.
|
|
341
|
+
- **Three files are generated from the declaration** — both supervision files and
|
|
342
|
+
the client-safe identity module — plus the `env.variables` block of the
|
|
343
|
+
declaration itself. `bun run gen:declaration` renders them and the test suite
|
|
344
|
+
refuses a stale copy. The example's declaration is generated from the
|
|
345
|
+
template's in the same way.
|
|
346
|
+
- **The generated application targets Stitchkit `^0.59.0`.** The template's
|
|
347
|
+
catalog pointed at `^0.52.0`, so a project scaffolded today started seven
|
|
348
|
+
minors behind the framework — without neutral client-disconnect handling,
|
|
349
|
+
managed files, composed auth, async operation contracts or the CLI presentation
|
|
350
|
+
policy. Both the packed target lane and the packed HEAD lane pass on the new
|
|
351
|
+
target.
|
|
352
|
+
- **The scaffolder prints the addresses the generated project will actually
|
|
353
|
+
use**, read back from its declaration and example environment rather than
|
|
354
|
+
restated as constants.
|
|
355
|
+
|
|
9
356
|
## [0.3.3] — 2026-08-18
|
|
10
357
|
|
|
11
358
|
### 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
|
|