@decocms/start 2.28.2 → 2.30.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.
Files changed (49) hide show
  1. package/.agents/skills/deco-to-tanstack-migration/SKILL.md +1 -1
  2. package/.agents/skills/deco-to-tanstack-migration/references/vtex-commerce.md +5 -1
  3. package/.agents/skills/deco-to-tanstack-migration/references/worker-cloudflare.md +107 -10
  4. package/.agents/skills/deco-to-tanstack-migration/templates/package-json.md +5 -1
  5. package/.cursor/rules/migration-tooling-policy.mdc +22 -2
  6. package/.github/workflows/deploy.yml +115 -0
  7. package/.github/workflows/preview.yml +143 -0
  8. package/.github/workflows/regen-blocks.yml +56 -0
  9. package/.github/workflows/release.yml +26 -0
  10. package/.github/workflows/sync-secrets.yml +173 -0
  11. package/CODEOWNERS +16 -0
  12. package/MIGRATION_TOOLING_PLAN.md +16 -4
  13. package/README.md +178 -79
  14. package/deploy/README.md +85 -0
  15. package/deploy/sites/als-tanstack.jsonc +7 -0
  16. package/deploy/sites/americanas-tanstack.jsonc +4 -0
  17. package/deploy/sites/baggagio-tanstack.jsonc +4 -0
  18. package/deploy/sites/casaevideo-storefront.jsonc +11 -0
  19. package/deploy/sites/lebiscuit-tanstack.jsonc +19 -0
  20. package/deploy/sites/miess-01-tanstack.jsonc +8 -0
  21. package/deploy/wrangler-template.jsonc +28 -0
  22. package/package.json +18 -15
  23. package/scripts/deploy/build-wrangler-config.mjs +49 -0
  24. package/scripts/deploy/jsonc.mjs +76 -0
  25. package/scripts/deploy/resolve-site.mjs +58 -0
  26. package/scripts/deploy/site-registry.mjs +142 -0
  27. package/scripts/deploy/wrangler-wrapper.mjs +126 -0
  28. package/scripts/migrate/phase-scaffold.ts +13 -3
  29. package/scripts/migrate/phase-verify.ts +6 -1
  30. package/scripts/migrate/templates/github-workflows.ts +98 -0
  31. package/scripts/migrate/templates/package-json.ts +9 -2
  32. package/src/cms/resolve.ts +81 -63
  33. package/src/cms/sectionLoaders.ts +11 -0
  34. package/src/index.ts +3 -0
  35. package/src/sdk/cachedLoader.ts +36 -13
  36. package/src/sdk/composite.test.ts +121 -0
  37. package/src/sdk/composite.ts +114 -0
  38. package/src/sdk/instrumentedFetch.ts +56 -0
  39. package/src/sdk/logger.test.ts +135 -0
  40. package/src/sdk/logger.ts +166 -0
  41. package/src/sdk/observability.ts +75 -0
  42. package/src/sdk/otel.test.ts +59 -0
  43. package/src/sdk/otel.ts +270 -29
  44. package/src/sdk/otelAdapters.test.ts +135 -0
  45. package/src/sdk/otelAdapters.ts +401 -0
  46. package/src/sdk/sampler.test.ts +127 -0
  47. package/src/sdk/sampler.ts +183 -0
  48. package/src/sdk/workerEntry.ts +541 -476
  49. package/scripts/migrate/templates/wrangler.ts +0 -30
