@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,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics utilities using the data-event attribute pattern.
|
|
3
|
+
* Compatible with Deco's analytics pipeline and GTM.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface DataEventParams {
|
|
7
|
+
on: "view" | "click" | "change";
|
|
8
|
+
event: { name: string; params?: Record<string, unknown> };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useSendEvent({ on, event }: DataEventParams) {
|
|
12
|
+
return {
|
|
13
|
+
"data-event": encodeURIComponent(JSON.stringify(event)),
|
|
14
|
+
"data-event-trigger": on,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Inline script that observes data-event attributes and dispatches events.
|
|
20
|
+
* Inject once in the root layout via a <script> tag.
|
|
21
|
+
*/
|
|
22
|
+
export const ANALYTICS_SCRIPT = `
|
|
23
|
+
(function() {
|
|
24
|
+
function dispatch(event) {
|
|
25
|
+
if (window.dataLayer) {
|
|
26
|
+
window.dataLayer.push({ event: event.name, ...event.params });
|
|
27
|
+
}
|
|
28
|
+
if (window.DECO && window.DECO.events) {
|
|
29
|
+
window.DECO.events.dispatch(event);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getEvent(el) {
|
|
34
|
+
var raw = el.getAttribute("data-event");
|
|
35
|
+
if (!raw) return null;
|
|
36
|
+
try { return JSON.parse(decodeURIComponent(raw)); } catch(e) { return null; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
var viewObserver = new IntersectionObserver(function(entries) {
|
|
40
|
+
entries.forEach(function(entry) {
|
|
41
|
+
if (entry.isIntersecting) {
|
|
42
|
+
var event = getEvent(entry.target);
|
|
43
|
+
if (event) dispatch(event);
|
|
44
|
+
viewObserver.unobserve(entry.target);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}, { threshold: 0.5 });
|
|
48
|
+
|
|
49
|
+
document.addEventListener("click", function(e) {
|
|
50
|
+
var el = e.target.closest("[data-event-trigger='click']");
|
|
51
|
+
if (el) {
|
|
52
|
+
var event = getEvent(el);
|
|
53
|
+
if (event) dispatch(event);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function observeAll() {
|
|
58
|
+
document.querySelectorAll("[data-event-trigger='view']").forEach(function(el) {
|
|
59
|
+
viewObserver.observe(el);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
observeAll();
|
|
64
|
+
new MutationObserver(observeAll).observe(document.body, { childList: true, subtree: true });
|
|
65
|
+
})();
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
/** Returns a GTM container snippet. Returns empty string if no containerId. */
|
|
69
|
+
export function gtmScript(containerId?: string): string {
|
|
70
|
+
if (!containerId) return "";
|
|
71
|
+
return `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${containerId}');`;
|
|
72
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache-Control header generation for different page types.
|
|
3
|
+
*
|
|
4
|
+
* Produces spec-compliant `Cache-Control` values suitable for CDNs
|
|
5
|
+
* (Cloudflare, Vercel, Fastly) with `s-maxage` and `stale-while-revalidate`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* // In a TanStack Start route:
|
|
10
|
+
* import { cacheHeaders, routeCacheDefaults } from "@decocms/start/sdk/cacheHeaders";
|
|
11
|
+
*
|
|
12
|
+
* export const Route = createFileRoute('/products/$slug')({
|
|
13
|
+
* ...routeCacheDefaults("product"),
|
|
14
|
+
* headers: () => cacheHeaders("product"),
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type CacheProfile =
|
|
20
|
+
| "static"
|
|
21
|
+
| "product"
|
|
22
|
+
| "listing"
|
|
23
|
+
| "search"
|
|
24
|
+
| "cart"
|
|
25
|
+
| "private"
|
|
26
|
+
| "none";
|
|
27
|
+
|
|
28
|
+
export interface CacheHeadersConfig {
|
|
29
|
+
/** Browser max-age in seconds. */
|
|
30
|
+
maxAge: number;
|
|
31
|
+
/** CDN/edge max-age in seconds. */
|
|
32
|
+
sMaxAge: number;
|
|
33
|
+
/** Stale-while-revalidate window in seconds. */
|
|
34
|
+
staleWhileRevalidate: number;
|
|
35
|
+
/** Whether the response is public (cacheable by CDN). */
|
|
36
|
+
isPublic: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const PROFILES: Record<CacheProfile, CacheHeadersConfig> = {
|
|
40
|
+
static: {
|
|
41
|
+
maxAge: 120,
|
|
42
|
+
sMaxAge: 86400,
|
|
43
|
+
staleWhileRevalidate: 86400,
|
|
44
|
+
isPublic: true,
|
|
45
|
+
},
|
|
46
|
+
product: {
|
|
47
|
+
maxAge: 60,
|
|
48
|
+
sMaxAge: 300,
|
|
49
|
+
staleWhileRevalidate: 3600,
|
|
50
|
+
isPublic: true,
|
|
51
|
+
},
|
|
52
|
+
listing: {
|
|
53
|
+
maxAge: 30,
|
|
54
|
+
sMaxAge: 120,
|
|
55
|
+
staleWhileRevalidate: 600,
|
|
56
|
+
isPublic: true,
|
|
57
|
+
},
|
|
58
|
+
search: {
|
|
59
|
+
maxAge: 0,
|
|
60
|
+
sMaxAge: 60,
|
|
61
|
+
staleWhileRevalidate: 300,
|
|
62
|
+
isPublic: true,
|
|
63
|
+
},
|
|
64
|
+
cart: {
|
|
65
|
+
maxAge: 0,
|
|
66
|
+
sMaxAge: 0,
|
|
67
|
+
staleWhileRevalidate: 0,
|
|
68
|
+
isPublic: false,
|
|
69
|
+
},
|
|
70
|
+
private: {
|
|
71
|
+
maxAge: 0,
|
|
72
|
+
sMaxAge: 0,
|
|
73
|
+
staleWhileRevalidate: 0,
|
|
74
|
+
isPublic: false,
|
|
75
|
+
},
|
|
76
|
+
none: {
|
|
77
|
+
maxAge: 0,
|
|
78
|
+
sMaxAge: 0,
|
|
79
|
+
staleWhileRevalidate: 0,
|
|
80
|
+
isPublic: false,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Generate a `Cache-Control` header value from a named profile or custom config.
|
|
86
|
+
* Returns a headers object ready to spread into route `headers()`.
|
|
87
|
+
*
|
|
88
|
+
* Always includes `Vary: Accept-Encoding` for public responses.
|
|
89
|
+
*/
|
|
90
|
+
export function cacheHeaders(
|
|
91
|
+
profileOrConfig: CacheProfile | CacheHeadersConfig,
|
|
92
|
+
): Record<string, string> {
|
|
93
|
+
const config = typeof profileOrConfig === "string" ? PROFILES[profileOrConfig] : profileOrConfig;
|
|
94
|
+
|
|
95
|
+
if (!config.isPublic || (config.sMaxAge === 0 && config.maxAge === 0)) {
|
|
96
|
+
return {
|
|
97
|
+
"Cache-Control": "private, no-cache, no-store, must-revalidate",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const parts: string[] = ["public"];
|
|
102
|
+
|
|
103
|
+
if (config.maxAge > 0) {
|
|
104
|
+
parts.push(`max-age=${config.maxAge}`);
|
|
105
|
+
} else {
|
|
106
|
+
parts.push("max-age=0");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (config.sMaxAge > 0) {
|
|
110
|
+
parts.push(`s-maxage=${config.sMaxAge}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (config.staleWhileRevalidate > 0) {
|
|
114
|
+
parts.push(`stale-while-revalidate=${config.staleWhileRevalidate}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
"Cache-Control": parts.join(", "),
|
|
119
|
+
Vary: "Accept-Encoding",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get the raw config for a named cache profile.
|
|
125
|
+
* Useful when you need the numeric values (e.g. for custom logic).
|
|
126
|
+
*/
|
|
127
|
+
export function getCacheProfileConfig(profile: CacheProfile): CacheHeadersConfig {
|
|
128
|
+
return PROFILES[profile];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Client-side route cache defaults (TanStack Router staleTime / gcTime)
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
interface RouteCacheDefaults {
|
|
136
|
+
/** How long route data is considered fresh on the client (ms). */
|
|
137
|
+
staleTime: number;
|
|
138
|
+
/** How long stale data is kept in memory before garbage collection (ms). */
|
|
139
|
+
gcTime: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const ROUTE_CACHE: Record<CacheProfile, RouteCacheDefaults> = {
|
|
143
|
+
static: { staleTime: 5 * 60_000, gcTime: 30 * 60_000 },
|
|
144
|
+
product: { staleTime: 60_000, gcTime: 5 * 60_000 },
|
|
145
|
+
listing: { staleTime: 60_000, gcTime: 5 * 60_000 },
|
|
146
|
+
search: { staleTime: 30_000, gcTime: 2 * 60_000 },
|
|
147
|
+
cart: { staleTime: 0, gcTime: 0 },
|
|
148
|
+
private: { staleTime: 0, gcTime: 0 },
|
|
149
|
+
none: { staleTime: 0, gcTime: 0 },
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Returns `{ staleTime, gcTime }` for a cache profile, ready to spread
|
|
154
|
+
* into a TanStack Router route definition.
|
|
155
|
+
*
|
|
156
|
+
* In dev mode, uses short staleTime (5s) to keep data fresh enough for
|
|
157
|
+
* development while avoiding redundant re-fetches during rapid
|
|
158
|
+
* interactions (e.g. variant switching on a PDP).
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```ts
|
|
162
|
+
* export const Route = createFileRoute("/$")({
|
|
163
|
+
* ...routeCacheDefaults("listing"),
|
|
164
|
+
* loader: ...,
|
|
165
|
+
* headers: () => cacheHeaders("listing"),
|
|
166
|
+
* });
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export function routeCacheDefaults(profile: CacheProfile): RouteCacheDefaults {
|
|
170
|
+
const env = typeof globalThis.process !== "undefined" ? globalThis.process.env : undefined;
|
|
171
|
+
const isDev = env?.DECO_CACHE_DISABLE === "true" || env?.NODE_ENV === "development";
|
|
172
|
+
if (isDev) return { staleTime: 5_000, gcTime: 30_000 };
|
|
173
|
+
return ROUTE_CACHE[profile];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// URL-based cache profile detection
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
interface CachePattern {
|
|
181
|
+
test: (pathname: string, searchParams: URLSearchParams) => boolean;
|
|
182
|
+
profile: CacheProfile;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const builtinPatterns: CachePattern[] = [
|
|
186
|
+
// Private routes — must be first (highest priority)
|
|
187
|
+
{
|
|
188
|
+
test: (p) =>
|
|
189
|
+
p.startsWith("/cart") ||
|
|
190
|
+
p.startsWith("/checkout") ||
|
|
191
|
+
p.startsWith("/account") ||
|
|
192
|
+
p.startsWith("/login") ||
|
|
193
|
+
p.startsWith("/my-account"),
|
|
194
|
+
profile: "private",
|
|
195
|
+
},
|
|
196
|
+
// Internal / API routes
|
|
197
|
+
{
|
|
198
|
+
test: (p) =>
|
|
199
|
+
p.startsWith("/api/") ||
|
|
200
|
+
p.startsWith("/deco/") ||
|
|
201
|
+
p.startsWith("/_server") ||
|
|
202
|
+
p.startsWith("/_build"),
|
|
203
|
+
profile: "none",
|
|
204
|
+
},
|
|
205
|
+
// Search pages
|
|
206
|
+
{
|
|
207
|
+
test: (p, sp) => p === "/s" || p.startsWith("/s/") || sp.has("q"),
|
|
208
|
+
profile: "search",
|
|
209
|
+
},
|
|
210
|
+
// PDP — VTEX convention: URL ends with /p
|
|
211
|
+
{
|
|
212
|
+
test: (p) => p.endsWith("/p") || /\/p[?#]/.test(p),
|
|
213
|
+
profile: "product",
|
|
214
|
+
},
|
|
215
|
+
// Home page
|
|
216
|
+
{
|
|
217
|
+
test: (p) => p === "/" || p === "",
|
|
218
|
+
profile: "static",
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
const customPatterns: CachePattern[] = [];
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Register additional URL-to-profile patterns. Custom patterns are evaluated
|
|
226
|
+
* before built-in ones, so they can override defaults.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```ts
|
|
230
|
+
* registerCachePattern({
|
|
231
|
+
* test: (p) => p.startsWith("/institucional"),
|
|
232
|
+
* profile: "static",
|
|
233
|
+
* });
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
export function registerCachePattern(pattern: CachePattern): void {
|
|
237
|
+
customPatterns.push(pattern);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Detect the appropriate cache profile based on a URL.
|
|
242
|
+
* Evaluates custom patterns first, then built-in patterns.
|
|
243
|
+
* Falls back to "listing" (conservative public cache) for unmatched paths.
|
|
244
|
+
*/
|
|
245
|
+
export function detectCacheProfile(pathnameOrUrl: string | URL): CacheProfile {
|
|
246
|
+
let pathname: string;
|
|
247
|
+
let searchParams: URLSearchParams;
|
|
248
|
+
|
|
249
|
+
if (typeof pathnameOrUrl === "string" && !pathnameOrUrl.startsWith("http")) {
|
|
250
|
+
const qIdx = pathnameOrUrl.indexOf("?");
|
|
251
|
+
pathname = qIdx >= 0 ? pathnameOrUrl.slice(0, qIdx) : pathnameOrUrl;
|
|
252
|
+
searchParams = new URLSearchParams(qIdx >= 0 ? pathnameOrUrl.slice(qIdx) : "");
|
|
253
|
+
} else {
|
|
254
|
+
const url = pathnameOrUrl instanceof URL ? pathnameOrUrl : new URL(pathnameOrUrl);
|
|
255
|
+
pathname = url.pathname;
|
|
256
|
+
searchParams = url.searchParams;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const pattern of customPatterns) {
|
|
260
|
+
if (pattern.test(pathname, searchParams)) return pattern.profile;
|
|
261
|
+
}
|
|
262
|
+
for (const pattern of builtinPatterns) {
|
|
263
|
+
if (pattern.test(pathname, searchParams)) return pattern.profile;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Default: listing (conservative, short edge TTL)
|
|
267
|
+
return "listing";
|
|
268
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side loader caching with stale-while-revalidate semantics.
|
|
3
|
+
*
|
|
4
|
+
* This provides a lightweight in-memory cache layer for commerce loaders
|
|
5
|
+
* that runs on the server during SSR, without requiring TanStack Query
|
|
6
|
+
* (which is optional and operates at the client/route level).
|
|
7
|
+
*
|
|
8
|
+
* For client-side SWR, use TanStack Query's `staleTime` / `gcTime`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type CachePolicy = "no-store" | "no-cache" | "stale-while-revalidate";
|
|
12
|
+
|
|
13
|
+
export interface CachedLoaderOptions {
|
|
14
|
+
policy: CachePolicy;
|
|
15
|
+
/** Max age in milliseconds before an entry is considered stale. Default: 60_000 (1 min). */
|
|
16
|
+
maxAge?: number;
|
|
17
|
+
/** Key function to generate a cache key from loader props. Default: JSON.stringify. */
|
|
18
|
+
keyFn?: (props: unknown) => string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Loader module interface for modules that export cache configuration alongside
|
|
23
|
+
* their default loader function. Mirrors deco-cx/apps pattern where loaders
|
|
24
|
+
* can declare their own caching policy.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* // In a loader file:
|
|
29
|
+
* export const cache = "stale-while-revalidate";
|
|
30
|
+
* export const cacheKey = (props: any) => `myLoader:${props.slug}`;
|
|
31
|
+
* export default async function myLoader(props: Props) { ... }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export interface LoaderModule<TProps = any, TResult = any> {
|
|
35
|
+
default: (props: TProps) => Promise<TResult>;
|
|
36
|
+
cache?: CachePolicy | { maxAge: number };
|
|
37
|
+
cacheKey?: (props: TProps) => string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CacheEntry<T = unknown> {
|
|
41
|
+
value: T;
|
|
42
|
+
createdAt: number;
|
|
43
|
+
refreshing: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DEFAULT_MAX_AGE = 60_000;
|
|
47
|
+
const MAX_CACHE_ENTRIES = 500;
|
|
48
|
+
|
|
49
|
+
const cache = new Map<string, CacheEntry>();
|
|
50
|
+
|
|
51
|
+
function evictIfNeeded() {
|
|
52
|
+
if (cache.size <= MAX_CACHE_ENTRIES) return;
|
|
53
|
+
const oldest = [...cache.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
54
|
+
const toDelete = oldest.slice(0, cache.size - MAX_CACHE_ENTRIES);
|
|
55
|
+
for (const [key] of toDelete) cache.delete(key);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const inflightRequests = new Map<string, Promise<unknown>>();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Wraps a loader function with server-side caching and single-flight dedup.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const cachedProductList = createCachedLoader(
|
|
66
|
+
* "vtex/loaders/productList",
|
|
67
|
+
* vtexProductList,
|
|
68
|
+
* { policy: "stale-while-revalidate", maxAge: 30_000 }
|
|
69
|
+
* );
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function createCachedLoader<TProps, TResult>(
|
|
73
|
+
name: string,
|
|
74
|
+
loaderFn: (props: TProps) => Promise<TResult>,
|
|
75
|
+
options: CachedLoaderOptions,
|
|
76
|
+
): (props: TProps) => Promise<TResult> {
|
|
77
|
+
const { policy, maxAge = DEFAULT_MAX_AGE, keyFn = JSON.stringify } = options;
|
|
78
|
+
|
|
79
|
+
const env = typeof globalThis.process !== "undefined" ? globalThis.process.env : undefined;
|
|
80
|
+
const isDev = env?.DECO_CACHE_DISABLE === "true" || env?.NODE_ENV === "development";
|
|
81
|
+
|
|
82
|
+
if (policy === "no-store") return loaderFn;
|
|
83
|
+
|
|
84
|
+
return async (props: TProps): Promise<TResult> => {
|
|
85
|
+
const cacheKey = `${name}::${keyFn(props)}`;
|
|
86
|
+
|
|
87
|
+
// Single-flight dedup: if an identical request is already in-flight, reuse it
|
|
88
|
+
const inflight = inflightRequests.get(cacheKey);
|
|
89
|
+
if (inflight) return inflight as Promise<TResult>;
|
|
90
|
+
|
|
91
|
+
// In dev mode, skip SWR cache but keep inflight dedup
|
|
92
|
+
if (isDev) {
|
|
93
|
+
const promise = loaderFn(props).finally(() => inflightRequests.delete(cacheKey));
|
|
94
|
+
inflightRequests.set(cacheKey, promise);
|
|
95
|
+
return promise;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const entry = cache.get(cacheKey) as CacheEntry<TResult> | undefined;
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const isStale = entry ? now - entry.createdAt > maxAge : true;
|
|
101
|
+
|
|
102
|
+
if (policy === "no-cache") {
|
|
103
|
+
if (entry && !isStale) return entry.value;
|
|
104
|
+
// Stale or missing — fetch fresh
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (policy === "stale-while-revalidate") {
|
|
108
|
+
if (entry && !isStale) return entry.value;
|
|
109
|
+
|
|
110
|
+
if (entry && isStale && !entry.refreshing) {
|
|
111
|
+
entry.refreshing = true;
|
|
112
|
+
// Fire-and-forget background refresh
|
|
113
|
+
loaderFn(props)
|
|
114
|
+
.then((result) => {
|
|
115
|
+
cache.set(cacheKey, {
|
|
116
|
+
value: result,
|
|
117
|
+
createdAt: Date.now(),
|
|
118
|
+
refreshing: false,
|
|
119
|
+
});
|
|
120
|
+
})
|
|
121
|
+
.catch(() => {
|
|
122
|
+
entry.refreshing = false;
|
|
123
|
+
});
|
|
124
|
+
return entry.value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (entry) return entry.value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const promise = loaderFn(props)
|
|
131
|
+
.then((result) => {
|
|
132
|
+
cache.set(cacheKey, {
|
|
133
|
+
value: result,
|
|
134
|
+
createdAt: Date.now(),
|
|
135
|
+
refreshing: false,
|
|
136
|
+
});
|
|
137
|
+
evictIfNeeded();
|
|
138
|
+
inflightRequests.delete(cacheKey);
|
|
139
|
+
return result;
|
|
140
|
+
})
|
|
141
|
+
.catch((err) => {
|
|
142
|
+
inflightRequests.delete(cacheKey);
|
|
143
|
+
throw err;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
inflightRequests.set(cacheKey, promise);
|
|
147
|
+
return promise;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Create a cached loader from a module that exports `cache` and/or `cacheKey`.
|
|
153
|
+
* Falls back to the provided defaults if the module doesn't declare them.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* import * as myLoaderModule from "./loaders/myLoader";
|
|
158
|
+
* const cached = createCachedLoaderFromModule("myLoader", myLoaderModule, {
|
|
159
|
+
* policy: "stale-while-revalidate",
|
|
160
|
+
* maxAge: 60_000,
|
|
161
|
+
* });
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export function createCachedLoaderFromModule<TProps, TResult>(
|
|
165
|
+
name: string,
|
|
166
|
+
mod: LoaderModule<TProps, TResult>,
|
|
167
|
+
defaults?: Partial<CachedLoaderOptions>,
|
|
168
|
+
): (props: TProps) => Promise<TResult> {
|
|
169
|
+
const moduleCache = mod.cache;
|
|
170
|
+
let policy: CachePolicy;
|
|
171
|
+
let maxAge: number | undefined;
|
|
172
|
+
|
|
173
|
+
if (typeof moduleCache === "string") {
|
|
174
|
+
policy = moduleCache;
|
|
175
|
+
} else if (moduleCache && typeof moduleCache === "object") {
|
|
176
|
+
policy = "stale-while-revalidate";
|
|
177
|
+
maxAge = moduleCache.maxAge;
|
|
178
|
+
} else {
|
|
179
|
+
policy = defaults?.policy ?? "stale-while-revalidate";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
maxAge = maxAge ?? defaults?.maxAge;
|
|
183
|
+
|
|
184
|
+
const keyFn = mod.cacheKey
|
|
185
|
+
? (props: unknown) => {
|
|
186
|
+
const key = mod.cacheKey!(props as TProps);
|
|
187
|
+
return key ?? JSON.stringify(props);
|
|
188
|
+
}
|
|
189
|
+
: defaults?.keyFn;
|
|
190
|
+
|
|
191
|
+
return createCachedLoader(name, mod.default, { policy, maxAge, keyFn });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Clear all cached entries. Useful for decofile hot-reload. */
|
|
195
|
+
export function clearLoaderCache() {
|
|
196
|
+
cache.clear();
|
|
197
|
+
inflightRequests.clear();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Get cache stats for diagnostics. */
|
|
201
|
+
export function getLoaderCacheStats() {
|
|
202
|
+
return {
|
|
203
|
+
entries: cache.size,
|
|
204
|
+
inflight: inflightRequests.size,
|
|
205
|
+
};
|
|
206
|
+
}
|
package/src/sdk/clx.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function getCookie(name: string): string {
|
|
2
|
+
return (
|
|
3
|
+
globalThis.window?.document?.cookie?.split("; ").reduce((r, v) => {
|
|
4
|
+
const parts = v.split("=");
|
|
5
|
+
return parts[0] === name ? decodeURIComponent(parts[1]) : r;
|
|
6
|
+
}, "") ?? ""
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function setCookie(name: string, value: string, days: number) {
|
|
11
|
+
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
|
12
|
+
if (globalThis?.window?.document) {
|
|
13
|
+
globalThis.window.document.cookie =
|
|
14
|
+
name + "=" + encodeURIComponent(value) + "; expires=" + expires + "; path=/";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function deleteCookie(name: string) {
|
|
19
|
+
if (globalThis?.window?.document) {
|
|
20
|
+
globalThis.window.document.cookie = name + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getServerSideCookie(req: Request, name: string): string {
|
|
25
|
+
const cookie = req.headers
|
|
26
|
+
.get("cookie")
|
|
27
|
+
?.split(";")
|
|
28
|
+
.find((c) => c.trim().startsWith(name))
|
|
29
|
+
?.split("=")[1];
|
|
30
|
+
return cookie ? decodeURIComponent(cookie) : "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function decodeCookie(cookieValue: string): any {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(decodeURIComponent(cookieValue));
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic bridge that turns any async function into a TanStack Start server function.
|
|
3
|
+
*
|
|
4
|
+
* Used by @decocms/apps to expose commerce actions/loaders as typed
|
|
5
|
+
* `invoke.*` calls that execute on the server with full credentials.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { createInvokeFn } from "@decocms/start/sdk/createInvoke";
|
|
10
|
+
* import { addItemsToCart } from "./actions/checkout";
|
|
11
|
+
*
|
|
12
|
+
* export const invoke = {
|
|
13
|
+
* vtex: {
|
|
14
|
+
* actions: {
|
|
15
|
+
* addItemsToCart: createInvokeFn(
|
|
16
|
+
* (input: { orderFormId: string; orderItems: CartItem[] }) =>
|
|
17
|
+
* addItemsToCart(input.orderFormId, input.orderItems),
|
|
18
|
+
* { unwrap: true },
|
|
19
|
+
* ),
|
|
20
|
+
* },
|
|
21
|
+
* },
|
|
22
|
+
* };
|
|
23
|
+
*
|
|
24
|
+
* // Client-side usage:
|
|
25
|
+
* await invoke.vtex.actions.addItemsToCart({ data: { orderFormId, orderItems } });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
29
|
+
|
|
30
|
+
export interface InvokeFnOpts {
|
|
31
|
+
/**
|
|
32
|
+
* When true, extracts `.data` from the result before returning.
|
|
33
|
+
* Use for VTEX checkout functions that return VtexFetchResult<T>
|
|
34
|
+
* (i.e. `{ data: T, setCookies: string[] }`).
|
|
35
|
+
*/
|
|
36
|
+
unwrap?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Transforms an async function into a `createServerFn` wrapper.
|
|
41
|
+
*
|
|
42
|
+
* - Client calls: `fn({ data: input })`
|
|
43
|
+
* - Server executes: `action(input)`
|
|
44
|
+
* - If `unwrap: true`, extracts `.data` from VtexFetchResult-shaped results
|
|
45
|
+
*/
|
|
46
|
+
export function createInvokeFn<TInput, TOutput>(
|
|
47
|
+
action: (input: TInput) => Promise<TOutput>,
|
|
48
|
+
opts?: InvokeFnOpts,
|
|
49
|
+
): (ctx: { data: TInput }) => Promise<TOutput> {
|
|
50
|
+
return createServerFn({ method: "POST" }).handler(async (ctx) => {
|
|
51
|
+
const result = await action(ctx.data as TInput);
|
|
52
|
+
if (opts?.unwrap && result && typeof result === "object" && "data" in result) {
|
|
53
|
+
return (result as any).data;
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}) as unknown as (ctx: { data: TInput }) => Promise<TOutput>;
|
|
57
|
+
}
|
package/src/sdk/csp.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Security Policy header utilities.
|
|
3
|
+
*
|
|
4
|
+
* Sets frame-ancestors to allow the Deco admin to embed the
|
|
5
|
+
* storefront in an iframe for live preview.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DEFAULT_ADMIN_ORIGINS = ["https://admin.deco.cx", "https://deco.cx", "https://localhost:*"];
|
|
9
|
+
|
|
10
|
+
export interface CSPOptions {
|
|
11
|
+
/** Additional origins allowed to frame the storefront. */
|
|
12
|
+
extraOrigins?: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Deco admin origins. Defaults to admin.deco.cx + localhost.
|
|
15
|
+
* Set to empty array to disallow all external framing.
|
|
16
|
+
*/
|
|
17
|
+
adminOrigins?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Set Content-Security-Policy frame-ancestors header on a Response.
|
|
22
|
+
*
|
|
23
|
+
* This is required for the Deco admin live preview iframe to work.
|
|
24
|
+
* Also removes X-Frame-Options if present (CSP supersedes it).
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { setCSPHeaders } from "@decocms/start/sdk/csp";
|
|
29
|
+
*
|
|
30
|
+
* // In middleware:
|
|
31
|
+
* const response = await next();
|
|
32
|
+
* setCSPHeaders(response);
|
|
33
|
+
* return response;
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function setCSPHeaders(response: Response, options?: CSPOptions): void {
|
|
37
|
+
const origins = [
|
|
38
|
+
"'self'",
|
|
39
|
+
...(options?.adminOrigins ?? DEFAULT_ADMIN_ORIGINS),
|
|
40
|
+
...(options?.extraOrigins ?? []),
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
response.headers.set("Content-Security-Policy", `frame-ancestors ${origins.join(" ")}`);
|
|
44
|
+
|
|
45
|
+
response.headers.delete("X-Frame-Options");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build the CSP header value string without applying it.
|
|
50
|
+
* Useful when constructing headers in route definitions.
|
|
51
|
+
*/
|
|
52
|
+
export function buildCSPHeaderValue(options?: CSPOptions): string {
|
|
53
|
+
const origins = [
|
|
54
|
+
"'self'",
|
|
55
|
+
...(options?.adminOrigins ?? DEFAULT_ADMIN_ORIGINS),
|
|
56
|
+
...(options?.extraOrigins ?? []),
|
|
57
|
+
];
|
|
58
|
+
return `frame-ancestors ${origins.join(" ")}`;
|
|
59
|
+
}
|