@decocms/blocks 7.0.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/package.json +97 -0
- package/src/cms/applySectionConventions.ts +128 -0
- package/src/cms/blockSource.test.ts +67 -0
- package/src/cms/blockSource.ts +124 -0
- package/src/cms/index.ts +104 -0
- package/src/cms/layoutCacheRace.test.ts +146 -0
- package/src/cms/loadDecofileDirectory.test.ts +52 -0
- package/src/cms/loadDecofileDirectory.ts +48 -0
- package/src/cms/loader.test.ts +184 -0
- package/src/cms/loader.ts +284 -0
- package/src/cms/registry.test.ts +118 -0
- package/src/cms/registry.ts +252 -0
- package/src/cms/resolve.test.ts +547 -0
- package/src/cms/resolve.ts +2003 -0
- package/src/cms/schema.ts +993 -0
- package/src/cms/sectionLoaders.test.ts +409 -0
- package/src/cms/sectionLoaders.ts +562 -0
- package/src/cms/sectionMixins.test.ts +163 -0
- package/src/cms/sectionMixins.ts +153 -0
- package/src/hooks/LazySection.tsx +121 -0
- package/src/hooks/LiveControls.tsx +122 -0
- package/src/hooks/RenderSection.test.tsx +81 -0
- package/src/hooks/RenderSection.tsx +91 -0
- package/src/hooks/SectionErrorFallback.tsx +97 -0
- package/src/hooks/index.ts +4 -0
- package/src/index.ts +8 -0
- package/src/matchers/builtins.test.ts +251 -0
- package/src/matchers/builtins.ts +437 -0
- package/src/matchers/countryNames.ts +104 -0
- package/src/matchers/override.test.ts +205 -0
- package/src/matchers/override.ts +136 -0
- package/src/matchers/posthog.ts +154 -0
- package/src/middleware/decoState.ts +55 -0
- package/src/middleware/healthMetrics.ts +133 -0
- package/src/middleware/hydrationContext.test.ts +61 -0
- package/src/middleware/hydrationContext.ts +79 -0
- package/src/middleware/index.ts +88 -0
- package/src/middleware/liveness.ts +21 -0
- package/src/middleware/observability.test.ts +238 -0
- package/src/middleware/observability.ts +620 -0
- package/src/middleware/validateSection.test.ts +147 -0
- package/src/middleware/validateSection.ts +100 -0
- package/src/sdk/abTesting.test.ts +326 -0
- package/src/sdk/abTesting.ts +499 -0
- package/src/sdk/analytics.ts +77 -0
- package/src/sdk/cacheHeaders.test.ts +115 -0
- package/src/sdk/cacheHeaders.ts +424 -0
- package/src/sdk/cachedLoader.ts +364 -0
- package/src/sdk/clx.ts +5 -0
- package/src/sdk/cn.test.ts +34 -0
- package/src/sdk/cn.ts +28 -0
- package/src/sdk/composite.test.ts +121 -0
- package/src/sdk/composite.ts +114 -0
- package/src/sdk/cookie.test.ts +108 -0
- package/src/sdk/cookie.ts +129 -0
- package/src/sdk/crypto.ts +185 -0
- package/src/sdk/csp.ts +59 -0
- package/src/sdk/djb2.ts +20 -0
- package/src/sdk/encoding.test.ts +71 -0
- package/src/sdk/encoding.ts +47 -0
- package/src/sdk/env.ts +31 -0
- package/src/sdk/http.test.ts +71 -0
- package/src/sdk/http.ts +124 -0
- package/src/sdk/index.ts +90 -0
- package/src/sdk/inflightTimeout.test.ts +35 -0
- package/src/sdk/inflightTimeout.ts +53 -0
- package/src/sdk/instrumentedFetch.test.ts +559 -0
- package/src/sdk/instrumentedFetch.ts +339 -0
- package/src/sdk/invoke.test.ts +115 -0
- package/src/sdk/invoke.ts +260 -0
- package/src/sdk/logger.test.ts +432 -0
- package/src/sdk/logger.ts +304 -0
- package/src/sdk/mergeCacheControl.ts +150 -0
- package/src/sdk/normalizeUrls.ts +91 -0
- package/src/sdk/observability.ts +109 -0
- package/src/sdk/otel.test.ts +526 -0
- package/src/sdk/otel.ts +981 -0
- package/src/sdk/otelAdapters/clickhouseCollector.ts +65 -0
- package/src/sdk/otelAdapters.test.ts +89 -0
- package/src/sdk/otelAdapters.ts +144 -0
- package/src/sdk/otelHttpLog.test.ts +457 -0
- package/src/sdk/otelHttpLog.ts +419 -0
- package/src/sdk/otelHttpMeter.test.ts +292 -0
- package/src/sdk/otelHttpMeter.ts +506 -0
- package/src/sdk/otelHttpTracer.test.ts +474 -0
- package/src/sdk/otelHttpTracer.ts +543 -0
- package/src/sdk/redirects.ts +225 -0
- package/src/sdk/requestContext.ts +281 -0
- package/src/sdk/requestContextStorage.browser.test.ts +29 -0
- package/src/sdk/requestContextStorage.browser.ts +43 -0
- package/src/sdk/requestContextStorage.ts +35 -0
- package/src/sdk/retry.ts +45 -0
- package/src/sdk/serverTimings.ts +68 -0
- package/src/sdk/signal.ts +42 -0
- package/src/sdk/sitemap.ts +160 -0
- package/src/sdk/urlRedaction.test.ts +73 -0
- package/src/sdk/urlRedaction.ts +82 -0
- package/src/sdk/urlUtils.ts +134 -0
- package/src/sdk/useDevice.test.ts +130 -0
- package/src/sdk/useDevice.ts +109 -0
- package/src/sdk/useDeviceContext.tsx +108 -0
- package/src/sdk/useId.ts +7 -0
- package/src/sdk/useScript.test.ts +128 -0
- package/src/sdk/useScript.ts +210 -0
- package/src/sdk/useSuggestions.test.ts +230 -0
- package/src/sdk/useSuggestions.ts +188 -0
- package/src/sdk/wrapCaughtErrors.ts +107 -0
- package/src/setup.ts +119 -0
- package/src/types/index.ts +39 -0
- package/src/types/widgets.ts +14 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,993 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-level schema definitions and runtime meta composition.
|
|
3
|
+
*
|
|
4
|
+
* The schema generator (scripts/generate-schema.ts) produces section and app
|
|
5
|
+
* schemas from site TypeScript files. Framework-managed block types -- pages,
|
|
6
|
+
* loaders, matchers, flags -- have their schemas defined here and injected
|
|
7
|
+
* at runtime via composeMeta().
|
|
8
|
+
*
|
|
9
|
+
* This keeps the generator focused on site-specific concerns while the
|
|
10
|
+
* framework owns the schemas for its own block types.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface MetaResponse {
|
|
14
|
+
major: number;
|
|
15
|
+
version: string;
|
|
16
|
+
namespace: string;
|
|
17
|
+
site: string;
|
|
18
|
+
manifest: {
|
|
19
|
+
blocks: Record<string, Record<string, { $ref: string; namespace?: string }>>;
|
|
20
|
+
};
|
|
21
|
+
schema: {
|
|
22
|
+
definitions: Record<string, any>;
|
|
23
|
+
root: Record<string, any>;
|
|
24
|
+
};
|
|
25
|
+
platform?: string;
|
|
26
|
+
cloudProvider?: string;
|
|
27
|
+
framework?: string;
|
|
28
|
+
etag?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Standard base64 encoding that matches the browser's btoa().
|
|
33
|
+
* The admin uses btoa(resolveType) in some code paths to construct
|
|
34
|
+
* definition refs, so our keys MUST include the = padding.
|
|
35
|
+
*/
|
|
36
|
+
function toBase64(str: string): string {
|
|
37
|
+
if (typeof btoa === "function") return btoa(str);
|
|
38
|
+
return Buffer.from(str).toString("base64");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The admin's deRefUntil and ArrayFieldTemplate look for the LITERAL
|
|
42
|
+
// string "Resolvable" (not base64-encoded). Both keys are needed:
|
|
43
|
+
// - literal for admin detection
|
|
44
|
+
// - base64 for backward compat with any code that does btoa("Resolvable")
|
|
45
|
+
const RESOLVABLE_LITERAL_KEY = "Resolvable";
|
|
46
|
+
const RESOLVABLE_B64_KEY = toBase64("Resolvable");
|
|
47
|
+
|
|
48
|
+
function buildResolvableDefinition() {
|
|
49
|
+
return {
|
|
50
|
+
title: "Select from saved",
|
|
51
|
+
type: "object",
|
|
52
|
+
required: ["__resolveType"],
|
|
53
|
+
additionalProperties: true,
|
|
54
|
+
properties: { __resolveType: { type: "string" } },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Loader definitions — dynamic registry
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
export interface LoaderConfig {
|
|
63
|
+
key: string;
|
|
64
|
+
title: string;
|
|
65
|
+
namespace: string;
|
|
66
|
+
description?: string;
|
|
67
|
+
icon?: string;
|
|
68
|
+
propsSchema: Record<string, any>;
|
|
69
|
+
/** Tags for property matching (e.g., "product-list" enables injection into Product[] props). */
|
|
70
|
+
tags?: string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const loaderRegistry: LoaderConfig[] = [];
|
|
74
|
+
|
|
75
|
+
/** Register a single loader schema for the admin. */
|
|
76
|
+
export function registerLoaderSchema(config: LoaderConfig) {
|
|
77
|
+
const idx = loaderRegistry.findIndex((l) => l.key === config.key);
|
|
78
|
+
if (idx >= 0) {
|
|
79
|
+
loaderRegistry[idx] = config;
|
|
80
|
+
} else {
|
|
81
|
+
loaderRegistry.push(config);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Register multiple loader schemas at once. */
|
|
86
|
+
export function registerLoaderSchemas(configs: LoaderConfig[]) {
|
|
87
|
+
for (const config of configs) registerLoaderSchema(config);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Get all registered loader schemas. */
|
|
91
|
+
export function getRegisteredLoaders(): LoaderConfig[] {
|
|
92
|
+
return [...loaderRegistry];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getProductListLoaderKeys(): string[] {
|
|
96
|
+
return loaderRegistry.filter((l) => l.tags?.includes("product-list")).map((l) => l.key);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Action definitions — dynamic registry
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
export interface ActionConfig {
|
|
104
|
+
key: string;
|
|
105
|
+
title: string;
|
|
106
|
+
namespace: string;
|
|
107
|
+
description?: string;
|
|
108
|
+
icon?: string;
|
|
109
|
+
propsSchema: Record<string, any>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const actionRegistry: ActionConfig[] = [];
|
|
113
|
+
|
|
114
|
+
/** Register a single action schema for the admin. */
|
|
115
|
+
export function registerActionSchema(config: ActionConfig) {
|
|
116
|
+
const idx = actionRegistry.findIndex((a) => a.key === config.key);
|
|
117
|
+
if (idx >= 0) {
|
|
118
|
+
actionRegistry[idx] = config;
|
|
119
|
+
} else {
|
|
120
|
+
actionRegistry.push(config);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Register multiple action schemas at once. */
|
|
125
|
+
export function registerActionSchemas(configs: ActionConfig[]) {
|
|
126
|
+
for (const config of configs) registerActionSchema(config);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildActionDefinitions() {
|
|
130
|
+
const definitions: Record<string, any> = {};
|
|
131
|
+
const manifestBlocks: Record<string, any> = {};
|
|
132
|
+
|
|
133
|
+
for (const action of actionRegistry) {
|
|
134
|
+
const defKey = toBase64(action.key);
|
|
135
|
+
|
|
136
|
+
definitions[defKey] = {
|
|
137
|
+
title: action.key,
|
|
138
|
+
...(action.description ? { description: action.description } : {}),
|
|
139
|
+
...(action.icon ? { icon: action.icon } : {}),
|
|
140
|
+
type: "object",
|
|
141
|
+
required: ["__resolveType", ...(action.propsSchema?.required || [])],
|
|
142
|
+
properties: {
|
|
143
|
+
__resolveType: {
|
|
144
|
+
type: "string",
|
|
145
|
+
enum: [action.key],
|
|
146
|
+
default: action.key,
|
|
147
|
+
},
|
|
148
|
+
...(action.propsSchema?.properties || {}),
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
manifestBlocks[action.key] = {
|
|
153
|
+
$ref: `#/definitions/${defKey}`,
|
|
154
|
+
namespace: action.namespace,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { definitions, manifestBlocks };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Matcher definitions — dynamic registry
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
export interface MatcherConfig {
|
|
166
|
+
key: string;
|
|
167
|
+
title: string;
|
|
168
|
+
namespace: string;
|
|
169
|
+
description?: string;
|
|
170
|
+
icon?: string;
|
|
171
|
+
propsSchema?: Record<string, any>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const matcherRegistry: MatcherConfig[] = [];
|
|
175
|
+
|
|
176
|
+
/** Register a single matcher schema for the admin. */
|
|
177
|
+
export function registerMatcherSchema(config: MatcherConfig) {
|
|
178
|
+
const idx = matcherRegistry.findIndex((m) => m.key === config.key);
|
|
179
|
+
if (idx >= 0) {
|
|
180
|
+
matcherRegistry[idx] = config;
|
|
181
|
+
} else {
|
|
182
|
+
matcherRegistry.push(config);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Register multiple matcher schemas at once. */
|
|
187
|
+
export function registerMatcherSchemas(configs: MatcherConfig[]) {
|
|
188
|
+
for (const config of configs) registerMatcherSchema(config);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Get all registered matcher schemas. */
|
|
192
|
+
export function getRegisteredMatchers(): MatcherConfig[] {
|
|
193
|
+
return matcherRegistry;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Register built-in matchers that are always available
|
|
197
|
+
registerMatcherSchemas([
|
|
198
|
+
{
|
|
199
|
+
key: "website/matchers/always.ts",
|
|
200
|
+
title: "Always",
|
|
201
|
+
description: "Target all users",
|
|
202
|
+
icon: "eye",
|
|
203
|
+
namespace: "website",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
key: "website/matchers/never.ts",
|
|
207
|
+
title: "Never",
|
|
208
|
+
description: "Hide from all users",
|
|
209
|
+
icon: "eye-off",
|
|
210
|
+
namespace: "website",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
key: "website/matchers/device.ts",
|
|
214
|
+
title: "Device",
|
|
215
|
+
description: "Target users based on their device type, such as desktop, tablet, or mobile",
|
|
216
|
+
icon: "device-mobile",
|
|
217
|
+
namespace: "website",
|
|
218
|
+
propsSchema: {
|
|
219
|
+
type: "object",
|
|
220
|
+
properties: {
|
|
221
|
+
mobile: { type: "boolean", title: "Mobile" },
|
|
222
|
+
desktop: { type: "boolean", title: "Desktop" },
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
key: "website/matchers/date.ts",
|
|
228
|
+
title: "Date and Time",
|
|
229
|
+
description: "Target users based on specific dates or date ranges, including specific times",
|
|
230
|
+
icon: "calendar-event",
|
|
231
|
+
namespace: "website",
|
|
232
|
+
propsSchema: {
|
|
233
|
+
type: "object",
|
|
234
|
+
properties: {
|
|
235
|
+
start: { type: "string", title: "Start Date", format: "date-time" },
|
|
236
|
+
end: { type: "string", title: "End Date", format: "date-time" },
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
key: "website/matchers/cron.ts",
|
|
242
|
+
title: "Cron",
|
|
243
|
+
description: "Target users with precision using recurring schedules",
|
|
244
|
+
icon: "refresh",
|
|
245
|
+
namespace: "website",
|
|
246
|
+
propsSchema: {
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: {
|
|
249
|
+
start: { type: "string", title: "Start", format: "date-time" },
|
|
250
|
+
end: { type: "string", title: "End", format: "date-time" },
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
key: "website/matchers/cookie.ts",
|
|
256
|
+
title: "Cookie",
|
|
257
|
+
description: "Target users that have a specific cookie",
|
|
258
|
+
icon: "cookie",
|
|
259
|
+
namespace: "website",
|
|
260
|
+
propsSchema: {
|
|
261
|
+
type: "object",
|
|
262
|
+
properties: {
|
|
263
|
+
name: { type: "string", title: "Cookie Name" },
|
|
264
|
+
value: { type: "string", title: "Cookie Value" },
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
key: "website/matchers/host.ts",
|
|
270
|
+
title: "Host",
|
|
271
|
+
description: "Target users based on the domain or subdomain they are accessing your site from",
|
|
272
|
+
icon: "world-www",
|
|
273
|
+
namespace: "website",
|
|
274
|
+
propsSchema: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: {
|
|
277
|
+
host: { type: "string", title: "Hostname" },
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
key: "website/matchers/pathname.ts",
|
|
283
|
+
title: "Pathname",
|
|
284
|
+
description: "Target users based on the pathname",
|
|
285
|
+
icon: "world-www",
|
|
286
|
+
namespace: "website",
|
|
287
|
+
propsSchema: {
|
|
288
|
+
type: "object",
|
|
289
|
+
properties: {
|
|
290
|
+
pattern: { type: "string", title: "Regex Pattern" },
|
|
291
|
+
includes: {
|
|
292
|
+
type: "array",
|
|
293
|
+
title: "Includes",
|
|
294
|
+
items: { type: "string" },
|
|
295
|
+
},
|
|
296
|
+
excludes: {
|
|
297
|
+
type: "array",
|
|
298
|
+
title: "Excludes",
|
|
299
|
+
items: { type: "string" },
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
key: "website/matchers/queryString.ts",
|
|
306
|
+
title: "Query String",
|
|
307
|
+
description: "Match with a specific querystring",
|
|
308
|
+
icon: "question-mark",
|
|
309
|
+
namespace: "website",
|
|
310
|
+
propsSchema: {
|
|
311
|
+
type: "object",
|
|
312
|
+
properties: {
|
|
313
|
+
key: { type: "string", title: "Parameter Name" },
|
|
314
|
+
value: { type: "string", title: "Parameter Value" },
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
key: "website/matchers/random.ts",
|
|
320
|
+
title: "Random",
|
|
321
|
+
description: "Target a percentage of the total traffic to do an A/B test",
|
|
322
|
+
icon: "arrow-split",
|
|
323
|
+
namespace: "website",
|
|
324
|
+
propsSchema: {
|
|
325
|
+
type: "object",
|
|
326
|
+
properties: {
|
|
327
|
+
traffic: {
|
|
328
|
+
type: "number",
|
|
329
|
+
title: "Traffic Percentage (0\u20131)",
|
|
330
|
+
minimum: 0,
|
|
331
|
+
maximum: 1,
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
key: "website/matchers/location.ts",
|
|
338
|
+
title: "Location",
|
|
339
|
+
description:
|
|
340
|
+
"Target users based on their geographical location, such as country, city, or region",
|
|
341
|
+
icon: "map-2",
|
|
342
|
+
namespace: "website",
|
|
343
|
+
propsSchema: (() => {
|
|
344
|
+
// `includeLocations`/`excludeLocations` are `(Location | Map)[]` in the
|
|
345
|
+
// matcher source. Emit the union as an `anyOf` of two branches so the CMS
|
|
346
|
+
// renders a Location/Map choice instead of merging every field into one
|
|
347
|
+
// form. The Location branch is exactly {city, regionCode, country}; the
|
|
348
|
+
// Map branch carries the `@format: map` coordinate picker.
|
|
349
|
+
const locationBranch = {
|
|
350
|
+
type: "object" as const,
|
|
351
|
+
title: "Location",
|
|
352
|
+
properties: {
|
|
353
|
+
city: {
|
|
354
|
+
type: "string",
|
|
355
|
+
title: "City",
|
|
356
|
+
examples: ["São Paulo"],
|
|
357
|
+
description:
|
|
358
|
+
"Exact city name (case-insensitive) as returned by Cloudflare's cf-ipcity header.",
|
|
359
|
+
},
|
|
360
|
+
regionCode: {
|
|
361
|
+
type: "string",
|
|
362
|
+
title: "Region Code",
|
|
363
|
+
examples: ["SP", "RJ", "MG", "47"],
|
|
364
|
+
description:
|
|
365
|
+
'Matches Cloudflare\'s cf-region-code header. Usually the ISO 3166-2 subdivision code (SP, RJ, MG, …); for some regions Cloudflare returns numeric codes (e.g. 47). Use the same value cf-region-code returns for your audience — full region names like "São Paulo" are NOT matched.',
|
|
366
|
+
},
|
|
367
|
+
country: {
|
|
368
|
+
type: "string",
|
|
369
|
+
title: "Country",
|
|
370
|
+
examples: ["BR", "Brasil", "US"],
|
|
371
|
+
description:
|
|
372
|
+
"ISO 3166-1 alpha-2 code (BR, US, AR, …) or a full country name (Brasil, United States) — the matcher resolves common aliases.",
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
const mapBranch = {
|
|
377
|
+
type: "object" as const,
|
|
378
|
+
title: "Map",
|
|
379
|
+
properties: {
|
|
380
|
+
coordinates: {
|
|
381
|
+
type: "string",
|
|
382
|
+
title: "Area selection",
|
|
383
|
+
format: "map",
|
|
384
|
+
examples: ["-23.5505,-46.6333,5000"],
|
|
385
|
+
description:
|
|
386
|
+
'"latitude,longitude,radius_in_meters" for haversine-radius matching. Set this for the Map mode.',
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
// Mustache title so array items summarise their content instead of
|
|
391
|
+
// "Item 1". Absent fields render empty; the label is trimmed. Covers both
|
|
392
|
+
// branches — Location fields and the Map coordinates.
|
|
393
|
+
const locationOrMapItem = {
|
|
394
|
+
title: "{{{city}}} {{{regionCode}}} {{{country}}} {{{coordinates}}}",
|
|
395
|
+
anyOf: [locationBranch, mapBranch],
|
|
396
|
+
};
|
|
397
|
+
return {
|
|
398
|
+
type: "object" as const,
|
|
399
|
+
properties: {
|
|
400
|
+
includeLocations: {
|
|
401
|
+
type: "array",
|
|
402
|
+
title: "Include Locations",
|
|
403
|
+
items: locationOrMapItem,
|
|
404
|
+
},
|
|
405
|
+
excludeLocations: {
|
|
406
|
+
type: "array",
|
|
407
|
+
title: "Exclude Locations",
|
|
408
|
+
items: locationOrMapItem,
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
})(),
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
key: "website/matchers/userAgent.ts",
|
|
416
|
+
title: "User Agent",
|
|
417
|
+
description: "Target users based on their web browser or operational system",
|
|
418
|
+
icon: "world",
|
|
419
|
+
namespace: "website",
|
|
420
|
+
propsSchema: {
|
|
421
|
+
type: "object",
|
|
422
|
+
properties: {
|
|
423
|
+
includes: { type: "string", title: "Includes (substring)" },
|
|
424
|
+
match: { type: "string", title: "Match (regex)" },
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
key: "website/matchers/environment.ts",
|
|
430
|
+
title: "Environment",
|
|
431
|
+
description:
|
|
432
|
+
"Target users based from where they are accessing your site (development, testing, or production)",
|
|
433
|
+
icon: "code",
|
|
434
|
+
namespace: "website",
|
|
435
|
+
propsSchema: {
|
|
436
|
+
type: "object",
|
|
437
|
+
properties: {
|
|
438
|
+
environment: {
|
|
439
|
+
type: "string",
|
|
440
|
+
title: "Environment",
|
|
441
|
+
enum: ["production", "development"],
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
key: "website/matchers/multi.ts",
|
|
448
|
+
title: "Multi",
|
|
449
|
+
description: "Create more complex conditions by combining multiple matchers",
|
|
450
|
+
icon: "plus",
|
|
451
|
+
namespace: "website",
|
|
452
|
+
propsSchema: {
|
|
453
|
+
type: "object",
|
|
454
|
+
properties: {
|
|
455
|
+
op: {
|
|
456
|
+
type: "string",
|
|
457
|
+
title: "Operator",
|
|
458
|
+
enum: ["and", "or"],
|
|
459
|
+
default: "and",
|
|
460
|
+
},
|
|
461
|
+
matchers: {
|
|
462
|
+
type: "array",
|
|
463
|
+
title: "Matchers",
|
|
464
|
+
items: {
|
|
465
|
+
type: "object",
|
|
466
|
+
required: ["__resolveType"],
|
|
467
|
+
properties: { __resolveType: { type: "string" } },
|
|
468
|
+
additionalProperties: true,
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
key: "website/matchers/negate.ts",
|
|
476
|
+
title: "Negates",
|
|
477
|
+
description: "Create conditions that target users who do not meet certain criteria",
|
|
478
|
+
icon: "minus",
|
|
479
|
+
namespace: "website",
|
|
480
|
+
propsSchema: {
|
|
481
|
+
type: "object",
|
|
482
|
+
properties: {
|
|
483
|
+
matcher: {
|
|
484
|
+
type: "object",
|
|
485
|
+
title: "Matcher",
|
|
486
|
+
required: ["__resolveType"],
|
|
487
|
+
properties: { __resolveType: { type: "string" } },
|
|
488
|
+
additionalProperties: true,
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
]);
|
|
494
|
+
|
|
495
|
+
function buildLoaderDefinitions() {
|
|
496
|
+
const definitions: Record<string, any> = {};
|
|
497
|
+
const manifestBlocks: Record<string, any> = {};
|
|
498
|
+
const loaderAnyOf: any[] = [{ $ref: `#/definitions/${RESOLVABLE_LITERAL_KEY}` }];
|
|
499
|
+
|
|
500
|
+
for (const loader of loaderRegistry) {
|
|
501
|
+
const defKey = toBase64(loader.key);
|
|
502
|
+
|
|
503
|
+
definitions[defKey] = {
|
|
504
|
+
title: loader.key,
|
|
505
|
+
...(loader.description ? { description: loader.description } : {}),
|
|
506
|
+
...(loader.icon ? { icon: loader.icon } : {}),
|
|
507
|
+
type: "object",
|
|
508
|
+
required: ["__resolveType", ...(loader.propsSchema?.required || [])],
|
|
509
|
+
properties: {
|
|
510
|
+
__resolveType: {
|
|
511
|
+
type: "string",
|
|
512
|
+
enum: [loader.key],
|
|
513
|
+
default: loader.key,
|
|
514
|
+
},
|
|
515
|
+
...(loader.propsSchema?.properties || {}),
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
manifestBlocks[loader.key] = {
|
|
520
|
+
$ref: `#/definitions/${defKey}`,
|
|
521
|
+
namespace: loader.namespace,
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
loaderAnyOf.push({ $ref: `#/definitions/${defKey}` });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return { definitions, manifestBlocks, loaderAnyOf };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
// Matcher definitions
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
function buildMatcherDefinitions() {
|
|
535
|
+
const definitions: Record<string, any> = {};
|
|
536
|
+
const manifestBlocks: Record<string, any> = {};
|
|
537
|
+
const matcherAnyOf: any[] = [{ $ref: `#/definitions/${RESOLVABLE_LITERAL_KEY}` }];
|
|
538
|
+
|
|
539
|
+
for (const matcher of matcherRegistry) {
|
|
540
|
+
const defKey = toBase64(matcher.key);
|
|
541
|
+
definitions[defKey] = {
|
|
542
|
+
title: matcher.title,
|
|
543
|
+
...(matcher.description ? { description: matcher.description } : {}),
|
|
544
|
+
...(matcher.icon ? { icon: matcher.icon } : {}),
|
|
545
|
+
type: "object",
|
|
546
|
+
required: ["__resolveType"],
|
|
547
|
+
properties: {
|
|
548
|
+
__resolveType: {
|
|
549
|
+
type: "string",
|
|
550
|
+
enum: [matcher.key],
|
|
551
|
+
default: matcher.key,
|
|
552
|
+
},
|
|
553
|
+
...(matcher.propsSchema?.properties || {}),
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
manifestBlocks[matcher.key] = {
|
|
557
|
+
$ref: `#/definitions/${defKey}`,
|
|
558
|
+
namespace: matcher.namespace,
|
|
559
|
+
};
|
|
560
|
+
matcherAnyOf.push({ $ref: `#/definitions/${defKey}` });
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return { definitions, manifestBlocks, matcherAnyOf };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
// Multivariate flag schema
|
|
568
|
+
// ---------------------------------------------------------------------------
|
|
569
|
+
|
|
570
|
+
function buildMultivariateFlagSchema(innerSchema: any) {
|
|
571
|
+
return {
|
|
572
|
+
type: "object",
|
|
573
|
+
required: ["__resolveType"],
|
|
574
|
+
properties: {
|
|
575
|
+
__resolveType: {
|
|
576
|
+
type: "string",
|
|
577
|
+
enum: ["website/flags/multivariate.ts", "website/flags/multivariate/section.ts"],
|
|
578
|
+
},
|
|
579
|
+
variants: {
|
|
580
|
+
type: "array",
|
|
581
|
+
title: "Variants",
|
|
582
|
+
items: {
|
|
583
|
+
type: "object",
|
|
584
|
+
properties: {
|
|
585
|
+
rule: {
|
|
586
|
+
title: "Rule",
|
|
587
|
+
type: "object",
|
|
588
|
+
required: ["__resolveType"],
|
|
589
|
+
properties: {
|
|
590
|
+
__resolveType: { type: "string" },
|
|
591
|
+
},
|
|
592
|
+
additionalProperties: true,
|
|
593
|
+
},
|
|
594
|
+
value: innerSchema,
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
// Page schema
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
|
|
606
|
+
function buildPageSchema(sectionAnyOf: any[]) {
|
|
607
|
+
const PAGE_TYPE = "website/pages/Page.tsx";
|
|
608
|
+
const defKey = toBase64(PAGE_TYPE);
|
|
609
|
+
|
|
610
|
+
const sectionsArraySchema = {
|
|
611
|
+
type: "array",
|
|
612
|
+
title: "Sections",
|
|
613
|
+
items: { anyOf: sectionAnyOf },
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const sectionsMultivariateSchema = buildMultivariateFlagSchema(sectionsArraySchema);
|
|
617
|
+
|
|
618
|
+
const definition = {
|
|
619
|
+
title: PAGE_TYPE,
|
|
620
|
+
type: "object",
|
|
621
|
+
required: ["__resolveType"],
|
|
622
|
+
properties: {
|
|
623
|
+
__resolveType: {
|
|
624
|
+
type: "string",
|
|
625
|
+
enum: [PAGE_TYPE],
|
|
626
|
+
default: PAGE_TYPE,
|
|
627
|
+
},
|
|
628
|
+
name: { type: "string", title: "Name" },
|
|
629
|
+
path: { type: "string", title: "Path" },
|
|
630
|
+
seo: { title: "SEO", anyOf: sectionAnyOf },
|
|
631
|
+
sections: {
|
|
632
|
+
title: "Sections",
|
|
633
|
+
anyOf: [sectionsArraySchema, sectionsMultivariateSchema],
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
definitions: { [defKey]: definition },
|
|
640
|
+
manifestBlocks: {
|
|
641
|
+
[PAGE_TYPE]: {
|
|
642
|
+
$ref: `#/definitions/${defKey}`,
|
|
643
|
+
namespace: "website",
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
rootAnyOf: [
|
|
647
|
+
{ $ref: `#/definitions/${RESOLVABLE_LITERAL_KEY}` },
|
|
648
|
+
{ $ref: `#/definitions/${defKey}` },
|
|
649
|
+
],
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ---------------------------------------------------------------------------
|
|
654
|
+
// Framework sections
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
|
|
657
|
+
function buildFrameworkSections(sectionAnyOf: any[]) {
|
|
658
|
+
const definitions: Record<string, any> = {};
|
|
659
|
+
const manifestBlocks: Record<string, any> = {};
|
|
660
|
+
const extraAnyOf: any[] = [];
|
|
661
|
+
|
|
662
|
+
// --- website/sections/Rendering/Lazy.tsx ---
|
|
663
|
+
const LAZY_TYPE = "website/sections/Rendering/Lazy.tsx";
|
|
664
|
+
const lazyKey = toBase64(LAZY_TYPE);
|
|
665
|
+
definitions[lazyKey] = {
|
|
666
|
+
title: LAZY_TYPE,
|
|
667
|
+
type: "object",
|
|
668
|
+
required: ["__resolveType"],
|
|
669
|
+
properties: {
|
|
670
|
+
__resolveType: {
|
|
671
|
+
type: "string",
|
|
672
|
+
enum: [LAZY_TYPE],
|
|
673
|
+
default: LAZY_TYPE,
|
|
674
|
+
},
|
|
675
|
+
section: {
|
|
676
|
+
title: "Section",
|
|
677
|
+
anyOf: sectionAnyOf,
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
};
|
|
681
|
+
manifestBlocks[LAZY_TYPE] = {
|
|
682
|
+
$ref: `#/definitions/${lazyKey}`,
|
|
683
|
+
namespace: "website",
|
|
684
|
+
};
|
|
685
|
+
extraAnyOf.push({ $ref: `#/definitions/${lazyKey}` });
|
|
686
|
+
|
|
687
|
+
// --- website/sections/Seo/Seo.tsx ---
|
|
688
|
+
const SEO_TYPE = "website/sections/Seo/Seo.tsx";
|
|
689
|
+
const seoKey = toBase64(SEO_TYPE);
|
|
690
|
+
definitions[seoKey] = {
|
|
691
|
+
title: SEO_TYPE,
|
|
692
|
+
type: "object",
|
|
693
|
+
required: ["__resolveType"],
|
|
694
|
+
properties: {
|
|
695
|
+
__resolveType: {
|
|
696
|
+
type: "string",
|
|
697
|
+
enum: [SEO_TYPE],
|
|
698
|
+
default: SEO_TYPE,
|
|
699
|
+
},
|
|
700
|
+
title: { type: "string", title: "Title" },
|
|
701
|
+
description: { type: "string", title: "Description" },
|
|
702
|
+
canonical: { type: "string", title: "Canonical URL" },
|
|
703
|
+
favicon: { type: "string", title: "Favicon", format: "image-uri" },
|
|
704
|
+
noIndexing: { type: "boolean", title: "No Indexing" },
|
|
705
|
+
titleTemplate: { type: "string", title: "Title Template" },
|
|
706
|
+
descriptionTemplate: { type: "string", title: "Description Template" },
|
|
707
|
+
type: { type: "string", title: "Page Type" },
|
|
708
|
+
image: { type: "string", title: "OG Image", format: "image-uri" },
|
|
709
|
+
themeColor: { type: "string", title: "Theme Color", format: "color" },
|
|
710
|
+
jsonLDs: {
|
|
711
|
+
type: "array",
|
|
712
|
+
title: "JSON-LD Structured Data",
|
|
713
|
+
items: { type: "object", additionalProperties: true },
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
};
|
|
717
|
+
manifestBlocks[SEO_TYPE] = {
|
|
718
|
+
$ref: `#/definitions/${seoKey}`,
|
|
719
|
+
namespace: "website",
|
|
720
|
+
};
|
|
721
|
+
extraAnyOf.push({ $ref: `#/definitions/${seoKey}` });
|
|
722
|
+
|
|
723
|
+
// --- website/sections/Seo/SeoV2.tsx ---
|
|
724
|
+
const SEOV2_TYPE = "website/sections/Seo/SeoV2.tsx";
|
|
725
|
+
const seoV2Key = toBase64(SEOV2_TYPE);
|
|
726
|
+
definitions[seoV2Key] = {
|
|
727
|
+
title: SEOV2_TYPE,
|
|
728
|
+
type: "object",
|
|
729
|
+
required: ["__resolveType"],
|
|
730
|
+
properties: {
|
|
731
|
+
__resolveType: {
|
|
732
|
+
type: "string",
|
|
733
|
+
enum: [SEOV2_TYPE],
|
|
734
|
+
default: SEOV2_TYPE,
|
|
735
|
+
},
|
|
736
|
+
title: { type: "string", title: "Title" },
|
|
737
|
+
description: { type: "string", title: "Description" },
|
|
738
|
+
canonical: { type: "string", title: "Canonical URL" },
|
|
739
|
+
favicon: { type: "string", title: "Favicon", format: "image-uri" },
|
|
740
|
+
noIndexing: { type: "boolean", title: "No Indexing" },
|
|
741
|
+
titleTemplate: { type: "string", title: "Title Template" },
|
|
742
|
+
descriptionTemplate: { type: "string", title: "Description Template" },
|
|
743
|
+
type: { type: "string", title: "Page Type" },
|
|
744
|
+
image: { type: "string", title: "OG Image", format: "image-uri" },
|
|
745
|
+
themeColor: { type: "string", title: "Theme Color", format: "color" },
|
|
746
|
+
jsonLDs: {
|
|
747
|
+
type: "array",
|
|
748
|
+
title: "JSON-LD Structured Data",
|
|
749
|
+
items: { type: "object", additionalProperties: true },
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
};
|
|
753
|
+
manifestBlocks[SEOV2_TYPE] = {
|
|
754
|
+
$ref: `#/definitions/${seoV2Key}`,
|
|
755
|
+
namespace: "website",
|
|
756
|
+
};
|
|
757
|
+
extraAnyOf.push({ $ref: `#/definitions/${seoV2Key}` });
|
|
758
|
+
|
|
759
|
+
// --- website/flags/multivariate/section.ts ---
|
|
760
|
+
const MV_SECTION_TYPE = "website/flags/multivariate/section.ts";
|
|
761
|
+
const mvSectionKey = toBase64(MV_SECTION_TYPE);
|
|
762
|
+
definitions[mvSectionKey] = {
|
|
763
|
+
title: MV_SECTION_TYPE,
|
|
764
|
+
type: "object",
|
|
765
|
+
required: ["__resolveType"],
|
|
766
|
+
properties: {
|
|
767
|
+
__resolveType: {
|
|
768
|
+
type: "string",
|
|
769
|
+
enum: [MV_SECTION_TYPE],
|
|
770
|
+
default: MV_SECTION_TYPE,
|
|
771
|
+
},
|
|
772
|
+
variants: {
|
|
773
|
+
type: "array",
|
|
774
|
+
title: "Variants",
|
|
775
|
+
items: {
|
|
776
|
+
type: "object",
|
|
777
|
+
properties: {
|
|
778
|
+
rule: {
|
|
779
|
+
title: "Rule",
|
|
780
|
+
type: "object",
|
|
781
|
+
required: ["__resolveType"],
|
|
782
|
+
properties: { __resolveType: { type: "string" } },
|
|
783
|
+
additionalProperties: true,
|
|
784
|
+
},
|
|
785
|
+
value: {
|
|
786
|
+
title: "Section",
|
|
787
|
+
anyOf: sectionAnyOf,
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
manifestBlocks[MV_SECTION_TYPE] = {
|
|
795
|
+
$ref: `#/definitions/${mvSectionKey}`,
|
|
796
|
+
namespace: "website",
|
|
797
|
+
};
|
|
798
|
+
extraAnyOf.push({ $ref: `#/definitions/${mvSectionKey}` });
|
|
799
|
+
|
|
800
|
+
return { definitions, manifestBlocks, extraAnyOf };
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// ---------------------------------------------------------------------------
|
|
804
|
+
// Post-processing: wrap complex properties with Resolvable anyOf
|
|
805
|
+
// ---------------------------------------------------------------------------
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Walk all @Props definitions and wrap complex array/object properties
|
|
809
|
+
* with anyOf [Resolvable, original, ...matchingLoaders].
|
|
810
|
+
*
|
|
811
|
+
* In the deco CMS, ANY complex property can be replaced by a loader
|
|
812
|
+
* reference ({ __resolveType: "some/loader.ts", props: {...} }).
|
|
813
|
+
* This function ensures the schema accepts both inline data and
|
|
814
|
+
* loader references for all such properties.
|
|
815
|
+
*/
|
|
816
|
+
function wrapResolvableProperties(
|
|
817
|
+
definitions: Record<string, any>,
|
|
818
|
+
_loaderDefinitions: Record<string, any>,
|
|
819
|
+
) {
|
|
820
|
+
const resolvableRef = { $ref: `#/definitions/${RESOLVABLE_LITERAL_KEY}` };
|
|
821
|
+
|
|
822
|
+
const productLoaderRefs = getProductListLoaderKeys().map((key) => ({
|
|
823
|
+
$ref: `#/definitions/${toBase64(key)}`,
|
|
824
|
+
}));
|
|
825
|
+
|
|
826
|
+
for (const [defKey, def] of Object.entries(definitions)) {
|
|
827
|
+
if (!defKey.endsWith("@Props")) continue;
|
|
828
|
+
if (!def || !def.properties) continue;
|
|
829
|
+
|
|
830
|
+
for (const [propName, propSchema] of Object.entries(def.properties as Record<string, any>)) {
|
|
831
|
+
if (!propSchema || typeof propSchema !== "object") continue;
|
|
832
|
+
if (propSchema.anyOf || propSchema.$ref) continue;
|
|
833
|
+
|
|
834
|
+
const shouldWrap = isLoaderCompatibleProperty(propSchema);
|
|
835
|
+
if (!shouldWrap) continue;
|
|
836
|
+
|
|
837
|
+
const { nullable, title, hide, ...rest } = propSchema;
|
|
838
|
+
|
|
839
|
+
// Determine which loader refs to include based on property type
|
|
840
|
+
const loaderRefs = isProductArrayProperty(propSchema) ? productLoaderRefs : [];
|
|
841
|
+
|
|
842
|
+
const wrapped: any = {
|
|
843
|
+
anyOf: [resolvableRef, { ...rest, title: title || "Inline data" }, ...loaderRefs],
|
|
844
|
+
};
|
|
845
|
+
if (nullable) wrapped.nullable = true;
|
|
846
|
+
if (title) wrapped.title = title;
|
|
847
|
+
if (hide) wrapped.hide = hide;
|
|
848
|
+
|
|
849
|
+
def.properties[propName] = wrapped;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Also walk nested object properties (e.g., Tab.products inside tabs array items)
|
|
853
|
+
wrapNestedProperties(def, resolvableRef, productLoaderRefs);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function isLoaderCompatibleProperty(schema: any): boolean {
|
|
858
|
+
if (schema.type === "array" && schema.items?.type === "object") {
|
|
859
|
+
const propCount = Object.keys(schema.items.properties || {}).length;
|
|
860
|
+
return propCount > 3;
|
|
861
|
+
}
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function isProductArrayProperty(schema: any): boolean {
|
|
866
|
+
if (schema.type !== "array" || !schema.items?.properties) return false;
|
|
867
|
+
const itemProps = schema.items.properties;
|
|
868
|
+
return !!(itemProps.productID || itemProps.name || itemProps.offers || itemProps.brand);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Recursively walk nested object/array schemas to wrap deeply nested
|
|
873
|
+
* loader-compatible properties. Handles cases like Tab.products where
|
|
874
|
+
* the products field is inside an array item's object schema.
|
|
875
|
+
*/
|
|
876
|
+
function wrapNestedProperties(
|
|
877
|
+
schema: any,
|
|
878
|
+
resolvableRef: any,
|
|
879
|
+
productLoaderRefs: any[],
|
|
880
|
+
depth = 0,
|
|
881
|
+
) {
|
|
882
|
+
if (depth > 5 || !schema || typeof schema !== "object") return;
|
|
883
|
+
|
|
884
|
+
if (schema.type === "array" && schema.items?.type === "object" && schema.items.properties) {
|
|
885
|
+
for (const [propName, propSchema] of Object.entries(
|
|
886
|
+
schema.items.properties as Record<string, any>,
|
|
887
|
+
)) {
|
|
888
|
+
if (!propSchema || typeof propSchema !== "object") continue;
|
|
889
|
+
if (propSchema.anyOf || propSchema.$ref) continue;
|
|
890
|
+
|
|
891
|
+
if (isLoaderCompatibleProperty(propSchema)) {
|
|
892
|
+
const { nullable, title, hide, ...rest } = propSchema;
|
|
893
|
+
const loaderRefs = isProductArrayProperty(propSchema) ? productLoaderRefs : [];
|
|
894
|
+
const wrapped: any = {
|
|
895
|
+
anyOf: [resolvableRef, { ...rest, title: title || "Inline data" }, ...loaderRefs],
|
|
896
|
+
};
|
|
897
|
+
if (nullable) wrapped.nullable = true;
|
|
898
|
+
if (title) wrapped.title = title;
|
|
899
|
+
if (hide) wrapped.hide = hide;
|
|
900
|
+
schema.items.properties[propName] = wrapped;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
wrapNestedProperties(propSchema, resolvableRef, productLoaderRefs, depth + 1);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (schema.properties) {
|
|
908
|
+
for (const propSchema of Object.values(schema.properties)) {
|
|
909
|
+
wrapNestedProperties(propSchema as any, resolvableRef, productLoaderRefs, depth + 1);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// ---------------------------------------------------------------------------
|
|
915
|
+
// composeMeta
|
|
916
|
+
// ---------------------------------------------------------------------------
|
|
917
|
+
|
|
918
|
+
const SECTION_REF_DEF_KEY = "__SECTION_REF__";
|
|
919
|
+
|
|
920
|
+
export function composeMeta(siteMeta: MetaResponse): MetaResponse {
|
|
921
|
+
const siteAnyOf = siteMeta.schema?.root?.sections?.anyOf || [];
|
|
922
|
+
|
|
923
|
+
// Build all framework components
|
|
924
|
+
const fwSections = buildFrameworkSections(siteAnyOf);
|
|
925
|
+
const fullSectionAnyOf = [...siteAnyOf, ...fwSections.extraAnyOf];
|
|
926
|
+
const page = buildPageSchema(fullSectionAnyOf);
|
|
927
|
+
const loaders = buildLoaderDefinitions();
|
|
928
|
+
const actions = buildActionDefinitions();
|
|
929
|
+
const matchers = buildMatcherDefinitions();
|
|
930
|
+
|
|
931
|
+
const sectionRefDef = { title: "Section", anyOf: fullSectionAnyOf };
|
|
932
|
+
|
|
933
|
+
const resolvableDef = buildResolvableDefinition();
|
|
934
|
+
|
|
935
|
+
// Merge all definitions.
|
|
936
|
+
// Priority (last wins): auto-registered empty schemas → framework sections/pages →
|
|
937
|
+
// site-generated schemas (meta.gen.json, with full loader props) → framework-critical keys.
|
|
938
|
+
// This ensures detailed site-generated schemas always win over auto-registered stubs.
|
|
939
|
+
const allDefinitions: Record<string, any> = {
|
|
940
|
+
...loaders.definitions,
|
|
941
|
+
...actions.definitions,
|
|
942
|
+
...matchers.definitions,
|
|
943
|
+
...fwSections.definitions,
|
|
944
|
+
...page.definitions,
|
|
945
|
+
...(siteMeta.schema?.definitions || {}),
|
|
946
|
+
[SECTION_REF_DEF_KEY]: sectionRefDef,
|
|
947
|
+
[RESOLVABLE_LITERAL_KEY]: resolvableDef,
|
|
948
|
+
[RESOLVABLE_B64_KEY]: resolvableDef,
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
// Post-process: wrap complex section properties with Resolvable anyOf
|
|
952
|
+
wrapResolvableProperties(allDefinitions, loaders.definitions);
|
|
953
|
+
|
|
954
|
+
return {
|
|
955
|
+
...siteMeta,
|
|
956
|
+
framework: "tanstack-start",
|
|
957
|
+
manifest: {
|
|
958
|
+
blocks: {
|
|
959
|
+
...(siteMeta.manifest?.blocks || {}),
|
|
960
|
+
sections: {
|
|
961
|
+
...(siteMeta.manifest?.blocks?.sections || {}),
|
|
962
|
+
...fwSections.manifestBlocks,
|
|
963
|
+
},
|
|
964
|
+
pages: {
|
|
965
|
+
...(siteMeta.manifest?.blocks?.pages || {}),
|
|
966
|
+
...page.manifestBlocks,
|
|
967
|
+
},
|
|
968
|
+
loaders: {
|
|
969
|
+
...(siteMeta.manifest?.blocks?.loaders || {}),
|
|
970
|
+
...loaders.manifestBlocks,
|
|
971
|
+
},
|
|
972
|
+
actions: {
|
|
973
|
+
...(siteMeta.manifest?.blocks?.actions || {}),
|
|
974
|
+
...actions.manifestBlocks,
|
|
975
|
+
},
|
|
976
|
+
matchers: {
|
|
977
|
+
...(siteMeta.manifest?.blocks?.matchers || {}),
|
|
978
|
+
...matchers.manifestBlocks,
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
schema: {
|
|
983
|
+
definitions: allDefinitions,
|
|
984
|
+
root: {
|
|
985
|
+
...(siteMeta.schema?.root || {}),
|
|
986
|
+
sections: { anyOf: fullSectionAnyOf },
|
|
987
|
+
pages: { anyOf: page.rootAnyOf },
|
|
988
|
+
loaders: { anyOf: loaders.loaderAnyOf },
|
|
989
|
+
matchers: { anyOf: matchers.matcherAnyOf },
|
|
990
|
+
},
|
|
991
|
+
},
|
|
992
|
+
};
|
|
993
|
+
}
|