Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/create/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down Expand Up @@ -157,6 +158,14 @@ async function main(): Promise<void> {
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
Expand Down
37 changes: 32 additions & 5 deletions packages/starter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion packages/starter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
84 changes: 84 additions & 0 deletions packages/starter/scripts/patch-wrangler-config.mjs
Original file line number Diff line number Diff line change
@@ -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 <name>`
* 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(", ")}`);
}
Loading