@decocms/start 2.16.0 → 2.18.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 +24 -6
- package/package.json +1 -1
- package/scripts/migrate/phase-scaffold.ts +12 -0
- package/scripts/migrate/post-cleanup/rules.ts +54 -0
- package/scripts/migrate/post-cleanup/runner.test.ts +124 -1
- package/scripts/migrate/templates/cursor-rules.test.ts +59 -0
- package/scripts/migrate/templates/cursor-rules.ts +70 -0
- package/scripts/migrate/templates/hooks.test.ts +56 -0
- package/scripts/migrate/templates/hooks.ts +39 -8
|
@@ -291,12 +291,30 @@ When you see real findings, update the imports to point at
|
|
|
291
291
|
gets MUCH better — segment cookies, IS cookies, VTEX session auth all
|
|
292
292
|
start working again instead of being silently stubbed to `{}` / `null`.
|
|
293
293
|
|
|
294
|
-
**Note on `--fix
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
294
|
+
**Note on `--fix`** (since `@decocms/start >= 2.16.0`): the rule
|
|
295
|
+
auto-fixes the SAFE subset of swaps — when every imported symbol from
|
|
296
|
+
a given shim is a `kind: "swap"` hint to the SAME canonical module.
|
|
297
|
+
Concretely:
|
|
298
|
+
|
|
299
|
+
| Pattern | `--fix` behaviour |
|
|
300
|
+
|---|---|
|
|
301
|
+
| `import { toProduct } from "~/lib/vtex-transform"` | rewritten to `@decocms/apps/vtex/utils/transform` |
|
|
302
|
+
| `import { withSegmentCookie } from "~/lib/vtex-segment"` | rewritten to `@decocms/apps/vtex/utils/segment` |
|
|
303
|
+
| `import { getSegmentFromBag, withSegmentCookie } from "~/lib/vtex-segment"` | left untouched (mixed swap + refactor) |
|
|
304
|
+
| `import { getISCookiesFromBag } from "~/lib/vtex-intelligent-search"` | left untouched (refactor-only — no canonical drop-in) |
|
|
305
|
+
| `import { toProduct, isFilterParam } from "~/lib/vtex-transform"` | left untouched (would lose the real impl) |
|
|
306
|
+
|
|
307
|
+
The auto-fix rewrites only the `from "..."` clause — the imported
|
|
308
|
+
names list is preserved verbatim, so `as`-aliased imports (e.g.
|
|
309
|
+
`{ toProduct as toP }`) keep working. After the import swap you may
|
|
310
|
+
still need to expand 1-arg `toProduct(p)` call sites to the canonical
|
|
311
|
+
4-arg signature — see § 5 below.
|
|
312
|
+
|
|
313
|
+
The refactor-only cases (`getSegmentFromBag`, `getISCookiesFromBag`,
|
|
314
|
+
mixed surfaces) intentionally stay manual: the bag-based lookup
|
|
315
|
+
mechanism doesn't exist on TanStack Start, so each call site needs a
|
|
316
|
+
human reading `request.headers.get('cookie')` and calling
|
|
317
|
+
`buildSegmentFromCookies()` from the canonical module.
|
|
300
318
|
|
|
301
319
|
## 6. Drop `src/types/widgets.ts` — framework owns it
|
|
302
320
|
|
package/package.json
CHANGED
|
@@ -18,6 +18,7 @@ import { generateCommerceLoaders } from "./templates/commerce-loaders";
|
|
|
18
18
|
import { generateSectionLoaders } from "./templates/section-loaders";
|
|
19
19
|
import { generateCacheConfig } from "./templates/cache-config";
|
|
20
20
|
import { generateSdkFiles } from "./templates/sdk-gen";
|
|
21
|
+
import { generateMigrationPolicyPointerRule } from "./templates/cursor-rules";
|
|
21
22
|
// `lib-utils` is imported lazily — see end of phase-cleanup. Eager
|
|
22
23
|
// generation of all 11 shims left every site with dead code that had
|
|
23
24
|
// to be cleaned up by hand.
|
|
@@ -139,6 +140,17 @@ export function scaffold(ctx: MigrationContext): void {
|
|
|
139
140
|
writeFile(ctx, "src/components/ui/Theme.tsx", generateSiteThemeComponent());
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
// Migration tooling policy pointer rule (D1–D5 + priorities).
|
|
144
|
+
// The canonical rule lives in decocms/deco-start; this is a tiny
|
|
145
|
+
// pointer that loads on every Cursor session in the migrated site
|
|
146
|
+
// so agents working on the site know where the policy is and what
|
|
147
|
+
// it means here. See MIGRATION_TOOLING_PLAN.md (Wave 12-H).
|
|
148
|
+
writeFile(
|
|
149
|
+
ctx,
|
|
150
|
+
".cursor/rules/migration-tooling-policy.mdc",
|
|
151
|
+
generateMigrationPolicyPointerRule(ctx.siteName),
|
|
152
|
+
);
|
|
153
|
+
|
|
142
154
|
// Create public/ directory
|
|
143
155
|
if (!ctx.dryRun) {
|
|
144
156
|
fs.mkdirSync(path.join(ctx.sourceDir, "public"), { recursive: true });
|
|
@@ -452,6 +452,60 @@ const ruleVtexShimRegression: Rule = {
|
|
|
452
452
|
}
|
|
453
453
|
return findings;
|
|
454
454
|
},
|
|
455
|
+
applyFix({ siteDir, fs }, findings, writer): FixAction[] {
|
|
456
|
+
if (findings.length === 0) return [];
|
|
457
|
+
const actions: FixAction[] = [];
|
|
458
|
+
|
|
459
|
+
// Per-file rewrite. Conservative: only swap the import path when EVERY
|
|
460
|
+
// imported symbol from the shim is a `kind: "swap"` hint pointing at
|
|
461
|
+
// the same canonical module. Mixed surfaces (some swap + some
|
|
462
|
+
// refactor, or a real impl + a stub) stay untouched — those need a
|
|
463
|
+
// human looking at call-site signatures.
|
|
464
|
+
for (const finding of findings) {
|
|
465
|
+
const stubsBySim = (finding.meta?.stubsBySim ?? {}) as Record<string, string[]>;
|
|
466
|
+
const abs = `${siteDir}/${finding.file}`;
|
|
467
|
+
if (!fs.exists(abs)) continue;
|
|
468
|
+
|
|
469
|
+
let content = fs.readText(abs);
|
|
470
|
+
let modified = false;
|
|
471
|
+
|
|
472
|
+
for (const [shim, _stubSyms] of Object.entries(stubsBySim)) {
|
|
473
|
+
const oldSpec = `~/lib/${shim}`;
|
|
474
|
+
const importedSymbols = namedRuntimeImportsFrom(content, oldSpec);
|
|
475
|
+
if (importedSymbols.length === 0) continue;
|
|
476
|
+
|
|
477
|
+
// Every imported symbol must be a swap-kind hint AND every hint
|
|
478
|
+
// must point at the same canonical module — otherwise we'd
|
|
479
|
+
// either drop a real impl or split the import across two paths,
|
|
480
|
+
// both of which are unsafe to do mechanically here.
|
|
481
|
+
const hints = importedSymbols.map((s) => STUB_FIX_HINTS[s]);
|
|
482
|
+
const allSwap = hints.every((h) => h && h.kind === "swap");
|
|
483
|
+
if (!allSwap) continue;
|
|
484
|
+
const targets = new Set(
|
|
485
|
+
hints.map((h) => (h as { kind: "swap"; canonical: string }).canonical),
|
|
486
|
+
);
|
|
487
|
+
if (targets.size !== 1) continue;
|
|
488
|
+
const target = [...targets][0];
|
|
489
|
+
|
|
490
|
+
const escaped = oldSpec.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
491
|
+
const importLineRe = new RegExp(`from\\s+(['"])${escaped}\\1`, "g");
|
|
492
|
+
const next = content.replace(importLineRe, (_m, q) => `from ${q}${target}${q}`);
|
|
493
|
+
if (next !== content) {
|
|
494
|
+
content = next;
|
|
495
|
+
modified = true;
|
|
496
|
+
actions.push({
|
|
497
|
+
file: finding.file,
|
|
498
|
+
kind: "rewrite-imports",
|
|
499
|
+
detail: `${oldSpec} → ${target} (${importedSymbols.join(", ")})`,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (modified) writer.writeText(abs, content);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return actions;
|
|
508
|
+
},
|
|
455
509
|
};
|
|
456
510
|
|
|
457
511
|
/* ------------------------------------------------------------------ */
|
|
@@ -587,7 +587,12 @@ describe("runAudit — totals", () => {
|
|
|
587
587
|
.map((r) => r.rule)
|
|
588
588
|
.sort();
|
|
589
589
|
expect(supported).toEqual(
|
|
590
|
-
[
|
|
590
|
+
[
|
|
591
|
+
"dead-lib-shims",
|
|
592
|
+
"dead-runtime-shim",
|
|
593
|
+
"local-widgets-types",
|
|
594
|
+
"vtex-shim-regression",
|
|
595
|
+
].sort(),
|
|
591
596
|
);
|
|
592
597
|
});
|
|
593
598
|
});
|
|
@@ -681,3 +686,121 @@ describe("runAudit — fix mode", () => {
|
|
|
681
686
|
expect(store["/site/src/sections/A.tsx"]).toContain('"~/types/widgets-extra"');
|
|
682
687
|
});
|
|
683
688
|
});
|
|
689
|
+
|
|
690
|
+
/* ------------------------------------------------------------------ */
|
|
691
|
+
/* W12-D / W12-E — vtex-shim-regression --fix for swap-able cases */
|
|
692
|
+
/* ------------------------------------------------------------------ */
|
|
693
|
+
|
|
694
|
+
describe("runAudit — fix mode — vtex-shim-regression swap cases", () => {
|
|
695
|
+
it("rewrites `toProduct` import to canonical when it is the only stub imported", () => {
|
|
696
|
+
const { fs, writer, store } = makeMutableFs({
|
|
697
|
+
"/site/src/lib/vtex-transform.ts":
|
|
698
|
+
"export function toProduct(p: any): unknown { return p as unknown; }\n",
|
|
699
|
+
"/site/src/loaders/search.ts":
|
|
700
|
+
'import { toProduct } from "~/lib/vtex-transform";\nconsole.log(toProduct);\n',
|
|
701
|
+
});
|
|
702
|
+
const report = runAudit(SITE, fs, { writer });
|
|
703
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
704
|
+
expect(r.fixes).toHaveLength(1);
|
|
705
|
+
expect(r.fixes![0].file).toBe("src/loaders/search.ts");
|
|
706
|
+
expect(r.fixes![0].kind).toBe("rewrite-imports");
|
|
707
|
+
expect(r.fixes![0].detail).toContain("@decocms/apps/vtex/utils/transform");
|
|
708
|
+
expect(r.fixes![0].detail).toContain("toProduct");
|
|
709
|
+
expect(store["/site/src/loaders/search.ts"]).toContain(
|
|
710
|
+
'"@decocms/apps/vtex/utils/transform"',
|
|
711
|
+
);
|
|
712
|
+
expect(store["/site/src/loaders/search.ts"]).not.toContain('"~/lib/vtex-transform"');
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it("rewrites `withSegmentCookie` import to canonical when it is the only stub imported", () => {
|
|
716
|
+
const { fs, writer, store } = makeMutableFs({
|
|
717
|
+
// Mirrors the post-#123 throwing-stub body — `throw new Error(...)`
|
|
718
|
+
// is recognised by the shim classifier as `unconditional throw`.
|
|
719
|
+
"/site/src/lib/vtex-segment.ts":
|
|
720
|
+
'export function withSegmentCookie(..._args: any[]): any { throw new Error("stub"); }\n',
|
|
721
|
+
"/site/src/loaders/x.ts":
|
|
722
|
+
'import { withSegmentCookie } from "~/lib/vtex-segment";\nconsole.log(withSegmentCookie);\n',
|
|
723
|
+
});
|
|
724
|
+
const report = runAudit(SITE, fs, { writer });
|
|
725
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
726
|
+
expect(r.fixes).toHaveLength(1);
|
|
727
|
+
expect(r.fixes![0].detail).toContain("@decocms/apps/vtex/utils/segment");
|
|
728
|
+
expect(store["/site/src/loaders/x.ts"]).toContain(
|
|
729
|
+
'"@decocms/apps/vtex/utils/segment"',
|
|
730
|
+
);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it("does NOT rewrite mixed swap+refactor surface (getSegmentFromBag is refactor-only)", () => {
|
|
734
|
+
const { fs, writer, store } = makeMutableFs({
|
|
735
|
+
"/site/src/lib/vtex-segment.ts":
|
|
736
|
+
"export function getSegmentFromBag(_req?: any): null { return null; }\n" +
|
|
737
|
+
'export function withSegmentCookie(..._args: any[]): any { throw new Error("stub"); }\n',
|
|
738
|
+
"/site/src/loaders/x.ts":
|
|
739
|
+
'import { getSegmentFromBag, withSegmentCookie } from "~/lib/vtex-segment";\n',
|
|
740
|
+
});
|
|
741
|
+
const before = store["/site/src/loaders/x.ts"];
|
|
742
|
+
const report = runAudit(SITE, fs, { writer });
|
|
743
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
744
|
+
// Finding still emitted (audit), but no fix applied (mixed surface).
|
|
745
|
+
expect(r.findings).toHaveLength(1);
|
|
746
|
+
expect(r.fixes).toEqual([]);
|
|
747
|
+
expect(store["/site/src/loaders/x.ts"]).toBe(before);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it("does NOT rewrite when the file imports a real impl from the same shim", () => {
|
|
751
|
+
// vtex-intelligent-search exports both `getISCookiesFromBag` (stub →
|
|
752
|
+
// refactor) and `isFilterParam` (real impl, not in STUB_FIX_HINTS).
|
|
753
|
+
// Even if only one symbol is imported, the rule's classifier already
|
|
754
|
+
// skips real impls. But if a file imports BOTH, our --fix must not
|
|
755
|
+
// rewrite — the canonical doesn't expose isFilterParam.
|
|
756
|
+
const { fs, writer, store } = makeMutableFs({
|
|
757
|
+
"/site/src/lib/vtex-intelligent-search.ts":
|
|
758
|
+
"export function getISCookiesFromBag(_r?: any): Record<string,string> { return {}; }\n" +
|
|
759
|
+
"export function isFilterParam(k: string): boolean { return k.startsWith('filter.'); }\n",
|
|
760
|
+
"/site/src/loaders/x.ts":
|
|
761
|
+
'import { getISCookiesFromBag, isFilterParam } from "~/lib/vtex-intelligent-search";\n',
|
|
762
|
+
});
|
|
763
|
+
const before = store["/site/src/loaders/x.ts"];
|
|
764
|
+
const report = runAudit(SITE, fs, { writer });
|
|
765
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
766
|
+
expect(r.findings).toHaveLength(1);
|
|
767
|
+
expect(r.fixes).toEqual([]);
|
|
768
|
+
expect(store["/site/src/loaders/x.ts"]).toBe(before);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it("rewrites multiple files using the same swap-able shim in one pass", () => {
|
|
772
|
+
const { fs, writer, store } = makeMutableFs({
|
|
773
|
+
"/site/src/lib/vtex-transform.ts":
|
|
774
|
+
"export function toProduct(p: any): unknown { return p as unknown; }\n",
|
|
775
|
+
"/site/src/loaders/A.ts":
|
|
776
|
+
'import { toProduct } from "~/lib/vtex-transform";\n',
|
|
777
|
+
"/site/src/loaders/B.ts":
|
|
778
|
+
"import { toProduct } from '~/lib/vtex-transform';\n",
|
|
779
|
+
});
|
|
780
|
+
const report = runAudit(SITE, fs, { writer });
|
|
781
|
+
const r = report.rules.find((r) => r.rule === "vtex-shim-regression")!;
|
|
782
|
+
expect(r.fixes!.length).toBe(2);
|
|
783
|
+
expect(store["/site/src/loaders/A.ts"]).toContain(
|
|
784
|
+
'"@decocms/apps/vtex/utils/transform"',
|
|
785
|
+
);
|
|
786
|
+
expect(store["/site/src/loaders/B.ts"]).toContain(
|
|
787
|
+
"'@decocms/apps/vtex/utils/transform'",
|
|
788
|
+
);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it("preserves the named-imports list verbatim when swapping", () => {
|
|
792
|
+
// The fix only rewrites the FROM clause, not the imported names. Keeps
|
|
793
|
+
// local aliases (`as`) intact.
|
|
794
|
+
const { fs, writer, store } = makeMutableFs({
|
|
795
|
+
"/site/src/lib/vtex-transform.ts":
|
|
796
|
+
"export function toProduct(p: any): unknown { return p as unknown; }\n",
|
|
797
|
+
"/site/src/loaders/x.ts":
|
|
798
|
+
'import { toProduct as toP } from "~/lib/vtex-transform";\n' +
|
|
799
|
+
"console.log(toP);\n",
|
|
800
|
+
});
|
|
801
|
+
runAudit(SITE, fs, { writer });
|
|
802
|
+
expect(store["/site/src/loaders/x.ts"]).toContain(
|
|
803
|
+
'import { toProduct as toP } from "@decocms/apps/vtex/utils/transform"',
|
|
804
|
+
);
|
|
805
|
+
});
|
|
806
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { generateMigrationPolicyPointerRule } from "./cursor-rules";
|
|
3
|
+
|
|
4
|
+
describe("generateMigrationPolicyPointerRule", () => {
|
|
5
|
+
const body = generateMigrationPolicyPointerRule("acme");
|
|
6
|
+
|
|
7
|
+
it("emits a Cursor MDC rule with alwaysApply: true frontmatter", () => {
|
|
8
|
+
expect(body.startsWith("---\n")).toBe(true);
|
|
9
|
+
expect(body).toContain("alwaysApply: true");
|
|
10
|
+
expect(body).toContain("description:");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("interpolates the site name into the body", () => {
|
|
14
|
+
expect(body).toContain("`acme`");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("links to the canonical rule and plan in decocms/deco-start", () => {
|
|
18
|
+
expect(body).toContain(
|
|
19
|
+
"https://github.com/decocms/deco-start/blob/main/.cursor/rules/migration-tooling-policy.mdc",
|
|
20
|
+
);
|
|
21
|
+
expect(body).toContain(
|
|
22
|
+
"https://github.com/decocms/deco-start/blob/main/MIGRATION_TOOLING_PLAN.md",
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("documents D1–D5 by ID, not just by name", () => {
|
|
27
|
+
expect(body).toContain("**D1**");
|
|
28
|
+
expect(body).toContain("**D2**");
|
|
29
|
+
expect(body).toContain("**D3**");
|
|
30
|
+
expect(body).toContain("**D4**");
|
|
31
|
+
expect(body).toContain("**D5**");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("points at the post-cleanup --fix command rather than restating policy", () => {
|
|
35
|
+
expect(body).toContain("deco-post-cleanup --fix");
|
|
36
|
+
expect(body).toContain("deco-post-cleanup --strict");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("does NOT restate the canonical rule body verbatim (pointer, not a copy)", () => {
|
|
40
|
+
// Length budget: pointer must stay short to discourage drift.
|
|
41
|
+
// The canonical rule in decocms/deco-start is ~110 lines / 4–5 KB;
|
|
42
|
+
// the pointer must be substantially smaller than a copy.
|
|
43
|
+
expect(body.length).toBeLessThan(3000);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("is deterministic — same site name, same output", () => {
|
|
47
|
+
const a = generateMigrationPolicyPointerRule("foo");
|
|
48
|
+
const b = generateMigrationPolicyPointerRule("foo");
|
|
49
|
+
expect(a).toBe(b);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("escapes nothing weird from siteName — siteName is used as a label only", () => {
|
|
53
|
+
// We don't sanitise; we trust the migration script to pass a real
|
|
54
|
+
// package name. But verify nothing surprising happens with hyphens
|
|
55
|
+
// (a common shape, e.g. "casaevideo-storefront").
|
|
56
|
+
const out = generateMigrationPolicyPointerRule("casaevideo-storefront");
|
|
57
|
+
expect(out).toContain("`casaevideo-storefront`");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor rule scaffolding for migrated sites.
|
|
3
|
+
*
|
|
4
|
+
* The canonical migration tooling policy (D1–D5, priorities, process)
|
|
5
|
+
* lives in `decocms/deco-start`. We don't duplicate it into every site
|
|
6
|
+
* — that would drift the moment the canonical changes. Instead the
|
|
7
|
+
* migration scaffolds a tiny pointer rule, marked `alwaysApply: true`,
|
|
8
|
+
* that loads on every agent session inside the migrated site and tells
|
|
9
|
+
* the agent where the real policy lives.
|
|
10
|
+
*
|
|
11
|
+
* Closes Wave 12-H from MIGRATION_TOOLING_PLAN.md.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate the contents of `.cursor/rules/migration-tooling-policy.mdc`
|
|
16
|
+
* for a freshly migrated site. Pure function: `siteName` is the only
|
|
17
|
+
* input that influences the body, and only as a friendly mention.
|
|
18
|
+
*/
|
|
19
|
+
export function generateMigrationPolicyPointerRule(
|
|
20
|
+
siteName: string,
|
|
21
|
+
): string {
|
|
22
|
+
return `---
|
|
23
|
+
description: Pointer to the canonical migration tooling policy (D1–D5, PR-only, etc.). Always loaded.
|
|
24
|
+
alwaysApply: true
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# Migration Tooling Policy — Pointer
|
|
28
|
+
|
|
29
|
+
> This site (\`${siteName}\`) was generated by the \`@decocms/start\`
|
|
30
|
+
> migration script. The canonical policy that governs how the migration
|
|
31
|
+
> tooling, framework (\`@decocms/start\`), and commerce layer
|
|
32
|
+
> (\`@decocms/apps\`) evolve lives **upstream**, not in this repo.
|
|
33
|
+
|
|
34
|
+
## Where to read
|
|
35
|
+
|
|
36
|
+
- **Rule (always-applied) — full text:**
|
|
37
|
+
https://github.com/decocms/deco-start/blob/main/.cursor/rules/migration-tooling-policy.mdc
|
|
38
|
+
- **Plan (living tracker, decisions + waves):**
|
|
39
|
+
https://github.com/decocms/deco-start/blob/main/MIGRATION_TOOLING_PLAN.md
|
|
40
|
+
- **Migration skill (phase playbook):**
|
|
41
|
+
\`@decocms/start/.agents/skills/deco-to-tanstack-migration\`
|
|
42
|
+
|
|
43
|
+
## What you need to know in this site
|
|
44
|
+
|
|
45
|
+
| ID | Decision | What it means here |
|
|
46
|
+
|----|----------|--------------------|
|
|
47
|
+
| **D1** | Force convergence — no fork runtime support | Site customisations live in \`src/apps/local/\` or open a PR to \`@decocms/apps\`. Don't wrap framework/commerce code in soft adapters. |
|
|
48
|
+
| **D2** | Rewrite HTMX on migration | If you find HTMX residue, rewrite to React. Don't bring back \`hx-*\` runtime. |
|
|
49
|
+
| **D3** | Generated stubs throw at runtime | If a \`~/lib/vtex-*\` import comes from a stub that returns \`null\` / \`{}\` / identity-cast, replace it. \`npx -p @decocms/start deco-post-cleanup --fix\` does the safe swaps automatically. |
|
|
50
|
+
| **D4** | Site-local apps by default, promote at 3+ sites | Don't try to upstream a pattern that has only shipped here. Build it twice in different sites first, then PR it to \`@decocms/apps\`. |
|
|
51
|
+
| **D5** | Failed migrations: \`rm -rf\` and re-run | No restart-mode magic. If the migration goes sideways, blow away the working tree and run again. |
|
|
52
|
+
|
|
53
|
+
## How to find issues this rule wants you to fix
|
|
54
|
+
|
|
55
|
+
\`\`\`bash
|
|
56
|
+
npx -p @decocms/start deco-post-cleanup # audit only
|
|
57
|
+
npx -p @decocms/start deco-post-cleanup --fix # auto-fix the safe rules
|
|
58
|
+
npx -p @decocms/start deco-post-cleanup --strict # exit 1 on any finding (CI)
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
## Process
|
|
62
|
+
|
|
63
|
+
- **PR-only.** No direct pushes to \`main\`. Self-merge after green CI is fine.
|
|
64
|
+
- **Conventional commits** (\`feat\`, \`fix\`, \`chore\`, \`docs\`, \`refactor\`, \`test\`, \`perf\`).
|
|
65
|
+
- **CI must be green before merge.**
|
|
66
|
+
- When the canonical rule changes upstream, update this pointer if any
|
|
67
|
+
link or table heading goes stale. Keep it short — body content lives
|
|
68
|
+
upstream.
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
@@ -66,6 +66,43 @@ describe("generateHooks (vtex)", () => {
|
|
|
66
66
|
// Should be well under 20 lines (factory call + re-export + imports).
|
|
67
67
|
expect(lineCount).toBeLessThan(20);
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
it("useUser is the createUseUser factory shim (no signal-stub boilerplate)", () => {
|
|
71
|
+
const code = files["src/hooks/useUser.ts"];
|
|
72
|
+
expect(code).toContain(
|
|
73
|
+
'import { createUseUser } from "@decocms/apps/vtex/hooks/createUseUser"',
|
|
74
|
+
);
|
|
75
|
+
expect(code).toContain('import { invoke } from "~/server/invoke"');
|
|
76
|
+
expect(code).toContain(
|
|
77
|
+
'export type { Person } from "@decocms/apps/vtex/loaders/user"',
|
|
78
|
+
);
|
|
79
|
+
expect(code).toContain("export const { useUser, resetUser } = createUseUser");
|
|
80
|
+
// Must NOT scaffold the legacy signal stub.
|
|
81
|
+
expect(code).not.toContain('signal<User | null>(null)');
|
|
82
|
+
expect(code).not.toContain("export interface User {");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("useWishlist is the createUseWishlist factory shim", () => {
|
|
86
|
+
const code = files["src/hooks/useWishlist.ts"];
|
|
87
|
+
expect(code).toContain(
|
|
88
|
+
'import { createUseWishlist } from "@decocms/apps/vtex/hooks/createUseWishlist"',
|
|
89
|
+
);
|
|
90
|
+
expect(code).toContain('import { invoke } from "~/server/invoke"');
|
|
91
|
+
expect(code).toContain(
|
|
92
|
+
'export type { WishlistItem } from "@decocms/apps/vtex/loaders/wishlist"',
|
|
93
|
+
);
|
|
94
|
+
expect(code).toContain(
|
|
95
|
+
"export const { useWishlist, resetWishlist } = createUseWishlist",
|
|
96
|
+
);
|
|
97
|
+
// Must NOT scaffold the legacy stub with TODO action bodies.
|
|
98
|
+
expect(code).not.toContain("TODO: Implement");
|
|
99
|
+
expect(code).not.toContain("getItem(_productId: string): boolean");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("useUser + useWishlist VTEX shims are each well under 10 lines", () => {
|
|
103
|
+
expect(files["src/hooks/useUser.ts"].split("\n").length).toBeLessThan(10);
|
|
104
|
+
expect(files["src/hooks/useWishlist.ts"].split("\n").length).toBeLessThan(10);
|
|
105
|
+
});
|
|
69
106
|
});
|
|
70
107
|
|
|
71
108
|
describe("generateHooks (non-vtex)", () => {
|
|
@@ -82,4 +119,23 @@ describe("generateHooks (non-vtex)", () => {
|
|
|
82
119
|
// Until a shopify factory exists, non-vtex platforms get the generic stub.
|
|
83
120
|
expect(code).toContain("Cart Hook stub");
|
|
84
121
|
});
|
|
122
|
+
|
|
123
|
+
it("non-vtex platforms keep the legacy signal-based useUser stub", () => {
|
|
124
|
+
const files = generateHooks(makeCtx("custom"));
|
|
125
|
+
const code = files["src/hooks/useUser.ts"];
|
|
126
|
+
// No factory CALL — docstring may mention it as a pointer for VTEX.
|
|
127
|
+
expect(code).not.toContain("createUseUser({");
|
|
128
|
+
expect(code).not.toContain("export const { useUser, resetUser }");
|
|
129
|
+
// Legacy stub shape (signal + User interface).
|
|
130
|
+
expect(code).toContain("signal<User | null>(null)");
|
|
131
|
+
expect(code).toContain("export interface User {");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("non-vtex platforms keep the legacy useWishlist stub", () => {
|
|
135
|
+
const files = generateHooks(makeCtx("custom"));
|
|
136
|
+
const code = files["src/hooks/useWishlist.ts"];
|
|
137
|
+
expect(code).not.toContain("createUseWishlist({");
|
|
138
|
+
expect(code).not.toContain("export const { useWishlist, resetWishlist }");
|
|
139
|
+
expect(code).toContain("TODO: Implement");
|
|
140
|
+
});
|
|
85
141
|
});
|
|
@@ -5,13 +5,14 @@ export function generateHooks(ctx: MigrationContext): Record<string, string> {
|
|
|
5
5
|
|
|
6
6
|
if (ctx.platform === "vtex") {
|
|
7
7
|
files["src/hooks/useCart.ts"] = generateVtexUseCart();
|
|
8
|
+
files["src/hooks/useUser.ts"] = generateVtexUseUser();
|
|
9
|
+
files["src/hooks/useWishlist.ts"] = generateVtexUseWishlist();
|
|
8
10
|
} else {
|
|
9
11
|
files["src/hooks/useCart.ts"] = generateGenericUseCart();
|
|
12
|
+
files["src/hooks/useUser.ts"] = generateGenericUseUser();
|
|
13
|
+
files["src/hooks/useWishlist.ts"] = generateGenericUseWishlist();
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
files["src/hooks/useUser.ts"] = generateUseUser();
|
|
13
|
-
files["src/hooks/useWishlist.ts"] = generateUseWishlist();
|
|
14
|
-
|
|
15
16
|
return files;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -66,9 +67,39 @@ export default useCart;
|
|
|
66
67
|
`;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
// VTEX path — these are five-line factory shims. The heavy lifting
|
|
71
|
+
// (singleton state, listener pattern, async actions, signal-shaped
|
|
72
|
+
// accessors, legacy arg-swap conventions) lives in
|
|
73
|
+
// @decocms/apps/vtex/hooks/createUseUser and createUseWishlist.
|
|
74
|
+
function generateVtexUseUser(): string {
|
|
75
|
+
return `import { createUseUser } from "@decocms/apps/vtex/hooks/createUseUser";
|
|
76
|
+
import { invoke } from "~/server/invoke";
|
|
77
|
+
|
|
78
|
+
export type { Person } from "@decocms/apps/vtex/loaders/user";
|
|
79
|
+
|
|
80
|
+
export const { useUser, resetUser } = createUseUser({ invoke });
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function generateVtexUseWishlist(): string {
|
|
85
|
+
return `import { createUseWishlist } from "@decocms/apps/vtex/hooks/createUseWishlist";
|
|
86
|
+
import { invoke } from "~/server/invoke";
|
|
87
|
+
|
|
88
|
+
export type { WishlistItem } from "@decocms/apps/vtex/loaders/wishlist";
|
|
89
|
+
|
|
90
|
+
export const { useWishlist, resetWishlist } = createUseWishlist({ invoke });
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Non-VTEX fallback — keeps the legacy signal-based stub shape so any
|
|
95
|
+
// generic platform port that already consumes `~/hooks/useUser` keeps
|
|
96
|
+
// type-checking. Sites must wire their own platform-specific impl.
|
|
97
|
+
function generateGenericUseUser(): string {
|
|
70
98
|
return `/**
|
|
71
99
|
* User Hook — wire to invoke.site.loaders for your platform's user API.
|
|
100
|
+
*
|
|
101
|
+
* VTEX sites get a real factory from @decocms/apps/vtex/hooks/createUseUser;
|
|
102
|
+
* see migration template hooks.ts for the canonical five-line shim.
|
|
72
103
|
*/
|
|
73
104
|
import { signal } from "~/sdk/signal";
|
|
74
105
|
|
|
@@ -90,12 +121,12 @@ export default useUser;
|
|
|
90
121
|
`;
|
|
91
122
|
}
|
|
92
123
|
|
|
93
|
-
function
|
|
124
|
+
function generateGenericUseWishlist(): string {
|
|
94
125
|
return `/**
|
|
95
126
|
* Wishlist Hook — wire to invoke.site.loaders/actions for your platform.
|
|
96
127
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
128
|
+
* VTEX sites get a real factory from @decocms/apps/vtex/hooks/createUseWishlist;
|
|
129
|
+
* see migration template hooks.ts for the canonical five-line shim.
|
|
99
130
|
*/
|
|
100
131
|
import { signal } from "~/sdk/signal";
|
|
101
132
|
|
|
@@ -105,7 +136,7 @@ export function useWishlist() {
|
|
|
105
136
|
return {
|
|
106
137
|
loading,
|
|
107
138
|
async addItem(_productId: string, _productGroupId: string) {
|
|
108
|
-
// TODO: Implement
|
|
139
|
+
// TODO: Implement for your platform
|
|
109
140
|
},
|
|
110
141
|
async removeItem(_productId: string) {
|
|
111
142
|
// TODO: Implement
|