@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.
Files changed (92) hide show
  1. package/dist/builder-settings.d.ts +11 -666
  2. package/dist/builder-settings.d.ts.map +1 -1
  3. package/dist/builder-settings.js +2 -1
  4. package/dist/canonical-settings.d.ts +18 -0
  5. package/dist/canonical-settings.d.ts.map +1 -0
  6. package/dist/canonical-settings.js +106 -0
  7. package/dist/custom-pages.d.ts +15 -0
  8. package/dist/custom-pages.d.ts.map +1 -0
  9. package/dist/custom-pages.js +40 -0
  10. package/dist/dedicated-pages.d.ts +15 -0
  11. package/dist/dedicated-pages.d.ts.map +1 -0
  12. package/dist/dedicated-pages.js +142 -0
  13. package/dist/events.d.ts +35 -240
  14. package/dist/events.d.ts.map +1 -1
  15. package/dist/events.js +7 -0
  16. package/dist/fields.d.ts +229 -10
  17. package/dist/fields.d.ts.map +1 -1
  18. package/dist/fields.js +27 -0
  19. package/dist/index.d.ts +10 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +10 -0
  22. package/dist/legacy-manifest.d.ts +18 -0
  23. package/dist/legacy-manifest.d.ts.map +1 -1
  24. package/dist/legacy-manifest.js +137 -22
  25. package/dist/migrations.d.ts.map +1 -1
  26. package/dist/migrations.js +55 -6
  27. package/dist/persistence.d.ts +7 -0
  28. package/dist/persistence.d.ts.map +1 -0
  29. package/dist/persistence.js +58 -0
  30. package/dist/preview-boot.d.ts +68 -0
  31. package/dist/preview-boot.d.ts.map +1 -0
  32. package/dist/preview-boot.js +38 -0
  33. package/dist/preview-protocol.d.ts +227 -459
  34. package/dist/preview-protocol.d.ts.map +1 -1
  35. package/dist/preview-protocol.js +112 -0
  36. package/dist/preview-session-resolve.d.ts +115 -0
  37. package/dist/preview-session-resolve.d.ts.map +1 -0
  38. package/dist/preview-session-resolve.js +25 -0
  39. package/dist/preview-trusted-origins.d.ts +4 -0
  40. package/dist/preview-trusted-origins.d.ts.map +1 -0
  41. package/dist/preview-trusted-origins.js +28 -0
  42. package/dist/storefront-initial-data-html.d.ts +17 -0
  43. package/dist/storefront-initial-data-html.d.ts.map +1 -0
  44. package/dist/storefront-initial-data-html.js +83 -0
  45. package/dist/storefront-typography-fonts.d.ts +18 -0
  46. package/dist/storefront-typography-fonts.d.ts.map +1 -0
  47. package/dist/storefront-typography-fonts.js +89 -0
  48. package/dist/style-slots.d.ts +50 -152
  49. package/dist/style-slots.d.ts.map +1 -1
  50. package/dist/style-slots.js +80 -32
  51. package/dist/theme-manifest.d.ts +287 -456
  52. package/dist/theme-manifest.d.ts.map +1 -1
  53. package/dist/theme-manifest.js +92 -0
  54. package/dist/theme-schemes.d.ts +10 -0
  55. package/dist/theme-schemes.d.ts.map +1 -0
  56. package/dist/theme-schemes.js +25 -0
  57. package/dist/validation.d.ts +1 -1
  58. package/dist/validation.d.ts.map +1 -1
  59. package/dist/validation.js +23 -12
  60. package/package.json +43 -1
  61. package/src/builder-contracts.test.ts +416 -3
  62. package/src/builder-settings.ts +4 -1
  63. package/src/canonical-settings.ts +156 -0
  64. package/src/custom-pages.test.ts +74 -0
  65. package/src/custom-pages.ts +70 -0
  66. package/src/dedicated-pages.test.ts +88 -0
  67. package/src/dedicated-pages.ts +173 -0
  68. package/src/events.ts +8 -0
  69. package/src/fields.ts +30 -0
  70. package/src/index.ts +10 -0
  71. package/src/legacy-manifest.ts +147 -23
  72. package/src/migrations.ts +70 -6
  73. package/src/persistence.ts +77 -0
  74. package/src/preview-boot.test.ts +72 -0
  75. package/src/preview-boot.ts +49 -0
  76. package/src/preview-protocol.test.ts +132 -0
  77. package/src/preview-protocol.ts +122 -0
  78. package/src/preview-session-resolve.test.ts +37 -0
  79. package/src/preview-session-resolve.ts +34 -0
  80. package/src/preview-trusted-origins.test.ts +24 -0
  81. package/src/preview-trusted-origins.ts +35 -0
  82. package/src/storefront-initial-data-html.test.ts +73 -0
  83. package/src/storefront-initial-data-html.ts +112 -0
  84. package/src/storefront-typography-fonts.test.ts +48 -0
  85. package/src/storefront-typography-fonts.ts +108 -0
  86. package/src/style-slots.ts +102 -34
  87. package/src/theme-manifest.ts +118 -1
  88. package/src/theme-schemes.ts +34 -0
  89. package/src/validation.ts +32 -13
  90. package/dist/builder-contracts.test.d.ts +0 -2
  91. package/dist/builder-contracts.test.d.ts.map +0 -1
  92. package/dist/builder-contracts.test.js +0 -361
