@decocms/start 2.13.0 → 2.14.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 (24) hide show
  1. package/.agents/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md +32 -6
  2. package/CLAUDE.md +1 -1
  3. package/package.json +1 -1
  4. package/scripts/migrate/post-cleanup/rules.ts +77 -6
  5. package/scripts/migrate/post-cleanup/runner.test.ts +123 -2
  6. package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
  7. package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
  8. package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +0 -655
  9. package/.cursor/skills/deco-to-tanstack-migration/references/codemod-commands.md +0 -174
  10. package/.cursor/skills/deco-to-tanstack-migration/references/commerce/README.md +0 -78
  11. package/.cursor/skills/deco-to-tanstack-migration/references/deco-framework/README.md +0 -174
  12. package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +0 -834
  13. package/.cursor/skills/deco-to-tanstack-migration/references/imports/README.md +0 -70
  14. package/.cursor/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +0 -121
  15. package/.cursor/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md +0 -231
  16. package/.cursor/skills/deco-to-tanstack-migration/references/signals/README.md +0 -220
  17. package/.cursor/skills/deco-to-tanstack-migration/references/vite-config/README.md +0 -103
  18. package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +0 -75
  19. package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +0 -127
  20. package/.cursor/skills/deco-to-tanstack-migration/templates/router.md +0 -96
  21. package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +0 -148
  22. package/.cursor/skills/deco-to-tanstack-migration/templates/vite-config.md +0 -197
  23. package/.cursor/skills/deco-to-tanstack-migration/templates/worker-entry.md +0 -67
  24. /package/{.cursor → .agents}/skills/deco-to-tanstack-migration/references/server-functions/README.md +0 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Per-export classifier for `~/lib/vtex-*` shim files.
