@decocms/start 2.30.0 → 3.0.0

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/deploy/README.md CHANGED
@@ -1,85 +1,121 @@
1
- # `deploy/` — central deploy registry
1
+ # `deploy/` — central wrangler template
2
2
 
3
- This directory is the single source of truth for **what gets deployed where**
4
- across every storefront on the platform. It is consumed by the reusable GitHub
5
- workflows under [`.github/workflows/`](../.github/workflows/) (`deploy.yml`,
6
- `preview.yml`, `sync-secrets.yml`) and by the local `deco-wrangler` CLI.
3
+ This directory holds **`wrangler-template.jsonc`** the canonical wrangler config
4
+ that every storefront on the platform inherits. It is consumed by the reusable
5
+ GitHub workflows under [`.github/workflows/`](../.github/workflows/)
6
+ (`deploy.yml`, `preview.yml`, `sync-secrets.yml`) and by the local
7
+ `deco-wrangler` CLI.
7
8
 
8
- ## Files
9
-
10
- | File | Purpose |
11
- |------|---------|
12
- | `wrangler-template.jsonc` | Canonical wrangler config that every site inherits. Compatibility flags, worker-entry path, observability — everything that is the same for every site. |
13
- | `sites/<repo-name>.jsonc` | Per-site overrides. Only the keys that genuinely vary per-site live here (`worker_name` always; `routes`, `kv_namespaces`, `analytics_engine_datasets`, `version_metadata` when used). |
14
-
15
- The repository name (the part of `${{ github.repository }}` after the `/`) is
16
- the lookup key. `als-tanstack` deploys via `sites/als-tanstack.jsonc`. There is
17
- no other way to identify a site.
9
+ There is **no per-site registry**. Worker name is the storefront repo basename
10
+ by convention (`deco-sites/baggagio-tanstack` → worker `baggagio-tanstack`).
11
+ Anything that must vary deterministically per worker (like the Analytics Engine
12
+ dataset name) is encoded as a substitution token in the template — see
13
+ [Substitution tokens](#substitution-tokens) below.
18
14
 
19
15
  ## Trust model
20
16
 
21
- - Customer caller workflows pass **no inputs** to the central reusable workflow.
22
- - The central workflow derives the site name from `${{ github.repository }}`
23
- (set by GitHub, untamperable by user code) and looks up
24
- `sites/<repo-name>.jsonc` from this registry.
25
- - A customer cannot misroute a deploy onto another customer's worker because
26
- they can't write to `decocms/deco-start`.
27
-
28
- `deploy/**` is CODEOWNERS-protected. Only the platform team can change site
29
- manifests or the template.
30
-
31
- ## How wrangler.jsonc is generated
17
+ The deploy is gated by the `decocms-deployer` **GitHub App** being installed on
18
+ the target storefront repo:
19
+
20
+ 1. The storefront's caller workflow mints a short-lived App-installation token.
21
+ 2. It calls `gh workflow run deploy.yml --repo decocms/deco-start -f site_owner=… -f site_name=…`.
22
+ 3. The central deploy workflow runs **in this repo's context** and itself mints
23
+ another short-lived App-installation token to check out the storefront. If
24
+ the App isn't installed on `<site_owner>/<site_name>`, the mint fails and
25
+ the deploy never starts.
26
+ 4. The central workflow then runs build + `wrangler deploy` using
27
+ `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` from this repo's plain
28
+ repo secrets.
29
+
30
+ Properties this gives:
31
+
32
+ - **CF credentials never leave decocms/deco-start.** The storefront repo holds
33
+ zero Cloudflare credentials — it only has the GitHub App credentials, which
34
+ can be used solely to trigger workflows on this repo.
35
+ - **Worker naming is convention-based and not customer-controlled.** A
36
+ customer with push access to their own storefront cannot rename the worker
37
+ their deploy lands on (the central workflow always uses
38
+ `inputs.site_name` as the worker name; modifying the caller stub to pass a
39
+ different `site_name` would also require the App to be installed on that
40
+ other repo).
41
+ - **Force-rollback is impossible for production.** The central deploy
42
+ workflow ignores any caller-supplied sha and always resolves the
43
+ storefront's current default-branch HEAD itself. The worst a compromised
44
+ storefront can do across tenants is trigger a no-op redeploy of another
45
+ storefront's current main.
46
+
47
+ `deploy/` and `scripts/deploy/` and the central workflow files are
48
+ CODEOWNERS-protected — only the platform team approves changes.
49
+
50
+ ### Where Cloudflare credentials live
51
+
52
+ | Secret class | Lives in | How it reaches the worker |
53
+ |---|---|---|
54
+ | `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` | this repo's **repo secrets** | central workflow runs in this repo's context, env-var resolves natively |
55
+ | `DECOCMS_DEPLOYER_APP_ID` / `DECOCMS_DEPLOYER_APP_PRIVATE_KEY` | this repo's repo secrets AND deco-sites org-level secrets | mints short-lived installation tokens for both directions of the dispatch flow |
56
+ | `SECRET_*` runtime secrets (per site) | this repo's `<site_name>-secrets` GitHub Environment | `sync-secrets.yml` binds to that environment, reads `SECRET_*` from `${{ secrets }}`, runs `wrangler secret put` |
57
+
58
+ To rotate Cloudflare credentials, edit them in this repo only. To rotate a
59
+ runtime secret for one storefront, edit the corresponding environment in this
60
+ repo only. No storefront PR needed for either.
61
+
62
+ ## How `wrangler.jsonc` is generated
32
63
 
33
64
  At deploy time, the central workflow runs
34
65
  [`scripts/deploy/build-wrangler-config.mjs`](../scripts/deploy/build-wrangler-config.mjs),
35
66
  which:
36
67
 
37
- 1. Loads `deploy/wrangler-template.jsonc` (canonical defaults).
38
- 2. Loads `deploy/sites/<site>.jsonc` (per-site overrides).
39
- 3. Deep-merges: site overrides win. `worker_name` becomes wrangler's `name`.
40
- Arrays are replaced, not concatenated.
41
- 4. Writes the result to `./wrangler.jsonc` in the caller checkout.
68
+ 1. Loads `deploy/wrangler-template.jsonc`.
69
+ 2. Substitutes `$WORKER_*` tokens (see below) using the worker name passed by
70
+ the central workflow (= storefront repo basename).
71
+ 3. Writes the result to `./wrangler.jsonc` in the storefront checkout, with
72
+ `name` injected as the first key.
42
73
 
43
74
  `account_id` is never written to JSON — wrangler reads it from
44
- `CLOUDFLARE_ACCOUNT_ID` (env var in CI; `wrangler login` locally).
75
+ `CLOUDFLARE_ACCOUNT_ID` (env var in CI; `wrangler login` locally). This way a
76
+ typo cannot misroute a deploy to a different Cloudflare account.
77
+
78
+ ### Substitution tokens
79
+
80
+ Any string in the template containing one of these literals is replaced at
81
+ build time:
82
+
83
+ | Token | Replacement | Example use |
84
+ |---|---|---|
85
+ | `$WORKER_NAME` | worker name verbatim | rare; mostly available for parity |
86
+ | `$WORKER_UNDERSCORE` | worker name with `-` → `_` | `analytics_engine_datasets[].dataset` (must be a valid Postgres-style identifier) |
87
+
88
+ To add a new derived field, add the token wherever it makes sense in
89
+ `wrangler-template.jsonc`. Anything not in the substitution table appears
90
+ verbatim in the generated config.
45
91
 
46
92
  ## Adding a new site
47
93
 
48
- 1. Open a PR to this repo adding `deploy/sites/<new-repo>.jsonc`:
49
- ```jsonc
50
- {
51
- "worker_name": "<new-repo>" // can differ from repo name if needed
52
- }
53
- ```
54
- 2. After merge, the next `v2.x.y` semantic-release publish auto-moves the
55
- `@v2` major tag (the major-tag advance step lives inline in
56
- [`.github/workflows/release.yml`](../.github/workflows/release.yml)).
57
- 3. In the new repo, add the four caller workflows from
58
- [`.github/workflows/`](../.github/workflows/) and set the org-level
59
- `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` GitHub secrets.
60
- 4. Push to `main` and verify the deploy lands on the right worker.
61
-
62
- ## Per-site override schema
63
-
64
- ```jsonc
65
- {
66
- "worker_name": "string (required, immutable)",
67
- "routes": [ // optional
68
- { "pattern": "www.example.com/*", "zone_name": "decocdn.com" }
69
- ],
70
- "kv_namespaces": [ // optional
71
- { "binding": "SITES_KV", "id": "<cf-kv-id>" }
72
- ],
73
- "analytics_engine_datasets": [ // optional
74
- { "binding": "DECO_METRICS", "dataset": "deco_metrics_<site>" }
75
- ],
76
- "version_metadata": { // optional
77
- "binding": "CF_VERSION_METADATA"
78
- }
79
- }
80
- ```
81
-
82
- All other wrangler keys (compatibility flags, `main`, observability, etc.) come
83
- from the template — do not duplicate them per-site. If a per-site override is
84
- genuinely needed for one of those keys, add it to the schema and document the
85
- reason here.
94
+ 1. Install the `decocms-deployer` GitHub App on the new storefront repo
95
+ (Settings → Integrations → GitHub Apps in the deco-sites org).
96
+ 2. Add the four caller workflow stubs to the new repo (copy from any existing
97
+ storefront's `.github/workflows/{deploy,preview,sync-secrets,regen-blocks}.yml`).
98
+ 3. Add `wrangler.jsonc` to the new repo's `.gitignore` and add the
99
+ `gen:wrangler` / `predev` / `prebuild` / `types` scripts to `package.json`
100
+ so local dev still works (use any existing storefront as a template).
101
+ 4. If the site needs runtime secrets, create a new environment in this repo
102
+ named `<repo-basename>-secrets` and add the `SECRET_*` values there. Set
103
+ environment protection rules to grant the site team self-service access to
104
+ their own environment.
105
+ 5. Push to `main` and verify the deploy lands on a worker named after the repo.
106
+
107
+ ## Migrating an existing site whose worker name doesn't match its repo
108
+
109
+ Two cases to be aware of:
110
+
111
+ - **Worker rename.** The worker created by the first deploy will use the repo
112
+ basename. If an old worker exists with a different name (e.g.
113
+ `miess-01-tanstack` repo whose old worker was `miess-tanstack`), you'll need
114
+ a manual cutover: deploy the new worker, re-attach custom domain routes via
115
+ the Cloudflare dashboard, copy any wrangler secrets, then delete the old
116
+ worker. There is intentionally no per-site override for this — these cases
117
+ are rare and best resolved at the CF layer.
118
+ - **AE dataset rename.** The dataset name is derived from worker name, so a
119
+ worker rename also changes the AE dataset. Old data remains queryable under
120
+ the old dataset name; new data goes to the new name. Update Grafana panels
121
+ and saved queries accordingly.
@@ -1,15 +1,26 @@
1
1
  // Canonical wrangler.jsonc template for every storefront on the platform.
2
2
  //
3
- // Per-site overrides live in `deploy/sites/<repo-name>.jsonc`. The deploy
4
- // workflow deep-merges site overrides on top of this template at runtime and
5
- // writes the result to `wrangler.jsonc` in the caller checkout. Site overrides
6
- // always win. Arrays are replaced (not concatenated).
3
+ // There is no per-site registry. Worker name == storefront repo basename by
4
+ // convention; the central deploy workflows pass `WORKER_NAME` to the build
5
+ // script, which substitutes `$WORKER_*` tokens in this template and writes
6
+ // the result to `wrangler.jsonc` in the caller checkout.
7
+ //
8
+ // Substitution tokens (see scripts/deploy/site-registry.mjs):
9
+ // $WORKER_NAME -> worker name verbatim (e.g. "als-tanstack")
10
+ // $WORKER_UNDERSCORE -> worker name, `-` -> `_` (e.g. "als_tanstack")
11
+ //
12
+ // To upgrade compatibility flags, observability, KV bindings, or any field
13
+ // that should change for every site at once, change this file and tag a new
14
+ // deco-start release.
7
15
  //
8
16
  // Notes on what is INTENTIONALLY missing here:
9
17
  // - "name" -- always derived from the per-site `worker_name`.
10
18
  // - "account_id" -- never lives in the JSON. Wrangler reads it from the
11
19
  // CLOUDFLARE_ACCOUNT_ID env var in CI and from local config otherwise.
12
20
  // This way, a typo in JSON cannot misroute a deploy.
21
+ // - "routes" -- production custom domains are managed in the Cloudflare
22
+ // dashboard, not in JSON. Avoids drift between JSON and CF reality and
23
+ // means a site going live doesn't need a deco-start PR.
13
24
  //
14
25
  // To upgrade compatibility flags, observability, or worker-entry path across
15
26
  // every site at once, change this file and tag a new deco-start release.
@@ -19,6 +30,13 @@
19
30
  "main": "./src/worker-entry.ts",
20
31
  "workers_dev": true,
21
32
  "preview_urls": true,
33
+ "kv_namespaces": [
34
+ { "binding": "SITES_KV", "id": "ad0b74fc4d9341c9af9149c4ab85132f" }
35
+ ],
36
+ "version_metadata": { "binding": "CF_VERSION_METADATA" },
37
+ "analytics_engine_datasets": [
38
+ { "binding": "DECO_METRICS", "dataset": "deco_metrics_$WORKER_UNDERSCORE" }
39
+ ],
22
40
  "observability": {
23
41
  "logs": {
24
42
  "enabled": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "2.30.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
@@ -1,19 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  // build-wrangler-config.mjs
3
3
  //
4
- // Materializes a `wrangler.jsonc` from the canonical template + the site's
5
- // per-site overrides. The output file is what `wrangler deploy` /
4
+ // Materializes a `wrangler.jsonc` from the canonical template, with
5
+ // $WORKER_* tokens substituted. The output file is what `wrangler deploy` /
6
6
  // `wrangler versions upload` / `wrangler secret put` will read.
7
7
  //
8
8
  // Required env:
9
9
  // DECO_START_PATH - path to a checked-out deco-start (e.g. ".deco-start")
10
- // SITE_NAME - the registry key (== caller repo name)
10
+ // WORKER_NAME - the Cloudflare worker name (= storefront repo basename
11
+ // by convention; passed by the central CI workflows)
11
12
  // OUTPUT_PATH - where to write the merged wrangler.jsonc
12
13
  // (e.g. "./wrangler.jsonc" in the caller checkout)
13
14
 
14
15
  import { writeFileSync } from "node:fs";
15
16
  import { resolve } from "node:path";
16
- import { loadSiteManifest, loadTemplate, mergeWithTemplate } from "./site-registry.mjs";
17
+ import { applyWorkerName, loadTemplate } from "./site-registry.mjs";
17
18
 
18
19
  function fail(message) {
19
20
  console.error(`::error::${message}`);
@@ -21,29 +22,26 @@ function fail(message) {
21
22
  }
22
23
 
23
24
  const decoStartPath = process.env.DECO_START_PATH;
24
- const siteName = process.env.SITE_NAME;
25
+ const workerName = process.env.WORKER_NAME;
25
26
  const outputPath = process.env.OUTPUT_PATH;
26
27
 
27
28
  if (!decoStartPath) fail("DECO_START_PATH env var is required");
28
- if (!siteName) fail("SITE_NAME env var is required");
29
+ if (!workerName) fail("WORKER_NAME env var is required");
29
30
  if (!outputPath) fail("OUTPUT_PATH env var is required");
30
31
 
31
32
  let merged;
32
33
  try {
33
- const template = loadTemplate(decoStartPath);
34
- const manifest = loadSiteManifest(decoStartPath, siteName);
35
- merged = mergeWithTemplate(template, manifest);
34
+ merged = applyWorkerName(loadTemplate(decoStartPath), workerName);
36
35
  } catch (err) {
37
36
  fail(err instanceof Error ? err.message : String(err));
38
37
  }
39
38
 
40
39
  const header = `// AUTOGENERATED by @decocms/start at deploy time.
41
40
  // Do not edit -- changes will be overwritten on the next deploy.
42
- // Source: decocms/deco-start deploy/sites/${siteName}.jsonc
43
- // + decocms/deco-start deploy/wrangler-template.jsonc
41
+ // Source: decocms/deco-start deploy/wrangler-template.jsonc + worker name "${workerName}"
44
42
  `;
45
43
 
46
44
  const body = JSON.stringify(merged, null, 2);
47
45
  writeFileSync(resolve(outputPath), `${header}${body}\n`);
48
46
 
49
- console.log(`Wrote ${outputPath} for site "${siteName}" (worker "${merged.name}")`);
47
+ console.log(`Wrote ${outputPath} (worker "${workerName}")`);
@@ -1,36 +1,23 @@
1
- // Shared registry helpers for the deploy scripts.
1
+ // Template loader + token substitution for the canonical wrangler config.
2
2
  //
3
- // `loadSiteManifest(decoStartPath, siteName)` returns the validated
4
- // per-site manifest object. `mergeWithTemplate(template, site)` deep-merges a
5
- // site manifest on top of the canonical template and returns the wrangler
6
- // config object ready for serialization.
3
+ // There is no per-site "registry" anymore: every site's wrangler config is
4
+ // produced from `deploy/wrangler-template.jsonc` plus the worker name (which
5
+ // equals the storefront repo basename by convention). To accommodate fields
6
+ // that must vary deterministically per worker, the template can use these
7
+ // substitution tokens, which are replaced at config-build time:
7
8
  //
8
- // Trust model: `siteName` is always derived from `${{ github.repository }}` in
9
- // CI (or from the local git remote in the wrapper CLI), never from a
10
- // user-supplied input. A site that is not registered in `deploy/sites/` cannot
11
- // be deployed -- this is enforced here.
9
+ // $WORKER_NAME -> worker name verbatim (e.g. "als-tanstack")
10
+ // $WORKER_UNDERSCORE -> worker name, `-` -> `_` (e.g. "als_tanstack")
11
+ //
12
+ // Trust model: callers cannot pass a fabricated worker name to the central
13
+ // CI workflows -- the deploy is gated by the `decocms-deployer` GitHub App
14
+ // being installed on the target storefront repo. If the App isn't installed
15
+ // there, the App-token mint fails and the deploy never starts.
12
16
 
13
17
  import { existsSync } from "node:fs";
14
18
  import { join } from "node:path";
15
19
  import { readJsoncFile } from "./jsonc.mjs";
16
20
 
17
- /** @typedef {{
18
- * worker_name: string;
19
- * routes?: Array<{ pattern: string; zone_name?: string; custom_domain?: boolean }>;
20
- * kv_namespaces?: Array<{ binding: string; id: string; preview_id?: string }>;
21
- * analytics_engine_datasets?: Array<{ binding: string; dataset: string }>;
22
- * version_metadata?: { binding: string };
23
- * }} SiteManifest
24
- */
25
-
26
- const ALLOWED_SITE_KEYS = new Set([
27
- "worker_name",
28
- "routes",
29
- "kv_namespaces",
30
- "analytics_engine_datasets",
31
- "version_metadata",
32
- ]);
33
-
34
21
  /**
35
22
  * @param {string} decoStartPath
36
23
  * @returns {string}
@@ -39,56 +26,6 @@ export function templatePath(decoStartPath) {
39
26
  return join(decoStartPath, "deploy", "wrangler-template.jsonc");
40
27
  }
41
28
 
42
- /**
43
- * @param {string} decoStartPath
44
- * @param {string} siteName
45
- * @returns {string}
46
- */
47
- export function siteManifestPath(decoStartPath, siteName) {
48
- return join(decoStartPath, "deploy", "sites", `${siteName}.jsonc`);
49
- }
50
-
51
- /**
52
- * @param {string} decoStartPath
53
- * @param {string} siteName
54
- * @returns {SiteManifest}
55
- */
56
- export function loadSiteManifest(decoStartPath, siteName) {
57
- if (!siteName || !/^[a-z0-9][a-z0-9-]*$/.test(siteName)) {
58
- throw new Error(
59
- `Refusing to load manifest for invalid site name: ${JSON.stringify(siteName)}. Site names must be lowercase, hyphen-separated.`,
60
- );
61
- }
62
- const path = siteManifestPath(decoStartPath, siteName);
63
- if (!existsSync(path)) {
64
- throw new Error(
65
- `No registry entry for site "${siteName}" at ${path}.\n` +
66
- `Add deploy/sites/${siteName}.jsonc to decocms/deco-start before deploying.`,
67
- );
68
- }
69
- const raw = readJsoncFile(path);
70
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
71
- throw new Error(`Site manifest at ${path} must be a JSON object.`);
72
- }
73
- const manifest = /** @type {Record<string, unknown>} */ (raw);
74
- if (typeof manifest.worker_name !== "string" || manifest.worker_name.length === 0) {
75
- throw new Error(`Site manifest at ${path} is missing the required "worker_name" string.`);
76
- }
77
- if (!/^[a-z0-9][a-z0-9-]*$/.test(/** @type {string} */ (manifest.worker_name))) {
78
- throw new Error(
79
- `Site manifest at ${path} has an invalid "worker_name": ${JSON.stringify(manifest.worker_name)}. Use lowercase, hyphen-separated.`,
80
- );
81
- }
82
- for (const key of Object.keys(manifest)) {
83
- if (!ALLOWED_SITE_KEYS.has(key)) {
84
- throw new Error(
85
- `Site manifest at ${path} contains unsupported key "${key}". Allowed: ${[...ALLOWED_SITE_KEYS].join(", ")}.`,
86
- );
87
- }
88
- }
89
- return /** @type {SiteManifest} */ (manifest);
90
- }
91
-
92
29
  /**
93
30
  * @param {string} decoStartPath
94
31
  * @returns {Record<string, unknown>}
@@ -106,37 +43,53 @@ export function loadTemplate(decoStartPath) {
106
43
  }
107
44
 
108
45
  /**
109
- * Deep-merge `source` on top of `target`. Arrays in `source` REPLACE arrays in
110
- * `target` (they are not concatenated) -- this matches the semantics wrangler
111
- * itself expects for `routes`, `kv_namespaces`, etc.
46
+ * Recursively replace `$WORKER_*` tokens in any string value of an
47
+ * object/array tree. Returns a new tree.
112
48
  *
113
- * @template T
114
- * @param {T} target
115
- * @param {unknown} source
116
- * @returns {T}
49
+ * @param {unknown} value
50
+ * @param {Record<string, string>} replacements
51
+ * @returns {unknown}
117
52
  */
118
- function deepMerge(target, source) {
119
- if (source === null || source === undefined) return target;
120
- if (Array.isArray(source)) return /** @type {T} */ (source);
121
- if (typeof source !== "object") return /** @type {T} */ (source);
122
- const base = target && typeof target === "object" && !Array.isArray(target) ? target : {};
123
- const out = /** @type {Record<string, unknown>} */ ({ ...base });
124
- for (const [k, v] of Object.entries(source)) {
125
- out[k] = deepMerge(out[k], v);
53
+ function substituteTokens(value, replacements) {
54
+ if (typeof value === "string") {
55
+ let out = value;
56
+ for (const [token, repl] of Object.entries(replacements)) {
57
+ out = out.split(token).join(repl);
58
+ }
59
+ return out;
126
60
  }
127
- return /** @type {T} */ (out);
61
+ if (Array.isArray(value)) {
62
+ return value.map((v) => substituteTokens(v, replacements));
63
+ }
64
+ if (value && typeof value === "object") {
65
+ const out = /** @type {Record<string, unknown>} */ ({});
66
+ for (const [k, v] of Object.entries(value)) {
67
+ out[k] = substituteTokens(v, replacements);
68
+ }
69
+ return out;
70
+ }
71
+ return value;
128
72
  }
129
73
 
130
74
  /**
131
- * Produce the wrangler config object by deep-merging a site manifest on top of
132
- * the canonical template. The site's `worker_name` becomes wrangler's `name`.
75
+ * Produce the wrangler config object by substituting `$WORKER_*` tokens in
76
+ * the template and prepending `name`.
133
77
  *
134
78
  * @param {Record<string, unknown>} template
135
- * @param {SiteManifest} site
79
+ * @param {string} workerName
136
80
  * @returns {Record<string, unknown>}
137
81
  */
138
- export function mergeWithTemplate(template, site) {
139
- const { worker_name, ...rest } = site;
140
- const merged = deepMerge(template, rest);
141
- return { name: worker_name, ...merged };
82
+ export function applyWorkerName(template, workerName) {
83
+ if (typeof workerName !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(workerName)) {
84
+ throw new Error(
85
+ `Invalid worker name: ${JSON.stringify(workerName)}. Use lowercase, hyphen-separated.`,
86
+ );
87
+ }
88
+ const substituted = /** @type {Record<string, unknown>} */ (
89
+ substituteTokens(template, {
90
+ $WORKER_UNDERSCORE: workerName.replace(/-/g, "_"),
91
+ $WORKER_NAME: workerName,
92
+ })
93
+ );
94
+ return { name: workerName, ...substituted };
142
95
  }
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // Local-dev wrapper around `wrangler` for storefront repos that no longer
5
5
  // commit a `wrangler.jsonc`. Generates the canonical config from
6
- // `@decocms/start`'s registry into `./wrangler.jsonc` (gitignored), then
6
+ // `@decocms/start`'s template into `./wrangler.jsonc` (gitignored), then
7
7
  // optionally execs the real `wrangler` with that config in cwd.
8
8
  //
9
9
  // Usage from a customer repo (after `npm i -D @decocms/start@latest`):
@@ -16,26 +16,25 @@
16
16
  // npx deco-wrangler --help # passes through to wrangler
17
17
  //
18
18
  // All non-`gen` invocations also (re)generate ./wrangler.jsonc first so the
19
- // file is always in sync with the registry before wrangler runs. The
19
+ // file is always in sync with the template before wrangler runs. The
20
20
  // `@cloudflare/vite-plugin` and the `wrangler` CLI both auto-discover
21
21
  // ./wrangler.jsonc in cwd, so no extra config plumbing is required.
22
22
  //
23
- // Site identity (which entry of `deploy/sites/` to load) resolves in this
24
- // order:
23
+ // Worker name resolves in this order:
25
24
  //
26
- // 1. DECO_SITE_NAME env var (explicit override)
27
- // 2. git remote `origin` URL parsed for the GitHub repo name
25
+ // 1. DECO_WORKER_NAME env var (explicit override)
26
+ // 2. git remote `origin` URL parsed for the GitHub repo basename
28
27
  // 3. package.json `name` field
29
28
  //
30
- // Fails loudly if none of these match a registered site -- no implicit "guess
31
- // the worker" behavior. The local wrapper enforces the same trust model the CI
32
- // pipeline does.
29
+ // By convention worker name == storefront repo basename. The CI deploys use
30
+ // the same convention. Local dev never deploys -- this wrapper is purely for
31
+ // generating a config that matches what the central workflow will produce.
33
32
 
34
33
  import { execSync, spawnSync } from "node:child_process";
35
34
  import { readFileSync, writeFileSync } from "node:fs";
36
35
  import { dirname, resolve } from "node:path";
37
36
  import { fileURLToPath } from "node:url";
38
- import { loadSiteManifest, loadTemplate, mergeWithTemplate } from "./site-registry.mjs";
37
+ import { applyWorkerName, loadTemplate } from "./site-registry.mjs";
39
38
 
40
39
  const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
41
40
  // scripts/deploy/wrangler-wrapper.mjs -> deco-start root
@@ -45,7 +44,7 @@ function eprintln(msg) {
45
44
  process.stderr.write(`[deco-wrangler] ${msg}\n`);
46
45
  }
47
46
 
48
- function tryGitRemoteSiteName() {
47
+ function tryGitRemoteWorkerName() {
49
48
  try {
50
49
  const url = execSync("git remote get-url origin", {
51
50
  stdio: ["ignore", "pipe", "ignore"],
@@ -69,54 +68,47 @@ function tryPackageJsonName() {
69
68
  }
70
69
  }
71
70
 
72
- function resolveSiteName() {
73
- const envName = process.env.DECO_SITE_NAME?.trim();
74
- if (envName) return { name: envName, source: "DECO_SITE_NAME env var" };
75
- const gitName = tryGitRemoteSiteName();
71
+ function resolveWorkerName() {
72
+ const envName = process.env.DECO_WORKER_NAME?.trim();
73
+ if (envName) return { name: envName, source: "DECO_WORKER_NAME env var" };
74
+ const gitName = tryGitRemoteWorkerName();
76
75
  if (gitName) return { name: gitName, source: "git remote origin" };
77
76
  const pkgName = tryPackageJsonName();
78
77
  if (pkgName) return { name: pkgName, source: "package.json name" };
79
78
  return undefined;
80
79
  }
81
80
 
82
- const resolved = resolveSiteName();
81
+ const resolved = resolveWorkerName();
83
82
  if (!resolved) {
84
83
  eprintln(
85
- "Could not determine site name. Set DECO_SITE_NAME or run from a repo with a github.com 'origin' remote.",
84
+ "Could not determine worker name. Set DECO_WORKER_NAME or run from a repo with a github.com 'origin' remote.",
86
85
  );
87
86
  process.exit(1);
88
87
  }
89
88
 
90
- let manifest;
89
+ let merged;
91
90
  try {
92
- manifest = loadSiteManifest(DECO_START_PATH, resolved.name);
91
+ merged = applyWorkerName(loadTemplate(DECO_START_PATH), resolved.name);
93
92
  } catch (err) {
94
93
  eprintln(err instanceof Error ? err.message : String(err));
95
- eprintln(`Site name "${resolved.name}" was inferred from ${resolved.source}.`);
96
- eprintln(
97
- `If this is a personal sandbox, set DECO_SITE_NAME=<registered-site> or open a PR to deco-start adding deploy/sites/${resolved.name}.jsonc.`,
98
- );
94
+ eprintln(`Worker name "${resolved.name}" was inferred from ${resolved.source}.`);
99
95
  process.exit(1);
100
96
  }
101
97
 
102
- const merged = mergeWithTemplate(loadTemplate(DECO_START_PATH), manifest);
103
-
104
98
  const outputPath = resolve(process.cwd(), "wrangler.jsonc");
105
99
  const header = `// AUTOGENERATED by deco-wrangler -- DO NOT COMMIT.
106
100
  // Add wrangler.jsonc to .gitignore. Regenerated on every \`deco-wrangler\` run.
107
- // Source: @decocms/start deploy/sites/${resolved.name}.jsonc + wrangler-template.jsonc
101
+ // Source: @decocms/start deploy/wrangler-template.jsonc + worker name "${resolved.name}"
108
102
  `;
109
103
  writeFileSync(outputPath, `${header}${JSON.stringify(merged, null, 2)}\n`);
110
104
 
111
- eprintln(
112
- `Resolved site "${resolved.name}" -> worker "${manifest.worker_name}" (via ${resolved.source})`,
113
- );
105
+ eprintln(`Resolved worker name "${resolved.name}" (via ${resolved.source})`);
114
106
  eprintln(`Generated ${outputPath}`);
115
107
 
116
108
  const argv = process.argv.slice(2);
117
109
  // `gen` mode: just write the config and exit. Used by package.json
118
110
  // predev/prebuild/prepare hooks to keep wrangler.jsonc in sync with the
119
- // registry without invoking wrangler itself.
111
+ // template without invoking wrangler itself.
120
112
  if (argv[0] === "gen" || argv[0] === "generate") {
121
113
  process.exit(0);
122
114
  }
@@ -51,10 +51,10 @@ export function scaffold(ctx: MigrationContext): void {
51
51
  logPhase("Scaffold");
52
52
 
53
53
  // Root config files. wrangler.jsonc is INTENTIONALLY not generated --
54
- // per D6, the canonical wrangler config lives in decocms/deco-start under
55
- // deploy/wrangler-template.jsonc and per-site overrides under
56
- // deploy/sites/<repo>.jsonc; the file is materialized locally by
57
- // `deco-wrangler gen` and gitignored.
54
+ // per D6.2, the canonical wrangler config lives in decocms/deco-start under
55
+ // deploy/wrangler-template.jsonc; the file is materialized locally by
56
+ // `deco-wrangler gen` and gitignored. Worker name = storefront repo basename
57
+ // by convention; there is no per-site registry.
58
58
  writeFile(ctx, "package.json", generatePackageJson(ctx));
59
59
  writeFile(ctx, "tsconfig.json", generateTsconfig());
60
60
  writeFile(ctx, "vite.config.ts", generateViteConfig(ctx));
@@ -216,8 +216,8 @@ dist/
216
216
  # Cloudflare Workers
217
217
  .wrangler/
218
218
  .dev.vars
219
- # Generated by \`deco-wrangler\` from @decocms/start's central registry.
220
- # Source of truth lives in decocms/deco-start under deploy/sites/<repo>.jsonc.
219
+ # Generated by \`deco-wrangler\` from @decocms/start's wrangler template.
220
+ # Worker name is derived from the git remote / package.json by convention.
221
221
  wrangler.jsonc
222
222
 
223
223
  # TanStack Router (auto-generated)