@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,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, "&")
|
|
89
|
+
.replace(/</g, "<")
|
|
90
|
+
.replace(/>/g, ">")
|
|
91
|
+
.replace(/"/g, """)
|
|
92
|
+
.replace(/'/g, "'");
|
|
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
|
+
}
|