@@ -0,0 +1,19 @@
1
+ // Per-site overrides for `lebiscuit-tanstack`.
2
+ //
3
+ // NOTE: `kv_namespaces[].id` is shared with `casaevideo-storefront`. See the
4
+ // note in casaevideo's site file. R4 will add CI validation for this.
5
+ //
6
+ // `version_metadata` and `analytics_engine_datasets` are consumed by
7
+ // @decocms/start's `instrumentWorker`:
8
+ // - version_metadata populates `service.version` on the OTel resource.
9
+ // - analytics_engine_datasets is the dual-emit metrics target.
10
+ {
11
+ "worker_name": "lebiscuit-tanstack",
12
+ "kv_namespaces": [
13
+ { "binding": "SITES_KV", "id": "ad0b74fc4d9341c9af9149c4ab85132f" }
14
+ ],
15
+ "version_metadata": { "binding": "CF_VERSION_METADATA" },
16
+ "analytics_engine_datasets": [
17
+ { "binding": "DECO_METRICS", "dataset": "deco_metrics_lebiscuit" }
18
+ ]
19
+ }
@@ -0,0 +1,8 @@
1
+ // Per-site overrides for `miess-01-tanstack`.
2
+ //
3
+ // Worker name keeps the historical `miess-tanstack` (no "-01") so the deploy
4
+ // continues to land on the existing Cloudflare worker. The repo name has the
5
+ // `-01-` suffix; the worker name does not.
6
+ {
7
+ "worker_name": "miess-tanstack"
8
+ }
@@ -0,0 +1,28 @@
1
+ // Canonical wrangler.jsonc template for every storefront on the platform.
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).
7
+ //
8
+ // Notes on what is INTENTIONALLY missing here:
9
+ // - "name" -- always derived from the per-site `worker_name`.
10
+ // - "account_id" -- never lives in the JSON. Wrangler reads it from the
11
+ // CLOUDFLARE_ACCOUNT_ID env var in CI and from local config otherwise.
12
+ // This way, a typo in JSON cannot misroute a deploy.
13
+ //
14
+ // To upgrade compatibility flags, observability, or worker-entry path across
15
+ // every site at once, change this file and tag a new deco-start release.
16
+ {
17
+ "compatibility_date": "2026-02-14",
18
+ "compatibility_flags": ["nodejs_compat", "no_handle_cross_request_promise_resolution"],
19
+ "main": "./src/worker-entry.ts",
20
+ "workers_dev": true,
21
+ "preview_urls": true,
22
+ "observability": {
23
+ "logs": {
24
+ "enabled": true,
25
+ "invocation_logs": true
26
+ }
27
+ }
28
+ }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "2.28.2",
3
+ "version": "2.30.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",
7
7
  "bin": {
8
8
  "deco-migrate": "./scripts/migrate.ts",
9
9
  "deco-post-cleanup": "./scripts/migrate-post-cleanup.ts",
10
- "deco-htmx-analyze": "./scripts/htmx-analyze.ts"
10
+ "deco-htmx-analyze": "./scripts/htmx-analyze.ts",
11
+ "deco-wrangler": "./scripts/deploy/wrangler-wrapper.mjs"
11
12
  },
12
13
  "exports": {
13
14
  ".": "./src/index.ts",
@@ -35,6 +36,11 @@
35
36
  "./sdk/invoke": "./src/sdk/invoke.ts",
36
37
  "./sdk/instrumentedFetch": "./src/sdk/instrumentedFetch.ts",
37
38
  "./sdk/otel": "./src/sdk/otel.ts",
39
+ "./sdk/logger": "./src/sdk/logger.ts",
40
+ "./sdk/composite": "./src/sdk/composite.ts",
41
+ "./sdk/otelAdapters": "./src/sdk/otelAdapters.ts",
42
+ "./sdk/sampler": "./src/sdk/sampler.ts",
43
+ "./sdk/observability": "./src/sdk/observability.ts",
38
44
  "./sdk/workerEntry": "./src/sdk/workerEntry.ts",
39
45
  "./sdk/abTesting": "./src/sdk/abTesting.ts",
40
46
  "./sdk/redirects": "./src/sdk/redirects.ts",
@@ -99,6 +105,15 @@
99
105
  },
