@decocms/start 2.29.0 → 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 (31) 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/deploy/README.md +85 -0
  14. package/deploy/sites/als-tanstack.jsonc +7 -0
  15. package/deploy/sites/americanas-tanstack.jsonc +4 -0
  16. package/deploy/sites/baggagio-tanstack.jsonc +4 -0
  17. package/deploy/sites/casaevideo-storefront.jsonc +11 -0
  18. package/deploy/sites/lebiscuit-tanstack.jsonc +19 -0
  19. package/deploy/sites/miess-01-tanstack.jsonc +8 -0
  20. package/deploy/wrangler-template.jsonc +28 -0
  21. package/package.json +3 -2
  22. package/scripts/deploy/build-wrangler-config.mjs +49 -0
  23. package/scripts/deploy/jsonc.mjs +76 -0
  24. package/scripts/deploy/resolve-site.mjs +58 -0
  25. package/scripts/deploy/site-registry.mjs +142 -0
  26. package/scripts/deploy/wrangler-wrapper.mjs +126 -0
  27. package/scripts/migrate/phase-scaffold.ts +13 -3
  28. package/scripts/migrate/phase-verify.ts +6 -1
  29. package/scripts/migrate/templates/github-workflows.ts +98 -0
  30. package/scripts/migrate/templates/package-json.ts +9 -2
  31. package/scripts/migrate/templates/wrangler.ts +0 -30
@@ -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",
@@ -0,0 +1,98 @@
1
+ // Caller workflow stubs for new sites. Each stub delegates to a reusable
2
+ // workflow under `decocms/deco-start/.github/workflows/` -- the customer repo
3
+ // holds no deploy/build logic of its own. See D6 in
4
+ // `.cursor/rules/migration-tooling-policy.mdc` and the `deploy/` directory
5
+ // for the central registry contract.
6
+
7
+ const DEPLOY_YML = `name: Deploy
8
+
9
+ # Thin caller for decocms/deco-start's central deploy workflow.
10
+
11
+ on:
12
+ push:
13
+ branches: [main]
14
+
15
+ permissions:
16
+ contents: write
17
+
18
+ jobs:
19
+ deploy:
20
+ uses: decocms/deco-start/.github/workflows/deploy.yml@v2
21
+ secrets: inherit
22
+ `;
23
+
24
+ const PREVIEW_YML = `name: Preview
25
+
26
+ # Thin caller for decocms/deco-start's central preview workflow.
27
+
28
+ on:
29
+ repository_dispatch:
30
+ types: [preview-deploy]
31
+ pull_request:
32
+ types: [opened, synchronize, reopened]
33
+ push:
34
+ branches: ['env/**']
35
+
36
+ permissions:
37
+ contents: read
38
+ pull-requests: write
39
+ statuses: write
40
+
41
+ jobs:
42
+ preview:
43
+ uses: decocms/deco-start/.github/workflows/preview.yml@v2
44
+ secrets: inherit
45
+ `;
46
+
47
+ const REGEN_BLOCKS_YML = `name: Regenerate blocks.gen.json
48
+
49
+ # Thin caller for decocms/deco-start's central regen-blocks workflow.
50
+
51
+ on:
52
+ push:
53
+ branches: [main]
54
+ paths:
55
+ - ".deco/blocks/**"
56
+
57
+ permissions:
58
+ contents: write
59
+
60
+ jobs:
61
+ regen:
62
+ uses: decocms/deco-start/.github/workflows/regen-blocks.yml@v2
63
+ secrets: inherit
64
+ `;
65
+
66
+ const SYNC_SECRETS_YML = `name: Sync worker secrets
67
+
68
+ # Thin caller for decocms/deco-start's central sync-secrets workflow.
69
+
70
+ on:
71
+ workflow_dispatch:
72
+ inputs:
73
+ mode:
74
+ description: "dry-run = print diff only | apply = set secrets on worker"
75
+ required: true
76
+ default: "dry-run"
77
+ type: choice
78
+ options: [dry-run, apply]
79
+
80
+ permissions:
81
+ contents: read
82
+
83
+ jobs:
84
+ sync:
85
+ uses: decocms/deco-start/.github/workflows/sync-secrets.yml@v2
86
+ with:
87
+ mode: \${{ inputs.mode }}
88
+ secrets: inherit
89
+ `;
90
+
91
+ export function generateGithubWorkflows(): Record<string, string> {
92
+ return {
93
+ ".github/workflows/deploy.yml": DEPLOY_YML,
94
+ ".github/workflows/preview.yml": PREVIEW_YML,
95
+ ".github/workflows/regen-blocks.yml": REGEN_BLOCKS_YML,
96
+ ".github/workflows/sync-secrets.yml": SYNC_SECRETS_YML,
97
+ };
98
+ }
@@ -109,8 +109,15 @@ export function generatePackageJson(ctx: MigrationContext): string {
109
109
  build:
110
110
  "npm run generate:blocks && npm run generate:sections && npm run generate:loaders && npm run generate:schema && npm run generate:invoke && tsr generate && vite build",
111
111
  preview: "vite preview",
112
- deploy: "npm run build && wrangler deploy",
113
- types: "wrangler types",
112
+ // wrangler.jsonc is generated by `deco-wrangler gen` from the central
113
+ // registry in @decocms/start (D6). Predev/prebuild ensure it is up to
114
+ // date before vite runs the @cloudflare/vite-plugin which reads it.
115
+ "gen:wrangler": "deco-wrangler gen",
116
+ predev: "deco-wrangler gen",
117
+ prebuild: "deco-wrangler gen",
118
+ deploy:
119
+ "echo 'Production deploys are managed by .github/workflows/deploy.yml on push to main. For an emergency manual deploy run: npx deco-wrangler deploy'; exit 1",
120
+ types: "deco-wrangler types",
114
121
  typecheck: "tsc --noEmit",
115
122
  format: 'prettier --write "src/**/*.{ts,tsx}"',
116
123
  "format:check": 'prettier --check "src/**/*.{ts,tsx}"',
@@ -1,30 +0,0 @@
1
- import type { MigrationContext } from "../types";
2
-
3
- export function generateWrangler(ctx: MigrationContext): string {
4
- const workerName = ctx.siteName
5
- .toLowerCase()
6
- .replace(/[^a-z0-9-]/g, "-")
7
- .replace(/-+/g, "-");
8
-
9
- return `{
10
- "name": "${workerName}-tanstack",
11
- // TODO: Set your Cloudflare account_id for deployment
12
- // "account_id": "YOUR_ACCOUNT_ID",
13
- "compatibility_date": "2026-02-14",
14
- "compatibility_flags": ["nodejs_compat", "no_handle_cross_request_promise_resolution"],
15
- "main": "./src/worker-entry.ts",
16
- "workers_dev": true,
17
- "preview_urls": true,
18
- // Uncomment and set KV namespace ID for redirect/AB testing:
19
- // "kv_namespaces": [
20
- // { "binding": "SITES_KV", "id": "YOUR_KV_NAMESPACE_ID" }
21
- // ],
22
- "observability": {
23
- "logs": {
24
- "enabled": true,
25
- "invocation_logs": true
26
- }
27
- }
28
- }
29
- `;
30
- }