@shoppexio/builder-contracts 0.1.2 → 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.
package/dist/fields.d.ts CHANGED
@@ -14,13 +14,13 @@ export declare const ListItemFieldTypeSchema: z.ZodEnum<{
14
14
  number: "number";
15
15
  boolean: "boolean";
16
16
  link: "link";
17
+ product: "product";
17
18
  text: "text";
18
19
  richtext: "richtext";
19
20
  image: "image";
20
21
  range: "range";
21
22
  select: "select";
22
23
  color: "color";
23
- product: "product";
24
24
  }>;
25
25
  export type ListItemFieldType = z.infer<typeof ListItemFieldTypeSchema>;
26
26
  export declare const ListItemFieldSchema: z.ZodObject<{
@@ -38,13 +38,13 @@ export declare const ListItemFieldSchema: z.ZodObject<{
38
38
  number: "number";
39
39
  boolean: "boolean";
40
40
  link: "link";
41
+ product: "product";
41
42
  text: "text";
42
43
  richtext: "richtext";
43
44
  image: "image";
44
45
  range: "range";
45
46
  select: "select";
46
47
  color: "color";
47
- product: "product";
48
48
  }>;
49
49
  options: z.ZodOptional<z.ZodArray<z.ZodObject<{
50
50
  label: z.ZodString;
@@ -266,13 +266,13 @@ export declare const BuilderFieldSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
266
266
  number: "number";
267
267
  boolean: "boolean";
268
268
  link: "link";
269
+ product: "product";
269
270
  text: "text";
270
271
  richtext: "richtext";
271
272
  image: "image";
272
273
  range: "range";
273
274
  select: "select";
274
275
  color: "color";
275
- product: "product";
276
276
  }>;
277
277
  options: z.ZodOptional<z.ZodArray<z.ZodObject<{
278
278
  label: z.ZodString;
@@ -495,13 +495,13 @@ export declare const BlockSettingsSchema: z.ZodRecord<z.ZodString, z.ZodDiscrimi
495
495
  number: "number";
496
496
  boolean: "boolean";
497
497
  link: "link";
498
+ product: "product";
498
499
  text: "text";
499
500
  richtext: "richtext";
500
501
  image: "image";
501
502
  range: "range";
502
503
  select: "select";
503
504
  color: "color";
504
- product: "product";
505
505
  }>;
506
506
  options: z.ZodOptional<z.ZodArray<z.ZodObject<{
507
507
  label: z.ZodString;
@@ -27,5 +27,6 @@ export declare function createBlockInstance(input: {
27
27
  export declare function createPageLayoutFromBlocks(blocks: BlockInstance[]): PageLayout;
28
28
  export declare function migrateLegacyBuilderSettings(input: LegacyBuilderSettingsInput, revision?: number): BuilderSettings;
29
29
  export declare function applyManifestDefaultLayout(settings: BuilderSettings, manifest: ThemeManifest): BuilderSettings;
30
+ export declare function mapLegacyNestedThemeTypographyToStyleSlots(theme: Record<string, unknown>, baseSlots?: StyleSlots): StyleSlots;
30
31
  export declare function mapLegacyTokenOverridesToStyleSlots(tokens: Record<string, unknown>): StyleSlots;
31
32
  //# sourceMappingURL=migrations.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,eAAe,EAEpB,KAAK,UAAU,EAChB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,CAAC,EAAE;QACN,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAClC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC1C,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACvC,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,0BAA0B,CAAC,QAAQ,SAAI,GAAG,eAAe,CAYxE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,eAAe,CAAC,EAAE,UAAU,CAAC;CAC9B,GAAG,aAAa,CAShB;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,UAAU,CAE9E;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,0BAA0B,EAAE,QAAQ,SAAI,GAAG,eAAe,CAoB7G;AAED,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,eAAe,EAAE,QAAQ,EAAE,aAAa,GAAG,eAAe,CAwB9G;AAED,wBAAgB,mCAAmC,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,UAAU,CAuC/F"}
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,eAAe,EAEpB,KAAK,UAAU,EAChB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,CAAC,EAAE;QACN,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAClC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC1C,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACvC,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,0BAA0B,CAAC,QAAQ,SAAI,GAAG,eAAe,CAYxE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,eAAe,CAAC,EAAE,UAAU,CAAC;CAC9B,GAAG,aAAa,CAShB;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,UAAU,CAE9E;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,0BAA0B,EAAE,QAAQ,SAAI,GAAG,eAAe,CAoB7G;AAED,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,eAAe,EAAE,QAAQ,EAAE,aAAa,GAAG,eAAe,CAwB9G;AAED,wBAAgB,0CAA0C,CACxD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,SAAS,GAAE,UAAe,GACzB,UAAU,CAkBZ;AAeD,wBAAgB,mCAAmC,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,UAAU,CAuC/F"}
@@ -69,6 +69,31 @@ export function applyManifestDefaultLayout(settings, manifest) {
69
69
  }
70
70
  return BuilderSettingsSchema.parse(next);
71
71
  }
72
+ export function mapLegacyNestedThemeTypographyToStyleSlots(theme, baseSlots = {}) {
73
+ const slots = { ...baseSlots };
74
+ const typography = theme.typography;
75
+ if (!isRecord(typography)) {
76
+ return slots;
77
+ }
78
+ const bodyFont = readLegacyTypographyString(typography, 'font_family', 'fontFamily');
79
+ const headingFont = readLegacyTypographyString(typography, 'heading_font', 'headingFont');
80
+ if (bodyFont && slots['theme.typography.font.family'] === undefined) {
81
+ slots['theme.typography.font.family'] = bodyFont;
82
+ }
83
+ if (headingFont && slots['theme.typography.heading.font'] === undefined) {
84
+ slots['theme.typography.heading.font'] = headingFont;
85
+ }
86
+ return slots;
87
+ }
88
+ function readLegacyTypographyString(typography, ...keys) {
89
+ for (const key of keys) {
90
+ const value = typography[key];
91
+ if (typeof value === 'string' && value.trim().length > 0) {
92
+ return value.trim();
93
+ }
94
+ }
95
+ return null;
96
+ }
72
97
  export function mapLegacyTokenOverridesToStyleSlots(tokens) {
73
98
  const slots = {};
74
99
  assignResponsiveNumber(tokens, slots, 'buttons.borderRadius', 'button.radius');
@@ -1,6 +1,15 @@
1
1
  import { type BuilderSettings } from './builder-settings.ts';
2
2
  type JsonRecord = Record<string, unknown>;
3
3
  export declare function extractPersistedBuilderSettings(input: unknown): BuilderSettings | null;
4
+ /**
5
+ * Single read boundary for builder v2 state: strict extract first, then legacy migration.
6
+ * Strips invalid keys such as nested `theme.typography` while preserving style_slots and pages.
7
+ */
8
+ export declare function coercePersistedBuilderSettings(input: unknown): BuilderSettings | null;
9
+ /**
10
+ * Coerces builder v2 fields for storefront export while preserving non-v2 siblings (seo, appearance, …).
11
+ */
12
+ export declare function exportCoercedBuilderSettingsBlob(builderSettingsBlob: unknown): JsonRecord | null;
4
13
  export declare function sanitizeBuilderSettingsState(settings: BuilderSettings): BuilderSettings;
5
14
  export declare function mergeBuilderSettingsIntoThemeSettings(currentSettings: unknown, builderSettings: BuilderSettings): JsonRecord;
6
15
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../src/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,eAAe,EAErB,MAAM,uBAAuB,CAAC;AAE/B,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAmB1C,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,CAYtF;AAED,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,eAAe,GAAG,eAAe,CASvF;AAaD,wBAAgB,qCAAqC,CACnD,eAAe,EAAE,OAAO,EACxB,eAAe,EAAE,eAAe,GAC/B,UAAU,CAaZ"}
1
+ {"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../src/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,eAAe,EAErB,MAAM,uBAAuB,CAAC;AAQ/B,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAmB1C,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,CAYtF;AA0BD;;;GAGG;AACH,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,CAkDrF;AAED;;GAEG;AACH,wBAAgB,gCAAgC,CAAC,mBAAmB,EAAE,OAAO,GAAG,UAAU,GAAG,IAAI,CAgBhG;AAED,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,eAAe,GAAG,eAAe,CASvF;AAaD,wBAAgB,qCAAqC,CACnD,eAAe,EAAE,OAAO,EACxB,eAAe,EAAE,eAAe,GAC/B,UAAU,CAaZ"}
@@ -1,4 +1,5 @@
1
1
  import { BuilderSettingsSchema, } from "./builder-settings.js";
2
+ import { mapLegacyNestedThemeTypographyToStyleSlots, mapLegacyTokenOverridesToStyleSlots, migrateLegacyBuilderSettings, } from "./migrations.js";
2
3
  const RESERVED_THEME_CONTENT_KEYS = new Set(['layout']);
3
4
  function isRecord(value) {
4
5
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -24,6 +25,85 @@ export function extractPersistedBuilderSettings(input) {
24
25
  const parsedNested = BuilderSettingsSchema.safeParse(pickBuilderSettingsFields(input.builder_settings));
25
26
  return parsedNested.success ? sanitizeBuilderSettingsState(parsedNested.data) : null;
26
27
  }
28
+ function parseBuilderSettingsRevision(input) {
29
+ return typeof input === 'number' && Number.isInteger(input) && input >= 0 ? input : 0;
30
+ }
31
+ function unwrapBuilderSettingsRecord(input) {
32
+ if (!isRecord(input)) {
33
+ return null;
34
+ }
35
+ if (isRecord(input.builder_settings)) {
36
+ return input.builder_settings;
37
+ }
38
+ if ('version' in input || 'revision' in input || 'theme' in input) {
39
+ return input;
40
+ }
41
+ return null;
42
+ }
43
+ function readStyleSlotsRecord(theme) {
44
+ return isRecord(theme.style_slots) ? theme.style_slots : {};
45
+ }
46
+ /**
47
+ * Single read boundary for builder v2 state: strict extract first, then legacy migration.
48
+ * Strips invalid keys such as nested `theme.typography` while preserving style_slots and pages.
49
+ */
50
+ export function coercePersistedBuilderSettings(input) {
51
+ const extracted = extractPersistedBuilderSettings(input);
52
+ if (extracted) {
53
+ return extracted;
54
+ }
55
+ if (isRecord(input) && isRecord(input.builder_settings)) {
56
+ const nestedExtracted = extractPersistedBuilderSettings(input.builder_settings);
57
+ if (nestedExtracted) {
58
+ return nestedExtracted;
59
+ }
60
+ }
61
+ const blob = unwrapBuilderSettingsRecord(input);
62
+ if (!blob) {
63
+ return null;
64
+ }
65
+ const theme = isRecord(blob.theme) ? blob.theme : {};
66
+ const tokensOverride = isRecord(theme.tokens_override) ? theme.tokens_override : {};
67
+ const styleSlots = mapLegacyNestedThemeTypographyToStyleSlots(theme, {
68
+ ...mapLegacyTokenOverridesToStyleSlots(tokensOverride),
69
+ ...readStyleSlotsRecord(theme),
70
+ });
71
+ const migrated = migrateLegacyBuilderSettings({
72
+ theme: {
73
+ content: isRecord(theme.content) ? theme.content : {},
74
+ layout: isRecord(theme.layout) ? theme.layout : {},
75
+ tokens_override: tokensOverride,
76
+ style_slots: styleSlots,
77
+ },
78
+ }, parseBuilderSettingsRevision(blob.revision));
79
+ const merged = BuilderSettingsSchema.safeParse({
80
+ ...migrated,
81
+ theme: {
82
+ ...migrated.theme,
83
+ pages: Array.isArray(theme.pages) ? theme.pages : migrated.theme.pages,
84
+ terms: isRecord(theme.terms) ? theme.terms : migrated.theme.terms,
85
+ },
86
+ });
87
+ return merged.success ? sanitizeBuilderSettingsState(merged.data) : migrated;
88
+ }
89
+ /**
90
+ * Coerces builder v2 fields for storefront export while preserving non-v2 siblings (seo, appearance, …).
91
+ */
92
+ export function exportCoercedBuilderSettingsBlob(builderSettingsBlob) {
93
+ if (!isRecord(builderSettingsBlob)) {
94
+ return null;
95
+ }
96
+ const coerced = coercePersistedBuilderSettings(builderSettingsBlob);
97
+ if (!coerced) {
98
+ return null;
99
+ }
100
+ return {
101
+ ...builderSettingsBlob,
102
+ version: coerced.version,
103
+ revision: coerced.revision,
104
+ theme: coerced.theme,
105
+ };
106
+ }
27
107
  export function sanitizeBuilderSettingsState(settings) {
28
108
  const content = sanitizeThemeContent(settings.theme.content);
29
109
  return BuilderSettingsSchema.parse({
@@ -66,6 +66,24 @@ export declare const PreviewApplyStateMessageSchema: z.ZodObject<{
66
66
  }, z.core.$strict>;
67
67
  }, z.core.$strict>;
68
68
  }, z.core.$strict>;
69
+ export declare const EdgeBlockRenderModeSchema: z.ZodLiteral<"edge-block-engine">;
70
+ export type EdgeBlockRenderMode = z.infer<typeof EdgeBlockRenderModeSchema>;
71
+ export declare const PreviewBlockHtmlPatchSchema: z.ZodObject<{
72
+ blockId: z.ZodString;
73
+ html: z.ZodString;
74
+ }, z.core.$strict>;
75
+ export type PreviewBlockHtmlPatch = z.infer<typeof PreviewBlockHtmlPatchSchema>;
76
+ export declare const PreviewApplyBlockHtmlMessageSchema: z.ZodObject<{
77
+ type: z.ZodLiteral<"APPLY_BLOCK_HTML">;
78
+ revision: z.ZodNumber;
79
+ pageId: z.ZodString;
80
+ sourceRevision: z.ZodString;
81
+ renderMode: z.ZodLiteral<"edge-block-engine">;
82
+ blocks: z.ZodArray<z.ZodObject<{
83
+ blockId: z.ZodString;
84
+ html: z.ZodString;
85
+ }, z.core.$strict>>;
86
+ }, z.core.$strict>;
69
87
  export declare const PreviewReloadMessageSchema: z.ZodObject<{
70
88
  type: z.ZodLiteral<"RELOAD">;
71
89
  revision: z.ZodNumber;
@@ -164,6 +182,16 @@ export declare const PreviewMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
164
182
  }, z.core.$strict>>;
165
183
  }, z.core.$strict>;
166
184
  }, z.core.$strict>;
185
+ }, z.core.$strict>, z.ZodObject<{
186
+ type: z.ZodLiteral<"APPLY_BLOCK_HTML">;
187
+ revision: z.ZodNumber;
188
+ pageId: z.ZodString;
189
+ sourceRevision: z.ZodString;
190
+ renderMode: z.ZodLiteral<"edge-block-engine">;
191
+ blocks: z.ZodArray<z.ZodObject<{
192
+ blockId: z.ZodString;
193
+ html: z.ZodString;
194
+ }, z.core.$strict>>;
167
195
  }, z.core.$strict>, z.ZodObject<{
168
196
  type: z.ZodLiteral<"RELOAD">;
169
197
  revision: z.ZodNumber;
@@ -209,14 +237,28 @@ export declare const PreviewBootstrapOkResponseSchema: z.ZodObject<{
209
237
  shopId: z.ZodString;
210
238
  artifactStale: z.ZodOptional<z.ZodBoolean>;
211
239
  }, z.core.$strict>;
240
+ export declare const PreviewReadyHealthSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
241
+ reactMounted: z.ZodLiteral<true>;
242
+ builderRuntimeProvider: z.ZodLiteral<true>;
243
+ protocolVersion: z.ZodLiteral<2>;
244
+ }, z.core.$strict>, z.ZodObject<{
245
+ edgeBlockEngine: z.ZodLiteral<true>;
246
+ blockHtmlReconcile: z.ZodLiteral<true>;
247
+ protocolVersion: z.ZodLiteral<3>;
248
+ }, z.core.$strict>], "protocolVersion">;
249
+ export type PreviewReadyHealth = z.infer<typeof PreviewReadyHealthSchema>;
212
250
  export declare const PreviewReadyResponseSchema: z.ZodObject<{
213
251
  type: z.ZodLiteral<"READY">;
214
252
  revision: z.ZodNumber;
215
- health: z.ZodOptional<z.ZodObject<{
253
+ health: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
216
254
  reactMounted: z.ZodLiteral<true>;
217
255
  builderRuntimeProvider: z.ZodLiteral<true>;
218
256
  protocolVersion: z.ZodLiteral<2>;
219
- }, z.core.$strict>>;
257
+ }, z.core.$strict>, z.ZodObject<{
258
+ edgeBlockEngine: z.ZodLiteral<true>;
259
+ blockHtmlReconcile: z.ZodLiteral<true>;
260
+ protocolVersion: z.ZodLiteral<3>;
261
+ }, z.core.$strict>], "protocolVersion">>;
220
262
  }, z.core.$strict>;
