@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,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Liveness and readiness probe handlers.
|
|
3
|
+
*
|
|
4
|
+
* - `/_liveness` — simple 200 OK for load balancers (K8s, Cloudflare)
|
|
5
|
+
* - `/deco/_health` — detailed JSON health metrics (delegated to healthMetrics)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { handleHealthCheck } from "./healthMetrics";
|
|
9
|
+
|
|
10
|
+
export function handleLiveness(request: Request): Response | null {
|
|
11
|
+
const url = new URL(request.url);
|
|
12
|
+
|
|
13
|
+
if (url.pathname === "/_liveness" || url.pathname === "/deco/_liveness") {
|
|
14
|
+
return new Response("OK", {
|
|
15
|
+
status: 200,
|
|
16
|
+
headers: { "Content-Type": "text/plain", "Cache-Control": "no-store" },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return handleHealthCheck(request);
|
|
21
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability utilities for deco middleware.
|
|
3
|
+
*
|
|
4
|
+
* Pluggable adapters for tracing (spans) and metrics (counters, gauges,
|
|
5
|
+
* histograms). Works with any backend: OpenTelemetry, Sentry, Datadog, etc.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { configureTracer, configureMeter } from "@decocms/start/middleware";
|
|
10
|
+
* import { trace, metrics } from "@opentelemetry/api";
|
|
11
|
+
*
|
|
12
|
+
* configureTracer({
|
|
13
|
+
* startSpan: (name, attrs) => {
|
|
14
|
+
* const span = trace.getTracer("deco").startSpan(name, { attributes: attrs });
|
|
15
|
+
* return {
|
|
16
|
+
* end: () => span.end(),
|
|
17
|
+
* setError: (e) => span.recordException(e),
|
|
18
|
+
* setAttribute: (k, v) => span.setAttribute(k, v),
|
|
19
|
+
* };
|
|
20
|
+
* },
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* configureMeter({
|
|
24
|
+
* counterInc: (name, value, labels) => metrics.getMeter("deco").createCounter(name).add(value, labels),
|
|
25
|
+
* histogramRecord: (name, value, labels) => metrics.getMeter("deco").createHistogram(name).record(value, labels),
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Tracer
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
import * as asyncHooks from "node:async_hooks";
|
|
35
|
+
|
|
36
|
+
export interface Span {
|
|
37
|
+
end(): void;
|
|
38
|
+
setError?(error: unknown): void;
|
|
39
|
+
setAttribute?(key: string, value: string | number | boolean): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TracerAdapter {
|
|
43
|
+
startSpan(name: string, attributes?: Record<string, string | number | boolean>): Span;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let tracer: TracerAdapter | null = null;
|
|
47
|
+
|
|
48
|
+
// Per-request active span stored in AsyncLocalStorage so concurrent requests
|
|
49
|
+
// cannot overwrite each other's span when `withTracing` awaits async work.
|
|
50
|
+
// The namespace import + runtime guard mirrors loader.ts to stay safe in client builds.
|
|
51
|
+
const ALS = (asyncHooks as any).AsyncLocalStorage as
|
|
52
|
+
| (new <T>() => { getStore(): T | undefined; run<R>(store: T, fn: () => R): R })
|
|
53
|
+
| undefined;
|
|
54
|
+
const spanStorage: {
|
|
55
|
+
getStore(): Span | null | undefined;
|
|
56
|
+
run<R>(store: Span | null, fn: () => R): R;
|
|
57
|
+
} = ALS ? new ALS<Span | null>() : { getStore: () => undefined, run: (_s: any, fn: any) => fn() };
|
|
58
|
+
|
|
59
|
+
export function configureTracer(t: TracerAdapter) {
|
|
60
|
+
tracer = t;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getTracer(): TracerAdapter | null {
|
|
64
|
+
return tracer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Get the currently active span for the current async context, if any. */
|
|
68
|
+
export function getActiveSpan(): Span | null {
|
|
69
|
+
return spanStorage.getStore() ?? null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Set an attribute on the active span, if one exists. */
|
|
73
|
+
export function setSpanAttribute(key: string, value: string | number | boolean) {
|
|
74
|
+
getActiveSpan()?.setAttribute?.(key, value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function withTracing<T>(
|
|
78
|
+
name: string,
|
|
79
|
+
fn: () => Promise<T>,
|
|
80
|
+
attributes?: Record<string, string | number | boolean>,
|
|
81
|
+
): Promise<T> {
|
|
82
|
+
if (!tracer) return fn();
|
|
83
|
+
|
|
84
|
+
const span = tracer.startSpan(name, attributes);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const result = await spanStorage.run(span, fn);
|
|
88
|
+
span.end();
|
|
89
|
+
return result;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
span.setError?.(error);
|
|
92
|
+
span.end();
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Meter
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
type Labels = Record<string, string | number | boolean>;
|
|
102
|
+
|
|
103
|
+
export interface MeterAdapter {
|
|
104
|
+
counterInc(name: string, value?: number, labels?: Labels): void;
|
|
105
|
+
gaugeSet?(name: string, value: number, labels?: Labels): void;
|
|
106
|
+
histogramRecord?(name: string, value: number, labels?: Labels): void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let meter: MeterAdapter | null = null;
|
|
110
|
+
|
|
111
|
+
export function configureMeter(m: MeterAdapter) {
|
|
112
|
+
meter = m;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getMeter(): MeterAdapter | null {
|
|
116
|
+
return meter;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Pre-defined metric names for consistency. */
|
|
120
|
+
export const MetricNames = {
|
|
121
|
+
HTTP_REQUESTS_TOTAL: "http_requests_total",
|
|
122
|
+
HTTP_REQUEST_DURATION_MS: "http_request_duration_ms",
|
|
123
|
+
HTTP_REQUEST_ERRORS: "http_request_errors_total",
|
|
124
|
+
CACHE_HIT: "cache_hit_total",
|
|
125
|
+
CACHE_MISS: "cache_miss_total",
|
|
126
|
+
RESOLVE_DURATION_MS: "resolve_duration_ms",
|
|
127
|
+
FETCH_DURATION_MS: "fetch_duration_ms",
|
|
128
|
+
} as const;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Record an HTTP request metric.
|
|
132
|
+
* Call in middleware after the response is produced.
|
|
133
|
+
*/
|
|
134
|
+
export function recordRequestMetric(
|
|
135
|
+
method: string,
|
|
136
|
+
path: string,
|
|
137
|
+
status: number,
|
|
138
|
+
durationMs: number,
|
|
139
|
+
) {
|
|
140
|
+
if (!meter) return;
|
|
141
|
+
const labels: Labels = { method, path: normalizePath(path), status };
|
|
142
|
+
meter.counterInc(MetricNames.HTTP_REQUESTS_TOTAL, 1, labels);
|
|
143
|
+
meter.histogramRecord?.(MetricNames.HTTP_REQUEST_DURATION_MS, durationMs, labels);
|
|
144
|
+
if (status >= 500) {
|
|
145
|
+
meter.counterInc(MetricNames.HTTP_REQUEST_ERRORS, 1, labels);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Record a cache hit/miss metric.
|
|
151
|
+
*/
|
|
152
|
+
export function recordCacheMetric(hit: boolean, profile?: string) {
|
|
153
|
+
if (!meter) return;
|
|
154
|
+
const labels: Labels = profile ? { profile } : {};
|
|
155
|
+
meter.counterInc(hit ? MetricNames.CACHE_HIT : MetricNames.CACHE_MISS, 1, labels);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalizePath(path: string): string {
|
|
159
|
+
// Collapse dynamic segments to reduce cardinality
|
|
160
|
+
return path
|
|
161
|
+
.replace(/\/[0-9a-f]{8,}/gi, "/:id")
|
|
162
|
+
.replace(/\/\d+/g, "/:id")
|
|
163
|
+
.replace(/\/[^/]+\/p$/, "/:slug/p");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Request logging
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
const isDev =
|
|
171
|
+
typeof globalThis.process !== "undefined" && globalThis.process.env?.NODE_ENV === "development";
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Structured request log entry.
|
|
175
|
+
* JSON in production, colorized in development.
|
|
176
|
+
* Includes traceId when available.
|
|
177
|
+
*/
|
|
178
|
+
export function logRequest(
|
|
179
|
+
request: Request,
|
|
180
|
+
status: number,
|
|
181
|
+
durationMs: number,
|
|
182
|
+
extra?: Record<string, unknown>,
|
|
183
|
+
) {
|
|
184
|
+
const url = new URL(request.url);
|
|
185
|
+
|
|
186
|
+
if (isDev) {
|
|
187
|
+
const color = status >= 500 ? "\x1b[31m" : status >= 400 ? "\x1b[33m" : "\x1b[32m";
|
|
188
|
+
const extraStr = extra ? ` ${JSON.stringify(extra)}` : "";
|
|
189
|
+
console.log(
|
|
190
|
+
`${color}${request.method}\x1b[0m ${url.pathname} ${status} ${durationMs.toFixed(0)}ms${extraStr}`,
|
|
191
|
+
);
|
|
192
|
+
} else {
|
|
193
|
+
console.log(
|
|
194
|
+
JSON.stringify({
|
|
195
|
+
level: status >= 500 ? "error" : "info",
|
|
196
|
+
method: request.method,
|
|
197
|
+
path: url.pathname,
|
|
198
|
+
status,
|
|
199
|
+
durationMs: Math.round(durationMs),
|
|
200
|
+
timestamp: new Date().toISOString(),
|
|
201
|
+
...extra,
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin Route Helpers
|
|
3
|
+
*
|
|
4
|
+
* Pre-built server handler configs for the Deco admin protocol routes.
|
|
5
|
+
* Sites spread these into their `createFileRoute` definitions to avoid
|
|
6
|
+
* repeating the same CORS + handler boilerplate.
|
|
7
|
+
*
|
|
8
|
+
* @example Site's `src/routes/deco/meta.ts`:
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createFileRoute } from "@tanstack/react-router";
|
|
11
|
+
* import { decoMetaRoute } from "@decocms/start/routes";
|
|
12
|
+
*
|
|
13
|
+
* export const Route = createFileRoute("/deco/meta")(decoMetaRoute);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import { corsHeaders } from "../admin/cors";
|
|
17
|
+
import { handleInvoke } from "../admin/invoke";
|
|
18
|
+
import { handleMeta } from "../admin/meta";
|
|
19
|
+
import { handleRender } from "../admin/render";
|
|
20
|
+
|
|
21
|
+
type HandlerFn = (ctx: { request: Request }) => Promise<Response> | Response;
|
|
22
|
+
|
|
23
|
+
function withCors(handler: HandlerFn): HandlerFn {
|
|
24
|
+
return async (ctx) => {
|
|
25
|
+
const response = await handler(ctx);
|
|
26
|
+
const headers = new Headers(response.headers);
|
|
27
|
+
for (const [k, v] of Object.entries(corsHeaders(ctx.request))) {
|
|
28
|
+
headers.set(k, v);
|
|
29
|
+
}
|
|
30
|
+
return new Response(response.body, {
|
|
31
|
+
status: response.status,
|
|
32
|
+
headers,
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function optionsHandler(ctx: { request: Request }): Response {
|
|
38
|
+
return new Response(null, {
|
|
39
|
+
status: 204,
|
|
40
|
+
headers: corsHeaders(ctx.request),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Route config for `/deco/meta` — serves JSON Schema + manifest.
|
|
46
|
+
* Spread into `createFileRoute("/deco/meta")({...})`.
|
|
47
|
+
*/
|
|
48
|
+
export const decoMetaRoute = {
|
|
49
|
+
server: {
|
|
50
|
+
handlers: {
|
|
51
|
+
GET: withCors(({ request }) => handleMeta(request)),
|
|
52
|
+
OPTIONS: optionsHandler,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Route config for `/deco/render` — section/page preview in iframe.
|
|
59
|
+
* Spread into `createFileRoute("/deco/render")({...})`.
|
|
60
|
+
*/
|
|
61
|
+
export const decoRenderRoute = {
|
|
62
|
+
server: {
|
|
63
|
+
handlers: {
|
|
64
|
+
GET: withCors(async ({ request }) => handleRender(request)),
|
|
65
|
+
POST: withCors(async ({ request }) => handleRender(request)),
|
|
66
|
+
OPTIONS: optionsHandler,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Route config for `/deco/invoke/$` — loader/action execution.
|
|
73
|
+
* Spread into `createFileRoute("/deco/invoke/$")({...})`.
|
|
74
|
+
*/
|
|
75
|
+
export const decoInvokeRoute = {
|
|
76
|
+
server: {
|
|
77
|
+
handlers: {
|
|
78
|
+
GET: withCors(async ({ request }) => handleInvoke(request)),
|
|
79
|
+
POST: withCors(async ({ request }) => handleInvoke(request)),
|
|
80
|
+
OPTIONS: optionsHandler,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMS Route Helpers
|
|
3
|
+
*
|
|
4
|
+
* Reusable building blocks for CMS catch-all and homepage routes.
|
|
5
|
+
* Since TanStack Router requires routes to be file-based in the site repo,
|
|
6
|
+
* we export helper functions and config that sites use in their route files.
|
|
7
|
+
*
|
|
8
|
+
* @example Site's `src/routes/$.tsx`:
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createFileRoute, notFound } from "@tanstack/react-router";
|
|
11
|
+
* import { loadCmsPage, cmsRouteConfig } from "@decocms/start/routes";
|
|
12
|
+
* import { DecoPageRenderer } from "@decocms/start/hooks";
|
|
13
|
+
*
|
|
14
|
+
* export const Route = createFileRoute("/$")({
|
|
15
|
+
* ...cmsRouteConfig({
|
|
16
|
+
* siteName: "My Store",
|
|
17
|
+
* defaultTitle: "My Store - Best products",
|
|
18
|
+
* ignoreSearchParams: ["skuId"],
|
|
19
|
+
* }),
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
25
|
+
import {
|
|
26
|
+
getCookies,
|
|
27
|
+
getRequest,
|
|
28
|
+
getRequestHeader,
|
|
29
|
+
getRequestUrl,
|
|
30
|
+
} from "@tanstack/react-start/server";
|
|
31
|
+
import { createElement } from "react";
|
|
32
|
+
import { preloadSectionComponents } from "../cms/registry";
|
|
33
|
+
import type { DeferredSection, MatcherContext, ResolvedSection } from "../cms/resolve";
|
|
34
|
+
import { resolveDecoPage, resolveDeferredSection } from "../cms/resolve";
|
|
35
|
+
import { runSectionLoaders, runSingleSectionLoader } from "../cms/sectionLoaders";
|
|
36
|
+
import {
|
|
37
|
+
type CacheProfile,
|
|
38
|
+
cacheHeaders,
|
|
39
|
+
detectCacheProfile,
|
|
40
|
+
routeCacheDefaults,
|
|
41
|
+
} from "../sdk/cacheHeaders";
|
|
42
|
+
|
|
43
|
+
const isServer = typeof document === "undefined";
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Server function — loads a CMS page, runs section loaders, detects cache
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
type PageResult = Awaited<ReturnType<typeof loadCmsPageInternal>>;
|
|
50
|
+
const pageInflight = new Map<string, Promise<PageResult>>();
|
|
51
|
+
|
|
52
|
+
async function loadCmsPageInternal(fullPath: string) {
|
|
53
|
+
const [basePath] = fullPath.split("?");
|
|
54
|
+
const serverUrl = getRequestUrl();
|
|
55
|
+
// Prefer the real server URL when available — it preserves duplicate query
|
|
56
|
+
// params (e.g. filter.category-1=a&filter.category-1=b) that the TanStack
|
|
57
|
+
// Router search object (plain Record<string,string>) would collapse.
|
|
58
|
+
const realUrlPath = serverUrl.pathname + serverUrl.search;
|
|
59
|
+
const urlWithSearch =
|
|
60
|
+
realUrlPath.startsWith(basePath) && serverUrl.search
|
|
61
|
+
? serverUrl.toString()
|
|
62
|
+
: fullPath.includes("?")
|
|
63
|
+
? new URL(fullPath, serverUrl.origin).toString()
|
|
64
|
+
: serverUrl.toString();
|
|
65
|
+
|
|
66
|
+
const matcherCtx: MatcherContext = {
|
|
67
|
+
userAgent: getRequestHeader("user-agent") ?? "",
|
|
68
|
+
url: urlWithSearch,
|
|
69
|
+
path: basePath,
|
|
70
|
+
cookies: getCookies(),
|
|
71
|
+
};
|
|
72
|
+
const page = await resolveDecoPage(basePath, matcherCtx);
|
|
73
|
+
if (!page) return null;
|
|
74
|
+
|
|
75
|
+
const request = new Request(urlWithSearch, {
|
|
76
|
+
headers: getRequest().headers,
|
|
77
|
+
});
|
|
78
|
+
const enrichedSections = await runSectionLoaders(page.resolvedSections, request);
|
|
79
|
+
|
|
80
|
+
// Pre-import eager section modules so their default exports are cached
|
|
81
|
+
// in resolvedComponents. This ensures SSR renders with direct component
|
|
82
|
+
// refs, and the client hydration can skip React.lazy/Suspense.
|
|
83
|
+
const eagerKeys = enrichedSections.map((s) => s.component);
|
|
84
|
+
await preloadSectionComponents(eagerKeys);
|
|
85
|
+
|
|
86
|
+
const cacheProfile = detectCacheProfile(basePath);
|
|
87
|
+
return {
|
|
88
|
+
...page,
|
|
89
|
+
resolvedSections: enrichedSections,
|
|
90
|
+
deferredSections: page.deferredSections,
|
|
91
|
+
cacheProfile,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const loadCmsPage = createServerFn({ method: "GET" })
|
|
96
|
+
.inputValidator((data: unknown) => data as string)
|
|
97
|
+
.handler(async (ctx) => {
|
|
98
|
+
const fullPath = ctx.data;
|
|
99
|
+
const [basePath] = fullPath.split("?");
|
|
100
|
+
|
|
101
|
+
const existing = pageInflight.get(basePath);
|
|
102
|
+
if (existing) return existing;
|
|
103
|
+
|
|
104
|
+
const promise = loadCmsPageInternal(fullPath).finally(() => pageInflight.delete(basePath));
|
|
105
|
+
pageInflight.set(basePath, promise);
|
|
106
|
+
return promise;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Same as loadCmsPage but hardcoded to "/" path.
|
|
111
|
+
* Avoids passing data through the server function for the homepage.
|
|
112
|
+
*/
|
|
113
|
+
export const loadCmsHomePage = createServerFn({ method: "GET" }).handler(async () => {
|
|
114
|
+
const matcherCtx: MatcherContext = {
|
|
115
|
+
userAgent: getRequestHeader("user-agent") ?? "",
|
|
116
|
+
url: getRequestUrl().toString(),
|
|
117
|
+
path: "/",
|
|
118
|
+
cookies: getCookies(),
|
|
119
|
+
};
|
|
120
|
+
const page = await resolveDecoPage("/", matcherCtx);
|
|
121
|
+
if (!page) return null;
|
|
122
|
+
|
|
123
|
+
const request = getRequest();
|
|
124
|
+
const enrichedSections = await runSectionLoaders(page.resolvedSections, request);
|
|
125
|
+
|
|
126
|
+
const eagerKeys = enrichedSections.map((s) => s.component);
|
|
127
|
+
await preloadSectionComponents(eagerKeys);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
...page,
|
|
131
|
+
resolvedSections: enrichedSections,
|
|
132
|
+
deferredSections: page.deferredSections,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Deferred section loader — resolves + enriches a single section on demand
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
export const loadDeferredSection = createServerFn({ method: "POST" })
|
|
141
|
+
.inputValidator(
|
|
142
|
+
(data: unknown) =>
|
|
143
|
+
data as { component: string; rawProps: Record<string, any>; pagePath: string },
|
|
144
|
+
)
|
|
145
|
+
.handler(async (ctx) => {
|
|
146
|
+
const { component, rawProps, pagePath } = ctx.data;
|
|
147
|
+
|
|
148
|
+
const matcherCtx: MatcherContext = {
|
|
149
|
+
userAgent: getRequestHeader("user-agent") ?? "",
|
|
150
|
+
url: getRequestUrl().toString(),
|
|
151
|
+
path: pagePath,
|
|
152
|
+
cookies: getCookies(),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const section = await resolveDeferredSection(component, rawProps, pagePath, matcherCtx);
|
|
156
|
+
if (!section) return null;
|
|
157
|
+
|
|
158
|
+
const request = new Request(getRequestUrl().toString(), {
|
|
159
|
+
headers: getRequest().headers,
|
|
160
|
+
});
|
|
161
|
+
const enriched = await runSingleSectionLoader(section, request);
|
|
162
|
+
return enriched;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Default pending component — shown during SPA navigation while loader runs
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
export function CmsPagePendingFallback() {
|
|
170
|
+
return createElement(
|
|
171
|
+
"div",
|
|
172
|
+
{ className: "w-full min-h-[60vh] flex flex-col gap-6 py-8" },
|
|
173
|
+
createElement("div", {
|
|
174
|
+
className: "skeleton animate-pulse w-full rounded",
|
|
175
|
+
style: { aspectRatio: "1440/400", minHeight: 200 },
|
|
176
|
+
}),
|
|
177
|
+
createElement(
|
|
178
|
+
"div",
|
|
179
|
+
{ className: "px-4 lg:px-8 flex flex-col gap-4" },
|
|
180
|
+
createElement("div", { className: "skeleton animate-pulse w-48 h-8 rounded" }),
|
|
181
|
+
createElement(
|
|
182
|
+
"div",
|
|
183
|
+
{ className: "grid grid-cols-2 lg:grid-cols-4 gap-4" },
|
|
184
|
+
...Array.from({ length: 4 }, (_, i) =>
|
|
185
|
+
createElement("div", {
|
|
186
|
+
key: i,
|
|
187
|
+
className: "skeleton animate-pulse w-full h-48 lg:h-64 rounded",
|
|
188
|
+
}),
|
|
189
|
+
),
|
|
190
|
+
),
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Route configuration factory
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
export interface CmsRouteOptions {
|
|
200
|
+
/** Site name used in page titles (e.g. "Espaço Smart"). */
|
|
201
|
+
siteName: string;
|
|
202
|
+
/** Default page title when CMS page has no name. */
|
|
203
|
+
defaultTitle: string;
|
|
204
|
+
/**
|
|
205
|
+
* Search params to exclude from loader deps.
|
|
206
|
+
* These params won't trigger a server re-fetch when they change.
|
|
207
|
+
* Defaults to `["skuId"]` — variant selection is client-side only.
|
|
208
|
+
*/
|
|
209
|
+
ignoreSearchParams?: string[];
|
|
210
|
+
/** Custom pending component shown during SPA navigation. */
|
|
211
|
+
pendingComponent?: () => any;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Returns a TanStack Router route config object for a CMS catch-all route.
|
|
216
|
+
* Spread the result into your `createFileRoute("/$")({...})` call.
|
|
217
|
+
*
|
|
218
|
+
* Includes: loaderDeps, loader, headers, head, staleTime/gcTime.
|
|
219
|
+
* Does NOT include: component, notFoundComponent (site provides these).
|
|
220
|
+
*/
|
|
221
|
+
export function cmsRouteConfig(options: CmsRouteOptions) {
|
|
222
|
+
const { siteName, defaultTitle, ignoreSearchParams = ["skuId"], pendingComponent } = options;
|
|
223
|
+
|
|
224
|
+
const ignoreSet = new Set(ignoreSearchParams);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
loaderDeps: ({ search }: { search: Record<string, string> }) => {
|
|
228
|
+
const filtered = Object.fromEntries(
|
|
229
|
+
Object.entries(search ?? {}).filter(([k]) => !ignoreSet.has(k)),
|
|
230
|
+
);
|
|
231
|
+
return {
|
|
232
|
+
search: Object.keys(filtered).length ? filtered : undefined,
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
loader: async ({
|
|
237
|
+
params,
|
|
238
|
+
deps,
|
|
239
|
+
}: {
|
|
240
|
+
params: { _splat?: string };
|
|
241
|
+
deps: { search?: Record<string, string> };
|
|
242
|
+
}) => {
|
|
243
|
+
const basePath = "/" + (params._splat || "");
|
|
244
|
+
const searchStr = deps.search
|
|
245
|
+
? "?" + new URLSearchParams(deps.search as Record<string, string>).toString()
|
|
246
|
+
: "";
|
|
247
|
+
const page = await loadCmsPage({ data: basePath + searchStr });
|
|
248
|
+
|
|
249
|
+
// On the client (SPA navigation or initial hydration), pre-import
|
|
250
|
+
// eager section modules BEFORE React renders. This ensures
|
|
251
|
+
// getResolvedComponent() returns a value and we skip React.lazy.
|
|
252
|
+
if (!isServer && page?.resolvedSections) {
|
|
253
|
+
const keys = page.resolvedSections.map((s: ResolvedSection) => s.component);
|
|
254
|
+
await preloadSectionComponents(keys);
|
|
255
|
+
}
|
|
256
|
+
return page;
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
...(pendingComponent ? { pendingComponent } : {}),
|
|
260
|
+
|
|
261
|
+
...routeCacheDefaults("product"),
|
|
262
|
+
|
|
263
|
+
headers: ({ loaderData }: { loaderData?: { cacheProfile?: CacheProfile } }) => {
|
|
264
|
+
const profile = loaderData?.cacheProfile ?? "listing";
|
|
265
|
+
return cacheHeaders(profile);
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
head: ({ loaderData }: { loaderData?: { name?: string } }) => ({
|
|
269
|
+
meta: [
|
|
270
|
+
{
|
|
271
|
+
title: loaderData?.name ? `${loaderData.name} | ${siteName}` : defaultTitle,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
}),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Returns a TanStack Router route config for the CMS homepage route.
|
|
280
|
+
* Spread into `createFileRoute("/")({...})`.
|
|
281
|
+
*/
|
|
282
|
+
export function cmsHomeRouteConfig(options: {
|
|
283
|
+
defaultTitle: string;
|
|
284
|
+
pendingComponent?: () => any;
|
|
285
|
+
}) {
|
|
286
|
+
return {
|
|
287
|
+
loader: async () => {
|
|
288
|
+
const page = await loadCmsHomePage();
|
|
289
|
+
if (!isServer && page?.resolvedSections) {
|
|
290
|
+
const keys = page.resolvedSections.map((s: ResolvedSection) => s.component);
|
|
291
|
+
await preloadSectionComponents(keys);
|
|
292
|
+
}
|
|
293
|
+
return page;
|
|
294
|
+
},
|
|
295
|
+
...(options.pendingComponent ? { pendingComponent: options.pendingComponent } : {}),
|
|
296
|
+
...routeCacheDefaults("static"),
|
|
297
|
+
headers: () => cacheHeaders("static"),
|
|
298
|
+
head: () => ({
|
|
299
|
+
meta: [{ title: options.defaultTitle }],
|
|
300
|
+
}),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router";
|
|
2
|
+
import type { ResolvedSection } from "../cms/resolve";
|
|
3
|
+
import { DecoPageRenderer } from "../hooks/DecoPageRenderer";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default CMS page component. Renders all resolved sections.
|
|
7
|
+
* Sites can use this directly or build their own.
|
|
8
|
+
*/
|
|
9
|
+
export function CmsPage({ sections }: { sections: ResolvedSection[] }) {
|
|
10
|
+
return (
|
|
11
|
+
<div>
|
|
12
|
+
<DecoPageRenderer sections={sections} />
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Default 404 page for CMS routes.
|
|
19
|
+
* Sites can override with their own branded version.
|
|
20
|
+
*/
|
|
21
|
+
export function NotFoundPage() {
|
|
22
|
+
return (
|
|
23
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
24
|
+
<div className="text-center">
|
|
25
|
+
<h1 className="text-6xl font-bold text-base-content/20 mb-4">404</h1>
|
|
26
|
+
<h2 className="text-2xl font-bold mb-2">Page Not Found</h2>
|
|
27
|
+
<p className="text-base-content/60 mb-6">No CMS page block matches this URL.</p>
|
|
28
|
+
<Link to="/" className="btn btn-primary">
|
|
29
|
+
Go Home
|
|
30
|
+
</Link>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
decoInvokeRoute,
|
|
3
|
+
decoMetaRoute,
|
|
4
|
+
decoRenderRoute,
|
|
5
|
+
} from "./adminRoutes";
|
|
6
|
+
export {
|
|
7
|
+
CmsPagePendingFallback,
|
|
8
|
+
type CmsRouteOptions,
|
|
9
|
+
cmsHomeRouteConfig,
|
|
10
|
+
cmsRouteConfig,
|
|
11
|
+
loadCmsHomePage,
|
|
12
|
+
loadCmsPage,
|
|
13
|
+
loadDeferredSection,
|
|
14
|
+
} from "./cmsRoute";
|
|
15
|
+
export { CmsPage, NotFoundPage } from "./components";
|