100
106
  "dependencies": {
101
107
  "@deco-cx/warp-node": "^0.3.16",
108
+ "@microlabs/otel-cf-workers": "^1.0.0-rc.52",
109
+ "@opentelemetry/api": "^1.9.1",
110
+ "@opentelemetry/api-logs": "^0.200.0",
111
+ "@opentelemetry/exporter-logs-otlp-http": "^0.200.0",
112
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.200.0",
113
+ "@opentelemetry/resources": "^2.6.1",
114
+ "@opentelemetry/sdk-logs": "^0.200.0",
115
+ "@opentelemetry/sdk-metrics": "^2.0.0",
116
+ "@opentelemetry/sdk-trace-base": "^2.6.1",
102
117
  "clsx": "^2.1.1",
103
118
  "fast-json-patch": "^3.1.0",
104
119
  "tailwind-merge": "^3.3.1",
@@ -106,8 +121,6 @@
106
121
  "ws": "^8.18.0"
107
122
  },
108
123
  "peerDependencies": {
109
- "@microlabs/otel-cf-workers": ">=1.0.0-rc.0",
110
- "@opentelemetry/api": ">=1.9.0",
111
124
  "@tanstack/react-query": ">=5.0.0",
112
125
  "@tanstack/react-start": ">=1.0.0",
113
126
  "@tanstack/store": ">=0.7.0",
@@ -115,25 +128,15 @@
115
128
  "react-dom": "^19.0.0",
116
129
  "vite": ">=6.0.0 || >=7.0.0 || >=8.0.0"
117
130
  },
