@decocms/blocks-cli 7.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.
Files changed (93) hide show
  1. package/package.json +44 -0
  2. package/scripts/analyze-traces.mjs +1117 -0
  3. package/scripts/audit-observability-config.test.ts +446 -0
  4. package/scripts/audit-observability-config.ts +511 -0
  5. package/scripts/deco-migrate-cli.ts +444 -0
  6. package/scripts/fast-deploy-kv.test.ts +131 -0
  7. package/scripts/generate-blocks.test.ts +94 -0
  8. package/scripts/generate-blocks.ts +274 -0
  9. package/scripts/generate-invoke.test.ts +195 -0
  10. package/scripts/generate-invoke.ts +469 -0
  11. package/scripts/generate-loaders.ts +217 -0
  12. package/scripts/generate-schema.ts +1287 -0
  13. package/scripts/generate-sections.ts +237 -0
  14. package/scripts/htmx-analyze.ts +226 -0
  15. package/scripts/lib/blocks-dedupe.test.ts +179 -0
  16. package/scripts/lib/blocks-dedupe.ts +142 -0
  17. package/scripts/lib/cf-kv-rest.ts +78 -0
  18. package/scripts/lib/jsonc.ts +122 -0
  19. package/scripts/lib/kv-snapshot.ts +51 -0
  20. package/scripts/lib/read-decofile.ts +70 -0
  21. package/scripts/lib/sync-helpers.ts +44 -0
  22. package/scripts/migrate/analyzers/htmx-analyze.test.ts +372 -0
  23. package/scripts/migrate/analyzers/htmx-analyze.ts +425 -0
  24. package/scripts/migrate/analyzers/island-classifier.ts +96 -0
  25. package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
  26. package/scripts/migrate/analyzers/section-metadata.ts +147 -0
  27. package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
  28. package/scripts/migrate/colors.ts +46 -0
  29. package/scripts/migrate/config.test.ts +202 -0
  30. package/scripts/migrate/config.ts +186 -0
  31. package/scripts/migrate/phase-analyze.test.ts +63 -0
  32. package/scripts/migrate/phase-analyze.ts +782 -0
  33. package/scripts/migrate/phase-cleanup-audit.test.ts +137 -0
  34. package/scripts/migrate/phase-cleanup-audit.ts +105 -0
  35. package/scripts/migrate/phase-cleanup.test.ts +141 -0
  36. package/scripts/migrate/phase-cleanup.ts +1588 -0
  37. package/scripts/migrate/phase-compile.test.ts +193 -0
  38. package/scripts/migrate/phase-compile.ts +177 -0
  39. package/scripts/migrate/phase-report.ts +243 -0
  40. package/scripts/migrate/phase-scaffold.ts +593 -0
  41. package/scripts/migrate/phase-transform.ts +310 -0
  42. package/scripts/migrate/phase-verify.test.ts +127 -0
  43. package/scripts/migrate/phase-verify.ts +572 -0
  44. package/scripts/migrate/post-cleanup/rules.ts +1708 -0
  45. package/scripts/migrate/post-cleanup/runner.test.ts +1771 -0
  46. package/scripts/migrate/post-cleanup/runner.ts +137 -0
  47. package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
  48. package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
  49. package/scripts/migrate/post-cleanup/types.ts +106 -0
  50. package/scripts/migrate/source-layout.test.ts +111 -0
  51. package/scripts/migrate/source-layout.ts +103 -0
  52. package/scripts/migrate/templates/app-css.ts +366 -0
  53. package/scripts/migrate/templates/cache-config.ts +26 -0
  54. package/scripts/migrate/templates/commerce-loaders.ts +230 -0
  55. package/scripts/migrate/templates/cursor-rules.test.ts +59 -0
  56. package/scripts/migrate/templates/cursor-rules.ts +70 -0
  57. package/scripts/migrate/templates/hooks.test.ts +141 -0
  58. package/scripts/migrate/templates/hooks.ts +152 -0
  59. package/scripts/migrate/templates/knip-config.ts +27 -0
  60. package/scripts/migrate/templates/lib-utils.test.ts +139 -0
  61. package/scripts/migrate/templates/lib-utils.ts +326 -0
  62. package/scripts/migrate/templates/lockfile-check-yml.test.ts +26 -0
  63. package/scripts/migrate/templates/lockfile-check-yml.ts +66 -0
  64. package/scripts/migrate/templates/package-json.ts +177 -0
  65. package/scripts/migrate/templates/routes.ts +237 -0
  66. package/scripts/migrate/templates/sdk-gen.ts +59 -0
  67. package/scripts/migrate/templates/section-loaders.ts +456 -0
  68. package/scripts/migrate/templates/server-entry.ts +561 -0
  69. package/scripts/migrate/templates/setup.ts +148 -0
  70. package/scripts/migrate/templates/tsconfig.ts +21 -0
  71. package/scripts/migrate/templates/types-gen.ts +174 -0
  72. package/scripts/migrate/templates/ui-components.ts +144 -0
  73. package/scripts/migrate/templates/vite-config.ts +101 -0
  74. package/scripts/migrate/transforms/dead-code.ts +455 -0
  75. package/scripts/migrate/transforms/deno-isms.ts +85 -0
  76. package/scripts/migrate/transforms/fresh-apis.ts +223 -0
  77. package/scripts/migrate/transforms/htmx-on-events.test.ts +305 -0
  78. package/scripts/migrate/transforms/htmx-on-events.ts +193 -0
  79. package/scripts/migrate/transforms/imports.ts +385 -0
  80. package/scripts/migrate/transforms/jsx.ts +317 -0
  81. package/scripts/migrate/transforms/section-conventions.ts +210 -0
  82. package/scripts/migrate/transforms/tailwind.ts +739 -0
  83. package/scripts/migrate/types.ts +244 -0
  84. package/scripts/migrate-blocks-to-kv.ts +104 -0
  85. package/scripts/migrate-post-cleanup.ts +191 -0
  86. package/scripts/migrate-to-cf-observability.test.ts +215 -0
  87. package/scripts/migrate-to-cf-observability.ts +699 -0
  88. package/scripts/migrate.ts +282 -0
  89. package/scripts/smoke-otlp-errorlog.ts +46 -0
  90. package/scripts/smoke-otlp-meter.ts +48 -0
  91. package/scripts/sync-blocks-to-kv.ts +153 -0
  92. package/scripts/tailwind-lint.ts +518 -0
  93. package/tsconfig.json +7 -0
