@decocms/start 2.13.0 → 2.15.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 +132 -6
- package/CLAUDE.md +1 -1
- package/package.json +1 -1
- package/scripts/migrate/post-cleanup/rules.ts +196 -7
- package/scripts/migrate/post-cleanup/runner.test.ts +225 -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,144 @@ 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 **and emits per-symbol
|
|
172
|
+
fix guidance**, e.g.
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
[WARNING] src/loaders/search/x.ts — Imports stub-only symbols from
|
|
176
|
+
vtex-transform (toProduct); vtex-segment (getSegmentFromBag) —
|
|
177
|
+
runtime is silently stubbed
|
|
178
|
+
fix: toProduct → @decocms/apps/vtex/utils/transform (1:1 import swap)
|
|
179
|
+
— canonical signature is `toProduct(product, sku, level, options)`;
|
|
180
|
+
1-arg call sites need to expand args first | getSegmentFromBag →
|
|
181
|
+
call-site refactor: read cookies via `request.headers.get('cookie')`
|
|
182
|
+
then call `buildSegmentFromCookies()` from
|
|
183
|
+
'@decocms/apps/vtex/utils/segment'.
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
JSON consumers can read structured guidance from `meta.fixHints`:
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"rule": "vtex-shim-regression",
|
|
191
|
+
"meta": {
|
|
192
|
+
"stubsBySim": { "vtex-transform": ["toProduct"], "vtex-segment": ["getSegmentFromBag"] },
|
|
193
|
+
"fixHints": {
|
|
194
|
+
"toProduct": { "kind": "swap", "canonical": "@decocms/apps/vtex/utils/transform", "note": "..." },
|
|
195
|
+
"getSegmentFromBag": { "kind": "refactor", "note": "..." }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Canonical replacement table
|
|
202
|
+
|
|
203
|
+
| Stub symbol | Kind | Canonical / fix |
|
|
204
|
+
|---|---|---|
|
|
205
|
+
| `toProduct` | swap | `@decocms/apps/vtex/utils/transform.toProduct` — note canonical signature is `(product, sku, level, options)`; 1-arg call sites need to expand args |
|
|
206
|
+
| `withSegmentCookie` | swap | `@decocms/apps/vtex/utils/segment.withSegmentCookie` — note canonical signature is `(segment, headers?)` |
|
|
207
|
+
| `getSegmentFromBag` | refactor | read cookies via `request.headers.get('cookie')`, then `buildSegmentFromCookies()` from `@decocms/apps/vtex/utils/segment` |
|
|
208
|
+
| `getISCookiesFromBag` | refactor | extract IS cookies from `request.headers.get('cookie')` directly — no canonical helper, the bag-based mechanism doesn't exist on TanStack Start |
|
|
209
|
+
|
|
210
|
+
Symbols not in the table get the generic guidance ("repoint to
|
|
211
|
+
`@decocms/apps/vtex/...` or `apps/commerce/utils/...`") — when you find
|
|
212
|
+
a new one worth pinning down, add it to `STUB_FIX_HINTS` in
|
|
213
|
+
[`scripts/migrate/post-cleanup/rules.ts`](https://github.com/decocms/deco-start/blob/main/scripts/migrate/post-cleanup/rules.ts).
|
|
214
|
+
|
|
215
|
+
### Recipe: expanding 1-arg `toProduct(p)` call sites
|
|
216
|
+
|
|
217
|
+
Two real-world patterns surface, requiring different fixes:
|
|
218
|
+
|
|
219
|
+
**Pattern A — call site already passes 4 args under `as any`** (e.g.
|
|
220
|
+
`smartShelfForYou.ts` on casaevideo): the dev wrote the call for
|
|
221
|
+
canonical, the import pointed at the stub. Fix is **import-only**:
|
|
222
|
+
|
|
223
|
+
```diff
|
|
224
|
+
-import { toProduct } from "~/lib/vtex-transform";
|
|
225
|
+
+import { toProduct } from "@decocms/apps/vtex/utils/transform";
|
|
226
|
+
|
|
227
|
+
const normalizedProducts = rawProducts.data.map((p: VTEXProduct) =>
|
|
228
|
+
(toProduct as any)(p, p.items?.[0], 0, {
|
|
229
|
+
baseUrl: baseURL,
|
|
230
|
+
priceCurrency: "BRL",
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The `as any` cast may stay if local `~/types/vtex.Product` and
|
|
236
|
+
canonical `LegacyProductVTEX | ProductVTEX` differ structurally — that's
|
|
237
|
+
a separate refactor.
|
|
238
|
+
|
|
239
|
+
**Pattern B — call site uses true 1-arg form** (e.g.
|
|
240
|
+
`intelligenseSearch.ts` on casaevideo): the dev relied on the stub's
|
|
241
|
+
identity-cast behaviour. Fix is to **expand the call** mirroring the
|
|
242
|
+
canonical pattern in
|
|
243
|
+
[`apps-start/vtex/loaders/autocomplete.ts`](https://github.com/decocms/apps-start/blob/main/vtex/loaders/autocomplete.ts):
|
|
244
|
+
|
|
245
|
+
```diff
|
|
246
|
+
-import { toProduct } from "~/lib/vtex-transform";
|
|
247
|
+
+import { pickSku, toProduct } from "@decocms/apps/vtex/utils/transform";
|
|
248
|
+
|
|
249
|
+
const baseURL = new URL(req.url).origin;
|
|
250
|
+
return {
|
|
251
|
+
searches,
|
|
252
|
+
- products: (products ?? []).map((p) => toProduct(p)).slice(0, count),
|
|
253
|
+
+ products: (products ?? []).slice(0, count).map((p: any) => {
|
|
254
|
+
+ const sku = pickSku(p);
|
|
255
|
+
+ return toProduct(p, sku, 0, { baseUrl: baseURL, priceCurrency: "BRL" });
|
|
256
|
+
+ }),
|
|
257
|
+
};
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`pickSku` handles the IS-shape SKU selection; without it, downstream
|
|
261
|
+
fields like `productID`, `gtin`, `additionalProperty[]` come back
|
|
262
|
+
empty.
|
|
263
|
+
|
|
264
|
+
**Pattern C — keep the stub deliberately**: rare, but valid when the
|
|
265
|
+
upstream API already returns canonical `Product[]` shape and the call
|
|
266
|
+
is purely a type-narrowing cast. Replace with a typed cast at the
|
|
267
|
+
boundary instead of importing a stub:
|
|
268
|
+
|
|
269
|
+
```diff
|
|
270
|
+
-import { toProduct } from "~/lib/vtex-transform";
|
|
271
|
+
+import type { Product } from "@decocms/apps/commerce/types";
|
|
272
|
+
|
|
273
|
+
-products: (products ?? []).map((p) => toProduct(p)).slice(0, count),
|
|
274
|
+
+products: ((products ?? []) as Product[]).slice(0, count),
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
This silences the audit (the stub import is gone) without changing
|
|
278
|
+
behaviour. Only do this if you've **verified** the upstream payload is
|
|
279
|
+
already schema.org-shaped.
|
|
280
|
+
|
|
281
|
+
Manual sweep (still useful if you don't have the audit handy):
|
|
163
282
|
|
|
164
283
|
```bash
|
|
165
284
|
rg "from ['\"]~/lib/vtex-" src/
|
|
166
285
|
# Expected: 0 hits (or only site-specific reasons you can articulate)
|
|
167
286
|
```
|
|
168
287
|
|
|
169
|
-
|
|
170
|
-
directly (or the corresponding
|
|
171
|
-
utility). Your runtime behavior
|
|
172
|
-
cookies, VTEX session auth all
|
|
173
|
-
silently stubbed to `{}` / `null`.
|
|
288
|
+
When you see real findings, update the imports to point at
|
|
289
|
+
`@decocms/apps/vtex/...` directly (or the corresponding
|
|
290
|
+
`commerce/utils/*` if it's a generic utility). Your runtime behavior
|
|
291
|
+
gets MUCH better — segment cookies, IS cookies, VTEX session auth all
|
|
292
|
+
start working again instead of being silently stubbed to `{}` / `null`.
|
|
293
|
+
|
|
294
|
+
**Note on `--fix`**: this rule is intentionally detect-only. Repointing
|
|
295
|
+
imports requires a per-symbol map to canonical apps/start exports
|
|
296
|
+
(e.g. `getSegmentFromBag` → `@decocms/apps/vtex/utils/segment`), which
|
|
297
|
+
the framework doesn't ship yet. Detect-only is still strictly more
|
|
298
|
+
useful than nothing — the precision means each finding maps to exactly
|
|
299
|
+
one PR's worth of mechanical work.
|
|
174
300
|
|
|
175
301
|
## 6. Drop `src/types/widgets.ts` — framework owns it
|
|
176
302
|
|
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,215 @@ const ruleSiteLocalGlobals: Rule = {
|
|
|
238
239
|
/* Rule 5 — `~/lib/vtex-*` shim regression */
|
|
239
240
|
/* ------------------------------------------------------------------ */
|
|
240
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Per-symbol guidance for the canonical replacement of each known
|
|
244
|
+
* shim stub. Used by the `vtex-shim-regression` rule to compose
|
|
245
|
+
* actionable `fix:` messages instead of the generic "Repoint imports"
|
|
246
|
+
* fallback.
|
|
247
|
+
*
|
|
248
|
+
* Kept as data (not code) so the JSON output of the audit can carry
|
|
249
|
+
* structured fix metadata for downstream tooling (CI dashboards,
|
|
250
|
+
* follow-up auto-fix rules, etc.).
|
|
251
|
+
*
|
|
252
|
+
* Categories:
|
|
253
|
+
* - `swap`: 1:1 import swap is safe — caller imports the symbol from
|
|
254
|
+
* `canonical` instead of the local shim. Note may flag a signature
|
|
255
|
+
* gotcha that the caller has to address at the call site.
|
|
256
|
+
* - `refactor`: a call-site rewrite is required (typically because the
|
|
257
|
+
* stub's "bag-based" API has no analog on TanStack Start; the request
|
|
258
|
+
* headers are the new source of truth). The note explains the pattern.
|
|
259
|
+
*
|
|
260
|
+
* Symbols absent from this table fall back to the generic guidance.
|
|
261
|
+
* The rule still flags them — only the `fix:` prose changes.
|
|
262
|
+
*/
|
|
263
|
+
export type FixHint =
|
|
264
|
+
| { kind: "swap"; canonical: string; note?: string }
|
|
265
|
+
| { kind: "refactor"; note: string };
|
|
266
|
+
|
|
267
|
+
export const STUB_FIX_HINTS: Record<string, FixHint> = {
|
|
268
|
+
// src/lib/vtex-transform
|
|
269
|
+
toProduct: {
|
|
270
|
+
kind: "swap",
|
|
271
|
+
canonical: "@decocms/apps/vtex/utils/transform",
|
|
272
|
+
note:
|
|
273
|
+
"canonical signature is `toProduct(product, sku, level, options)`; " +
|
|
274
|
+
"1-arg call sites need to expand args first — see skill § 5",
|
|
275
|
+
},
|
|
276
|
+
// src/lib/vtex-segment
|
|
277
|
+
getSegmentFromBag: {
|
|
278
|
+
kind: "refactor",
|
|
279
|
+
note:
|
|
280
|
+
"read cookies via `request.headers.get('cookie')` then call " +
|
|
281
|
+
"`buildSegmentFromCookies()` from '@decocms/apps/vtex/utils/segment'. " +
|
|
282
|
+
"The bag-based lookup mechanism does not exist on TanStack Start.",
|
|
283
|
+
},
|
|
284
|
+
withSegmentCookie: {
|
|
285
|
+
kind: "swap",
|
|
286
|
+
canonical: "@decocms/apps/vtex/utils/segment",
|
|
287
|
+
note:
|
|
288
|
+
"canonical signature is `withSegmentCookie(segment, headers?)`; " +
|
|
289
|
+
"if you currently pass only headers, also pass a segment object",
|
|
290
|
+
},
|
|
291
|
+
// src/lib/vtex-intelligent-search
|
|
292
|
+
getISCookiesFromBag: {
|
|
293
|
+
kind: "refactor",
|
|
294
|
+
note:
|
|
295
|
+
"extract IS cookies from `request.headers.get('cookie')` directly. " +
|
|
296
|
+
"The bag-based lookup mechanism does not exist on TanStack Start.",
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Format a single symbol's fix guidance as a one-liner suitable for
|
|
302
|
+
* the audit's `fix:` field. Returns undefined when the symbol has no
|
|
303
|
+
* specific entry in `STUB_FIX_HINTS`.
|
|
304
|
+
*/
|
|
305
|
+
export function formatFixHint(symbol: string): string | undefined {
|
|
306
|
+
const hint = STUB_FIX_HINTS[symbol];
|
|
307
|
+
if (!hint) return undefined;
|
|
308
|
+
if (hint.kind === "swap") {
|
|
309
|
+
const head = `${symbol} → ${hint.canonical} (1:1 import swap)`;
|
|
310
|
+
return hint.note ? `${head} — ${hint.note}` : head;
|
|
311
|
+
}
|
|
312
|
+
return `${symbol} → call-site refactor: ${hint.note}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Compose the `fix:` message for a finding from the per-shim stub map.
|
|
317
|
+
* Splits symbols into "have specific guidance" vs "fall back to generic".
|
|
318
|
+
* Output joins each piece with ` | ` so the message stays one logical
|
|
319
|
+
* line even when there are several stubs.
|
|
320
|
+
*/
|
|
321
|
+
export function buildVtexShimFixMessage(stubsBySim: Map<string, string[]>): string {
|
|
322
|
+
const known: string[] = [];
|
|
323
|
+
const unknown: string[] = [];
|
|
324
|
+
for (const syms of stubsBySim.values()) {
|
|
325
|
+
for (const s of syms) {
|
|
326
|
+
const hint = formatFixHint(s);
|
|
327
|
+
if (hint) known.push(hint);
|
|
328
|
+
else unknown.push(s);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const parts: string[] = [...known];
|
|
332
|
+
if (unknown.length > 0) {
|
|
333
|
+
parts.push(
|
|
334
|
+
`${unknown.join(", ")} → repoint to '@decocms/apps/vtex/...' or 'apps/commerce/utils/...'`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return parts.length > 0
|
|
338
|
+
? parts.join(" | ")
|
|
339
|
+
: "Repoint imports to '@decocms/apps/vtex/...' or 'apps/commerce/utils/...'";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Build the structured `fixHints` payload for `meta` so JSON consumers
|
|
344
|
+
* (CI dashboards, follow-up tooling) can render their own UI. Each
|
|
345
|
+
* entry is keyed by symbol; symbols without specific guidance are
|
|
346
|
+
* omitted (the prose fallback covers them).
|
|
347
|
+
*/
|
|
348
|
+
function fixHintsToMeta(stubsBySim: Map<string, string[]>): Record<string, FixHint> {
|
|
349
|
+
const out: Record<string, FixHint> = {};
|
|
350
|
+
for (const syms of stubsBySim.values()) {
|
|
351
|
+
for (const s of syms) {
|
|
352
|
+
const hint = STUB_FIX_HINTS[s];
|
|
353
|
+
if (hint) out[s] = hint;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Parse one or more ES `import { a, b as c, type d } from "spec"` blocks
|
|
361
|
+
* targeting a specific source spec out of a file. Returns the list of
|
|
362
|
+
* imported names (resolved to their original symbol, ignoring `as`
|
|
363
|
+
* rebinds), with `import type {…}` and inline `type` modifiers stripped
|
|
364
|
+
* — those carry no runtime, so the rule treats them as out-of-scope.
|
|
365
|
+
*/
|
|
366
|
+
function namedRuntimeImportsFrom(content: string, spec: string): string[] {
|
|
367
|
+
const escaped = spec.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
368
|
+
// `(type\s+)?` captures the entire-import `import type { … }` form.
|
|
369
|
+
// Per-symbol `type` modifiers inside the braces are stripped below.
|
|
370
|
+
const re = new RegExp(
|
|
371
|
+
`import\\s+(type\\s+)?\\{([^}]+)\\}\\s+from\\s+['\"]${escaped}['\"]`,
|
|
372
|
+
"g",
|
|
373
|
+
);
|
|
374
|
+
const out: string[] = [];
|
|
375
|
+
for (const m of content.matchAll(re)) {
|
|
376
|
+
if (m[1]) continue; // entire import is type-only
|
|
377
|
+
for (const raw of m[2].split(",")) {
|
|
378
|
+
const trimmed = raw.trim();
|
|
379
|
+
if (!trimmed || trimmed.startsWith("type ")) continue;
|
|
380
|
+
// `foo as bar` → `foo` (we want the source symbol, not the local alias).
|
|
381
|
+
const sourceName = trimmed.split(/\s+as\s+/)[0].trim();
|
|
382
|
+
if (sourceName) out.push(sourceName);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return out;
|
|
386
|
+
}
|
|
387
|
+
|
|
241
388
|
const ruleVtexShimRegression: Rule = {
|
|
242
389
|
id: "vtex-shim-regression",
|
|
243
390
|
title: "Imports from ~/lib/vtex-* (silent stub regression)",
|
|
244
391
|
run({ siteDir, fs }: RuleContext): Finding[] {
|
|
245
392
|
const tsFiles = fs.glob(siteDir, "src/**/*.{ts,tsx}", SRC_GLOB_EXCLUDES);
|
|
246
393
|
const findings: Finding[] = [];
|
|
247
|
-
|
|
394
|
+
|
|
395
|
+
// Per-shim classification cache. Each shim file is read at most once
|
|
396
|
+
// per audit run, even when imported by dozens of consumers.
|
|
397
|
+
const shimClasses = new Map<string, Map<string, ExportClass>>();
|
|
398
|
+
function classOf(shim: string, symbol: string): ExportClass {
|
|
399
|
+
let map = shimClasses.get(shim);
|
|
400
|
+
if (!map) {
|
|
401
|
+
const abs = `${siteDir}/src/lib/${shim}.ts`;
|
|
402
|
+
map = new Map<string, ExportClass>();
|
|
403
|
+
if (fs.exists(abs)) {
|
|
404
|
+
for (const ce of classifyShimExports(fs.readText(abs))) {
|
|
405
|
+
map.set(ce.name, ce.class);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
shimClasses.set(shim, map);
|
|
409
|
+
}
|
|
410
|
+
// Unknown symbols (file missing or not exported) default to "stub" —
|
|
411
|
+
// pessimistic on purpose. If the symbol can't be found locally, the
|
|
412
|
+
// import is at best dead code, at worst a TS error; either way the
|
|
413
|
+
// user wants visibility into it. Compile phase catches the TS side.
|
|
414
|
+
return map.get(symbol) ?? "stub";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Match the bare `from "~/lib/vtex-X"` to know which shims are touched.
|
|
418
|
+
const fromRe = /from\s+['"]~\/lib\/vtex-([A-Za-z0-9-]+)['"]/g;
|
|
248
419
|
for (const abs of tsFiles) {
|
|
249
420
|
if (abs.includes("/src/lib/")) continue;
|
|
250
421
|
const content = fs.readText(abs);
|
|
251
|
-
const
|
|
252
|
-
|
|
422
|
+
const usedShims = new Set<string>(
|
|
423
|
+
[...content.matchAll(fromRe)].map((m) => `vtex-${m[1]}`),
|
|
424
|
+
);
|
|
425
|
+
if (usedShims.size === 0) continue;
|
|
426
|
+
|
|
427
|
+
// Per-file: which shim → which stub symbols are imported.
|
|
428
|
+
const stubsBySim = new Map<string, string[]>();
|
|
429
|
+
for (const shim of usedShims) {
|
|
430
|
+
const symbols = namedRuntimeImportsFrom(content, `~/lib/${shim}`);
|
|
431
|
+
const stubs = symbols.filter((s) => classOf(shim, s) === "stub");
|
|
432
|
+
if (stubs.length > 0) stubsBySim.set(shim, stubs);
|
|
433
|
+
}
|
|
434
|
+
if (stubsBySim.size === 0) continue;
|
|
435
|
+
|
|
253
436
|
const rel = abs.slice(siteDir.length + 1);
|
|
254
|
-
const
|
|
437
|
+
const detail = [...stubsBySim.entries()]
|
|
438
|
+
.map(([s, syms]) => `${s} (${syms.join(", ")})`)
|
|
439
|
+
.join("; ");
|
|
440
|
+
const fixHintsMeta = fixHintsToMeta(stubsBySim);
|
|
255
441
|
findings.push({
|
|
256
442
|
rule: "vtex-shim-regression",
|
|
257
443
|
severity: "warning",
|
|
258
444
|
file: rel,
|
|
259
|
-
message: `Imports
|
|
260
|
-
fix:
|
|
261
|
-
meta: {
|
|
445
|
+
message: `Imports stub-only symbols from ${detail} — runtime is silently stubbed`,
|
|
446
|
+
fix: buildVtexShimFixMessage(stubsBySim),
|
|
447
|
+
meta: {
|
|
448
|
+
stubsBySim: Object.fromEntries(stubsBySim),
|
|
449
|
+
...(Object.keys(fixHintsMeta).length > 0 ? { fixHints: fixHintsMeta } : {}),
|
|
450
|
+
},
|
|
262
451
|
});
|
|
263
452
|
}
|
|
264
453
|
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,224 @@ 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
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe("rule: vtex-shim-regression — per-symbol fix hints", () => {
|
|
408
|
+
it("emits 1:1 swap hint for `toProduct`", () => {
|
|
409
|
+
const fs = makeFs({
|
|
410
|
+
"/site/src/lib/vtex-transform.ts":
|
|
411
|
+
"export function toProduct(p: any): unknown { return p as unknown; }\n",
|
|
412
|
+
"/site/src/loaders/x.ts":
|
|
413
|
+
'import { toProduct } from "~/lib/vtex-transform";\n',
|
|
414
|
+
});
|
|
415
|
+
const report = runAudit(SITE, fs);
|
|
416
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
417
|
+
expect(r.findings).toHaveLength(1);
|
|
418
|
+
const f = r.findings[0];
|
|
419
|
+
expect(f.fix).toContain("toProduct → @decocms/apps/vtex/utils/transform");
|
|
420
|
+
expect(f.fix).toContain("1:1 import swap");
|
|
421
|
+
expect(f.meta?.fixHints).toEqual({
|
|
422
|
+
toProduct: {
|
|
423
|
+
kind: "swap",
|
|
424
|
+
canonical: "@decocms/apps/vtex/utils/transform",
|
|
425
|
+
note: expect.stringContaining("canonical signature"),
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("emits refactor hint for `getSegmentFromBag`", () => {
|
|
431
|
+
const fs = makeFs({
|
|
432
|
+
"/site/src/lib/vtex-segment.ts":
|
|
433
|
+
"export function getSegmentFromBag(): null { return null; }\n",
|
|
434
|
+
"/site/src/loaders/x.ts":
|
|
435
|
+
'import { getSegmentFromBag } from "~/lib/vtex-segment";\n',
|
|
436
|
+
});
|
|
437
|
+
const report = runAudit(SITE, fs);
|
|
438
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
439
|
+
const f = r.findings[0];
|
|
440
|
+
expect(f.fix).toContain("getSegmentFromBag → call-site refactor");
|
|
441
|
+
expect(f.fix).toContain("buildSegmentFromCookies");
|
|
442
|
+
expect(f.meta?.fixHints).toEqual({
|
|
443
|
+
getSegmentFromBag: {
|
|
444
|
+
kind: "refactor",
|
|
445
|
+
note: expect.stringContaining("buildSegmentFromCookies"),
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("composes hints for files with multiple stubs", () => {
|
|
451
|
+
const fs = makeFs({
|
|
452
|
+
"/site/src/lib/vtex-segment.ts":
|
|
453
|
+
"export function getSegmentFromBag(): null { return null; }\n",
|
|
454
|
+
"/site/src/lib/vtex-transform.ts":
|
|
455
|
+
"export function toProduct(p: any): unknown { return p as unknown; }\n",
|
|
456
|
+
"/site/src/loaders/x.ts":
|
|
457
|
+
'import { getSegmentFromBag } from "~/lib/vtex-segment";\n' +
|
|
458
|
+
'import { toProduct } from "~/lib/vtex-transform";\n',
|
|
459
|
+
});
|
|
460
|
+
const report = runAudit(SITE, fs);
|
|
461
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
462
|
+
const f = r.findings[0];
|
|
463
|
+
expect(f.fix).toContain("getSegmentFromBag → call-site refactor");
|
|
464
|
+
expect(f.fix).toContain("toProduct → @decocms/apps/vtex/utils/transform");
|
|
465
|
+
// Joined with " | " for visual separation.
|
|
466
|
+
expect(f.fix).toContain(" | ");
|
|
467
|
+
expect(Object.keys(f.meta?.fixHints as object)).toEqual(
|
|
468
|
+
expect.arrayContaining(["toProduct", "getSegmentFromBag"]),
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("falls back to generic hint for symbols without entries", () => {
|
|
473
|
+
const fs = makeFs({
|
|
474
|
+
"/site/src/lib/vtex-mystery.ts":
|
|
475
|
+
"export function unknownStub(): null { return null; }\n",
|
|
476
|
+
"/site/src/loaders/x.ts":
|
|
477
|
+
'import { unknownStub } from "~/lib/vtex-mystery";\n',
|
|
478
|
+
});
|
|
479
|
+
const report = runAudit(SITE, fs);
|
|
480
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
481
|
+
const f = r.findings[0];
|
|
482
|
+
expect(f.fix).toContain("unknownStub → repoint to '@decocms/apps/vtex/...");
|
|
483
|
+
// No fixHints in meta when no symbols match the table.
|
|
484
|
+
expect(f.meta?.fixHints).toBeUndefined();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("mixes specific hints and generic fallback in one message", () => {
|
|
488
|
+
const fs = makeFs({
|
|
489
|
+
"/site/src/lib/vtex-transform.ts":
|
|
490
|
+
"export function toProduct(p: any): unknown { return p as unknown; }\n",
|
|
491
|
+
"/site/src/lib/vtex-mystery.ts":
|
|
492
|
+
"export function unknownStub(): null { return null; }\n",
|
|
493
|
+
"/site/src/loaders/x.ts":
|
|
494
|
+
'import { toProduct } from "~/lib/vtex-transform";\n' +
|
|
495
|
+
'import { unknownStub } from "~/lib/vtex-mystery";\n',
|
|
496
|
+
});
|
|
497
|
+
const report = runAudit(SITE, fs);
|
|
498
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
499
|
+
const f = r.findings[0];
|
|
500
|
+
expect(f.fix).toContain("toProduct → @decocms/apps/vtex/utils/transform");
|
|
501
|
+
expect(f.fix).toContain("unknownStub → repoint");
|
|
502
|
+
// Only the known symbol shows up in fixHints.
|
|
503
|
+
expect(f.meta?.fixHints).toEqual({
|
|
504
|
+
toProduct: expect.objectContaining({ kind: "swap" }),
|
|
505
|
+
});
|
|
506
|
+
});
|
|
284
507
|
});
|
|
285
508
|
|
|
286
509
|
describe("rule: local-widgets-types", () => {
|