@decocms/start 2.14.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.
@@ -168,16 +168,116 @@ one imported symbol is a real silent stub (returns `null` / `{}` / `[]`
168
168
  alongside stubs (e.g. a `parseCookie` cookie parser, a `fetchSafe`
169
169
  wrapper) no longer create noise.
170
170
 
171
- The audit's finding names the exact stub symbols, e.g.
171
+ The audit's finding names the exact stub symbols **and emits per-symbol
172
+ fix guidance**, e.g.
172
173
 
173
174
  ```
174
175
  [WARNING] src/loaders/search/x.ts — Imports stub-only symbols from
175
176
  vtex-transform (toProduct); vtex-segment (getSegmentFromBag) —
176
177
  runtime is silently stubbed
177
- fix: Repoint imports to '@decocms/apps/vtex/...' or
178
- 'apps/commerce/utils/...'
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'.
179
184
  ```
180
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
+
181
281
  Manual sweep (still useful if you don't have the audit handy):
182
282
 
183
283
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
@@ -239,6 +239,123 @@ const ruleSiteLocalGlobals: Rule = {
239
239
  /* Rule 5 — `~/lib/vtex-*` shim regression */
240
240
  /* ------------------------------------------------------------------ */
241
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
+
242
359
  /**
243
360
  * Parse one or more ES `import { a, b as c, type d } from "spec"` blocks
244
361
  * targeting a specific source spec out of a file. Returns the list of
@@ -319,16 +436,17 @@ const ruleVtexShimRegression: Rule = {
319
436
  const rel = abs.slice(siteDir.length + 1);
320
437
  const detail = [...stubsBySim.entries()]
321
438
  .map(([s, syms]) => `${s} (${syms.join(", ")})`)
322
- .join("; ")
323
- ;
439
+ .join("; ");
440
+ const fixHintsMeta = fixHintsToMeta(stubsBySim);
324
441
  findings.push({
325
442
  rule: "vtex-shim-regression",
326
443
  severity: "warning",
327
444
  file: rel,
328
445
  message: `Imports stub-only symbols from ${detail} — runtime is silently stubbed`,
329
- fix: "Repoint imports to '@decocms/apps/vtex/...' or 'apps/commerce/utils/...'",
446
+ fix: buildVtexShimFixMessage(stubsBySim),
330
447
  meta: {
331
448
  stubsBySim: Object.fromEntries(stubsBySim),
449
+ ...(Object.keys(fixHintsMeta).length > 0 ? { fixHints: fixHintsMeta } : {}),
332
450
  },
333
451
  });
334
452
  }
@@ -404,6 +404,108 @@ describe("rule: vtex-shim-regression", () => {
404
404
  });
405
405
  });
406
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
+ });
507
+ });
508
+
407
509
  describe("rule: local-widgets-types", () => {
408
510
  it("flags presence of src/types/widgets.ts and counts imports", () => {
409
511
  const fs = makeFs({