@decocms/start 2.28.1 → 2.29.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/README.md +178 -79
- package/package.json +16 -14
- package/src/cms/resolve.ts +81 -63
- package/src/cms/sectionLoaders.ts +11 -0
- package/src/index.ts +3 -0
- package/src/sdk/cachedLoader.ts +36 -13
- package/src/sdk/composite.test.ts +121 -0
- package/src/sdk/composite.ts +114 -0
- package/src/sdk/instrumentedFetch.ts +56 -0
- package/src/sdk/logger.test.ts +135 -0
- package/src/sdk/logger.ts +166 -0
- package/src/sdk/observability.ts +75 -0
- package/src/sdk/otel.test.ts +59 -0
- package/src/sdk/otel.ts +270 -29
- package/src/sdk/otelAdapters.test.ts +135 -0
- package/src/sdk/otelAdapters.ts +401 -0
- package/src/sdk/sampler.test.ts +127 -0
- package/src/sdk/sampler.ts +183 -0
- package/src/sdk/workerEntry.ts +541 -476
- package/src/vite/plugin.js +6 -3
package/README.md
CHANGED
|
@@ -1,124 +1,219 @@
|
|
|
1
|
-
# @decocms/start
|
|
1
|
+
# @decocms/start
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@decocms/start)
|
|
4
4
|
[](https://github.com/decocms/deco-start/blob/main/LICENSE)
|
|
5
5
|
|
|
6
|
-
Framework layer for [
|
|
6
|
+
Framework layer for [deco.cx](https://deco.cx) storefronts on **TanStack Start + React 19 + Cloudflare Workers**.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
`@decocms/start` is the npm package that storefronts depend on. It provides the CMS bridge, admin protocol, section registry, schema generation, edge caching, the Vite plugin, and a small SDK. It is **not** itself a storefront — it is what storefronts build on top of.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
📖 **[Read the full documentation →](https://docs.deco.cx/v2/en/getting-started/overview)**
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What's in the box
|
|
11
15
|
|
|
12
|
-
```
|
|
13
|
-
|
|
16
|
+
```
|
|
17
|
+
┌─────────────────────────────────────────────────┐
|
|
18
|
+
│ Site repo (your storefront) │ ← Components, sections, routes
|
|
19
|
+
├─────────────────────────────────────────────────┤
|
|
20
|
+
│ @decocms/apps (commerce integrations) │ ← VTEX, Shopify, Resend
|
|
21
|
+
├─────────────────────────────────────────────────┤
|
|
22
|
+
│ @decocms/start (framework — this package) │ ← CMS bridge, admin, caching
|
|
23
|
+
└─────────────────────────────────────────────────┘
|
|
24
|
+
↓ runs on ↓
|
|
25
|
+
TanStack Start + React 19 + Cloudflare Workers
|
|
14
26
|
```
|
|
15
27
|
|
|
16
|
-
|
|
28
|
+
`@decocms/start` exports cover four surfaces:
|
|
29
|
+
|
|
30
|
+
- **Worker entry** — `createDecoWorkerEntry` wraps your Cloudflare Worker with admin routes, edge cache, and asset bypass.
|
|
31
|
+
- **CMS bridge** — `loadCmsPage`, `resolveDecoPage`, `registerSectionLoaders`, `registerLayoutSections`.
|
|
32
|
+
- **Admin protocol** — `handleMeta`, `handleDecofile`, `handleRender`, `handleInvoke`.
|
|
33
|
+
- **SDK** — `createCachedLoader`, `createInstrumentedFetch`, `createInvoke`, `decoVitePlugin`, plus utilities (cookies, redirects, sitemap, A/B testing).
|
|
34
|
+
|
|
35
|
+
Full export reference: [docs.deco.cx/v2/en/reference/package-exports](https://docs.deco.cx/v2/en/reference/package-exports).
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Hello, World
|
|
40
|
+
|
|
41
|
+
A minimal v2 storefront has six files. Here they are.
|
|
42
|
+
|
|
43
|
+
### `package.json`
|
|
44
|
+
|
|
45
|
+
```jsonc
|
|
46
|
+
{
|
|
47
|
+
"name": "my-store",
|
|
48
|
+
"type": "module",
|
|
49
|
+
"scripts": {
|
|
50
|
+
"dev": "vite dev",
|
|
51
|
+
"build": "vite build",
|
|
52
|
+
"deploy": "wrangler deploy"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@decocms/start": "^2.28.0",
|
|
56
|
+
"@decocms/apps": "^1.11.0",
|
|
57
|
+
"@tanstack/react-start": "^1.166.0",
|
|
58
|
+
"react": "^19.0.0",
|
|
59
|
+
"react-dom": "^19.0.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"vite": "^6.0.0",
|
|
63
|
+
"wrangler": "^4.72.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
17
67
|
|
|
68
|
+
### `vite.config.ts`
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { defineConfig } from "vite";
|
|
72
|
+
import { cloudflare } from "@cloudflare/vite-plugin";
|
|
73
|
+
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
74
|
+
import react from "@vitejs/plugin-react";
|
|
75
|
+
import decoVitePlugin from "@decocms/start/vite";
|
|
76
|
+
|
|
77
|
+
export default defineConfig({
|
|
78
|
+
plugins: [
|
|
79
|
+
cloudflare({ viteEnvironment: { name: "ssr" } }),
|
|
80
|
+
tanstackStart({ server: { entry: "server" } }),
|
|
81
|
+
react({ babel: { plugins: ["babel-plugin-react-compiler"] } }),
|
|
82
|
+
decoVitePlugin(),
|
|
83
|
+
],
|
|
84
|
+
resolve: {
|
|
85
|
+
alias: { "~": "/src" },
|
|
86
|
+
deduplicate: ["react", "react-dom", "@decocms/start", "@decocms/apps"],
|
|
87
|
+
},
|
|
88
|
+
});
|
|
18
89
|
```
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
90
|
+
|
|
91
|
+
### `wrangler.jsonc`
|
|
92
|
+
|
|
93
|
+
```jsonc
|
|
94
|
+
{
|
|
95
|
+
"name": "my-store",
|
|
96
|
+
"main": "./src/worker-entry.ts",
|
|
97
|
+
"compatibility_date": "2026-02-14",
|
|
98
|
+
"compatibility_flags": [
|
|
99
|
+
"nodejs_compat",
|
|
100
|
+
"no_handle_cross_request_promise_resolution"
|
|
101
|
+
],
|
|
102
|
+
"assets": { "directory": "./dist/client" }
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `src/setup.ts`
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { createSiteSetup } from "@decocms/start/setup";
|
|
110
|
+
import { applySectionConventions } from "@decocms/start/cms";
|
|
111
|
+
|
|
112
|
+
import blocks from "./server/cms/blocks.gen";
|
|
113
|
+
import sectionsGen from "./server/cms/sections.gen";
|
|
114
|
+
import meta from "./server/cms/meta.gen.json";
|
|
115
|
+
|
|
116
|
+
createSiteSetup({
|
|
117
|
+
sections: import.meta.glob("./sections/**/*.tsx", { eager: true }),
|
|
118
|
+
blocks,
|
|
119
|
+
meta: () => meta,
|
|
120
|
+
productionOrigins: ["https://my-store.com"],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
applySectionConventions(sectionsGen);
|
|
22
124
|
```
|
|
23
125
|
|
|
24
|
-
###
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
| `@decocms/start/sdk/useScript` | Inline `<script>` with minification |
|
|
38
|
-
| `@decocms/start/sdk/useDevice` | SSR-safe device detection |
|
|
39
|
-
| `@decocms/start/sdk/analytics` | Analytics event types |
|
|
40
|
-
| `@decocms/start/matchers/*` | Feature flag matchers (PostHog, built-ins) |
|
|
41
|
-
| `@decocms/start/types` | Section, App, FnContext type definitions |
|
|
42
|
-
| `@decocms/start/scripts/*` | Code generation (blocks, schema, invoke) |
|
|
43
|
-
|
|
44
|
-
### Worker Entry Request Flow
|
|
126
|
+
### `src/worker-entry.ts`
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import "./setup"; // MUST be first
|
|
130
|
+
|
|
131
|
+
import { createDecoWorkerEntry } from "@decocms/start/sdk/workerEntry";
|
|
132
|
+
import {
|
|
133
|
+
handleMeta,
|
|
134
|
+
handleDecofile,
|
|
135
|
+
handleRender,
|
|
136
|
+
handleInvoke,
|
|
137
|
+
} from "@decocms/start/admin";
|
|
138
|
+
import serverEntry from "./server";
|
|
45
139
|
|
|
140
|
+
export default createDecoWorkerEntry(serverEntry, {
|
|
141
|
+
admin: { handleMeta, handleDecofile, handleRender, handleInvoke },
|
|
142
|
+
});
|
|
46
143
|
```
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
144
|
+
|
|
145
|
+
### `src/routes/$.tsx`
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
149
|
+
import { cmsRouteConfig } from "@decocms/start/routes";
|
|
150
|
+
|
|
151
|
+
export const Route = createFileRoute("/$")(
|
|
152
|
+
cmsRouteConfig({ siteName: "my-store" }),
|
|
153
|
+
);
|
|
53
154
|
```
|
|
54
155
|
|
|
55
|
-
|
|
156
|
+
That is the entire skeleton. `npm install`, `npm run dev`, point `admin.deco.cx` at it, and you have a working CMS-driven site.
|
|
56
157
|
|
|
57
|
-
|
|
58
|
-
|-------------|---------|----------|
|
|
59
|
-
| `/` | static | 1 day |
|
|
60
|
-
| `*/p` | product | 5 min |
|
|
61
|
-
| `/s`, `?q=` | search | 60s |
|
|
62
|
-
| `/cart`, `/checkout` | private | none |
|
|
63
|
-
| Everything else | listing | 2 min |
|
|
158
|
+
For commerce integrations (VTEX, Shopify) see [`@decocms/apps`](https://www.npmjs.com/package/@decocms/apps).
|
|
64
159
|
|
|
65
|
-
|
|
160
|
+
---
|
|
66
161
|
|
|
67
|
-
|
|
162
|
+
## Migrating from Fresh / Preact / Deno
|
|
163
|
+
|
|
164
|
+
`@decocms/start` ships an Agent Skill that handles the migration for you. It works with Claude Code, Cursor, Codex, and any tool that supports skills.
|
|
68
165
|
|
|
69
166
|
```bash
|
|
70
167
|
npx skills add decocms/deco-start
|
|
71
168
|
```
|
|
72
169
|
|
|
73
|
-
Then
|
|
170
|
+
Then, in your editor, point at your Fresh storefront and prompt:
|
|
74
171
|
|
|
75
172
|
> migrate this project to TanStack Start
|
|
76
173
|
|
|
77
|
-
The skill
|
|
174
|
+
The skill runs the migration script, walks you through `MIGRATION_REPORT.md`, fixes typecheck/build errors interactively, and shows the diff before committing.
|
|
78
175
|
|
|
79
|
-
### Or run the script
|
|
176
|
+
### Or run the script directly
|
|
80
177
|
|
|
81
178
|
```bash
|
|
82
|
-
#
|
|
179
|
+
# from inside the v1 storefront directory
|
|
83
180
|
npx -p @decocms/start deco-migrate
|
|
84
181
|
```
|
|
85
182
|
|
|
86
|
-
|
|
183
|
+
The script runs seven phases (analyze → scaffold → transform → cleanup → report → verify → bootstrap), produces `MIGRATION_REPORT.md` with manual TODOs, and gets you to "compiles clean, builds clean".
|
|
184
|
+
|
|
185
|
+
Full migration playbook: [docs.deco.cx/v2/en/migration/overview](https://docs.deco.cx/v2/en/migration/overview).
|
|
87
186
|
|
|
88
|
-
|
|
89
|
-
|------|-------------|
|
|
90
|
-
| `--source <dir>` | Source directory (default: current directory) |
|
|
91
|
-
| `--dry-run` | Preview changes without writing files |
|
|
92
|
-
| `--verbose` | Show detailed output |
|
|
93
|
-
| `--help`, `-h` | Show help message |
|
|
187
|
+
---
|
|
94
188
|
|
|
95
|
-
|
|
189
|
+
## Documentation
|
|
96
190
|
|
|
97
|
-
|
|
98
|
-
2. **Scaffold** — generate `vite.config.ts`, `wrangler.jsonc`, routes, `setup.ts`, worker entry
|
|
99
|
-
3. **Transform** — rewrite imports (70+ rules), JSX attrs, Fresh APIs, Deno-isms, Tailwind v3→v4
|
|
100
|
-
4. **Cleanup** — delete `islands/`, old routes, `deno.json`, move `static/` → `public/`
|
|
101
|
-
5. **Report** — generate `MIGRATION_REPORT.md` with manual review items
|
|
102
|
-
6. **Verify** — 18+ smoke tests (zero old imports, scaffolded files exist)
|
|
103
|
-
7. **Bootstrap** — `npm install`, generate CMS blocks, generate routes
|
|
191
|
+
The full v2 docs live at **[docs.deco.cx/v2](https://docs.deco.cx/v2/en/getting-started/overview)**:
|
|
104
192
|
|
|
105
|
-
|
|
193
|
+
- [Getting started](https://docs.deco.cx/v2/en/getting-started/overview) — install paths, project structure, stack overview.
|
|
194
|
+
- [Concepts](https://docs.deco.cx/v2/en/concepts/sections) — sections, loaders, blocks, routes, deferred rendering.
|
|
195
|
+
- [Framework reference](https://docs.deco.cx/v2/en/framework/overview) — every export of `@decocms/start`, page by page.
|
|
196
|
+
- [Migration](https://docs.deco.cx/v2/en/migration/overview) — v1 → v2 playbook + script + skill.
|
|
197
|
+
- [Case studies](https://docs.deco.cx/v2/en/case-studies/overview) — three production stores end-to-end.
|
|
106
198
|
|
|
107
|
-
|
|
199
|
+
---
|
|
108
200
|
|
|
109
|
-
|
|
201
|
+
## Peer dependencies
|
|
110
202
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
203
|
+
```json
|
|
204
|
+
{
|
|
205
|
+
"@tanstack/react-start": ">=1.0.0",
|
|
206
|
+
"@tanstack/store": ">=0.7.0",
|
|
207
|
+
"@tanstack/react-query": ">=5.0.0",
|
|
208
|
+
"react": "^19.0.0",
|
|
209
|
+
"react-dom": "^19.0.0",
|
|
210
|
+
"vite": ">=6.0.0"
|
|
211
|
+
}
|
|
212
|
+
```
|
|
115
213
|
|
|
116
|
-
|
|
214
|
+
OpenTelemetry is optional but recommended: `@microlabs/otel-cf-workers >=1.0.0-rc.0`, `@opentelemetry/api >=1.9.0`.
|
|
117
215
|
|
|
118
|
-
|
|
119
|
-
- `@tanstack/store` >= 0.7.0
|
|
120
|
-
- `react` ^19.0.0
|
|
121
|
-
- `react-dom` ^19.0.0
|
|
216
|
+
---
|
|
122
217
|
|
|
123
218
|
## Development
|
|
124
219
|
|
|
@@ -128,7 +223,11 @@ npm run lint # biome check
|
|
|
128
223
|
npm run check # typecheck + lint + unused exports
|
|
129
224
|
```
|
|
130
225
|
|
|
131
|
-
This is a library — no dev server. Consumer
|
|
226
|
+
This is a library — there is no dev server here. Consumer storefronts run their own `vite dev`.
|
|
227
|
+
|
|
228
|
+
Contributing? See `CLAUDE.md` for the architectural decisions, and `MIGRATION_TOOLING_PLAN.md` for the append-only history of the migration tooling.
|
|
229
|
+
|
|
230
|
+
---
|
|
132
231
|
|
|
133
232
|
## License
|
|
134
233
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/start",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.29.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
"./sdk/invoke": "./src/sdk/invoke.ts",
|
|
36
36
|
"./sdk/instrumentedFetch": "./src/sdk/instrumentedFetch.ts",
|
|
37
37
|
"./sdk/otel": "./src/sdk/otel.ts",
|
|
38
|
+
"./sdk/logger": "./src/sdk/logger.ts",
|
|
39
|
+
"./sdk/composite": "./src/sdk/composite.ts",
|
|
40
|
+
"./sdk/otelAdapters": "./src/sdk/otelAdapters.ts",
|
|
41
|
+
"./sdk/sampler": "./src/sdk/sampler.ts",
|
|
42
|
+
"./sdk/observability": "./src/sdk/observability.ts",
|
|
38
43
|
"./sdk/workerEntry": "./src/sdk/workerEntry.ts",
|
|
39
44
|
"./sdk/abTesting": "./src/sdk/abTesting.ts",
|
|
40
45
|
"./sdk/redirects": "./src/sdk/redirects.ts",
|
|
@@ -99,6 +104,15 @@
|
|
|
99
104
|
},
|
|
100
105
|
"dependencies": {
|
|
101
106
|
"@deco-cx/warp-node": "^0.3.16",
|
|
107
|
+
"@microlabs/otel-cf-workers": "^1.0.0-rc.52",
|
|
108
|
+
"@opentelemetry/api": "^1.9.1",
|
|
109
|
+
"@opentelemetry/api-logs": "^0.200.0",
|
|
110
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.200.0",
|
|
111
|
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.200.0",
|
|
112
|
+
"@opentelemetry/resources": "^2.6.1",
|
|
113
|
+
"@opentelemetry/sdk-logs": "^0.200.0",
|
|
114
|
+
"@opentelemetry/sdk-metrics": "^2.0.0",
|
|
115
|
+
"@opentelemetry/sdk-trace-base": "^2.6.1",
|
|
102
116
|
"clsx": "^2.1.1",
|
|
103
117
|
"fast-json-patch": "^3.1.0",
|
|
104
118
|
"tailwind-merge": "^3.3.1",
|
|
@@ -106,8 +120,6 @@
|
|
|
106
120
|
"ws": "^8.18.0"
|
|
107
121
|
},
|
|
108
122
|
"peerDependencies": {
|
|
109
|
-
"@microlabs/otel-cf-workers": ">=1.0.0-rc.0",
|
|
110
|
-
"@opentelemetry/api": ">=1.9.0",
|
|
111
123
|
"@tanstack/react-query": ">=5.0.0",
|
|
112
124
|
"@tanstack/react-start": ">=1.0.0",
|
|
113
125
|
"@tanstack/store": ">=0.7.0",
|
|
@@ -115,25 +127,15 @@
|
|
|
115
127
|
"react-dom": "^19.0.0",
|
|
116
128
|
"vite": ">=6.0.0 || >=7.0.0 || >=8.0.0"
|
|
117
129
|
},
|
|
118
|
-
"peerDependenciesMeta": {
|
|
119
|
-
"@microlabs/otel-cf-workers": {
|
|
120
|
-
"optional": true
|
|
121
|
-
},
|
|
122
|
-
"@opentelemetry/api": {
|
|
123
|
-
"optional": true
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
130
|
"devDependencies": {
|
|
127
131
|
"@biomejs/biome": "^2.4.6",
|
|
128
|
-
"@microlabs/otel-cf-workers": "^1.0.0-rc.52",
|
|
129
|
-
"@opentelemetry/api": "^1.9.1",
|
|
130
132
|
"@semantic-release/exec": "^7.1.0",
|
|
131
133
|
"@semantic-release/git": "^10.0.1",
|
|
132
134
|
"@tanstack/react-query": "^5.96.0",
|
|
133
135
|
"@tanstack/store": "^0.9.1",
|
|
134
136
|
"@types/react": "^19.0.0",
|
|
135
|
-
"@types/ws": "^8.18.0",
|
|
136
137
|
"@types/react-dom": "^19.0.0",
|
|
138
|
+
"@types/ws": "^8.18.0",
|
|
137
139
|
"jsdom": "^29.0.0",
|
|
138
140
|
"knip": "^5.86.0",
|
|
139
141
|
"ts-morph": "^27.0.0",
|
package/src/cms/resolve.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ActionConfig,
|
|
3
|
+
type LoaderConfig,
|
|
4
|
+
registerActionSchemas,
|
|
5
|
+
registerLoaderSchemas,
|
|
6
|
+
} from "../admin/schema";
|
|
7
|
+
import { getMeter, MetricNames, withTracing } from "../middleware/observability";
|
|
8
|
+
import { djb2Hex } from "../sdk/djb2";
|
|
9
|
+
import { normalizeUrlsInObject } from "../sdk/normalizeUrls";
|
|
1
10
|
import { findPageByPath, loadBlocks } from "./loader";
|
|
2
11
|
import { getOnBeforeResolveProps, getSection, registerOnBeforeResolveProps } from "./registry";
|
|
3
12
|
import { isLayoutSection, runSingleSectionLoader } from "./sectionLoaders";
|
|
4
|
-
import { normalizeUrlsInObject } from "../sdk/normalizeUrls";
|
|
5
|
-
import { djb2Hex } from "../sdk/djb2";
|
|
6
|
-
import { registerLoaderSchemas, registerActionSchemas, type LoaderConfig, type ActionConfig } from "../admin/schema";
|
|
7
13
|
|
|
8
14
|
// globalThis-backed: share state across Vite server function split modules
|
|
9
15
|
const G = globalThis as any;
|
|
@@ -134,10 +140,7 @@ export function setAsyncRenderingConfig(config?: {
|
|
|
134
140
|
respectCmsLazy?: boolean;
|
|
135
141
|
}): void {
|
|
136
142
|
const existing = getAsyncConfig();
|
|
137
|
-
const merged = new Set([
|
|
138
|
-
...(existing?.alwaysEager ?? []),
|
|
139
|
-
...(config?.alwaysEager ?? []),
|
|
140
|
-
]);
|
|
143
|
+
const merged = new Set([...(existing?.alwaysEager ?? []), ...(config?.alwaysEager ?? [])]);
|
|
141
144
|
G.__deco.asyncConfig = {
|
|
142
145
|
respectCmsLazy: config?.respectCmsLazy ?? existing?.respectCmsLazy ?? true,
|
|
143
146
|
foldThreshold: config?.foldThreshold ?? existing?.foldThreshold ?? Infinity,
|
|
@@ -321,7 +324,13 @@ export function registerCommerceLoaders(loaders: Record<string, CommerceLoader>)
|
|
|
321
324
|
if (key.includes("/actions/")) {
|
|
322
325
|
actionConfigs.push({ key, title: key, namespace, propsSchema: schema });
|
|
323
326
|
} else {
|
|
324
|
-
loaderConfigs.push({
|
|
327
|
+
loaderConfigs.push({
|
|
328
|
+
key,
|
|
329
|
+
title: key,
|
|
330
|
+
namespace,
|
|
331
|
+
propsSchema: schema,
|
|
332
|
+
tags: inferLoaderTags(key),
|
|
333
|
+
});
|
|
325
334
|
}
|
|
326
335
|
}
|
|
327
336
|
|
|
@@ -374,18 +383,24 @@ export function registerMatcher(
|
|
|
374
383
|
if (!G.__deco._builtinMatchersRegistered) {
|
|
375
384
|
G.__deco._builtinMatchersRegistered = true;
|
|
376
385
|
|
|
377
|
-
const builtinMatchers: Record<
|
|
386
|
+
const builtinMatchers: Record<
|
|
387
|
+
string,
|
|
388
|
+
(rule: Record<string, unknown>, ctx: MatcherContext) => boolean
|
|
389
|
+
> = {
|
|
378
390
|
"website/matchers/always.ts": () => true,
|
|
379
391
|
"$live/matchers/MatchAlways.ts": () => true,
|
|
380
392
|
"website/matchers/never.ts": () => false,
|
|
381
393
|
"website/matchers/device.ts": (rule, ctx) => {
|
|
382
394
|
const ua = (ctx.userAgent || "").toLowerCase();
|
|
383
395
|
const isTablet = /ipad|android(?!.*mobile)|tablet/i.test(ua);
|
|
384
|
-
const isMobile =
|
|
396
|
+
const isMobile =
|
|
397
|
+
!isTablet && /mobile|android|iphone|ipod|webos|blackberry|opera mini|iemobile/i.test(ua);
|
|
385
398
|
const isDesktop = !isMobile && !isTablet;
|
|
386
399
|
// If no flags are set, match everything (permissive default)
|
|
387
400
|
if (!rule.mobile && !rule.tablet && !rule.desktop) return true;
|
|
388
|
-
return
|
|
401
|
+
return (
|
|
402
|
+
!!(rule.mobile && isMobile) || !!(rule.tablet && isTablet) || !!(rule.desktop && isDesktop)
|
|
403
|
+
);
|
|
389
404
|
},
|
|
390
405
|
"website/matchers/random.ts": (rule) => {
|
|
391
406
|
const traffic = typeof rule.traffic === "number" ? rule.traffic : 0.5;
|
|
@@ -457,7 +472,10 @@ function ensureInitialized() {
|
|
|
457
472
|
// Matcher evaluation
|
|
458
473
|
// ---------------------------------------------------------------------------
|
|
459
474
|
|
|
460
|
-
export function evaluateMatcher(
|
|
475
|
+
export function evaluateMatcher(
|
|
476
|
+
rule: Record<string, unknown> | undefined,
|
|
477
|
+
ctx: MatcherContext,
|
|
478
|
+
): boolean {
|
|
461
479
|
if (!rule) return true;
|
|
462
480
|
|
|
463
481
|
const resolveType = rule.__resolveType as string | undefined;
|
|
@@ -828,10 +846,7 @@ export async function resolvePageSeoBlock(
|
|
|
828
846
|
}
|
|
829
847
|
|
|
830
848
|
// Multivariate flag — evaluate matcher and follow matched variant
|
|
831
|
-
if (
|
|
832
|
-
rt === "website/flags/multivariate.ts" ||
|
|
833
|
-
rt === "website/flags/multivariate/section.ts"
|
|
834
|
-
) {
|
|
849
|
+
if (rt === "website/flags/multivariate.ts" || rt === "website/flags/multivariate/section.ts") {
|
|
835
850
|
const variants = current.variants as Array<{ value: unknown; rule?: unknown }> | undefined;
|
|
836
851
|
if (!variants?.length) return null;
|
|
837
852
|
let matched: unknown = null;
|
|
@@ -909,10 +924,7 @@ function isRawSectionLayout(section: unknown): string | null {
|
|
|
909
924
|
* unwrapping Lazy/Deferred wrappers, and evaluating multivariate flags.
|
|
910
925
|
* Returns null if not determinable.
|
|
911
926
|
*/
|
|
912
|
-
function resolveFinalSectionKey(
|
|
913
|
-
section: unknown,
|
|
914
|
-
matcherCtx?: MatcherContext,
|
|
915
|
-
): string | null {
|
|
927
|
+
function resolveFinalSectionKey(section: unknown, matcherCtx?: MatcherContext): string | null {
|
|
916
928
|
if (!section || typeof section !== "object") return null;
|
|
917
929
|
|
|
918
930
|
const blocks = loadBlocks();
|
|
@@ -946,13 +958,8 @@ function resolveFinalSectionKey(
|
|
|
946
958
|
continue;
|
|
947
959
|
}
|
|
948
960
|
|
|
949
|
-
if (
|
|
950
|
-
|
|
951
|
-
rt === WELL_KNOWN_TYPES.MULTIVARIATE_SECTION
|
|
952
|
-
) {
|
|
953
|
-
const variants = current.variants as
|
|
954
|
-
| Array<{ value: unknown; rule?: unknown }>
|
|
955
|
-
| undefined;
|
|
961
|
+
if (rt === WELL_KNOWN_TYPES.MULTIVARIATE || rt === WELL_KNOWN_TYPES.MULTIVARIATE_SECTION) {
|
|
962
|
+
const variants = current.variants as Array<{ value: unknown; rule?: unknown }> | undefined;
|
|
956
963
|
if (!variants?.length) return null;
|
|
957
964
|
|
|
958
965
|
let matched: unknown = null;
|
|
@@ -999,21 +1006,13 @@ function isCmsDeferralWrapped(section: unknown, matcherCtx?: MatcherContext): bo
|
|
|
999
1006
|
const rt = current.__resolveType as string | undefined;
|
|
1000
1007
|
if (!rt) return false;
|
|
1001
1008
|
|
|
1002
|
-
if (
|
|
1003
|
-
rt === WELL_KNOWN_TYPES.LAZY ||
|
|
1004
|
-
rt === WELL_KNOWN_TYPES.DEFERRED
|
|
1005
|
-
) {
|
|
1009
|
+
if (rt === WELL_KNOWN_TYPES.LAZY || rt === WELL_KNOWN_TYPES.DEFERRED) {
|
|
1006
1010
|
return true;
|
|
1007
1011
|
}
|
|
1008
1012
|
|
|
1009
1013
|
// Walk through multivariate flags to check the matched variant
|
|
1010
|
-
if (
|
|
1011
|
-
|
|
1012
|
-
rt === WELL_KNOWN_TYPES.MULTIVARIATE_SECTION
|
|
1013
|
-
) {
|
|
1014
|
-
const variants = current.variants as
|
|
1015
|
-
| Array<{ value: unknown; rule?: unknown }>
|
|
1016
|
-
| undefined;
|
|
1014
|
+
if (rt === WELL_KNOWN_TYPES.MULTIVARIATE || rt === WELL_KNOWN_TYPES.MULTIVARIATE_SECTION) {
|
|
1015
|
+
const variants = current.variants as Array<{ value: unknown; rule?: unknown }> | undefined;
|
|
1017
1016
|
if (!variants?.length) return false;
|
|
1018
1017
|
|
|
1019
1018
|
let matched: unknown = null;
|
|
@@ -1125,13 +1124,8 @@ function resolveSectionShallow(
|
|
|
1125
1124
|
}
|
|
1126
1125
|
|
|
1127
1126
|
// Multivariate flags — evaluate matchers and continue with matched variant
|
|
1128
|
-
if (
|
|
1129
|
-
|
|
1130
|
-
rt === WELL_KNOWN_TYPES.MULTIVARIATE_SECTION
|
|
1131
|
-
) {
|
|
1132
|
-
const variants = current.variants as
|
|
1133
|
-
| Array<{ value: unknown; rule?: unknown }>
|
|
1134
|
-
| undefined;
|
|
1127
|
+
if (rt === WELL_KNOWN_TYPES.MULTIVARIATE || rt === WELL_KNOWN_TYPES.MULTIVARIATE_SECTION) {
|
|
1128
|
+
const variants = current.variants as Array<{ value: unknown; rule?: unknown }> | undefined;
|
|
1135
1129
|
if (!variants?.length) return null;
|
|
1136
1130
|
|
|
1137
1131
|
let matched: unknown = null;
|
|
@@ -1331,6 +1325,30 @@ export interface DecoPageResult {
|
|
|
1331
1325
|
export async function resolveDecoPage(
|
|
1332
1326
|
targetPath: string,
|
|
1333
1327
|
matcherCtx?: MatcherContext,
|
|
1328
|
+
): Promise<DecoPageResult | null> {
|
|
1329
|
+
const startedAt = performance.now();
|
|
1330
|
+
return withTracing(
|
|
1331
|
+
"deco.cms.resolvePage",
|
|
1332
|
+
async () => {
|
|
1333
|
+
const result = await resolveDecoPageImpl(targetPath, matcherCtx);
|
|
1334
|
+
try {
|
|
1335
|
+
getMeter()?.histogramRecord?.(
|
|
1336
|
+
MetricNames.RESOLVE_DURATION_MS,
|
|
1337
|
+
performance.now() - startedAt,
|
|
1338
|
+
{ path: targetPath },
|
|
1339
|
+
);
|
|
1340
|
+
} catch {
|
|
1341
|
+
/* observability never fails the request */
|
|
1342
|
+
}
|
|
1343
|
+
return result;
|
|
1344
|
+
},
|
|
1345
|
+
{ "deco.route": targetPath },
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
async function resolveDecoPageImpl(
|
|
1350
|
+
targetPath: string,
|
|
1351
|
+
matcherCtx?: MatcherContext,
|
|
1334
1352
|
): Promise<DecoPageResult | null> {
|
|
1335
1353
|
ensureInitialized();
|
|
1336
1354
|
|
|
@@ -1378,7 +1396,12 @@ export async function resolveDecoPage(
|
|
|
1378
1396
|
// Cache rawProps server-side and strip from the deferred object
|
|
1379
1397
|
// so they are NOT serialized into the HTML payload.
|
|
1380
1398
|
if (deferred.rawProps) {
|
|
1381
|
-
cacheDeferredRawProps(
|
|
1399
|
+
cacheDeferredRawProps(
|
|
1400
|
+
targetPath,
|
|
1401
|
+
deferred.component,
|
|
1402
|
+
currentFlatIndex,
|
|
1403
|
+
deferred.rawProps,
|
|
1404
|
+
);
|
|
1382
1405
|
delete deferred.rawProps;
|
|
1383
1406
|
}
|
|
1384
1407
|
|
|
@@ -1430,10 +1453,12 @@ export async function resolveDecoPage(
|
|
|
1430
1453
|
})();
|
|
1431
1454
|
|
|
1432
1455
|
const idx = currentFlatIndex;
|
|
1433
|
-
eagerResults.push(
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1456
|
+
eagerResults.push(
|
|
1457
|
+
promise.then((sections) => {
|
|
1458
|
+
for (const s of sections) s.index = idx;
|
|
1459
|
+
return sections;
|
|
1460
|
+
}),
|
|
1461
|
+
);
|
|
1437
1462
|
flatIndex++;
|
|
1438
1463
|
}
|
|
1439
1464
|
}
|
|
@@ -1445,10 +1470,7 @@ export async function resolveDecoPage(
|
|
|
1445
1470
|
let seoSection: ResolvedSection | null = null;
|
|
1446
1471
|
if (page.seo) {
|
|
1447
1472
|
try {
|
|
1448
|
-
seoSection = await resolvePageSeoBlock(
|
|
1449
|
-
page.seo as Record<string, unknown>,
|
|
1450
|
-
rctx,
|
|
1451
|
-
);
|
|
1473
|
+
seoSection = await resolvePageSeoBlock(page.seo as Record<string, unknown>, rctx);
|
|
1452
1474
|
} catch (e) {
|
|
1453
1475
|
onResolveError(e, "page.seo", "Page SEO block resolution");
|
|
1454
1476
|
}
|
|
@@ -1588,18 +1610,14 @@ export async function resolveDeferredSectionFull(
|
|
|
1588
1610
|
matcherCtx?: MatcherContext,
|
|
1589
1611
|
): Promise<ResolvedSection | null> {
|
|
1590
1612
|
// rawProps may be stripped from the client payload — resolve from cache or page
|
|
1591
|
-
const rawProps =
|
|
1592
|
-
|
|
1593
|
-
|
|
1613
|
+
const rawProps =
|
|
1614
|
+
ds.rawProps ??
|
|
1615
|
+
getDeferredRawProps(pagePath, ds.component, ds.index) ??
|
|
1616
|
+
(await reExtractRawProps(pagePath, ds.component, ds.index, matcherCtx));
|
|
1594
1617
|
|
|
1595
1618
|
if (!rawProps) return null;
|
|
1596
1619
|
|
|
1597
|
-
const section = await resolveDeferredSection(
|
|
1598
|
-
ds.component,
|
|
1599
|
-
rawProps,
|
|
1600
|
-
pagePath,
|
|
1601
|
-
matcherCtx,
|
|
1602
|
-
);
|
|
1620
|
+
const section = await resolveDeferredSection(ds.component, rawProps, pagePath, matcherCtx);
|
|
1603
1621
|
if (!section) return null;
|
|
1604
1622
|
section.index = ds.index;
|
|
1605
1623
|
const enriched = await runSingleSectionLoader(section, request);
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* This runs AFTER resolveDecoPage and BEFORE React rendering,
|
|
9
9
|
* inside the TanStack Start server function.
|
|
10
10
|
*/
|
|
11
|
+
|
|
12
|
+
import { withTracing } from "../middleware/observability";
|
|
11
13
|
import { getCacheProfile } from "../sdk/cacheHeaders";
|
|
12
14
|
import { djb2 } from "../sdk/djb2";
|
|
13
15
|
import type { ResolvedSection } from "./resolve";
|
|
@@ -326,6 +328,15 @@ function withPageContext(loader: SectionLoaderFn): SectionLoaderFn {
|
|
|
326
328
|
export async function runSingleSectionLoader(
|
|
327
329
|
section: ResolvedSection,
|
|
328
330
|
request: Request,
|
|
331
|
+
): Promise<ResolvedSection> {
|
|
332
|
+
return withTracing("deco.section.loader", () => runSingleSectionLoaderImpl(section, request), {
|
|
333
|
+
"deco.section": section.component,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function runSingleSectionLoaderImpl(
|
|
338
|
+
section: ResolvedSection,
|
|
339
|
+
request: Request,
|
|
329
340
|
): Promise<ResolvedSection> {
|
|
330
341
|
const loader = loaderRegistry.get(section.component);
|
|
331
342
|
|