@shoppexio/builder-contracts 0.1.1 → 0.1.3

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.
Files changed (77) hide show
  1. package/dist/builder-settings.d.ts +2 -0
  2. package/dist/builder-settings.d.ts.map +1 -1
  3. package/dist/builder-settings.js +2 -1
  4. package/dist/custom-pages.d.ts +15 -0
  5. package/dist/custom-pages.d.ts.map +1 -0
  6. package/dist/custom-pages.js +40 -0
  7. package/dist/dedicated-pages.d.ts +15 -0
  8. package/dist/dedicated-pages.d.ts.map +1 -0
  9. package/dist/dedicated-pages.js +142 -0
  10. package/dist/fields.d.ts +64 -6
  11. package/dist/fields.d.ts.map +1 -1
  12. package/dist/fields.js +4 -4
  13. package/dist/index.d.ts +3 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -0
  16. package/dist/legacy-manifest.d.ts +7 -0
  17. package/dist/legacy-manifest.d.ts.map +1 -1
  18. package/dist/legacy-manifest.js +31 -6
  19. package/dist/migrations.d.ts +1 -0
  20. package/dist/migrations.d.ts.map +1 -1
  21. package/dist/migrations.js +30 -2
  22. package/dist/persistence.d.ts +9 -0
  23. package/dist/persistence.d.ts.map +1 -1
  24. package/dist/persistence.js +80 -0
  25. package/dist/preview-boot.d.ts +2 -2
  26. package/dist/preview-boot.d.ts.map +1 -1
  27. package/dist/preview-boot.js +3 -1
  28. package/dist/preview-protocol.d.ts +88 -4
  29. package/dist/preview-protocol.d.ts.map +1 -1
  30. package/dist/preview-protocol.js +57 -7
  31. package/dist/preview-session-resolve.d.ts +2 -2
  32. package/dist/preview-session-resolve.d.ts.map +1 -1
  33. package/dist/preview-session-resolve.js +2 -2
  34. package/dist/preview-trusted-origins.d.ts.map +1 -1
  35. package/dist/preview-trusted-origins.js +10 -8
  36. package/dist/storefront-initial-data-html.js +1 -1
  37. package/dist/storefront-typography-fonts.d.ts +18 -0
  38. package/dist/storefront-typography-fonts.d.ts.map +1 -0
  39. package/dist/storefront-typography-fonts.js +89 -0
  40. package/dist/style-slots.d.ts +2 -2
  41. package/dist/style-slots.d.ts.map +1 -1
  42. package/dist/style-slots.js +5 -3
  43. package/dist/theme-manifest.d.ts +60 -4
  44. package/dist/theme-manifest.d.ts.map +1 -1
  45. package/dist/theme-schemes.d.ts +2 -2
  46. package/dist/theme-schemes.d.ts.map +1 -1
  47. package/dist/theme-schemes.js +2 -0
  48. package/dist/validation.d.ts.map +1 -1
  49. package/dist/validation.js +6 -4
  50. package/package.json +1 -1
  51. package/src/builder-contracts.test.ts +66 -0
  52. package/src/builder-settings.ts +4 -1
  53. package/src/custom-pages.test.ts +74 -0
  54. package/src/custom-pages.ts +70 -0
  55. package/src/dedicated-pages.test.ts +88 -0
  56. package/src/dedicated-pages.ts +173 -0
  57. package/src/fields.ts +4 -4
  58. package/src/index.ts +3 -0
  59. package/src/legacy-manifest.ts +40 -7
  60. package/src/migrations.ts +41 -2
  61. package/src/persistence.ts +107 -0
  62. package/src/preview-boot.test.ts +72 -0
  63. package/src/preview-boot.ts +3 -1
  64. package/src/preview-protocol.test.ts +90 -0
  65. package/src/preview-protocol.ts +67 -8
  66. package/src/preview-session-resolve.test.ts +37 -0
  67. package/src/preview-session-resolve.ts +2 -2
  68. package/src/storefront-initial-data-html.test.ts +18 -0
  69. package/src/storefront-initial-data-html.ts +1 -1
  70. package/src/storefront-typography-fonts.test.ts +48 -0
  71. package/src/storefront-typography-fonts.ts +108 -0
  72. package/src/style-slots.ts +6 -3
  73. package/src/theme-schemes.ts +2 -0
  74. package/src/validation.ts +6 -4
  75. package/dist/builder-contracts.test.d.ts +0 -2
  76. package/dist/builder-contracts.test.d.ts.map +0 -1
  77. package/dist/builder-contracts.test.js +0 -431
