@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.
- package/dist/canonical-settings.d.ts +12 -0
- package/dist/canonical-settings.d.ts.map +1 -1
- package/dist/canonical-settings.js +102 -0
- package/dist/fields.d.ts +4 -4
- package/dist/migrations.d.ts +1 -0
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +33 -0
- package/dist/preview-route.d.ts +1 -1
- package/dist/preview-route.d.ts.map +1 -1
- package/dist/preview-route.js +2 -2
- package/dist/shop-structured-data.d.ts +15 -0
- package/dist/shop-structured-data.d.ts.map +1 -0
- package/dist/shop-structured-data.js +34 -0
- package/dist/storefront-render-frame-origins.d.ts +7 -1
- package/dist/storefront-render-frame-origins.d.ts.map +1 -1
- package/dist/storefront-render-frame-origins.js +10 -0
- package/dist/storefront-render-structured-data.d.ts +9 -0
- package/dist/storefront-render-structured-data.d.ts.map +1 -0
- package/dist/storefront-render-structured-data.js +30 -0
- package/dist/theme-manifest.d.ts +2 -2
- package/dist/theme-schemes.d.ts +1 -1
- package/dist/theme-schemes.d.ts.map +1 -1
- package/dist/theme-schemes.js +1 -0
- package/package.json +13 -1
- package/src/builder-contracts.test.ts +75 -0
- package/src/canonical-settings.ts +160 -0
- package/src/migrations.ts +42 -0
- package/src/preview-route.test.ts +5 -0
- package/src/preview-route.ts +2 -2
- package/src/shop-structured-data.ts +44 -0
- package/src/storefront-render-frame-origins.ts +11 -0
- package/src/storefront-render-structured-data.ts +43 -0
- package/src/theme-schemes.ts +1 -0
|
@@ -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;
|
|
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;
|
package/dist/migrations.d.ts
CHANGED
|
@@ -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
|
package/dist/migrations.d.ts.map
CHANGED
|
@@ -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"}
|
package/dist/migrations.js
CHANGED
|
@@ -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;
|
package/dist/preview-route.d.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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"}
|
package/dist/preview-route.js
CHANGED
|
@@ -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 = '',
|
|
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}
|
|
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,
|
|
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
|
+
}
|
package/dist/theme-manifest.d.ts
CHANGED
|
@@ -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;
|
package/dist/theme-schemes.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/theme-schemes.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shoppexio/builder-contracts",
|
|
3
|
-
"version": "0.1.
|
|
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
|
});
|
package/src/preview-route.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
export function readPreviewStorefrontRoutePath(
|
|
6
6
|
pathname: string,
|
|
7
7
|
search = '',
|
|
8
|
-
|
|
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}
|
|
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
|
+
}
|