@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,572 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { MigrationContext } from "./types";
4
+ import { logPhase } from "./types";
5
+
6
+ interface Check {
7
+ name: string;
8
+ fn: (ctx: MigrationContext) => boolean;
9
+ severity: "error" | "warning";
10
+ }
11
+
12
+ const REQUIRED_FILES = [
13
+ "package.json",
14
+ "tsconfig.json",
15
+ "vite.config.ts",
16
+ // Deploy / preview / sync-secrets pipelines are owned by Cloudflare
17
+ // Workers Builds (D6.3) -- configured in the CF dashboard, not via
18
+ // GitHub workflow files in the site repo.
19
+ ".github/workflows/regen-blocks.yml",
20
+ ".github/workflows/lockfile-check.yml",
21
+ "knip.config.ts",
22
+ ".prettierrc",
23
+ "src/server.ts",
24
+ "src/worker-entry.ts",
25
+ "src/router.tsx",
26
+ "src/runtime.ts",
27
+ "src/context.ts",
28
+ "src/setup.ts",
29
+ "src/cache-config.ts",
30
+ "src/setup/commerce-loaders.ts",
31
+ "src/setup/section-loaders.ts",
32
+ "src/styles/app.css",
33
+ "src/apps/site.ts",
34
+ "src/hooks/useCart.ts",
35
+ "src/hooks/useUser.ts",
36
+ "src/hooks/useWishlist.ts",
37
+ // src/types/widgets.ts intentionally omitted — provided by the
38
+ // framework at `@decocms/start/types/widgets`; sites no longer
39
+ // shadow the file locally.
40
+ "src/types/deco.ts",
41
+ "src/types/commerce-app.ts",
42
+ "src/components/ui/Image.tsx",
43
+ "src/components/ui/Picture.tsx",
44
+ "src/routes/__root.tsx",
45
+ "src/routes/index.tsx",
46
+ "src/routes/$.tsx",
47
+ "src/routes/deco/meta.ts",
48
+ "src/routes/deco/invoke.$.ts",
49
+ "src/routes/deco/render.ts",
50
+ ];
51
+
52
+ const MUST_NOT_EXIST = [
53
+ "deno.json",
54
+ "fresh.gen.ts",
55
+ "manifest.gen.ts",
56
+ "dev.ts",
57
+ "main.ts",
58
+ "routes/_app.tsx",
59
+ "routes/_middleware.ts",
60
+ ];
61
+
62
+ export const checks: Check[] = [
63
+ {
64
+ name: "All scaffolded files exist",
65
+ severity: "error",
66
+ fn: (ctx) => {
67
+ const missing = REQUIRED_FILES.filter(
68
+ (f) => !fs.existsSync(path.join(ctx.sourceDir, f)),
69
+ );
70
+ if (missing.length > 0) {
71
+ console.log(` Missing: ${missing.join(", ")}`);
72
+ return false;
73
+ }
74
+ return true;
75
+ },
76
+ },
77
+ {
78
+ name: "Old artifacts removed",
79
+ severity: "error",
80
+ fn: (ctx) => {
81
+ const remaining = MUST_NOT_EXIST.filter(
82
+ (f) => fs.existsSync(path.join(ctx.sourceDir, f)),
83
+ );
84
+ if (remaining.length > 0) {
85
+ console.log(` Still exists: ${remaining.join(", ")}`);
86
+ return false;
87
+ }
88
+ return true;
89
+ },
90
+ },
91
+ {
92
+ name: "No preact imports in src/",
93
+ severity: "error",
94
+ fn: (ctx) => {
95
+ const srcDir = path.join(ctx.sourceDir, "src");
96
+ if (!fs.existsSync(srcDir)) return true;
97
+ const bad = findFilesWithPattern(srcDir, /from\s+["']preact/);
98
+ if (bad.length > 0) {
99
+ console.log(` Still has preact imports: ${bad.join(", ")}`);
100
+ return false;
101
+ }
102
+ return true;
103
+ },
104
+ },
105
+ {
106
+ name: "No $fresh imports in src/",
107
+ severity: "error",
108
+ fn: (ctx) => {
109
+ const srcDir = path.join(ctx.sourceDir, "src");
110
+ if (!fs.existsSync(srcDir)) return true;
111
+ const bad = findFilesWithPattern(srcDir, /from\s+["']\$fresh/);
112
+ if (bad.length > 0) {
113
+ console.log(` Still has $fresh imports: ${bad.join(", ")}`);
114
+ return false;
115
+ }
116
+ return true;
117
+ },
118
+ },
119
+ {
120
+ name: "No deno-lint-ignore in src/",
121
+ severity: "warning",
122
+ fn: (ctx) => {
123
+ const srcDir = path.join(ctx.sourceDir, "src");
124
+ if (!fs.existsSync(srcDir)) return true;
125
+ const bad = findFilesWithPattern(srcDir, /deno-lint-ignore/);
126
+ if (bad.length > 0) {
127
+ console.log(` Still has deno-lint-ignore: ${bad.join(", ")}`);
128
+ return false;
129
+ }
130
+ return true;
131
+ },
132
+ },
133
+ {
134
+ name: 'No class= in JSX (should be className=)',
135
+ severity: "warning",
136
+ fn: (ctx) => {
137
+ const srcDir = path.join(ctx.sourceDir, "src");
138
+ if (!fs.existsSync(srcDir)) return true;
139
+ const bad = findFilesWithPattern(srcDir, /<[a-zA-Z][^>]*\sclass\s*=/);
140
+ if (bad.length > 0) {
141
+ console.log(` Still has class= in JSX: ${bad.slice(0, 5).join(", ")}${bad.length > 5 ? ` (+${bad.length - 5} more)` : ""}`);
142
+ return false;
143
+ }
144
+ return true;
145
+ },
146
+ },
147
+ {
148
+ name: "public/ has static assets",
149
+ severity: "warning",
150
+ fn: (ctx) => {
151
+ const publicDir = path.join(ctx.sourceDir, "public");
152
+ if (!fs.existsSync(publicDir)) {
153
+ console.log(" public/ directory missing");
154
+ return false;
155
+ }
156
+ const hasSprites = fs.existsSync(
157
+ path.join(publicDir, "sprites.svg"),
158
+ );
159
+ const hasFavicon = fs.existsSync(
160
+ path.join(publicDir, "favicon.ico"),
161
+ );
162
+ if (!hasSprites) console.log(" Missing: public/sprites.svg");
163
+ if (!hasFavicon) console.log(" Missing: public/favicon.ico");
164
+ return hasSprites && hasFavicon;
165
+ },
166
+ },
167
+ {
168
+ name: "package.json has correct dependencies",
169
+ severity: "error",
170
+ fn: (ctx) => {
171
+ const pkgPath = path.join(ctx.sourceDir, "package.json");
172
+ if (!fs.existsSync(pkgPath)) return false;
173
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
174
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
175
+ const required = [
176
+ "@decocms/start",
177
+ "@decocms/apps",
178
+ "react",
179
+ "react-dom",
180
+ "@tanstack/react-start",
181
+ "vite",
182
+ "knip",
183
+ ];
184
+ const missing = required.filter((d) => !deps[d]);
185
+ if (missing.length > 0) {
186
+ console.log(` Missing deps: ${missing.join(", ")}`);
187
+ return false;
188
+ }
189
+ return true;
190
+ },
191
+ },
192
+ {
193
+ name: "No site/ or $store/ imports (should be ~/)",
194
+ severity: "warning",
195
+ fn: (ctx) => {
196
+ const srcDir = path.join(ctx.sourceDir, "src");
197
+ if (!fs.existsSync(srcDir)) return true;
198
+ const bad = findFilesWithPattern(srcDir, /from\s+["'](site\/|\$store\/)/);
199
+ if (bad.length > 0) {
200
+ console.log(` Still has site/ or $store/ imports: ${bad.join(", ")}`);
201
+ return false;
202
+ }
203
+ return true;
204
+ },
205
+ },
206
+ {
207
+ name: "No .ts/.tsx extensions in relative import paths",
208
+ severity: "warning",
209
+ fn: (ctx) => {
210
+ const srcDir = path.join(ctx.sourceDir, "src");
211
+ if (!fs.existsSync(srcDir)) return true;
212
+ // Match relative imports with .ts/.tsx extensions
213
+ const bad = findFilesWithPattern(srcDir, /from\s+["'](?:\.\.?\/|~\/)[^"']*\.tsx?["']/);
214
+ if (bad.length > 0) {
215
+ console.log(` Still has .ts/.tsx extensions in imports: ${bad.join(", ")}`);
216
+ return false;
217
+ }
218
+ return true;
219
+ },
220
+ },
221
+ {
222
+ name: "No for= in JSX (should be htmlFor=)",
223
+ severity: "warning",
224
+ fn: (ctx) => {
225
+ const srcDir = path.join(ctx.sourceDir, "src");
226
+ if (!fs.existsSync(srcDir)) return true;
227
+ const bad = findFilesWithPattern(srcDir, /<label[^>]*\sfor\s*=/);
228
+ if (bad.length > 0) {
229
+ console.log(` Still has for= in JSX: ${bad.join(", ")}`);
230
+ return false;
231
+ }
232
+ return true;
233
+ },
234
+ },
235
+ {
236
+ // Only flag imports to files that phase-cleanup actually deletes:
237
+ // `sdk/clx.ts`, `sdk/useId.ts`, `sdk/usePlatform.tsx`. Earlier versions
238
+ // also matched `useOffer` and `useVariantPossiblities`, which are
239
+ // explicitly KEPT as site files (RELATIVE_SDK_REWRITES in
240
+ // transforms/imports.ts skips them on purpose because sites customize
241
+ // them). That false positive made every Magento/custom-SDK migration
242
+ // fail verify even when the imports were valid. See #212.
243
+ name: "No relative imports to deleted SDK files",
244
+ severity: "error",
245
+ fn: (ctx) => {
246
+ const srcDir = path.join(ctx.sourceDir, "src");
247
+ if (!fs.existsSync(srcDir)) return true;
248
+ const pattern = /from\s+["'](?:\.\.?\/)[^"']*\/sdk\/(?:clx|useId|usePlatform)(?:\.tsx?)?["']/;
249
+ const bad = findMatchesWithPattern(srcDir, pattern);
250
+ if (bad.length > 0) {
251
+ for (const { file, line } of bad) {
252
+ console.log(` ${file}: ${line.trim()}`);
253
+ }
254
+ return false;
255
+ }
256
+ return true;
257
+ },
258
+ },
259
+ {
260
+ name: "No negative z-index on non-image elements",
261
+ severity: "warning",
262
+ fn: (ctx) => {
263
+ const srcDir = path.join(ctx.sourceDir, "src");
264
+ if (!fs.existsSync(srcDir)) return true;
265
+ // Find -z-{n} that are NOT on img/Image elements (those are auto-fixed to z-0)
266
+ const bad = findFilesWithPattern(srcDir, /(?<!<(?:img|Image)[^>]*)-z-\d+/);
267
+ if (bad.length > 0) {
268
+ console.log(` Negative z-index on non-image elements: ${bad.join(", ")}`);
269
+ console.log(` These may be invisible due to stacking contexts. Replace with z-0 or positive z-index.`);
270
+ return false;
271
+ }
272
+ return true;
273
+ },
274
+ },
275
+ {
276
+ name: "No imports to deleted static files",
277
+ severity: "error",
278
+ fn: (ctx) => {
279
+ const srcDir = path.join(ctx.sourceDir, "src");
280
+ if (!fs.existsSync(srcDir)) return true;
281
+ const bad = findFilesWithPattern(srcDir, /from\s+["'][^"']*static\/adminIcons/);
282
+ if (bad.length > 0) {
283
+ console.log(` Still has imports to static/adminIcons: ${bad.join(", ")}`);
284
+ return false;
285
+ }
286
+ return true;
287
+ },
288
+ },
289
+ {
290
+ name: "No dead cache/cacheKey exports (old SWR system)",
291
+ severity: "warning",
292
+ fn: (ctx) => {
293
+ const srcDir = path.join(ctx.sourceDir, "src");
294
+ if (!fs.existsSync(srcDir)) return true;
295
+ // Only flag the OLD cache patterns: cache = "stale-while-revalidate" or cache = { maxAge: ... }
296
+ // NOT the new-stack section convention: cache = "listing" / "product" / "search" / "static"
297
+ const bad = findFilesWithPattern(srcDir, /^export\s+const\s+cacheKey\s*=/m);
298
+ if (bad.length > 0) {
299
+ console.log(` Dead cacheKey exports found: ${bad.join(", ")}`);
300
+ return false;
301
+ }
302
+ return true;
303
+ },
304
+ },
305
+ {
306
+ name: "invoke.* calls have runtime.ts proxy available",
307
+ severity: "warning",
308
+ fn: (ctx) => {
309
+ const srcDir = path.join(ctx.sourceDir, "src");
310
+ if (!fs.existsSync(srcDir)) return true;
311
+ const hasInvoke = findFilesWithPattern(srcDir, /\binvoke\.\w+\.\w+/);
312
+ if (hasInvoke.length > 0) {
313
+ // Check that runtime.ts exists and has the invoke proxy
314
+ const runtimePath = path.join(srcDir, "runtime.ts");
315
+ if (!fs.existsSync(runtimePath)) {
316
+ console.log(` Files use invoke.* but src/runtime.ts is missing: ${hasInvoke.join(", ")}`);
317
+ return false;
318
+ }
319
+ }
320
+ return true;
321
+ },
322
+ },
323
+ {
324
+ name: "No Deno-only crypto.subtle.digestSync",
325
+ severity: "warning",
326
+ fn: (ctx) => {
327
+ const srcDir = path.join(ctx.sourceDir, "src");
328
+ if (!fs.existsSync(srcDir)) return true;
329
+ const bad = findFilesWithPattern(srcDir, /digestSync/);
330
+ if (bad.length > 0) {
331
+ console.log(` Deno-only digestSync found: ${bad.join(", ")}`);
332
+ return false;
333
+ }
334
+ return true;
335
+ },
336
+ },
337
+ {
338
+ name: ".gitignore exists with new stack entries",
339
+ severity: "warning",
340
+ fn: (ctx) => {
341
+ const gitignorePath = path.join(ctx.sourceDir, ".gitignore");
342
+ if (!fs.existsSync(gitignorePath)) {
343
+ console.log(" .gitignore missing");
344
+ return false;
345
+ }
346
+ const content = fs.readFileSync(gitignorePath, "utf-8");
347
+ const required = [".wrangler/", "node_modules/", ".tanstack/", "src/routeTree.gen.ts"];
348
+ const missing = required.filter((r) => !content.includes(r));
349
+ if (missing.length > 0) {
350
+ console.log(` .gitignore missing entries: ${missing.join(", ")}`);
351
+ return false;
352
+ }
353
+ return true;
354
+ },
355
+ },
356
+ {
357
+ name: "No HTMX attributes (hx-*) in components",
358
+ severity: "warning",
359
+ fn: (ctx) => {
360
+ const srcDir = path.join(ctx.sourceDir, "src");
361
+ if (!fs.existsSync(srcDir)) return true;
362
+ const bad = findFilesWithPattern(srcDir, /\bhx-(?:get|post|put|delete|patch|trigger|target|swap|on|indicator|sync|select)\b/);
363
+ if (bad.length > 0) {
364
+ console.log(` HTMX attributes found (needs manual React migration): ${bad.join(", ")}`);
365
+ return false;
366
+ }
367
+ return true;
368
+ },
369
+ },
370
+ {
371
+ name: "No @deco/deco imports in src/",
372
+ severity: "error",
373
+ fn: (ctx) => {
374
+ const srcDir = path.join(ctx.sourceDir, "src");
375
+ if (!fs.existsSync(srcDir)) return true;
376
+ const bad = findFilesWithPattern(srcDir, /from\s+["']@deco\/deco/);
377
+ if (bad.length > 0) {
378
+ console.log(` Still has @deco/deco imports: ${bad.join(", ")}`);
379
+ return false;
380
+ }
381
+ return true;
382
+ },
383
+ },
384
+ {
385
+ name: "No apps/ imports in src/ (should be @decocms/apps or ~/)",
386
+ severity: "error",
387
+ fn: (ctx) => {
388
+ const srcDir = path.join(ctx.sourceDir, "src");
389
+ if (!fs.existsSync(srcDir)) return true;
390
+ const bad = findFilesWithPattern(srcDir, /from\s+["']apps\//);
391
+ if (bad.length > 0) {
392
+ console.log(` Still has apps/ imports: ${bad.join(", ")}`);
393
+ return false;
394
+ }
395
+ return true;
396
+ },
397
+ },
398
+ {
399
+ name: "Setup infrastructure is complete",
400
+ severity: "error",
401
+ fn: (ctx) => {
402
+ const setupFiles = [
403
+ "src/setup.ts",
404
+ "src/cache-config.ts",
405
+ "src/setup/commerce-loaders.ts",
406
+ "src/setup/section-loaders.ts",
407
+ ];
408
+ const missing = setupFiles.filter(
409
+ (f) => !fs.existsSync(path.join(ctx.sourceDir, f)),
410
+ );
411
+ if (missing.length > 0) {
412
+ console.log(` Missing setup infrastructure: ${missing.join(", ")}`);
413
+ return false;
414
+ }
415
+ return true;
416
+ },
417
+ },
418
+ {
419
+ name: "No islands/ directory (should be eliminated)",
420
+ severity: "warning",
421
+ fn: (ctx) => {
422
+ const islandsDir = path.join(ctx.sourceDir, "src", "islands");
423
+ if (fs.existsSync(islandsDir)) {
424
+ try {
425
+ const files = fs.readdirSync(islandsDir, { recursive: true });
426
+ const tsxFiles = (files as string[]).filter((f: string) => f.endsWith(".tsx") || f.endsWith(".ts"));
427
+ if (tsxFiles.length > 0) {
428
+ console.log(` src/islands/ still has ${tsxFiles.length} files — should be moved to components/`);
429
+ return false;
430
+ }
431
+ } catch {}
432
+ }
433
+ return true;
434
+ },
435
+ },
436
+ {
437
+ name: "Hooks are scaffolded",
438
+ severity: "warning",
439
+ fn: (ctx) => {
440
+ const hookFiles = [
441
+ "src/hooks/useCart.ts",
442
+ "src/hooks/useUser.ts",
443
+ "src/hooks/useWishlist.ts",
444
+ ];
445
+ const missing = hookFiles.filter(
446
+ (f) => !fs.existsSync(path.join(ctx.sourceDir, f)),
447
+ );
448
+ if (missing.length > 0) {
449
+ console.log(` Missing hooks: ${missing.join(", ")}`);
450
+ return false;
451
+ }
452
+ return true;
453
+ },
454
+ },
455
+ {
456
+ name: "Type files are scaffolded",
457
+ severity: "warning",
458
+ fn: (ctx) => {
459
+ const typeFiles = [
460
+ // widgets.ts is provided by @decocms/start/types/widgets, not
461
+ // scaffolded locally.
462
+ "src/types/deco.ts",
463
+ "src/types/commerce-app.ts",
464
+ ];
465
+ const missing = typeFiles.filter(
466
+ (f) => !fs.existsSync(path.join(ctx.sourceDir, f)),
467
+ );
468
+ if (missing.length > 0) {
469
+ console.log(` Missing type files: ${missing.join(", ")}`);
470
+ return false;
471
+ }
472
+ return true;
473
+ },
474
+ },
475
+ ];
476
+
477
+ function findFilesWithPattern(
478
+ dir: string,
479
+ pattern: RegExp,
480
+ results: string[] = [],
481
+ baseDir?: string,
482
+ ): string[] {
483
+ const root = baseDir ?? dir;
484
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
485
+ for (const entry of entries) {
486
+ const fullPath = path.join(dir, entry.name);
487
+ if (entry.isDirectory()) {
488
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "server") continue;
489
+ findFilesWithPattern(fullPath, pattern, results, root);
490
+ } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
491
+ const content = fs.readFileSync(fullPath, "utf-8");
492
+ // Only test non-comment lines
493
+ const uncommented = content
494
+ .split("\n")
495
+ .filter((line) => !line.trimStart().startsWith("//") && !line.trimStart().startsWith("*"))
496
+ .join("\n");
497
+ if (pattern.test(uncommented)) {
498
+ results.push(path.basename(fullPath));
499
+ }
500
+ }
501
+ }
502
+ return results;
503
+ }
504
+
505
+ /**
506
+ * Like {@link findFilesWithPattern} but returns each offending line alongside
507
+ * its file, so the verify output can show *what* matched — not just *where*.
508
+ * Skips comment-only lines so a doc reference doesn't trigger a false fail.
509
+ */
510
+ function findMatchesWithPattern(
511
+ dir: string,
512
+ pattern: RegExp,
513
+ results: Array<{ file: string; line: string }> = [],
514
+ baseDir?: string,
515
+ ): Array<{ file: string; line: string }> {
516
+ const root = baseDir ?? dir;
517
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
518
+ for (const entry of entries) {
519
+ const fullPath = path.join(dir, entry.name);
520
+ if (entry.isDirectory()) {
521
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "server") continue;
522
+ findMatchesWithPattern(fullPath, pattern, results, root);
523
+ } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
524
+ const content = fs.readFileSync(fullPath, "utf-8");
525
+ const rel = path.relative(root, fullPath);
526
+ for (const line of content.split("\n")) {
527
+ const trimmed = line.trimStart();
528
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
529
+ if (pattern.test(line)) results.push({ file: rel, line });
530
+ }
531
+ }
532
+ }
533
+ return results;
534
+ }
535
+
536
+ export function verify(ctx: MigrationContext): boolean {
537
+ logPhase("Verify (Smoke Test)");
538
+
539
+ if (ctx.dryRun) {
540
+ console.log(" Skipping verify in dry-run mode\n");
541
+ return true;
542
+ }
543
+
544
+ let errors = 0;
545
+ let warnings = 0;
546
+
547
+ for (const check of checks) {
548
+ const pass = check.fn(ctx);
549
+ const icon = pass ? "\x1b[32m✓\x1b[0m" : check.severity === "error" ? "\x1b[31m✗\x1b[0m" : "\x1b[33m⚠\x1b[0m";
550
+ console.log(` ${icon} ${check.name}`);
551
+ if (!pass) {
552
+ if (check.severity === "error") errors++;
553
+ else warnings++;
554
+ }
555
+ }
556
+
557
+ console.log(
558
+ `\n Result: ${checks.length - errors - warnings} passed, ${errors} errors, ${warnings} warnings`,
559
+ );
560
+
561
+ if (errors > 0) {
562
+ console.log(" \x1b[31mVerification FAILED — migration has issues that must be fixed\x1b[0m");
563
+ return false;
564
+ }
565
+ if (warnings > 0) {
566
+ console.log(" \x1b[33mVerification passed with warnings\x1b[0m");
567
+ } else {
568
+ console.log(" \x1b[32mVerification PASSED\x1b[0m");
569
+ }
570
+
571
+ return true;
572
+ }