@@ -25,9 +25,12 @@ export const ResponsiveStringSchema = z
25
25
  .strict();
26
26
  export type ResponsiveString = z.infer<typeof ResponsiveStringSchema>;
27
27
 
28
- export const ColorSchema = z
29
- .string()
30
- .regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, 'Expected a hex color');
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([
@@ -7,6 +7,8 @@ export const PUBLIC_BUILDER_THEME_SCHEMES = [
7
7
  'starlight',
8
8
  'apex',
9
9
  'vault',
10
+ 'vortex',
11
+ 'clean-minimal',
10
12
  ] as const;
11
13
 
12
14
  export const STARTER_BUILDER_THEME_SCHEMES = ['blank'] as const;
package/src/validation.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { BlockInstance, BuilderSettings } from './builder-settings.ts';
2
+ import { mergeCustomPagesIntoManifest } from './custom-pages.ts';
2
3
  import { ThemeStyleSlotIdSchema } from './style-slots.ts';
3
4
  import type { ThemeManifest } from './theme-manifest.ts';
4
5
 
@@ -33,12 +34,13 @@ export function validateBuilderSettingsAgainstManifest(
33
34
  settings: BuilderSettings,
34
35
  manifest: ThemeManifest,
35
36
  ): BuilderManifestValidationIssue[] {
37
+ const effectiveManifest = mergeCustomPagesIntoManifest(manifest, settings);
36
38
  const issues: BuilderManifestValidationIssue[] = [];
37
39
 
38
- validateGlobalStyleSlots(settings, manifest, issues);
40
+ validateGlobalStyleSlots(settings, effectiveManifest, issues);
39
41
 
40
42
  for (const [pageId, layout] of Object.entries(settings.theme.layout)) {
41
- const page = manifest.pages[pageId];
43
+ const page = effectiveManifest.pages[pageId];
42
44
  if (!page) {
43
45
  issues.push({
44
46
  code: 'unknown_page',
@@ -63,7 +65,7 @@ export function validateBuilderSettingsAgainstManifest(
63
65
  }
64
66
  blockIds.add(block.id);
65
67
 
66
- const blockDefinition = manifest.blocks[block.type];
68
+ const blockDefinition = effectiveManifest.blocks[block.type];
67
69
  if (!blockDefinition) {
68
70
  issues.push({
69
71
  code: 'unknown_block_type',
@@ -88,7 +90,7 @@ export function validateBuilderSettingsAgainstManifest(
88
90
  }
89
91
 
90
92
  for (const [blockType, count] of blockTypeCounts.entries()) {
91
- const maxInstances = manifest.blocks[blockType]?.maxInstances;
93
+ const maxInstances = effectiveManifest.blocks[blockType]?.maxInstances;
92
94
  if (maxInstances !== undefined && count > maxInstances) {
93
95
  issues.push({
94
96
  code: 'too_many_block_instances',
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=builder-contracts.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"builder-contracts.test.d.ts","sourceRoot":"","sources":["../src/builder-contracts.test.ts"],"names":[],"mappings":""}
@@ -1,431 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { BuilderEventSchema, BuilderSettingsSchema, PreviewResponseSchema, PreviewMessageSchema, StyleSlotsSchema, ThemeManifestSchema, applyManifestDefaultLayout, validateBuilderSettingsAgainstManifest, createBlockInstance, createEmptyBuilderSettings, convertLegacyThemeManifest, migrateLegacyBuilderSettings, } from "./index.js";
3
- describe('@shoppex/builder-contracts', () => {
4
- test('accepts an empty builder settings document', () => {
5
- const settings = createEmptyBuilderSettings(7);
6
- expect(BuilderSettingsSchema.parse(settings)).toEqual({
7
- version: 2,
8
- revision: 7,
9
- theme: {
10
- content: {},
11
- layout: {},
12
- style_slots: {},
13
- pages: [],
14
- terms: {},
15
- },
16
- });
17
- });
18
- test('rejects unknown style slots', () => {
19
- const result = StyleSlotsSchema.safeParse({
20
- 'button.radius': { base: 10 },
21
- 'hero.magic.glow': true,
22
- });
23
- expect(result.success).toBe(false);
24
- });
25
- test('rejects invalid colors', () => {
26
- const result = StyleSlotsSchema.safeParse({
27
- 'color.primary': 'tomato',
28
- });
29
- expect(result.success).toBe(false);
30
- });
31
- test('validates manifest block references', () => {
32
- const result = ThemeManifestSchema.safeParse({
33
- id: 'default',
34
- name: 'Default',
35
- version: '2.0.0',
36
- pages: {
37
- home: {
38
- label: 'Home',
39
- allowedBlocks: ['hero', 'faq'],
40
- defaultBlocks: [{ type: 'hero', variant: 'split' }],
41
- },
42
- },
43
- blocks: {
44
- hero: {
45
- label: 'Hero',
46
- variants: [{ id: 'split', label: 'Split' }],
47
- settings: {
48
- title: { type: 'text', label: 'Headline' },
49
- },
50
- exposedStyleSlots: ['button.radius'],
51
- },
52
- },
53
- });
54
- expect(result.success).toBe(false);
55
- });
56
- test('accepts preview APPLY_STATE messages', () => {
57
- const settings = createEmptyBuilderSettings(3);
58
- const result = PreviewMessageSchema.safeParse({
59
- type: 'APPLY_STATE',
60
- revision: 3,
61
- state: settings,
62
- });
63
- expect(result.success).toBe(true);
64
- });
65
- test('accepts preview REQUEST_READY messages', () => {
66
- const result = PreviewMessageSchema.safeParse({
67
- type: 'REQUEST_READY',
68
- });
69
- expect(result.success).toBe(true);
70
- });
71
- test('accepts preview READY health and runtime error responses', () => {
72
- expect(PreviewResponseSchema.safeParse({
73
- type: 'READY',
74
- revision: 3,
75
- health: {
76
- reactMounted: true,
77
- builderRuntimeProvider: true,
78
- protocolVersion: 2,
79
- },
80
- }).success).toBe(true);
81
- expect(PreviewResponseSchema.safeParse({
82
- type: 'PREVIEW_ERROR',
83
- revision: 3,
84
- message: 'useBuilderRuntime must be used inside BuilderRuntimeProvider',
85
- stack: 'Error: useBuilderRuntime must be used inside BuilderRuntimeProvider',
86
- source: 'error',
87
- }).success).toBe(true);
88
- });
89
- test('accepts block add events', () => {
90
- const result = BuilderEventSchema.safeParse({
91
- id: 'evt_1',
92
- type: 'block.add',
93
- revision: 4,
94
- createdAt: '2026-04-23T10:00:00.000Z',
95
- source: 'dashboard',
96
- pageId: 'home',
97
- block: createBlockInstance({
98
- id: 'hero-1',
99
- type: 'hero',
100
- settings: { title: 'New drop' },
101
- }),
102
- index: 0,
103
- });
104
- expect(result.success).toBe(true);
105
- });
106
- test('migrates legacy content and token overrides', () => {
107
- const migrated = migrateLegacyBuilderSettings({
108
- theme: {
109
- content: {
110
- 'hero.title': 'Summer sale',
111
- },
112
- tokens_override: {
113
- 'buttons.borderRadius': 12,
114
- 'colors.primary': '#ff5500',
115
- },
116
- },
117
- }, 12);
118
- expect(migrated.revision).toBe(12);
119
- expect(migrated.theme.content['hero.title']).toBe('Summer sale');
120
- expect(migrated.theme.style_slots['button.radius']).toEqual({ base: 12 });
121
- expect(migrated.theme.style_slots['color.primary']).toBe('#ff5500');
122
- });
123
- test('converts legacy theme manifests into the v2 manifest contract', () => {
124
- const manifest = convertLegacyThemeManifest({
125
- id: 'default',
126
- name: 'Default Theme',
127
- version: '1.0.0',
128
- presets: [
129
- {
130
- id: 'gaming',
131
- name: 'Gaming Store',
132
- description: 'Gaming copy preset',
133
- overrides: {
134
- content: {
135
- hero: {
136
- title: 'Instant keys',
137
- },
138
- },
139
- },
140
- },
141
- ],
142
- builder: {
143
- pages: [
144
- {
145
- id: 'home',
146
- label: 'Home',
147
- previewPath: '/',
148
- blocks: ['hero', 'faq'],
149
- },
150
- {
151
- id: 'contact-page',
152
- label: 'Contact',
153
- previewPath: '/contact',
154
- blocks: [],
155
- fields: [
156
- { path: 'pages.contact.title', label: 'Title', defaultValue: 'Contact Us' },
157
- ],
158
- },
159
- ],
160
- blocks: {
161
- hero: {
162
- label: 'Hero',
163
- maxInstances: 1,
164
- fields: [
165
- { path: 'hero.title', label: 'Title' },
166
- {
167
- path: 'hero.style',
168
- type: 'select',
169
- label: 'Hero Style',
170
- options: [{ value: 'split', label: 'Split' }],
171
- },
172
- ],
173
- },
174
- faq: {
175
- label: 'FAQ',
176
- lists: [
177
- { path: 'faq.items', label: 'FAQ Items', kind: 'faqItems' },
178
- ],
179
- },
180
- },
181
- },
182
- });
183
- expect(manifest.pages.home.allowedBlocks).toEqual(['hero', 'faq']);
184
- expect(manifest.pages.home.defaultBlocks).toEqual([{ type: 'hero' }, { type: 'faq' }]);
185
- expect(manifest.blocks.hero.settings['hero.title']).toMatchObject({ type: 'text', label: 'Title' });
186
- expect(manifest.blocks.hero.settings['hero.style']).toMatchObject({ type: 'select' });
187
- expect(manifest.blocks.faq.settings['faq.items']).toMatchObject({ type: 'list' });
188
- expect(manifest.blocks['page-contact-page'].settings['pages.contact.title']).toMatchObject({ type: 'text' });
189
- expect(manifest.pages['contact-page'].allowedBlocks).toEqual(['page-contact-page']);
190
- expect(manifest.presets.gaming).toMatchObject({
191
- label: 'Gaming Store',
192
- description: 'Gaming copy preset',
193
- content: {
194
- 'hero.title': 'Instant keys',
195
- },
196
- });
197
- });
198
- test('applies manifest default blocks to empty builder settings', () => {
199
- const settings = applyManifestDefaultLayout(createEmptyBuilderSettings(2), {
200
- id: 'default',
201
- name: 'Default',
202
- version: '2.0.0',
203
- pages: {
204
- home: {
205
- label: 'Home',
206
- allowedBlocks: ['hero', 'faq'],
207
- defaultBlocks: [{ type: 'hero' }, { type: 'faq' }],
208
- },
209
- },
210
- blocks: {
211
- hero: { label: 'Hero', settings: {}, variants: [], exposedStyleSlots: [], presets: [] },
212
- faq: { label: 'FAQ', settings: {}, variants: [], exposedStyleSlots: [], presets: [] },
213
- },
214
- styleSlots: {},
215
- presets: {},
216
- });
217
- expect(settings.theme.layout.home.blocks.map((block) => block.type)).toEqual(['hero', 'faq']);
218
- expect(settings.theme.layout.home.blocks.map((block) => block.id)).toEqual(['home-hero-1', 'home-faq-2']);
219
- });
220
- test('applies default layout for converted page-level fields', () => {
221
- const manifest = convertLegacyThemeManifest({
222
- id: 'classic',
223
- name: 'Classic',
224
- version: '1.0.0',
225
- builder: {
226
- pages: [
227
- {
228
- id: 'faq-page',
229
- label: 'FAQ',
230
- previewPath: '/faq',
231
- fields: [
232
- { path: 'pages.faq.title', label: 'Title', defaultValue: 'Questions' },
233
- ],
234
- },
235
- ],
236
- blocks: {},
237
- },
238
- });
239
- const settings = applyManifestDefaultLayout(createEmptyBuilderSettings(0), manifest);
240
- expect(settings.theme.layout['faq-page'].blocks).toHaveLength(1);
241
- expect(settings.theme.layout['faq-page'].blocks[0]).toMatchObject({
242
- id: 'faq-page-page-faq-page-1',
243
- type: 'page-faq-page',
244
- visible: true,
245
- });
246
- });
247
- test('validates builder settings against the active manifest', () => {
248
- const settings = BuilderSettingsSchema.parse({
249
- version: 2,
250
- revision: 1,
251
- theme: {
252
- content: {},
253
- layout: {
254
- home: {
255
- blocks: [
256
- {
257
- id: 'hero-1',
258
- type: 'hero',
259
- variant: 'missing',
260
- visible: true,
261
- settings: {
262
- 'hero.title': 'Launch sale',
263
- title: 'Launch sale',
264
- 'hero.magic': 'not declared',
265
- },
266
- style_overrides: {
267
- 'input.radius': { base: 10 },
268
- },
269
- },
270
- { id: 'hero-2', type: 'hero', visible: true, settings: {} },
271
- { id: 'hero-2', type: 'faq', visible: true, settings: {} },
272
- { id: 'ghost-1', type: 'ghost', visible: true, settings: {} },
273
- ],
274
- },
275
- checkout: {
276
- blocks: [],
277
- },
278
- },
279
- style_slots: {},
280
- pages: [],
281
- terms: {},
282
- },
283
- });
284
- const issues = validateBuilderSettingsAgainstManifest(settings, {
285
- id: 'default',
286
- name: 'Default',
287
- version: '2.0.0',
288
- pages: {
289
- home: {
290
- label: 'Home',
291
- allowedBlocks: ['hero'],
292
- defaultBlocks: [{ type: 'hero' }],
293
- },
294
- },
295
- blocks: {
296
- hero: {
297
- label: 'Hero',
298
- settings: {
299
- 'hero.title': { type: 'text', label: 'Title' },
300
- },
301
- variants: [{ id: 'split', label: 'Split' }],
302
- exposedStyleSlots: ['button.radius'],
303
- maxInstances: 1,
304
- presets: [],
305
- },
306
- faq: {
307
- label: 'FAQ',
308
- settings: {},
309
- variants: [],
310
- exposedStyleSlots: [],
311
- presets: [],
312
- },
313
- },
314
- styleSlots: {},
315
- presets: {},
316
- });
317
- expect(issues.map((issue) => issue.code)).toContain('unknown_page');
318
- expect(issues.map((issue) => issue.code)).toContain('duplicate_block_id');
319
- expect(issues.map((issue) => issue.code)).toContain('unknown_block_type');
320
- expect(issues.map((issue) => issue.code)).toContain('block_not_allowed');
321
- expect(issues.map((issue) => issue.code)).toContain('too_many_block_instances');
322
- expect(issues.map((issue) => issue.code)).toContain('unknown_block_variant');
323
- expect(issues.map((issue) => issue.code)).toContain('unknown_block_setting');
324
- expect(issues.map((issue) => issue.code)).toContain('unexposed_style_slot');
325
- });
326
- test('accepts full and short block setting keys declared by the manifest', () => {
327
- const settings = BuilderSettingsSchema.parse({
328
- version: 2,
329
- revision: 1,
330
- theme: {
331
- content: {},
332
- layout: {
333
- home: {
334
- blocks: [
335
- {
336
- id: 'hero-1',
337
- type: 'hero',
338
- visible: true,
339
- settings: {
340
- 'hero.title': 'Launch sale',
341
- title: 'Launch sale',
342
- },
343
- },
344
- ],
345
- },
346
- },
347
- style_slots: {},
348
- pages: [],
349
- terms: {},
350
- },
351
- });
352
- const issues = validateBuilderSettingsAgainstManifest(settings, {
353
- id: 'default',
354
- name: 'Default',
355
- version: '2.0.0',
356
- pages: {
357
- home: {
358
- label: 'Home',
359
- allowedBlocks: ['hero'],
360
- defaultBlocks: [{ type: 'hero' }],
361
- },
362
- },
363
- blocks: {
364
- hero: {
365
- label: 'Hero',
366
- settings: {
367
- 'hero.title': { type: 'text', label: 'Title' },
368
- },
369
- variants: [],
370
- exposedStyleSlots: [],
371
- presets: [],
372
- },
373
- },
374
- styleSlots: {},
375
- presets: {},
376
- });
377
- expect(issues).toEqual([]);
378
- });
379
- test('accepts block-prefixed setting keys when the manifest declares short block fields', () => {
380
- const settings = BuilderSettingsSchema.parse({
381
- version: 2,
382
- revision: 1,
383
- theme: {
384
- content: {},
385
- layout: {
386
- home: {
387
- blocks: [
388
- {
389
- id: 'home-hero-1',
390
- type: 'hero',
391
- visible: true,
392
- settings: {
393
- 'hero.title': 'Launch sale',
394
- },
395
- },
396
- ],
397
- },
398
- },
399
- style_slots: {},
400
- pages: [],
401
- terms: {},
402
- },
403
- });
404
- const issues = validateBuilderSettingsAgainstManifest(settings, {
405
- id: 'starlight',
406
- name: 'Starlight',
407
- version: '1.0.0',
408
- pages: {
409
- home: {
410
- label: 'Home',
411
- allowedBlocks: ['hero'],
412
- defaultBlocks: [{ type: 'hero' }],
413
- },
414
- },
415
- blocks: {
416
- hero: {
417
- label: 'Hero',
418
- settings: {
419
- title: { type: 'text', label: 'Title' },
420
- },
421
- variants: [],
422
- exposedStyleSlots: [],
423
- presets: [],
424
- },
425
- },
426
- styleSlots: {},
427
- presets: {},
428
- });
429
- expect(issues).toEqual([]);
430
- });
431
- });