@@ -1,361 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { BuilderEventSchema, BuilderSettingsSchema, 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 block add events', () => {
72
- const result = BuilderEventSchema.safeParse({
73
- id: 'evt_1',
74
- type: 'block.add',
75
- revision: 4,
76
- createdAt: '2026-04-23T10:00:00.000Z',
77
- source: 'dashboard',
78
- pageId: 'home',
79
- block: createBlockInstance({
80
- id: 'hero-1',
81
- type: 'hero',
82
- settings: { title: 'New drop' },
83
- }),
84
- index: 0,
85
- });
86
- expect(result.success).toBe(true);
87
- });
88
- test('migrates legacy content and token overrides', () => {
89
- const migrated = migrateLegacyBuilderSettings({
90
- theme: {
91
- content: {
92
- 'hero.title': 'Summer sale',
93
- },
94
- tokens_override: {
95
- 'buttons.borderRadius': 12,
96
- 'colors.primary': '#ff5500',
97
- },
98
- },
99
- }, 12);
100
- expect(migrated.revision).toBe(12);
101
- expect(migrated.theme.content['hero.title']).toBe('Summer sale');
102
- expect(migrated.theme.style_slots['button.radius']).toEqual({ base: 12 });
103
- expect(migrated.theme.style_slots['color.primary']).toBe('#ff5500');
104
- });
105
- test('converts legacy theme manifests into the v2 manifest contract', () => {
106
- const manifest = convertLegacyThemeManifest({
107
- id: 'default',
108
- name: 'Default Theme',
109
- version: '1.0.0',
110
- presets: [
111
- {
112
- id: 'gaming',
113
- name: 'Gaming Store',
114
- description: 'Gaming copy preset',
115
- overrides: {
116
- content: {
117
- hero: {
118
- title: 'Instant keys',
119
- },
120
- },
121
- },
122
- },
123
- ],
124
- builder: {
125
- pages: [
126
- {
127
- id: 'home',
128
- label: 'Home',
129
- previewPath: '/',
130
- blocks: ['hero', 'faq'],
131
- },
132
- {
133
- id: 'contact-page',
134
- label: 'Contact',
135
- previewPath: '/contact',
136
- blocks: [],
137
- fields: [
138
- { path: 'pages.contact.title', label: 'Title', defaultValue: 'Contact Us' },
139
- ],
140
- },
141
- ],
142
- blocks: {
143
- hero: {
144
- label: 'Hero',
145
- maxInstances: 1,
146
- fields: [
147
- { path: 'hero.title', label: 'Title' },
148
- {
149
- path: 'hero.style',
150
- type: 'select',
151
- label: 'Hero Style',
152
- options: [{ value: 'split', label: 'Split' }],
153
- },
154
- ],
155
- },
156
- faq: {
157
- label: 'FAQ',
158
- lists: [
159
- { path: 'faq.items', label: 'FAQ Items', kind: 'faqItems' },
160
- ],
161
- },
162
- },
163
- },
164
- });
165
- expect(manifest.pages.home.allowedBlocks).toEqual(['hero', 'faq']);
166
- expect(manifest.pages.home.defaultBlocks).toEqual([{ type: 'hero' }, { type: 'faq' }]);
167
- expect(manifest.blocks.hero.settings['hero.title']).toMatchObject({ type: 'text', label: 'Title' });
168
- expect(manifest.blocks.hero.settings['hero.style']).toMatchObject({ type: 'select' });
169
- expect(manifest.blocks.faq.settings['faq.items']).toMatchObject({ type: 'list' });
170
- expect(manifest.blocks['page-contact-page'].settings['pages.contact.title']).toMatchObject({ type: 'text' });
171
- expect(manifest.pages['contact-page'].allowedBlocks).toEqual(['page-contact-page']);
172
- expect(manifest.presets.gaming).toMatchObject({
173
- label: 'Gaming Store',
174
- description: 'Gaming copy preset',
175
- content: {
176
- 'hero.title': 'Instant keys',
177
- },
178
- });
179
- });
180
- test('applies manifest default blocks to empty builder settings', () => {
181
- const settings = applyManifestDefaultLayout(createEmptyBuilderSettings(2), {
182
- id: 'default',
183
- name: 'Default',
184
- version: '2.0.0',
185
- pages: {
186
- home: {
187
- label: 'Home',
188
- allowedBlocks: ['hero', 'faq'],
189
- defaultBlocks: [{ type: 'hero' }, { type: 'faq' }],
190
- },
191
- },
192
- blocks: {
193
- hero: { label: 'Hero', settings: {}, variants: [], exposedStyleSlots: [], presets: [] },
194
- faq: { label: 'FAQ', settings: {}, variants: [], exposedStyleSlots: [], presets: [] },
195
- },
196
- styleSlots: {},
197
- presets: {},
198
- });
199
- expect(settings.theme.layout.home.blocks.map((block) => block.type)).toEqual(['hero', 'faq']);
200
- expect(settings.theme.layout.home.blocks.map((block) => block.id)).toEqual(['home-hero-1', 'home-faq-2']);
201
- });
202
- test('applies default layout for converted page-level fields', () => {
203
- const manifest = convertLegacyThemeManifest({
204
- id: 'classic',
205
- name: 'Classic',
206
- version: '1.0.0',
207
- builder: {
208
- pages: [
209
- {
210
- id: 'faq-page',
211
- label: 'FAQ',
212
- previewPath: '/faq',
213
- fields: [
214
- { path: 'pages.faq.title', label: 'Title', defaultValue: 'Questions' },
215
- ],
216
- },
217
- ],
218
- blocks: {},
219
- },
220
- });
221
- const settings = applyManifestDefaultLayout(createEmptyBuilderSettings(0), manifest);
222
- expect(settings.theme.layout['faq-page'].blocks).toHaveLength(1);
223
- expect(settings.theme.layout['faq-page'].blocks[0]).toMatchObject({
224
- id: 'faq-page-page-faq-page-1',
225
- type: 'page-faq-page',
226
- visible: true,
227
- });
228
- });
229
- test('validates builder settings against the active manifest', () => {
230
- const settings = BuilderSettingsSchema.parse({
231
- version: 2,
232
- revision: 1,
233
- theme: {
234
- content: {},
235
- layout: {
236
- home: {
237
- blocks: [
238
- {
239
- id: 'hero-1',
240
- type: 'hero',
241
- variant: 'missing',
242
- visible: true,
243
- settings: {
244
- 'hero.title': 'Launch sale',
245
- title: 'Launch sale',
246
- 'hero.magic': 'not declared',
247
- },
248
- style_overrides: {
249
- 'input.radius': { base: 10 },
250
- },
251
- },
252
- { id: 'hero-2', type: 'hero', visible: true, settings: {} },
253
- { id: 'hero-2', type: 'faq', visible: true, settings: {} },
254
- { id: 'ghost-1', type: 'ghost', visible: true, settings: {} },
255
- ],
256
- },
257
- checkout: {
258
- blocks: [],
259
- },
260
- },
261
- style_slots: {},
262
- pages: [],
263
- terms: {},
264
- },
265
- });
266
- const issues = validateBuilderSettingsAgainstManifest(settings, {
267
- id: 'default',
268
- name: 'Default',
269
- version: '2.0.0',
270
- pages: {
271
- home: {
272
- label: 'Home',
273
- allowedBlocks: ['hero'],
274
- defaultBlocks: [{ type: 'hero' }],
275
- },
276
- },
277
- blocks: {
278
- hero: {
279
- label: 'Hero',
280
- settings: {
281
- 'hero.title': { type: 'text', label: 'Title' },
282
- },
283
- variants: [{ id: 'split', label: 'Split' }],
284
- exposedStyleSlots: ['button.radius'],
285
- maxInstances: 1,
286
- presets: [],
287
- },
288
- faq: {
289
- label: 'FAQ',
290
- settings: {},
291
- variants: [],
292
- exposedStyleSlots: [],
293
- presets: [],
294
- },
295
- },
296
- styleSlots: {},
297
- presets: {},
298
- });
299
- expect(issues.map((issue) => issue.code)).toContain('unknown_page');
300
- expect(issues.map((issue) => issue.code)).toContain('duplicate_block_id');
301
- expect(issues.map((issue) => issue.code)).toContain('unknown_block_type');
302
- expect(issues.map((issue) => issue.code)).toContain('block_not_allowed');
303
- expect(issues.map((issue) => issue.code)).toContain('too_many_block_instances');
304
- expect(issues.map((issue) => issue.code)).toContain('unknown_block_variant');
305
- expect(issues.map((issue) => issue.code)).toContain('unknown_block_setting');
306
- expect(issues.map((issue) => issue.code)).toContain('unexposed_style_slot');
307
- });
308
- test('accepts full and short block setting keys declared by the manifest', () => {
309
- const settings = BuilderSettingsSchema.parse({
310
- version: 2,
311
- revision: 1,
312
- theme: {
313
- content: {},
314
- layout: {
315
- home: {
316
- blocks: [
317
- {
318
- id: 'hero-1',
319
- type: 'hero',
320
- visible: true,
321
- settings: {
322
- 'hero.title': 'Launch sale',
323
- title: 'Launch sale',
324
- },
325
- },
326
- ],
327
- },
328
- },
329
- style_slots: {},
330
- pages: [],
331
- terms: {},
332
- },
333
- });
334
- const issues = validateBuilderSettingsAgainstManifest(settings, {
335
- id: 'default',
336
- name: 'Default',
337
- version: '2.0.0',
338
- pages: {
339
- home: {
340
- label: 'Home',
341
- allowedBlocks: ['hero'],
342
- defaultBlocks: [{ type: 'hero' }],
343
- },
344
- },
345
- blocks: {
346
- hero: {
347
- label: 'Hero',
348
- settings: {
349
- 'hero.title': { type: 'text', label: 'Title' },
350
- },
351
- variants: [],
352
- exposedStyleSlots: [],
353
- presets: [],
354
- },
355
- },
356
- styleSlots: {},
357
- presets: {},
358
- });
359
- expect(issues).toEqual([]);
360
- });
361
- });