@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.
- package/.cursor/skills/deco-api-call-dedup/SKILL.md +443 -0
- package/.cursor/skills/deco-apps-architecture/SKILL.md +255 -0
- package/.cursor/skills/deco-apps-architecture/app-pattern.md +288 -0
- package/.cursor/skills/deco-apps-architecture/commerce-types.md +239 -0
- package/.cursor/skills/deco-apps-architecture/new-app-guide.md +268 -0
- package/.cursor/skills/deco-apps-architecture/scripts-codegen.md +148 -0
- package/.cursor/skills/deco-apps-architecture/shared-utils.md +181 -0
- package/.cursor/skills/deco-apps-architecture/vtex-deep-structure.md +253 -0
- package/.cursor/skills/deco-apps-architecture/website-app.md +169 -0
- package/.cursor/skills/deco-apps-vtex-porting/SKILL.md +189 -0
- package/.cursor/skills/deco-apps-vtex-porting/adaptation-patterns.md +335 -0
- package/.cursor/skills/deco-apps-vtex-porting/commerce-porting.md +155 -0
- package/.cursor/skills/deco-apps-vtex-porting/cookie-auth-patterns.md +148 -0
- package/.cursor/skills/deco-apps-vtex-porting/structure-map.md +234 -0
- package/.cursor/skills/deco-apps-vtex-porting/transform-mapping.md +99 -0
- package/.cursor/skills/deco-apps-vtex-porting/website-porting.md +194 -0
- package/.cursor/skills/deco-apps-vtex-review/SKILL.md +234 -0
- package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +270 -0
- package/.cursor/skills/deco-async-rendering-site-guide/SKILL.md +417 -0
- package/.cursor/skills/deco-cms-layout-caching/SKILL.md +293 -0
- package/.cursor/skills/deco-cms-route-config/SKILL.md +388 -0
- package/.cursor/skills/deco-core-architecture/SKILL.md +185 -0
- package/.cursor/skills/deco-core-architecture/blocks.md +196 -0
- package/.cursor/skills/deco-core-architecture/deco-vs-deco-start.md +191 -0
- package/.cursor/skills/deco-core-architecture/engine.md +220 -0
- package/.cursor/skills/deco-core-architecture/hooks-components.md +157 -0
- package/.cursor/skills/deco-core-architecture/plugins-clients.md +136 -0
- package/.cursor/skills/deco-core-architecture/runtime.md +116 -0
- package/.cursor/skills/deco-core-architecture/site-usage.md +165 -0
- package/.cursor/skills/deco-e2e-testing/SKILL.md +372 -0
- package/.cursor/skills/deco-e2e-testing/discovery.md +337 -0
- package/.cursor/skills/deco-e2e-testing/scripts/scaffold.sh +81 -0
- package/.cursor/skills/deco-e2e-testing/selectors.md +175 -0
- package/.cursor/skills/deco-e2e-testing/templates/package.json +18 -0
- package/.cursor/skills/deco-e2e-testing/templates/playwright.config.ts +65 -0
- package/.cursor/skills/deco-e2e-testing/templates/scripts/baseline.ts +279 -0
- package/.cursor/skills/deco-e2e-testing/templates/scripts/run-e2e.ts +194 -0
- package/.cursor/skills/deco-e2e-testing/templates/specs/ecommerce-flow.spec.ts +612 -0
- package/.cursor/skills/deco-e2e-testing/templates/tsconfig.json +12 -0
- package/.cursor/skills/deco-e2e-testing/templates/utils/metrics-collector.ts +918 -0
- package/.cursor/skills/deco-e2e-testing/troubleshooting.md +602 -0
- package/.cursor/skills/deco-edge-caching/SKILL.md +316 -0
- package/.cursor/skills/deco-full-analysis/SKILL.md +898 -0
- package/.cursor/skills/deco-full-analysis/checklists/asset-optimization.md +251 -0
- package/.cursor/skills/deco-full-analysis/checklists/bug-fix.md +189 -0
- package/.cursor/skills/deco-full-analysis/checklists/cache-strategy.md +144 -0
- package/.cursor/skills/deco-full-analysis/checklists/dependency-update.md +150 -0
- package/.cursor/skills/deco-full-analysis/checklists/hydration-fix.md +191 -0
- package/.cursor/skills/deco-full-analysis/checklists/image-optimization.md +180 -0
- package/.cursor/skills/deco-full-analysis/checklists/loader-optimization.md +165 -0
- package/.cursor/skills/deco-full-analysis/checklists/seo-fix.md +183 -0
- package/.cursor/skills/deco-full-analysis/checklists/site-cleanup.md +281 -0
- package/.cursor/skills/deco-full-analysis/discovery.md +548 -0
- package/.cursor/skills/deco-incident-debugging/SKILL.md +378 -0
- package/.cursor/skills/deco-incident-debugging/headless-mode.md +510 -0
- package/.cursor/skills/deco-incident-debugging/learnings-index.md +227 -0
- package/.cursor/skills/deco-incident-debugging/triage-workflow.md +312 -0
- package/.cursor/skills/deco-islands-migration/SKILL.md +251 -0
- package/.cursor/skills/deco-loader-n-plus-1-detector/SKILL.md +275 -0
- package/.cursor/skills/deco-performance-audit/SKILL.md +530 -0
- package/.cursor/skills/deco-performance-audit/tools-reference.md +428 -0
- package/.cursor/skills/deco-performance-audit/workflow.md +457 -0
- package/.cursor/skills/deco-server-functions-invoke/SKILL.md +92 -0
- package/.cursor/skills/deco-server-functions-invoke/architecture.md +166 -0
- package/.cursor/skills/deco-server-functions-invoke/generator.md +122 -0
- package/.cursor/skills/deco-server-functions-invoke/problem.md +98 -0
- package/.cursor/skills/deco-server-functions-invoke/troubleshooting.md +110 -0
- package/.cursor/skills/deco-site-deployment/SKILL.md +396 -0
- package/.cursor/skills/deco-site-memory-debugging/SKILL.md +121 -0
- package/.cursor/skills/deco-site-memory-debugging/cdp-connection.md +222 -0
- package/.cursor/skills/deco-site-memory-debugging/memory-analysis.md +362 -0
- package/.cursor/skills/deco-site-patterns/SKILL.md +124 -0
- package/.cursor/skills/deco-site-patterns/app-composition.md +337 -0
- package/.cursor/skills/deco-site-patterns/client-patterns.md +341 -0
- package/.cursor/skills/deco-site-patterns/cms-wiring.md +230 -0
- package/.cursor/skills/deco-site-patterns/section-patterns.md +340 -0
- package/.cursor/skills/deco-site-scaling-tuning/SKILL.md +240 -0
- package/.cursor/skills/deco-site-scaling-tuning/analysis-scripts.md +267 -0
- package/.cursor/skills/deco-start-architecture/SKILL.md +218 -0
- package/.cursor/skills/deco-start-architecture/admin-protocol.md +156 -0
- package/.cursor/skills/deco-start-architecture/cms-resolution.md +201 -0
- package/.cursor/skills/deco-start-architecture/code-quality.md +158 -0
- package/.cursor/skills/deco-start-architecture/gap-analysis.md +129 -0
- package/.cursor/skills/deco-start-architecture/sdk-utilities.md +197 -0
- package/.cursor/skills/deco-start-architecture/worker-entry-caching.md +154 -0
- package/.cursor/skills/deco-startup-analysis/SKILL.md +248 -0
- package/.cursor/skills/deco-storefront-test-checklist/SKILL.md +369 -0
- package/.cursor/skills/deco-tanstack-hydration-fixes/SKILL.md +468 -0
- package/.cursor/skills/deco-tanstack-navigation/SKILL.md +681 -0
- package/.cursor/skills/deco-tanstack-search/SKILL.md +411 -0
- package/.cursor/skills/deco-tanstack-storefront-patterns/SKILL.md +1013 -0
- package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +518 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/codemod-commands.md +174 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/commerce/README.md +78 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/deco-framework/README.md +128 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +719 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/imports/README.md +70 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +154 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/signals/README.md +220 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/vite-config/README.md +78 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +55 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +110 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/router.md +96 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +167 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/vite-config.md +122 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/worker-entry.md +67 -0
- package/.cursor/skills/deco-typescript-fixes/SKILL.md +178 -0
- package/.cursor/skills/deco-typescript-fixes/common-fixes.md +330 -0
- package/.cursor/skills/deco-typescript-fixes/strategy.md +148 -0
- package/.cursor/skills/deco-variant-selection-perf/SKILL.md +272 -0
- package/.cursor/skills/deco-vtex-fetch-cache/SKILL.md +225 -0
- package/.cursor/skills/find-skills/SKILL.md +133 -0
- package/.cursor/skills/incident-report/SKILL.md +179 -0
- package/.cursor/skills/incident-report/references/5-whys.md +75 -0
- package/.cursor/skills/incident-report/templates/client-report.md +187 -0
- package/.cursor/skills/incident-report/templates/internal-report.md +206 -0
- package/.cursor/skills/template-skill/SKILL.md +38 -0
- package/.github/workflows/release.yml +32 -0
- package/.releaserc.json +25 -0
- package/CLAUDE.md +135 -0
- package/GAP_ANALYSIS.md +224 -0
- package/GAP_ANALYSIS_V2.md +1013 -0
- package/biome.json +39 -0
- package/knip.json +5 -0
- package/package.json +87 -0
- package/scripts/generate-blocks.ts +69 -0
- package/scripts/generate-invoke.ts +378 -0
- package/scripts/generate-schema.ts +657 -0
- package/src/admin/cors.ts +29 -0
- package/src/admin/decofile.ts +72 -0
- package/src/admin/index.ts +24 -0
- package/src/admin/invoke.ts +163 -0
- package/src/admin/liveControls.ts +29 -0
- package/src/admin/meta.ts +70 -0
- package/src/admin/render.ts +205 -0
- package/src/admin/schema.ts +686 -0
- package/src/admin/setup.ts +44 -0
- package/src/cms/index.ts +59 -0
- package/src/cms/loader.ts +180 -0
- package/src/cms/registry.ts +162 -0
- package/src/cms/resolve.ts +1005 -0
- package/src/cms/sectionLoaders.ts +294 -0
- package/src/hooks/DecoPageRenderer.tsx +444 -0
- package/src/hooks/LazySection.tsx +109 -0
- package/src/hooks/LiveControls.tsx +108 -0
- package/src/hooks/SectionErrorFallback.tsx +85 -0
- package/src/hooks/index.ts +8 -0
- package/src/index.ts +5 -0
- package/src/matchers/builtins.ts +184 -0
- package/src/matchers/posthog.ts +154 -0
- package/src/middleware/decoState.ts +55 -0
- package/src/middleware/healthMetrics.ts +131 -0
- package/src/middleware/index.ts +80 -0
- package/src/middleware/liveness.ts +21 -0
- package/src/middleware/observability.ts +205 -0
- package/src/routes/adminRoutes.ts +83 -0
- package/src/routes/cmsRoute.ts +302 -0
- package/src/routes/components.tsx +34 -0
- package/src/routes/index.ts +15 -0
- package/src/sdk/analytics.ts +72 -0
- package/src/sdk/cacheHeaders.ts +268 -0
- package/src/sdk/cachedLoader.ts +206 -0
- package/src/sdk/clx.ts +3 -0
- package/src/sdk/cookie.ts +39 -0
- package/src/sdk/createInvoke.ts +57 -0
- package/src/sdk/csp.ts +59 -0
- package/src/sdk/env.ts +27 -0
- package/src/sdk/index.ts +63 -0
- package/src/sdk/instrumentedFetch.ts +137 -0
- package/src/sdk/invoke.ts +133 -0
- package/src/sdk/mergeCacheControl.ts +150 -0
- package/src/sdk/redirects.ts +217 -0
- package/src/sdk/requestContext.ts +184 -0
- package/src/sdk/serverTimings.ts +68 -0
- package/src/sdk/signal.ts +41 -0
- package/src/sdk/sitemap.ts +143 -0
- package/src/sdk/urlUtils.ts +117 -0
- package/src/sdk/useDevice.ts +82 -0
- package/src/sdk/useId.ts +7 -0
- package/src/sdk/useScript.ts +101 -0
- package/src/sdk/workerEntry.ts +703 -0
- package/src/sdk/wrapCaughtErrors.ts +107 -0
- package/src/types/index.ts +39 -0
- package/src/types/widgets.ts +13 -0
- 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
|