@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
|
@@ -159,18 +159,44 @@ opt-in and tested.
|
|
|
159
159
|
Older versions of the migration script's `phase-cleanup` had a bug where
|
|
160
160
|
it actively rewrote valid `@decocms/apps/vtex/utils/*` and
|
|
161
161
|
`@decocms/apps/vtex/client` imports back to the dead `~/lib/vtex-*` shims.
|
|
162
|
-
|
|
162
|
+
|
|
163
|
+
The post-cleanup audit now classifies **per-symbol**: it reads each
|
|
164
|
+
`~/lib/vtex-*` shim file, labels every named export as `stub`,
|
|
165
|
+
`type-only`, or `functional`, and only flags an import when at least
|
|
166
|
+
one imported symbol is a real silent stub (returns `null` / `{}` / `[]`
|
|
167
|
+
/ identity-cast / unconditional throw). Functional helpers shipped
|
|
168
|
+
alongside stubs (e.g. a `parseCookie` cookie parser, a `fetchSafe`
|
|
169
|
+
wrapper) no longer create noise.
|
|
170
|
+
|
|
171
|
+
The audit's finding names the exact stub symbols, e.g.
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
[WARNING] src/loaders/search/x.ts — Imports stub-only symbols from
|
|
175
|
+
vtex-transform (toProduct); vtex-segment (getSegmentFromBag) —
|
|
176
|
+
runtime is silently stubbed
|
|
177
|
+
fix: Repoint imports to '@decocms/apps/vtex/...' or
|
|
178
|
+
'apps/commerce/utils/...'
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Manual sweep (still useful if you don't have the audit handy):
|
|
163
182
|
|
|
164
183
|
```bash
|
|
165
184
|
rg "from ['\"]~/lib/vtex-" src/
|
|
166
185
|
# Expected: 0 hits (or only site-specific reasons you can articulate)
|
|
167
186
|
```
|
|
168
187
|
|
|
169
|
-
|
|
170
|
-
directly (or the corresponding
|
|
171
|
-
utility). Your runtime behavior
|
|
172
|
-
cookies, VTEX session auth all
|
|
173
|
-
silently stubbed to `{}` / `null`.
|
|
188
|
+
When you see real findings, update the imports to point at
|
|
189
|
+
`@decocms/apps/vtex/...` directly (or the corresponding
|
|
190
|
+
`commerce/utils/*` if it's a generic utility). Your runtime behavior
|
|
191
|
+
gets MUCH better — segment cookies, IS cookies, VTEX session auth all
|
|
192
|
+
start working again instead of being silently stubbed to `{}` / `null`.
|
|
193
|
+
|
|
194
|
+
**Note on `--fix`**: this rule is intentionally detect-only. Repointing
|
|
195
|
+
imports requires a per-symbol map to canonical apps/start exports
|
|
196
|
+
(e.g. `getSegmentFromBag` → `@decocms/apps/vtex/utils/segment`), which
|
|
197
|
+
the framework doesn't ship yet. Detect-only is still strictly more
|
|
198
|
+
useful than nothing — the precision means each finding maps to exactly
|
|
199
|
+
one PR's worth of mechanical work.
|
|
174
200
|
|
|
175
201
|
## 6. Drop `src/types/widgets.ts` — framework owns it
|
|
176
202
|
|
package/CLAUDE.md
CHANGED
|
@@ -117,7 +117,7 @@ Schema is composed at runtime: `generate-schema.ts` produces section schemas, `c
|
|
|
117
117
|
|
|
118
118
|
## Migration Guide
|
|
119
119
|
|
|
120
|
-
Detailed migration playbook from Fresh/Preact/Deno to TanStack Start/React/Workers is available at `.
|
|
120
|
+
Detailed migration playbook from Fresh/Preact/Deno to TanStack Start/React/Workers is available at `.agents/skills/deco-to-tanstack-migration/` (the canonical location — also surfaced as a Cursor skill via the `.agents/` skills root). Covers:
|
|
121
121
|
|
|
122
122
|
- Import rewrites (Preact → React, @preact/signals → @tanstack/store)
|
|
123
123
|
- Deco framework elimination (@deco/deco/*, $fresh/*)
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Rules are intentionally read-only here — `--fix` is a follow-up.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { classifyShimExports, type ExportClass } from "./shim-classify";
|
|
12
13
|
import type { Finding, FixAction, FsWriter, Rule, RuleContext } from "./types";
|
|
13
14
|
|
|
14
15
|
const SRC_GLOB_EXCLUDES = ["node_modules", "dist", ".wrangler", ".vite", ".tanstack", "build"];
|
|
@@ -238,27 +239,97 @@ const ruleSiteLocalGlobals: Rule = {
|
|
|
238
239
|
/* Rule 5 — `~/lib/vtex-*` shim regression */
|
|
239
240
|
/* ------------------------------------------------------------------ */
|
|
240
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Parse one or more ES `import { a, b as c, type d } from "spec"` blocks
|
|
244
|
+
* targeting a specific source spec out of a file. Returns the list of
|
|
245
|
+
* imported names (resolved to their original symbol, ignoring `as`
|
|
246
|
+
* rebinds), with `import type {…}` and inline `type` modifiers stripped
|
|
247
|
+
* — those carry no runtime, so the rule treats them as out-of-scope.
|
|
248
|
+
*/
|
|
249
|
+
function namedRuntimeImportsFrom(content: string, spec: string): string[] {
|
|
250
|
+
const escaped = spec.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
251
|
+
// `(type\s+)?` captures the entire-import `import type { … }` form.
|
|
252
|
+
// Per-symbol `type` modifiers inside the braces are stripped below.
|
|
253
|
+
const re = new RegExp(
|
|
254
|
+
`import\\s+(type\\s+)?\\{([^}]+)\\}\\s+from\\s+['\"]${escaped}['\"]`,
|
|
255
|
+
"g",
|
|
256
|
+
);
|
|
257
|
+
const out: string[] = [];
|
|
258
|
+
for (const m of content.matchAll(re)) {
|
|
259
|
+
if (m[1]) continue; // entire import is type-only
|
|
260
|
+
for (const raw of m[2].split(",")) {
|
|
261
|
+
const trimmed = raw.trim();
|
|
262
|
+
if (!trimmed || trimmed.startsWith("type ")) continue;
|
|
263
|
+
// `foo as bar` → `foo` (we want the source symbol, not the local alias).
|
|
264
|
+
const sourceName = trimmed.split(/\s+as\s+/)[0].trim();
|
|
265
|
+
if (sourceName) out.push(sourceName);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
241
271
|
const ruleVtexShimRegression: Rule = {
|
|
242
272
|
id: "vtex-shim-regression",
|
|
243
273
|
title: "Imports from ~/lib/vtex-* (silent stub regression)",
|
|
244
274
|
run({ siteDir, fs }: RuleContext): Finding[] {
|
|
245
275
|
const tsFiles = fs.glob(siteDir, "src/**/*.{ts,tsx}", SRC_GLOB_EXCLUDES);
|
|
246
276
|
const findings: Finding[] = [];
|
|
247
|
-
|
|
277
|
+
|
|
278
|
+
// Per-shim classification cache. Each shim file is read at most once
|
|
279
|
+
// per audit run, even when imported by dozens of consumers.
|
|
280
|
+
const shimClasses = new Map<string, Map<string, ExportClass>>();
|
|
281
|
+
function classOf(shim: string, symbol: string): ExportClass {
|
|
282
|
+
let map = shimClasses.get(shim);
|
|
283
|
+
if (!map) {
|
|
284
|
+
const abs = `${siteDir}/src/lib/${shim}.ts`;
|
|
285
|
+
map = new Map<string, ExportClass>();
|
|
286
|
+
if (fs.exists(abs)) {
|
|
287
|
+
for (const ce of classifyShimExports(fs.readText(abs))) {
|
|
288
|
+
map.set(ce.name, ce.class);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
shimClasses.set(shim, map);
|
|
292
|
+
}
|
|
293
|
+
// Unknown symbols (file missing or not exported) default to "stub" —
|
|
294
|
+
// pessimistic on purpose. If the symbol can't be found locally, the
|
|
295
|
+
// import is at best dead code, at worst a TS error; either way the
|
|
296
|
+
// user wants visibility into it. Compile phase catches the TS side.
|
|
297
|
+
return map.get(symbol) ?? "stub";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Match the bare `from "~/lib/vtex-X"` to know which shims are touched.
|
|
301
|
+
const fromRe = /from\s+['"]~\/lib\/vtex-([A-Za-z0-9-]+)['"]/g;
|
|
248
302
|
for (const abs of tsFiles) {
|
|
249
303
|
if (abs.includes("/src/lib/")) continue;
|
|
250
304
|
const content = fs.readText(abs);
|
|
251
|
-
const
|
|
252
|
-
|
|
305
|
+
const usedShims = new Set<string>(
|
|
306
|
+
[...content.matchAll(fromRe)].map((m) => `vtex-${m[1]}`),
|
|
307
|
+
);
|
|
308
|
+
if (usedShims.size === 0) continue;
|
|
309
|
+
|
|
310
|
+
// Per-file: which shim → which stub symbols are imported.
|
|
311
|
+
const stubsBySim = new Map<string, string[]>();
|
|
312
|
+
for (const shim of usedShims) {
|
|
313
|
+
const symbols = namedRuntimeImportsFrom(content, `~/lib/${shim}`);
|
|
314
|
+
const stubs = symbols.filter((s) => classOf(shim, s) === "stub");
|
|
315
|
+
if (stubs.length > 0) stubsBySim.set(shim, stubs);
|
|
316
|
+
}
|
|
317
|
+
if (stubsBySim.size === 0) continue;
|
|
318
|
+
|
|
253
319
|
const rel = abs.slice(siteDir.length + 1);
|
|
254
|
-
const
|
|
320
|
+
const detail = [...stubsBySim.entries()]
|
|
321
|
+
.map(([s, syms]) => `${s} (${syms.join(", ")})`)
|
|
322
|
+
.join("; ")
|
|
323
|
+
;
|
|
255
324
|
findings.push({
|
|
256
325
|
rule: "vtex-shim-regression",
|
|
257
326
|
severity: "warning",
|
|
258
327
|
file: rel,
|
|
259
|
-
message: `Imports
|
|
328
|
+
message: `Imports stub-only symbols from ${detail} — runtime is silently stubbed`,
|
|
260
329
|
fix: "Repoint imports to '@decocms/apps/vtex/...' or 'apps/commerce/utils/...'",
|
|
261
|
-
meta: {
|
|
330
|
+
meta: {
|
|
331
|
+
stubsBySim: Object.fromEntries(stubsBySim),
|
|
332
|
+
},
|
|
262
333
|
});
|
|
263
334
|
}
|
|
264
335
|
return findings;
|
|
@@ -263,14 +263,19 @@ describe("rule: site-local-with-globals", () => {
|
|
|
263
263
|
});
|
|
264
264
|
|
|
265
265
|
describe("rule: vtex-shim-regression", () => {
|
|
266
|
-
|
|
266
|
+
// Default-pessimistic case: shim file missing → unknown symbols treated
|
|
267
|
+
// as stubs so audit always surfaces the import. (Compile phase catches
|
|
268
|
+
// the underlying TS error separately.)
|
|
269
|
+
it("flags imports when shim file is missing (defensive default)", () => {
|
|
267
270
|
const fs = makeFs({
|
|
268
271
|
"/site/src/sections/Foo.tsx": 'import { getSegment } from "~/lib/vtex-segment";\n',
|
|
269
272
|
});
|
|
270
273
|
const report = runAudit(SITE, fs);
|
|
271
274
|
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
272
275
|
expect(r.findings).toHaveLength(1);
|
|
273
|
-
expect(r.findings[0].meta?.
|
|
276
|
+
expect(r.findings[0].meta?.stubsBySim).toEqual({
|
|
277
|
+
"vtex-segment": ["getSegment"],
|
|
278
|
+
});
|
|
274
279
|
});
|
|
275
280
|
|
|
276
281
|
it("does not flag imports from src/lib itself", () => {
|
|
@@ -281,6 +286,122 @@ describe("rule: vtex-shim-regression", () => {
|
|
|
281
286
|
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
282
287
|
expect(r.findings).toEqual([]);
|
|
283
288
|
});
|
|
289
|
+
|
|
290
|
+
it("does NOT flag when imported symbols are all functional", () => {
|
|
291
|
+
const fs = makeFs({
|
|
292
|
+
"/site/src/lib/vtex-id.ts":
|
|
293
|
+
"export function parseCookie(s?: string): Record<string,string> {\n" +
|
|
294
|
+
" if (!s) return {};\n" +
|
|
295
|
+
" return Object.fromEntries(s.split(';').map(c => c.split('=') as [string,string]));\n" +
|
|
296
|
+
"}\n",
|
|
297
|
+
"/site/src/actions/x.ts": 'import { parseCookie } from "~/lib/vtex-id";\n',
|
|
298
|
+
});
|
|
299
|
+
const report = runAudit(SITE, fs);
|
|
300
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
301
|
+
// parseCookie has nested-block functional impl → not a stub → no warning.
|
|
302
|
+
expect(r.findings).toEqual([]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("flags only the stub symbols when import set is mixed", () => {
|
|
306
|
+
const fs = makeFs({
|
|
307
|
+
"/site/src/lib/vtex-segment.ts":
|
|
308
|
+
"export function getSegmentFromBag(_req?: any): null { return null; }\n" +
|
|
309
|
+
"export function withSegmentCookie(headers: Headers): Headers {\n" +
|
|
310
|
+
" headers.set('x', 'y');\n" +
|
|
311
|
+
" return headers;\n" +
|
|
312
|
+
"}\n",
|
|
313
|
+
"/site/src/loaders/x.ts":
|
|
314
|
+
'import { getSegmentFromBag, withSegmentCookie } from "~/lib/vtex-segment";\n',
|
|
315
|
+
});
|
|
316
|
+
const report = runAudit(SITE, fs);
|
|
317
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
318
|
+
expect(r.findings).toHaveLength(1);
|
|
319
|
+
expect(r.findings[0].meta?.stubsBySim).toEqual({
|
|
320
|
+
"vtex-segment": ["getSegmentFromBag"],
|
|
321
|
+
});
|
|
322
|
+
expect(r.findings[0].message).toContain("getSegmentFromBag");
|
|
323
|
+
expect(r.findings[0].message).not.toContain("withSegmentCookie");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("flags identity-cast (toProduct) as a stub", () => {
|
|
327
|
+
const fs = makeFs({
|
|
328
|
+
"/site/src/lib/vtex-transform.ts":
|
|
329
|
+
"export function toProduct(p: any): unknown { return p as unknown; }\n",
|
|
330
|
+
"/site/src/loaders/search.ts":
|
|
331
|
+
'import { toProduct } from "~/lib/vtex-transform";\n',
|
|
332
|
+
});
|
|
333
|
+
const report = runAudit(SITE, fs);
|
|
334
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
335
|
+
expect(r.findings).toHaveLength(1);
|
|
336
|
+
expect(r.findings[0].meta?.stubsBySim).toEqual({
|
|
337
|
+
"vtex-transform": ["toProduct"],
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("does NOT flag `import type { X }` from a stub-having shim", () => {
|
|
342
|
+
const fs = makeFs({
|
|
343
|
+
"/site/src/lib/vtex-client.ts":
|
|
344
|
+
"export interface VTEXCommerceStable { account: string; }\n" +
|
|
345
|
+
"export function stub(): null { return null; }\n",
|
|
346
|
+
"/site/src/loaders/x.ts":
|
|
347
|
+
'import type { VTEXCommerceStable } from "~/lib/vtex-client";\n',
|
|
348
|
+
});
|
|
349
|
+
const report = runAudit(SITE, fs);
|
|
350
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
351
|
+
// Type-only imports have no runtime → never a regression.
|
|
352
|
+
expect(r.findings).toEqual([]);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("ignores per-symbol `type` modifier and only flags runtime imports", () => {
|
|
356
|
+
const fs = makeFs({
|
|
357
|
+
"/site/src/lib/vtex-mixed.ts":
|
|
358
|
+
"export interface Cfg { a: string; }\n" +
|
|
359
|
+
"export function stub(): null { return null; }\n" +
|
|
360
|
+
"export function ok(): boolean { return true; }\n",
|
|
361
|
+
"/site/src/loaders/x.ts":
|
|
362
|
+
'import { type Cfg, stub, ok } from "~/lib/vtex-mixed";\n',
|
|
363
|
+
});
|
|
364
|
+
const report = runAudit(SITE, fs);
|
|
365
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
366
|
+
expect(r.findings).toHaveLength(1);
|
|
367
|
+
expect(r.findings[0].meta?.stubsBySim).toEqual({
|
|
368
|
+
"vtex-mixed": ["stub"],
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("aggregates findings per file across multiple shims", () => {
|
|
373
|
+
const fs = makeFs({
|
|
374
|
+
"/site/src/lib/vtex-segment.ts":
|
|
375
|
+
"export function getSegmentFromBag(): null { return null; }\n",
|
|
376
|
+
"/site/src/lib/vtex-transform.ts":
|
|
377
|
+
"export function toProduct(p: any): unknown { return p as unknown; }\n",
|
|
378
|
+
"/site/src/loaders/search.ts":
|
|
379
|
+
'import { getSegmentFromBag } from "~/lib/vtex-segment";\n' +
|
|
380
|
+
'import { toProduct } from "~/lib/vtex-transform";\n',
|
|
381
|
+
});
|
|
382
|
+
const report = runAudit(SITE, fs);
|
|
383
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
384
|
+
expect(r.findings).toHaveLength(1);
|
|
385
|
+
expect(r.findings[0].meta?.stubsBySim).toEqual({
|
|
386
|
+
"vtex-segment": ["getSegmentFromBag"],
|
|
387
|
+
"vtex-transform": ["toProduct"],
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("supports `as`-renamed imports (resolves to source name)", () => {
|
|
392
|
+
const fs = makeFs({
|
|
393
|
+
"/site/src/lib/vtex-segment.ts":
|
|
394
|
+
"export function getSegmentFromBag(): null { return null; }\n",
|
|
395
|
+
"/site/src/loaders/x.ts":
|
|
396
|
+
'import { getSegmentFromBag as getSeg } from "~/lib/vtex-segment";\n',
|
|
397
|
+
});
|
|
398
|
+
const report = runAudit(SITE, fs);
|
|
399
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
400
|
+
expect(r.findings).toHaveLength(1);
|
|
401
|
+
expect(r.findings[0].meta?.stubsBySim).toEqual({
|
|
402
|
+
"vtex-segment": ["getSegmentFromBag"],
|
|
403
|
+
});
|
|
404
|
+
});
|
|
284
405
|
});
|
|
285
406
|
|
|
286
407
|
describe("rule: local-widgets-types", () => {
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { classifyShimExports, type ClassifiedExport } from "./shim-classify";
|
|
3
|
+
|
|
4
|
+
function classMap(content: string): Record<string, ClassifiedExport["class"]> {
|
|
5
|
+
return Object.fromEntries(
|
|
6
|
+
classifyShimExports(content).map((e) => [e.name, e.class]),
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("classifyShimExports — single statement function bodies", () => {
|
|
11
|
+
it("returns null → stub", () => {
|
|
12
|
+
const code = `
|
|
13
|
+
export function getSegmentFromBag(_req?: any): Record<string, unknown> | null {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
expect(classMap(code)).toEqual({ getSegmentFromBag: "stub" });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("returns {} → stub", () => {
|
|
21
|
+
const code = `
|
|
22
|
+
export function getISCookiesFromBag(_req?: any): Record<string, string> {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
expect(classMap(code)).toEqual({ getISCookiesFromBag: "stub" });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns [] → stub", () => {
|
|
30
|
+
const code = `export function emptyList(): string[] { return []; }`;
|
|
31
|
+
expect(classMap(code)).toEqual({ emptyList: "stub" });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("returns empty string → stub", () => {
|
|
35
|
+
const code = `export function emptyStr(): string { return ""; }`;
|
|
36
|
+
expect(classMap(code)).toEqual({ emptyStr: "stub" });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("identity cast (return x as T) → stub", () => {
|
|
40
|
+
const code = `
|
|
41
|
+
import type { Product } from "@decocms/apps/commerce/types";
|
|
42
|
+
export function toProduct(vtexProduct: any): Product {
|
|
43
|
+
return vtexProduct as Product;
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
expect(classMap(code)).toEqual({ toProduct: "stub" });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("identity cast on member (return x.foo as T) → stub", () => {
|
|
50
|
+
const code = `export function toThing(o: any): X { return o.payload as X; }`;
|
|
51
|
+
expect(classMap(code)).toEqual({ toThing: "stub" });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("unconditional throw → stub", () => {
|
|
55
|
+
const code = `
|
|
56
|
+
export function notImplemented(): never {
|
|
57
|
+
throw new Error("not implemented");
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
60
|
+
expect(classMap(code)).toEqual({ notImplemented: "stub" });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("returns non-empty value → functional", () => {
|
|
64
|
+
const code = `export function answer(): number { return 42; }`;
|
|
65
|
+
expect(classMap(code)).toEqual({ answer: "functional" });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns non-identity expression → functional", () => {
|
|
69
|
+
const code = `
|
|
70
|
+
export function isFilterParam(key: string): boolean {
|
|
71
|
+
return key.startsWith("filter.");
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
expect(classMap(code)).toEqual({ isFilterParam: "functional" });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("multi-statement body → functional (default safe)", () => {
|
|
78
|
+
const code = `
|
|
79
|
+
export async function fetchSafe(input: string): Promise<Response> {
|
|
80
|
+
const response = await fetch(input);
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
console.error(response.status);
|
|
83
|
+
}
|
|
84
|
+
return response;
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
expect(classMap(code)).toEqual({ fetchSafe: "functional" });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("body with nested blocks → functional", () => {
|
|
91
|
+
const code = `
|
|
92
|
+
export function withDefaultParams(params: any, defaults?: any): any {
|
|
93
|
+
if (params instanceof URLSearchParams) {
|
|
94
|
+
if (defaults) {
|
|
95
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
96
|
+
if (!params.has(key)) {
|
|
97
|
+
params.set(key, value);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return params;
|
|
102
|
+
}
|
|
103
|
+
return { ...params, ...defaults };
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
106
|
+
expect(classMap(code)).toEqual({ withDefaultParams: "functional" });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("comments before return → still classified by content", () => {
|
|
110
|
+
const code = `
|
|
111
|
+
export function stubWithComment(): null {
|
|
112
|
+
// Intentionally a stub — see migration notes.
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
expect(classMap(code)).toEqual({ stubWithComment: "stub" });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("trailing semicolon optional", () => {
|
|
120
|
+
const code = `export function noSemi(): null { return null }`;
|
|
121
|
+
expect(classMap(code)).toEqual({ noSemi: "stub" });
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("classifyShimExports — async functions", () => {
|
|
126
|
+
it("async returns null → stub", () => {
|
|
127
|
+
const code = `export async function asyncStub(): Promise<null> { return null; }`;
|
|
128
|
+
expect(classMap(code)).toEqual({ asyncStub: "stub" });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("async with real work → functional", () => {
|
|
132
|
+
const code = `
|
|
133
|
+
export async function fetcher(): Promise<Response> {
|
|
134
|
+
const r = await fetch("/x");
|
|
135
|
+
return r;
|
|
136
|
+
}
|
|
137
|
+
`;
|
|
138
|
+
expect(classMap(code)).toEqual({ fetcher: "functional" });
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("classifyShimExports — const arrow functions", () => {
|
|
143
|
+
it("arrow returns null (block body) → stub", () => {
|
|
144
|
+
const code = `export const noop = (): null => { return null; };`;
|
|
145
|
+
expect(classMap(code)).toEqual({ noop: "stub" });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("arrow returns null (expression body) → stub", () => {
|
|
149
|
+
const code = `export const noop = (): null => null;`;
|
|
150
|
+
expect(classMap(code)).toEqual({ noop: "stub" });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("arrow returns empty object literal expression → stub", () => {
|
|
154
|
+
const code = `export const noop = () => ({});`;
|
|
155
|
+
expect(classMap(code)).toEqual({ noop: "stub" });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("arrow with real expression → functional", () => {
|
|
159
|
+
const code = `export const square = (n: number) => n * n;`;
|
|
160
|
+
expect(classMap(code)).toEqual({ square: "functional" });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("non-arrow const (object literal) → functional", () => {
|
|
164
|
+
const code = `export const config = { account: "x" };`;
|
|
165
|
+
expect(classMap(code)).toEqual({ config: "functional" });
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("classifyShimExports — type/interface declarations", () => {
|
|
170
|
+
it("interface → type-only", () => {
|
|
171
|
+
const code = `
|
|
172
|
+
export interface VTEXCommerceStable {
|
|
173
|
+
account: string;
|
|
174
|
+
environment?: string;
|
|
175
|
+
}
|
|
176
|
+
`;
|
|
177
|
+
expect(classMap(code)).toEqual({ VTEXCommerceStable: "type-only" });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("type alias → type-only", () => {
|
|
181
|
+
const code = `export type AccountId = string;`;
|
|
182
|
+
expect(classMap(code)).toEqual({ AccountId: "type-only" });
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("classifyShimExports — real casaevideo-storefront fixtures", () => {
|
|
187
|
+
it("vtex-segment.ts (mixed: stub + functional)", () => {
|
|
188
|
+
const code = `
|
|
189
|
+
export function getSegmentFromBag(_req?: any): Record<string, unknown> | null {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function withSegmentCookie(..._args: any[]): any {
|
|
194
|
+
for (const arg of _args) {
|
|
195
|
+
if (arg instanceof Headers) {
|
|
196
|
+
return arg;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return new Headers();
|
|
200
|
+
}
|
|
201
|
+
`;
|
|
202
|
+
expect(classMap(code)).toEqual({
|
|
203
|
+
getSegmentFromBag: "stub",
|
|
204
|
+
// Multi-statement / nested block — defaults to functional. This is a
|
|
205
|
+
// *known* false negative (see module docstring): the function looks
|
|
206
|
+
// functional but actually drops the segment cookie. We accept this
|
|
207
|
+
// trade-off rather than risk false positives on real implementations.
|
|
208
|
+
withSegmentCookie: "functional",
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("vtex-transform.ts (single identity-cast stub)", () => {
|
|
213
|
+
const code = `
|
|
214
|
+
import type { Product } from "@decocms/apps/commerce/types";
|
|
215
|
+
export function toProduct(vtexProduct: any): Product {
|
|
216
|
+
return vtexProduct as Product;
|
|
217
|
+
}
|
|
218
|
+
`;
|
|
219
|
+
expect(classMap(code)).toEqual({ toProduct: "stub" });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("vtex-client.ts (interface only)", () => {
|
|
223
|
+
const code = `
|
|
224
|
+
export interface VTEXCommerceStable {
|
|
225
|
+
account: string;
|
|
226
|
+
environment?: string;
|
|
227
|
+
}
|
|
228
|
+
`;
|
|
229
|
+
expect(classMap(code)).toEqual({ VTEXCommerceStable: "type-only" });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("vtex-fetch.ts (functional)", () => {
|
|
233
|
+
const code = `
|
|
234
|
+
export async function fetchSafe(
|
|
235
|
+
input: string | URL | Request,
|
|
236
|
+
init?: RequestInit,
|
|
237
|
+
): Promise<Response> {
|
|
238
|
+
const response = await fetch(input, init);
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
console.error(\`VTEX fetch failed: \${response.status}\`);
|
|
241
|
+
}
|
|
242
|
+
return response;
|
|
243
|
+
}
|
|
244
|
+
`;
|
|
245
|
+
expect(classMap(code)).toEqual({ fetchSafe: "functional" });
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("vtex-id.ts (functional cookie parser)", () => {
|
|
249
|
+
const code = `
|
|
250
|
+
export function parseCookie(cookieStr?: string | null): Record<string, string> {
|
|
251
|
+
if (!cookieStr) return {};
|
|
252
|
+
return Object.fromEntries(
|
|
253
|
+
cookieStr.split(";").map((c) => {
|
|
254
|
+
const [key, ...rest] = c.trim().split("=");
|
|
255
|
+
return [key, rest.join("=")];
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
`;
|
|
260
|
+
expect(classMap(code)).toEqual({ parseCookie: "functional" });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("vtex-intelligent-search.ts (mixed: 1 stub + 4 functional)", () => {
|
|
264
|
+
const code = `
|
|
265
|
+
export function getISCookiesFromBag(_req?: any): Record<string, string> {
|
|
266
|
+
return {};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function isFilterParam(key: string): boolean {
|
|
270
|
+
return key.startsWith("filter.");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function toPath(facets: { key: string; value: string }[]): string {
|
|
274
|
+
return facets.map((f) => \`\${f.key}/\${f.value}\`).join("/");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function withDefaultFacets(
|
|
278
|
+
facets: { key: string; value: string }[],
|
|
279
|
+
defaults?: any,
|
|
280
|
+
): { key: string; value: string }[] {
|
|
281
|
+
if (Array.isArray(defaults)) {
|
|
282
|
+
return [...defaults, ...facets];
|
|
283
|
+
}
|
|
284
|
+
return [...facets];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function withDefaultParams(
|
|
288
|
+
params: any,
|
|
289
|
+
defaults?: Record<string, string>,
|
|
290
|
+
): any {
|
|
291
|
+
if (params instanceof URLSearchParams) {
|
|
292
|
+
if (defaults) {
|
|
293
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
294
|
+
if (!params.has(key)) {
|
|
295
|
+
params.set(key, value);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return params;
|
|
300
|
+
}
|
|
301
|
+
return { ...params, ...defaults };
|
|
302
|
+
}
|
|
303
|
+
`;
|
|
304
|
+
expect(classMap(code)).toEqual({
|
|
305
|
+
getISCookiesFromBag: "stub",
|
|
306
|
+
isFilterParam: "functional",
|
|
307
|
+
toPath: "functional",
|
|
308
|
+
withDefaultFacets: "functional",
|
|
309
|
+
withDefaultParams: "functional",
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe("classifyShimExports — defensive cases", () => {
|
|
315
|
+
it("empty file → no exports", () => {
|
|
316
|
+
expect(classifyShimExports("")).toEqual([]);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("file with only imports → no exports", () => {
|
|
320
|
+
const code = `import { x } from "y";`;
|
|
321
|
+
expect(classifyShimExports(code)).toEqual([]);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("non-export function ignored", () => {
|
|
325
|
+
const code = `function private_(): null { return null; }`;
|
|
326
|
+
expect(classifyShimExports(code)).toEqual([]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("export default skipped (intentional — only flag named exports)", () => {
|
|
330
|
+
const code = `export default function() { return null; }`;
|
|
331
|
+
expect(classifyShimExports(code)).toEqual([]);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("strings containing braces don't break body extraction", () => {
|
|
335
|
+
const code = `
|
|
336
|
+
export function withBrace(): string {
|
|
337
|
+
return "{not a real brace}";
|
|
338
|
+
}
|
|
339
|
+
`;
|
|
340
|
+
expect(classMap(code)).toEqual({ withBrace: "functional" });
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("template literal with brace-like substitution", () => {
|
|
344
|
+
const code = `
|
|
345
|
+
export function withTemplate(): string {
|
|
346
|
+
return \`hello \${1 + 1}\`;
|
|
347
|
+
}
|
|
348
|
+
`;
|
|
349
|
+
// Single statement, but the value is non-empty/non-null → functional.
|
|
350
|
+
expect(classMap(code)).toEqual({ withTemplate: "functional" });
|
|
351
|
+
});
|
|
352
|
+
});
|