@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.
- package/.agents/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md +32 -6
- package/CLAUDE.md +1 -1
- package/package.json +1 -1
- package/scripts/migrate/post-cleanup/rules.ts +77 -6
- package/scripts/migrate/post-cleanup/runner.test.ts +123 -2
- package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
- package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
- package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +0 -655
- package/.cursor/skills/deco-to-tanstack-migration/references/codemod-commands.md +0 -174
- package/.cursor/skills/deco-to-tanstack-migration/references/commerce/README.md +0 -78
- package/.cursor/skills/deco-to-tanstack-migration/references/deco-framework/README.md +0 -174
- package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +0 -834
- package/.cursor/skills/deco-to-tanstack-migration/references/imports/README.md +0 -70
- package/.cursor/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +0 -121
- package/.cursor/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md +0 -231
- package/.cursor/skills/deco-to-tanstack-migration/references/signals/README.md +0 -220
- package/.cursor/skills/deco-to-tanstack-migration/references/vite-config/README.md +0 -103
- package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +0 -75
- package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +0 -127
- package/.cursor/skills/deco-to-tanstack-migration/templates/router.md +0 -96
- package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +0 -148
- package/.cursor/skills/deco-to-tanstack-migration/templates/vite-config.md +0 -197
- package/.cursor/skills/deco-to-tanstack-migration/templates/worker-entry.md +0 -67
- /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
|
+
}
|