118
- "peerDependenciesMeta": {
119
- "@microlabs/otel-cf-workers": {
120
- "optional": true
121
- },
122
- "@opentelemetry/api": {
123
- "optional": true
124
- }
125
- },
126
131
  "devDependencies": {
127
132
  "@biomejs/biome": "^2.4.6",
128
- "@microlabs/otel-cf-workers": "^1.0.0-rc.52",
129
- "@opentelemetry/api": "^1.9.1",
130
133
  "@semantic-release/exec": "^7.1.0",
131
134
  "@semantic-release/git": "^10.0.1",
132
135
  "@tanstack/react-query": "^5.96.0",
133
136
  "@tanstack/store": "^0.9.1",
134
137
  "@types/react": "^19.0.0",
135
- "@types/ws": "^8.18.0",
136
138
  "@types/react-dom": "^19.0.0",
139
+ "@types/ws": "^8.18.0",
137
140
  "jsdom": "^29.0.0",
138
141
  "knip": "^5.86.0",
139
142
  "ts-morph": "^27.0.0",
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ // build-wrangler-config.mjs
3
+ //
4
+ // Materializes a `wrangler.jsonc` from the canonical template + the site's
5
+ // per-site overrides. The output file is what `wrangler deploy` /
6
+ // `wrangler versions upload` / `wrangler secret put` will read.
7
+ //
8
+ // Required env:
9
+ // DECO_START_PATH - path to a checked-out deco-start (e.g. ".deco-start")
10
+ // SITE_NAME - the registry key (== caller repo name)
11
+ // OUTPUT_PATH - where to write the merged wrangler.jsonc
12
+ // (e.g. "./wrangler.jsonc" in the caller checkout)
13
+
14
+ import { writeFileSync } from "node:fs";
15
+ import { resolve } from "node:path";
16
+ import { loadSiteManifest, loadTemplate, mergeWithTemplate } from "./site-registry.mjs";
17
+
18
+ function fail(message) {
19
+ console.error(`::error::${message}`);
20
+ process.exit(1);
21
+ }
22
+
23
+ const decoStartPath = process.env.DECO_START_PATH;
24
+ const siteName = process.env.SITE_NAME;
25
+ const outputPath = process.env.OUTPUT_PATH;
26
+
27
+ if (!decoStartPath) fail("DECO_START_PATH env var is required");
28
+ if (!siteName) fail("SITE_NAME env var is required");
29
+ if (!outputPath) fail("OUTPUT_PATH env var is required");
30
+
31
+ let merged;
32
+ try {
33
+ const template = loadTemplate(decoStartPath);
34
+ const manifest = loadSiteManifest(decoStartPath, siteName);
35
+ merged = mergeWithTemplate(template, manifest);
36
+ } catch (err) {
37
+ fail(err instanceof Error ? err.message : String(err));
38
+ }
39
+
40
+ const header = `// AUTOGENERATED by @decocms/start at deploy time.
41
+ // 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
44
+ `;
45
+
46
+ const body = JSON.stringify(merged, null, 2);
47
+ writeFileSync(resolve(outputPath), `${header}${body}\n`);
48
+
49
+ console.log(`Wrote ${outputPath} for site "${siteName}" (worker "${merged.name}")`);
@@ -0,0 +1,76 @@
1
+ // Minimal JSONC -> JSON parser used by the deploy scripts.
2
+ //
3
+ // Strips // line comments and /* block comments */ outside of string literals
4
+ // and tolerates trailing commas in objects/arrays. Dependency-free so the
5
+ // deploy scripts can run with vanilla `node` in CI.
6
+
7
+ import { readFileSync } from "node:fs";
8
+
9
+ /**
10
+ * @param {string} input
11
+ * @returns {string}
12
+ */
13
+ function stripComments(input) {
14
+ let out = "";
15
+ let i = 0;
16
+ let inStr = false;
17
+ let strChar = "";
18
+ while (i < input.length) {
19
+ const c = input[i];
20
+ const n = input[i + 1];
21
+ if (inStr) {
22
+ out += c;
23
+ if (c === "\\" && i + 1 < input.length) {
24
+ out += input[i + 1];
25
+ i += 2;
26
+ continue;
27
+ }
28
+ if (c === strChar) inStr = false;
29
+ i++;
30
+ continue;
31
+ }
32
+ if (c === '"') {
33
+ inStr = true;
34
+ strChar = c;
35
+ out += c;
36
+ i++;
37
+ continue;
38
+ }
39
+ if (c === "/" && n === "/") {
40
+ while (i < input.length && input[i] !== "\n") i++;
41
+ continue;
42
+ }
43
+ if (c === "/" && n === "*") {
44
+ i += 2;
45
+ while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
46
+ i += 2;
47
+ continue;
48
+ }
49
+ out += c;
50
+ i++;
51
+ }
52
+ return out;
53
+ }
54
+
55
+ /**
56
+ * @param {string} text
57
+ * @returns {unknown}
58
+ */
59
+ export function parseJsonc(text) {
60
+ const stripped = stripComments(text).replace(/,\s*([}\]])/g, "$1");
61
+ return JSON.parse(stripped);
62
+ }
63
+
64
+ /**
65
+ * @param {string} path
66
+ * @returns {unknown}
67
+ */
68
+ export function readJsoncFile(path) {
69
+ const raw = readFileSync(path, "utf8");
70
+ try {
71
+ return parseJsonc(raw);
72
+ } catch (err) {
73
+ const message = err instanceof Error ? err.message : String(err);
74
+ throw new Error(`Failed to parse JSONC at ${path}: ${message}`);
75
+ }
76
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // resolve-site.mjs
3
+ //
4
+ // Validates that the calling repository has a registered site manifest in
5
+ // deco-start, and emits the resolved fields to GITHUB_OUTPUT for downstream
6
+ // steps (wrangler tail, status comments, etc.).
7
+ //
8
+ // Required env:
9
+ // DECO_START_PATH - path to a checked-out deco-start (e.g. ".deco-start")
10
+ // SITE_NAME - typically `${GITHUB_REPOSITORY#*/}` set by the workflow
11
+ //
12
+ // Optional env:
13
+ // GITHUB_OUTPUT - if set, emit `key=value` lines here (CI mode).
14
+ // If unset, prints a human-readable summary to stdout.
15
+
16
+ import { appendFileSync } from "node:fs";
17
+ import { loadSiteManifest } from "./site-registry.mjs";
18
+
19
+ function fail(message) {
20
+ console.error(`::error::${message}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ const decoStartPath = process.env.DECO_START_PATH;
25
+ const siteName = process.env.SITE_NAME;
26
+ const ghOutput = process.env.GITHUB_OUTPUT;
27
+
28
+ if (!decoStartPath) fail("DECO_START_PATH env var is required");
29
+ if (!siteName) fail("SITE_NAME env var is required");
30
+
31
+ let manifest;
32
+ try {
33
+ manifest = loadSiteManifest(decoStartPath, siteName);
34
+ } catch (err) {
35
+ fail(err instanceof Error ? err.message : String(err));
36
+ }
37
+
38
+ const summary = {
39
+ site_name: siteName,
40
+ worker_name: manifest.worker_name,
41
+ has_routes: String(Array.isArray(manifest.routes) && manifest.routes.length > 0),
42
+ has_kv: String(Array.isArray(manifest.kv_namespaces) && manifest.kv_namespaces.length > 0),
43
+ has_analytics: String(
44
+ Array.isArray(manifest.analytics_engine_datasets) &&
45
+ manifest.analytics_engine_datasets.length > 0,
46
+ ),
47
+ has_version_metadata: String(Boolean(manifest.version_metadata)),
48
+ };
49
+
50
+ if (ghOutput) {
51
+ const lines = Object.entries(summary).map(([k, v]) => `${k}=${v}`);
52
+ appendFileSync(ghOutput, `${lines.join("\n")}\n`);
53
+ }
54
+
55
+ console.log(`Resolved site "${siteName}" -> worker "${manifest.worker_name}"`);
56
+ for (const [k, v] of Object.entries(summary)) {
57
+ console.log(` ${k}: ${v}`);
58
+ }
@@ -0,0 +1,142 @@
1
+ // Shared registry helpers for the deploy scripts.
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.
7
+ //
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.
12
+
13
+ import { existsSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { readJsoncFile } from "./jsonc.mjs";
16
+
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
+ /**
35
+ * @param {string} decoStartPath
36
+ * @returns {string}
37
+ */
38
+ export function templatePath(decoStartPath) {
39
+ return join(decoStartPath, "deploy", "wrangler-template.jsonc");
40
+ }
41
+
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
+ /**
93
+ * @param {string} decoStartPath
94
+ * @returns {Record<string, unknown>}
95
+ */
96
+ export function loadTemplate(decoStartPath) {
97
+ const path = templatePath(decoStartPath);
98
+ if (!existsSync(path)) {
99
+ throw new Error(`wrangler-template.jsonc not found at ${path}.`);
100
+ }
101
+ const raw = readJsoncFile(path);
102
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
103
+ throw new Error(`Template at ${path} must be a JSON object.`);
104
+ }
105
+ return /** @type {Record<string, unknown>} */ (raw);
106
+ }
107
+
108
+ /**
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.
112
+ *
113
+ * @template T
114
+ * @param {T} target
115
+ * @param {unknown} source
116
+ * @returns {T}
117
+ */
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);
126
+ }
127
+ return /** @type {T} */ (out);
128
+ }
129
+
130
+ /**
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`.
133
+ *
134
+ * @param {Record<string, unknown>} template
135
+ * @param {SiteManifest} site
136
+ * @returns {Record<string, unknown>}
137
+ */
138
+ export function mergeWithTemplate(template, site) {
139
+ const { worker_name, ...rest } = site;
140
+ const merged = deepMerge(template, rest);
141
+ return { name: worker_name, ...merged };
142
+ }
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ // deco-wrangler
3
+ //
4
+ // Local-dev wrapper around `wrangler` for storefront repos that no longer
5
+ // commit a `wrangler.jsonc`. Generates the canonical config from
6
+ // `@decocms/start`'s registry into `./wrangler.jsonc` (gitignored), then
7
+ // optionally execs the real `wrangler` with that config in cwd.
8
+ //
9
+ // Usage from a customer repo (after `npm i -D @decocms/start@latest`):
10
+ //
11
+ // npx deco-wrangler gen # generate ./wrangler.jsonc and exit
12
+ // # (used by predev/prebuild/prepare hooks)
13
+ // npx deco-wrangler tail # tail prod logs (worker name resolved automatically)
14
+ // npx deco-wrangler types # regenerate worker-configuration.d.ts
15
+ // npx deco-wrangler deploy # discouraged in dev; deploys go via CI
16
+ // npx deco-wrangler --help # passes through to wrangler
17
+ //
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
20
+ // `@cloudflare/vite-plugin` and the `wrangler` CLI both auto-discover
21
+ // ./wrangler.jsonc in cwd, so no extra config plumbing is required.
22
+ //
23
+ // Site identity (which entry of `deploy/sites/` to load) resolves in this
24
+ // order:
25
+ //
26
+ // 1. DECO_SITE_NAME env var (explicit override)
27
+ // 2. git remote `origin` URL parsed for the GitHub repo name
28
+ // 3. package.json `name` field
29
+ //
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.
33
+
34
+ import { execSync, spawnSync } from "node:child_process";
35
+ import { readFileSync, writeFileSync } from "node:fs";
36
+ import { dirname, resolve } from "node:path";
37
+ import { fileURLToPath } from "node:url";
38
+ import { loadSiteManifest, loadTemplate, mergeWithTemplate } from "./site-registry.mjs";
39
+
40
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
41
+ // scripts/deploy/wrangler-wrapper.mjs -> deco-start root
42
+ const DECO_START_PATH = resolve(SCRIPT_DIR, "..", "..");
43
+
44
+ function eprintln(msg) {
45
+ process.stderr.write(`[deco-wrangler] ${msg}\n`);
46
+ }
47
+
48
+ function tryGitRemoteSiteName() {
49
+ try {
50
+ const url = execSync("git remote get-url origin", {
51
+ stdio: ["ignore", "pipe", "ignore"],
52
+ })
53
+ .toString()
54
+ .trim();
55
+ // matches both git@github.com:org/repo(.git) and https://github.com/org/repo(.git)
56
+ const m = url.match(/github\.com[:/][^/]+\/([^/]+?)(?:\.git)?$/);
57
+ return m?.[1];
58
+ } catch {
59
+ return undefined;
60
+ }
61
+ }
62
+
63
+ function tryPackageJsonName() {
64
+ try {
65
+ const pkg = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf8"));
66
+ return typeof pkg.name === "string" ? pkg.name : undefined;
67
+ } catch {
68
+ return undefined;
69
+ }
70
+ }
71
+
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();
76
+ if (gitName) return { name: gitName, source: "git remote origin" };
77
+ const pkgName = tryPackageJsonName();
78
+ if (pkgName) return { name: pkgName, source: "package.json name" };
79
+ return undefined;
80
+ }
81
+
82
+ const resolved = resolveSiteName();
83
+ if (!resolved) {
84
+ eprintln(
85
+ "Could not determine site name. Set DECO_SITE_NAME or run from a repo with a github.com 'origin' remote.",
86
+ );
87
+ process.exit(1);
88
+ }
89
+
90
+ let manifest;
91
+ try {
92
+ manifest = loadSiteManifest(DECO_START_PATH, resolved.name);
93
+ } catch (err) {
94
+ 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
+ );
99
+ process.exit(1);
100
+ }
101
+
102
+ const merged = mergeWithTemplate(loadTemplate(DECO_START_PATH), manifest);
103
+
104
+ const outputPath = resolve(process.cwd(), "wrangler.jsonc");
105
+ const header = `// AUTOGENERATED by deco-wrangler -- DO NOT COMMIT.
106
+ // Add wrangler.jsonc to .gitignore. Regenerated on every \`deco-wrangler\` run.
107
+ // Source: @decocms/start deploy/sites/${resolved.name}.jsonc + wrangler-template.jsonc
108
+ `;
109
+ writeFileSync(outputPath, `${header}${JSON.stringify(merged, null, 2)}\n`);
110
+
111
+ eprintln(
112
+ `Resolved site "${resolved.name}" -> worker "${manifest.worker_name}" (via ${resolved.source})`,
113
+ );
114
+ eprintln(`Generated ${outputPath}`);
115
+
116
+ const argv = process.argv.slice(2);
117
+ // `gen` mode: just write the config and exit. Used by package.json
118
+ // predev/prebuild/prepare hooks to keep wrangler.jsonc in sync with the
119
+ // registry without invoking wrangler itself.
120
+ if (argv[0] === "gen" || argv[0] === "generate") {
121
+ process.exit(0);
122
+ }
123
+
124
+ const wranglerArgs = ["wrangler", ...argv];
125
+ const result = spawnSync("npx", wranglerArgs, { stdio: "inherit" });
126
+ process.exit(result.status ?? 1);
@@ -5,7 +5,7 @@ import { log, logPhase } from "./types";
5
5
  import { generatePackageJson } from "./templates/package-json";
6
6
  import { generateTsconfig } from "./templates/tsconfig";
7
7
  import { generateViteConfig } from "./templates/vite-config";
8
- import { generateWrangler } from "./templates/wrangler";
8
+ import { generateGithubWorkflows } from "./templates/github-workflows";
9
9
  import { generateKnipConfig } from "./templates/knip-config";
10
10
  import { generateRoutes } from "./templates/routes";
11
11
  import { generateSetup } from "./templates/setup";
@@ -50,13 +50,20 @@ function writeMultiFile(ctx: MigrationContext, files: Record<string, string>) {
50
50
  export function scaffold(ctx: MigrationContext): void {
51
51
  logPhase("Scaffold");
52
52
 
53
- // Root config files
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
58
  writeFile(ctx, "package.json", generatePackageJson(ctx));
55
59
  writeFile(ctx, "tsconfig.json", generateTsconfig());
56
60
  writeFile(ctx, "vite.config.ts", generateViteConfig(ctx));
57
- writeFile(ctx, "wrangler.jsonc", generateWrangler(ctx));
58
61
  writeFile(ctx, "knip.config.ts", generateKnipConfig());
59
62
  writeFile(ctx, ".gitignore", generateGitignore());
63
+
64
+ // Caller workflow stubs that delegate to decocms/deco-start's reusable
65
+ // workflows. The customer repo holds no deploy logic of its own.
66
+ writeMultiFile(ctx, generateGithubWorkflows());
60
67
  writeFile(ctx, ".prettierrc", JSON.stringify({
61
68
  semi: true,
62
69
  singleQuote: false,
@@ -209,6 +216,9 @@ dist/
209
216
  # Cloudflare Workers
210
217
  .wrangler/
211
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.
221
+ wrangler.jsonc
212
222
 
213
223
  # TanStack Router (auto-generated)
214
224
  src/routeTree.gen.ts
@@ -13,7 +13,12 @@ const REQUIRED_FILES = [
13
13
  "package.json",
14
14
  "tsconfig.json",
15
15
  "vite.config.ts",
16
- "wrangler.jsonc",
16
+ // wrangler.jsonc is INTENTIONALLY absent -- D6: it lives in decocms/deco-start
17
+ // under deploy/sites/<repo>.jsonc and is generated locally by `deco-wrangler gen`.
18
+ ".github/workflows/deploy.yml",
19
+ ".github/workflows/preview.yml",
20
+ ".github/workflows/regen-blocks.yml",
21
+ ".github/workflows/sync-secrets.yml",
17
22
  "knip.config.ts",
18
23
  ".prettierrc",
19
24
  "src/server.ts",