@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,217 @@
1
+ /**
2
+ * CMS-managed redirect system.
3
+ *
4
+ * Loads redirect definitions from .deco/blocks/ and provides
5
+ * fast path matching for use in TanStack Start middleware.
6
+ *
7
+ * Supports:
8
+ * - Exact path matches (/old-page -> /new-page)
9
+ * - Glob patterns (/old/* -> /new/*)
10
+ * - Permanent (301) and temporary (302) redirects
11
+ * - CSV import for bulk redirects
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * // In TanStack Start middleware
16
+ * import { loadRedirects, matchRedirect } from "@decocms/start/sdk/redirects";
17
+ * import { loadBlocks } from "@decocms/start/cms";
18
+ *
19
+ * const redirects = loadRedirects(loadBlocks());
20
+ *
21
+ * const middleware = createMiddleware().server(async ({ next, request }) => {
22
+ * const url = new URL(request.url);
23
+ * const redirect = matchRedirect(url.pathname, redirects);
24
+ * if (redirect) {
25
+ * return new Response(null, {
26
+ * status: redirect.status,
27
+ * headers: { Location: redirect.to },
28
+ * });
29
+ * }
30
+ * return next();
31
+ * });
32
+ * ```
33
+ */
34
+
35
+ // -------------------------------------------------------------------------
36
+ // Types
37
+ // -------------------------------------------------------------------------
38
+
39
+ export interface Redirect {
40
+ from: string;
41
+ to: string;
42
+ status: 301 | 302;
43
+ }
44
+
45
+ export interface RedirectMap {
46
+ /** Exact match redirects for O(1) lookup. */
47
+ exact: Map<string, Redirect>;
48
+ /** Glob/prefix redirects checked sequentially (few in practice). */
49
+ patterns: Array<{ prefix: string; redirect: Redirect }>;
50
+ }
51
+
52
+ // -------------------------------------------------------------------------
53
+ // Loading from CMS blocks
54
+ // -------------------------------------------------------------------------
55
+
56
+ interface BlockRedirectEntry {
57
+ from: string;
58
+ to: string;
59
+ type?: "permanent" | "temporary";
60
+ }
61
+
62
+ const REDIRECT_RESOLVE_TYPES = new Set([
63
+ "website/loaders/redirect.ts",
64
+ "website/loaders/redirects.ts",
65
+ "website/loaders/redirectsFromCsv.ts",
66
+ "deco-sites/std/loaders/x/redirects.ts",
67
+ ]);
68
+
69
+ /**
70
+ * Load all redirect definitions from CMS blocks.
71
+ *
72
+ * Scans the blocks for known redirect resolve types and builds
73
+ * a fast-lookup redirect map.
74
+ */
75
+ export function loadRedirects(blocks: Record<string, unknown>): RedirectMap {
76
+ const exact = new Map<string, Redirect>();
77
+ const patterns: Array<{ prefix: string; redirect: Redirect }> = [];
78
+
79
+ for (const [_key, block] of Object.entries(blocks)) {
80
+ if (!block || typeof block !== "object") continue;
81
+ const obj = block as Record<string, unknown>;
82
+ const resolveType = obj.__resolveType as string | undefined;
83
+
84
+ if (!resolveType || !REDIRECT_RESOLVE_TYPES.has(resolveType)) continue;
85
+
86
+ const entries = (obj.redirects ?? obj.redirect) as
87
+ | BlockRedirectEntry[]
88
+ | BlockRedirectEntry
89
+ | undefined;
90
+
91
+ if (!entries) continue;
92
+
93
+ const list = Array.isArray(entries) ? entries : [entries];
94
+
95
+ for (const entry of list) {
96
+ if (!entry.from || !entry.to) continue;
97
+
98
+ const redirect: Redirect = {
99
+ from: normalizePath(entry.from),
100
+ to: entry.to,
101
+ status: entry.type === "permanent" ? 301 : 302,
102
+ };
103
+
104
+ if (redirect.from.includes("*")) {
105
+ const prefix = redirect.from.replace(/\*+$/, "");
106
+ patterns.push({ prefix, redirect });
107
+ } else {
108
+ exact.set(redirect.from, redirect);
109
+ }
110
+ }
111
+ }
112
+
113
+ return { exact, patterns };
114
+ }
115
+
116
+ // -------------------------------------------------------------------------
117
+ // CSV import
118
+ // -------------------------------------------------------------------------
119
+
120
+ /**
121
+ * Parse a CSV string into redirect entries.
122
+ *
123
+ * Expected format: `from,to[,type]` (one per line).
124
+ * Lines starting with # are comments. Empty lines are skipped.
125
+ * Type is "permanent" (301) or "temporary" (302, default).
126
+ */
127
+ export function parseRedirectsCsv(csv: string): Redirect[] {
128
+ const redirects: Redirect[] = [];
129
+ const lines = csv.split("\n");
130
+
131
+ for (const raw of lines) {
132
+ const line = raw.trim();
133
+ if (!line || line.startsWith("#")) continue;
134
+
135
+ const parts = line.split(",").map((p) => p.trim());
136
+ if (parts.length < 2) continue;
137
+
138
+ const [from, to, type] = parts;
139
+ if (!from || !to) continue;
140
+
141
+ redirects.push({
142
+ from: normalizePath(from),
143
+ to,
144
+ status: type === "permanent" || type === "301" ? 301 : 302,
145
+ });
146
+ }
147
+
148
+ return redirects;
149
+ }
150
+
151
+ /**
152
+ * Add parsed redirects to an existing redirect map.
153
+ */
154
+ export function addRedirects(map: RedirectMap, redirects: Redirect[]): void {
155
+ for (const redirect of redirects) {
156
+ if (redirect.from.includes("*")) {
157
+ const prefix = redirect.from.replace(/\*+$/, "");
158
+ map.patterns.push({ prefix, redirect });
159
+ } else {
160
+ map.exact.set(redirect.from, redirect);
161
+ }
162
+ }
163
+ }
164
+
165
+ // -------------------------------------------------------------------------
166
+ // Matching
167
+ // -------------------------------------------------------------------------
168
+
169
+ /**
170
+ * Find a redirect matching the given path.
171
+ *
172
+ * Checks exact matches first (O(1)), then glob patterns (O(n), but
173
+ * typically few patterns exist).
174
+ */
175
+ export function matchRedirect(pathname: string, map: RedirectMap): Redirect | null {
176
+ const normalized = normalizePath(pathname);
177
+
178
+ const exactMatch = map.exact.get(normalized);
179
+ if (exactMatch) return exactMatch;
180
+
181
+ for (const { prefix, redirect } of map.patterns) {
182
+ if (normalized.startsWith(prefix)) {
183
+ const suffix = normalized.slice(prefix.length);
184
+ const to = redirect.to.includes("*") ? redirect.to.replace("*", suffix) : redirect.to;
185
+ return { ...redirect, to };
186
+ }
187
+ }
188
+
189
+ return null;
190
+ }
191
+
192
+ // -------------------------------------------------------------------------
193
+ // Helpers
194
+ // -------------------------------------------------------------------------
195
+
196
+ function normalizePath(path: string): string {
197
+ let p = path.trim();
198
+
199
+ // If the "from" is a full URL, extract just the pathname
200
+ if (p.startsWith("http://") || p.startsWith("https://")) {
201
+ try {
202
+ p = new URL(p).pathname;
203
+ } catch {
204
+ // malformed URL, keep as-is and try the prefix fallback
205
+ const slashIdx = p.indexOf("/", p.indexOf("//") + 2);
206
+ p = slashIdx >= 0 ? p.slice(slashIdx) : p;
207
+ }
208
+ }
209
+
210
+ if (!p.startsWith("/")) {
211
+ p = "/" + p;
212
+ }
213
+ if (p.length > 1 && p.endsWith("/")) {
214
+ p = p.slice(0, -1);
215
+ }
216
+ return p.toLowerCase();
217
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Per-request context via AsyncLocalStorage.
3
+ *
4
+ * Binds request-scoped state (request, abort signal, device info, flags)
5
+ * that any code in the call stack can access without prop drilling.
6
+ *
7
+ * Requires `nodejs_compat` in wrangler.jsonc (already enabled).
8
+ *
9
+ * **Design decisions:**
10
+ * - We do NOT monkey-patch global `fetch`. Instead, `RequestContext.fetch`
11
+ * provides a fetch that auto-injects the request's AbortSignal.
12
+ * - The context is optional -- code that doesn't need it just doesn't call it.
13
+ * Commerce loaders receive it explicitly via the updated `CommerceLoader` sig.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * // In TanStack Start middleware:
18
+ * import { RequestContext } from "@decocms/start/sdk/requestContext";
19
+ *
20
+ * const middleware = createMiddleware().server(async ({ next, request }) => {
21
+ * return RequestContext.run(request, () => next());
22
+ * });
23
+ *
24
+ * // Anywhere in the call stack:
25
+ * const req = RequestContext.request; // the current request
26
+ * const signal = RequestContext.signal; // AbortSignal
27
+ * const resp = await RequestContext.fetch(url); // auto-aborts on disconnect
28
+ * ```
29
+ */
30
+
31
+ import { AsyncLocalStorage } from "node:async_hooks";
32
+
33
+ // -------------------------------------------------------------------------
34
+ // Types
35
+ // -------------------------------------------------------------------------
36
+
37
+ export interface RequestContextData {
38
+ request: Request;
39
+ signal: AbortSignal;
40
+ startedAt: number;
41
+ /** Lazily computed device type. */
42
+ _device?: "mobile" | "desktop";
43
+ /** Lazily computed bot detection result. */
44
+ _isBot?: boolean;
45
+ /** Arbitrary bag for middleware to attach custom data. */
46
+ bag: Map<string, unknown>;
47
+ }
48
+
49
+ // -------------------------------------------------------------------------
50
+ // Storage
51
+ // -------------------------------------------------------------------------
52
+
53
+ const storage = new AsyncLocalStorage<RequestContextData>();
54
+
55
+ const MOBILE_RE = /mobile|android|iphone|ipad|ipod|webos|blackberry|opera mini|iemobile/i;
56
+ const BOT_RE =
57
+ /bot|crawl|spider|slurp|bingpreview|facebookexternalhit|linkedinbot|twitterbot|whatsapp|telegram|googlebot|yandex|baidu|duckduck/i;
58
+
59
+ // -------------------------------------------------------------------------
60
+ // Public API
61
+ // -------------------------------------------------------------------------
62
+
63
+ export const RequestContext = {
64
+ /**
65
+ * Run a function within a request context.
66
+ *
67
+ * Call this at the outermost middleware level. Everything inside
68
+ * the callback (loaders, resolvers, utilities) can access the
69
+ * context via the static getters.
70
+ */
71
+ run<T>(request: Request, fn: () => T): T {
72
+ const controller = new AbortController();
73
+
74
+ if (request.signal) {
75
+ if (request.signal.aborted) {
76
+ controller.abort(request.signal.reason);
77
+ } else {
78
+ request.signal.addEventListener("abort", () => controller.abort(request.signal.reason), {
79
+ once: true,
80
+ });
81
+ }
82
+ }
83
+
84
+ const ctx: RequestContextData = {
85
+ request,
86
+ signal: controller.signal,
87
+ startedAt: Date.now(),
88
+ bag: new Map(),
89
+ };
90
+
91
+ return storage.run(ctx, fn);
92
+ },
93
+
94
+ /**
95
+ * Get the current request context, or null if not in a request scope.
96
+ */
97
+ get current(): RequestContextData | null {
98
+ return storage.getStore() ?? null;
99
+ },
100
+
101
+ /**
102
+ * Get the current Request object.
103
+ * @throws if called outside a request context
104
+ */
105
+ get request(): Request {
106
+ const ctx = storage.getStore();
107
+ if (!ctx) throw new Error("RequestContext.request accessed outside a request scope");
108
+ return ctx.request;
109
+ },
110
+
111
+ /**
112
+ * Get the current AbortSignal.
113
+ * Use this to cancel in-flight operations when the client disconnects.
114
+ */
115
+ get signal(): AbortSignal {
116
+ const ctx = storage.getStore();
117
+ if (!ctx) throw new Error("RequestContext.signal accessed outside a request scope");
118
+ return ctx.signal;
119
+ },
120
+
121
+ /**
122
+ * Detected device type based on User-Agent.
123
+ */
124
+ get device(): "mobile" | "desktop" {
125
+ const ctx = storage.getStore();
126
+ if (!ctx) return "desktop";
127
+ if (ctx._device) return ctx._device;
128
+ const ua = ctx.request.headers.get("user-agent") ?? "";
129
+ ctx._device = MOBILE_RE.test(ua) ? "mobile" : "desktop";
130
+ return ctx._device;
131
+ },
132
+
133
+ /**
134
+ * Whether the request appears to be from a bot/crawler.
135
+ */
136
+ get isBot(): boolean {
137
+ const ctx = storage.getStore();
138
+ if (!ctx) return false;
139
+ if (ctx._isBot !== undefined) return ctx._isBot;
140
+ const ua = ctx.request.headers.get("user-agent") ?? "";
141
+ ctx._isBot = BOT_RE.test(ua);
142
+ return ctx._isBot;
143
+ },
144
+
145
+ /**
146
+ * Elapsed time since the request started (in milliseconds).
147
+ */
148
+ get elapsed(): number {
149
+ const ctx = storage.getStore();
150
+ if (!ctx) return 0;
151
+ return Date.now() - ctx.startedAt;
152
+ },
153
+
154
+ /**
155
+ * Fetch with automatic AbortSignal injection.
156
+ *
157
+ * When the client disconnects, this fetch aborts automatically.
158
+ * This is NOT a global monkey-patch -- only code that explicitly
159
+ * calls `RequestContext.fetch()` gets this behavior.
160
+ */
161
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
162
+ const ctx = storage.getStore();
163
+ if (!ctx) return globalThis.fetch(input, init);
164
+
165
+ return globalThis.fetch(input, {
166
+ ...init,
167
+ signal: init?.signal ?? ctx.signal,
168
+ });
169
+ },
170
+
171
+ /**
172
+ * Get/set arbitrary values in the request bag.
173
+ * Useful for middleware to pass data to loaders.
174
+ */
175
+ getBag<T>(key: string): T | undefined {
176
+ const ctx = storage.getStore();
177
+ return ctx?.bag.get(key) as T | undefined;
178
+ },
179
+
180
+ setBag(key: string, value: unknown): void {
181
+ const ctx = storage.getStore();
182
+ ctx?.bag.set(key, value);
183
+ },
184
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Server-Timing header builder.
3
+ *
4
+ * Tracks named timing entries during a request and serializes them
5
+ * into the `Server-Timing` HTTP header for visibility in DevTools.
6
+ *
7
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const timings = createServerTimings();
12
+ * const end = timings.start("resolve-cms");
13
+ * await resolvePage();
14
+ * end();
15
+ * response.headers.set("Server-Timing", timings.toHeader());
16
+ * ```
17
+ */
18
+ export interface ServerTimings {
19
+ /** Start a named timing. Returns a function to call when done. */
20
+ start(name: string, description?: string): () => void;
21
+ /** Record a completed timing with a known duration. */
22
+ record(name: string, durationMs: number, description?: string): void;
23
+ /** Serialize all timings to a `Server-Timing` header value. */
24
+ toHeader(): string;
25
+ /** Get all recorded entries for diagnostics. */
26
+ entries(): TimingEntry[];
27
+ }
28
+
29
+ export interface TimingEntry {
30
+ name: string;
31
+ durationMs: number;
32
+ description?: string;
33
+ }
34
+
35
+ export function createServerTimings(): ServerTimings {
36
+ const timingEntries: TimingEntry[] = [];
37
+
38
+ return {
39
+ start(name: string, description?: string) {
40
+ const startTime = performance.now();
41
+ return () => {
42
+ const durationMs = performance.now() - startTime;
43
+ timingEntries.push({ name, durationMs, description });
44
+ };
45
+ },
46
+
47
+ record(name: string, durationMs: number, description?: string) {
48
+ timingEntries.push({ name, durationMs, description });
49
+ },
50
+
51
+ toHeader(): string {
52
+ return timingEntries
53
+ .map((entry) => {
54
+ let value = entry.name;
55
+ if (entry.description) {
56
+ value += `;desc="${entry.description}"`;
57
+ }
58
+ value += `;dur=${entry.durationMs.toFixed(1)}`;
59
+ return value;
60
+ })
61
+ .join(", ");
62
+ },
63
+
64
+ entries() {
65
+ return [...timingEntries];
66
+ },
67
+ };
68
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Reactive signal backed by @tanstack/store.
3
+ *
4
+ * Drop-in replacement for the preact signals shim.
5
+ * Preserves the .value getter/setter API so consumers don't need to change.
6
+ *
7
+ * For React components that need to re-render on state changes,
8
+ * use the useStore() hook from @tanstack/react-store:
9
+ *
10
+ * import { useStore } from "@tanstack/react-store";
11
+ * const value = useStore(mySignal.store);
12
+ */
13
+ import { Store } from "@tanstack/store";
14
+
15
+ export interface ReactiveSignal<T> {
16
+ readonly store: Store<T>;
17
+ value: T;
18
+ peek(): T;
19
+ subscribe(fn: () => void): () => void;
20
+ }
21
+
22
+ export function signal<T>(initialValue: T): ReactiveSignal<T> {
23
+ const store = new Store<T>(initialValue);
24
+
25
+ return {
26
+ store,
27
+ get value(): T {
28
+ return store.state;
29
+ },
30
+ set value(v: T) {
31
+ store.setState(() => v);
32
+ },
33
+ peek(): T {
34
+ return store.state;
35
+ },
36
+ subscribe(fn: () => void): () => void {
37
+ const sub = store.subscribe(() => fn());
38
+ return () => sub.unsubscribe();
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Sitemap generation utilities.
3
+ *
4
+ * Provides XML sitemap generation from CMS page blocks and arbitrary
5
+ * URL lists. Designed to be composed with commerce-specific sitemap
6
+ * sources (VTEX, Shopify) in a TanStack Start API route.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * // src/routes/sitemap[.]xml.ts
11
+ * import { createAPIFileRoute } from "@tanstack/react-start/api";
12
+ * import { getCMSSitemapEntries, generateSitemapXml } from "@decocms/start/sdk/sitemap";
13
+ * import { getVtexSitemapEntries } from "@decocms/apps/vtex/utils/sitemap";
14
+ *
15
+ * export const APIRoute = createAPIFileRoute("/sitemap.xml")({
16
+ * GET: async ({ request }) => {
17
+ * const origin = new URL(request.url).origin;
18
+ * const cmsEntries = getCMSSitemapEntries(origin);
19
+ * const vtexEntries = await getVtexSitemapEntries(origin);
20
+ * const xml = generateSitemapXml([...cmsEntries, ...vtexEntries]);
21
+ * return new Response(xml, {
22
+ * headers: {
23
+ * "Content-Type": "application/xml",
24
+ * "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400",
25
+ * },
26
+ * });
27
+ * },
28
+ * });
29
+ * ```
30
+ */
31
+
32
+ import { getAllPages, loadBlocks } from "../cms/loader";
33
+
34
+ // -------------------------------------------------------------------------
35
+ // Types
36
+ // -------------------------------------------------------------------------
37
+
38
+ export interface SitemapEntry {
39
+ loc: string;
40
+ lastmod?: string;
41
+ changefreq?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
42
+ priority?: number;
43
+ }
44
+
45
+ export interface SitemapOptions {
46
+ /** Maximum entries per sitemap (Google limit is 50,000). @default 50000 */
47
+ maxEntries?: number;
48
+ }
49
+
50
+ // -------------------------------------------------------------------------
51
+ // CMS page entries
52
+ // -------------------------------------------------------------------------
53
+
54
+ /**
55
+ * Extract sitemap entries from CMS page blocks.
56
+ *
57
+ * Reads all pages from the block store and generates URLs from their
58
+ * path patterns (excluding wildcard-only patterns like `/*`).
59
+ */
60
+ export function getCMSSitemapEntries(origin: string): SitemapEntry[] {
61
+ const pages = getAllPages();
62
+ const entries: SitemapEntry[] = [];
63
+ const today = new Date().toISOString().split("T")[0];
64
+
65
+ for (const { page } of pages) {
66
+ if (!page.path) continue;
67
+
68
+ if (page.path.includes("*") || page.path.includes(":")) continue;
69
+
70
+ const loc = `${origin}${page.path === "/" ? "" : page.path}`;
71
+ entries.push({
72
+ loc: loc || origin,
73
+ lastmod: today,
74
+ changefreq: page.path === "/" ? "daily" : "weekly",
75
+ priority: page.path === "/" ? 1.0 : 0.7,
76
+ });
77
+ }
78
+
79
+ return entries;
80
+ }
81
+
82
+ // -------------------------------------------------------------------------
83
+ // XML generation
84
+ // -------------------------------------------------------------------------
85
+
86
+ function escapeXml(str: string): string {
87
+ return str
88
+ .replace(/&/g, "&amp;")
89
+ .replace(/</g, "&lt;")
90
+ .replace(/>/g, "&gt;")
91
+ .replace(/"/g, "&quot;")
92
+ .replace(/'/g, "&apos;");
93
+ }
94
+
95
+ /**
96
+ * Generate a sitemap XML string from entries.
97
+ */
98
+ export function generateSitemapXml(entries: SitemapEntry[], options?: SitemapOptions): string {
99
+ const max = options?.maxEntries ?? 50000;
100
+ const limited = entries.slice(0, max);
101
+
102
+ const urls = limited.map((entry) => {
103
+ let url = ` <url>\n <loc>${escapeXml(entry.loc)}</loc>`;
104
+ if (entry.lastmod) url += `\n <lastmod>${entry.lastmod}</lastmod>`;
105
+ if (entry.changefreq) url += `\n <changefreq>${entry.changefreq}</changefreq>`;
106
+ if (entry.priority != null) url += `\n <priority>${entry.priority.toFixed(1)}</priority>`;
107
+ url += "\n </url>";
108
+ return url;
109
+ });
110
+
111
+ return [
112
+ '<?xml version="1.0" encoding="UTF-8"?>',
113
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
114
+ ...urls,
115
+ "</urlset>",
116
+ ].join("\n");
117
+ }
118
+
119
+ /**
120
+ * Generate a sitemap index XML for splitting large sitemaps.
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * const index = generateSitemapIndexXml([
125
+ * `${origin}/sitemap-pages.xml`,
126
+ * `${origin}/sitemap-products.xml`,
127
+ * ]);
128
+ * ```
129
+ */
130
+ export function generateSitemapIndexXml(sitemapUrls: string[]): string {
131
+ const today = new Date().toISOString().split("T")[0];
132
+ const sitemaps = sitemapUrls.map(
133
+ (url) =>
134
+ ` <sitemap>\n <loc>${escapeXml(url)}</loc>\n <lastmod>${today}</lastmod>\n </sitemap>`,
135
+ );
136
+
137
+ return [
138
+ '<?xml version="1.0" encoding="UTF-8"?>',
139
+ '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
140
+ ...sitemaps,
141
+ "</sitemapindex>",
142
+ ].join("\n");
143
+ }