@shoppexio/builder-contracts 0.1.13 → 0.1.15

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.
@@ -1,5 +1,7 @@
1
1
  import type { BuilderSettings } from './builder-settings.ts';
2
2
  import type { ThemeManifest } from './theme-manifest.ts';
3
+ /** Render-time enrichment keys that must never be persisted in builder settings. */
4
+ export declare const NON_PERSISTABLE_BLOCK_SETTING_KEYS: Set<string>;
3
5
  export type BuilderSettingsCanonicalizationConflict = {
4
6
  pageId: string;
5
7
  blockId: string;
@@ -13,6 +15,16 @@ export type BuilderSettingsCanonicalizationResult = {
13
15
  removedAliases: number;
14
16
  conflicts: BuilderSettingsCanonicalizationConflict[];
15
17
  };
18
+ export type BuilderSettingsSanitizationPrunedEntry = {
19
+ path: string;
20
+ key: string;
21
+ reason: 'non_persistable_block_setting' | 'unknown_block_setting' | 'unexposed_style_slot' | 'unknown_style_slot';
22
+ };
23
+ export type BuilderSettingsSanitizationResult = BuilderSettingsCanonicalizationResult & {
24
+ pruned: BuilderSettingsSanitizationPrunedEntry[];
25
+ };
16
26
  export declare function canonicalizeBuilderSettingsForManifest(settings: BuilderSettings, manifest: ThemeManifest): BuilderSettings;
17
27
  export declare function canonicalizeBuilderSettingsForManifestWithReport(settings: BuilderSettings, manifest: ThemeManifest): BuilderSettingsCanonicalizationResult;
28
+ export declare function sanitizeBuilderSettingsForManifest(settings: BuilderSettings, manifest: ThemeManifest): BuilderSettings;
29
+ export declare function sanitizeBuilderSettingsForManifestWithReport(settings: BuilderSettings, manifest: ThemeManifest): BuilderSettingsSanitizationResult;
18
30
  //# sourceMappingURL=canonical-settings.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"canonical-settings.d.ts","sourceRoot":"","sources":["../src/canonical-settings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,MAAM,uCAAuC,GAAG;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,qCAAqC,GAAG;IAClD,QAAQ,EAAE,eAAe,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,uCAAuC,EAAE,CAAC;CACtD,CAAC;AAEF,wBAAgB,sCAAsC,CACpD,QAAQ,EAAE,eAAe,EACzB,QAAQ,EAAE,aAAa,GACtB,eAAe,CAEjB;AAED,wBAAgB,gDAAgD,CAC9D,QAAQ,EAAE,eAAe,EACzB,QAAQ,EAAE,aAAa,GACtB,qCAAqC,CAwBvC"}
1
+ {"version":3,"file":"canonical-settings.d.ts","sourceRoot":"","sources":["../src/canonical-settings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAI5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,oFAAoF;AACpF,eAAO,MAAM,kCAAkC,aAK7C,CAAC;AAEH,MAAM,MAAM,uCAAuC,GAAG;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,qCAAqC,GAAG;IAClD,QAAQ,EAAE,eAAe,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,uCAAuC,EAAE,CAAC;CACtD,CAAC;AAEF,MAAM,MAAM,sCAAsC,GAAG;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EACF,+BAA+B,GAC/B,uBAAuB,GACvB,sBAAsB,GACtB,oBAAoB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG,qCAAqC,GAAG;IACtF,MAAM,EAAE,sCAAsC,EAAE,CAAC;CAClD,CAAC;AAEF,wBAAgB,sCAAsC,CACpD,QAAQ,EAAE,eAAe,EACzB,QAAQ,EAAE,aAAa,GACtB,eAAe,CAEjB;AAED,wBAAgB,gDAAgD,CAC9D,QAAQ,EAAE,eAAe,EACzB,QAAQ,EAAE,aAAa,GACtB,qCAAqC,CAwBvC;AAED,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,eAAe,EACzB,QAAQ,EAAE,aAAa,GACtB,eAAe,CAEjB;AAED,wBAAgB,4CAA4C,CAC1D,QAAQ,EAAE,eAAe,EACzB,QAAQ,EAAE,aAAa,GACtB,iCAAiC,CA0BnC"}
@@ -1,4 +1,13 @@
1
+ import { mergeCustomPagesIntoManifest } from "./custom-pages.js";
1
2
  import { sanitizeBuilderSettingsState } from "./persistence.js";
3
+ import { ThemeStyleSlotIdSchema } from "./style-slots.js";
4
+ /** Render-time enrichment keys that must never be persisted in builder settings. */
5
+ export const NON_PERSISTABLE_BLOCK_SETTING_KEYS = new Set([
6
+ 'titleHasHighlight',
7
+ 'titleBefore',
8
+ 'titleHighlight',
9
+ 'titleAfter',
10
+ ]);
2
11
  export function canonicalizeBuilderSettingsForManifest(settings, manifest) {
3
12
  return canonicalizeBuilderSettingsForManifestWithReport(settings, manifest).settings;
4
13
  }
@@ -25,6 +34,99 @@ export function canonicalizeBuilderSettingsForManifestWithReport(settings, manif
25
34
  conflicts,
26
35
  };
27
36
  }
37
+ export function sanitizeBuilderSettingsForManifest(settings, manifest) {
38
+ return sanitizeBuilderSettingsForManifestWithReport(settings, manifest).settings;
39
+ }
40
+ export function sanitizeBuilderSettingsForManifestWithReport(settings, manifest) {
41
+ const canonical = canonicalizeBuilderSettingsForManifestWithReport(settings, manifest);
42
+ const pruned = [];
43
+ stripNonPersistableBlockSettings(canonical.settings, pruned);
44
+ const effectiveManifest = mergeCustomPagesIntoManifest(manifest, canonical.settings);
45
+ pruneUnknownGlobalStyleSlots(canonical.settings, effectiveManifest, pruned);
46
+ for (const [pageId, layout] of Object.entries(canonical.settings.theme.layout)) {
47
+ for (const [index, block] of layout.blocks.entries()) {
48
+ const blockPath = `theme.layout.${pageId}.blocks.${index}`;
49
+ const blockDefinition = effectiveManifest.blocks[block.type];
50
+ if (!blockDefinition) {
51
+ continue;
52
+ }
53
+ pruneBlockSettings(block, Object.keys(blockDefinition.settings), blockPath, pruned);
54
+ pruneBlockStyleOverrides(block, blockDefinition.exposedStyleSlots, blockPath, pruned);
55
+ }
56
+ }
57
+ return {
58
+ ...canonical,
59
+ pruned,
60
+ };
61
+ }
62
+ function stripNonPersistableBlockSettings(settings, pruned) {
63
+ for (const [pageId, layout] of Object.entries(settings.theme.layout)) {
64
+ for (const [index, block] of layout.blocks.entries()) {
65
+ const blockPath = `theme.layout.${pageId}.blocks.${index}`;
66
+ for (const key of Object.keys(block.settings)) {
67
+ if (!NON_PERSISTABLE_BLOCK_SETTING_KEYS.has(key)) {
68
+ continue;
69
+ }
70
+ delete block.settings[key];
71
+ pruned.push({
72
+ path: `${blockPath}.settings.${key}`,
73
+ key,
74
+ reason: 'non_persistable_block_setting',
75
+ });
76
+ }
77
+ }
78
+ }
79
+ }
80
+ function pruneUnknownGlobalStyleSlots(settings, manifest, pruned) {
81
+ const declaredThemeSlots = new Set(Object.keys(manifest.styleSlots));
82
+ for (const slotId of Object.keys(settings.theme.style_slots)) {
83
+ if (!ThemeStyleSlotIdSchema.safeParse(slotId).success) {
84
+ continue;
85
+ }
86
+ if (declaredThemeSlots.has(slotId)) {
87
+ continue;
88
+ }
89
+ const styleSlots = settings.theme.style_slots;
90
+ delete styleSlots[slotId];
91
+ pruned.push({
92
+ path: `theme.style_slots.${slotId}`,
93
+ key: slotId,
94
+ reason: 'unknown_style_slot',
95
+ });
96
+ }
97
+ }
98
+ function pruneBlockSettings(block, settingPaths, blockPath, pruned) {
99
+ const allowedSettings = new Set(settingPaths);
100
+ for (const key of Object.keys(block.settings)) {
101
+ if (allowedSettings.has(key)) {
102
+ continue;
103
+ }
104
+ delete block.settings[key];
105
+ pruned.push({
106
+ path: `${blockPath}.settings.${key}`,
107
+ key,
108
+ reason: 'unknown_block_setting',
109
+ });
110
+ }
111
+ }
112
+ function pruneBlockStyleOverrides(block, exposedStyleSlots, blockPath, pruned) {
113
+ if (!block.style_overrides) {
114
+ return;
115
+ }
116
+ const exposed = new Set(exposedStyleSlots);
117
+ const overrides = block.style_overrides;
118
+ for (const slotId of Object.keys(overrides)) {
119
+ if (exposed.has(slotId)) {
120
+ continue;
121
+ }
122
+ delete overrides[slotId];
123
+ pruned.push({
124
+ path: `${blockPath}.style_overrides.${slotId}`,
125
+ key: slotId,
126
+ reason: 'unexposed_style_slot',
127
+ });
128
+ }
129
+ }
28
130
  function canonicalizeBlockSettings(pageId, block, settingPaths) {
29
131
  const aliases = buildAliasMap(block.type, settingPaths);
30
132
  const conflicts = [];
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";
18
17
  text: "text";
19
18
  richtext: "richtext";
20
19
  image: "image";
21
20
  range: "range";
22
21
  select: "select";
23
22
  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";
42
41
  text: "text";
43
42
  richtext: "richtext";
44
43
  image: "image";
45
44
  range: "range";
46
45
  select: "select";
47
46
  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";
270
269
  text: "text";
271
270
  richtext: "richtext";
272
271
  image: "image";
273
272
  range: "range";
274
273
  select: "select";
275
274
  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";
499
498
  text: "text";
500
499
  richtext: "richtext";
501
500
  image: "image";
502
501
  range: "range";
503
502
  select: "select";
504
503
  color: "color";
504
+ product: "product";
505
505
  }>;
506
506
  options: z.ZodOptional<z.ZodArray<z.ZodObject<{
507
507
  label: z.ZodString;
@@ -27,6 +27,7 @@ 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 buildDefaultBuilderSettingsFromManifest(manifest: ThemeManifest, revision?: number): BuilderSettings;
30
31
  export declare function mapLegacyNestedThemeTypographyToStyleSlots(theme: Record<string, unknown>, baseSlots?: StyleSlots): StyleSlots;
31
32
  export declare function mapLegacyTokenOverridesToStyleSlots(tokens: Record<string, unknown>): StyleSlots;
32
33
  //# 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,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"}
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,uCAAuC,CACrD,QAAQ,EAAE,aAAa,EACvB,QAAQ,SAAI,GACX,eAAe,CAqCjB;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,39 @@ export function applyManifestDefaultLayout(settings, manifest) {
69
69
  }
70
70
  return BuilderSettingsSchema.parse(next);
71
71
  }
72
+ export function buildDefaultBuilderSettingsFromManifest(manifest, revision = 1) {
73
+ const settings = createEmptyBuilderSettings(revision);
74
+ for (const [pageId, page] of Object.entries(manifest.pages)) {
75
+ if (page.defaultBlocks.length === 0) {
76
+ continue;
77
+ }
78
+ settings.theme.layout[pageId] = {
79
+ blocks: page.defaultBlocks.map((defaultBlock, index) => {
80
+ const blockDefinition = manifest.blocks[defaultBlock.type];
81
+ const blockSettings = {
82
+ ...(defaultBlock.settings ?? {}),
83
+ };
84
+ if (blockDefinition) {
85
+ for (const [settingPath, field] of Object.entries(blockDefinition.settings)) {
86
+ if (Object.prototype.hasOwnProperty.call(blockSettings, settingPath)) {
87
+ continue;
88
+ }
89
+ if (field.defaultValue !== undefined) {
90
+ blockSettings[settingPath] = field.defaultValue;
91
+ }
92
+ }
93
+ }
94
+ return createBlockInstance({
95
+ id: createDefaultBlockId(pageId, defaultBlock.type, index),
96
+ type: defaultBlock.type,
97
+ variant: defaultBlock.variant,
98
+ settings: blockSettings,
99
+ });
100
+ }),
101
+ };
102
+ }
103
+ return BuilderSettingsSchema.parse(settings);
104
+ }
72
105
  export function mapLegacyNestedThemeTypographyToStyleSlots(theme, baseSlots = {}) {
73
106
  const slots = { ...baseSlots };
74
107
  const typography = theme.typography;
@@ -2,5 +2,5 @@
2
2
  * Normalizes the storefront route visible inside a theme-preview iframe after the
3
3
  * worker strips the `/s/:sessionId/:draftToken` prefix via history.replaceState.
4
4
  */
5
- export declare function readPreviewStorefrontRoutePath(pathname: string, search?: string, hash?: string): string | null;
5
+ export declare function readPreviewStorefrontRoutePath(pathname: string, search?: string, _hash?: string): string | null;
6
6
  //# sourceMappingURL=preview-route.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"preview-route.d.ts","sourceRoot":"","sources":["../src/preview-route.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,MAAM,EAChB,MAAM,SAAK,EACX,IAAI,SAAK,GACR,MAAM,GAAG,IAAI,CAaf"}
1
+ {"version":3,"file":"preview-route.d.ts","sourceRoot":"","sources":["../src/preview-route.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,MAAM,EAChB,MAAM,SAAK,EACX,KAAK,SAAK,GACT,MAAM,GAAG,IAAI,CAaf"}
@@ -2,7 +2,7 @@
2
2
  * Normalizes the storefront route visible inside a theme-preview iframe after the
3
3
  * worker strips the `/s/:sessionId/:draftToken` prefix via history.replaceState.
4
4
  */
5
- export function readPreviewStorefrontRoutePath(pathname, search = '', hash = '') {
5
+ export function readPreviewStorefrontRoutePath(pathname, search = '', _hash = '') {
6
6
  const path = pathname.trim() || '/';
7
7
  if (path.includes('/__shoppex/')) {
8
8
  return null;
@@ -12,5 +12,5 @@ export function readPreviewStorefrontRoutePath(pathname, search = '', hash = '')
12
12
  return null;
13
13
  }
14
14
  const normalizedPath = path.replace(/\/+$/, '') || '/';
15
- return `${normalizedPath}${search}${hash}`;
15
+ return `${normalizedPath}${search}`;
16
16
  }
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+ export declare const ShopStructuredDataOrganizationSchema: z.ZodObject<{
3
+ legal_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
4
+ }, z.core.$strict>;
5
+ export declare const ShopStructuredDataSettingsSchema: z.ZodObject<{
6
+ enabled: z.ZodBoolean;
7
+ organization: z.ZodOptional<z.ZodObject<{
8
+ legal_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
9
+ }, z.core.$strict>>;
10
+ }, z.core.$strict>;
11
+ export type ShopStructuredDataOrganization = z.infer<typeof ShopStructuredDataOrganizationSchema>;
12
+ export type ShopStructuredDataSettings = z.infer<typeof ShopStructuredDataSettingsSchema>;
13
+ export declare const DEFAULT_SHOP_STRUCTURED_DATA_SETTINGS: ShopStructuredDataSettings;
14
+ export declare function normalizeShopStructuredDataSettings(value: unknown): ShopStructuredDataSettings;
15
+ //# sourceMappingURL=shop-structured-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shop-structured-data.d.ts","sourceRoot":"","sources":["../src/shop-structured-data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,oCAAoC;;kBAEtC,CAAC;AAEZ,eAAO,MAAM,gCAAgC;;;;;kBAGlC,CAAC;AAEZ,MAAM,MAAM,8BAA8B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oCAAoC,CAAC,CAAC;AAClG,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gCAAgC,CAAC,CAAC;AAE1F,eAAO,MAAM,qCAAqC,EAAE,0BAGnD,CAAC;AAMF,wBAAgB,mCAAmC,CAAC,KAAK,EAAE,OAAO,GAAG,0BAA0B,CAoB9F"}
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ export const ShopStructuredDataOrganizationSchema = z.object({
3
+ legal_name: z.string().trim().max(120).nullable().optional(),
4
+ }).strict();
5
+ export const ShopStructuredDataSettingsSchema = z.object({
6
+ enabled: z.boolean(),
7
+ organization: ShopStructuredDataOrganizationSchema.optional(),
8
+ }).strict();
9
+ export const DEFAULT_SHOP_STRUCTURED_DATA_SETTINGS = {
10
+ enabled: true,
11
+ organization: {},
12
+ };
13
+ function isRecord(value) {
14
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
15
+ }
16
+ export function normalizeShopStructuredDataSettings(value) {
17
+ if (!isRecord(value)) {
18
+ return { ...DEFAULT_SHOP_STRUCTURED_DATA_SETTINGS, organization: {} };
19
+ }
20
+ const enabled = typeof value.enabled === 'boolean' ? value.enabled : DEFAULT_SHOP_STRUCTURED_DATA_SETTINGS.enabled;
21
+ const organizationRaw = isRecord(value.organization) ? value.organization : {};
22
+ const legalName = organizationRaw.legal_name;
23
+ const organization = {
24
+ ...(typeof legalName === 'string' && legalName.trim().length > 0
25
+ ? { legal_name: legalName.trim() }
26
+ : legalName === null
27
+ ? { legal_name: null }
28
+ : {}),
29
+ };
30
+ return {
31
+ enabled,
32
+ organization,
33
+ };
34
+ }
@@ -3,9 +3,15 @@
3
3
  * and for custom-embed postMessage trust in storefront-commerce (when registered).
4
4
  */
5
5
  export declare const STOREFRONT_RENDER_YOUTUBE_FRAME_ORIGINS: readonly ["https://www.youtube.com", "https://www.youtube-nocookie.com"];
6
+ /**
7
+ * Streamable embed iframe origins. The embed document is served from
8
+ * `https://streamable.com/e/<shortcode>`; the wildcard covers player/CDN
9
+ * subdomains Streamable may serve the iframe document from.
10
+ */
11
+ export declare const STOREFRONT_RENDER_STREAMABLE_FRAME_ORIGINS: readonly ["https://streamable.com", "https://*.streamable.com"];
6
12
  /** Trusted origins for custom-embed resize postMessage (storefront-commerce). */
7
13
  export declare const STOREFRONT_RENDER_CUSTOM_EMBED_TRUSTED_ORIGINS: readonly ["https://calendly.com", "https://substack.com", "https://shoppex.io"];
8
14
  /** frame-src entries for custom embed blocks (includes Substack wildcard). */
9
15
  export declare const STOREFRONT_RENDER_CUSTOM_EMBED_FRAME_SRC_ORIGINS: readonly ["https://calendly.com", "https://substack.com", "https://shoppex.io", "https://*.substack.com"];
10
- export declare const STOREFRONT_RENDER_ALLOWED_FRAME_ORIGINS: readonly ["https://www.youtube.com", "https://www.youtube-nocookie.com", "https://calendly.com", "https://substack.com", "https://shoppex.io", "https://*.substack.com"];
16
+ export declare const STOREFRONT_RENDER_ALLOWED_FRAME_ORIGINS: readonly ["https://www.youtube.com", "https://www.youtube-nocookie.com", "https://streamable.com", "https://*.streamable.com", "https://calendly.com", "https://substack.com", "https://shoppex.io", "https://*.substack.com"];
11
17
  //# sourceMappingURL=storefront-render-frame-origins.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"storefront-render-frame-origins.d.ts","sourceRoot":"","sources":["../src/storefront-render-frame-origins.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,uCAAuC,0EAG1C,CAAC;AAEX,iFAAiF;AACjF,eAAO,MAAM,8CAA8C,iFAIjD,CAAC;AAEX,8EAA8E;AAC9E,eAAO,MAAM,gDAAgD,2GAGnD,CAAC;AAEX,eAAO,MAAM,uCAAuC,0KAG1C,CAAC"}
1
+ {"version":3,"file":"storefront-render-frame-origins.d.ts","sourceRoot":"","sources":["../src/storefront-render-frame-origins.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,uCAAuC,0EAG1C,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,0CAA0C,iEAG7C,CAAC;AAEX,iFAAiF;AACjF,eAAO,MAAM,8CAA8C,iFAIjD,CAAC;AAEX,8EAA8E;AAC9E,eAAO,MAAM,gDAAgD,2GAGnD,CAAC;AAEX,eAAO,MAAM,uCAAuC,gOAI1C,CAAC"}
@@ -6,6 +6,15 @@ export const STOREFRONT_RENDER_YOUTUBE_FRAME_ORIGINS = [
6
6
  'https://www.youtube.com',
7
7
  'https://www.youtube-nocookie.com',
8
8
  ];
9
+ /**
10
+ * Streamable embed iframe origins. The embed document is served from
11
+ * `https://streamable.com/e/<shortcode>`; the wildcard covers player/CDN
12
+ * subdomains Streamable may serve the iframe document from.
13
+ */
14
+ export const STOREFRONT_RENDER_STREAMABLE_FRAME_ORIGINS = [
15
+ 'https://streamable.com',
16
+ 'https://*.streamable.com',
17
+ ];
9
18
  /** Trusted origins for custom-embed resize postMessage (storefront-commerce). */
10
19
  export const STOREFRONT_RENDER_CUSTOM_EMBED_TRUSTED_ORIGINS = [
11
20
  'https://calendly.com',
@@ -19,5 +28,6 @@ export const STOREFRONT_RENDER_CUSTOM_EMBED_FRAME_SRC_ORIGINS = [
19
28
  ];
20
29
  export const STOREFRONT_RENDER_ALLOWED_FRAME_ORIGINS = [
21
30
  ...STOREFRONT_RENDER_YOUTUBE_FRAME_ORIGINS,
31
+ ...STOREFRONT_RENDER_STREAMABLE_FRAME_ORIGINS,
22
32
  ...STOREFRONT_RENDER_CUSTOM_EMBED_FRAME_SRC_ORIGINS,
23
33
  ];
@@ -0,0 +1,9 @@
1
+ export declare const STOREFRONT_RENDER_STRUCTURED_DATA_HEADER = "X-Shoppex-Storefront-Structured-Data";
2
+ export interface StorefrontRenderStructuredData {
3
+ jsonLd: string | null;
4
+ shopSlug: string;
5
+ }
6
+ export declare function normalizeStorefrontRenderStructuredData(value: unknown): StorefrontRenderStructuredData | null;
7
+ export declare function encodeStorefrontRenderStructuredDataHeader(metadata: StorefrontRenderStructuredData): string;
8
+ export declare function decodeStorefrontRenderStructuredDataHeader(value: string | null): StorefrontRenderStructuredData;
9
+ //# sourceMappingURL=storefront-render-structured-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storefront-render-structured-data.d.ts","sourceRoot":"","sources":["../src/storefront-render-structured-data.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,wCAAwC,yCAAyC,CAAC;AAE/F,MAAM,WAAW,8BAA8B;IAC7C,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAMD,wBAAgB,uCAAuC,CAAC,KAAK,EAAE,OAAO,GAAG,8BAA8B,GAAG,IAAI,CAc7G;AAED,wBAAgB,0CAA0C,CAAC,QAAQ,EAAE,8BAA8B,GAAG,MAAM,CAE3G;AAED,wBAAgB,0CAA0C,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,8BAA8B,CAW/G"}
@@ -0,0 +1,30 @@
1
+ export const STOREFRONT_RENDER_STRUCTURED_DATA_HEADER = 'X-Shoppex-Storefront-Structured-Data';
2
+ function isRecord(value) {
3
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
4
+ }
5
+ export function normalizeStorefrontRenderStructuredData(value) {
6
+ if (!isRecord(value)) {
7
+ return null;
8
+ }
9
+ const { jsonLd, shopSlug } = value;
10
+ if (jsonLd !== null && typeof jsonLd !== 'string') {
11
+ return null;
12
+ }
13
+ if (typeof shopSlug !== 'string' || shopSlug.trim().length === 0) {
14
+ return null;
15
+ }
16
+ return { jsonLd, shopSlug: shopSlug.trim() };
17
+ }
18
+ export function encodeStorefrontRenderStructuredDataHeader(metadata) {
19
+ return encodeURIComponent(JSON.stringify(metadata));
20
+ }
21
+ export function decodeStorefrontRenderStructuredDataHeader(value) {
22
+ if (value === null) {
23
+ throw new Error(`${STOREFRONT_RENDER_STRUCTURED_DATA_HEADER} is required.`);
24
+ }
25
+ const metadata = normalizeStorefrontRenderStructuredData(JSON.parse(decodeURIComponent(value)));
26
+ if (!metadata) {
27
+ throw new Error(`${STOREFRONT_RENDER_STRUCTURED_DATA_HEADER} is invalid.`);
28
+ }
29
+ return metadata;
30
+ }
@@ -253,13 +253,13 @@ export declare const ManifestBlockSchema: z.ZodObject<{
253
253
  number: "number";
254
254
  boolean: "boolean";
255
255
  link: "link";
256
- product: "product";
257
256
  text: "text";
258
257
  richtext: "richtext";
259
258
  image: "image";
260
259
  range: "range";
261
260
  select: "select";
262
261
  color: "color";
262
+ product: "product";
263
263
  }>;
264
264
  options: z.ZodOptional<z.ZodArray<z.ZodObject<{
265
265
  label: z.ZodString;
@@ -587,13 +587,13 @@ export declare const ThemeManifestSchema: z.ZodObject<{
587
587
  number: "number";
588
588
  boolean: "boolean";
589
589
  link: "link";
590
- product: "product";
591
590
  text: "text";
592
591
  richtext: "richtext";
593
592
  image: "image";
594
593
  range: "range";
595
594
  select: "select";
596
595
  color: "color";
596
+ product: "product";
597
597
  }>;
598
598
  options: z.ZodOptional<z.ZodArray<z.ZodObject<{
599
599
  label: z.ZodString;
@@ -3,7 +3,7 @@ export type PublicBuilderThemeScheme = (typeof PUBLIC_BUILDER_THEME_SCHEMES)[num
3
3
  export declare const STARTER_BUILDER_THEME_SCHEMES: readonly ["blank"];
4
4
  export declare const THEME_STORE_DISABLED_SCHEMES: readonly [];
5
5
  export declare const THEME_STORE_INSTALLABLE_SCHEMES: readonly ["default", "starlight", "pulse", "vault", "clean-minimal", "classic", "phantom", "nebula", "apex", "shadow"];
6
- export declare const BUILDER_READY_THEME_SCHEMES: readonly ["blank", "default", "classic", "nebula", "pulse", "phantom", "starlight", "apex", "vault", "clean-minimal"];
6
+ export declare const BUILDER_READY_THEME_SCHEMES: readonly ["blank", "default", "classic", "nebula", "pulse", "phantom", "starlight", "apex", "vault", "clean-minimal", "shadow"];
7
7
  export type ThemeStoreInstallableScheme = (typeof THEME_STORE_INSTALLABLE_SCHEMES)[number];
8
8
  export type StarterBuilderThemeScheme = (typeof STARTER_BUILDER_THEME_SCHEMES)[number];
9
9
  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,wHAW/B,CAAC;AAEX,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,4BAA4B,CAAC,CAAC,MAAM,CAAC,CAAC;AAErF,eAAO,MAAM,6BAA6B,oBAAqB,CAAC;AAEhE,eAAO,MAAM,4BAA4B,aAA4D,CAAC;AAItG,eAAO,MAAM,+BAA+B,wHAWY,CAAC;AAEzD,eAAO,MAAM,2BAA2B,uHAW9B,CAAC;AAEX,MAAM,MAAM,2BAA2B,GAAG,CAAC,OAAO,+BAA+B,CAAC,CAAC,MAAM,CAAC,CAAC;AAC3F,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,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEjE;AAED,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,MAAM,GACZ,KAAK,IAAI,2BAA2B,CAEtC;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,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,4BAA4B,CAAC,CAAC,MAAM,CAAC,CAAC;AAErF,eAAO,MAAM,6BAA6B,oBAAqB,CAAC;AAEhE,eAAO,MAAM,4BAA4B,aAA4D,CAAC;AAItG,eAAO,MAAM,+BAA+B,wHAWY,CAAC;AAEzD,eAAO,MAAM,2BAA2B,iIAY9B,CAAC;AAEX,MAAM,MAAM,2BAA2B,GAAG,CAAC,OAAO,+BAA+B,CAAC,CAAC,MAAM,CAAC,CAAC;AAC3F,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,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEjE;AAED,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,MAAM,GACZ,KAAK,IAAI,2BAA2B,CAEtC;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,yBAAyB,CAE7F;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,uBAAuB,CAEzF"}
@@ -36,6 +36,7 @@ export const BUILDER_READY_THEME_SCHEMES = [
36
36
  'apex',
37
37
  'vault',
38
38
  'clean-minimal',
39
+ 'shadow',
39
40
  ];
40
41
  export function isPublicBuilderThemeScheme(value) {
41
42
  return PUBLIC_BUILDER_THEME_SCHEMES.includes(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shoppexio/builder-contracts",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Shared Builder v2 contracts for Shoppex dashboard, backend, preview runtime, and themes",
5
5
  "type": "module",
6
6
  "repository": {
@@ -55,6 +55,18 @@
55
55
  "import": "./dist/storefront-render-seo.js",
56
56
  "default": "./dist/storefront-render-seo.js"
57
57
  },
58
+ "./shop-structured-data": {
59
+ "bun": "./src/shop-structured-data.ts",
60
+ "types": "./dist/shop-structured-data.d.ts",
61
+ "import": "./dist/shop-structured-data.js",
62
+ "default": "./dist/shop-structured-data.js"
63
+ },
64
+ "./storefront-render-structured-data": {
65
+ "bun": "./src/storefront-render-structured-data.ts",
66
+ "types": "./dist/storefront-render-structured-data.d.ts",
67
+ "import": "./dist/storefront-render-structured-data.js",
68
+ "default": "./dist/storefront-render-structured-data.js"
69
+ },
58
70
  "./events": {
59
71
  "bun": "./src/events.ts",
60
72
  "types": "./dist/events.d.ts",
@@ -17,6 +17,7 @@ import {
17
17
  exportCoercedBuilderSettingsBlob,
18
18
  extractPersistedBuilderSettings,
19
19
  canonicalizeBuilderSettingsForManifestWithReport,
20
+ sanitizeBuilderSettingsForManifestWithReport,
20
21
  mergeBuilderSettingsIntoThemeSettings,
21
22
  migrateLegacyBuilderSettings,
22
23
  listThemeManifestPresets,
@@ -849,6 +850,80 @@ describe('@shoppex/builder-contracts', () => {
849
850
  expect(validateBuilderSettingsAgainstManifest(result.settings, manifest)).toEqual([]);
850
851
  });
851
852
 
853
+ test('sanitizeBuilderSettingsForManifest prunes unknown block settings and non-persistable enrichment keys', () => {
854
+ const settings = BuilderSettingsSchema.parse({
855
+ version: 2,
856
+ revision: 1,
857
+ theme: {
858
+ content: {},
859
+ layout: {
860
+ home: {
861
+ blocks: [
862
+ {
863
+ id: 'home-hero-1',
864
+ type: 'hero',
865
+ visible: true,
866
+ settings: {
867
+ 'hero.title': 'Launch sale',
868
+ 'style.accentColor': '#ff0000',
869
+ titleBefore: 'ignored',
870
+ },
871
+ },
872
+ ],
873
+ },
874
+ },
875
+ style_slots: {},
876
+ pages: [],
877
+ terms: {},
878
+ },
879
+ });
880
+
881
+ const manifest = {
882
+ id: 'pulse',
883
+ name: 'Pulse',
884
+ version: '1.0.0',
885
+ pages: {
886
+ home: {
887
+ label: 'Home',
888
+ allowedBlocks: ['hero'],
889
+ defaultBlocks: [{ type: 'hero' }],
890
+ },
891
+ },
892
+ blocks: {
893
+ hero: {
894
+ label: 'Hero',
895
+ settings: {
896
+ 'hero.title': { type: 'text', label: 'Title' },
897
+ },
898
+ variants: [],
899
+ exposedStyleSlots: [],
900
+ presets: [],
901
+ },
902
+ },
903
+ styleSlots: {},
904
+ presets: {},
905
+ };
906
+
907
+ const result = sanitizeBuilderSettingsForManifestWithReport(settings, manifest);
908
+
909
+ expect(result.pruned).toEqual(
910
+ expect.arrayContaining([
911
+ expect.objectContaining({
912
+ key: 'style.accentColor',
913
+ reason: 'unknown_block_setting',
914
+ }),
915
+ expect.objectContaining({
916
+ key: 'titleBefore',
917
+ reason: 'non_persistable_block_setting',
918
+ }),
919
+ ]),
920
+ );
921
+ expect(result.settings.theme.layout.home.blocks[0].settings).toEqual({
922
+ 'hero.title': 'Launch sale',
923
+ });
924
+ expect(validateBuilderSettingsAgainstManifest(result.settings, manifest)).toEqual([]);
925
+ });
926
+
852
927
  test('accepts transparent color literals used by theme manifests', () => {
853
928
  expect(ColorSchema.parse('transparent')).toBe('transparent');
854
929
  expect(ColorSchema.parse('#ffffff')).toBe('#ffffff');
@@ -1,7 +1,17 @@
1
1
  import type { BlockInstance, BuilderSettings } from './builder-settings.ts';
2
+ import { mergeCustomPagesIntoManifest } from './custom-pages.ts';
2
3
  import { sanitizeBuilderSettingsState } from './persistence.ts';
4
+ import { ThemeStyleSlotIdSchema } from './style-slots.ts';
3
5
  import type { ThemeManifest } from './theme-manifest.ts';
4
6
 
7
+ /** Render-time enrichment keys that must never be persisted in builder settings. */
8
+ export const NON_PERSISTABLE_BLOCK_SETTING_KEYS = new Set([
9
+ 'titleHasHighlight',
10
+ 'titleBefore',
11
+ 'titleHighlight',
12
+ 'titleAfter',
13
+ ]);
14
+
5
15
  export type BuilderSettingsCanonicalizationConflict = {
6
16
  pageId: string;
7
17
  blockId: string;
@@ -17,6 +27,20 @@ export type BuilderSettingsCanonicalizationResult = {
17
27
  conflicts: BuilderSettingsCanonicalizationConflict[];
18
28
  };
19
29
 
30
+ export type BuilderSettingsSanitizationPrunedEntry = {
31
+ path: string;
32
+ key: string;
33
+ reason:
34
+ | 'non_persistable_block_setting'
35
+ | 'unknown_block_setting'
36
+ | 'unexposed_style_slot'
37
+ | 'unknown_style_slot';
38
+ };
39
+
40
+ export type BuilderSettingsSanitizationResult = BuilderSettingsCanonicalizationResult & {
41
+ pruned: BuilderSettingsSanitizationPrunedEntry[];
42
+ };
43
+
20
44
  export function canonicalizeBuilderSettingsForManifest(
21
45
  settings: BuilderSettings,
22
46
  manifest: ThemeManifest,
@@ -53,6 +77,142 @@ export function canonicalizeBuilderSettingsForManifestWithReport(
53
77
  };
54
78
  }
55
79
 
80
+ export function sanitizeBuilderSettingsForManifest(
81
+ settings: BuilderSettings,
82
+ manifest: ThemeManifest,
83
+ ): BuilderSettings {
84
+ return sanitizeBuilderSettingsForManifestWithReport(settings, manifest).settings;
85
+ }
86
+
87
+ export function sanitizeBuilderSettingsForManifestWithReport(
88
+ settings: BuilderSettings,
89
+ manifest: ThemeManifest,
90
+ ): BuilderSettingsSanitizationResult {
91
+ const canonical = canonicalizeBuilderSettingsForManifestWithReport(settings, manifest);
92
+ const pruned: BuilderSettingsSanitizationPrunedEntry[] = [];
93
+
94
+ stripNonPersistableBlockSettings(canonical.settings, pruned);
95
+
96
+ const effectiveManifest = mergeCustomPagesIntoManifest(manifest, canonical.settings);
97
+ pruneUnknownGlobalStyleSlots(canonical.settings, effectiveManifest, pruned);
98
+
99
+ for (const [pageId, layout] of Object.entries(canonical.settings.theme.layout)) {
100
+ for (const [index, block] of layout.blocks.entries()) {
101
+ const blockPath = `theme.layout.${pageId}.blocks.${index}`;
102
+ const blockDefinition = effectiveManifest.blocks[block.type];
103
+ if (!blockDefinition) {
104
+ continue;
105
+ }
106
+
107
+ pruneBlockSettings(block, Object.keys(blockDefinition.settings), blockPath, pruned);
108
+ pruneBlockStyleOverrides(block, blockDefinition.exposedStyleSlots, blockPath, pruned);
109
+ }
110
+ }
111
+
112
+ return {
113
+ ...canonical,
114
+ pruned,
115
+ };
116
+ }
117
+
118
+ function stripNonPersistableBlockSettings(
119
+ settings: BuilderSettings,
120
+ pruned: BuilderSettingsSanitizationPrunedEntry[],
121
+ ): void {
122
+ for (const [pageId, layout] of Object.entries(settings.theme.layout)) {
123
+ for (const [index, block] of layout.blocks.entries()) {
124
+ const blockPath = `theme.layout.${pageId}.blocks.${index}`;
125
+
126
+ for (const key of Object.keys(block.settings)) {
127
+ if (!NON_PERSISTABLE_BLOCK_SETTING_KEYS.has(key)) {
128
+ continue;
129
+ }
130
+
131
+ delete block.settings[key];
132
+ pruned.push({
133
+ path: `${blockPath}.settings.${key}`,
134
+ key,
135
+ reason: 'non_persistable_block_setting',
136
+ });
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ function pruneUnknownGlobalStyleSlots(
143
+ settings: BuilderSettings,
144
+ manifest: ThemeManifest,
145
+ pruned: BuilderSettingsSanitizationPrunedEntry[],
146
+ ): void {
147
+ const declaredThemeSlots = new Set(Object.keys(manifest.styleSlots));
148
+
149
+ for (const slotId of Object.keys(settings.theme.style_slots)) {
150
+ if (!ThemeStyleSlotIdSchema.safeParse(slotId).success) {
151
+ continue;
152
+ }
153
+
154
+ if (declaredThemeSlots.has(slotId)) {
155
+ continue;
156
+ }
157
+
158
+ const styleSlots = settings.theme.style_slots as Record<string, unknown>;
159
+ delete styleSlots[slotId];
160
+ pruned.push({
161
+ path: `theme.style_slots.${slotId}`,
162
+ key: slotId,
163
+ reason: 'unknown_style_slot',
164
+ });
165
+ }
166
+ }
167
+
168
+ function pruneBlockSettings(
169
+ block: BlockInstance,
170
+ settingPaths: string[],
171
+ blockPath: string,
172
+ pruned: BuilderSettingsSanitizationPrunedEntry[],
173
+ ): void {
174
+ const allowedSettings = new Set(settingPaths);
175
+
176
+ for (const key of Object.keys(block.settings)) {
177
+ if (allowedSettings.has(key)) {
178
+ continue;
179
+ }
180
+
181
+ delete block.settings[key];
182
+ pruned.push({
183
+ path: `${blockPath}.settings.${key}`,
184
+ key,
185
+ reason: 'unknown_block_setting',
186
+ });
187
+ }
188
+ }
189
+
190
+ function pruneBlockStyleOverrides(
191
+ block: BlockInstance,
192
+ exposedStyleSlots: string[],
193
+ blockPath: string,
194
+ pruned: BuilderSettingsSanitizationPrunedEntry[],
195
+ ): void {
196
+ if (!block.style_overrides) {
197
+ return;
198
+ }
199
+
200
+ const exposed = new Set(exposedStyleSlots);
201
+ const overrides = block.style_overrides as Record<string, unknown>;
202
+ for (const slotId of Object.keys(overrides)) {
203
+ if (exposed.has(slotId)) {
204
+ continue;
205
+ }
206
+
207
+ delete overrides[slotId];
208
+ pruned.push({
209
+ path: `${blockPath}.style_overrides.${slotId}`,
210
+ key: slotId,
211
+ reason: 'unexposed_style_slot',
212
+ });
213
+ }
214
+ }
215
+
56
216
  function canonicalizeBlockSettings(
57
217
  pageId: string,
58
218
  block: BlockInstance,
package/src/migrations.ts CHANGED
@@ -110,6 +110,48 @@ export function applyManifestDefaultLayout(settings: BuilderSettings, manifest:
110
110
  return BuilderSettingsSchema.parse(next);
111
111
  }
112
112
 
113
+ export function buildDefaultBuilderSettingsFromManifest(
114
+ manifest: ThemeManifest,
115
+ revision = 1,
116
+ ): BuilderSettings {
117
+ const settings = createEmptyBuilderSettings(revision);
118
+
119
+ for (const [pageId, page] of Object.entries(manifest.pages)) {
120
+ if (page.defaultBlocks.length === 0) {
121
+ continue;
122
+ }
123
+
124
+ settings.theme.layout[pageId] = {
125
+ blocks: page.defaultBlocks.map((defaultBlock, index) => {
126
+ const blockDefinition = manifest.blocks[defaultBlock.type];
127
+ const blockSettings: Record<string, unknown> = {
128
+ ...(defaultBlock.settings ?? {}),
129
+ };
130
+
131
+ if (blockDefinition) {
132
+ for (const [settingPath, field] of Object.entries(blockDefinition.settings)) {
133
+ if (Object.prototype.hasOwnProperty.call(blockSettings, settingPath)) {
134
+ continue;
135
+ }
136
+ if (field.defaultValue !== undefined) {
137
+ blockSettings[settingPath] = field.defaultValue;
138
+ }
139
+ }
140
+ }
141
+
142
+ return createBlockInstance({
143
+ id: createDefaultBlockId(pageId, defaultBlock.type, index),
144
+ type: defaultBlock.type,
145
+ variant: defaultBlock.variant,
146
+ settings: blockSettings,
147
+ });
148
+ }),
149
+ };
150
+ }
151
+
152
+ return BuilderSettingsSchema.parse(settings);
153
+ }
154
+
113
155
  export function mapLegacyNestedThemeTypographyToStyleSlots(
114
156
  theme: Record<string, unknown>,
115
157
  baseSlots: StyleSlots = {},
@@ -7,6 +7,11 @@ describe('readPreviewStorefrontRoutePath', () => {
7
7
  .toBe('/products?category=fivem&sort=price-asc');
8
8
  });
9
9
 
10
+ test('omits hash fragments from storefront routes', () => {
11
+ expect(readPreviewStorefrontRoutePath('/products', '?category=fivem', '#faq'))
12
+ .toBe('/products?category=fivem');
13
+ });
14
+
10
15
  test('normalizes trailing slashes on non-root paths', () => {
11
16
  expect(readPreviewStorefrontRoutePath('/products/', '?page=2')).toBe('/products?page=2');
12
17
  });
@@ -5,7 +5,7 @@
5
5
  export function readPreviewStorefrontRoutePath(
6
6
  pathname: string,
7
7
  search = '',
8
- hash = '',
8
+ _hash = '',
9
9
  ): string | null {
10
10
  const path = pathname.trim() || '/';
11
11
  if (path.includes('/__shoppex/')) {
@@ -18,5 +18,5 @@ export function readPreviewStorefrontRoutePath(
18
18
  }
19
19
 
20
20
  const normalizedPath = path.replace(/\/+$/, '') || '/';
21
- return `${normalizedPath}${search}${hash}`;
21
+ return `${normalizedPath}${search}`;
22
22
  }
@@ -0,0 +1,44 @@
1
+ import { z } from 'zod';
2
+
3
+ export const ShopStructuredDataOrganizationSchema = z.object({
4
+ legal_name: z.string().trim().max(120).nullable().optional(),
5
+ }).strict();
6
+
7
+ export const ShopStructuredDataSettingsSchema = z.object({
8
+ enabled: z.boolean(),
9
+ organization: ShopStructuredDataOrganizationSchema.optional(),
10
+ }).strict();
11
+
12
+ export type ShopStructuredDataOrganization = z.infer<typeof ShopStructuredDataOrganizationSchema>;
13
+ export type ShopStructuredDataSettings = z.infer<typeof ShopStructuredDataSettingsSchema>;
14
+
15
+ export const DEFAULT_SHOP_STRUCTURED_DATA_SETTINGS: ShopStructuredDataSettings = {
16
+ enabled: true,
17
+ organization: {},
18
+ };
19
+
20
+ function isRecord(value: unknown): value is Record<string, unknown> {
21
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
22
+ }
23
+
24
+ export function normalizeShopStructuredDataSettings(value: unknown): ShopStructuredDataSettings {
25
+ if (!isRecord(value)) {
26
+ return { ...DEFAULT_SHOP_STRUCTURED_DATA_SETTINGS, organization: {} };
27
+ }
28
+
29
+ const enabled = typeof value.enabled === 'boolean' ? value.enabled : DEFAULT_SHOP_STRUCTURED_DATA_SETTINGS.enabled;
30
+ const organizationRaw = isRecord(value.organization) ? value.organization : {};
31
+ const legalName = organizationRaw.legal_name;
32
+ const organization = {
33
+ ...(typeof legalName === 'string' && legalName.trim().length > 0
34
+ ? { legal_name: legalName.trim() }
35
+ : legalName === null
36
+ ? { legal_name: null }
37
+ : {}),
38
+ };
39
+
40
+ return {
41
+ enabled,
42
+ organization,
43
+ };
44
+ }
@@ -7,6 +7,16 @@ export const STOREFRONT_RENDER_YOUTUBE_FRAME_ORIGINS = [
7
7
  'https://www.youtube-nocookie.com',
8
8
  ] as const;
9
9
 
10
+ /**
11
+ * Streamable embed iframe origins. The embed document is served from
12
+ * `https://streamable.com/e/<shortcode>`; the wildcard covers player/CDN
13
+ * subdomains Streamable may serve the iframe document from.
14
+ */
15
+ export const STOREFRONT_RENDER_STREAMABLE_FRAME_ORIGINS = [
16
+ 'https://streamable.com',
17
+ 'https://*.streamable.com',
18
+ ] as const;
19
+
10
20
  /** Trusted origins for custom-embed resize postMessage (storefront-commerce). */
11
21
  export const STOREFRONT_RENDER_CUSTOM_EMBED_TRUSTED_ORIGINS = [
12
22
  'https://calendly.com',
@@ -22,5 +32,6 @@ export const STOREFRONT_RENDER_CUSTOM_EMBED_FRAME_SRC_ORIGINS = [
22
32
 
23
33
  export const STOREFRONT_RENDER_ALLOWED_FRAME_ORIGINS = [
24
34
  ...STOREFRONT_RENDER_YOUTUBE_FRAME_ORIGINS,
35
+ ...STOREFRONT_RENDER_STREAMABLE_FRAME_ORIGINS,
25
36
  ...STOREFRONT_RENDER_CUSTOM_EMBED_FRAME_SRC_ORIGINS,
26
37
  ] as const;
@@ -0,0 +1,43 @@
1
+ export const STOREFRONT_RENDER_STRUCTURED_DATA_HEADER = 'X-Shoppex-Storefront-Structured-Data';
2
+
3
+ export interface StorefrontRenderStructuredData {
4
+ jsonLd: string | null;
5
+ shopSlug: string;
6
+ }
7
+
8
+ function isRecord(value: unknown): value is Record<string, unknown> {
9
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
10
+ }
11
+
12
+ export function normalizeStorefrontRenderStructuredData(value: unknown): StorefrontRenderStructuredData | null {
13
+ if (!isRecord(value)) {
14
+ return null;
15
+ }
16
+
17
+ const { jsonLd, shopSlug } = value;
18
+ if (jsonLd !== null && typeof jsonLd !== 'string') {
19
+ return null;
20
+ }
21
+ if (typeof shopSlug !== 'string' || shopSlug.trim().length === 0) {
22
+ return null;
23
+ }
24
+
25
+ return { jsonLd, shopSlug: shopSlug.trim() };
26
+ }
27
+
28
+ export function encodeStorefrontRenderStructuredDataHeader(metadata: StorefrontRenderStructuredData): string {
29
+ return encodeURIComponent(JSON.stringify(metadata));
30
+ }
31
+
32
+ export function decodeStorefrontRenderStructuredDataHeader(value: string | null): StorefrontRenderStructuredData {
33
+ if (value === null) {
34
+ throw new Error(`${STOREFRONT_RENDER_STRUCTURED_DATA_HEADER} is required.`);
35
+ }
36
+
37
+ const metadata = normalizeStorefrontRenderStructuredData(JSON.parse(decodeURIComponent(value)) as unknown);
38
+ if (!metadata) {
39
+ throw new Error(`${STOREFRONT_RENDER_STRUCTURED_DATA_HEADER} is invalid.`);
40
+ }
41
+
42
+ return metadata;
43
+ }
@@ -43,6 +43,7 @@ export const BUILDER_READY_THEME_SCHEMES = [
43
43
  'apex',
44
44
  'vault',
45
45
  'clean-minimal',
46
+ 'shadow',
46
47
  ] as const;
47
48
 
48
49
  export type ThemeStoreInstallableScheme = (typeof THEME_STORE_INSTALLABLE_SCHEMES)[number];