@decocms/start 0.19.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.
Files changed (185) hide show
  1. package/.cursor/skills/deco-api-call-dedup/SKILL.md +443 -0
  2. package/.cursor/skills/deco-apps-architecture/SKILL.md +255 -0
  3. package/.cursor/skills/deco-apps-architecture/app-pattern.md +288 -0
  4. package/.cursor/skills/deco-apps-architecture/commerce-types.md +239 -0
  5. package/.cursor/skills/deco-apps-architecture/new-app-guide.md +268 -0
  6. package/.cursor/skills/deco-apps-architecture/scripts-codegen.md +148 -0
  7. package/.cursor/skills/deco-apps-architecture/shared-utils.md +181 -0
  8. package/.cursor/skills/deco-apps-architecture/vtex-deep-structure.md +253 -0
  9. package/.cursor/skills/deco-apps-architecture/website-app.md +169 -0
  10. package/.cursor/skills/deco-apps-vtex-porting/SKILL.md +189 -0
  11. package/.cursor/skills/deco-apps-vtex-porting/adaptation-patterns.md +335 -0
  12. package/.cursor/skills/deco-apps-vtex-porting/commerce-porting.md +155 -0
  13. package/.cursor/skills/deco-apps-vtex-porting/cookie-auth-patterns.md +148 -0
  14. package/.cursor/skills/deco-apps-vtex-porting/structure-map.md +234 -0
  15. package/.cursor/skills/deco-apps-vtex-porting/transform-mapping.md +99 -0
  16. package/.cursor/skills/deco-apps-vtex-porting/website-porting.md +194 -0
  17. package/.cursor/skills/deco-apps-vtex-review/SKILL.md +234 -0
  18. package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +270 -0
  19. package/.cursor/skills/deco-async-rendering-site-guide/SKILL.md +417 -0
  20. package/.cursor/skills/deco-cms-layout-caching/SKILL.md +293 -0
  21. package/.cursor/skills/deco-cms-route-config/SKILL.md +388 -0
  22. package/.cursor/skills/deco-core-architecture/SKILL.md +185 -0
  23. package/.cursor/skills/deco-core-architecture/blocks.md +196 -0
  24. package/.cursor/skills/deco-core-architecture/deco-vs-deco-start.md +191 -0
  25. package/.cursor/skills/deco-core-architecture/engine.md +220 -0
  26. package/.cursor/skills/deco-core-architecture/hooks-components.md +157 -0
  27. package/.cursor/skills/deco-core-architecture/plugins-clients.md +136 -0
  28. package/.cursor/skills/deco-core-architecture/runtime.md +116 -0
  29. package/.cursor/skills/deco-core-architecture/site-usage.md +165 -0
  30. package/.cursor/skills/deco-e2e-testing/SKILL.md +372 -0
  31. package/.cursor/skills/deco-e2e-testing/discovery.md +337 -0
  32. package/.cursor/skills/deco-e2e-testing/scripts/scaffold.sh +81 -0
  33. package/.cursor/skills/deco-e2e-testing/selectors.md +175 -0
  34. package/.cursor/skills/deco-e2e-testing/templates/package.json +18 -0
  35. package/.cursor/skills/deco-e2e-testing/templates/playwright.config.ts +65 -0
  36. package/.cursor/skills/deco-e2e-testing/templates/scripts/baseline.ts +279 -0
  37. package/.cursor/skills/deco-e2e-testing/templates/scripts/run-e2e.ts +194 -0
  38. package/.cursor/skills/deco-e2e-testing/templates/specs/ecommerce-flow.spec.ts +612 -0
  39. package/.cursor/skills/deco-e2e-testing/templates/tsconfig.json +12 -0
  40. package/.cursor/skills/deco-e2e-testing/templates/utils/metrics-collector.ts +918 -0
  41. package/.cursor/skills/deco-e2e-testing/troubleshooting.md +602 -0
  42. package/.cursor/skills/deco-edge-caching/SKILL.md +316 -0
  43. package/.cursor/skills/deco-full-analysis/SKILL.md +898 -0
  44. package/.cursor/skills/deco-full-analysis/checklists/asset-optimization.md +251 -0
  45. package/.cursor/skills/deco-full-analysis/checklists/bug-fix.md +189 -0
  46. package/.cursor/skills/deco-full-analysis/checklists/cache-strategy.md +144 -0
  47. package/.cursor/skills/deco-full-analysis/checklists/dependency-update.md +150 -0
  48. package/.cursor/skills/deco-full-analysis/checklists/hydration-fix.md +191 -0
  49. package/.cursor/skills/deco-full-analysis/checklists/image-optimization.md +180 -0
  50. package/.cursor/skills/deco-full-analysis/checklists/loader-optimization.md +165 -0
  51. package/.cursor/skills/deco-full-analysis/checklists/seo-fix.md +183 -0
  52. package/.cursor/skills/deco-full-analysis/checklists/site-cleanup.md +281 -0
  53. package/.cursor/skills/deco-full-analysis/discovery.md +548 -0
  54. package/.cursor/skills/deco-incident-debugging/SKILL.md +378 -0
  55. package/.cursor/skills/deco-incident-debugging/headless-mode.md +510 -0
  56. package/.cursor/skills/deco-incident-debugging/learnings-index.md +227 -0
  57. package/.cursor/skills/deco-incident-debugging/triage-workflow.md +312 -0
  58. package/.cursor/skills/deco-islands-migration/SKILL.md +251 -0
  59. package/.cursor/skills/deco-loader-n-plus-1-detector/SKILL.md +275 -0
  60. package/.cursor/skills/deco-performance-audit/SKILL.md +530 -0
  61. package/.cursor/skills/deco-performance-audit/tools-reference.md +428 -0
  62. package/.cursor/skills/deco-performance-audit/workflow.md +457 -0
  63. package/.cursor/skills/deco-server-functions-invoke/SKILL.md +92 -0
  64. package/.cursor/skills/deco-server-functions-invoke/architecture.md +166 -0
  65. package/.cursor/skills/deco-server-functions-invoke/generator.md +122 -0
  66. package/.cursor/skills/deco-server-functions-invoke/problem.md +98 -0
  67. package/.cursor/skills/deco-server-functions-invoke/troubleshooting.md +110 -0
  68. package/.cursor/skills/deco-site-deployment/SKILL.md +396 -0
  69. package/.cursor/skills/deco-site-memory-debugging/SKILL.md +121 -0
  70. package/.cursor/skills/deco-site-memory-debugging/cdp-connection.md +222 -0
  71. package/.cursor/skills/deco-site-memory-debugging/memory-analysis.md +362 -0
  72. package/.cursor/skills/deco-site-patterns/SKILL.md +124 -0
  73. package/.cursor/skills/deco-site-patterns/app-composition.md +337 -0
  74. package/.cursor/skills/deco-site-patterns/client-patterns.md +341 -0
  75. package/.cursor/skills/deco-site-patterns/cms-wiring.md +230 -0
  76. package/.cursor/skills/deco-site-patterns/section-patterns.md +340 -0
  77. package/.cursor/skills/deco-site-scaling-tuning/SKILL.md +240 -0
  78. package/.cursor/skills/deco-site-scaling-tuning/analysis-scripts.md +267 -0
  79. package/.cursor/skills/deco-start-architecture/SKILL.md +218 -0
  80. package/.cursor/skills/deco-start-architecture/admin-protocol.md +156 -0
  81. package/.cursor/skills/deco-start-architecture/cms-resolution.md +201 -0
  82. package/.cursor/skills/deco-start-architecture/code-quality.md +158 -0
  83. package/.cursor/skills/deco-start-architecture/gap-analysis.md +129 -0
  84. package/.cursor/skills/deco-start-architecture/sdk-utilities.md +197 -0
  85. package/.cursor/skills/deco-start-architecture/worker-entry-caching.md +154 -0
  86. package/.cursor/skills/deco-startup-analysis/SKILL.md +248 -0
  87. package/.cursor/skills/deco-storefront-test-checklist/SKILL.md +369 -0
  88. package/.cursor/skills/deco-tanstack-hydration-fixes/SKILL.md +468 -0
  89. package/.cursor/skills/deco-tanstack-navigation/SKILL.md +681 -0
  90. package/.cursor/skills/deco-tanstack-search/SKILL.md +411 -0
  91. package/.cursor/skills/deco-tanstack-storefront-patterns/SKILL.md +1013 -0
  92. package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +518 -0
  93. package/.cursor/skills/deco-to-tanstack-migration/references/codemod-commands.md +174 -0
  94. package/.cursor/skills/deco-to-tanstack-migration/references/commerce/README.md +78 -0
  95. package/.cursor/skills/deco-to-tanstack-migration/references/deco-framework/README.md +128 -0
  96. package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +719 -0
  97. package/.cursor/skills/deco-to-tanstack-migration/references/imports/README.md +70 -0
  98. package/.cursor/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +154 -0
  99. package/.cursor/skills/deco-to-tanstack-migration/references/signals/README.md +220 -0
  100. package/.cursor/skills/deco-to-tanstack-migration/references/vite-config/README.md +78 -0
  101. package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +55 -0
  102. package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +110 -0
  103. package/.cursor/skills/deco-to-tanstack-migration/templates/router.md +96 -0
  104. package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +167 -0
  105. package/.cursor/skills/deco-to-tanstack-migration/templates/vite-config.md +122 -0
  106. package/.cursor/skills/deco-to-tanstack-migration/templates/worker-entry.md +67 -0
  107. package/.cursor/skills/deco-typescript-fixes/SKILL.md +178 -0
  108. package/.cursor/skills/deco-typescript-fixes/common-fixes.md +330 -0
  109. package/.cursor/skills/deco-typescript-fixes/strategy.md +148 -0
  110. package/.cursor/skills/deco-variant-selection-perf/SKILL.md +272 -0
  111. package/.cursor/skills/deco-vtex-fetch-cache/SKILL.md +225 -0
  112. package/.cursor/skills/find-skills/SKILL.md +133 -0
  113. package/.cursor/skills/incident-report/SKILL.md +179 -0
  114. package/.cursor/skills/incident-report/references/5-whys.md +75 -0
  115. package/.cursor/skills/incident-report/templates/client-report.md +187 -0
  116. package/.cursor/skills/incident-report/templates/internal-report.md +206 -0
  117. package/.cursor/skills/template-skill/SKILL.md +38 -0
  118. package/.github/workflows/release.yml +32 -0
  119. package/.releaserc.json +25 -0
  120. package/CLAUDE.md +135 -0
  121. package/GAP_ANALYSIS.md +224 -0
  122. package/GAP_ANALYSIS_V2.md +1013 -0
  123. package/biome.json +39 -0
  124. package/knip.json +5 -0
  125. package/package.json +87 -0
  126. package/scripts/generate-blocks.ts +69 -0
  127. package/scripts/generate-invoke.ts +378 -0
  128. package/scripts/generate-schema.ts +657 -0
  129. package/src/admin/cors.ts +29 -0
  130. package/src/admin/decofile.ts +72 -0
  131. package/src/admin/index.ts +24 -0
  132. package/src/admin/invoke.ts +163 -0
  133. package/src/admin/liveControls.ts +29 -0
  134. package/src/admin/meta.ts +70 -0
  135. package/src/admin/render.ts +205 -0
  136. package/src/admin/schema.ts +686 -0
  137. package/src/admin/setup.ts +44 -0
  138. package/src/cms/index.ts +59 -0
  139. package/src/cms/loader.ts +180 -0
  140. package/src/cms/registry.ts +162 -0
  141. package/src/cms/resolve.ts +1005 -0
  142. package/src/cms/sectionLoaders.ts +294 -0
  143. package/src/hooks/DecoPageRenderer.tsx +444 -0
  144. package/src/hooks/LazySection.tsx +109 -0
  145. package/src/hooks/LiveControls.tsx +108 -0
  146. package/src/hooks/SectionErrorFallback.tsx +85 -0
  147. package/src/hooks/index.ts +8 -0
  148. package/src/index.ts +5 -0
  149. package/src/matchers/builtins.ts +184 -0
  150. package/src/matchers/posthog.ts +154 -0
  151. package/src/middleware/decoState.ts +55 -0
  152. package/src/middleware/healthMetrics.ts +131 -0
  153. package/src/middleware/index.ts +80 -0
  154. package/src/middleware/liveness.ts +21 -0
  155. package/src/middleware/observability.ts +205 -0
  156. package/src/routes/adminRoutes.ts +83 -0
  157. package/src/routes/cmsRoute.ts +302 -0
  158. package/src/routes/components.tsx +34 -0
  159. package/src/routes/index.ts +15 -0
  160. package/src/sdk/analytics.ts +72 -0
  161. package/src/sdk/cacheHeaders.ts +268 -0
  162. package/src/sdk/cachedLoader.ts +206 -0
  163. package/src/sdk/clx.ts +3 -0
  164. package/src/sdk/cookie.ts +39 -0
  165. package/src/sdk/createInvoke.ts +57 -0
  166. package/src/sdk/csp.ts +59 -0
  167. package/src/sdk/env.ts +27 -0
  168. package/src/sdk/index.ts +63 -0
  169. package/src/sdk/instrumentedFetch.ts +137 -0
  170. package/src/sdk/invoke.ts +133 -0
  171. package/src/sdk/mergeCacheControl.ts +150 -0
  172. package/src/sdk/redirects.ts +217 -0
  173. package/src/sdk/requestContext.ts +184 -0
  174. package/src/sdk/serverTimings.ts +68 -0
  175. package/src/sdk/signal.ts +41 -0
  176. package/src/sdk/sitemap.ts +143 -0
  177. package/src/sdk/urlUtils.ts +117 -0
  178. package/src/sdk/useDevice.ts +82 -0
  179. package/src/sdk/useId.ts +7 -0
  180. package/src/sdk/useScript.ts +101 -0
  181. package/src/sdk/workerEntry.ts +703 -0
  182. package/src/sdk/wrapCaughtErrors.ts +107 -0
  183. package/src/types/index.ts +39 -0
  184. package/src/types/widgets.ts +13 -0
  185. package/tsconfig.json +13 -0