221
263
  export declare const PreviewAppliedResponseSchema: z.ZodObject<{
222
264
  type: z.ZodLiteral<"APPLIED">;
@@ -227,6 +269,26 @@ export declare const PreviewApplyFailedResponseSchema: z.ZodObject<{
227
269
  revision: z.ZodNumber;
228
270
  error: z.ZodString;
229
271
  }, z.core.$strict>;
272
+ export declare const PreviewBlockHtmlAppliedResponseSchema: z.ZodObject<{
273
+ type: z.ZodLiteral<"BLOCK_HTML_APPLIED">;
274
+ revision: z.ZodNumber;
275
+ pageId: z.ZodString;
276
+ sourceRevision: z.ZodString;
277
+ renderMode: z.ZodLiteral<"edge-block-engine">;
278
+ blocks: z.ZodArray<z.ZodObject<{
279
+ blockId: z.ZodString;
280
+ html: z.ZodString;
281
+ }, z.core.$strict>>;
282
+ }, z.core.$strict>;
283
+ export declare const PreviewBlockHtmlFailedResponseSchema: z.ZodObject<{
284
+ type: z.ZodLiteral<"BLOCK_HTML_FAILED">;
285
+ revision: z.ZodNumber;
286
+ pageId: z.ZodString;
287
+ sourceRevision: z.ZodOptional<z.ZodString>;
288
+ renderMode: z.ZodLiteral<"edge-block-engine">;
289
+ blockIds: z.ZodArray<z.ZodString>;
290
+ error: z.ZodString;
291
+ }, z.core.$strict>;
230
292
  export declare const PreviewElementClickedResponseSchema: z.ZodObject<{
231
293
  type: z.ZodLiteral<"ELEMENT_CLICKED">;
232
294
  revision: z.ZodOptional<z.ZodNumber>;
@@ -351,11 +413,15 @@ export declare const PreviewResponseSchema: z.ZodDiscriminatedUnion<[z.ZodObject
351
413
  }, z.core.$strict>, z.ZodObject<{
352
414
  type: z.ZodLiteral<"READY">;
353
415
  revision: z.ZodNumber;
354
- health: z.ZodOptional<z.ZodObject<{
416
+ health: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
355
417
  reactMounted: z.ZodLiteral<true>;
356
418
  builderRuntimeProvider: z.ZodLiteral<true>;
357
419
  protocolVersion: z.ZodLiteral<2>;
358
- }, z.core.$strict>>;
420
+ }, z.core.$strict>, z.ZodObject<{
421
+ edgeBlockEngine: z.ZodLiteral<true>;
422
+ blockHtmlReconcile: z.ZodLiteral<true>;
423
+ protocolVersion: z.ZodLiteral<3>;
424
+ }, z.core.$strict>], "protocolVersion">>;
359
425
  }, z.core.$strict>, z.ZodObject<{
360
426
  type: z.ZodLiteral<"APPLIED">;
361
427
  revision: z.ZodNumber;
@@ -363,6 +429,24 @@ export declare const PreviewResponseSchema: z.ZodDiscriminatedUnion<[z.ZodObject
363
429
  type: z.ZodLiteral<"APPLY_FAILED">;
364
430
  revision: z.ZodNumber;
365
431
  error: z.ZodString;
432
+ }, z.core.$strict>, z.ZodObject<{
433
+ type: z.ZodLiteral<"BLOCK_HTML_APPLIED">;
434
+ revision: z.ZodNumber;
435
+ pageId: z.ZodString;
436
+ sourceRevision: z.ZodString;
437
+ renderMode: z.ZodLiteral<"edge-block-engine">;
438
+ blocks: z.ZodArray<z.ZodObject<{
439
+ blockId: z.ZodString;
440
+ html: z.ZodString;
441
+ }, z.core.$strict>>;
442
+ }, z.core.$strict>, z.ZodObject<{
443
+ type: z.ZodLiteral<"BLOCK_HTML_FAILED">;
444
+ revision: z.ZodNumber;
445
+ pageId: z.ZodString;
446
+ sourceRevision: z.ZodOptional<z.ZodString>;
447
+ renderMode: z.ZodLiteral<"edge-block-engine">;
448
+ blockIds: z.ZodArray<z.ZodString>;
449
+ error: z.ZodString;
366
450
  }, z.core.$strict>, z.ZodObject<{
367
451
  type: z.ZodLiteral<"ELEMENT_CLICKED">;
368
452
  revision: z.ZodOptional<z.ZodNumber>;
@@ -1 +1 @@
1
- {"version":3,"file":"preview-protocol.d.ts","sourceRoot":"","sources":["../src/preview-protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAG5B,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;kBAYxB,CAAC;AACZ,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAMhC,CAAC;AAEZ,eAAO,MAAM,0BAA0B;;;;;;;kBAM5B,CAAC;AAEZ,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;;;;;;kBAMnC,CAAC;AAEZ,eAAO,MAAM,gCAAgC;;kBAIlC,CAAC;AAEZ,eAAO,MAAM,qBAAqB;;;EAA8B,CAAC;AACjE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;;;;GAKG;AACH,eAAO,MAAM,sCAAsC;;;;;;kBAKxC,CAAC;AAEZ,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BAM/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,eAAO,MAAM,gCAAgC;;;;;;kBAQlC,CAAC;AAEZ,eAAO,MAAM,0BAA0B;;;;;;;;kBAa5B,CAAC;AAEZ,eAAO,MAAM,4BAA4B;;;kBAK9B,CAAC;AAEZ,eAAO,MAAM,gCAAgC;;;;kBAMlC,CAAC;AAEZ,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;kBAMrC,CAAC;AAEZ,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA0B5B,CAAC;AAEZ;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB;;;;;kBAOnB,CAAC;AACZ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,8BAA8B;;;;;;;;;;kBAOhC,CAAC;AAEZ;;;;GAIG;AACH,eAAO,MAAM,kCAAkC;;;;;;;;;;kBAOpC,CAAC;AAEZ;;;;GAIG;AACH,eAAO,MAAM,qCAAqC;;;;;;kBAQvC,CAAC;AAEZ,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BAUhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
1
+ {"version":3,"file":"preview-protocol.d.ts","sourceRoot":"","sources":["../src/preview-protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAG5B,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;kBAYxB,CAAC;AACZ,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAMhC,CAAC;AAEZ,eAAO,MAAM,yBAAyB,mCAAiC,CAAC;AACxE,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,eAAO,MAAM,2BAA2B;;;kBAK7B,CAAC;AACZ,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEhF,eAAO,MAAM,kCAAkC;;;;;;;;;;kBASpC,CAAC;AAEZ,eAAO,MAAM,0BAA0B;;;;;;;kBAM5B,CAAC;AAEZ,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;;;;;;kBAMnC,CAAC;AAEZ,eAAO,MAAM,gCAAgC;;kBAIlC,CAAC;AAEZ,eAAO,MAAM,qBAAqB;;;EAA8B,CAAC;AACjE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;;;;GAKG;AACH,eAAO,MAAM,sCAAsC;;;;;;kBAKxC,CAAC;AAEZ,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BAO/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,eAAO,MAAM,gCAAgC;;;;;;kBAQlC,CAAC;AAEZ,eAAO,MAAM,wBAAwB;;;;;;;;uCAenC,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,eAAO,MAAM,0BAA0B;;;;;;;;;;;;kBAM5B,CAAC;AAEZ,eAAO,MAAM,4BAA4B;;;kBAK9B,CAAC;AAEZ,eAAO,MAAM,gCAAgC;;;;kBAMlC,CAAC;AAEZ,eAAO,MAAM,qCAAqC;;;;;;;;;;kBASvC,CAAC;AAEZ,eAAO,MAAM,oCAAoC;;;;;;;;kBAUtC,CAAC;AAEZ,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;kBAMrC,CAAC;AAEZ,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA0B5B,CAAC;AAEZ;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB;;;;;kBAOnB,CAAC;AACZ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,8BAA8B;;;;;;;;;;kBAOhC,CAAC;AAEZ;;;;GAIG;AACH,eAAO,MAAM,kCAAkC;;;;;;;;;;kBAOpC,CAAC;AAEZ;;;;GAIG;AACH,eAAO,MAAM,qCAAqC;;;;;;kBAQvC,CAAC;AAEZ,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BAYhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
@@ -20,6 +20,23 @@ export const PreviewApplyStateMessageSchema = z
20
20
  state: BuilderSettingsSchema,
21
21
  })
22
22
  .strict();
23
+ export const EdgeBlockRenderModeSchema = z.literal('edge-block-engine');
24
+ export const PreviewBlockHtmlPatchSchema = z
25
+ .object({
26
+ blockId: z.string().min(1),
27
+ html: z.string().min(1),
28
+ })
29
+ .strict();
30
+ export const PreviewApplyBlockHtmlMessageSchema = z
31
+ .object({
32
+ type: z.literal('APPLY_BLOCK_HTML'),
33
+ revision: z.number().int().nonnegative(),
34
+ pageId: z.string().min(1),
35
+ sourceRevision: z.string().min(1),
36
+ renderMode: EdgeBlockRenderModeSchema,
37
+ blocks: z.array(PreviewBlockHtmlPatchSchema).min(1),
38
+ })
39
+ .strict();
23
40
  export const PreviewReloadMessageSchema = z
24
41
  .object({
25
42
  type: z.literal('RELOAD'),
@@ -54,6 +71,7 @@ export const PreviewSetInteractionModeMessageSchema = z
54
71
  .strict();
55
72
  export const PreviewMessageSchema = z.discriminatedUnion('type', [
56
73
  PreviewApplyStateMessageSchema,
74
+ PreviewApplyBlockHtmlMessageSchema,
57
75
  PreviewReloadMessageSchema,
58
76
  PreviewSelectElementMessageSchema,
59
77
  PreviewRequestReadyMessageSchema,
@@ -68,18 +86,27 @@ export const PreviewBootstrapOkResponseSchema = z
68
86
  artifactStale: z.boolean().optional(),
69
87
  })
70
88
  .strict();
71
- export const PreviewReadyResponseSchema = z
72
- .object({
73
- type: z.literal('READY'),
74
- revision: z.number().int().nonnegative(),
75
- health: z
89
+ export const PreviewReadyHealthSchema = z.discriminatedUnion('protocolVersion', [
90
+ z
76
91
  .object({
77
92
  reactMounted: z.literal(true),
78
93
  builderRuntimeProvider: z.literal(true),
79
94
  protocolVersion: z.literal(2),
80
95
  })
81
- .strict()
82
- .optional(),
96
+ .strict(),
97
+ z
98
+ .object({
99
+ edgeBlockEngine: z.literal(true),
100
+ blockHtmlReconcile: z.literal(true),
101
+ protocolVersion: z.literal(3),
102
+ })
103
+ .strict(),
104
+ ]);
105
+ export const PreviewReadyResponseSchema = z
106
+ .object({
107
+ type: z.literal('READY'),
108
+ revision: z.number().int().nonnegative(),
109
+ health: PreviewReadyHealthSchema.optional(),
83
110
  })
84
111
  .strict();
85
112
  export const PreviewAppliedResponseSchema = z
@@ -95,6 +122,27 @@ export const PreviewApplyFailedResponseSchema = z
95
122
  error: z.string().min(1),
96
123
  })
97
124
  .strict();
125
+ export const PreviewBlockHtmlAppliedResponseSchema = z
126
+ .object({
127
+ type: z.literal('BLOCK_HTML_APPLIED'),
128
+ revision: z.number().int().nonnegative(),
129
+ pageId: z.string().min(1),
130
+ sourceRevision: z.string().min(1),
131
+ renderMode: EdgeBlockRenderModeSchema,
132
+ blocks: z.array(PreviewBlockHtmlPatchSchema).min(1),
133
+ })
134
+ .strict();
135
+ export const PreviewBlockHtmlFailedResponseSchema = z
136
+ .object({
137
+ type: z.literal('BLOCK_HTML_FAILED'),
138
+ revision: z.number().int().nonnegative(),
139
+ pageId: z.string().min(1),
140
+ sourceRevision: z.string().min(1).optional(),
141
+ renderMode: EdgeBlockRenderModeSchema,
142
+ blockIds: z.array(z.string().min(1)).min(1),
143
+ error: z.string().min(1),
144
+ })
145
+ .strict();
98
146
  export const PreviewElementClickedResponseSchema = z
99
147
  .object({
100
148
  type: z.literal('ELEMENT_CLICKED'),
@@ -183,6 +231,8 @@ export const PreviewResponseSchema = z.discriminatedUnion('type', [
183
231
  PreviewReadyResponseSchema,
184
232
  PreviewAppliedResponseSchema,
185
233
  PreviewApplyFailedResponseSchema,
234
+ PreviewBlockHtmlAppliedResponseSchema,
235
+ PreviewBlockHtmlFailedResponseSchema,
186
236
  PreviewElementClickedResponseSchema,
187
237
  PreviewErrorResponseSchema,
188
238
  PreviewBlockRectResponseSchema,
@@ -44,7 +44,7 @@ export function buildPlainInitialDataScript(initialData) {
44
44
  export function buildWrappedStorefrontInitialDataScript(serialized, deployedThemeEntryPathPattern) {
45
45
  return [
46
46
  '<!--shoppex-initial-data:start--><script>(function(){',
47
- 'if(typeof window!=="undefined"){var path=window.location.pathname||"";',
47
+ 'if(typeof window!=="undefined"){window.__SHOPPEX_DEPLOYED_THEME_ARTIFACT__=true;var path=window.location.pathname||"";',
48
48
  `if(${deployedThemeEntryPathPattern}.test(path)){window.__SHOPPEX_DEPLOYED_THEME_ARTIFACT_PATH__=path;window.history.replaceState(window.history.state,"","/"+window.location.search+window.location.hash);}}`,
49
49
  `window.__SHOPPEX_INITIAL__=${serialized};})();</script><!--shoppex-initial-data:end-->`,
50
50
  ].join('');
@@ -252,13 +252,13 @@ export declare const ManifestBlockSchema: z.ZodObject<{
252
252
  number: "number";
253
253
  boolean: "boolean";
254
254
  link: "link";
255
+ product: "product";
255
256
  text: "text";
256
257
  richtext: "richtext";
257
258
  image: "image";
258
259
  range: "range";
259
260
  select: "select";
260
261
  color: "color";
261
- product: "product";
262
262
  }>;
263
263
  options: z.ZodOptional<z.ZodArray<z.ZodObject<{
264
264
  label: z.ZodString;
@@ -585,13 +585,13 @@ export declare const ThemeManifestSchema: z.ZodObject<{
585
585
  number: "number";
586
586
  boolean: "boolean";
587
587
  link: "link";
588
+ product: "product";
588
589
  text: "text";
589
590
  richtext: "richtext";
590
591
  image: "image";
591
592
  range: "range";
592
593
  select: "select";
593
594
  color: "color";
594
- product: "product";
595
595
  }>;
596
596
  options: z.ZodOptional<z.ZodArray<z.ZodObject<{
597
597
  label: z.ZodString;
@@ -1,6 +1,6 @@
1
- export declare const PUBLIC_BUILDER_THEME_SCHEMES: readonly ["default", "classic", "nebula", "pulse", "phantom", "starlight", "apex", "vault", "clean-minimal"];
1
+ export declare const PUBLIC_BUILDER_THEME_SCHEMES: readonly ["default", "classic", "nebula", "pulse", "phantom", "starlight", "apex", "vault", "vortex", "clean-minimal"];
2
2
  export declare const STARTER_BUILDER_THEME_SCHEMES: readonly ["blank"];
3
- export declare const BUILDER_READY_THEME_SCHEMES: readonly ["blank", "default", "classic", "nebula", "pulse", "phantom", "starlight", "apex", "vault", "clean-minimal"];
3
+ export declare const BUILDER_READY_THEME_SCHEMES: readonly ["blank", "default", "classic", "nebula", "pulse", "phantom", "starlight", "apex", "vault", "vortex", "clean-minimal"];
4
4
  export type PublicBuilderThemeScheme = (typeof PUBLIC_BUILDER_THEME_SCHEMES)[number];
5
5
  export type StarterBuilderThemeScheme = (typeof STARTER_BUILDER_THEME_SCHEMES)[number];
6
6
  export type BuilderReadyThemeScheme = (typeof BUILDER_READY_THEME_SCHEMES)[number];
@@ -1 +1 @@
1
- {"version":3,"file":"theme-schemes.d.ts","sourceRoot":"","sources":["../src/theme-schemes.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,4BAA4B,8GAU/B,CAAC;AAEX,eAAO,MAAM,6BAA6B,oBAAqB,CAAC;AAEhE,eAAO,MAAM,2BAA2B,uHAG9B,CAAC;AAEX,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,4BAA4B,CAAC,CAAC,MAAM,CAAC,CAAC;AACrF,MAAM,MAAM,yBAAyB,GAAG,CAAC,OAAO,6BAA6B,CAAC,CAAC,MAAM,CAAC,CAAC;AACvF,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,wBAAwB,CAE3F;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,yBAAyB,CAE7F;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,uBAAuB,CAEzF"}
1
+ {"version":3,"file":"theme-schemes.d.ts","sourceRoot":"","sources":["../src/theme-schemes.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,4BAA4B,wHAW/B,CAAC;AAEX,eAAO,MAAM,6BAA6B,oBAAqB,CAAC;AAEhE,eAAO,MAAM,2BAA2B,iIAG9B,CAAC;AAEX,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,4BAA4B,CAAC,CAAC,MAAM,CAAC,CAAC;AACrF,MAAM,MAAM,yBAAyB,GAAG,CAAC,OAAO,6BAA6B,CAAC,CAAC,MAAM,CAAC,CAAC;AACvF,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,wBAAwB,CAE3F;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,yBAAyB,CAE7F;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,uBAAuB,CAEzF"}
@@ -7,6 +7,7 @@ export const PUBLIC_BUILDER_THEME_SCHEMES = [
7
7
  'starlight',
8
8
  'apex',
9
9
  'vault',
10
+ 'vortex',
10
11
  'clean-minimal',
11
12
  ];
12
13
  export const STARTER_BUILDER_THEME_SCHEMES = ['blank'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shoppexio/builder-contracts",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Shared Builder v2 contracts for Shoppex dashboard, backend, preview runtime, and themes",
5
5
  "type": "module",
6
6
  "repository": {
@@ -13,6 +13,8 @@ import {
13
13
  createBlockInstance,
14
14
  createEmptyBuilderSettings,
15
15
  convertLegacyThemeManifest,
16
+ coercePersistedBuilderSettings,
17
+ exportCoercedBuilderSettingsBlob,
16
18
  extractPersistedBuilderSettings,
17
19
  canonicalizeBuilderSettingsForManifestWithReport,
18
20
  mergeBuilderSettingsIntoThemeSettings,
@@ -220,6 +222,52 @@ describe('@shoppex/builder-contracts', () => {
220
222
  expect(extractPersistedBuilderSettings(persisted)).toEqual(settings);
221
223
  });
222
224
 
225
+ test('coerces polluted v2 theme.typography into style_slots and strips invalid keys', () => {
226
+ const coerced = coercePersistedBuilderSettings({
227
+ version: 2,
228
+ revision: 9,
229
+ theme: {
230
+ content: {},
231
+ layout: {},
232
+ style_slots: {
233
+ 'theme.typography.font.family': 'Manrope, sans-serif',
234
+ },
235
+ typography: {
236
+ font_family: 'Legacy Body',
237
+ heading_font: 'Legacy Heading',
238
+ },
239
+ pages: [],
240
+ terms: {},
241
+ },
242
+ });
243
+
244
+ expect(coerced).not.toBeNull();
245
+ expect(coerced?.theme.style_slots['theme.typography.font.family']).toBe('Manrope, sans-serif');
246
+ expect(coerced?.theme.style_slots['theme.typography.heading.font']).toBe('Legacy Heading');
247
+ expect(coerced?.theme).not.toHaveProperty('typography');
248
+ expect(() => BuilderSettingsSchema.parse(coerced)).not.toThrow();
249
+ });
250
+
251
+ test('exportCoercedBuilderSettingsBlob preserves non-v2 siblings', () => {
252
+ const exported = exportCoercedBuilderSettingsBlob({
253
+ seo: { default_title: 'Keep this title' },
254
+ version: 2,
255
+ revision: 4,
256
+ theme: {
257
+ content: {},
258
+ layout: {},
259
+ style_slots: {},
260
+ typography: { font_family: 'Inter' },
261
+ pages: [],
262
+ terms: {},
263
+ },
264
+ });
265
+
266
+ expect(exported?.seo).toEqual({ default_title: 'Keep this title' });
267
+ expect(exported?.theme).not.toHaveProperty('typography');
268
+ expect(exported?.theme.style_slots['theme.typography.font.family']).toBe('Inter');
269
+ });
270
+
223
271
  test('removes reserved content keys from persisted builder v2 state', () => {
224
272
  const settings = BuilderSettingsSchema.parse({
225
273
  ...createEmptyBuilderSettings(5),
@@ -809,7 +857,7 @@ describe('@shoppex/builder-contracts', () => {
809
857
 
810
858
  test('parses official theme manifests with transparent contact-form backgrounds', () => {
811
859
  const repoRoot = join(import.meta.dir, '../../..');
812
- for (const theme of ['default', 'nebula', 'classic', 'pulse', 'starlight', 'vault', 'phantom', 'apex']) {
860
+ for (const theme of ['default', 'nebula', 'classic', 'pulse', 'starlight', 'vault', 'phantom', 'apex', 'vortex']) {
813
861
  const manifestPath = join(repoRoot, 'themes', theme, 'theme.manifest.json');
814
862
  const raw = JSON.parse(readFileSync(manifestPath, 'utf8'));
815
863
  expect(ThemeManifestSchema.safeParse(raw).success, `${theme} manifest should validate`).toBe(true);
package/src/migrations.ts CHANGED
@@ -110,6 +110,42 @@ export function applyManifestDefaultLayout(settings: BuilderSettings, manifest:
110
110
  return BuilderSettingsSchema.parse(next);
111
111
  }
112
112
 
113
+ export function mapLegacyNestedThemeTypographyToStyleSlots(
114
+ theme: Record<string, unknown>,
115
+ baseSlots: StyleSlots = {},
116
+ ): StyleSlots {
117
+ const slots: StyleSlots = { ...baseSlots };
118
+ const typography = theme.typography;
119
+ if (!isRecord(typography)) {
120
+ return slots;
121
+ }
122
+
123
+ const bodyFont = readLegacyTypographyString(typography, 'font_family', 'fontFamily');
124
+ const headingFont = readLegacyTypographyString(typography, 'heading_font', 'headingFont');
125
+
126
+ if (bodyFont && slots['theme.typography.font.family'] === undefined) {
127
+ slots['theme.typography.font.family'] = bodyFont;
128
+ }
129
+ if (headingFont && slots['theme.typography.heading.font'] === undefined) {
130
+ slots['theme.typography.heading.font'] = headingFont;
131
+ }
132
+
133
+ return slots;
134
+ }
135
+
136
+ function readLegacyTypographyString(
137
+ typography: Record<string, unknown>,
138
+ ...keys: string[]
139
+ ): string | null {
140
+ for (const key of keys) {
141
+ const value = typography[key];
142
+ if (typeof value === 'string' && value.trim().length > 0) {
143
+ return value.trim();
144
+ }
145
+ }
146
+ return null;
147
+ }
148
+
113
149
  export function mapLegacyTokenOverridesToStyleSlots(tokens: Record<string, unknown>): StyleSlots {
114
150
  const slots: StyleSlots = {};
115
151
 
@@ -2,6 +2,12 @@ import {
2
2
  type BuilderSettings,
3
3
  BuilderSettingsSchema,
4
4
  } from './builder-settings.ts';
5
+ import {
6
+ mapLegacyNestedThemeTypographyToStyleSlots,
7
+ mapLegacyTokenOverridesToStyleSlots,
8
+ migrateLegacyBuilderSettings,
9
+ } from './migrations.ts';
10
+ import type { StyleSlots } from './style-slots.ts';
5
11
 
6
12
  type JsonRecord = Record<string, unknown>;
7
13
  const RESERVED_THEME_CONTENT_KEYS = new Set(['layout']);
@@ -36,6 +42,107 @@ export function extractPersistedBuilderSettings(input: unknown): BuilderSettings
36
42
  return parsedNested.success ? sanitizeBuilderSettingsState(parsedNested.data) : null;
37
43
  }
38
44
 
45
+ function parseBuilderSettingsRevision(input: unknown): number {
46
+ return typeof input === 'number' && Number.isInteger(input) && input >= 0 ? input : 0;
47
+ }
48
+
49
+ function unwrapBuilderSettingsRecord(input: unknown): JsonRecord | null {
50
+ if (!isRecord(input)) {
51
+ return null;
52
+ }
53
+
54
+ if (isRecord(input.builder_settings)) {
55
+ return input.builder_settings;
56
+ }
57
+
58
+ if ('version' in input || 'revision' in input || 'theme' in input) {
59
+ return input;
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ function readStyleSlotsRecord(theme: JsonRecord): StyleSlots {
66
+ return isRecord(theme.style_slots) ? theme.style_slots as StyleSlots : {};
67
+ }
68
+
69
+ /**
70
+ * Single read boundary for builder v2 state: strict extract first, then legacy migration.
71
+ * Strips invalid keys such as nested `theme.typography` while preserving style_slots and pages.
72
+ */
73
+ export function coercePersistedBuilderSettings(input: unknown): BuilderSettings | null {
74
+ const extracted = extractPersistedBuilderSettings(input);
75
+ if (extracted) {
76
+ return extracted;
77
+ }
78
+
79
+ if (isRecord(input) && isRecord(input.builder_settings)) {
80
+ const nestedExtracted = extractPersistedBuilderSettings(input.builder_settings);
81
+ if (nestedExtracted) {
82
+ return nestedExtracted;
83
+ }
84
+ }
85
+
86
+ const blob = unwrapBuilderSettingsRecord(input);
87
+ if (!blob) {
88
+ return null;
89
+ }
90
+
91
+ const theme = isRecord(blob.theme) ? blob.theme : {};
92
+ const tokensOverride = isRecord(theme.tokens_override) ? theme.tokens_override : {};
93
+ const styleSlots = mapLegacyNestedThemeTypographyToStyleSlots(
94
+ theme,
95
+ {
96
+ ...mapLegacyTokenOverridesToStyleSlots(tokensOverride),
97
+ ...readStyleSlotsRecord(theme),
98
+ },
99
+ );
100
+
101
+ const migrated = migrateLegacyBuilderSettings(
102
+ {
103
+ theme: {
104
+ content: isRecord(theme.content) ? theme.content : {},
105
+ layout: isRecord(theme.layout) ? theme.layout : {},
106
+ tokens_override: tokensOverride,
107
+ style_slots: styleSlots,
108
+ },
109
+ },
110
+ parseBuilderSettingsRevision(blob.revision),
111
+ );
112
+
113
+ const merged = BuilderSettingsSchema.safeParse({
114
+ ...migrated,
115
+ theme: {
116
+ ...migrated.theme,
117
+ pages: Array.isArray(theme.pages) ? theme.pages : migrated.theme.pages,
118
+ terms: isRecord(theme.terms) ? theme.terms : migrated.theme.terms,
119
+ },
120
+ });
121
+
122
+ return merged.success ? sanitizeBuilderSettingsState(merged.data) : migrated;
123
+ }
124
+
125
+ /**
126
+ * Coerces builder v2 fields for storefront export while preserving non-v2 siblings (seo, appearance, …).
127
+ */
128
+ export function exportCoercedBuilderSettingsBlob(builderSettingsBlob: unknown): JsonRecord | null {
129
+ if (!isRecord(builderSettingsBlob)) {
130
+ return null;
131
+ }
132
+
133
+ const coerced = coercePersistedBuilderSettings(builderSettingsBlob);
134
+ if (!coerced) {
135
+ return null;
136
+ }
137
+
138
+ return {
139
+ ...builderSettingsBlob,
140
+ version: coerced.version,
141
+ revision: coerced.revision,
142
+ theme: coerced.theme,
143
+ };
144
+ }
145
+
39
146
  export function sanitizeBuilderSettingsState(settings: BuilderSettings): BuilderSettings {
40
147
  const content = sanitizeThemeContent(settings.theme.content);
41
148
  return BuilderSettingsSchema.parse({
@@ -5,6 +5,44 @@ import {
5
5
  } from './preview-protocol.ts';
6
6
 
7
7
  describe('PreviewMessageSchema', () => {
8
+ it('accepts v3 block HTML apply messages', () => {
9
+ const parsed = PreviewMessageSchema.parse({
10
+ type: 'APPLY_BLOCK_HTML',
11
+ revision: 12,
12
+ pageId: 'product',
13
+ sourceRevision: 'theme-source-12',
14
+ renderMode: 'edge-block-engine',
15
+ blocks: [
16
+ {
17
+ blockId: 'product-form-1',
18
+ html: '<section data-builder-block="product-form-1">Updated</section>',
19
+ },
20
+ ],
21
+ });
22
+
23
+ expect(parsed.type).toBe('APPLY_BLOCK_HTML');
24
+ if (parsed.type === 'APPLY_BLOCK_HTML') {
25
+ expect(parsed.blocks[0]?.blockId).toBe('product-form-1');
26
+ }
27
+ });
28
+
29
+ it('rejects v3 block HTML apply messages without sourceRevision', () => {
30
+ const result = PreviewMessageSchema.safeParse({
31
+ type: 'APPLY_BLOCK_HTML',
32
+ revision: 12,
33
+ pageId: 'product',
34
+ renderMode: 'edge-block-engine',
35
+ blocks: [
36
+ {
37
+ blockId: 'product-form-1',
38
+ html: '<section data-builder-block="product-form-1">Updated</section>',
39
+ },
40
+ ],
41
+ });
42
+
43
+ expect(result.success).toBe(false);
44
+ });
45
+
8
46
  it('accepts SET_INTERACTION_MODE with edit/preview', () => {
9
47
  for (const mode of ['edit', 'preview'] as const) {
10
48
  const parsed = PreviewMessageSchema.parse({
@@ -25,6 +63,58 @@ describe('PreviewMessageSchema', () => {
25
63
  });
26
64
 
27
65
  describe('PreviewResponseSchema', () => {
66
+ it('accepts READY health for edge block protocol v3', () => {
67
+ const parsed = PreviewResponseSchema.parse({
68
+ type: 'READY',
69
+ revision: 12,
70
+ health: {
71
+ edgeBlockEngine: true,
72
+ blockHtmlReconcile: true,
73
+ protocolVersion: 3,
74
+ },
75
+ });
76
+
77
+ expect(parsed.type).toBe('READY');
78
+ if (parsed.type === 'READY') {
79
+ expect(parsed.health?.protocolVersion).toBe(3);
80
+ }
81
+ });
82
+
83
+ it('accepts v3 block HTML applied responses', () => {
84
+ const parsed = PreviewResponseSchema.parse({
85
+ type: 'BLOCK_HTML_APPLIED',
86
+ revision: 12,
87
+ pageId: 'product',
88
+ sourceRevision: 'theme-source-12',
89
+ renderMode: 'edge-block-engine',
90
+ blocks: [
91
+ {
92
+ blockId: 'footer-1',
93
+ html: '<footer data-builder-block="footer-1">Secure delivery</footer>',
94
+ },
95
+ ],
96
+ });
97
+
98
+ expect(parsed.type).toBe('BLOCK_HTML_APPLIED');
99
+ if (parsed.type === 'BLOCK_HTML_APPLIED') {
100
+ expect(parsed.renderMode).toBe('edge-block-engine');
101
+ }
102
+ });
103
+
104
+ it('accepts v3 block HTML failed responses', () => {
105
+ const parsed = PreviewResponseSchema.parse({
106
+ type: 'BLOCK_HTML_FAILED',
107
+ revision: 12,
108
+ pageId: 'product',
109
+ sourceRevision: 'theme-source-12',
110
+ renderMode: 'edge-block-engine',
111
+ blockIds: ['footer-1'],
112
+ error: 'Rendered block root does not match block id.',
113
+ });
114
+
115
+ expect(parsed.type).toBe('BLOCK_HTML_FAILED');
116
+ });
117
+
28
118
  it('accepts BLOCK_RECT with rect', () => {
29
119
  const parsed = PreviewResponseSchema.parse({
30
120
  type: 'BLOCK_RECT',
@@ -24,6 +24,28 @@ export const PreviewApplyStateMessageSchema = z
24
24
  })
25
25
  .strict();
26
26
 
27
+ export const EdgeBlockRenderModeSchema = z.literal('edge-block-engine');
28
+ export type EdgeBlockRenderMode = z.infer<typeof EdgeBlockRenderModeSchema>;
29
+
30
+ export const PreviewBlockHtmlPatchSchema = z
31
+ .object({
32
+ blockId: z.string().min(1),
33
+ html: z.string().min(1),
34
+ })
35
+ .strict();
36
+ export type PreviewBlockHtmlPatch = z.infer<typeof PreviewBlockHtmlPatchSchema>;
37
+
38
+ export const PreviewApplyBlockHtmlMessageSchema = z
39
+ .object({
40
+ type: z.literal('APPLY_BLOCK_HTML'),
41
+ revision: z.number().int().nonnegative(),
42
+ pageId: z.string().min(1),
43
+ sourceRevision: z.string().min(1),
44
+ renderMode: EdgeBlockRenderModeSchema,
45
+ blocks: z.array(PreviewBlockHtmlPatchSchema).min(1),
46
+ })
47
+ .strict();
48
+
27
49
  export const PreviewReloadMessageSchema = z
28
50
  .object({
29
51
  type: z.literal('RELOAD'),
@@ -64,6 +86,7 @@ export const PreviewSetInteractionModeMessageSchema = z
64
86
 
65
87
  export const PreviewMessageSchema = z.discriminatedUnion('type', [
66
88
  PreviewApplyStateMessageSchema,
89
+ PreviewApplyBlockHtmlMessageSchema,
67
90
  PreviewReloadMessageSchema,
68
91
  PreviewSelectElementMessageSchema,
69
92
  PreviewRequestReadyMessageSchema,
@@ -81,18 +104,29 @@ export const PreviewBootstrapOkResponseSchema = z
81
104
  })
82
105
  .strict();
83
106
 
107
+ export const PreviewReadyHealthSchema = z.discriminatedUnion('protocolVersion', [
108
+ z
109
+ .object({
110
+ reactMounted: z.literal(true),
111
+ builderRuntimeProvider: z.literal(true),
112
+ protocolVersion: z.literal(2),
113
+ })
114
+ .strict(),
115
+ z
116
+ .object({
117
+ edgeBlockEngine: z.literal(true),
118
+ blockHtmlReconcile: z.literal(true),
119
+ protocolVersion: z.literal(3),
120
+ })
121
+ .strict(),
122
+ ]);
123
+ export type PreviewReadyHealth = z.infer<typeof PreviewReadyHealthSchema>;
124
+
84
125
  export const PreviewReadyResponseSchema = z
85
126
  .object({
86
127
  type: z.literal('READY'),
87
128
  revision: z.number().int().nonnegative(),
88
- health: z
89
- .object({
90
- reactMounted: z.literal(true),
91
- builderRuntimeProvider: z.literal(true),
92
- protocolVersion: z.literal(2),
93
- })
94
- .strict()
95
- .optional(),
129
+ health: PreviewReadyHealthSchema.optional(),
96
130
  })
97
131
  .strict();
98
132
 
@@ -111,6 +145,29 @@ export const PreviewApplyFailedResponseSchema = z
111
145
  })
112
146
  .strict();
113
147
 
148
+ export const PreviewBlockHtmlAppliedResponseSchema = z
149
+ .object({
150
+ type: z.literal('BLOCK_HTML_APPLIED'),
151
+ revision: z.number().int().nonnegative(),
152
+ pageId: z.string().min(1),
153
+ sourceRevision: z.string().min(1),
154
+ renderMode: EdgeBlockRenderModeSchema,
155
+ blocks: z.array(PreviewBlockHtmlPatchSchema).min(1),
156
+ })
157
+ .strict();
158
+
159
+ export const PreviewBlockHtmlFailedResponseSchema = z
160
+ .object({
161
+ type: z.literal('BLOCK_HTML_FAILED'),
162
+ revision: z.number().int().nonnegative(),
163
+ pageId: z.string().min(1),
164
+ sourceRevision: z.string().min(1).optional(),
165
+ renderMode: EdgeBlockRenderModeSchema,
166
+ blockIds: z.array(z.string().min(1)).min(1),
167
+ error: z.string().min(1),
168
+ })
169
+ .strict();
170
+
114
171
  export const PreviewElementClickedResponseSchema = z
115
172
  .object({
116
173
  type: z.literal('ELEMENT_CLICKED'),
@@ -206,6 +263,8 @@ export const PreviewResponseSchema = z.discriminatedUnion('type', [
206
263
  PreviewReadyResponseSchema,
207
264
  PreviewAppliedResponseSchema,
208
265
  PreviewApplyFailedResponseSchema,
266
+ PreviewBlockHtmlAppliedResponseSchema,
267
+ PreviewBlockHtmlFailedResponseSchema,
209
268
  PreviewElementClickedResponseSchema,
210
269
  PreviewErrorResponseSchema,
211
270
  PreviewBlockRectResponseSchema,
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'bun:test';
2
2
  import {
3
3
  assertSingleInitialDataScript,
4
4
  buildPlainInitialDataScript,
5
+ buildWrappedStorefrontInitialDataScript,
5
6
  countInitialDataScripts,
6
7
  findInitialDataPayload,
7
8
  injectPreviewInitialData,
@@ -70,4 +71,11 @@ describe('storefront-initial-data-html', () => {
70
71
  const stripped = stripAllInitialDataScripts(`${wrapped}${legacy}`);
71
72
  expect(stripped).not.toContain('__SHOPPEX_INITIAL__');
72
73
  });
74
+
75
+ it('marks wrapped initial data as a deployed theme artifact on friendly storefront paths', () => {
76
+ const script = buildWrappedStorefrontInitialDataScript('{"store":{"slug":"demo-shop"}}', /^\/themes-live\//);
77
+
78
+ expect(script).toContain('window.__SHOPPEX_DEPLOYED_THEME_ARTIFACT__=true');
79
+ expect(script).toContain('window.__SHOPPEX_INITIAL__={"store":{"slug":"demo-shop"}}');
80
+ });
73
81
  });
@@ -66,7 +66,7 @@ export function buildWrappedStorefrontInitialDataScript(
66
66
  ): string {
67
67
  return [
68
68
  '<!--shoppex-initial-data:start--><script>(function(){',
69
- 'if(typeof window!=="undefined"){var path=window.location.pathname||"";',
69
+ 'if(typeof window!=="undefined"){window.__SHOPPEX_DEPLOYED_THEME_ARTIFACT__=true;var path=window.location.pathname||"";',
70
70
  `if(${deployedThemeEntryPathPattern}.test(path)){window.__SHOPPEX_DEPLOYED_THEME_ARTIFACT_PATH__=path;window.history.replaceState(window.history.state,"","/"+window.location.search+window.location.hash);}}`,
71
71
  `window.__SHOPPEX_INITIAL__=${serialized};})();</script><!--shoppex-initial-data:end-->`,
72
72
  ].join('');
@@ -7,6 +7,7 @@ export const PUBLIC_BUILDER_THEME_SCHEMES = [
7
7
  'starlight',
8
8
  'apex',
9
9
  'vault',
10
+ 'vortex',
10
11
  'clean-minimal',
11
12
  ] as const;
12
13