@@ -0,0 +1,593 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { MigrationContext } from "./types";
4
+ import { log, logPhase } from "./types";
5
+ import { CANONICAL_BUN_VERSION, generatePackageJson } from "./templates/package-json";
6
+ import { generateLockfileCheckYml } from "./templates/lockfile-check-yml";
7
+ import { generateTsconfig } from "./templates/tsconfig";
8
+ import { generateViteConfig } from "./templates/vite-config";
9
+ import { generateKnipConfig } from "./templates/knip-config";
10
+ import { generateRoutes } from "./templates/routes";
11
+ import { generateSetup } from "./templates/setup";
12
+ import { generateServerEntry } from "./templates/server-entry";
13
+ import { generateAppCss } from "./templates/app-css";
14
+ import { generateTypeFiles } from "./templates/types-gen";
15
+ import { generateUiComponents } from "./templates/ui-components";
16
+ import { generateHooks } from "./templates/hooks";
17
+ import { generateCommerceLoaders } from "./templates/commerce-loaders";
18
+ import { generateSectionLoaders } from "./templates/section-loaders";
19
+ import { generateCacheConfig } from "./templates/cache-config";
20
+ import { generateSdkFiles } from "./templates/sdk-gen";
21
+ import { generateMigrationPolicyPointerRule } from "./templates/cursor-rules";
22
+ // `lib-utils` is imported lazily — see end of phase-cleanup. Eager
23
+ // generation of all 11 shims left every site with dead code that had
24
+ // to be cleaned up by hand.
25
+ import { extractTheme } from "./analyzers/theme-extractor";
26
+
27
+ function writeFile(ctx: MigrationContext, relPath: string, content: string) {
28
+ const fullPath = path.join(ctx.sourceDir, relPath);
29
+
30
+ if (ctx.dryRun) {
31
+ log(ctx, `[DRY] Would create: ${relPath}`);
32
+ ctx.scaffoldedFiles.push(relPath);
33
+ return;
34
+ }
35
+
36
+ const dir = path.dirname(fullPath);
37
+ fs.mkdirSync(dir, { recursive: true });
38
+
39
+ fs.writeFileSync(fullPath, content, "utf-8");
40
+ log(ctx, `Created: ${relPath}`);
41
+ ctx.scaffoldedFiles.push(relPath);
42
+ }
43
+
44
+ function writeMultiFile(ctx: MigrationContext, files: Record<string, string>) {
45
+ for (const [filePath, content] of Object.entries(files)) {
46
+ writeFile(ctx, filePath, content);
47
+ }
48
+ }
49
+
50
+ export function scaffold(ctx: MigrationContext): void {
51
+ logPhase("Scaffold");
52
+
53
+ // Root config files. wrangler.jsonc is intentionally NOT generated by
54
+ // the migration script -- it's per-site, lives in the site repo, and
55
+ // is wired by hand on first deploy (D6.3 interim state until the
56
+ // Cloudflare Workers Builds onboarding lands).
57
+ writeFile(ctx, "package.json", generatePackageJson(ctx));
58
+ writeFile(ctx, "tsconfig.json", generateTsconfig());
59
+ writeFile(ctx, "vite.config.ts", generateViteConfig(ctx));
60
+ writeFile(ctx, "knip.config.ts", generateKnipConfig());
61
+ writeFile(ctx, ".gitignore", generateGitignore());
62
+
63
+ // Deploy / preview pipelines are owned by Cloudflare Workers Builds
64
+ // configured per-worker in the CF dashboard (D6.3). The migration
65
+ // does NOT scaffold deploy/preview/sync-secrets workflows in the site
66
+ // repo; the operator wires the repo<->worker connection in the CF
67
+ // dashboard once after the first push.
68
+ writeFile(ctx, ".prettierrc", JSON.stringify({
69
+ semi: true,
70
+ singleQuote: false,
71
+ trailingComma: "all" as const,
72
+ printWidth: 100,
73
+ tabWidth: 2,
74
+ }, null, 2) + "\n");
75
+
76
+ // PR-time lockfile guardrail. Lives in the site repo (per-site, not
77
+ // a centralised reusable workflow) because per D6.3 we are not
78
+ // scaffolding caller stubs into storefronts. Bun version is pinned
79
+ // in lockstep with `package.json` via CANONICAL_BUN_VERSION.
80
+ writeFile(
81
+ ctx,
82
+ ".github/workflows/lockfile-check.yml",
83
+ generateLockfileCheckYml(CANONICAL_BUN_VERSION),
84
+ );
85
+
86
+ // Server entry files (server.ts, worker-entry.ts, router.tsx, runtime.ts, context.ts)
87
+ writeMultiFile(ctx, generateServerEntry(ctx));
88
+
89
+ // Route files
90
+ writeMultiFile(ctx, generateRoutes(ctx));
91
+
92
+ // Secrets — extract env vars referenced by source AppContext
93
+ // Must be generated BEFORE commerce-loaders and section-loaders since
94
+ // those templates check for the secrets file to wire `...secrets` spreads.
95
+ writeFile(ctx, "src/utils/secrets.ts", generateSecrets(ctx));
96
+
97
+ // Apps
98
+ writeFile(ctx, "src/apps/site.ts", generateSiteApp(ctx));
99
+
100
+ // account.json is copied from source (if exists) or generated as fallback
101
+ if (!ctx.files.some((f) => f.path === "account.json" && f.action !== "delete")) {
102
+ const accountName = ctx.vtexAccount || ctx.siteName;
103
+ writeFile(ctx, "src/account.json", JSON.stringify(accountName));
104
+ }
105
+
106
+ // Setup infrastructure
107
+ writeFile(ctx, "src/setup.ts", generateSetup(ctx));
108
+ writeFile(ctx, "src/cache-config.ts", generateCacheConfig(ctx));
109
+ writeFile(ctx, "src/setup/commerce-loaders.ts", generateCommerceLoaders(ctx));
110
+ writeFile(ctx, "src/setup/section-loaders.ts", generateSectionLoaders(ctx));
111
+
112
+ // Theme extraction + Styles
113
+ const theme = extractTheme(ctx);
114
+ writeFile(ctx, "src/styles/app.css", generateAppCss(ctx, theme));
115
+
116
+ // Type definitions
117
+ writeMultiFile(ctx, generateTypeFiles(ctx));
118
+
119
+ // UI components (Image, Picture, Video)
120
+ writeMultiFile(ctx, generateUiComponents(ctx));
121
+
122
+ // Platform hooks (useCart, useUser, useWishlist)
123
+ writeMultiFile(ctx, generateHooks(ctx));
124
+
125
+ // SDK shims + generated utilities
126
+ writeFile(ctx, "src/sdk/signal.ts", generateSignalShim());
127
+ writeFile(ctx, "src/sdk/clx.ts", generateClxShim());
128
+ writeFile(ctx, "src/sdk/debounce.ts", generateDebounceShim());
129
+ writeFile(ctx, "src/sdk/logger.ts", generateLoggerStub());
130
+ writeMultiFile(ctx, generateSdkFiles(ctx));
131
+
132
+ // VTEX utility wrappers (signature-compatible stubs) are no longer
133
+ // generated eagerly here. They're written lazily at end of phase-cleanup,
134
+ // after all import rewrites have run, so that we only emit shims that
135
+ // some file actually imports. See `writeImportedLibShims` in phase-cleanup.
136
+
137
+ // Replace Context-based useDevice with SSR-safe useSyncExternalStore version.
138
+ // @decocms/start shell-renders sections in a separate React root without
139
+ // Device.Provider, so the old createContext pattern throws during SSR.
140
+ writeFile(ctx, "src/contexts/device.tsx", generateDeviceContext());
141
+
142
+ // Location matcher — server-side geolocation matching
143
+ if (hasLocationMatcher(ctx)) {
144
+ writeFile(ctx, "src/matchers/location.ts", generateLocationMatcher());
145
+ }
146
+
147
+ // SiteTheme component (replaces apps/website/components/Theme.tsx)
148
+ const usesSiteTheme = ctx.files.some((f) => {
149
+ if (f.action === "delete") return false;
150
+ try {
151
+ const content = fs.readFileSync(f.absPath, "utf-8");
152
+ return content.includes("SiteTheme");
153
+ } catch {
154
+ return false;
155
+ }
156
+ });
157
+ if (usesSiteTheme) {
158
+ writeFile(ctx, "src/components/ui/Theme.tsx", generateSiteThemeComponent());
159
+ }
160
+
161
+ // Migration tooling policy pointer rule (D1–D5 + priorities).
162
+ // The canonical rule lives in decocms/deco-start; this is a tiny
163
+ // pointer that loads on every Cursor session in the migrated site
164
+ // so agents working on the site know where the policy is and what
165
+ // it means here. See MIGRATION_TOOLING_PLAN.md (Wave 12-H).
166
+ writeFile(
167
+ ctx,
168
+ ".cursor/rules/migration-tooling-policy.mdc",
169
+ generateMigrationPolicyPointerRule(ctx.siteName),
170
+ );
171
+
172
+ // Create public/ directory
173
+ if (!ctx.dryRun) {
174
+ fs.mkdirSync(path.join(ctx.sourceDir, "public"), { recursive: true });
175
+ }
176
+
177
+ console.log(` Scaffolded ${ctx.scaffoldedFiles.length} files`);
178
+ }
179
+
180
+ function generateSiteThemeComponent(): string {
181
+ return `export interface Font {
182
+ family: string;
183
+ styleSheet?: string;
184
+ }
185
+
186
+ export interface Props {
187
+ colorScheme?: "light" | "dark" | "any";
188
+ fonts?: Font[];
189
+ variables?: Array<{ name: string; value: string }>;
190
+ }
191
+
192
+ export default function SiteTheme({ variables, fonts, colorScheme }: Props) {
193
+ const cssVars = variables?.length
194
+ ? \`:root { \${variables.map((v) => \`\${v.name}: \${v.value};\`).join(" ")} }\`
195
+ : "";
196
+
197
+ const colorSchemeCss = colorScheme && colorScheme !== "any"
198
+ ? \`:root { color-scheme: \${colorScheme}; }\`
199
+ : "";
200
+
201
+ const css = [cssVars, colorSchemeCss].filter(Boolean).join("\\n");
202
+
203
+ return (
204
+ <>
205
+ {fonts?.map((font) =>
206
+ font.styleSheet ? (
207
+ <link key={font.family} rel="stylesheet" href={font.styleSheet} />
208
+ ) : null
209
+ )}
210
+ {css && <style dangerouslySetInnerHTML={{ __html: css }} />}
211
+ </>
212
+ );
213
+ }
214
+
215
+ export { type Font as SiteThemeFont };
216
+ `;
217
+ }
218
+
219
+ function generateGitignore(): string {
220
+ return `# Dependencies
221
+ node_modules/
222
+
223
+ # Build output
224
+ dist/
225
+ .cache/
226
+
227
+ # Cloudflare Workers
228
+ .wrangler/
229
+ .dev.vars
230
+
231
+ # TanStack Router (auto-generated)
232
+ src/routeTree.gen.ts
233
+ .tanstack/
234
+
235
+ # Vite
236
+ vite.config.timestamp_*
237
+ *.local
238
+
239
+ # Environment
240
+ .env
241
+ .env.*
242
+
243
+ # OS
244
+ .DS_Store
245
+
246
+ # Deco CMS
247
+ .deco/metadata/*
248
+
249
+ # IDE
250
+ .vscode/
251
+ .idea/
252
+
253
+ # Lockfiles — bun is canonical, prevent accidental drift
254
+ package-lock.json
255
+ yarn.lock
256
+ pnpm-lock.yaml
257
+ `;
258
+ }
259
+
260
+ function generateClxShim(): string {
261
+ return `/** Filter out nullable values, join and minify class names */
262
+ export const clx = (...args: (string | null | undefined | false)[]) =>
263
+ args.filter(Boolean).join(" ").replace(/\\s\\s+/g, " ");
264
+
265
+ /** Alias for compat — some files import as clsx */
266
+ export const clsx = clx;
267
+
268
+ export default clx;
269
+ `;
270
+ }
271
+
272
+ function generateLoggerStub(): string {
273
+ return `export const logger = {
274
+ error: console.error,
275
+ warn: console.warn,
276
+ info: console.info,
277
+ debug: console.debug,
278
+ log: console.log,
279
+ };
280
+ `;
281
+ }
282
+
283
+ function generateDebounceShim(): string {
284
+ return `/** Debounce a function call — drop-in replacement for Deno std/async/debounce */
285
+ export function debounce<T extends (...args: any[]) => any>(
286
+ fn: T,
287
+ delay = 250,
288
+ ): T & { clear(): void } {
289
+ let timer: ReturnType<typeof setTimeout> | undefined;
290
+
291
+ const debounced = ((...args: Parameters<T>) => {
292
+ if (timer !== undefined) clearTimeout(timer);
293
+ timer = setTimeout(() => {
294
+ timer = undefined;
295
+ fn(...args);
296
+ }, delay);
297
+ }) as T & { clear(): void };
298
+
299
+ debounced.clear = () => {
300
+ if (timer !== undefined) {
301
+ clearTimeout(timer);
302
+ timer = undefined;
303
+ }
304
+ };
305
+
306
+ return debounced;
307
+ }
308
+
309
+ export default debounce;
310
+ `;
311
+ }
312
+
313
+ function generateSignalShim(): string {
314
+ return `import { useState, useRef, useEffect, useMemo, useCallback } from "react";
315
+ export { signal, type ReactiveSignal } from "@decocms/start/sdk/signal";
316
+
317
+ /** Run a function immediately. Kept for legacy module-level side effects. */
318
+ export function effect(fn: () => void | (() => void)): () => void {
319
+ const cleanup = fn();
320
+ return typeof cleanup === "function" ? cleanup : () => {};
321
+ }
322
+
323
+ /**
324
+ * React shim for @preact/signals' useSignal.
325
+ * Returns a mutable ref-like object with a .value property that triggers re-renders.
326
+ */
327
+ export function useSignal<T>(initialValue: T): { value: T } {
328
+ const [value, setValue] = useState<T>(initialValue);
329
+ const ref = useRef(value);
330
+ ref.current = value;
331
+ return useMemo(
332
+ () => ({
333
+ get value() { return ref.current; },
334
+ set value(v: T) {
335
+ ref.current = v;
336
+ setValue(v);
337
+ },
338
+ }),
339
+ [],
340
+ );
341
+ }
342
+
343
+ /**
344
+ * React shim for @preact/signals' useComputed.
345
+ * Re-evaluates when deps change (but since we don't track signals, it runs every render).
346
+ */
347
+ export function useComputed<T>(compute: () => T): { readonly value: T } {
348
+ const [value, setValue] = useState<T>(compute);
349
+ useEffect(() => { setValue(compute()); });
350
+ return useMemo(() => ({ get value() { return value; } }), [value]);
351
+ }
352
+
353
+ /**
354
+ * React shim for @preact/signals' useSignalEffect.
355
+ * Runs the callback as a useEffect (no automatic signal tracking).
356
+ */
357
+ export function useSignalEffect(cb: () => void | (() => void)): void {
358
+ useEffect(cb);
359
+ }
360
+ `;
361
+ }
362
+
363
+ function generateDeviceContext(): string {
364
+ return `import { useSyncExternalStore } from "react";
365
+
366
+ const MOBILE_QUERY = "(max-width: 767px)";
367
+
368
+ function subscribe(cb: () => void) {
369
+ if (typeof window === "undefined") return () => {};
370
+ const mql = window.matchMedia(MOBILE_QUERY);
371
+ mql.addEventListener("change", cb);
372
+ return () => mql.removeEventListener("change", cb);
373
+ }
374
+
375
+ function getSnapshot(): boolean {
376
+ return window.matchMedia(MOBILE_QUERY).matches;
377
+ }
378
+
379
+ function getServerSnapshot(): boolean {
380
+ return false;
381
+ }
382
+
383
+ /**
384
+ * Reactive mobile detection based on viewport width via matchMedia.
385
+ * SSR defaults to desktop (false); hydrates to the real value on mount.
386
+ *
387
+ * For server-side device detection (UA-based), use the section loader
388
+ * pattern: registerSectionLoaders injects \`isMobile\` as a prop.
389
+ */
390
+ export const useDevice = () => {
391
+ const isMobile = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
392
+ return { isMobile };
393
+ };
394
+ `;
395
+ }
396
+
397
+ function generateSiteApp(ctx: MigrationContext): string {
398
+ // Try to read source site.ts to extract secrets and AppContext shape
399
+ const secretFields = extractSecretFields(ctx);
400
+
401
+ let secretImport = "";
402
+ let secretTypes = "";
403
+ if (secretFields.length > 0) {
404
+ secretImport = `\nimport type { Secret } from "~/utils/secrets";\n`;
405
+ secretTypes = secretFields.map((f) => ` ${f}: Secret;`).join("\n");
406
+ }
407
+
408
+ const vtexAccount = ctx.vtexAccount || ctx.siteName;
409
+
410
+ return `export type Platform =
411
+ | "vtex"
412
+ | "vnda"
413
+ | "shopify"
414
+ | "wake"
415
+ | "linx"
416
+ | "nuvemshop"
417
+ | "custom";
418
+
419
+ export const _platform: Platform = "${ctx.platform}";
420
+ ${secretImport}
421
+ export type AppContext = {
422
+ device: "mobile" | "desktop" | "tablet";
423
+ platform: Platform;${secretTypes ? `\n${secretTypes}` : ""}${ctx.platform === "vtex" ? `\n account: string;` : ""}
424
+ };
425
+ `;
426
+ }
427
+
428
+ function extractSecretFields(ctx: MigrationContext): string[] {
429
+ const siteAppPaths = [
430
+ path.join(ctx.sourceDir, "apps", "site.ts"),
431
+ path.join(ctx.sourceDir, "src", "apps", "site.ts"),
432
+ ];
433
+
434
+ for (const p of siteAppPaths) {
435
+ if (!fs.existsSync(p)) continue;
436
+ const content = fs.readFileSync(p, "utf-8");
437
+ const secretRe = /(\w+):\s*Secret\b/g;
438
+ const fields: string[] = [];
439
+ let match;
440
+ while ((match = secretRe.exec(content)) !== null) {
441
+ fields.push(match[1]);
442
+ }
443
+ return fields;
444
+ }
445
+ return [];
446
+ }
447
+
448
+ function generateSecrets(ctx: MigrationContext): string {
449
+ const fields = extractSecretFields(ctx);
450
+
451
+ if (fields.length === 0) {
452
+ return `export interface Secret {
453
+ get(): string;
454
+ }
455
+
456
+ function envSecret(envKey: string): Secret {
457
+ return {
458
+ get: () => (process.env[envKey] as string) ?? "",
459
+ };
460
+ }
461
+
462
+ export const secrets = {} as const;
463
+ `;
464
+ }
465
+
466
+ const envKeyMap: Record<string, string> = {
467
+ GatewayApiKey: "GATEWAY_API_KEY",
468
+ topsortkey: "TOPSORT_KEY",
469
+ yourviewsToken: "YOURVIEWS_TOKEN",
470
+ pickuppointsAppKey: "PICKUPPOINTS_APP_KEY",
471
+ pickuppointsAppToken: "PICKUPPOINTS_APP_TOKEN",
472
+ SAPUser: "SAP_USER",
473
+ SAPPassword: "SAP_PASSWORD",
474
+ };
475
+
476
+ const entries = fields.map((f) => {
477
+ const envKey = envKeyMap[f] || f.replace(/([A-Z])/g, "_$1").toUpperCase();
478
+ return ` ${f}: envSecret("${envKey}"),`;
479
+ });
480
+
481
+ return `export interface Secret {
482
+ get(): string;
483
+ }
484
+
485
+ function envSecret(envKey: string): Secret {
486
+ return {
487
+ get: () => (process.env[envKey] as string) ?? "",
488
+ };
489
+ }
490
+
491
+ /**
492
+ * All site-level secrets, sourced from Cloudflare Worker env bindings
493
+ * (process.env is polyfilled by nodejs_compat).
494
+ *
495
+ * Local dev: .dev.vars
496
+ * Production: \`wrangler secret put <KEY>\`
497
+ */
498
+ export const secrets = {
499
+ ${entries.join("\n")}
500
+ } as const;
501
+ `;
502
+ }
503
+
504
+ function hasLocationMatcher(ctx: MigrationContext): boolean {
505
+ const dirs = [
506
+ path.join(ctx.sourceDir, "matchers"),
507
+ path.join(ctx.sourceDir, "src", "matchers"),
508
+ ];
509
+ for (const dir of dirs) {
510
+ if (fs.existsSync(dir)) {
511
+ const files = fs.readdirSync(dir);
512
+ if (files.some((f) => f.includes("location"))) return true;
513
+ }
514
+ }
515
+ // Also check .deco/blocks for location matcher references
516
+ const blocksDir = path.join(ctx.sourceDir, ".deco", "blocks");
517
+ if (fs.existsSync(blocksDir)) {
518
+ for (const file of fs.readdirSync(blocksDir)) {
519
+ if (!file.endsWith(".json")) continue;
520
+ try {
521
+ const content = fs.readFileSync(path.join(blocksDir, file), "utf-8");
522
+ if (content.includes("website/matchers/location")) return true;
523
+ } catch { /* skip */ }
524
+ }
525
+ }
526
+ return false;
527
+ }
528
+
529
+ function generateLocationMatcher(): string {
530
+ return `/**
531
+ * Server-side location matcher for website/matchers/location.ts
532
+ *
533
+ * Reads CF geolocation data injected as internal cookies by worker-entry.ts
534
+ * (__cf_geo_region, __cf_geo_country, __cf_geo_city) to evaluate location
535
+ * rules server-side.
536
+ */
537
+
538
+ import { registerMatcher } from "@decocms/start/cms";
539
+ import type { MatcherContext } from "@decocms/start/cms";
540
+
541
+ const COUNTRY_NAME_TO_CODE: Record<string, string> = {
542
+ Brasil: "BR",
543
+ Brazil: "BR",
544
+ Argentina: "AR",
545
+ Chile: "CL",
546
+ Colombia: "CO",
547
+ Uruguay: "UY",
548
+ Paraguay: "PY",
549
+ Peru: "PE",
550
+ "United States": "US",
551
+ Portugal: "PT",
552
+ };
553
+
554
+ interface LocationRule {
555
+ regionCode?: string;
556
+ country?: string;
557
+ city?: string;
558
+ }
559
+
560
+ function matchesRule(loc: LocationRule, region: string, country: string, city: string): boolean {
561
+ if (loc.country) {
562
+ const code = COUNTRY_NAME_TO_CODE[loc.country] ?? loc.country;
563
+ if (code !== country) return false;
564
+ }
565
+ if (loc.regionCode && loc.regionCode !== region) return false;
566
+ if (loc.city && loc.city.toLowerCase() !== city.toLowerCase()) return false;
567
+ return true;
568
+ }
569
+
570
+ export function registerLocationMatcher(): void {
571
+ registerMatcher(
572
+ "website/matchers/location.ts",
573
+ (rule: Record<string, unknown>, ctx: MatcherContext): boolean => {
574
+ const includeLocations = (rule.includeLocations as LocationRule[] | undefined) ?? [];
575
+ const excludeLocations = (rule.excludeLocations as LocationRule[] | undefined) ?? [];
576
+
577
+ const cookies = ctx.cookies ?? {};
578
+ const region = cookies.__cf_geo_region ? decodeURIComponent(cookies.__cf_geo_region) : "";
579
+ const country = cookies.__cf_geo_country ? decodeURIComponent(cookies.__cf_geo_country) : "";
580
+ const city = cookies.__cf_geo_city ? decodeURIComponent(cookies.__cf_geo_city) : "";
581
+
582
+ if (excludeLocations.some((loc) => matchesRule(loc, region, country, city))) {
583
+ return false;
584
+ }
585
+
586
+ if (includeLocations.length === 0) return true;
587
+
588
+ return includeLocations.some((loc) => matchesRule(loc, region, country, city));
589
+ },
590
+ );
591
+ }
592
+ `;
593
+ }