@decocms/start 2.14.0 → 2.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "2.14.0",
3
+ "version": "2.16.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({
@@ -89,3 +89,51 @@ describe("selectImportedLibTemplates()", () => {
89
89
  }
90
90
  });
91
91
  });
92
+
93
+ describe("D3 — generated stubs throw at runtime", () => {
94
+ // Each stub MUST throw an Error whose message identifies:
95
+ // - the stub path so the dev sees it in their stack trace
96
+ // - the canonical replacement (so the fix is mechanical)
97
+ //
98
+ // See migration-tooling-policy.mdc § Decision 3.
99
+ it("vtex-transform.toProduct throws and points at the canonical path", () => {
100
+ const src = LIB_TEMPLATES["src/lib/vtex-transform.ts"];
101
+ expect(src).toMatch(/throw new Error/);
102
+ expect(src).toMatch(/@decocms\/apps\/vtex\/utils\/transform/);
103
+ expect(src).toMatch(/\[deco-migrate\]/);
104
+ });
105
+
106
+ it("vtex-intelligent-search.getISCookiesFromBag throws", () => {
107
+ const src = LIB_TEMPLATES["src/lib/vtex-intelligent-search.ts"];
108
+ expect(src).toMatch(/getISCookiesFromBag[\s\S]*?throw new Error/);
109
+ expect(src).toMatch(/\[deco-migrate\]/);
110
+ // The other helpers in this file (isFilterParam, toPath,
111
+ // withDefaultFacets, withDefaultParams) are real impls — must not
112
+ // throw.
113
+ expect(src).toMatch(/export function isFilterParam[\s\S]*?return key\.startsWith/);
114
+ });
115
+
116
+ it("vtex-segment.getSegmentFromBag and withSegmentCookie both throw", () => {
117
+ const src = LIB_TEMPLATES["src/lib/vtex-segment.ts"];
118
+ expect(src).toMatch(/getSegmentFromBag[\s\S]*?throw new Error/);
119
+ expect(src).toMatch(/withSegmentCookie[\s\S]*?throw new Error/);
120
+ expect(src).toMatch(/@decocms\/apps\/vtex\/utils\/segment/);
121
+ });
122
+
123
+ it("non-stub helpers stay implemented (negative check — no throw)", () => {
124
+ // These are real impls, not stubs. They must not throw.
125
+ const real = [
126
+ "src/lib/http-utils.ts",
127
+ "src/lib/vtex-id.ts",
128
+ "src/lib/graphql-utils.ts",
129
+ "src/lib/filter-navigate.ts",
130
+ "src/lib/fetch-utils.ts",
131
+ ];
132
+ for (const key of real) {
133
+ const src = LIB_TEMPLATES[key];
134
+ expect(src, `${key} should not contain a generated stub throw`).not.toMatch(
135
+ /\[deco-migrate\][^"]*generated stub/,
136
+ );
137
+ }
138
+ });
139
+ });
@@ -38,15 +38,41 @@ export function selectImportedLibTemplates(
38
38
  return out;
39
39
  }
40
40
 
41
+ // Per the migration tooling policy (D3 — Throwing stubs):
42
+ // generated stubs MUST throw at runtime so the first call surfaces the
43
+ // gap loudly. Silent identity-cast `toProduct` was the bug behind
44
+ // baggagio-tanstack#10 (PDP product data was being dropped on the floor
45
+ // for weeks before anyone noticed).
46
+ //
47
+ // Each thrown message points at the canonical replacement so the fix
48
+ // is mechanical. `deco-post-cleanup --fix` automates the swap.
41
49
  const LIB_VTEX_TRANSFORM = `import type { Product } from "@decocms/apps/commerce/types";
42
50
 
43
- export function toProduct(vtexProduct: any): Product {
44
- return vtexProduct as Product;
51
+ const STUB =
52
+ "[deco-migrate] \`~/lib/vtex-transform.toProduct\` is a generated stub. " +
53
+ "Replace with: import { toProduct } from '@decocms/apps/vtex/utils/transform' " +
54
+ "(canonical signature: \`toProduct(product, sku, level, options)\`). " +
55
+ "Run \`deco-post-cleanup --fix\` or see the deco-to-tanstack-migration skill " +
56
+ "(post-migration-cleanup § 5).";
57
+
58
+ export function toProduct(_vtexProduct: any, ..._rest: any[]): Product {
59
+ throw new Error(STUB);
45
60
  }
46
61
  `;
47
62
 
48
- const LIB_VTEX_INTELLIGENT_SEARCH = `export function getISCookiesFromBag(_req?: any): Record<string, string> {
49
- return {};
63
+ const LIB_VTEX_INTELLIGENT_SEARCH = `// Per the migration tooling policy (D3): \`getISCookiesFromBag\` cannot
64
+ // be implemented on TanStack Start because the bag-based lookup
65
+ // mechanism does not exist. Sites must read cookies directly from the
66
+ // request — see the \`vtex-shim-regression\` audit rule for guidance.
67
+ const STUB_GET_IS_COOKIES =
68
+ "[deco-migrate] \`~/lib/vtex-intelligent-search.getISCookiesFromBag\` is a " +
69
+ "generated stub. Refactor: extract IS cookies from " +
70
+ "\`request.headers.get('cookie')\` directly. The bag-based lookup mechanism " +
71
+ "does not exist on TanStack Start. See the deco-to-tanstack-migration " +
72
+ "skill (post-migration-cleanup § 5).";
73
+
74
+ export function getISCookiesFromBag(_req?: any): Record<string, string> {
75
+ throw new Error(STUB_GET_IS_COOKIES);
50
76
  }
51
77
 
52
78
  export function isFilterParam(key: string): boolean {
@@ -85,17 +111,30 @@ export function withDefaultParams(
85
111
  }
86
112
  `;
87
113
 
88
- const LIB_VTEX_SEGMENT = `export function getSegmentFromBag(_req?: any): Record<string, unknown> | null {
89
- return null;
114
+ const LIB_VTEX_SEGMENT = `// Per the migration tooling policy (D3): both these stubs throw at
115
+ // runtime to force the call site to be fixed. Silent fallbacks here
116
+ // mean the storefront silently fails to forward VTEX segment data
117
+ // (sales channel, regionId, currency, etc.) and pricing/inventory
118
+ // quietly diverge from what the user should see.
119
+ const STUB_GET_SEGMENT_FROM_BAG =
120
+ "[deco-migrate] \`~/lib/vtex-segment.getSegmentFromBag\` is a generated " +
121
+ "stub. Refactor: read cookies via \`request.headers.get('cookie')\` then " +
122
+ "call \`buildSegmentFromCookies()\` from '@decocms/apps/vtex/utils/segment'. " +
123
+ "The bag-based lookup mechanism does not exist on TanStack Start.";
124
+
125
+ const STUB_WITH_SEGMENT_COOKIE =
126
+ "[deco-migrate] \`~/lib/vtex-segment.withSegmentCookie\` is a generated " +
127
+ "stub. Replace with: import { withSegmentCookie } from " +
128
+ "'@decocms/apps/vtex/utils/segment' (canonical signature: " +
129
+ "\`withSegmentCookie(segment, headers?)\`). Run \`deco-post-cleanup --fix\` " +
130
+ "or see the deco-to-tanstack-migration skill.";
131
+
132
+ export function getSegmentFromBag(_req?: any): Record<string, unknown> | null {
133
+ throw new Error(STUB_GET_SEGMENT_FROM_BAG);
90
134
  }
91
135
 
92
136
  export function withSegmentCookie(..._args: any[]): any {
93
- for (const arg of _args) {
94
- if (arg instanceof Headers) {
95
- return arg;
96
- }
97
- }
98
- return new Headers();
137
+ throw new Error(STUB_WITH_SEGMENT_COOKIE);
99
138
  }
100
139
  `;
101
140