From d798ee9a069677fcf03175f044d88fbfc417e1c7 Mon Sep 17 00:00:00 2001 From: cavewebs Date: Mon, 20 Apr 2026 18:01:53 +0100 Subject: [PATCH 1/2] feat(cf): patch wrangler config from env vars at deploy time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Template needs to stay multi-tenant — you can't commit D1/KV ids into a starter that other people also deploy. Instead: each deployer sets their binding ids as environment variables on their Worker project, and a small build-time script injects them into the adapter-generated dist/server/wrangler.json before wrangler deploy reads it. - scripts/patch-wrangler-config.mjs reads CF_D1_DATABASE_ID, CF_KV_SESSION_ID, CF_R2_BUCKET, CF_R2_PUBLIC_URL from env and writes them into d1_databases[].database_id / kv_namespaces[].id / r2_buckets[].bucket_name in dist/server/wrangler.json. Runs chained into build:cf so the output artifact is always deploy-ready. - build:cf now: 'DEPLOY_TARGET=cloudflare astro build && node scripts/patch-wrangler-config.mjs' - README Cloudflare section rewritten: explicit env var table + deploy-command guidance for the CF dashboard. Verified locally: CF_D1_DATABASE_ID=test-uuid CF_KV_SESSION_ID=test-kv bun run build:cf emits dist/server/wrangler.json with those ids patched in. Empty env vars → patch script warns + leaves placeholders in, so the failure mode is loud. --- packages/starter/README.md | 37 ++++++-- packages/starter/package.json | 2 +- .../starter/scripts/patch-wrangler-config.mjs | 84 +++++++++++++++++++ 3 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 packages/starter/scripts/patch-wrangler-config.mjs diff --git a/packages/starter/README.md b/packages/starter/README.md index 8f16646..2e09973 100644 --- a/packages/starter/README.md +++ b/packages/starter/README.md @@ -104,16 +104,43 @@ When you're ready to ship, the starter builds for three targets from one codebas ### Cloudflare Workers (D1 + R2) -Runs on `@astrojs/cloudflare` + `@emdash-cms/cloudflare`. After clicking the button you'll still need to provision resources, wire secrets, and seed the DB. Locally: +Runs on `@astrojs/cloudflare` + `@emdash-cms/cloudflare`. The starter stays multi-tenant — you bring your own D1/KV/R2 ids and pass them in as Worker **environment variables** at deploy time. A build-time patch script wires them into the adapter-generated config right before `wrangler deploy`. + +**One-time resource provisioning:** ```sh -wrangler d1 create dashcommerce-demo # paste database_id into wrangler.jsonc +wrangler d1 create dashcommerce-demo # copy the database_id from output wrangler r2 bucket create dashcommerce-demo-media -wrangler kv namespace create SESSION # paste id into wrangler.jsonc +wrangler kv namespace create SESSION # copy the namespace id from output openssl rand -hex 32 | wrangler secret put EMDASH_AUTH_SECRET openssl rand -hex 32 | wrangler secret put EMDASH_PREVIEW_SECRET -bun run cf:d1:seed # dumps local SQLite → applies to D1 -bun run cf:deploy +``` + +**Set these env vars on the Worker project** (Cloudflare dashboard → Settings → Variables): + +| Variable | From | +|---|---| +| `CF_D1_DATABASE_ID` | `wrangler d1 create` output | +| `CF_KV_SESSION_ID` | `wrangler kv namespace create` output | +| `CF_R2_BUCKET` | optional — overrides `dashcommerce-demo-media` | +| `CF_R2_PUBLIC_URL` | optional — public bucket URL for media | + +**Seed the D1 database** (from your laptop, one time): + +```sh +bun run cf:d1:seed # dumps local SQLite → applies to D1 +``` + +**Deploy** — either click the button or run locally: + +```sh +CF_D1_DATABASE_ID=… CF_KV_SESSION_ID=… bun run cf:deploy +``` + +On CF's hosted build, the dashboard's deploy command should be: + +``` +npx wrangler deploy --config dist/server/wrangler.json ``` D1 migrations must run via wrangler before deploy (no runtime DDL on Workers). diff --git a/packages/starter/package.json b/packages/starter/package.json index b1e0723..bd059bb 100644 --- a/packages/starter/package.json +++ b/packages/starter/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "astro dev", "build": "astro build", - "build:cf": "DEPLOY_TARGET=cloudflare astro build", + "build:cf": "DEPLOY_TARGET=cloudflare astro build && node scripts/patch-wrangler-config.mjs", "preview": "astro preview", "start": "node ./dist/server/entry.mjs", "seed": "emdash seed --on-conflict=update", diff --git a/packages/starter/scripts/patch-wrangler-config.mjs b/packages/starter/scripts/patch-wrangler-config.mjs new file mode 100644 index 0000000..1027a10 --- /dev/null +++ b/packages/starter/scripts/patch-wrangler-config.mjs @@ -0,0 +1,84 @@ +#!/usr/bin/env node +/** + * Patch dist/server/wrangler.json with deployer-supplied binding ids. + * + * Why: wrangler.jsonc at the repo root ships without D1/KV ids so the starter + * stays multi-tenant (each deployer brings their own Cloudflare account). The + * Astro adapter copies the bindings as-is into dist/server/wrangler.json at + * build time. Without ids, `wrangler deploy` fails at the API layer with + * "binding X must have a valid id specified". + * + * This script reads the ids from env vars at deploy time and writes them into + * the generated config before `wrangler deploy` reads it. + * + * Expected env vars (set them on the Cloudflare Worker project): + * CF_D1_DATABASE_ID — UUID from `wrangler d1 create ` + * CF_KV_SESSION_ID — id from `wrangler kv namespace create SESSION` + * CF_R2_BUCKET — optional override; defaults to wrangler.jsonc value + * CF_R2_PUBLIC_URL — optional override; defaults to wrangler.jsonc value + * + * Runs as part of `build:cf`, so `wrangler deploy --config dist/server/wrangler.json` + * always sees a fully-resolved config. + */ + +import { existsSync, readFileSync, writeFileSync } from "node:fs"; + +const CONFIG_PATH = "dist/server/wrangler.json"; + +if (!existsSync(CONFIG_PATH)) { + console.error( + `[patch-wrangler] ${CONFIG_PATH} not found — run \`astro build\` with DEPLOY_TARGET=cloudflare first.`, + ); + process.exit(1); +} + +const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8")); +const patches = []; + +const d1Id = process.env.CF_D1_DATABASE_ID; +if (d1Id && Array.isArray(config.d1_databases)) { + for (const db of config.d1_databases) { + if (db.binding === "DB") { + db.database_id = d1Id; + patches.push(`d1.DB.database_id = ${d1Id.slice(0, 8)}…`); + } + } +} + +const kvId = process.env.CF_KV_SESSION_ID; +if (kvId && Array.isArray(config.kv_namespaces)) { + for (const ns of config.kv_namespaces) { + if (ns.binding === "SESSION") { + ns.id = kvId; + patches.push(`kv.SESSION.id = ${kvId.slice(0, 8)}…`); + } + } +} + +const r2Bucket = process.env.CF_R2_BUCKET; +if (r2Bucket && Array.isArray(config.r2_buckets)) { + for (const b of config.r2_buckets) { + if (b.binding === "MEDIA") { + b.bucket_name = r2Bucket; + patches.push(`r2.MEDIA.bucket_name = ${r2Bucket}`); + } + } +} + +const r2PublicUrl = process.env.CF_R2_PUBLIC_URL; +if (r2PublicUrl) { + // Baked into the bundle at build time via astro.config.mjs. Only informs + // the log — no config field to patch here since the value is already frozen + // into emitted JS at this point. + patches.push(`r2.publicUrl = ${r2PublicUrl} (compiled in)`); +} + +writeFileSync(CONFIG_PATH, JSON.stringify(config, null, "\t") + "\n"); + +if (patches.length === 0) { + console.warn( + "[patch-wrangler] No binding env vars set — deploy will likely fail with 'must have a valid id specified'. Set CF_D1_DATABASE_ID and CF_KV_SESSION_ID on the Worker project.", + ); +} else { + console.log(`[patch-wrangler] Patched: ${patches.join(", ")}`); +} From b11de2ee74ab69aaf048fad2f65eb22e837b5dbe Mon Sep 17 00:00:00 2001 From: cavewebs Date: Mon, 20 Apr 2026 18:12:14 +0100 Subject: [PATCH 2/2] fix(create): satisfy noUncheckedIndexedAccess in the CLI Three strict-null errors the monorepo root's tsconfig catches on CI but my local build didn't because the create package was still typecheck-clean against the older strict settings: - parseArgs: argv[i] is string | undefined under noUncheckedIndexedAccess. Added an early 'if (a === undefined) continue;' guard so the subsequent .startsWith() and equality checks see a narrowed string. - main: TEMPLATES[templateKey] is TemplateDef | undefined. Added an explicit error-out if the registry lookup misses (shouldn't happen in practice since --template is validated upstream and the prompts menu only offers registered keys, but closes the type hole and gives a clearer message than 'cannot read properties of undefined'). Verified: 'bun run --filter *' typecheck' clean across all three packages. --- packages/create/src/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/create/src/index.ts b/packages/create/src/index.ts index e5537aa..d968475 100644 --- a/packages/create/src/index.ts +++ b/packages/create/src/index.ts @@ -35,6 +35,7 @@ function parseArgs(argv: string[]): ParsedArgs { const args: ParsedArgs = {}; for (let i = 0; i < argv.length; i++) { const a = argv[i]; + if (a === undefined) continue; if (a === "-h" || a === "--help") { args.help = true; } else if (a === "-t" || a === "--template") { @@ -157,6 +158,14 @@ async function main(): Promise { const directory = parsed.directory ?? (response.directory as string); const templateKey = parsed.template ?? (response.template as string); const template = TEMPLATES[templateKey]; + if (!template) { + console.error( + pc.red( + `\n Template "${templateKey}" is not in the registry. Available: ${Object.keys(TEMPLATES).join(", ")}\n`, + ), + ); + process.exit(1); + } const targetDir = resolve(process.cwd(), directory); // Refuse to overwrite a non-empty target. Empty dirs are fine — users