create-stitchkit 0.4.0 → 0.4.2
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 +141 -0
- package/UPGRADING.md +117 -0
- package/dist/cli.js +5 -1
- package/examples/repository/scripts/runtime-smoke.ts +14 -2
- package/package.json +4 -2
- package/template/AGENTS.md +15 -5
- package/template/README.md +46 -8
- package/template/_env.example +8 -0
- package/template/_gitignore +1 -0
- package/template/biome.json +1 -1
- package/template/bun.lock +115 -98
- package/template/package.json +9 -8
- package/template/packages/backend/package.json +2 -2
- package/template/packages/backend/src/cleanup.ts +121 -0
- package/template/packages/backend/src/index.ts +12 -3
- package/template/packages/config/package.json +3 -1
- package/template/packages/config/src/declaration.ts +1 -1
- package/template/packages/config/src/shutdown.ts +20 -0
- package/template/packages/db/package.json +2 -2
- package/template/packages/frontend/package.json +11 -11
- package/template/packages/frontend/src/components/ui/toaster.tsx +4 -1
- package/template/packages/frontend/src/lib/seo/pages.ts +2 -2
- package/template/packages/shared/package.json +1 -1
- 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 +1 -1
- package/template/scripts/build-inputs.ts +4 -3
- package/template/scripts/build-stamp.test.ts +151 -0
- package/template/scripts/build-stamp.ts +169 -0
- package/template/scripts/client-boundary.test.ts +117 -0
- package/template/scripts/client-boundary.ts +148 -0
- package/template/scripts/declaration.ts +10 -7
- package/template/scripts/deployment-preflight.ts +41 -0
- package/template/scripts/dev.ts +8 -6
- package/template/scripts/local-env.ts +9 -3
- package/template/scripts/readiness.ts +92 -0
- package/template/scripts/release-steps.ts +5 -1
- package/template/scripts/release.ts +8 -0
- package/template/scripts/runtime-smoke.test.ts +178 -0
- package/template/scripts/runtime-smoke.ts +15 -2
- package/template/scripts/shutdown-budget.fixture.ts +29 -0
- package/template/scripts/shutdown-budget.test.ts +164 -0
- package/template/scripts/surface-conformance.ts +8 -1
- package/template/scripts/tooling-env.ts +30 -1
- package/template/scripts/web-surface-smoke.ts +125 -14
- package/template/packages/config/src/project-declaration.generated.ts +0 -611
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,147 @@ step is overwritten by the next release.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.4.2] — 2026-08-25
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **A freshly scaffolded project resolves the framework release that exists.**
|
|
20
|
+
The template ships a lockfile so a scaffold is reproducible, and that lockfile
|
|
21
|
+
still pinned the previous Stitchkit patch — so `create-stitchkit` published
|
|
22
|
+
minutes after a framework release produced a project on the older one, inside
|
|
23
|
+
a range that already allowed the newer. The range and the lock move together
|
|
24
|
+
now: `catalog.stitchkit` targets `^0.60.1` and the lockfile resolves it, which
|
|
25
|
+
is also what the packed target lane then tests against.
|
|
26
|
+
|
|
27
|
+
## [0.4.1] — 2026-08-25
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **The `Gates` list can be run top to bottom, and it never deploys.** It
|
|
32
|
+
presented five commands in a row, two of which check a *running* deployment —
|
|
33
|
+
with nothing in the list that starts one, so on a fresh project `bun run
|
|
34
|
+
runtime:smoke` failed with a connection reset from inside a check. The answer
|
|
35
|
+
is not a deploy command: `bun run pm2:prod` applies the declared migrations
|
|
36
|
+
and reloads the PM2 daemon the developer is running, so a gate list carrying
|
|
37
|
+
it quietly means "deploy". The runtime gates now run under `bun run
|
|
38
|
+
acceptance:local`, which creates a deployment of its own — separate
|
|
39
|
+
`PM2_HOME`, ephemeral ports, its own public-host allowlist, its own
|
|
40
|
+
database — and destroys it by naming the declared roles. Neither guide says
|
|
41
|
+
`pm2 delete all` any more: it empties whichever daemon it is pointed at,
|
|
42
|
+
including applications that have nothing to do with the project.
|
|
43
|
+
- **The acceptance gate writes only to a database of its own.** It inherited
|
|
44
|
+
`DATABASE_URL`, and the runtime gates write — the repository example's smoke
|
|
45
|
+
posts `/api/repository/refresh` twice, which upserts. So a command a developer
|
|
46
|
+
is told to run before handing work off wrote rows into whatever `.env` named.
|
|
47
|
+
It runs against `ACCEPTANCE_DATABASE_URL`, applies the declared migrations
|
|
48
|
+
there, and refuses to start when that variable is unset or reuses the
|
|
49
|
+
deployment's database **name** — with the line to paste in the refusal. The
|
|
50
|
+
name alone decides, because a hostname is not proof of a different server:
|
|
51
|
+
`localhost` and `127.0.0.1` are one, and so are two DNS names for the same
|
|
52
|
+
PostgreSQL. The deployment's URL is not in the harness's child environment at
|
|
53
|
+
all.
|
|
54
|
+
- **A release will not start an artifact that is not this source's.**
|
|
55
|
+
`assertBuildArtifacts()` checked that the declared paths exist, and existence
|
|
56
|
+
is not freshness: `pm2:prod` run without a build applied this source's
|
|
57
|
+
migrations and started the previous source's `dist` and `.next`, moving the
|
|
58
|
+
schema ahead of the code in the one direction a code rollback does not undo.
|
|
59
|
+
`bun run build` now leaves a digest of the source it read, and the release
|
|
60
|
+
refuses when the tree no longer hashes to it — a digest rather than a
|
|
61
|
+
timestamp, because a checkout rewrites every mtime and a formatter rewrites
|
|
62
|
+
some for no change at all. Everything is source unless something names it
|
|
63
|
+
otherwise, and the something is the **declaration**: the digest skips
|
|
64
|
+
`build.artifacts`, `node_modules`/`.git`, and runtime state. It no longer
|
|
65
|
+
skips by kind — every `.md`, every test file, every directory called
|
|
66
|
+
`generated` — because a project that imports MDX or keeps checked-in source in
|
|
67
|
+
a directory of that name would have changed its content, kept its digest, and
|
|
68
|
+
been told a stale artifact was current. `.env` stays out on purpose: a binding
|
|
69
|
+
is not an input to this build, and hashing it would refuse a correct artifact
|
|
70
|
+
whenever a deployment edited its own environment.
|
|
71
|
+
- **The shutdown budget is an upper bound again.** `terminationBudgetMs` adds a
|
|
72
|
+
fixed cleanup allowance to the drain floor and refuses a supervision policy
|
|
73
|
+
that allows less — but the closes that run after the drain (the MCP session,
|
|
74
|
+
the database pool) had no deadline of their own, so a hung close ran past the
|
|
75
|
+
very kill timeout the budget had approved and turned an orderly shutdown into
|
|
76
|
+
the SIGKILL that runs no cleanup at all. The role's cleanup now shares one
|
|
77
|
+
bounded budget with the generator, from one constant both read, and names
|
|
78
|
+
whatever it stopped waiting for — and then **ends the process**. Setting
|
|
79
|
+
`process.exitCode` only decides the code a process reports when it exits, and
|
|
80
|
+
a step that ran out of time is usually still holding the handle that stops it
|
|
81
|
+
from exiting; a close that *threw* is reported with its cause and is no longer
|
|
82
|
+
counted as a clean shutdown. The shared deadline is measured with
|
|
83
|
+
`performance.now()` and the clock can no longer be injected: a wall clock
|
|
84
|
+
stepped backwards widens the very upper bound the supervisor's kill timeout
|
|
85
|
+
was derived from, and a frozen injected clock hands every step a full budget.
|
|
86
|
+
- **The project declaration stays out of the browser bundle.**
|
|
87
|
+
`lib/seo/pages.ts` imported `appDeclaration` for one field, and a client
|
|
88
|
+
component imports that module — so role commands, working directories,
|
|
89
|
+
artifact and migration paths and every environment variable name travelled
|
|
90
|
+
into the client graph along with the Zod parser. It reads `appIdentity` now,
|
|
91
|
+
and a test walks every `'use client'` graph and fails if one reaches the
|
|
92
|
+
declaration — by resolving each specifier to a file rather than matching a
|
|
93
|
+
string, so a barrel re-export, a relative path into the config package and a
|
|
94
|
+
double-quoted import are all caught.
|
|
95
|
+
- **A duplicated host is one address.** The portability check counted the
|
|
96
|
+
entries of `PUBLIC_WEB_HOSTS` without deduplicating them, so the same host
|
|
97
|
+
written twice passed as two addresses and the proof compared the deployment
|
|
98
|
+
with itself.
|
|
99
|
+
- **Every dial in the runtime smoke is bounded.** An endpoint that accepts the
|
|
100
|
+
connection and never answers used to hang the gate with no output and no
|
|
101
|
+
deadline instead of failing it.
|
|
102
|
+
- **A build output cannot be published inside the template.** What the scaffolder
|
|
103
|
+
copies and what npm publishes were two lists that had to agree, and only one of
|
|
104
|
+
them was consulted when a name was excluded. The exclusions are data now, and a
|
|
105
|
+
test fails until the package manifest carries the same negation.
|
|
106
|
+
- **A role bound to an IPv6 address gets a readiness URL that parses.**
|
|
107
|
+
`BIND_HOST=::1` produced `http://::1:3211/health`, which is not an address
|
|
108
|
+
with a port and which `fetch` refuses — so the wait failed on the spelling
|
|
109
|
+
rather than on the role. The literal is bracketed.
|
|
110
|
+
- **`bun run dev` and `bun run pm2:prod` report the roles as running only once
|
|
111
|
+
they answer.** A supervisor returns at the spawn, seconds before a role
|
|
112
|
+
listens, so both printed their address at a moment when nothing was there and
|
|
113
|
+
every command after them raced the application they had just started. Both now
|
|
114
|
+
wait on each role's declared `readinessPath`.
|
|
115
|
+
- **`runtime:smoke` asks the deployment on the addresses it claims.** The
|
|
116
|
+
portability check carried two fixture hosts of its own, so it only ever passed
|
|
117
|
+
where somebody had put those exact names into `PUBLIC_WEB_HOSTS` — which only
|
|
118
|
+
the packed lane had. Everyone else got a bare 500 from a policy working as
|
|
119
|
+
designed. It now reads `PUBLIC_WEB_HOSTS`, and a deployment with too few
|
|
120
|
+
addresses to compare is told which line to add instead of being refused a
|
|
121
|
+
request.
|
|
122
|
+
- **A closed deployment is diagnosed, not reset.** `runtime:smoke` says what is
|
|
123
|
+
not listening and which command starts it, before the first check runs.
|
|
124
|
+
- **The theme value the toaster reads narrows again.** `@wrksz/themes` 1.2
|
|
125
|
+
changed `useThemeValue`'s type parameter to describe the map rather than the
|
|
126
|
+
value, and it now infers `const` — so naming the value union at the call site
|
|
127
|
+
widened the result to every member of `string` and the generated project
|
|
128
|
+
stopped type-checking. The call site lets inference do it.
|
|
129
|
+
- **A green `runtime:smoke` prints one line.** The MCP surface check asked the
|
|
130
|
+
server to list tools even when it advertised no tool capability, which made
|
|
131
|
+
the vendor client log a debug warning on every successful run. It reads the
|
|
132
|
+
advertised capability instead.
|
|
133
|
+
|
|
134
|
+
### Changed
|
|
135
|
+
|
|
136
|
+
- **Every dependency of the generated project is on its latest release.**
|
|
137
|
+
Next 16.3.2, `next-intl` 4.13.7, `@tanstack/react-query` 5.102.3,
|
|
138
|
+
`@tanstack/react-table` 9.1.2, `framer-motion` 13.1.1, `@wrksz/themes` 1.2.0,
|
|
139
|
+
`shiki` 4.4.3, `sonner` 2.0.8, `ai` 7.0.78, `pg` 8.23.0, Playwright 1.62.1,
|
|
140
|
+
Biome 2.5.10 and the `@types/*` that go with them. No major crossed; the
|
|
141
|
+
Stitchkit range is unchanged and still declared once, in the catalog.
|
|
142
|
+
- **The generated project imports the declaration schema instead of mirroring
|
|
143
|
+
it.** `packages/config/src/declaration.ts` now reads
|
|
144
|
+
`parseProjectDeclaration` from `stitchkit/declaration`, and the 611-line
|
|
145
|
+
generated copy — `packages/config/src/project-declaration.generated.ts` — is
|
|
146
|
+
gone with the script that maintained it. The copy existed only because the
|
|
147
|
+
entrypoint was not on npm yet; the template's catalog targets `^0.60.0`,
|
|
148
|
+
which publishes it, so "one schema, three readers" is now literally true.
|
|
149
|
+
Adopting it is one import and one deletion — see
|
|
150
|
+
[`UPGRADING.md`](./UPGRADING.md).
|
|
151
|
+
- **`scripts/local-env.ts` reads the identity module, not the declaration.** It
|
|
152
|
+
needs one slug, and a project scaffolded with `--no-install` renders its
|
|
153
|
+
`.env` before anything is installed — a script that reaches for the
|
|
154
|
+
framework's schema to read a name cannot run in that window.
|
|
155
|
+
|
|
15
156
|
## [0.4.0] — 2026-08-25
|
|
16
157
|
|
|
17
158
|
### ⚠️ Breaking changes
|
package/UPGRADING.md
CHANGED
|
@@ -60,6 +60,123 @@ the first scaffolder release with a migration channel of its own.
|
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
+
## Released migration: 0.4.1
|
|
64
|
+
|
|
65
|
+
### a release refuses a stale artifact, and cleanup is bounded
|
|
66
|
+
|
|
67
|
+
Two changes an existing project should take, both about a shutdown or a start
|
|
68
|
+
that looked safe and was not.
|
|
69
|
+
|
|
70
|
+
1. **Stamp the build.** Copy `scripts/build-stamp.ts` from a fresh scaffold,
|
|
71
|
+
append `&& bun scripts/build-stamp.ts` to your root `build` script, add
|
|
72
|
+
`.build-stamp.json` to `.gitignore`, and call `assertArtifactMatchesSource()`
|
|
73
|
+
at the end of `assertBuildArtifacts()` in `scripts/release-steps.ts`. Until
|
|
74
|
+
you do, `bun run pm2:prod` without a build applies your migrations and starts
|
|
75
|
+
the previous build.
|
|
76
|
+
|
|
77
|
+
2. **Bound the cleanup, and let it end the process.** Copy
|
|
78
|
+
`packages/config/src/shutdown.ts` and `packages/backend/src/cleanup.ts`, add
|
|
79
|
+
the `./shutdown` export to `packages/config/package.json`, and replace the
|
|
80
|
+
bare `await mcp.close(); await prisma.$disconnect();` in your API role's
|
|
81
|
+
`onComplete` with `closeWithinBudget([...])` followed by
|
|
82
|
+
`concludeShutdown(cleanup, result.outcome === 'clean')`. Then have
|
|
83
|
+
`scripts/declaration.ts` import `FORCE_BUDGET_MS` and `CLEANUP_BUDGET_MS`
|
|
84
|
+
from the new module instead of declaring its own copies — the number a
|
|
85
|
+
supervisor is told to allow and the number the role enforces have to be one
|
|
86
|
+
number. `concludeShutdown` is the half that makes the budget real: setting
|
|
87
|
+
`process.exitCode` decides the code a process reports *when it exits*, and a
|
|
88
|
+
step that ran out of time is usually still holding the handle that stops it
|
|
89
|
+
from exiting at all.
|
|
90
|
+
|
|
91
|
+
No operator step: nothing about a running machine changes.
|
|
92
|
+
|
|
93
|
+
### the gate list runs, and never deploys
|
|
94
|
+
|
|
95
|
+
The `Gates` list in the generated guides presented five commands in a row, two
|
|
96
|
+
of which check a **running** deployment — with nothing in the list that starts
|
|
97
|
+
one. The fix is not to add a deploy command: `bun run pm2:prod` applies your
|
|
98
|
+
declared migrations and reloads the deployment you are running, so a list that
|
|
99
|
+
contains it means "run these before handing work off" quietly says "deploy".
|
|
100
|
+
|
|
101
|
+
1. **Take `pm2:prod` out of your gate list**, in `README.md` and `AGENTS.md`
|
|
102
|
+
alike, and delete any advice to run `pm2 delete all` — that empties whichever
|
|
103
|
+
daemon it is pointed at, including applications with nothing to do with this
|
|
104
|
+
project.
|
|
105
|
+
|
|
106
|
+
2. **Add the harness that brings up a deployment of its own.** Copy
|
|
107
|
+
`scripts/acceptance-local.ts` and `scripts/acceptance-database.ts` from a
|
|
108
|
+
fresh scaffold and add `"acceptance:local": "bun scripts/acceptance-local.ts"`
|
|
109
|
+
to your root scripts. It creates and destroys its own deployment — separate
|
|
110
|
+
`PM2_HOME`, ephemeral ports, its own public-host allowlist — and runs
|
|
111
|
+
`runtime:smoke` and `e2e` against that.
|
|
112
|
+
|
|
113
|
+
3. **Give it a database of its own.** Add `ACCEPTANCE_DATABASE_URL` to `.env`
|
|
114
|
+
and `.env.example`, naming a throwaway database — not the one `DATABASE_URL`
|
|
115
|
+
names. The runtime gates WRITE (the repository example's smoke posts a
|
|
116
|
+
refresh, and that upserts), so a harness borrowing `DATABASE_URL` writes rows
|
|
117
|
+
into whatever your `.env` points at. The harness refuses to start when the
|
|
118
|
+
variable is unset or names the same database, and prints the line to add.
|
|
119
|
+
|
|
120
|
+
4. **Wait for readiness before reporting it.** Copy `scripts/readiness.ts` from
|
|
121
|
+
a fresh scaffold, then in `scripts/dev.ts` and `scripts/release.ts` await
|
|
122
|
+
`awaitRolesAnswering(declaredRoleReadiness(appDeclaration, environment))`
|
|
123
|
+
before printing that the roles are running. Until you do, anything you run
|
|
124
|
+
after `bun run dev` or `bun run pm2:prod` races the roles they started.
|
|
125
|
+
|
|
126
|
+
5. **Let the smoke read your allowlist.** In `scripts/web-surface-smoke.ts` the
|
|
127
|
+
portability check named two hosts of its own; take the version that reads
|
|
128
|
+
`PUBLIC_WEB_HOSTS` and pass it from `scripts/runtime-smoke.ts`. If you kept
|
|
129
|
+
the old one, your `.env` must claim `alpha.example` and `beta.example:8443`
|
|
130
|
+
or the check fails on a host your deployment correctly refuses. With the new
|
|
131
|
+
one, `_env.example` drops those example hosts — the harness supplies its own.
|
|
132
|
+
|
|
133
|
+
6. **Keep the declaration out of the browser.**
|
|
134
|
+
`packages/frontend/src/lib/seo/pages.ts` must import `appIdentity` from
|
|
135
|
+
`@app/config/app-identity` rather than `appDeclaration`: a client component
|
|
136
|
+
reaches that module, so the whole declaration was going into your browser
|
|
137
|
+
bundle.
|
|
138
|
+
|
|
139
|
+
No operator step: nothing about a running machine changes.
|
|
140
|
+
|
|
141
|
+
### the declaration schema is imported
|
|
142
|
+
|
|
143
|
+
For a project generated **before** the scaffolder version that drops the schema
|
|
144
|
+
mirror. Nothing in your tree stops working if you skip this — the copy keeps
|
|
145
|
+
parsing. What you lose by skipping is the guarantee: your copy no longer moves
|
|
146
|
+
when the framework's schema does, and nothing tells you.
|
|
147
|
+
|
|
148
|
+
1. **Point the config package at the framework.** In
|
|
149
|
+
`packages/config/src/declaration.ts`:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// before
|
|
153
|
+
import { findProjectRole, parseProjectDeclaration } from './project-declaration.generated';
|
|
154
|
+
// after
|
|
155
|
+
import { findProjectRole, parseProjectDeclaration } from 'stitchkit/declaration';
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Then the same for every `import type { ProjectDeclaration, … }` in
|
|
159
|
+
`scripts/` — they pointed at the same file.
|
|
160
|
+
|
|
161
|
+
2. **Declare the dependency.** `packages/config/package.json` gains
|
|
162
|
+
`"stitchkit": "catalog:"`, and `bun install` refreshes the lockfile.
|
|
163
|
+
|
|
164
|
+
3. **Delete `packages/config/src/project-declaration.generated.ts`.**
|
|
165
|
+
|
|
166
|
+
4. **Check your `stitchkit` range.** The entrypoint ships from **0.60.0**. If
|
|
167
|
+
your catalog targets less than that, raise it first — otherwise the import
|
|
168
|
+
resolves to nothing.
|
|
169
|
+
|
|
170
|
+
5. **Read `.env` without the schema.** If your `scripts/local-env.ts` imports
|
|
171
|
+
`appDeclaration`, switch it to `appIdentity` from
|
|
172
|
+
`packages/config/src/app-identity.generated`. It needs only the slug, and a
|
|
173
|
+
project scaffolded with `--no-install` renders `.env` before anything is
|
|
174
|
+
installed — in that window the framework is not there to import.
|
|
175
|
+
|
|
176
|
+
No operator step: nothing about a running machine changes.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
63
180
|
## Released migration: 0.4.0
|
|
64
181
|
|
|
65
182
|
### the project declares itself
|
package/dist/cli.js
CHANGED
|
@@ -292,6 +292,8 @@ var IGNORED_DIRECTORIES = new Set([
|
|
|
292
292
|
"playwright-report",
|
|
293
293
|
"test-results"
|
|
294
294
|
]);
|
|
295
|
+
var IGNORED_FILE_NAMES = new Set([".env", ".build-stamp.json", "next-env.d.ts"]);
|
|
296
|
+
var IGNORED_FILE_SUFFIXES = [".log", ".tsbuildinfo"];
|
|
295
297
|
function isTemplateSourcePathIncluded(sourcePath) {
|
|
296
298
|
const normalized = sourcePath.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
297
299
|
if (!normalized)
|
|
@@ -302,7 +304,9 @@ function isTemplateSourcePathIncluded(sourcePath) {
|
|
|
302
304
|
if (normalized === "packages/db/src/generated" || normalized.startsWith("packages/db/src/generated/"))
|
|
303
305
|
return false;
|
|
304
306
|
const name = basename2(normalized);
|
|
305
|
-
|
|
307
|
+
if (IGNORED_FILE_NAMES.has(name))
|
|
308
|
+
return false;
|
|
309
|
+
return !IGNORED_FILE_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
306
310
|
}
|
|
307
311
|
function shouldIncludeTemplatePath(templateDirectory, sourcePath) {
|
|
308
312
|
return isTemplateSourcePathIncluded(relative(templateDirectory, sourcePath));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RepositorySnapshotSchema, repositoryRealtimeContract } from '@app/shared';
|
|
2
2
|
import { createRealtimeClient, defineRealtimeContract } from 'stitchkit';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import { assertDeploymentIsAnswering } from './deployment-preflight';
|
|
4
5
|
import { defineSurfaceProbe, runSurfaceConformance } from './surface-conformance';
|
|
5
6
|
import { loadToolingEnv } from './tooling-env';
|
|
6
7
|
import { assertArtifactIsPlacementFree, assertPublicWebSurface } from './web-surface-smoke';
|
|
@@ -8,8 +9,16 @@ import { assertArtifactIsPlacementFree, assertPublicWebSurface } from './web-sur
|
|
|
8
9
|
const toolingEnv = loadToolingEnv();
|
|
9
10
|
const apiOrigin = toolingEnv.SMOKE_API_ORIGIN;
|
|
10
11
|
|
|
12
|
+
await assertDeploymentIsAnswering({
|
|
13
|
+
'the API role': apiOrigin,
|
|
14
|
+
'the web role': toolingEnv.SMOKE_WEB_ORIGIN,
|
|
15
|
+
});
|
|
16
|
+
|
|
11
17
|
async function json(path: string, init?: RequestInit): Promise<unknown> {
|
|
12
|
-
const response = await fetch(`${apiOrigin}${path}`,
|
|
18
|
+
const response = await fetch(`${apiOrigin}${path}`, {
|
|
19
|
+
...init,
|
|
20
|
+
signal: AbortSignal.timeout(30_000),
|
|
21
|
+
});
|
|
13
22
|
if (!response.ok)
|
|
14
23
|
throw new Error(`${init?.method ?? 'GET'} ${path} returned ${response.status}`);
|
|
15
24
|
return response.json();
|
|
@@ -185,7 +194,10 @@ await runSurfaceConformance({
|
|
|
185
194
|
}
|
|
186
195
|
|
|
187
196
|
await assertPublicWebSurface(toolingEnv.SMOKE_WEB_ORIGIN);
|
|
188
|
-
await assertArtifactIsPlacementFree(toolingEnv.SMOKE_WEB_ORIGIN
|
|
197
|
+
await assertArtifactIsPlacementFree(toolingEnv.SMOKE_WEB_ORIGIN, {
|
|
198
|
+
origin: toolingEnv.PUBLIC_WEB_ORIGIN,
|
|
199
|
+
hosts: toolingEnv.PUBLIC_WEB_HOSTS,
|
|
200
|
+
});
|
|
189
201
|
|
|
190
202
|
console.log(
|
|
191
203
|
'Runtime HTTP (same-origin and direct), OpenAPI, Socket.IO, MCP and public web smoke passed',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-stitchkit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Create a production-shaped Stitchkit application",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Max Listov <maxlistov@gmail.com>",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"template/**/*",
|
|
19
19
|
"examples/**/*",
|
|
20
20
|
"!template/**/.env",
|
|
21
|
+
"!template/**/.build-stamp.json",
|
|
21
22
|
"!template/**/node_modules/**",
|
|
22
23
|
"!template/**/.next/**",
|
|
23
24
|
"!template/**/dist/**",
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
"!template/**/*.tsbuildinfo",
|
|
30
31
|
"!template/**/coverage/**",
|
|
31
32
|
"!examples/**/.env",
|
|
33
|
+
"!examples/**/.build-stamp.json",
|
|
32
34
|
"!examples/**/node_modules/**",
|
|
33
35
|
"!examples/**/.next/**",
|
|
34
36
|
"!examples/**/dist/**",
|
|
@@ -57,7 +59,7 @@
|
|
|
57
59
|
"zod": "^4.4.3"
|
|
58
60
|
},
|
|
59
61
|
"devDependencies": {
|
|
60
|
-
"@types/bun": "^1.
|
|
62
|
+
"@types/bun": "^1.4.0",
|
|
61
63
|
"typescript": "^7.0.2"
|
|
62
64
|
},
|
|
63
65
|
"engines": {
|
package/template/AGENTS.md
CHANGED
|
@@ -19,10 +19,11 @@ framework source repository.
|
|
|
19
19
|
by machine — the scaffolder stamps the identity, `bun run gen:declaration`
|
|
20
20
|
derives `env.variables` — so the formatter leaves it alone and
|
|
21
21
|
`scripts/declaration.test.ts` is what checks it.
|
|
22
|
-
-
|
|
23
|
-
`ecosystem.config.cjs`, `ecosystem.dev.config.cjs
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
- Four things are generated from it and must not be hand-edited:
|
|
23
|
+
`ecosystem.config.cjs`, `ecosystem.dev.config.cjs`,
|
|
24
|
+
`packages/config/src/app-identity.generated.ts` and the `env.variables` block
|
|
25
|
+
of `project.json`. Run `bun run gen:declaration` after changing a role. It
|
|
26
|
+
holds nothing that differs between two deployments; those are named there by
|
|
26
27
|
variable and supplied by the place.
|
|
27
28
|
|
|
28
29
|
Dependencies point inward: frontend/backend → shared; backend → db/config.
|
|
@@ -58,6 +59,15 @@ vertical path. Before handing work off, run:
|
|
|
58
59
|
bun run check
|
|
59
60
|
bun run test
|
|
60
61
|
bun run build
|
|
61
|
-
bun run
|
|
62
|
+
bun run acceptance:local
|
|
62
63
|
```
|
|
63
64
|
|
|
65
|
+
`acceptance:local` is part of the list because `runtime:smoke` and `e2e` check a
|
|
66
|
+
running deployment: it creates one of its own — separate PM2 home, ephemeral
|
|
67
|
+
ports, and its own database from `ACCEPTANCE_DATABASE_URL` — runs both against
|
|
68
|
+
it, and destroys it. The separate database is not tidiness: the gates write, so
|
|
69
|
+
one borrowing `DATABASE_URL` writes rows wherever `.env` points. **Never put
|
|
70
|
+
`pm2:prod` in this list.** It applies the declared migrations to *your* database
|
|
71
|
+
and reloads the running deployment; deploying is its own command, asked for on
|
|
72
|
+
purpose, and no gate performs it.
|
|
73
|
+
|
package/template/README.md
CHANGED
|
@@ -9,14 +9,17 @@ before it starts, and the environment variables a deployment must supply.
|
|
|
9
9
|
The declaration is true **with no machine in existence**. A field you cannot
|
|
10
10
|
fill in without knowing where the code will run is a *binding*, not a
|
|
11
11
|
declaration: ports, hosts, addresses, machine paths and supervision policy are
|
|
12
|
-
named there by variable and never by value
|
|
13
|
-
|
|
12
|
+
named there by variable and never by value. The schema has no field that asks
|
|
13
|
+
for one, so nothing in it ever requires a value of the place; where a value
|
|
14
|
+
could still be written into a free-text field, a filter refuses the known
|
|
15
|
+
shapes of a machine name. Change the slug, display name, version or description there and package
|
|
14
16
|
names, process names, MCP/OpenAPI identity, UI copy and SEO follow.
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
`ecosystem.dev.config.cjs
|
|
18
|
-
|
|
19
|
-
refuses a stale
|
|
18
|
+
Four things are **generated** from it — `ecosystem.config.cjs`,
|
|
19
|
+
`ecosystem.dev.config.cjs`, `packages/config/src/app-identity.generated.ts` and
|
|
20
|
+
the `env.variables` block of the declaration itself. Run
|
|
21
|
+
`bun run gen:declaration` after changing a role; the test suite refuses a stale
|
|
22
|
+
copy.
|
|
20
23
|
|
|
21
24
|
## Start
|
|
22
25
|
|
|
@@ -87,10 +90,45 @@ Unsupported browsers and users requesting reduced motion switch immediately.
|
|
|
87
90
|
bun run check
|
|
88
91
|
bun run test
|
|
89
92
|
bun run build
|
|
90
|
-
bun run
|
|
91
|
-
bun run e2e
|
|
93
|
+
bun run acceptance:local
|
|
92
94
|
```
|
|
93
95
|
|
|
96
|
+
Top to bottom, in one terminal, and none of it touches a deployment. They
|
|
97
|
+
assume your development database is already set up — see [Start](#start).
|
|
98
|
+
|
|
99
|
+
`acceptance:local` is the last one because `runtime:smoke` and `e2e` check a
|
|
100
|
+
**running** deployment rather than a source tree — so it creates one and
|
|
101
|
+
destroys it: its own PM2 home, ephemeral ports, its own public-host allowlist,
|
|
102
|
+
and a stop that names the roles the declaration declares. It reloads nothing
|
|
103
|
+
you are running.
|
|
104
|
+
|
|
105
|
+
It also brings its own database, `ACCEPTANCE_DATABASE_URL`, and applies this
|
|
106
|
+
project's migrations to that — not to yours. The gates WRITE (the repository
|
|
107
|
+
example's smoke posts a refresh, and that upserts), so borrowing `DATABASE_URL`
|
|
108
|
+
would make a gate a writer in whatever database your `.env` names. It refuses to
|
|
109
|
+
start when the variable is unset or names the same database, and says what to
|
|
110
|
+
add. It needs `pm2` (see [Requirements](#requirements)).
|
|
111
|
+
|
|
112
|
+
Deploying is a separate, deliberate command — `bun run pm2:prod` under
|
|
113
|
+
[Production](#production) — and it is not a gate.
|
|
114
|
+
|
|
115
|
+
To run the two runtime gates against a deployment that already exists somewhere,
|
|
116
|
+
point `SMOKE_API_ORIGIN` / `SMOKE_WEB_ORIGIN` at it and call `bun run
|
|
117
|
+
runtime:smoke` / `bun run e2e` directly. `runtime:smoke` asks the web role to
|
|
118
|
+
answer as two of the addresses `PUBLIC_WEB_HOSTS` claims, to prove one artifact
|
|
119
|
+
serves many; a deployment claiming fewer than two addresses besides the one
|
|
120
|
+
being dialled is told exactly what to add rather than failing on a refused host.
|
|
121
|
+
|
|
122
|
+
## Requirements
|
|
123
|
+
|
|
124
|
+
- **Bun** and **Node ≥ 22**.
|
|
125
|
+
- **PostgreSQL** — external infrastructure, in development and production
|
|
126
|
+
alike. This project owns its schema and migrations, never the database
|
|
127
|
+
process.
|
|
128
|
+
- **PM2** on `PATH` (`bun add --global pm2`) for `bun run dev`,
|
|
129
|
+
`bun run acceptance:local` and `bun run pm2:prod`.
|
|
130
|
+
- **Playwright browsers** (`bunx playwright install`) for `bun run e2e`.
|
|
131
|
+
|
|
94
132
|
## Production
|
|
95
133
|
|
|
96
134
|
Provide a production `.env`, then:
|
package/template/_env.example
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
NODE_ENV=development
|
|
2
2
|
DATABASE_URL=postgresql://USER:PASSWORD@127.0.0.1:5432/stitchkit_starter
|
|
3
|
+
# The throwaway database `bun run acceptance:local` creates and writes to. The
|
|
4
|
+
# runtime gates WRITE, so they get one of their own: the harness refuses to
|
|
5
|
+
# start if this is unset or names the database above.
|
|
6
|
+
ACCEPTANCE_DATABASE_URL=postgresql://USER:PASSWORD@127.0.0.1:5432/stitchkit_starter_acceptance
|
|
3
7
|
# 0.0.0.0 exposes the app to every network interface — opt in consciously.
|
|
4
8
|
BIND_HOST=127.0.0.1
|
|
5
9
|
API_PORT=3211
|
|
@@ -9,6 +13,10 @@ SMOKE_WEB_ORIGIN=http://127.0.0.1:3210
|
|
|
9
13
|
# Hosts this deployment answers for, comma-separated. One built artifact can
|
|
10
14
|
# serve several addresses; a forwarded host outside this list is refused rather
|
|
11
15
|
# than believed. Leave unset and set PUBLIC_WEB_ORIGIN instead for a single one.
|
|
16
|
+
#
|
|
17
|
+
# `bun run acceptance:local` supplies its own list to the deployment it creates,
|
|
18
|
+
# so the addresses the portability check needs are not policy this project
|
|
19
|
+
# carries. List here only the hosts this deployment really answers for.
|
|
12
20
|
PUBLIC_WEB_HOSTS=127.0.0.1:3210
|
|
13
21
|
LOG_FORMAT=pretty
|
|
14
22
|
# CORS_ORIGIN is only needed for a genuinely cross-origin browser.
|
package/template/_gitignore
CHANGED
package/template/biome.json
CHANGED