@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
package/src/sdk/env.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized environment detection for @decocms/start.
|
|
3
|
+
*
|
|
4
|
+
* Works in Cloudflare Workers (wrangler dev), Node, and Vite SSR.
|
|
5
|
+
* Evaluates lazily on first call so it picks up env vars set after module load.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
let _isDev: boolean | null = null;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns `true` when running in a development environment.
|
|
12
|
+
*
|
|
13
|
+
* Detection order:
|
|
14
|
+
* 1. `DECO_CACHE_DISABLE=true` — explicit opt-in (always wins)
|
|
15
|
+
* 2. `NODE_ENV=development` — standard Node/Vite convention
|
|
16
|
+
*
|
|
17
|
+
* The result is memoised after the first evaluation.
|
|
18
|
+
*/
|
|
19
|
+
export function isDevMode(): boolean {
|
|
20
|
+
if (_isDev !== null) return _isDev;
|
|
21
|
+
|
|
22
|
+
const env = typeof globalThis.process !== "undefined" ? globalThis.process.env : undefined;
|
|
23
|
+
|
|
24
|
+
_isDev = env?.DECO_CACHE_DISABLE === "true" || env?.NODE_ENV === "development";
|
|
25
|
+
|
|
26
|
+
return _isDev;
|
|
27
|
+
}
|
package/src/sdk/index.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export { ANALYTICS_SCRIPT, type DataEventParams, gtmScript, useSendEvent } from "./analytics";
|
|
2
|
+
export {
|
|
3
|
+
type CachedLoaderOptions,
|
|
4
|
+
type CachePolicy,
|
|
5
|
+
clearLoaderCache,
|
|
6
|
+
createCachedLoader,
|
|
7
|
+
getLoaderCacheStats,
|
|
8
|
+
} from "./cachedLoader";
|
|
9
|
+
export {
|
|
10
|
+
type CacheHeadersConfig,
|
|
11
|
+
type CacheProfile,
|
|
12
|
+
cacheHeaders,
|
|
13
|
+
detectCacheProfile,
|
|
14
|
+
getCacheProfileConfig,
|
|
15
|
+
registerCachePattern,
|
|
16
|
+
routeCacheDefaults,
|
|
17
|
+
} from "./cacheHeaders";
|
|
18
|
+
export { clx } from "./clx";
|
|
19
|
+
export { decodeCookie, deleteCookie, getCookie, getServerSideCookie, setCookie } from "./cookie";
|
|
20
|
+
export { buildCSPHeaderValue, type CSPOptions, setCSPHeaders } from "./csp";
|
|
21
|
+
export { isDevMode } from "./env";
|
|
22
|
+
export {
|
|
23
|
+
createInstrumentedFetch,
|
|
24
|
+
type FetchInstrumentationOptions,
|
|
25
|
+
type FetchMetrics,
|
|
26
|
+
instrumentFetch,
|
|
27
|
+
} from "./instrumentedFetch";
|
|
28
|
+
export { batchInvoke, createInvokeProxy, type InvokeProxy, invokeQueryOptions } from "./invoke";
|
|
29
|
+
export { createCacheControlCollector, mergeCacheControl } from "./mergeCacheControl";
|
|
30
|
+
export {
|
|
31
|
+
addRedirects,
|
|
32
|
+
loadRedirects,
|
|
33
|
+
matchRedirect,
|
|
34
|
+
parseRedirectsCsv,
|
|
35
|
+
type Redirect,
|
|
36
|
+
type RedirectMap,
|
|
37
|
+
} from "./redirects";
|
|
38
|
+
export { RequestContext, type RequestContextData } from "./requestContext";
|
|
39
|
+
export { createServerTimings, type ServerTimings } from "./serverTimings";
|
|
40
|
+
export { type ReactiveSignal, signal } from "./signal";
|
|
41
|
+
export {
|
|
42
|
+
canonicalUrl,
|
|
43
|
+
cleanPathForCacheKey,
|
|
44
|
+
hasTrackingParams,
|
|
45
|
+
stripTrackingParams,
|
|
46
|
+
} from "./urlUtils";
|
|
47
|
+
export {
|
|
48
|
+
checkDesktop,
|
|
49
|
+
checkMobile,
|
|
50
|
+
checkTablet,
|
|
51
|
+
type Device,
|
|
52
|
+
detectDevice,
|
|
53
|
+
useDevice,
|
|
54
|
+
} from "./useDevice";
|
|
55
|
+
export { useId } from "./useId";
|
|
56
|
+
export { usePartialSection, useScript, useScriptAsDataURI, useSection } from "./useScript";
|
|
57
|
+
export { createDecoWorkerEntry, type DecoWorkerEntryOptions } from "./workerEntry";
|
|
58
|
+
export {
|
|
59
|
+
isWrappedError,
|
|
60
|
+
unwrapError,
|
|
61
|
+
type WrappedError,
|
|
62
|
+
wrapCaughtErrors,
|
|
63
|
+
} from "./wrapCaughtErrors";
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instrumented fetch wrapper that adds logging and tracing to outbound HTTP calls.
|
|
3
|
+
*
|
|
4
|
+
* Designed to be wired into commerce clients (VTEX, Shopify) so all
|
|
5
|
+
* API calls become visible in dev logs and production traces.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { createInstrumentedFetch } from "@decocms/start/sdk/instrumentedFetch";
|
|
10
|
+
*
|
|
11
|
+
* const vtexFetch = createInstrumentedFetch("vtex");
|
|
12
|
+
*
|
|
13
|
+
* // Use it instead of global fetch:
|
|
14
|
+
* const response = await vtexFetch("https://account.vtexcommercestable.com.br/api/...");
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { getTracer } from "../middleware/observability";
|
|
19
|
+
|
|
20
|
+
export interface FetchInstrumentationOptions {
|
|
21
|
+
/** Tag for log/trace grouping (e.g., "vtex", "shopify"). */
|
|
22
|
+
name: string;
|
|
23
|
+
/** Enable request/response logging. Default: true in development. */
|
|
24
|
+
logging?: boolean;
|
|
25
|
+
/** Enable tracing via the configured TracerAdapter. Default: true. */
|
|
26
|
+
tracing?: boolean;
|
|
27
|
+
/** Callback when a request completes (for custom metrics). */
|
|
28
|
+
onComplete?: (info: FetchMetrics) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Underlying fetch implementation to wrap. Defaults to `globalThis.fetch`.
|
|
31
|
+
* Use this when the client already has a custom fetch (e.g. with cookies,
|
|
32
|
+
* custom headers, or a proxy) that must be preserved.
|
|
33
|
+
*/
|
|
34
|
+
baseFetch?: typeof fetch;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface FetchMetrics {
|
|
38
|
+
name: string;
|
|
39
|
+
url: string;
|
|
40
|
+
method: string;
|
|
41
|
+
status: number;
|
|
42
|
+
durationMs: number;
|
|
43
|
+
cached: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const isDev =
|
|
47
|
+
typeof globalThis.process !== "undefined" && globalThis.process.env?.NODE_ENV === "development";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates a fetch wrapper that instruments all requests for a given integration.
|
|
51
|
+
*/
|
|
52
|
+
export function createInstrumentedFetch(
|
|
53
|
+
nameOrOptions: string | FetchInstrumentationOptions,
|
|
54
|
+
): typeof fetch {
|
|
55
|
+
const options: FetchInstrumentationOptions =
|
|
56
|
+
typeof nameOrOptions === "string" ? { name: nameOrOptions } : nameOrOptions;
|
|
57
|
+
|
|
58
|
+
const {
|
|
59
|
+
name,
|
|
60
|
+
logging = isDev,
|
|
61
|
+
tracing = true,
|
|
62
|
+
onComplete,
|
|
63
|
+
baseFetch = globalThis.fetch,
|
|
64
|
+
} = options;
|
|
65
|
+
|
|
66
|
+
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
67
|
+
const url =
|
|
68
|
+
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
69
|
+
const method = init?.method || "GET";
|
|
70
|
+
const startTime = performance.now();
|
|
71
|
+
|
|
72
|
+
const doFetch = async (): Promise<Response> => {
|
|
73
|
+
if (logging) {
|
|
74
|
+
console.log(`[${name}] ${method} ${truncateUrl(url)}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const response = await baseFetch(input, init);
|
|
78
|
+
const durationMs = performance.now() - startTime;
|
|
79
|
+
const cached = response.headers.get("x-cache") === "HIT";
|
|
80
|
+
|
|
81
|
+
if (logging) {
|
|
82
|
+
const color = response.ok ? "\x1b[32m" : "\x1b[31m";
|
|
83
|
+
console.log(
|
|
84
|
+
`[${name}] ${color}${response.status}\x1b[0m ${method} ${truncateUrl(url)} ${durationMs.toFixed(0)}ms${cached ? " (cached)" : ""}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onComplete?.({
|
|
89
|
+
name,
|
|
90
|
+
url,
|
|
91
|
+
method,
|
|
92
|
+
status: response.status,
|
|
93
|
+
durationMs,
|
|
94
|
+
cached,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return response;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (tracing) {
|
|
101
|
+
const tracer = getTracer();
|
|
102
|
+
if (tracer) {
|
|
103
|
+
const span = tracer.startSpan(`${name}.fetch`, {
|
|
104
|
+
"http.method": method,
|
|
105
|
+
"http.url": url,
|
|
106
|
+
"fetch.integration": name,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const response = await doFetch();
|
|
111
|
+
span.end();
|
|
112
|
+
return response;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
span.setError?.(error);
|
|
115
|
+
span.end();
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return doFetch();
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function truncateUrl(url: string, maxLen = 120): string {
|
|
126
|
+
if (url.length <= maxLen) return url;
|
|
127
|
+
return url.slice(0, maxLen - 3) + "...";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Wraps an existing fetch function with logging and tracing instrumentation.
|
|
132
|
+
* Unlike `createInstrumentedFetch`, this preserves the original fetch's
|
|
133
|
+
* behavior (custom headers, cookies, proxy logic) and adds observability on top.
|
|
134
|
+
*/
|
|
135
|
+
export function instrumentFetch(originalFetch: typeof fetch, name: string): typeof fetch {
|
|
136
|
+
return createInstrumentedFetch({ name, baseFetch: originalFetch });
|
|
137
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed invoke proxy for client-side RPC to deco loaders/actions.
|
|
3
|
+
*
|
|
4
|
+
* Creates a Proxy object that maps loader keys to `POST /deco/invoke/:key` calls,
|
|
5
|
+
* providing a type-safe, ergonomic API for client-side data fetching.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* // Define your loader registry type
|
|
10
|
+
* type Loaders = {
|
|
11
|
+
* "vtex/loaders/intelligentSearch/productList.ts": (props: { query: string }) => Promise<Product[]>;
|
|
12
|
+
* "vtex/loaders/intelligentSearch/productDetailsPage.ts": (props: { slug: string }) => Promise<ProductDetailsPage>;
|
|
13
|
+
* };
|
|
14
|
+
*
|
|
15
|
+
* const invoke = createInvokeProxy<Loaders>("/deco/invoke");
|
|
16
|
+
*
|
|
17
|
+
* // Type-safe calls:
|
|
18
|
+
* const products = await invoke["vtex/loaders/intelligentSearch/productList.ts"]({ query: "shoes" });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export type InvokeProxy<TLoaders extends Record<string, (props: any) => Promise<any>>> = {
|
|
23
|
+
[K in keyof TLoaders]: TLoaders[K] extends (props: infer P) => Promise<infer R>
|
|
24
|
+
? (props: P) => Promise<R>
|
|
25
|
+
: never;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a proxy that turns loader key access into fetch calls to `/deco/invoke/:key`.
|
|
30
|
+
*/
|
|
31
|
+
export function createInvokeProxy<TLoaders extends Record<string, (props: any) => Promise<any>>>(
|
|
32
|
+
basePath = "/deco/invoke",
|
|
33
|
+
): InvokeProxy<TLoaders> {
|
|
34
|
+
return new Proxy({} as InvokeProxy<TLoaders>, {
|
|
35
|
+
get(_target, prop: string) {
|
|
36
|
+
return async (props: unknown) => {
|
|
37
|
+
const url = `${basePath}/${encodeURIComponent(prop)}`;
|
|
38
|
+
const response = await fetch(url, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { "Content-Type": "application/json" },
|
|
41
|
+
body: JSON.stringify(props ?? {}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const error = await response.json().catch(() => ({ error: response.statusText }));
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Invoke ${prop} failed (${response.status}): ${(error as any).error || response.statusText}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return response.json();
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Batch invoke multiple loaders in a single request.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* const results = await batchInvoke("/deco/invoke", {
|
|
63
|
+
* products: { __resolveType: "vtex/loaders/productList.ts", query: "shoes" },
|
|
64
|
+
* details: { __resolveType: "vtex/loaders/productDetailsPage.ts", slug: "shoe-1" },
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export async function batchInvoke<T extends Record<string, unknown>>(
|
|
69
|
+
basePath: string,
|
|
70
|
+
payloads: T,
|
|
71
|
+
): Promise<{ [K in keyof T]: unknown }> {
|
|
72
|
+
const response = await fetch(basePath, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "Content-Type": "application/json" },
|
|
75
|
+
body: JSON.stringify(payloads),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error(`Batch invoke failed (${response.status})`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return response.json();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Helper to create TanStack Query `queryOptions` for an invoke call.
|
|
87
|
+
* The storefront must have `@tanstack/react-query` installed.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* import { useQuery } from "@tanstack/react-query";
|
|
92
|
+
* import { invokeQueryOptions } from "@decocms/start/sdk/invoke";
|
|
93
|
+
*
|
|
94
|
+
* const options = invokeQueryOptions(
|
|
95
|
+
* "vtex/loaders/productList.ts",
|
|
96
|
+
* { query: "shoes" },
|
|
97
|
+
* { staleTime: 60_000 }
|
|
98
|
+
* );
|
|
99
|
+
*
|
|
100
|
+
* // In a component:
|
|
101
|
+
* const { data } = useQuery(options);
|
|
102
|
+
*
|
|
103
|
+
* // In a route loader:
|
|
104
|
+
* queryClient.ensureQueryData(options);
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export function invokeQueryOptions<TResult = unknown>(
|
|
108
|
+
key: string,
|
|
109
|
+
props: unknown,
|
|
110
|
+
options?: { staleTime?: number; gcTime?: number; basePath?: string },
|
|
111
|
+
) {
|
|
112
|
+
const basePath = options?.basePath ?? "/deco/invoke";
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
queryKey: ["deco-invoke", key, props] as const,
|
|
116
|
+
queryFn: async (): Promise<TResult> => {
|
|
117
|
+
const url = `${basePath}/${encodeURIComponent(key)}`;
|
|
118
|
+
const response = await fetch(url, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: { "Content-Type": "application/json" },
|
|
121
|
+
body: JSON.stringify(props ?? {}),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw new Error(`Invoke ${key} failed (${response.status})`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return response.json();
|
|
129
|
+
},
|
|
130
|
+
staleTime: options?.staleTime,
|
|
131
|
+
gcTime: options?.gcTime,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache-Control merge utility.
|
|
3
|
+
*
|
|
4
|
+
* When a page makes multiple backend calls with different cache lifetimes,
|
|
5
|
+
* the final page response must use the most restrictive (shortest) cache
|
|
6
|
+
* values. This utility merges multiple Cache-Control headers following
|
|
7
|
+
* the "most restrictive wins" strategy.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { mergeCacheControl } from "@decocms/start/sdk/mergeCacheControl";
|
|
12
|
+
*
|
|
13
|
+
* // Product loader returns 60s, mega menu returns 3600s
|
|
14
|
+
* const merged = mergeCacheControl([
|
|
15
|
+
* "public, s-maxage=60, stale-while-revalidate=300",
|
|
16
|
+
* "public, s-maxage=3600, stale-while-revalidate=86400",
|
|
17
|
+
* ]);
|
|
18
|
+
* // => "public, s-maxage=60, stale-while-revalidate=300"
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
interface ParsedCacheControl {
|
|
23
|
+
isPublic: boolean;
|
|
24
|
+
isPrivate: boolean;
|
|
25
|
+
noCache: boolean;
|
|
26
|
+
noStore: boolean;
|
|
27
|
+
maxAge?: number;
|
|
28
|
+
sMaxAge?: number;
|
|
29
|
+
staleWhileRevalidate?: number;
|
|
30
|
+
staleIfError?: number;
|
|
31
|
+
mustRevalidate: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function safeParseInt(value: string | undefined): number | undefined {
|
|
35
|
+
if (!value) return undefined;
|
|
36
|
+
const n = parseInt(value, 10);
|
|
37
|
+
return Number.isFinite(n) ? n : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parse(header: string): ParsedCacheControl {
|
|
41
|
+
const directives = header.split(",").map((d) => d.trim().toLowerCase());
|
|
42
|
+
|
|
43
|
+
const result: ParsedCacheControl = {
|
|
44
|
+
isPublic: false,
|
|
45
|
+
isPrivate: false,
|
|
46
|
+
noCache: false,
|
|
47
|
+
noStore: false,
|
|
48
|
+
mustRevalidate: false,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
for (const directive of directives) {
|
|
52
|
+
if (directive === "public") result.isPublic = true;
|
|
53
|
+
else if (directive === "private") result.isPrivate = true;
|
|
54
|
+
else if (directive === "no-cache") result.noCache = true;
|
|
55
|
+
else if (directive === "no-store") result.noStore = true;
|
|
56
|
+
else if (directive === "must-revalidate") result.mustRevalidate = true;
|
|
57
|
+
else if (directive.startsWith("max-age=")) {
|
|
58
|
+
result.maxAge = safeParseInt(directive.split("=")[1]);
|
|
59
|
+
} else if (directive.startsWith("s-maxage=")) {
|
|
60
|
+
result.sMaxAge = safeParseInt(directive.split("=")[1]);
|
|
61
|
+
} else if (directive.startsWith("stale-while-revalidate=")) {
|
|
62
|
+
result.staleWhileRevalidate = safeParseInt(directive.split("=")[1]);
|
|
63
|
+
} else if (directive.startsWith("stale-if-error=")) {
|
|
64
|
+
result.staleIfError = safeParseInt(directive.split("=")[1]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function minDefined(...values: (number | undefined)[]): number | undefined {
|
|
72
|
+
const defined = values.filter((v): v is number => v != null);
|
|
73
|
+
return defined.length > 0 ? Math.min(...defined) : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Merge multiple Cache-Control headers using "most restrictive wins".
|
|
78
|
+
*
|
|
79
|
+
* - If any header is `private`, the result is `private`
|
|
80
|
+
* - If any header has `no-store`, the result has `no-store`
|
|
81
|
+
* - Numeric values (max-age, s-maxage, swr) use the minimum
|
|
82
|
+
*/
|
|
83
|
+
export function mergeCacheControl(headers: string[]): string {
|
|
84
|
+
if (headers.length === 0) return "public, s-maxage=0";
|
|
85
|
+
if (headers.length === 1) return headers[0];
|
|
86
|
+
|
|
87
|
+
const parsed = headers.map(parse);
|
|
88
|
+
|
|
89
|
+
const anyPrivate = parsed.some((p) => p.isPrivate);
|
|
90
|
+
const anyNoStore = parsed.some((p) => p.noStore);
|
|
91
|
+
const anyNoCache = parsed.some((p) => p.noCache);
|
|
92
|
+
const anyMustRevalidate = parsed.some((p) => p.mustRevalidate);
|
|
93
|
+
|
|
94
|
+
if (anyNoStore) {
|
|
95
|
+
return "private, no-cache, no-store, must-revalidate";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (anyPrivate) {
|
|
99
|
+
const maxAge = minDefined(...parsed.map((p) => p.maxAge));
|
|
100
|
+
const parts = ["private"];
|
|
101
|
+
if (anyNoCache) parts.push("no-cache");
|
|
102
|
+
if (maxAge != null) parts.push(`max-age=${maxAge}`);
|
|
103
|
+
if (anyMustRevalidate) parts.push("must-revalidate");
|
|
104
|
+
return parts.join(", ");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const maxAge = minDefined(...parsed.map((p) => p.maxAge));
|
|
108
|
+
const sMaxAge = minDefined(...parsed.map((p) => p.sMaxAge));
|
|
109
|
+
const swr = minDefined(...parsed.map((p) => p.staleWhileRevalidate));
|
|
110
|
+
const sie = minDefined(...parsed.map((p) => p.staleIfError));
|
|
111
|
+
|
|
112
|
+
const parts: string[] = ["public"];
|
|
113
|
+
if (maxAge != null) parts.push(`max-age=${maxAge}`);
|
|
114
|
+
if (sMaxAge != null) parts.push(`s-maxage=${sMaxAge}`);
|
|
115
|
+
if (swr != null) parts.push(`stale-while-revalidate=${swr}`);
|
|
116
|
+
if (sie != null) parts.push(`stale-if-error=${sie}`);
|
|
117
|
+
if (anyMustRevalidate) parts.push("must-revalidate");
|
|
118
|
+
|
|
119
|
+
return parts.join(", ");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Accumulator for collecting cache control headers across loaders.
|
|
124
|
+
*
|
|
125
|
+
* Use in middleware to collect headers from each loader call and
|
|
126
|
+
* compute the final merged header at the end.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* const collector = createCacheControlCollector();
|
|
131
|
+
* collector.add("public, s-maxage=60");
|
|
132
|
+
* collector.add("public, s-maxage=3600");
|
|
133
|
+
* response.headers.set("Cache-Control", collector.result());
|
|
134
|
+
* // => "public, s-maxage=60"
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export function createCacheControlCollector() {
|
|
138
|
+
const headers: string[] = [];
|
|
139
|
+
return {
|
|
140
|
+
add(header: string) {
|
|
141
|
+
headers.push(header);
|
|
142
|
+
},
|
|
143
|
+
result(): string {
|
|
144
|
+
return mergeCacheControl(headers);
|
|
145
|
+
},
|
|
146
|
+
get count() {
|
|
147
|
+
return headers.length;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|