create-substrat 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dev-servers.js +29 -0
- package/index.js +6 -1
- package/package.json +2 -1
- package/template/.claude/launch.json +15 -0
- package/template/.substrat/playbook.md +62 -5
package/dev-servers.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dev servers a scaffolded vertical runs — declared once, client-neutrally.
|
|
3
|
+
*
|
|
4
|
+
* `.claude/launch.json` is a Claude Desktop **adapter**: it lets the agent start
|
|
5
|
+
* the server, open it in the Browser pane, and verify its own changes. Under
|
|
6
|
+
* design/agent-surface.md §3 an adapter may route, describe and trigger but may
|
|
7
|
+
* never *hold* substance — so the topology lives here, in the `substrat` block of
|
|
8
|
+
* package.json that `substrat push` and the SessionStart hook already read, and
|
|
9
|
+
* the client file is emitted from it (`pnpm lint:launch`, guarded in CI).
|
|
10
|
+
*
|
|
11
|
+
* A port is deliberately NOT a field. `portEnv` + `portFrom` say *where* the port
|
|
12
|
+
* is bound; the number is read out of that file. Otherwise the declaration becomes
|
|
13
|
+
* a second copy of a number whose first copy is the one that actually runs, and
|
|
14
|
+
* with `autoPort: false` a stale copy is a hard boot failure — for an agent, mid
|
|
15
|
+
* session, which is the worst possible audience for it.
|
|
16
|
+
*
|
|
17
|
+
* @typedef {object} DevServer
|
|
18
|
+
* @property {string} name Entry name. Mirrors the `-n` label of the `dev` script.
|
|
19
|
+
* @property {string} run pnpm script to run. With `dir`, the script in that subdir.
|
|
20
|
+
* @property {string} [dir] Subdirectory of the project (a Vite app), if any.
|
|
21
|
+
* @property {string} portEnv The env var that moves this port (`PORT`, `WEB_PORT`, …).
|
|
22
|
+
* @property {string} portFrom Project-relative file binding it: `process.env.<portEnv> ?? N`.
|
|
23
|
+
* @property {Record<string,string>} [env] Env the `dev` script sets for this process.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** The template ships one process: the Hono API. There is no web app to scaffold yet. */
|
|
27
|
+
export const DEV_SERVERS = [
|
|
28
|
+
{ name: 'api', run: 'server', portEnv: 'PORT', portFrom: 'src/server.ts' },
|
|
29
|
+
];
|
package/index.js
CHANGED
|
@@ -16,6 +16,8 @@ import { cpSync, existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:
|
|
|
16
16
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
18
|
|
|
19
|
+
import { DEV_SERVERS } from './dev-servers.js';
|
|
20
|
+
|
|
19
21
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
20
22
|
const TEMPLATE = join(HERE, 'template');
|
|
21
23
|
|
|
@@ -73,7 +75,9 @@ function packageJson(name) {
|
|
|
73
75
|
// What `substrat push` reads: the permission surface (the registry the
|
|
74
76
|
// promotion checkpoint diffs) and the runtime needs the deploy config is
|
|
75
77
|
// derived from — you never author wrangler config (src/worker.ts is the
|
|
76
|
-
// entry; ScopeDO is the store it exports).
|
|
78
|
+
// entry; ScopeDO is the store it exports). Also the client-neutral home of
|
|
79
|
+
// the dev topology `.claude/launch.json` is emitted from — see
|
|
80
|
+
// dev-servers.js for why the port is read from the code and never declared.
|
|
77
81
|
substrat: {
|
|
78
82
|
permissions: 'src/provision.ts',
|
|
79
83
|
runtimeNeeds: {
|
|
@@ -84,6 +88,7 @@ function packageJson(name) {
|
|
|
84
88
|
{ binding: 'SWEEPER', class: 'SweeperDO' },
|
|
85
89
|
],
|
|
86
90
|
},
|
|
91
|
+
devServers: DEV_SERVERS,
|
|
87
92
|
},
|
|
88
93
|
scripts: {
|
|
89
94
|
dev: 'tsx watch src/server.ts',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-substrat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Scaffold a Substrat vertical — `npm create substrat <dir>`.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"index.js",
|
|
27
|
+
"dev-servers.js",
|
|
27
28
|
"template"
|
|
28
29
|
],
|
|
29
30
|
"engines": {
|
|
@@ -190,10 +190,21 @@ plain language, so nothing there is a surprise:
|
|
|
190
190
|
cross-tenant attacker gets nothing).
|
|
191
191
|
9. **Open decisions** — each with a **recommended default**, so the user chooses rather
|
|
192
192
|
than specifies:
|
|
193
|
-
- **Auth.** Local dev uses an `x-principal` header — a dev seam, not a login.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
- **Auth.** Local dev uses an `x-principal` header — a dev seam, not a login. Real auth
|
|
194
|
+
gates *exposing* the app, not *building* it — but if the app will be deployed for real
|
|
195
|
+
users, wire the OIDC seam from the start: the standard is a **separate OIDC issuer**
|
|
196
|
+
(an Auth Server app in the same team, or an external issuer — Supabase/Auth0/AuthHero/
|
|
197
|
+
Keycloak), never per-app credential storage. The vertical is a pure OIDC **relying
|
|
198
|
+
party**: depend on `@substrat-run/vertical-auth`, bind its `IdentityDO` (the
|
|
199
|
+
`sub → principal` directory, TOFU owner claim, invites) as a third DO store, read the
|
|
200
|
+
platform-delivered `substrat:auth` config per scope (`authWiring`), and build
|
|
201
|
+
`oidcRpAuthProvider` per request — see https://substrat.net/concepts/identity. The
|
|
202
|
+
dashboard's New-app **Identity** section does the automatic half (dynamic client
|
|
203
|
+
registration at the issuer + `/internal/configure` delivery) — but ONLY at app
|
|
204
|
+
creation, and only for verticals that implement this seam. An install created without
|
|
205
|
+
the identity choice stays `builtin`/unwired forever (switching issuers is deliberately
|
|
206
|
+
create-time only); a starter worker whose `authenticatedPrincipal` returns null answers
|
|
207
|
+
401 to everything deployed, however many auth servers exist in the team.
|
|
197
208
|
- **Deploy or stay local.** Local-first is a legitimate endpoint; default to it.
|
|
198
209
|
10. **Out of scope / deferred** — what you are deliberately not building, so the review is
|
|
199
210
|
about a bounded thing.
|
|
@@ -428,7 +439,9 @@ only in seed.ts would run locally and silently not deploy. The deploy path is th
|
|
|
428
439
|
authenticated CLI, and the author never holds a Cloudflare token:
|
|
429
440
|
|
|
430
441
|
- `substrat login` / `substrat whoami` — authenticate against the control plane.
|
|
431
|
-
- `substrat push` — push the vertical
|
|
442
|
+
- `substrat push` — push the vertical. By default the version is the registry's highest
|
|
443
|
+
semver, patch-bumped; `package.json`'s version is only a **seed for the first push of a
|
|
444
|
+
new slug**, and an explicit `--version` always wins. A **private** (tenant-owned)
|
|
432
445
|
vertical is admitted automatically; a **listed/shared** one waits for staff admission.
|
|
433
446
|
- `substrat promote <slug> --channel dev|staging|prod --version … [--ack-permissions]
|
|
434
447
|
[--ack-migrations]` — the owner promotes every channel, prod included, for their own
|
|
@@ -436,6 +449,50 @@ authenticated CLI, and the author never holds a Cloudflare token:
|
|
|
436
449
|
- `substrat hostnames bind <slug> --surface <s> [--domain <d>]` — mint a live hostname, or
|
|
437
450
|
record a custom domain pending DNS validation (`substrat hostnames verify`).
|
|
438
451
|
|
|
452
|
+
**A SPA ships as NATIVE assets — never inline it into the worker.** Declare it in
|
|
453
|
+
`runtimeNeeds` and `substrat push` builds, hashes, and uploads the directory to the
|
|
454
|
+
runtime's own asset store, served from the edge without invoking the worker:
|
|
455
|
+
|
|
456
|
+
```jsonc
|
|
457
|
+
"substrat": {
|
|
458
|
+
"runtimeNeeds": {
|
|
459
|
+
"entry": "src/worker.ts",
|
|
460
|
+
"build": "npm --prefix app install && npm --prefix app run build",
|
|
461
|
+
"assets": {
|
|
462
|
+
"directory": "app/dist",
|
|
463
|
+
"notFoundHandling": "single-page-application", // deep client routes → index.html
|
|
464
|
+
"runWorkerFirst": ["/api/*", "/internal/*"] // only these reach the worker
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
`build` runs before assets are collected, so the directory may be pure build output. Never
|
|
471
|
+
base64-inline a built `app/dist` into a generated worker module: it costs ~+33 % script
|
|
472
|
+
size and a worker invocation per image.
|
|
473
|
+
|
|
474
|
+
**Let changesets own the version, and pass it to push explicitly** — the default bump walks
|
|
475
|
+
the registry forward on its own, so `package.json` and the registry drift apart within a
|
|
476
|
+
few deploys. Set this up when the vertical first deploys:
|
|
477
|
+
|
|
478
|
+
```sh
|
|
479
|
+
pnpm add -D @changesets/cli && npx changeset init
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Then in `.changeset/config.json` add `"privatePackages": { "version": true, "tag": false }`
|
|
483
|
+
(the vertical is a private package, never npm-published), make sure `pnpm-workspace.yaml`
|
|
484
|
+
lists `packages: ["."]` so changesets can see the root package, and add the scripts:
|
|
485
|
+
|
|
486
|
+
```json
|
|
487
|
+
"changeset": "changeset",
|
|
488
|
+
"release": "pnpm test && pnpm typecheck && pnpm lint:boundaries && changeset version && substrat push --version $(node -p \"require('./package.json').version\") --promote prod"
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Read the version with `node -p` at that point in the script, not `$npm_package_version` —
|
|
492
|
+
the latter is captured before `changeset version` rewrites `package.json`, so it would push
|
|
493
|
+
the version you just replaced. Changesets needs a git repo with a commit on the base branch
|
|
494
|
+
(`git init -b main`) — a scaffold that isn't a repo yet must init before the first release.
|
|
495
|
+
|
|
439
496
|
Updates deploy **in place** from one stable script — data carries forward, migrations run
|
|
440
497
|
against prod data, backout is a time-boxed PITR rewind.
|
|
441
498
|
|