3
+ *
4
+ * The post-migration audit's `vtex-shim-regression` rule used to flag
5
+ * any import from these shims. That was overconfident — some shims have
6
+ * functional implementations of utility code (cookie parsing, fetch
7
+ * wrapping), while others are silent stubs (`return null`, `return {}`,
8
+ * identity casts) that drop runtime data and cause hard-to-trace bugs.
9
+ *
10
+ * This classifier inspects each export of a shim and labels it as:
11
+ *
12
+ * - **stub**: definitely a silent regression — body is `return null`,
13
+ * `return {}`, `return []`, an identity cast `return x as T`, or an
14
+ * unconditional `throw`. Importing this symbol means the shim is
15
+ * pretending to do work but isn't.
16
+ * - **type-only**: `interface` or `type` declaration — no runtime impact,
17
+ * never a regression.
18
+ * - **functional**: anything else (real implementations, even trivial
19
+ * ones like `return key.startsWith("filter.")`). Default-safe label —
20
+ * if we can't prove it's a stub, treat it as functional and don't flag.
21
+ *
22
+ * Trade-off — by design, we err toward "functional" when uncertain.
23
+ * That means some lossy implementations (e.g. a `withSegmentCookie`
24
+ * that returns `new Headers()` instead of attaching the cookie) classify
25
+ * as functional even though they're effectively stubs. Catching those
26
+ * would need real semantic analysis. False negatives are tolerable; the
27
+ * rule still warns when *any* imported symbol from the shim is a clear
28
+ * stub, which is enough to surface the real-world regressions we've
29
+ * actually seen on production sites (casaevideo: `getSegmentFromBag`,
30
+ * `getISCookiesFromBag`, `toProduct`).
31
+ *
32
+ * Implementation note — string parsing, not a real TypeScript AST. The
33
+ * shim files are tiny by design (the casaevideo ones are 1-39 lines).
34
+ * A balanced-brace body extractor + small set of stub patterns covers
35
+ * every case observed on real sites. If this ever needs to handle
36
+ * decorators, generics on consts, or weirder JSX forms, the right move
37
+ * is to swap in `typescript`'s `createSourceFile` — but we don't pay
38
+ * that dependency cost until it's clearly needed.
39
+ */
40
+
41
+ export type ExportClass = "stub" | "type-only" | "functional";
42
+
43
+ export interface ClassifiedExport {
44
+ name: string;
45
+ class: ExportClass;
46
+ /** Short, human-readable reason for the classification. */
47
+ reason?: string;
48
+ }
49
+
50
+ /**
51
+ * Strip line/block comments from a string before regex-matching the
52
+ * body. Keeps newlines so positions stay roughly aligned for debug.
53
+ */
54
+ function stripComments(s: string): string {
55
+ return s
56
+ .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "))
57
+ .replace(/\/\/[^\n]*/g, "");
58
+ }
59
+
60
+ /**
61
+ * Find the matching `}` for the `{` at `openIdx`. Returns the index of
62
+ * the closing brace, or -1 if unbalanced. Tolerates strings and template
63
+ * literals that may contain stray braces — we honor the basic quoting
64
+ * rules, not full TS lexer fidelity.
65
+ */
66
+ function findMatchingBrace(content: string, openIdx: number): number {
67
+ let depth = 0;
68
+ let i = openIdx;
69
+ let inStr: string | null = null;
70
+ let esc = false;
71
+ while (i < content.length) {
72
+ const c = content[i];
73
+ if (inStr) {
74
+ if (esc) {
75
+ esc = false;
76
+ } else if (c === "\\") {
77
+ esc = true;
78
+ } else if (c === inStr) {
79
+ inStr = null;
80
+ }
81
+ } else if (c === '"' || c === "'" || c === "`") {
82
+ inStr = c;
83
+ } else if (c === "{") {
84
+ depth++;
85
+ } else if (c === "}") {
86
+ depth--;
87
+ if (depth === 0) return i;
88
+ }
89
+ i++;
90
+ }
91
+ return -1;
92
+ }
93
+
94
+ /**
95
+ * Match `return null` / `return {}` / `return []` / `return X as Y` /
96
+ * `throw new Error(...)` as the only meaningful statement in the body.
97
+ * Comments and whitespace are tolerated.
98
+ */
99
+ function classifyBodyText(body: string): { class: ExportClass; reason?: string } | null {
100
+ const cleaned = stripComments(body).trim();
101
+ // Allow a trailing semicolon, but not multiple statements.
102
+ const single = cleaned.replace(/;\s*$/, "").trim();
103
+ if (single === "") return null;
104
+ // Reject anything that looks like multiple statements.
105
+ if (/[;\n]/.test(single.replace(/\s+/g, " ").trim()) && !/^return\s/.test(single)) {
106
+ return null;
107
+ }
108
+ if (/^return\s+null$/.test(single)) {
109
+ return { class: "stub", reason: "returns null" };
110
+ }
111
+ if (/^return\s+\{\s*\}$/.test(single)) {
112
+ return { class: "stub", reason: "returns empty object" };
113
+ }
114
+ if (/^return\s+\[\s*\]$/.test(single)) {
115
+ return { class: "stub", reason: "returns empty array" };
116
+ }
117
+ if (/^return\s+""$/.test(single) || /^return\s+''$/.test(single)) {
118
+ return { class: "stub", reason: "returns empty string" };
119
+ }
120
+ // Identity cast: `return ident as Type`. The right-hand side must be a
121
+ // bare identifier (or nested member like `obj.prop`) — anything else is
122
+ // probably real work.
123
+ const identityMatch = single.match(
124
+ /^return\s+([A-Za-z_][A-Za-z0-9_.]*)\s+as\s+[A-Za-z_][A-Za-z0-9_<>,\s.|&]*$/,
125
+ );
126
+ if (identityMatch) {
127
+ return { class: "stub", reason: `identity cast (return ${identityMatch[1]} as …)` };
128
+ }
129
+ if (/^throw\s+new\s+\w+\s*\(/.test(single)) {
130
+ return { class: "stub", reason: "unconditional throw" };
131
+ }
132
+ return null;
133
+ }
134
+
135
+ /**
136
+ * Locate `export function NAME(args): RT { body }` and `export async`
137
+ * variants. Returns each export with its body classified.
138
+ */
139
+ function classifyFunctionDecls(content: string): ClassifiedExport[] {
140
+ const out: ClassifiedExport[] = [];
141
+ // Anchored at start-of-line + optional indent — avoids catching
142
+ // `export default function` (which has no name immediately after).
143
+ const re = /(?:^|\n)[ \t]*export\s+(?:async\s+)?function\s+([A-Za-z_][A-Za-z0-9_]*)\b/g;
144
+ for (const m of content.matchAll(re)) {
145
+ const name = m[1];
146
+ const startSearchAt = (m.index ?? 0) + m[0].length;
147
+ const openIdx = content.indexOf("{", startSearchAt);
148
+ if (openIdx === -1) {
149
+ out.push({ name, class: "functional", reason: "no body found (declaration only)" });
150
+ continue;
151
+ }
152
+ const closeIdx = findMatchingBrace(content, openIdx);
153
+ if (closeIdx === -1) {
154
+ out.push({ name, class: "functional", reason: "unbalanced braces" });
155
+ continue;
156
+ }
157
+ const body = content.slice(openIdx + 1, closeIdx);
158
+ const verdict = classifyBodyText(body);
159
+ out.push(
160
+ verdict
161
+ ? { name, class: verdict.class, reason: verdict.reason }
162
+ : { name, class: "functional" },
163
+ );
164
+ }
165
+ return out;
166
+ }
167
+
168
+ /**
169
+ * Locate `export const NAME = (args) => …` and classify the arrow body.
170
+ * Block-bodied arrows reuse the function-body classifier; expression
171
+ * bodies are matched directly.
172
+ */
173
+ function classifyConstArrowDecls(content: string): ClassifiedExport[] {
174
+ const out: ClassifiedExport[] = [];
175
+ const re = /(?:^|\n)[ \t]*export\s+const\s+([A-Za-z_][A-Za-z0-9_]*)\s*=/g;
176
+ for (const m of content.matchAll(re)) {
177
+ const name = m[1];
178
+ const startIdx = (m.index ?? 0) + m[0].length;
179
+ const after = content.slice(startIdx);
180
+ // Look for: optional `async`, `(args)`, optional `: ReturnType`, `=>`.
181
+ const arrowHead = after.match(/^\s*(?:async\s+)?\([^)]*\)\s*(?::[^=]+)?=>\s*/);
182
+ if (!arrowHead) {
183
+ out.push({ name, class: "functional" });
184
+ continue;
185
+ }
186
+ const bodyStart = startIdx + arrowHead[0].length;
187
+ // Block body: classify the inside via the shared body classifier.
188
+ if (content[bodyStart] === "{") {
189
+ const closeIdx = findMatchingBrace(content, bodyStart);
190
+ if (closeIdx === -1) {
191
+ out.push({ name, class: "functional", reason: "unbalanced braces" });
192
+ continue;
193
+ }
194
+ const body = content.slice(bodyStart + 1, closeIdx);
195
+ const verdict = classifyBodyText(body);
196
+ out.push(
197
+ verdict
198
+ ? { name, class: verdict.class, reason: verdict.reason }
199
+ : { name, class: "functional" },
200
+ );
201
+ continue;
202
+ }
203
+ // Expression body: read until line break / semicolon.
204
+ const exprMatch = content.slice(bodyStart).match(/^([^;\n]+?)\s*;?\s*(?:\n|$)/);
205
+ if (!exprMatch) {
206
+ out.push({ name, class: "functional" });
207
+ continue;
208
+ }
209
+ const expr = exprMatch[1].trim();
210
+ if (expr === "null") {
211
+ out.push({ name, class: "stub", reason: "arrow returns null" });
212
+ } else if (/^\(\s*\{\s*\}\s*\)$/.test(expr) || expr === "{}") {
213
+ out.push({ name, class: "stub", reason: "arrow returns empty object" });
214
+ } else if (/^\[\s*\]$/.test(expr)) {
215
+ out.push({ name, class: "stub", reason: "arrow returns empty array" });
216
+ } else {
217
+ out.push({ name, class: "functional" });
218
+ }
219
+ }
220
+ return out;
221
+ }
222
+
223
+ /**
224
+ * Locate `export interface NAME` / `export type NAME` declarations.
225
+ */
226
+ function classifyTypeDecls(content: string): ClassifiedExport[] {
227
+ const out: ClassifiedExport[] = [];
228
+ const re = /(?:^|\n)[ \t]*export\s+(?:interface|type)\s+([A-Za-z_][A-Za-z0-9_]*)\b/g;
229
+ for (const m of content.matchAll(re)) {
230
+ out.push({ name: m[1], class: "type-only" });
231
+ }
232
+ return out;
233
+ }
234
+
235
+ /**
236
+ * Classify every top-level export in a shim file. Returns one entry per
237
+ * export, in source order is not guaranteed (we run three passes over
238
+ * the content); callers should look up by `name`.
239
+ */
240
+ export function classifyShimExports(content: string): ClassifiedExport[] {
241
+ return [
242
+ ...classifyFunctionDecls(content),
243
+ ...classifyConstArrowDecls(content),
244
+ ...classifyTypeDecls(content),
245
+ ];
246
+ }