@@ -0,0 +1,251 @@
1
+ ---
2
+ name: deco-islands-migration
3
+ description: Eliminate the src/islands/ directory when migrating Deco storefronts from Fresh/Preact to TanStack Start/React. Explains why islands don't make sense in the new stack, the problems they cause, how to systematically repoint or move them, and how to fix vanilla-JS DOM patterns that break React hydration. Use when auditing or removing islands from a migrated storefront.
4
+ ---
5
+
6
+ # Deco Islands Migration — From Fresh Islands to React Components
7
+
8
+ ## Why Islands Don't Make Sense in TanStack Start
9
+
10
+ In **Fresh/Preact**, islands were a core architecture concept: the server rendered static HTML and only specific `islands/` components shipped JavaScript to the browser for hydration. Everything outside `islands/` was server-only.
11
+
12
+ In **TanStack Start/React**, there is no islands boundary. React performs **full hydration** of the entire component tree. Every component — whether it lives in `src/components/` or `src/islands/` — is sent to the client and hydrated. The `src/islands/` directory is a **dead artifact** that:
13
+
14
+ 1. **Adds a useless indirection layer** — each island is typically a 3-line wrapper that re-exports a component
15
+ 2. **Confuses the mental model** — developers think islands have special client-side powers
16
+ 3. **Doubles the module graph** — Vite must resolve and bundle both the island wrapper AND the real component
17
+ 4. **Hides the real component path** — makes searching, refactoring, and debugging harder
18
+ 5. **Breaks tree-shaking assumptions** — bundler can't optimize through the extra re-export layer
19
+
20
+ ## What an Island Wrapper Looks Like
21
+
22
+ ```typescript
23
+ // src/islands/AddToCartButton/vtex.tsx (TYPICAL WRAPPER — DELETE THIS)
24
+ import Component from "~/components/product/AddToCartButton/vtex.tsx";
25
+ import type { Props } from "~/components/product/AddToCartButton/vtex.tsx";
26
+
27
+ function Island(props: Props) {
28
+ return <Component {...props} />;
29
+ }
30
+ export default Island;
31
+ ```
32
+
33
+ This does literally nothing. The `<Component {...props} />` call is a pass-through.
34
+
35
+ ## What a Standalone Island Looks Like
36
+
37
+ Some islands are NOT wrappers — they contain real logic and have no equivalent in `src/components/`:
38
+
39
+ ```typescript
40
+ // src/islands/ForgeViewer/ForgeViewer.tsx (STANDALONE — MUST BE MOVED)
41
+ import { useEffect } from "react";
42
+
43
+ export default function ForgeViewerIsland({ urn }) {
44
+ useEffect(() => {
45
+ // Real Autodesk Forge 3D viewer initialization
46
+ getAccessToken().then(({ token }) => {
47
+ Autodesk.Viewing.Initializer(options, () => { /* ... */ });
48
+ });
49
+ }, []);
50
+ return <div id="forgeViewer" />;
51
+ }
52
+ ```
53
+
54
+ These must be **moved** to `src/components/`, not just repointed.
55
+
56
+ ## Migration Strategy
57
+
58
+ ### Phase 1 — Discover and Classify
59
+
60
+ ```bash
61
+ # List all island files
62
+ find src/islands -name '*.tsx' -o -name '*.ts' | sort
63
+
64
+ # Find all imports referencing islands/
65
+ rg 'from ["'"'"'].*islands/' src/ --glob '*.{tsx,ts}' -l
66
+
67
+ # Classify each island:
68
+ # For each file in src/islands/X.tsx, check if it's a wrapper or standalone:
69
+ rg 'import.*from.*components' src/islands/X.tsx
70
+ # If it imports from components/ and re-exports → WRAPPER (repoint)
71
+ # If it has real logic → STANDALONE (move to components/)
72
+ ```
73
+
74
+ ### Phase 2 — Repoint Wrappers
75
+
76
+ For each wrapper island, find every file that imports it and change the import to point directly at the real component.
77
+
78
+ **Pattern:**
79
+ ```
80
+ ~/islands/SliderJS.tsx → ~/components/ui/SliderJS.tsx
81
+ ~/islands/AddToCartButton/vtex → ~/components/product/AddToCartButton/vtex.tsx
82
+ ~/islands/WishlistButton/vtex → ~/components/wishlist/WishlistButton/vtex.tsx
83
+ ~/islands/Header/CartDrawer → ~/components/header/Drawers.tsx (named export)
84
+ ~/islands/Newsletter → ~/components/footer/Newsletter.tsx
85
+ ```
86
+
87
+ **How to find the target:**
88
+ 1. Read the island file
89
+ 2. Look at its `import Component from "~/components/..."` line
90
+ 3. That's your target path
91
+
92
+ **How to find consumers:**
93
+ ```bash
94
+ rg 'from ["'"'"'].*islands/SliderJS' src/ --glob '*.{tsx,ts}'
95
+ ```
96
+
97
+ **Apply with StrReplace** on each consumer file.
98
+
99
+ ### Phase 3 — Move Standalone Islands
100
+
101
+ For islands with real logic:
102
+
103
+ 1. Create the target directory: `mkdir -p src/components/ForgeViewer/`
104
+ 2. Copy the file: `cp src/islands/ForgeViewer/ForgeViewer.tsx src/components/ForgeViewer/ForgeViewer.tsx`
105
+ 3. Update all imports that referenced the old path
106
+ 4. Verify no references remain
107
+
108
+ ### Phase 4 — Delete src/islands/
109
+
110
+ ```bash
111
+ # Final verification — MUST return zero results
112
+ rg 'from ["'"'"'].*islands/' src/ --glob '*.{tsx,ts}'
113
+
114
+ # Delete
115
+ rm -rf src/islands/
116
+ ```
117
+
118
+ ## Common Problems Islands Cause (and Fixes)
119
+
120
+ ### 1. Vanilla JS DOM Manipulation Conflicts with React
121
+
122
+ Many island components were written for Fresh where they needed vanilla JS to add interactivity. In React, this creates **hydration mismatches** and **broken event handlers**.
123
+
124
+ **Problem: `document.querySelector` to control UI state**
125
+ ```typescript
126
+ // BAD — bypasses React's state management
127
+ const cartCheckbox = document.querySelector('.drawer-end .drawer-toggle') as HTMLInputElement;
128
+ if (cartCheckbox) cartCheckbox.checked = true;
129
+ ```
130
+
131
+ **Fix: Use React state/signals**
132
+ ```typescript
133
+ // GOOD — let React manage the DOM
134
+ const { displayCart } = useUI();
135
+ displayCart.value = true;
136
+ ```
137
+
138
+ ### 2. `addEventListener` Without `window.` Prefix
139
+
140
+ In Fresh/Deno, bare `addEventListener` works at module scope. In React components rendered via SSR, it can fail or attach to the wrong scope.
141
+
142
+ **Problem:**
143
+ ```typescript
144
+ addEventListener("keydown", handler); // ambiguous scope
145
+ removeEventListener("keydown", handler); // might not match
146
+ ```
147
+
148
+ **Fix:**
149
+ ```typescript
150
+ window.addEventListener("keydown", handler);
151
+ // cleanup in useEffect return:
152
+ return () => window.removeEventListener("keydown", handler);
153
+ ```
154
+
155
+ ### 3. `removeEventListener` With New Function References
156
+
157
+ **Problem: Memory leak — listener never removed**
158
+ ```typescript
159
+ // BAD — anonymous function creates new reference each time
160
+ dots?.item(i).addEventListener("click", () => goToItem(i));
161
+ // Later...
162
+ dots?.item(i).removeEventListener("click", () => goToItem(i)); // DIFFERENT function!
163
+ ```
164
+
165
+ **Fix: Store handler references**
166
+ ```typescript
167
+ const dotHandlers: Array<() => void> = [];
168
+ for (let i = 0; i < (dots?.length ?? 0); i++) {
169
+ const handler = () => goToItem(i);
170
+ dotHandlers.push(handler);
171
+ dots?.item(i).addEventListener("click", handler);
172
+ }
173
+
174
+ // Cleanup
175
+ return () => {
176
+ for (let i = 0; i < dotHandlers.length; i++) {
177
+ dots?.item(i).removeEventListener("click", dotHandlers[i]);
178
+ }
179
+ };
180
+ ```
181
+
182
+ ### 4. Inline Scripts Without Cleanup
183
+
184
+ Components using `useScriptAsDataURI` or `dangerouslySetInnerHTML` to inject scripts won't have React lifecycle cleanup. For analytics/tracking this is acceptable (fire-and-forget). For interactive UI, convert to React hooks:
185
+
186
+ **Before (inline script):**
187
+ ```typescript
188
+ <script dangerouslySetInnerHTML={{
189
+ __html: `document.getElementById('${id}').addEventListener('click', ...)`
190
+ }} />
191
+ ```
192
+
193
+ **After (React hook):**
194
+ ```typescript
195
+ useEffect(() => {
196
+ const el = document.getElementById(id);
197
+ const handler = () => { /* ... */ };
198
+ el?.addEventListener('click', handler);
199
+ return () => el?.removeEventListener('click', handler);
200
+ }, [id]);
201
+ ```
202
+
203
+ ### 5. SVG Attributes Not Camel-Cased
204
+
205
+ Fresh/Preact accepted HTML-style SVG attributes. React requires camelCase:
206
+
207
+ | Fresh/Preact | React |
208
+ |-------------|-------|
209
+ | `stroke-linecap` | `strokeLinecap` |
210
+ | `stroke-linejoin` | `strokeLinejoin` |
211
+ | `stroke-width` | `strokeWidth` |
212
+ | `fill-rule` | `fillRule` |
213
+ | `clip-path` | `clipPath` |
214
+
215
+ ### 6. `class` vs `className`
216
+
217
+ Preact accepts both `class` and `className`. React only accepts `className`. Some components accept a `class` prop — rename to `className` or support both:
218
+
219
+ ```typescript
220
+ interface Props {
221
+ class?: string;
222
+ className?: string;
223
+ }
224
+ function Drawer({ class: classProp = "", className = "" }: Props) {
225
+ const cls = classProp || className;
226
+ return <div className={cls}>...</div>;
227
+ }
228
+ ```
229
+
230
+ ## Checklist for Complete Removal
231
+
232
+ - [ ] Zero results from `rg 'from.*islands/' src/ --glob '*.{tsx,ts}'`
233
+ - [ ] `src/islands/` directory deleted
234
+ - [ ] All `addEventListener` calls use explicit `window.` prefix
235
+ - [ ] All `removeEventListener` calls use stored function references
236
+ - [ ] No `document.querySelector` for state that React should manage
237
+ - [ ] SVG attributes are camelCased
238
+ - [ ] No `class` prop on native DOM elements (use `className`)
239
+ - [ ] Build succeeds (`npm run build` / `bun run build`)
240
+ - [ ] Dev server starts without errors
241
+ - [ ] Interactive elements work: add to cart, sliders, drawers, modals
242
+
243
+ ## Related Skills
244
+
245
+ | Skill | Purpose |
246
+ |-------|---------|
247
+ | `deco-to-tanstack-migration` | Full migration playbook (imports, signals, architecture) |
248
+ | `deco-tanstack-navigation` | SPA navigation patterns (`<a>` → `<Link>`, `useNavigate`, `loaderDeps`, forms) |
249
+ | `deco-tanstack-storefront-patterns` | Runtime fixes post-migration (nested sections, caching, SliderJS, async_hooks, cart, server functions) |
250
+ | `deco-storefront-test-checklist` | Context-aware QA checklist generation |
251
+ | `deco-typescript-fixes` | TypeScript error patterns and fixes |
@@ -0,0 +1,275 @@
1
+ ---
2
+ name: deco-loader-n-plus-1-detector
3
+ description: Detect and fix N+1 API call patterns in Deco storefront section loaders. Finds loops calling individual VTEX/Shopify APIs per product instead of batching or using already-available data. Use when investigating slow page loads, high API latency, rate limiting (429s), or when optimizing SSR performance on Deco sites (Fresh or TanStack Start).
4
+ ---
5
+
6
+ # Deco Loader N+1 Detector
7
+
8
+ Finds N+1 API call anti-patterns in Deco storefront section loaders — the #1 cause of slow SSR on e-commerce sites.
9
+
10
+ ## When to Use
11
+
12
+ - Page loads are slow (SSR > 3s)
13
+ - Terminal logs show many sequential/parallel API calls for the same endpoint
14
+ - VTEX returns 429 (Too Many Requests) errors
15
+ - User reports "a troca de pagina ta demorando"
16
+ - After migrating loaders or adding new shelf/search sections
17
+
18
+ ## What It Finds
19
+
20
+ | Pattern | Severity | Example |
21
+ |---------|----------|---------|
22
+ | **API call inside `.map()`** | Critical | `products.map(p => getSpec(p.id))` |
23
+ | **Missing batch alternative** | High | Individual calls where batch API exists |
24
+ | **Redundant data fetch** | High | Fetching data already in the Product object |
25
+ | **Sequential awaits in loop** | Medium | `for (p of products) { await fetch(p) }` |
26
+ | **Unbounded parallel calls** | Medium | `Promise.all(100items.map(fetch))` |
27
+
28
+ ## Workflow
29
+
30
+ ```
31
+ 1. Scan loaders → Find .map() + await + API call patterns
32
+ 2. Identify the API → Catalog, IS, simulation, masterdata
33
+ 3. Check if data is already available → Product.additionalProperty, offers, etc.
34
+ 4. If redundant → Remove the call, read from existing data
35
+ 5. If needed → Create batch endpoint or add caching
36
+ 6. Verify → Check terminal logs for eliminated calls
37
+ ```
38
+
39
+ ## Step 1: Scan for N+1 Patterns
40
+
41
+ Search for the telltale pattern: an API call inside a `.map()` or `forEach()` within a loader.
42
+
43
+ ### Search Commands
44
+
45
+ ```bash
46
+ # Find all loaders that call external APIs inside map/forEach
47
+ grep -rn "\.map(.*async" src/components/ src/sections/ --include="*.tsx" --include="*.ts" | grep -i "loader\|export const loader"
48
+
49
+ # Find getProductSpecification calls (most common N+1)
50
+ grep -rn "getProductSpecification" src/
51
+
52
+ # Find any VTEX API call inside a map
53
+ grep -rn "vtexFetch\|vtex.*fetch\|catalog_system\|intelligent-search" src/ --include="*.tsx" --include="*.ts"
54
+
55
+ # Find simulation calls per product
56
+ grep -rn "cartSimulation\|usePriceSimulation" src/ --include="*.tsx" --include="*.ts"
57
+ ```
58
+
59
+ ### Red Flag Patterns
60
+
61
+ ```typescript
62
+ // RED FLAG: API call per product in a map
63
+ export const loader = async (props: Props, _req: Request) => {
64
+ const results = props.products?.map(async (product) => {
65
+ const extra = await someApiCall(product.id); // N+1!
66
+ return { ...product, extra };
67
+ });
68
+ return { ...props, results: await Promise.all(results) };
69
+ };
70
+ ```
71
+
72
+ ## Step 2: Classify the API Call
73
+
74
+ | API Endpoint | What It Returns | Already in Product? |
75
+ |--------------|-----------------|---------------------|
76
+ | `/api/catalog_system/pvt/products/{id}/Specification` | Product specs by numeric ID | Yes — `product.isVariantOf.additionalProperty` |
77
+ | `/api/catalog_system/pub/products/crossselling/{id}/*` | Related products | No — but should be 1 call per page, not per product |
78
+ | `/api/checkout/pub/orderForms/simulation` | Price simulation | No — needs CEP, legitimate per-product call |
79
+ | `/api/catalog_system/pub/products/variations/{id}` | SKU variations | Yes — `product.isVariantOf.hasVariant` |
80
+ | `/api/dataentities/{entity}/search` | MasterData docs | No — check if can be batched with `_where=id=1 OR id=2` |
81
+
82
+ ## Step 3: Check If Data Already Exists
83
+
84
+ ### Product Specifications (Most Common N+1)
85
+
86
+ The VTEX Intelligent Search API returns `specificationGroups` which the `@decocms/apps` transform converts to `product.isVariantOf.additionalProperty`.
87
+
88
+ **Catalog API format** (what `getProductSpecification` returns):
89
+ ```json
90
+ [{ "Id": 208, "Name": "Rendimento", "Value": ["4.5"] }]
91
+ ```
92
+
93
+ **Schema.org format** (already in `product.isVariantOf.additionalProperty`):
94
+ ```json
95
+ [{ "name": "Rendimento", "value": "4.5", "propertyID": "groupName", "valueReference": "PROPERTY" }]
96
+ ```
97
+
98
+ To use the existing data, create a bridge helper:
99
+
100
+ ```typescript
101
+ // src/sdk/productSpecs.ts
102
+ import type { Product } from "@decocms/apps/commerce/types";
103
+
104
+ const SPEC_NAME_TO_ID: Record<string, number> = {
105
+ // Map exact IS spec names → legacy numeric IDs used by components
106
+ // IMPORTANT: verify exact names via IS API, some have double spaces
107
+ };
108
+
109
+ export function getSpecsFromProduct(product: Product) {
110
+ const props = product.isVariantOf?.additionalProperty ?? [];
111
+ const specs: Array<{ Id: number; Value: string[] }> = [];
112
+ for (const p of props) {
113
+ if (p.valueReference !== "PROPERTY") continue;
114
+ const id = SPEC_NAME_TO_ID[p.name];
115
+ if (id == null) continue;
116
+ const existing = specs.find((s) => s.Id === id);
117
+ if (existing) existing.Value.push(p.value);
118
+ else specs.push({ Id: id, Value: [p.value] });
119
+ }
120
+ return specs;
121
+ }
122
+ ```
123
+
124
+ ### How to Discover Spec Names
125
+
126
+ ```bash
127
+ # Hit the IS API directly and inspect specificationGroups
128
+ curl -s "https://{account}.vtexcommercestable.com.br/api/io/_v/api/intelligent-search/product_search/?count=3&query={product-type}&sc=1" \
129
+ | python3 -c "
130
+ import json, sys
131
+ for p in json.load(sys.stdin).get('products', []):
132
+ print(p['productId'], '-', p['productName'][:60])
133
+ for g in p.get('specificationGroups', []):
134
+ if g['name'] == 'allSpecifications':
135
+ for s in g['specifications']:
136
+ print(f' \"{s[\"name\"]}\": {[v[:40] for v in s[\"values\"]]}')
137
+ print('---')
138
+ "
139
+ ```
140
+
141
+ ### SKU Variations
142
+
143
+ If calling `/api/catalog_system/pub/products/variations/{id}`:
144
+ - Already available in `product.isVariantOf.hasVariant`
145
+ - Each variant has `additionalProperty` with variation attributes
146
+
147
+ ### Product Reviews/Ratings
148
+
149
+ If calling an external review API per product in shelves:
150
+ - Consider lazy-loading reviews only on PDP
151
+ - Or batch the API if it supports multiple product IDs
152
+
153
+ ## Step 4: Fix Strategies
154
+
155
+ ### Strategy A: Use Existing Data (Best)
156
+
157
+ Replace the API call with a synchronous read from the Product object.
158
+
159
+ **Before** (N HTTP calls):
160
+ ```typescript
161
+ const productAdditional = await getProductSpecification(element.inProductGroupWithID);
162
+ ```
163
+
164
+ **After** (0 HTTP calls):
165
+ ```typescript
166
+ const productAdditional = getSpecsFromProduct(element);
167
+ ```
168
+
169
+ ### Strategy B: Create Batch Endpoint
170
+
171
+ When the data genuinely doesn't exist in the Product:
172
+
173
+ ```typescript
174
+ // apps-start/vtex/loaders/catalog.ts
175
+ export async function getProductSpecifications(productIds: string[]) {
176
+ return Promise.all(
177
+ productIds.map(id => vtexFetch(`/api/catalog_system/pvt/products/${id}/Specification`))
178
+ );
179
+ }
180
+ ```
181
+
182
+ Even `Promise.all` with N calls is better than sequential awaits, but a true batch API is ideal.
183
+
184
+ ### Strategy C: Cache + Deduplicate
185
+
186
+ For data that changes infrequently:
187
+
188
+ ```typescript
189
+ const specCache = new Map<string, any>();
190
+
191
+ export async function getCachedSpec(productId: string) {
192
+ if (specCache.has(productId)) return specCache.get(productId)!;
193
+ const result = await getProductSpecification(productId);
194
+ specCache.set(productId, result);
195
+ return result;
196
+ }
197
+ ```
198
+
199
+ ### Strategy D: Lazy Load on Client
200
+
201
+ Move enrichment to client-side for non-critical data:
202
+
203
+ ```typescript
204
+ // Component fetches specs only when visible
205
+ const [specs, setSpecs] = useState(null);
206
+ useEffect(() => {
207
+ if (inView) fetchSpecs(productId).then(setSpecs);
208
+ }, [inView]);
209
+ ```
210
+
211
+ ## Step 5: Verify the Fix
212
+
213
+ ### Check Terminal Logs
214
+
215
+ After fixing, the terminal should show **zero** calls to the eliminated endpoint:
216
+
217
+ ```bash
218
+ # Before: dozens of these per page load
219
+ [vtex] GET .../api/catalog_system/pvt/products/123/Specification
220
+ [vtex] GET .../api/catalog_system/pvt/products/456/Specification
221
+ # ... 20+ more
222
+
223
+ # After: none of these, only intelligent-search calls
224
+ [vtex] GET .../api/io/_v/api/intelligent-search/product_search/...
225
+ ```
226
+
227
+ ### Measure Response Time
228
+
229
+ ```bash
230
+ # Cold start
231
+ curl -s -o /dev/null -w "%{http_code} %{time_total}s" http://localhost:5173/
232
+
233
+ # Warm request
234
+ curl -s -o /dev/null -w "%{http_code} %{time_total}s" http://localhost:5173/
235
+ ```
236
+
237
+ Expected improvement: 2-15 seconds faster on pages with multiple shelves.
238
+
239
+ ## Common N+1 Locations in Deco Sites
240
+
241
+ | Component | File Pattern | Typical N+1 |
242
+ |-----------|-------------|-------------|
243
+ | ProductShelf | `components/product/ProductShelf.tsx` | `getProductSpecification` per product |
244
+ | SearchResult | `components/search/SearchResult.tsx` | `getProductSpecification` per product |
245
+ | ProductTabbedShelf | `components/product/ProductTabbedShelf/` | Specs per product per tab |
246
+ | BuyTogether | `components/product/BuyTogether/` | Cross-selling + specs per suggestion |
247
+ | HouseCatalog | `components/search/HouseCatalog/` | Specs + simulation per product |
248
+ | ProductShelfDinamica | `components/product/ProductShelfDinamica.tsx` | Specs per product in dynamic shelf |
249
+
250
+ ## Quick Audit Checklist
251
+
252
+ - [ ] Search for `getProductSpecification` — replace with `getSpecsFromProduct` in shelf loaders
253
+ - [ ] Search for `.map(async` inside `export const loader` — each is a potential N+1
254
+ - [ ] Check for `usePriceSimulation` in loops — legitimate but verify it's parallelized
255
+ - [ ] Check for `getCrossSelling` in loops — should only be on PDP, not shelves
256
+ - [ ] Verify `Promise.all` wraps parallel calls — not sequential `await` in `for` loop
257
+ - [ ] Check terminal logs for repeated API patterns during page load
258
+ - [ ] Measure SSR time before and after changes
259
+
260
+ ## Impact Reference
261
+
262
+ | Products on Page | N+1 Calls | Latency per Call | Total Added Latency |
263
+ |------------------|-----------|------------------|---------------------|
264
+ | 12 (1 shelf) | 12 | ~370ms | ~4.4s |
265
+ | 24 (PLP) | 24 | ~370ms | ~8.9s |
266
+ | 48 (PLP + 2 shelves) | 48 | ~370ms | ~17.8s |
267
+ | 100 (homepage) | 100 | ~370ms | ~37s |
268
+
269
+ Even with parallelism, VTEX rate limits kick in after ~20 concurrent calls, serializing the rest.
270
+
271
+ ## Related Skills
272
+
273
+ - [deco-performance-audit](../deco-performance-audit/SKILL.md) — CDN-level metrics and cache analysis
274
+ - [deco-full-analysis](../deco-full-analysis/SKILL.md) — Full site architecture analysis
275
+ - [deco-edge-caching](../deco-edge-caching/SKILL.md) — Cache headers and edge configuration