@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.
- package/.agents/skills/deco-to-tanstack-migration/SKILL.md +1 -1
- package/.agents/skills/deco-to-tanstack-migration/references/vtex-commerce.md +5 -1
- package/.agents/skills/deco-to-tanstack-migration/references/worker-cloudflare.md +107 -10
- package/.agents/skills/deco-to-tanstack-migration/templates/package-json.md +5 -1
- package/.cursor/rules/migration-tooling-policy.mdc +22 -2
- package/.github/workflows/deploy.yml +115 -0
- package/.github/workflows/preview.yml +143 -0
- package/.github/workflows/regen-blocks.yml +56 -0
- package/.github/workflows/release.yml +26 -0
- package/.github/workflows/sync-secrets.yml +173 -0
- package/CODEOWNERS +16 -0
- package/MIGRATION_TOOLING_PLAN.md +16 -4
- package/README.md +178 -79
- package/deploy/README.md +85 -0
- package/deploy/sites/als-tanstack.jsonc +7 -0
- package/deploy/sites/americanas-tanstack.jsonc +4 -0
- package/deploy/sites/baggagio-tanstack.jsonc +4 -0
- package/deploy/sites/casaevideo-storefront.jsonc +11 -0
- package/deploy/sites/lebiscuit-tanstack.jsonc +19 -0
- package/deploy/sites/miess-01-tanstack.jsonc +8 -0
- package/deploy/wrangler-template.jsonc +28 -0
- package/package.json +18 -15
- package/scripts/deploy/build-wrangler-config.mjs +49 -0
- package/scripts/deploy/jsonc.mjs +76 -0
- package/scripts/deploy/resolve-site.mjs +58 -0
- package/scripts/deploy/site-registry.mjs +142 -0
- package/scripts/deploy/wrangler-wrapper.mjs +126 -0
- package/scripts/migrate/phase-scaffold.ts +13 -3
- package/scripts/migrate/phase-verify.ts +6 -1
- package/scripts/migrate/templates/github-workflows.ts +98 -0
- package/scripts/migrate/templates/package-json.ts +9 -2
- package/src/cms/resolve.ts +81 -63
- package/src/cms/sectionLoaders.ts +11 -0
- package/src/index.ts +3 -0
- package/src/sdk/cachedLoader.ts +36 -13
- package/src/sdk/composite.test.ts +121 -0
- package/src/sdk/composite.ts +114 -0
- package/src/sdk/instrumentedFetch.ts +56 -0
- package/src/sdk/logger.test.ts +135 -0
- package/src/sdk/logger.ts +166 -0
- package/src/sdk/observability.ts +75 -0
- package/src/sdk/otel.test.ts +59 -0
- package/src/sdk/otel.ts +270 -29
- package/src/sdk/otelAdapters.test.ts +135 -0
- package/src/sdk/otelAdapters.ts +401 -0
- package/src/sdk/sampler.test.ts +127 -0
- package/src/sdk/sampler.ts +183 -0
- package/src/sdk/workerEntry.ts +541 -476
- 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.
|
|
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 {
|
|
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
|
-
|
|
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",
|