@shoppexio/builder-contracts 0.1.0 → 0.1.2
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/dist/builder-settings.d.ts +11 -666
- package/dist/builder-settings.d.ts.map +1 -1
- package/dist/builder-settings.js +2 -1
- package/dist/canonical-settings.d.ts +18 -0
- package/dist/canonical-settings.d.ts.map +1 -0
- package/dist/canonical-settings.js +106 -0
- package/dist/custom-pages.d.ts +15 -0
- package/dist/custom-pages.d.ts.map +1 -0
- package/dist/custom-pages.js +40 -0
- package/dist/dedicated-pages.d.ts +15 -0
- package/dist/dedicated-pages.d.ts.map +1 -0
- package/dist/dedicated-pages.js +142 -0
- package/dist/events.d.ts +35 -240
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +7 -0
- package/dist/fields.d.ts +229 -10
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +27 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/legacy-manifest.d.ts +18 -0
- package/dist/legacy-manifest.d.ts.map +1 -1
- package/dist/legacy-manifest.js +137 -22
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +55 -6
- package/dist/persistence.d.ts +7 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +58 -0
- package/dist/preview-boot.d.ts +68 -0
- package/dist/preview-boot.d.ts.map +1 -0
- package/dist/preview-boot.js +38 -0
- package/dist/preview-protocol.d.ts +227 -459
- package/dist/preview-protocol.d.ts.map +1 -1
- package/dist/preview-protocol.js +112 -0
- package/dist/preview-session-resolve.d.ts +115 -0
- package/dist/preview-session-resolve.d.ts.map +1 -0
- package/dist/preview-session-resolve.js +25 -0
- package/dist/preview-trusted-origins.d.ts +4 -0
- package/dist/preview-trusted-origins.d.ts.map +1 -0
- package/dist/preview-trusted-origins.js +28 -0
- package/dist/storefront-initial-data-html.d.ts +17 -0
- package/dist/storefront-initial-data-html.d.ts.map +1 -0
- package/dist/storefront-initial-data-html.js +83 -0
- package/dist/storefront-typography-fonts.d.ts +18 -0
- package/dist/storefront-typography-fonts.d.ts.map +1 -0
- package/dist/storefront-typography-fonts.js +89 -0
- package/dist/style-slots.d.ts +50 -152
- package/dist/style-slots.d.ts.map +1 -1
- package/dist/style-slots.js +80 -32
- package/dist/theme-manifest.d.ts +287 -456
- package/dist/theme-manifest.d.ts.map +1 -1
- package/dist/theme-manifest.js +92 -0
- package/dist/theme-schemes.d.ts +10 -0
- package/dist/theme-schemes.d.ts.map +1 -0
- package/dist/theme-schemes.js +25 -0
- package/dist/validation.d.ts +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +23 -12
- package/package.json +43 -1
- package/src/builder-contracts.test.ts +416 -3
- package/src/builder-settings.ts +4 -1
- package/src/canonical-settings.ts +156 -0
- package/src/custom-pages.test.ts +74 -0
- package/src/custom-pages.ts +70 -0
- package/src/dedicated-pages.test.ts +88 -0
- package/src/dedicated-pages.ts +173 -0
- package/src/events.ts +8 -0
- package/src/fields.ts +30 -0
- package/src/index.ts +10 -0
- package/src/legacy-manifest.ts +147 -23
- package/src/migrations.ts +70 -6
- package/src/persistence.ts +77 -0
- package/src/preview-boot.test.ts +72 -0
- package/src/preview-boot.ts +49 -0
- package/src/preview-protocol.test.ts +132 -0
- package/src/preview-protocol.ts +122 -0
- package/src/preview-session-resolve.test.ts +37 -0
- package/src/preview-session-resolve.ts +34 -0
- package/src/preview-trusted-origins.test.ts +24 -0
- package/src/preview-trusted-origins.ts +35 -0
- package/src/storefront-initial-data-html.test.ts +73 -0
- package/src/storefront-initial-data-html.ts +112 -0
- package/src/storefront-typography-fonts.test.ts +48 -0
- package/src/storefront-typography-fonts.ts +108 -0
- package/src/style-slots.ts +102 -34
- package/src/theme-manifest.ts +118 -1
- package/src/theme-schemes.ts +34 -0
- package/src/validation.ts +32 -13
- package/dist/builder-contracts.test.d.ts +0 -2
- package/dist/builder-contracts.test.d.ts.map +0 -1
- package/dist/builder-contracts.test.js +0 -361
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export type StorefrontCuratedFont = {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
stack: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const STOREFRONT_SYSTEM_FONTS = new Set(['Arial', 'Georgia', 'System UI']);
|
|
8
|
+
|
|
9
|
+
export const STOREFRONT_CURATED_FONTS: StorefrontCuratedFont[] = [
|
|
10
|
+
{ value: 'Inter', label: 'Inter', stack: 'Inter, system-ui, sans-serif' },
|
|
11
|
+
{ value: 'Geist', label: 'Geist', stack: 'Geist, system-ui, sans-serif' },
|
|
12
|
+
{ value: 'Manrope', label: 'Manrope', stack: 'Manrope, system-ui, sans-serif' },
|
|
13
|
+
{
|
|
14
|
+
value: 'Plus Jakarta Sans',
|
|
15
|
+
label: 'Plus Jakarta Sans',
|
|
16
|
+
stack: '"Plus Jakarta Sans", system-ui, sans-serif',
|
|
17
|
+
},
|
|
18
|
+
{ value: 'DM Sans', label: 'DM Sans', stack: '"DM Sans", system-ui, sans-serif' },
|
|
19
|
+
{
|
|
20
|
+
value: 'Space Grotesk',
|
|
21
|
+
label: 'Space Grotesk',
|
|
22
|
+
stack: '"Space Grotesk", system-ui, sans-serif',
|
|
23
|
+
},
|
|
24
|
+
{ value: 'Sora', label: 'Sora', stack: 'Sora, system-ui, sans-serif' },
|
|
25
|
+
{
|
|
26
|
+
value: 'IBM Plex Sans',
|
|
27
|
+
label: 'IBM Plex Sans',
|
|
28
|
+
stack: '"IBM Plex Sans", system-ui, sans-serif',
|
|
29
|
+
},
|
|
30
|
+
{ value: 'Arial', label: 'Arial', stack: 'Arial, sans-serif' },
|
|
31
|
+
{ value: 'Georgia', label: 'Georgia', stack: 'Georgia, serif' },
|
|
32
|
+
{ value: 'System UI', label: 'System UI', stack: 'system-ui, sans-serif' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const GOOGLE_FONT_FAMILY_PATTERN = /^[A-Za-z0-9 +\-_]+$/;
|
|
36
|
+
|
|
37
|
+
export function resolvePrimaryFontFamilyName(raw: unknown): string | null {
|
|
38
|
+
if (typeof raw !== 'string') return null;
|
|
39
|
+
const trimmed = raw.trim();
|
|
40
|
+
if (!trimmed) return null;
|
|
41
|
+
|
|
42
|
+
const exact = STOREFRONT_CURATED_FONTS.find(
|
|
43
|
+
(font) => font.value === trimmed || font.stack === trimmed,
|
|
44
|
+
);
|
|
45
|
+
if (exact) return exact.value;
|
|
46
|
+
|
|
47
|
+
const prefix = STOREFRONT_CURATED_FONTS.find((font) => trimmed.startsWith(`${font.value},`));
|
|
48
|
+
if (prefix) return prefix.value;
|
|
49
|
+
|
|
50
|
+
const quoted = trimmed.match(/^["'](.+?)["']/);
|
|
51
|
+
if (quoted?.[1]) {
|
|
52
|
+
return quoted[1].trim() || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const primary = trimmed.split(',')[0]?.trim();
|
|
56
|
+
return primary && primary.length > 0 ? primary : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveCuratedFontValue(raw: unknown, fallback: string): string {
|
|
60
|
+
return resolvePrimaryFontFamilyName(raw) ?? fallback;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function curatedFontOptionsForValue(value: string): StorefrontCuratedFont[] {
|
|
64
|
+
if (STOREFRONT_CURATED_FONTS.some((font) => font.value === value)) {
|
|
65
|
+
return STOREFRONT_CURATED_FONTS;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return [
|
|
69
|
+
...STOREFRONT_CURATED_FONTS,
|
|
70
|
+
{
|
|
71
|
+
value,
|
|
72
|
+
label: value,
|
|
73
|
+
stack: value.includes(',') ? value : `${value}, system-ui, sans-serif`,
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function findStorefrontCuratedFont(value: string): StorefrontCuratedFont | undefined {
|
|
79
|
+
return curatedFontOptionsForValue(value).find((font) => font.value === value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isValidGoogleFontFamily(value: unknown): value is string {
|
|
83
|
+
if (typeof value !== 'string') return false;
|
|
84
|
+
const trimmed = value.trim();
|
|
85
|
+
return trimmed.length > 0 && trimmed.length <= 60 && GOOGLE_FONT_FAMILY_PATTERN.test(trimmed);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function buildStorefrontGoogleFontHref(family: string): string {
|
|
89
|
+
const familyParam = family.trim().replace(/\s+/g, '+');
|
|
90
|
+
return `https://fonts.googleapis.com/css2?family=${familyParam}:wght@400;500;600;700&display=swap`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getStorefrontGoogleFontHrefs(input: {
|
|
94
|
+
bodyFont?: unknown;
|
|
95
|
+
headingFont?: unknown;
|
|
96
|
+
}): string[] {
|
|
97
|
+
const families = new Set<string>();
|
|
98
|
+
|
|
99
|
+
for (const raw of [input.bodyFont, input.headingFont]) {
|
|
100
|
+
const family = resolvePrimaryFontFamilyName(raw);
|
|
101
|
+
if (!family) continue;
|
|
102
|
+
if (STOREFRONT_SYSTEM_FONTS.has(family)) continue;
|
|
103
|
+
if (!isValidGoogleFontFamily(family)) continue;
|
|
104
|
+
families.add(family);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return [...families].map((family) => buildStorefrontGoogleFontHref(family));
|
|
108
|
+
}
|
package/src/style-slots.ts
CHANGED
|
@@ -25,9 +25,12 @@ export const ResponsiveStringSchema = z
|
|
|
25
25
|
.strict();
|
|
26
26
|
export type ResponsiveString = z.infer<typeof ResponsiveStringSchema>;
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
29
|
+
|
|
30
|
+
export const ColorSchema = z.union([
|
|
31
|
+
z.literal('transparent'),
|
|
32
|
+
z.string().regex(HEX_COLOR_PATTERN, 'Expected a hex color'),
|
|
33
|
+
]);
|
|
31
34
|
export type Color = z.infer<typeof ColorSchema>;
|
|
32
35
|
|
|
33
36
|
export const FontWeightSchema = z.union([
|
|
@@ -43,7 +46,7 @@ export const FontWeightSchema = z.union([
|
|
|
43
46
|
]);
|
|
44
47
|
export type FontWeight = z.infer<typeof FontWeightSchema>;
|
|
45
48
|
|
|
46
|
-
export const
|
|
49
|
+
export const CoreStyleSlotIdSchema = z.enum([
|
|
47
50
|
'button.radius',
|
|
48
51
|
'button.background',
|
|
49
52
|
'button.foreground',
|
|
@@ -69,39 +72,104 @@ export const StyleSlotIdSchema = z.enum([
|
|
|
69
72
|
'typography.heading.weight',
|
|
70
73
|
'typography.body.size',
|
|
71
74
|
]);
|
|
72
|
-
export type
|
|
75
|
+
export type CoreStyleSlotId = z.infer<typeof CoreStyleSlotIdSchema>;
|
|
76
|
+
|
|
77
|
+
export const ThemeStyleSlotIdSchema = z
|
|
78
|
+
.string()
|
|
79
|
+
.regex(/^theme\.[a-z0-9][a-z0-9._-]*$/, 'Expected a theme-scoped style slot id');
|
|
80
|
+
export type ThemeStyleSlotId = `theme.${string}`;
|
|
81
|
+
|
|
82
|
+
export const StyleSlotIdSchema = z.union([CoreStyleSlotIdSchema, ThemeStyleSlotIdSchema]);
|
|
83
|
+
export type StyleSlotId = CoreStyleSlotId | ThemeStyleSlotId;
|
|
84
|
+
|
|
85
|
+
export const CORE_STYLE_SLOT_IDS = CoreStyleSlotIdSchema.options;
|
|
86
|
+
|
|
87
|
+
export const ThemeStyleSlotValueSchema = z.union([
|
|
88
|
+
ColorSchema,
|
|
89
|
+
ResponsiveNumberSchema,
|
|
90
|
+
ResponsiveStringSchema,
|
|
91
|
+
FontWeightSchema,
|
|
92
|
+
z.string().min(1),
|
|
93
|
+
z.number().finite(),
|
|
94
|
+
z.boolean(),
|
|
95
|
+
]);
|
|
96
|
+
export type ThemeStyleSlotValue = z.infer<typeof ThemeStyleSlotValueSchema>;
|
|
97
|
+
|
|
98
|
+
export type StyleSlotValue =
|
|
99
|
+
| Color
|
|
100
|
+
| ResponsiveNumber
|
|
101
|
+
| ResponsiveString
|
|
102
|
+
| FontWeight
|
|
103
|
+
| string
|
|
104
|
+
| number
|
|
105
|
+
| boolean;
|
|
73
106
|
|
|
74
|
-
|
|
107
|
+
const CORE_STYLE_SLOT_SCHEMAS: Record<CoreStyleSlotId, z.ZodType<unknown>> = {
|
|
108
|
+
'button.radius': ResponsiveNumberSchema,
|
|
109
|
+
'button.background': ColorSchema,
|
|
110
|
+
'button.foreground': ColorSchema,
|
|
111
|
+
'button.border': ColorSchema,
|
|
112
|
+
'button.font.weight': FontWeightSchema,
|
|
113
|
+
'input.radius': ResponsiveNumberSchema,
|
|
114
|
+
'input.height': ResponsiveNumberSchema,
|
|
115
|
+
'input.border': ColorSchema,
|
|
116
|
+
'input.background': ColorSchema,
|
|
117
|
+
'input.foreground': ColorSchema,
|
|
118
|
+
'card.radius': ResponsiveNumberSchema,
|
|
119
|
+
'card.background': ColorSchema,
|
|
120
|
+
'card.border': ColorSchema,
|
|
121
|
+
'section.padding.y': ResponsiveNumberSchema,
|
|
122
|
+
'section.padding.x': ResponsiveNumberSchema,
|
|
123
|
+
'container.width': ResponsiveNumberSchema,
|
|
124
|
+
'color.primary': ColorSchema,
|
|
125
|
+
'color.accent': ColorSchema,
|
|
126
|
+
'color.background': ColorSchema,
|
|
127
|
+
'color.foreground': ColorSchema,
|
|
128
|
+
'color.muted': ColorSchema,
|
|
129
|
+
'link.color': ColorSchema,
|
|
130
|
+
'typography.heading.weight': FontWeightSchema,
|
|
131
|
+
'typography.body.size': ResponsiveNumberSchema,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export type StyleSlots = Partial<Record<StyleSlotId, StyleSlotValue>>;
|
|
75
135
|
|
|
76
136
|
export const StyleSlotsSchema = z
|
|
77
|
-
.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
137
|
+
.record(z.string().min(1), z.unknown())
|
|
138
|
+
.superRefine((slots, ctx) => {
|
|
139
|
+
for (const [slotId, value] of Object.entries(slots)) {
|
|
140
|
+
const coreSlotId = CoreStyleSlotIdSchema.safeParse(slotId);
|
|
141
|
+
if (coreSlotId.success) {
|
|
142
|
+
const parsedValue = CORE_STYLE_SLOT_SCHEMAS[coreSlotId.data].safeParse(value);
|
|
143
|
+
if (!parsedValue.success) {
|
|
144
|
+
ctx.addIssue({
|
|
145
|
+
code: 'custom',
|
|
146
|
+
path: [slotId],
|
|
147
|
+
message: `Invalid value for core style slot "${slotId}"`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const themeSlotId = ThemeStyleSlotIdSchema.safeParse(slotId);
|
|
154
|
+
if (!themeSlotId.success) {
|
|
155
|
+
ctx.addIssue({
|
|
156
|
+
code: 'custom',
|
|
157
|
+
path: [slotId],
|
|
158
|
+
message: `Unknown style slot "${slotId}"`,
|
|
159
|
+
});
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const parsedThemeValue = ThemeStyleSlotValueSchema.safeParse(value);
|
|
164
|
+
if (!parsedThemeValue.success) {
|
|
165
|
+
ctx.addIssue({
|
|
166
|
+
code: 'custom',
|
|
167
|
+
path: [slotId],
|
|
168
|
+
message: `Invalid value for theme style slot "${slotId}"`,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}) as z.ZodType<StyleSlots>;
|
|
105
173
|
|
|
106
174
|
export const StyleSlotDefaultsSchema = StyleSlotsSchema;
|
|
107
175
|
export type StyleSlotDefaults = StyleSlots;
|
package/src/theme-manifest.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as z from 'zod/v4';
|
|
2
2
|
import { BlockSettingsSchema } from './fields.ts';
|
|
3
|
-
import { StyleSlotDefaultsSchema, StyleSlotIdSchema } from './style-slots.ts';
|
|
3
|
+
import { StyleSlotDefaultsSchema, StyleSlotIdSchema, type StyleSlotDefaults } from './style-slots.ts';
|
|
4
4
|
|
|
5
5
|
export const ThemeIdSchema = z.string().min(1).regex(/^[a-z0-9][a-z0-9-_.]*$/);
|
|
6
6
|
export type ThemeId = z.infer<typeof ThemeIdSchema>;
|
|
@@ -62,6 +62,7 @@ export const ThemePresetSchema = z
|
|
|
62
62
|
.object({
|
|
63
63
|
label: z.string().min(1),
|
|
64
64
|
description: z.string().min(1).optional(),
|
|
65
|
+
preview: z.string().min(1).optional(),
|
|
65
66
|
content: z.record(z.string().min(1), z.unknown()).default({}),
|
|
66
67
|
layout: z.record(z.string().min(1), z.unknown()).default({}),
|
|
67
68
|
style_slots: StyleSlotDefaultsSchema.default({}),
|
|
@@ -69,15 +70,63 @@ export const ThemePresetSchema = z
|
|
|
69
70
|
.strict();
|
|
70
71
|
export type ThemePreset = z.infer<typeof ThemePresetSchema>;
|
|
71
72
|
|
|
73
|
+
export const ThemeManifestMetadataSchema = z
|
|
74
|
+
.object({
|
|
75
|
+
author: z.string().min(1).optional(),
|
|
76
|
+
description: z.string().min(1).optional(),
|
|
77
|
+
preview: z.string().min(1).optional(),
|
|
78
|
+
features: z.array(z.string().min(1)).optional(),
|
|
79
|
+
techStack: z.array(z.string().min(1)).optional(),
|
|
80
|
+
templates: z.array(z.string().min(1)).optional(),
|
|
81
|
+
createdAt: z.string().min(1).optional(),
|
|
82
|
+
demoUrl: z.string().min(1).optional(),
|
|
83
|
+
audience: z.string().min(1).optional(),
|
|
84
|
+
tags: z.array(z.string().min(1)).optional(),
|
|
85
|
+
hotfixPaths: z.array(z.string().min(1)).optional(),
|
|
86
|
+
})
|
|
87
|
+
.strict();
|
|
88
|
+
export type ThemeManifestMetadata = z.infer<typeof ThemeManifestMetadataSchema>;
|
|
89
|
+
|
|
90
|
+
export const ThemeManifestLinkGroupSchema = z
|
|
91
|
+
.object({
|
|
92
|
+
path: z.string().min(1),
|
|
93
|
+
label: z.string().min(1),
|
|
94
|
+
description: z.string().min(1).optional(),
|
|
95
|
+
pageIds: z.array(z.string().min(1)).optional(),
|
|
96
|
+
})
|
|
97
|
+
.strict();
|
|
98
|
+
export type ThemeManifestLinkGroup = z.infer<typeof ThemeManifestLinkGroupSchema>;
|
|
99
|
+
|
|
100
|
+
export const ThemeManifestLinkItemSchema = z
|
|
101
|
+
.object({
|
|
102
|
+
label: z.string().min(1),
|
|
103
|
+
href: z.string().min(1),
|
|
104
|
+
})
|
|
105
|
+
.strict();
|
|
106
|
+
export type ThemeManifestLinkItem = z.infer<typeof ThemeManifestLinkItemSchema>;
|
|
107
|
+
|
|
72
108
|
export const ThemeManifestSchema = z
|
|
73
109
|
.object({
|
|
74
110
|
id: ThemeIdSchema,
|
|
75
111
|
name: z.string().min(1),
|
|
76
112
|
version: z.string().min(1),
|
|
113
|
+
author: ThemeManifestMetadataSchema.shape.author,
|
|
114
|
+
description: ThemeManifestMetadataSchema.shape.description,
|
|
115
|
+
preview: ThemeManifestMetadataSchema.shape.preview,
|
|
116
|
+
features: ThemeManifestMetadataSchema.shape.features,
|
|
117
|
+
techStack: ThemeManifestMetadataSchema.shape.techStack,
|
|
118
|
+
templates: ThemeManifestMetadataSchema.shape.templates,
|
|
119
|
+
createdAt: ThemeManifestMetadataSchema.shape.createdAt,
|
|
120
|
+
demoUrl: ThemeManifestMetadataSchema.shape.demoUrl,
|
|
121
|
+
audience: ThemeManifestMetadataSchema.shape.audience,
|
|
122
|
+
tags: ThemeManifestMetadataSchema.shape.tags,
|
|
123
|
+
hotfixPaths: ThemeManifestMetadataSchema.shape.hotfixPaths,
|
|
77
124
|
pages: z.record(z.string().min(1), ManifestPageSchema),
|
|
78
125
|
blocks: z.record(z.string().min(1), ManifestBlockSchema),
|
|
79
126
|
styleSlots: StyleSlotDefaultsSchema.default({}),
|
|
80
127
|
presets: z.record(z.string().min(1), ThemePresetSchema).default({}),
|
|
128
|
+
linkGroups: z.array(ThemeManifestLinkGroupSchema).optional(),
|
|
129
|
+
defaultLinkItems: z.record(z.string().min(1), z.array(ThemeManifestLinkItemSchema)).optional(),
|
|
81
130
|
})
|
|
82
131
|
.strict()
|
|
83
132
|
.superRefine((manifest, ctx) => {
|
|
@@ -138,3 +187,71 @@ export type ThemeManifest = z.infer<typeof ThemeManifestSchema>;
|
|
|
138
187
|
export function parseThemeManifest(input: unknown): ThemeManifest {
|
|
139
188
|
return ThemeManifestSchema.parse(input);
|
|
140
189
|
}
|
|
190
|
+
|
|
191
|
+
export type ThemeManifestPresetListItem = {
|
|
192
|
+
id: string;
|
|
193
|
+
name: string;
|
|
194
|
+
description?: string;
|
|
195
|
+
preview?: string;
|
|
196
|
+
overrides: {
|
|
197
|
+
content?: Record<string, unknown>;
|
|
198
|
+
layout?: Record<string, unknown>;
|
|
199
|
+
tokens?: Record<string, unknown>;
|
|
200
|
+
style_slots?: StyleSlotDefaults;
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export function listThemeManifestPresets(input: unknown): ThemeManifestPresetListItem[] {
|
|
205
|
+
if (!isRecord(input) || !isRecord(input.presets)) {
|
|
206
|
+
if (isRecord(input) && Array.isArray(input.presets)) {
|
|
207
|
+
return input.presets.flatMap((preset) => normalizeLegacyPresetListItem(preset));
|
|
208
|
+
}
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const manifest = ThemeManifestSchema.safeParse(input);
|
|
213
|
+
if (!manifest.success) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return Object.entries(manifest.data.presets).map(([id, preset]) => ({
|
|
218
|
+
id,
|
|
219
|
+
name: preset.label,
|
|
220
|
+
...(preset.description ? { description: preset.description } : {}),
|
|
221
|
+
...(preset.preview ? { preview: preset.preview } : {}),
|
|
222
|
+
overrides: {
|
|
223
|
+
...(Object.keys(preset.content).length > 0 ? { content: preset.content } : {}),
|
|
224
|
+
...(Object.keys(preset.layout).length > 0 ? { layout: preset.layout } : {}),
|
|
225
|
+
...(Object.keys(preset.style_slots).length > 0 ? { style_slots: preset.style_slots } : {}),
|
|
226
|
+
},
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function normalizeLegacyPresetListItem(input: unknown): ThemeManifestPresetListItem[] {
|
|
231
|
+
if (!isRecord(input) || typeof input.id !== 'string') {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const overrides = isRecord(input.overrides) ? input.overrides : {};
|
|
236
|
+
const styleSlots = StyleSlotDefaultsSchema.safeParse(overrides.style_slots ?? overrides.styleSlots);
|
|
237
|
+
return [{
|
|
238
|
+
id: input.id,
|
|
239
|
+
name: typeof input.name === 'string'
|
|
240
|
+
? input.name
|
|
241
|
+
: typeof input.label === 'string'
|
|
242
|
+
? input.label
|
|
243
|
+
: input.id,
|
|
244
|
+
...(typeof input.description === 'string' ? { description: input.description } : {}),
|
|
245
|
+
...(typeof input.preview === 'string' ? { preview: input.preview } : {}),
|
|
246
|
+
overrides: {
|
|
247
|
+
...(isRecord(overrides.content) ? { content: overrides.content } : {}),
|
|
248
|
+
...(isRecord(overrides.layout) ? { layout: overrides.layout } : {}),
|
|
249
|
+
...(isRecord(overrides.tokens) ? { tokens: overrides.tokens } : {}),
|
|
250
|
+
...(styleSlots.success && Object.keys(styleSlots.data).length > 0 ? { style_slots: styleSlots.data } : {}),
|
|
251
|
+
},
|
|
252
|
+
}];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
256
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
257
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const PUBLIC_BUILDER_THEME_SCHEMES = [
|
|
2
|
+
'default',
|
|
3
|
+
'classic',
|
|
4
|
+
'nebula',
|
|
5
|
+
'pulse',
|
|
6
|
+
'phantom',
|
|
7
|
+
'starlight',
|
|
8
|
+
'apex',
|
|
9
|
+
'vault',
|
|
10
|
+
'clean-minimal',
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
export const STARTER_BUILDER_THEME_SCHEMES = ['blank'] as const;
|
|
14
|
+
|
|
15
|
+
export const BUILDER_READY_THEME_SCHEMES = [
|
|
16
|
+
...STARTER_BUILDER_THEME_SCHEMES,
|
|
17
|
+
...PUBLIC_BUILDER_THEME_SCHEMES,
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
export type PublicBuilderThemeScheme = (typeof PUBLIC_BUILDER_THEME_SCHEMES)[number];
|
|
21
|
+
export type StarterBuilderThemeScheme = (typeof STARTER_BUILDER_THEME_SCHEMES)[number];
|
|
22
|
+
export type BuilderReadyThemeScheme = (typeof BUILDER_READY_THEME_SCHEMES)[number];
|
|
23
|
+
|
|
24
|
+
export function isPublicBuilderThemeScheme(value: string): value is PublicBuilderThemeScheme {
|
|
25
|
+
return (PUBLIC_BUILDER_THEME_SCHEMES as readonly string[]).includes(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isStarterBuilderThemeScheme(value: string): value is StarterBuilderThemeScheme {
|
|
29
|
+
return (STARTER_BUILDER_THEME_SCHEMES as readonly string[]).includes(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isBuilderReadyThemeScheme(value: string): value is BuilderReadyThemeScheme {
|
|
33
|
+
return (BUILDER_READY_THEME_SCHEMES as readonly string[]).includes(value);
|
|
34
|
+
}
|
package/src/validation.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { BlockInstance, BuilderSettings } from './builder-settings.ts';
|
|
2
|
+
import { mergeCustomPagesIntoManifest } from './custom-pages.ts';
|
|
3
|
+
import { ThemeStyleSlotIdSchema } from './style-slots.ts';
|
|
2
4
|
import type { ThemeManifest } from './theme-manifest.ts';
|
|
3
5
|
|
|
4
6
|
export type BuilderManifestValidationIssueCode =
|
|
@@ -9,6 +11,7 @@ export type BuilderManifestValidationIssueCode =
|
|
|
9
11
|
| 'too_many_block_instances'
|
|
10
12
|
| 'unknown_block_variant'
|
|
11
13
|
| 'unknown_block_setting'
|
|
14
|
+
| 'unknown_style_slot'
|
|
12
15
|
| 'unexposed_style_slot';
|
|
13
16
|
|
|
14
17
|
export type BuilderManifestValidationIssue = {
|
|
@@ -31,10 +34,13 @@ export function validateBuilderSettingsAgainstManifest(
|
|
|
31
34
|
settings: BuilderSettings,
|
|
32
35
|
manifest: ThemeManifest,
|
|
33
36
|
): BuilderManifestValidationIssue[] {
|
|
37
|
+
const effectiveManifest = mergeCustomPagesIntoManifest(manifest, settings);
|
|
34
38
|
const issues: BuilderManifestValidationIssue[] = [];
|
|
35
39
|
|
|
40
|
+
validateGlobalStyleSlots(settings, effectiveManifest, issues);
|
|
41
|
+
|
|
36
42
|
for (const [pageId, layout] of Object.entries(settings.theme.layout)) {
|
|
37
|
-
const page =
|
|
43
|
+
const page = effectiveManifest.pages[pageId];
|
|
38
44
|
if (!page) {
|
|
39
45
|
issues.push({
|
|
40
46
|
code: 'unknown_page',
|
|
@@ -59,7 +65,7 @@ export function validateBuilderSettingsAgainstManifest(
|
|
|
59
65
|
}
|
|
60
66
|
blockIds.add(block.id);
|
|
61
67
|
|
|
62
|
-
const blockDefinition =
|
|
68
|
+
const blockDefinition = effectiveManifest.blocks[block.type];
|
|
63
69
|
if (!blockDefinition) {
|
|
64
70
|
issues.push({
|
|
65
71
|
code: 'unknown_block_type',
|
|
@@ -84,7 +90,7 @@ export function validateBuilderSettingsAgainstManifest(
|
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
for (const [blockType, count] of blockTypeCounts.entries()) {
|
|
87
|
-
const maxInstances =
|
|
93
|
+
const maxInstances = effectiveManifest.blocks[blockType]?.maxInstances;
|
|
88
94
|
if (maxInstances !== undefined && count > maxInstances) {
|
|
89
95
|
issues.push({
|
|
90
96
|
code: 'too_many_block_instances',
|
|
@@ -98,6 +104,28 @@ export function validateBuilderSettingsAgainstManifest(
|
|
|
98
104
|
return issues;
|
|
99
105
|
}
|
|
100
106
|
|
|
107
|
+
function validateGlobalStyleSlots(
|
|
108
|
+
settings: BuilderSettings,
|
|
109
|
+
manifest: ThemeManifest,
|
|
110
|
+
issues: BuilderManifestValidationIssue[],
|
|
111
|
+
): void {
|
|
112
|
+
const declaredThemeSlots = new Set(Object.keys(manifest.styleSlots));
|
|
113
|
+
|
|
114
|
+
for (const slotId of Object.keys(settings.theme.style_slots)) {
|
|
115
|
+
if (!ThemeStyleSlotIdSchema.safeParse(slotId).success) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!declaredThemeSlots.has(slotId)) {
|
|
120
|
+
issues.push({
|
|
121
|
+
code: 'unknown_style_slot',
|
|
122
|
+
path: `theme.style_slots.${slotId}`,
|
|
123
|
+
message: `Theme style slot "${slotId}" is not declared by the manifest`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
101
129
|
export function assertBuilderSettingsMatchManifest(
|
|
102
130
|
settings: BuilderSettings,
|
|
103
131
|
manifest: ThemeManifest,
|
|
@@ -130,11 +158,7 @@ function validateBlockSettings(
|
|
|
130
158
|
return;
|
|
131
159
|
}
|
|
132
160
|
|
|
133
|
-
const allowedSettings = new Set
|
|
134
|
-
for (const path of settingPaths) {
|
|
135
|
-
allowedSettings.add(path);
|
|
136
|
-
allowedSettings.add(getShortSettingKey(path));
|
|
137
|
-
}
|
|
161
|
+
const allowedSettings = new Set(settingPaths);
|
|
138
162
|
|
|
139
163
|
for (const key of Object.keys(block.settings)) {
|
|
140
164
|
if (allowedSettings.has(key)) {
|
|
@@ -168,11 +192,6 @@ function validateBlockVariant(
|
|
|
168
192
|
}
|
|
169
193
|
}
|
|
170
194
|
|
|
171
|
-
function getShortSettingKey(path: string): string {
|
|
172
|
-
const parts = path.split('.').filter(Boolean);
|
|
173
|
-
return parts.at(-1) ?? path;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
195
|
function validateBlockStyleOverrides(
|
|
177
196
|
block: BlockInstance,
|
|
178
197
|
exposedStyleSlots: string[],
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"builder-contracts.test.d.ts","sourceRoot":"","sources":["../src/builder-contracts.test.ts"],"names":[],"mappings":""}
|