@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,703 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for creating a cache-aware Cloudflare Worker entry.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a TanStack Start server entry with:
|
|
5
|
+
* - Cloudflare Cache API integration (edge caching)
|
|
6
|
+
* - Device-specific cache keys (mobile/desktop split)
|
|
7
|
+
* - Per-URL cache profile detection via detectCacheProfile()
|
|
8
|
+
* - Immutable caching for fingerprinted static assets
|
|
9
|
+
* - Cache purge API endpoint
|
|
10
|
+
* - Protection against accidental caching of private/search paths
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // src/worker-entry.ts
|
|
15
|
+
* import handler, { createServerEntry } from "@tanstack/react-start/server-entry";
|
|
16
|
+
* import { createDecoWorkerEntry } from "@decocms/start/sdk/workerEntry";
|
|
17
|
+
*
|
|
18
|
+
* const serverEntry = createServerEntry({
|
|
19
|
+
* async fetch(request) {
|
|
20
|
+
* return await handler.fetch(request);
|
|
21
|
+
* },
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* export default createDecoWorkerEntry(serverEntry);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { getRenderShellConfig } from "../admin/setup";
|
|
29
|
+
import {
|
|
30
|
+
type CacheProfile,
|
|
31
|
+
cacheHeaders,
|
|
32
|
+
detectCacheProfile,
|
|
33
|
+
getCacheProfileConfig,
|
|
34
|
+
} from "./cacheHeaders";
|
|
35
|
+
import { cleanPathForCacheKey } from "./urlUtils";
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Types
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Minimal ExecutionContext interface compatible with Cloudflare Workers.
|
|
43
|
+
* Defined here so deco-start doesn't need @cloudflare/workers-types.
|
|
44
|
+
*/
|
|
45
|
+
interface WorkerExecutionContext {
|
|
46
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
47
|
+
passThroughOnException(): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ServerEntry {
|
|
51
|
+
fetch(
|
|
52
|
+
request: Request,
|
|
53
|
+
env: Record<string, unknown>,
|
|
54
|
+
ctx: WorkerExecutionContext,
|
|
55
|
+
): Response | Promise<Response>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Segment dimensions used to differentiate cache entries.
|
|
60
|
+
*
|
|
61
|
+
* The workerEntry calls `buildSegment` (if provided) to extract these
|
|
62
|
+
* from the request. Two requests with the same SegmentKey share a
|
|
63
|
+
* cache entry; different segments get different cached responses.
|
|
64
|
+
*/
|
|
65
|
+
export interface SegmentKey {
|
|
66
|
+
device: "mobile" | "desktop";
|
|
67
|
+
/** Whether the user is logged in (e.g., has a valid auth cookie). */
|
|
68
|
+
loggedIn?: boolean;
|
|
69
|
+
/** Commerce sales channel / price list. */
|
|
70
|
+
salesChannel?: string;
|
|
71
|
+
/** Sorted list of active A/B flag names for cache cohort splitting. */
|
|
72
|
+
flags?: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Admin route handlers injected by the site's worker-entry.ts.
|
|
77
|
+
* Kept as a runtime option so the imports only exist in the SSR entry
|
|
78
|
+
* (not pulled into the client Vite build).
|
|
79
|
+
*/
|
|
80
|
+
export interface AdminHandlers {
|
|
81
|
+
handleMeta: (request: Request) => Response;
|
|
82
|
+
handleDecofileRead: () => Response;
|
|
83
|
+
handleDecofileReload: (request: Request) => Response | Promise<Response>;
|
|
84
|
+
handleRender: (request: Request) => Response | Promise<Response>;
|
|
85
|
+
corsHeaders: (request: Request) => Record<string, string>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface DecoWorkerEntryOptions {
|
|
89
|
+
/**
|
|
90
|
+
* Admin route handlers (/live/_meta, /.decofile, /live/previews).
|
|
91
|
+
* Pass the handlers from `@decocms/start/admin` here.
|
|
92
|
+
* If not provided, admin routes are not handled.
|
|
93
|
+
*/
|
|
94
|
+
admin?: AdminHandlers;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Override the default cache profile detection.
|
|
98
|
+
* Return `null` to fall through to the built-in detector.
|
|
99
|
+
*/
|
|
100
|
+
detectProfile?: (url: URL) => CacheProfile | null;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Whether to create device-specific cache keys (mobile vs desktop).
|
|
104
|
+
* Useful when server-rendered HTML differs by device.
|
|
105
|
+
* @default true
|
|
106
|
+
*/
|
|
107
|
+
deviceSpecificKeys?: boolean;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build a full segment key from the incoming request.
|
|
111
|
+
*
|
|
112
|
+
* When provided, the segment key replaces the simple device-only
|
|
113
|
+
* cache key with a richer key that differentiates by login state,
|
|
114
|
+
* sales channel, and A/B flags.
|
|
115
|
+
*
|
|
116
|
+
* Logged-in segments (`loggedIn: true`) automatically bypass the
|
|
117
|
+
* cache (the response is fetched fresh every time).
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* import { extractVtexContext } from "@decocms/apps/vtex/middleware";
|
|
122
|
+
*
|
|
123
|
+
* createDecoWorkerEntry(serverEntry, {
|
|
124
|
+
* buildSegment: (request) => {
|
|
125
|
+
* const vtx = extractVtexContext(request);
|
|
126
|
+
* return {
|
|
127
|
+
* device: /mobile|android|iphone/i.test(request.headers.get("user-agent") ?? "") ? "mobile" : "desktop",
|
|
128
|
+
* loggedIn: vtx.isLoggedIn,
|
|
129
|
+
* salesChannel: vtx.salesChannel,
|
|
130
|
+
* };
|
|
131
|
+
* },
|
|
132
|
+
* });
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
buildSegment?: (request: Request) => SegmentKey;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Environment variable name holding the cache purge token.
|
|
139
|
+
* Set to `false` to disable the purge endpoint.
|
|
140
|
+
* @default "PURGE_TOKEN"
|
|
141
|
+
*/
|
|
142
|
+
purgeTokenEnv?: string | false;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Paths that should always bypass the edge cache, even if the
|
|
146
|
+
* profile detector would otherwise cache them.
|
|
147
|
+
* Defaults include `/_server`, `/_build`, `/assets`, `/deco/`.
|
|
148
|
+
*/
|
|
149
|
+
bypassPaths?: string[];
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Additional paths (beyond the defaults) that should bypass caching.
|
|
153
|
+
* Merged with the default bypass paths.
|
|
154
|
+
*/
|
|
155
|
+
extraBypassPaths?: string[];
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Custom HTML shell for the `/live/previews` iframe page.
|
|
159
|
+
* If not provided, a shell is generated from the render config
|
|
160
|
+
* (theme, CSS, fonts) set via setRenderShell().
|
|
161
|
+
*/
|
|
162
|
+
previewShell?: string;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Regex for detecting fingerprinted static assets (content-hashed filenames).
|
|
166
|
+
* Matched paths get `immutable, max-age=31536000`.
|
|
167
|
+
* @default /\/_build\/assets\/.*-[a-zA-Z0-9]{8,}\.\w+$/
|
|
168
|
+
*/
|
|
169
|
+
fingerprintedAssetPattern?: RegExp;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Whether to strip UTM and tracking params from cache keys.
|
|
173
|
+
* Two requests differing only in utm_source, fbclid, etc.
|
|
174
|
+
* will share the same cache entry.
|
|
175
|
+
* @default true
|
|
176
|
+
*/
|
|
177
|
+
stripTrackingParams?: boolean;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Optional proxy handler for commerce backend routes
|
|
181
|
+
* (checkout, account, API, login, etc.).
|
|
182
|
+
*
|
|
183
|
+
* Called early in the request pipeline — after admin routes and cache
|
|
184
|
+
* purge, but before static assets and edge cache logic. This ensures
|
|
185
|
+
* proxy requests never hit TanStack Start or the React SSR pipeline.
|
|
186
|
+
*
|
|
187
|
+
* Return a `Response` to proxy the request, or `null` to let the
|
|
188
|
+
* normal TanStack Start flow handle it.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```ts
|
|
192
|
+
* import { shouldProxyToVtex, proxyToVtex } from "@decocms/apps/vtex/utils/proxy";
|
|
193
|
+
*
|
|
194
|
+
* createDecoWorkerEntry(serverEntry, {
|
|
195
|
+
* proxyHandler: (request, url) => {
|
|
196
|
+
* if (shouldProxyToVtex(url.pathname)) {
|
|
197
|
+
* return proxyToVtex(request);
|
|
198
|
+
* }
|
|
199
|
+
* return null;
|
|
200
|
+
* },
|
|
201
|
+
* });
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
proxyHandler?: (request: Request, url: URL) => Promise<Response | null> | Response | null;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Environment variable name holding a build version string.
|
|
208
|
+
* The value is appended to every cache key so each deploy gets its own
|
|
209
|
+
* cache namespace — old entries become orphaned and expire naturally,
|
|
210
|
+
* preventing stale HTML that references old CSS/JS fingerprinted filenames.
|
|
211
|
+
*
|
|
212
|
+
* Set to `false` to disable. When the env var is missing or empty,
|
|
213
|
+
* cache keys remain unversioned (backward-compatible).
|
|
214
|
+
*
|
|
215
|
+
* @default "BUILD_HASH"
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```yaml
|
|
219
|
+
* # CI: pass git hash to wrangler
|
|
220
|
+
* - run: npx wrangler deploy --var BUILD_HASH:$(git rev-parse --short HEAD)
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
cacheVersionEnv?: string | false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Constants
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
const PREVIEW_SHELL_SCRIPT = `(function() {
|
|
231
|
+
if (window.__DECO_LIVE_CONTROLS__) return;
|
|
232
|
+
window.__DECO_LIVE_CONTROLS__ = true;
|
|
233
|
+
addEventListener("message", function(event) {
|
|
234
|
+
var data = event.data;
|
|
235
|
+
if (!data || typeof data !== "object") return;
|
|
236
|
+
switch (data.type) {
|
|
237
|
+
case "editor::inject":
|
|
238
|
+
if (data.args && data.args.script) {
|
|
239
|
+
try { eval(data.args.script); } catch(e) { console.error("[deco] inject error:", e); }
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
})();`;
|
|
245
|
+
|
|
246
|
+
function buildPreviewShell(): string {
|
|
247
|
+
const { cssHref, fontHrefs, themeName, bodyClass, htmlLang } = getRenderShellConfig();
|
|
248
|
+
|
|
249
|
+
const themeAttr = themeName ? ` data-theme="${themeName}"` : "";
|
|
250
|
+
const langAttr = htmlLang ? ` lang="${htmlLang}"` : "";
|
|
251
|
+
const bodyAttr = bodyClass ? ` class="${bodyClass}"` : "";
|
|
252
|
+
|
|
253
|
+
const stylesheets = [
|
|
254
|
+
...fontHrefs.map((href) => `<link rel="stylesheet" href="${href}" />`),
|
|
255
|
+
cssHref ? `<link rel="stylesheet" href="${cssHref}" />` : "",
|
|
256
|
+
]
|
|
257
|
+
.filter(Boolean)
|
|
258
|
+
.join("\n ");
|
|
259
|
+
|
|
260
|
+
return `<!DOCTYPE html>
|
|
261
|
+
<html${langAttr}${themeAttr}>
|
|
262
|
+
<head>
|
|
263
|
+
<meta charset="utf-8" />
|
|
264
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
265
|
+
<title>Preview</title>
|
|
266
|
+
${stylesheets}
|
|
267
|
+
<script>${PREVIEW_SHELL_SCRIPT}</script>
|
|
268
|
+
</head>
|
|
269
|
+
<body${bodyAttr}>
|
|
270
|
+
<div id="preview-root" style="display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:system-ui;color:#666;">
|
|
271
|
+
Loading preview...
|
|
272
|
+
</div>
|
|
273
|
+
</body>
|
|
274
|
+
</html>`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const MOBILE_RE = /mobile|android|iphone|ipad|ipod/i;
|
|
278
|
+
const ONE_YEAR = 31536000;
|
|
279
|
+
|
|
280
|
+
const DEFAULT_BYPASS_PATHS = ["/_server", "/_build", "/deco/", "/live/", "/.decofile"];
|
|
281
|
+
|
|
282
|
+
const FINGERPRINTED_ASSET_RE = /(?:\/_build)?\/assets\/.*-[a-zA-Z0-9_-]{8,}\.\w+$/;
|
|
283
|
+
|
|
284
|
+
const IMMUTABLE_HEADERS: Record<string, string> = {
|
|
285
|
+
"Cache-Control": `public, max-age=${ONE_YEAR}, immutable`,
|
|
286
|
+
Vary: "Accept-Encoding",
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// Factory
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Creates a Cloudflare Worker fetch handler that wraps a TanStack Start
|
|
295
|
+
* server entry with intelligent edge caching.
|
|
296
|
+
*/
|
|
297
|
+
export function createDecoWorkerEntry(
|
|
298
|
+
serverEntry: ServerEntry,
|
|
299
|
+
options: DecoWorkerEntryOptions = {},
|
|
300
|
+
): {
|
|
301
|
+
fetch(
|
|
302
|
+
request: Request,
|
|
303
|
+
env: Record<string, unknown>,
|
|
304
|
+
ctx: WorkerExecutionContext,
|
|
305
|
+
): Promise<Response>;
|
|
306
|
+
} {
|
|
307
|
+
const {
|
|
308
|
+
admin,
|
|
309
|
+
detectProfile: customDetect,
|
|
310
|
+
deviceSpecificKeys = true,
|
|
311
|
+
buildSegment,
|
|
312
|
+
purgeTokenEnv = "PURGE_TOKEN",
|
|
313
|
+
bypassPaths,
|
|
314
|
+
extraBypassPaths = [],
|
|
315
|
+
fingerprintedAssetPattern = FINGERPRINTED_ASSET_RE,
|
|
316
|
+
stripTrackingParams: shouldStripTracking = true,
|
|
317
|
+
previewShell: customPreviewShell,
|
|
318
|
+
cacheVersionEnv = "BUILD_HASH",
|
|
319
|
+
} = options;
|
|
320
|
+
|
|
321
|
+
const allBypassPaths = [...(bypassPaths ?? DEFAULT_BYPASS_PATHS), ...extraBypassPaths];
|
|
322
|
+
|
|
323
|
+
// -- Helpers ----------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
function isBypassPath(pathname: string): boolean {
|
|
326
|
+
return allBypassPaths.some((bp) => pathname.startsWith(bp));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function isStaticAsset(pathname: string): boolean {
|
|
330
|
+
return fingerprintedAssetPattern.test(pathname);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function isCacheable(request: Request, url: URL): boolean {
|
|
334
|
+
if (request.method !== "GET") return false;
|
|
335
|
+
if (isBypassPath(url.pathname)) return false;
|
|
336
|
+
if (url.searchParams.has("__deco_draft")) return false;
|
|
337
|
+
if (url.searchParams.has("__deco_preview")) return false;
|
|
338
|
+
if (url.searchParams.has("pathTemplate")) return false;
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getProfile(url: URL): CacheProfile {
|
|
343
|
+
if (customDetect) {
|
|
344
|
+
const custom = customDetect(url);
|
|
345
|
+
if (custom !== null) return custom;
|
|
346
|
+
}
|
|
347
|
+
return detectCacheProfile(url);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function hashSegment(seg: SegmentKey): string {
|
|
351
|
+
const parts: string[] = [seg.device];
|
|
352
|
+
if (seg.loggedIn) parts.push("auth");
|
|
353
|
+
if (seg.salesChannel) parts.push(`sc=${seg.salesChannel}`);
|
|
354
|
+
if (seg.flags?.length) parts.push(`f=${seg.flags.sort().join(",")}`);
|
|
355
|
+
return parts.join("|");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function buildCacheKey(request: Request, env: Record<string, unknown>): { key: Request; segment?: SegmentKey } {
|
|
359
|
+
const url = new URL(request.url);
|
|
360
|
+
|
|
361
|
+
if (shouldStripTracking) {
|
|
362
|
+
const cleanPath = cleanPathForCacheKey(url.toString());
|
|
363
|
+
const cleanUrl = new URL(cleanPath, url.origin);
|
|
364
|
+
url.search = cleanUrl.search;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (cacheVersionEnv !== false) {
|
|
368
|
+
const version = (env[cacheVersionEnv] as string) || "";
|
|
369
|
+
if (version) {
|
|
370
|
+
url.searchParams.set("__v", version);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (buildSegment) {
|
|
375
|
+
const segment = buildSegment(request);
|
|
376
|
+
url.searchParams.set("__seg", hashSegment(segment));
|
|
377
|
+
return { key: new Request(url.toString(), { method: "GET" }), segment };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (deviceSpecificKeys) {
|
|
381
|
+
const device = MOBILE_RE.test(request.headers.get("user-agent") ?? "") ? "mobile" : "desktop";
|
|
382
|
+
url.searchParams.set("__cf_device", device);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { key: new Request(url.toString(), { method: "GET" }) };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// -- Purge handler ----------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
async function handlePurge(request: Request, env: Record<string, unknown>): Promise<Response> {
|
|
391
|
+
if (purgeTokenEnv === false) {
|
|
392
|
+
return new Response("Purge disabled", { status: 404 });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const token = (env[purgeTokenEnv] as string) || "";
|
|
396
|
+
if (!token || request.headers.get("Authorization") !== `Bearer ${token}`) {
|
|
397
|
+
return new Response("Unauthorized", { status: 401 });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
let body: { paths?: string[] };
|
|
401
|
+
try {
|
|
402
|
+
body = await request.json();
|
|
403
|
+
} catch {
|
|
404
|
+
return new Response("Invalid JSON body", { status: 400 });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const paths = body.paths;
|
|
408
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
409
|
+
return new Response('Body must include "paths": ["/", "/page"]', { status: 400 });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const cache =
|
|
413
|
+
typeof caches !== "undefined"
|
|
414
|
+
? ((caches as unknown as { default?: Cache }).default ?? null)
|
|
415
|
+
: null;
|
|
416
|
+
|
|
417
|
+
if (!cache) {
|
|
418
|
+
return Response.json({ purged: [], total: 0, note: "Cache API unavailable" });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const baseUrl = new URL(request.url).origin;
|
|
422
|
+
const purged: string[] = [];
|
|
423
|
+
|
|
424
|
+
// If using segment-based keys, purge requires known segment combos.
|
|
425
|
+
// For simplicity, purge common combos: both devices, default sales channel.
|
|
426
|
+
const segments: SegmentKey[] = buildSegment
|
|
427
|
+
? [
|
|
428
|
+
{ device: "mobile" },
|
|
429
|
+
{ device: "desktop" },
|
|
430
|
+
{ device: "mobile", salesChannel: "1" },
|
|
431
|
+
{ device: "desktop", salesChannel: "1" },
|
|
432
|
+
]
|
|
433
|
+
: [];
|
|
434
|
+
|
|
435
|
+
for (const p of paths) {
|
|
436
|
+
if (buildSegment && segments.length > 0) {
|
|
437
|
+
for (const seg of segments) {
|
|
438
|
+
const url = new URL(p, baseUrl);
|
|
439
|
+
url.searchParams.set("__seg", hashSegment(seg));
|
|
440
|
+
const key = new Request(url.toString(), { method: "GET" });
|
|
441
|
+
try {
|
|
442
|
+
if (await cache.delete(key)) {
|
|
443
|
+
purged.push(`${p} (${hashSegment(seg)})`);
|
|
444
|
+
}
|
|
445
|
+
} catch {
|
|
446
|
+
/* ignore */
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
const devices = deviceSpecificKeys ? (["mobile", "desktop"] as const) : ([null] as const);
|
|
451
|
+
|
|
452
|
+
for (const device of devices) {
|
|
453
|
+
const url = new URL(p, baseUrl);
|
|
454
|
+
if (device) url.searchParams.set("__cf_device", device);
|
|
455
|
+
const key = new Request(url.toString(), { method: "GET" });
|
|
456
|
+
try {
|
|
457
|
+
if (await cache.delete(key)) {
|
|
458
|
+
purged.push(device ? `${p} (${device})` : p);
|
|
459
|
+
}
|
|
460
|
+
} catch {
|
|
461
|
+
/* ignore */
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return Response.json({ purged, total: purged.length });
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// -- Admin route handler ---------------------------------------------------
|
|
471
|
+
|
|
472
|
+
const ADMIN_NO_CACHE: Record<string, string> = {
|
|
473
|
+
"Cache-Control": "no-store, no-cache, must-revalidate",
|
|
474
|
+
"CDN-Cache-Control": "no-store",
|
|
475
|
+
"Surrogate-Control": "no-store",
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
function addCors(response: Response, request: Request): Response {
|
|
479
|
+
if (!admin) return response;
|
|
480
|
+
const cors = admin.corsHeaders(request);
|
|
481
|
+
const resp = new Response(response.body, {
|
|
482
|
+
status: response.status,
|
|
483
|
+
statusText: response.statusText,
|
|
484
|
+
headers: new Headers(response.headers),
|
|
485
|
+
});
|
|
486
|
+
for (const [k, v] of Object.entries({ ...cors, ...ADMIN_NO_CACHE })) {
|
|
487
|
+
resp.headers.set(k, v);
|
|
488
|
+
}
|
|
489
|
+
return resp;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function tryAdminRoute(request: Request): Promise<Response | null> {
|
|
493
|
+
if (!admin) return null;
|
|
494
|
+
|
|
495
|
+
const url = new URL(request.url);
|
|
496
|
+
const { pathname } = url;
|
|
497
|
+
const method = request.method;
|
|
498
|
+
|
|
499
|
+
if (pathname === "/live/_meta") {
|
|
500
|
+
if (method === "OPTIONS") {
|
|
501
|
+
return new Response(null, {
|
|
502
|
+
status: 204,
|
|
503
|
+
headers: { ...admin.corsHeaders(request), ...ADMIN_NO_CACHE },
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
return addCors(admin.handleMeta(request), request);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (pathname === "/.decofile") {
|
|
510
|
+
if (method === "OPTIONS") {
|
|
511
|
+
return new Response(null, {
|
|
512
|
+
status: 204,
|
|
513
|
+
headers: { ...admin.corsHeaders(request), ...ADMIN_NO_CACHE },
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
if (method === "POST") {
|
|
517
|
+
return addCors(await admin.handleDecofileReload(request), request);
|
|
518
|
+
}
|
|
519
|
+
return addCors(admin.handleDecofileRead(), request);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (pathname === "/deco/_liveness") {
|
|
523
|
+
return new Response("OK", {
|
|
524
|
+
status: 200,
|
|
525
|
+
headers: { "Content-Type": "text/plain", ...ADMIN_NO_CACHE },
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if ((pathname === "/live/previews" || pathname === "/live/previews/") && method === "GET") {
|
|
530
|
+
const shell = customPreviewShell ?? buildPreviewShell();
|
|
531
|
+
return new Response(shell, {
|
|
532
|
+
status: 200,
|
|
533
|
+
headers: {
|
|
534
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
535
|
+
...admin.corsHeaders(request),
|
|
536
|
+
...ADMIN_NO_CACHE,
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (pathname.startsWith("/live/previews/") && pathname !== "/live/previews/") {
|
|
542
|
+
if (method === "OPTIONS") {
|
|
543
|
+
return new Response(null, {
|
|
544
|
+
status: 204,
|
|
545
|
+
headers: { ...admin.corsHeaders(request), ...ADMIN_NO_CACHE },
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
return addCors(await admin.handleRender(request), request);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// -- Main fetch handler -----------------------------------------------------
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
async fetch(
|
|
558
|
+
request: Request,
|
|
559
|
+
env: Record<string, unknown>,
|
|
560
|
+
ctx: WorkerExecutionContext,
|
|
561
|
+
): Promise<Response> {
|
|
562
|
+
const url = new URL(request.url);
|
|
563
|
+
|
|
564
|
+
// Admin routes (/_meta, /.decofile, /live/previews) — always handled first
|
|
565
|
+
const adminResponse = await tryAdminRoute(request);
|
|
566
|
+
if (adminResponse) return adminResponse;
|
|
567
|
+
|
|
568
|
+
// Purge endpoint
|
|
569
|
+
if (url.pathname === "/_cache/purge" && request.method === "POST") {
|
|
570
|
+
return handlePurge(request, env);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Commerce proxy (checkout, account, API, etc.)
|
|
574
|
+
if (options.proxyHandler) {
|
|
575
|
+
const proxyResponse = await options.proxyHandler(request, url);
|
|
576
|
+
if (proxyResponse) return proxyResponse;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Static fingerprinted assets — serve from origin with immutable headers
|
|
580
|
+
if (isStaticAsset(url.pathname)) {
|
|
581
|
+
const origin = await serverEntry.fetch(request, env, ctx);
|
|
582
|
+
if (origin.status === 200) {
|
|
583
|
+
const ct = origin.headers.get("content-type") ?? "";
|
|
584
|
+
if (ct.includes("text/html")) {
|
|
585
|
+
return new Response("Not Found", { status: 404 });
|
|
586
|
+
}
|
|
587
|
+
const resp = new Response(origin.body, origin);
|
|
588
|
+
for (const [k, v] of Object.entries(IMMUTABLE_HEADERS)) {
|
|
589
|
+
resp.headers.set(k, v);
|
|
590
|
+
}
|
|
591
|
+
return resp;
|
|
592
|
+
}
|
|
593
|
+
return origin;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Non-cacheable requests — pass through but protect against accidental caching
|
|
597
|
+
if (!isCacheable(request, url)) {
|
|
598
|
+
const origin = await serverEntry.fetch(request, env, ctx);
|
|
599
|
+
const profile = getProfile(url);
|
|
600
|
+
|
|
601
|
+
// If the profile is private/none/cart, strip any public cache headers
|
|
602
|
+
// the route may have set (prevents the search caching bug)
|
|
603
|
+
if (profile === "private" || profile === "none" || profile === "cart") {
|
|
604
|
+
const resp = new Response(origin.body, origin);
|
|
605
|
+
resp.headers.set("Cache-Control", "private, no-cache, no-store, must-revalidate");
|
|
606
|
+
resp.headers.delete("CDN-Cache-Control");
|
|
607
|
+
return resp;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return origin;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Cacheable request — build segment-aware cache key
|
|
614
|
+
const { key: cacheKey, segment } = buildCacheKey(request, env);
|
|
615
|
+
|
|
616
|
+
// Logged-in users always bypass the cache (personalized content)
|
|
617
|
+
if (segment?.loggedIn) {
|
|
618
|
+
const origin = await serverEntry.fetch(request, env, ctx);
|
|
619
|
+
const resp = new Response(origin.body, origin);
|
|
620
|
+
resp.headers.set("Cache-Control", "private, no-cache, no-store, must-revalidate");
|
|
621
|
+
resp.headers.set("X-Cache", "BYPASS");
|
|
622
|
+
resp.headers.set("X-Cache-Reason", "logged-in");
|
|
623
|
+
return resp;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Check Cache API (may not be available in local dev / miniflare)
|
|
627
|
+
const cache =
|
|
628
|
+
typeof caches !== "undefined"
|
|
629
|
+
? ((caches as unknown as { default?: Cache }).default ?? null)
|
|
630
|
+
: null;
|
|
631
|
+
|
|
632
|
+
if (cache) {
|
|
633
|
+
try {
|
|
634
|
+
const cached = await cache.match(cacheKey);
|
|
635
|
+
if (cached) {
|
|
636
|
+
const hit = new Response(cached.body, cached);
|
|
637
|
+
hit.headers.set("X-Cache", "HIT");
|
|
638
|
+
if (segment) hit.headers.set("X-Cache-Segment", hashSegment(segment));
|
|
639
|
+
if (cacheVersionEnv !== false) {
|
|
640
|
+
const v = (env[cacheVersionEnv] as string) || "";
|
|
641
|
+
if (v) hit.headers.set("X-Cache-Version", v);
|
|
642
|
+
}
|
|
643
|
+
return hit;
|
|
644
|
+
}
|
|
645
|
+
} catch {
|
|
646
|
+
// Cache API unavailable in this environment — proceed without cache
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Cache MISS — fetch from origin
|
|
651
|
+
const origin = await serverEntry.fetch(request, env, ctx);
|
|
652
|
+
|
|
653
|
+
if (origin.status !== 200) {
|
|
654
|
+
return origin;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Determine the right cache profile for this URL
|
|
658
|
+
const profile = getProfile(url);
|
|
659
|
+
const profileConfig = getCacheProfileConfig(profile);
|
|
660
|
+
|
|
661
|
+
// Don't cache non-public profiles
|
|
662
|
+
if (!profileConfig.isPublic || profileConfig.sMaxAge === 0) {
|
|
663
|
+
const resp = new Response(origin.body, origin);
|
|
664
|
+
resp.headers.set("Cache-Control", "private, no-cache, no-store, must-revalidate");
|
|
665
|
+
return resp;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const headers = cacheHeaders(profile);
|
|
669
|
+
|
|
670
|
+
const toReturn = new Response(origin.body, {
|
|
671
|
+
status: origin.status,
|
|
672
|
+
statusText: origin.statusText,
|
|
673
|
+
headers: new Headers(origin.headers),
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
// Apply profile-specific cache headers for the client response
|
|
677
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
678
|
+
toReturn.headers.set(k, v);
|
|
679
|
+
}
|
|
680
|
+
toReturn.headers.set("X-Cache", "MISS");
|
|
681
|
+
toReturn.headers.set("X-Cache-Profile", profile);
|
|
682
|
+
if (segment) toReturn.headers.set("X-Cache-Segment", hashSegment(segment));
|
|
683
|
+
if (cacheVersionEnv !== false) {
|
|
684
|
+
const v = (env[cacheVersionEnv] as string) || "";
|
|
685
|
+
if (v) toReturn.headers.set("X-Cache-Version", v);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// For Cache API storage, use sMaxAge as max-age since the Cache API
|
|
689
|
+
// ignores s-maxage and only respects max-age for TTL decisions.
|
|
690
|
+
if (cache) {
|
|
691
|
+
try {
|
|
692
|
+
const toStore = toReturn.clone();
|
|
693
|
+
toStore.headers.set("Cache-Control", `public, max-age=${profileConfig.sMaxAge}`);
|
|
694
|
+
ctx.waitUntil(cache.put(cacheKey, toStore));
|
|
695
|
+
} catch {
|
|
696
|
+
// Cache API unavailable — skip storing
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return toReturn;
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
}
|