@shoppexio/builder-contracts 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder-settings.d.ts +2 -0
- package/dist/builder-settings.d.ts.map +1 -1
- package/dist/builder-settings.js +2 -1
- package/dist/custom-pages.d.ts +15 -0
- package/dist/custom-pages.d.ts.map +1 -0
- package/dist/custom-pages.js +40 -0
- package/dist/dedicated-pages.d.ts +15 -0
- package/dist/dedicated-pages.d.ts.map +1 -0
- package/dist/dedicated-pages.js +142 -0
- package/dist/fields.d.ts +60 -2
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +4 -4
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/legacy-manifest.d.ts +7 -0
- package/dist/legacy-manifest.d.ts.map +1 -1
- package/dist/legacy-manifest.js +31 -6
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +5 -2
- package/dist/preview-boot.d.ts +2 -2
- package/dist/preview-boot.d.ts.map +1 -1
- package/dist/preview-boot.js +3 -1
- package/dist/preview-session-resolve.d.ts +2 -2
- package/dist/preview-session-resolve.d.ts.map +1 -1
- package/dist/preview-session-resolve.js +2 -2
- package/dist/preview-trusted-origins.d.ts.map +1 -1
- package/dist/preview-trusted-origins.js +10 -8
- package/dist/storefront-typography-fonts.d.ts +18 -0
- package/dist/storefront-typography-fonts.d.ts.map +1 -0
- package/dist/storefront-typography-fonts.js +89 -0
- package/dist/style-slots.d.ts +2 -2
- package/dist/style-slots.d.ts.map +1 -1
- package/dist/style-slots.js +5 -3
- package/dist/theme-manifest.d.ts +58 -2
- package/dist/theme-manifest.d.ts.map +1 -1
- package/dist/theme-schemes.d.ts +2 -2
- package/dist/theme-schemes.d.ts.map +1 -1
- package/dist/theme-schemes.js +1 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +6 -4
- package/package.json +1 -1
- package/src/builder-contracts.test.ts +18 -0
- package/src/builder-settings.ts +4 -1
- package/src/custom-pages.test.ts +74 -0
- package/src/custom-pages.ts +70 -0
- package/src/dedicated-pages.test.ts +88 -0
- package/src/dedicated-pages.ts +173 -0
- package/src/fields.ts +4 -4
- package/src/index.ts +3 -0
- package/src/legacy-manifest.ts +40 -7
- package/src/migrations.ts +5 -2
- package/src/preview-boot.test.ts +72 -0
- package/src/preview-boot.ts +3 -1
- package/src/preview-session-resolve.test.ts +37 -0
- package/src/preview-session-resolve.ts +2 -2
- package/src/storefront-initial-data-html.test.ts +10 -0
- package/src/storefront-typography-fonts.test.ts +48 -0
- package/src/storefront-typography-fonts.ts +108 -0
- package/src/style-slots.ts +6 -3
- package/src/theme-schemes.ts +1 -0
- package/src/validation.ts +6 -4
- package/dist/builder-contracts.test.d.ts +0 -2
- package/dist/builder-contracts.test.d.ts.map +0 -1
- package/dist/builder-contracts.test.js +0 -431
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme-manifest.d.ts","sourceRoot":"","sources":["../src/theme-manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAE5B,OAAO,EAA8C,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAEtG,eAAO,MAAM,aAAa,aAAoD,CAAC;AAC/E,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD,eAAO,MAAM,kBAAkB;;;;kBAMpB,CAAC;AACZ,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,kBAAkB;;;;kBAMpB,CAAC;AACZ,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,kBAAkB;;;;;;;;;kBAOpB,CAAC;AACZ,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,iBAAiB;;;;;;kBAQnB,CAAC;AACZ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,mBAAmB
|
|
1
|
+
{"version":3,"file":"theme-manifest.d.ts","sourceRoot":"","sources":["../src/theme-manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAE5B,OAAO,EAA8C,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAEtG,eAAO,MAAM,aAAa,aAAoD,CAAC;AAC/E,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD,eAAO,MAAM,kBAAkB;;;;kBAMpB,CAAC;AACZ,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,kBAAkB;;;;kBAMpB,CAAC;AACZ,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,kBAAkB;;;;;;;;;kBAOpB,CAAC;AACZ,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,iBAAiB;;;;;;kBAQnB,CAAC;AACZ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAWrB,CAAC;AACZ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,iBAAiB;;;;;;;kBASnB,CAAC;AACZ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,2BAA2B;;;;;;;;;;;;kBAc7B,CAAC;AACZ,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEhF,eAAO,MAAM,4BAA4B;;;;;kBAO9B,CAAC;AACZ,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,eAAO,MAAM,2BAA2B;;;kBAK7B,CAAC;AACZ,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEhF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA4E5B,CAAC;AACL,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,aAAa,CAEhE;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE;QACT,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAClC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjC,WAAW,CAAC,EAAE,iBAAiB,CAAC;KACjC,CAAC;CACH,CAAC;AAEF,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,2BAA2B,EAAE,CAwBtF"}
|
package/dist/theme-schemes.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export declare const PUBLIC_BUILDER_THEME_SCHEMES: readonly ["default", "classic", "nebula", "pulse", "phantom", "starlight", "apex", "vault"];
|
|
1
|
+
export declare const PUBLIC_BUILDER_THEME_SCHEMES: readonly ["default", "classic", "nebula", "pulse", "phantom", "starlight", "apex", "vault", "clean-minimal"];
|
|
2
2
|
export declare const STARTER_BUILDER_THEME_SCHEMES: readonly ["blank"];
|
|
3
|
-
export declare const BUILDER_READY_THEME_SCHEMES: readonly ["blank", "default", "classic", "nebula", "pulse", "phantom", "starlight", "apex", "vault"];
|
|
3
|
+
export declare const BUILDER_READY_THEME_SCHEMES: readonly ["blank", "default", "classic", "nebula", "pulse", "phantom", "starlight", "apex", "vault", "clean-minimal"];
|
|
4
4
|
export type PublicBuilderThemeScheme = (typeof PUBLIC_BUILDER_THEME_SCHEMES)[number];
|
|
5
5
|
export type StarterBuilderThemeScheme = (typeof STARTER_BUILDER_THEME_SCHEMES)[number];
|
|
6
6
|
export type BuilderReadyThemeScheme = (typeof BUILDER_READY_THEME_SCHEMES)[number];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme-schemes.d.ts","sourceRoot":"","sources":["../src/theme-schemes.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,4BAA4B,
|
|
1
|
+
{"version":3,"file":"theme-schemes.d.ts","sourceRoot":"","sources":["../src/theme-schemes.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,4BAA4B,8GAU/B,CAAC;AAEX,eAAO,MAAM,6BAA6B,oBAAqB,CAAC;AAEhE,eAAO,MAAM,2BAA2B,uHAG9B,CAAC;AAEX,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,4BAA4B,CAAC,CAAC,MAAM,CAAC,CAAC;AACrF,MAAM,MAAM,yBAAyB,GAAG,CAAC,OAAO,6BAA6B,CAAC,CAAC,MAAM,CAAC,CAAC;AACvF,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,wBAAwB,CAE3F;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,yBAAyB,CAE7F;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,uBAAuB,CAEzF"}
|
package/dist/theme-schemes.js
CHANGED
package/dist/validation.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,eAAe,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAG5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,MAAM,kCAAkC,GAC1C,cAAc,GACd,oBAAoB,GACpB,oBAAoB,GACpB,mBAAmB,GACnB,0BAA0B,GAC1B,uBAAuB,GACvB,uBAAuB,GACvB,oBAAoB,GACpB,sBAAsB,CAAC;AAE3B,MAAM,MAAM,8BAA8B,GAAG;IAC3C,IAAI,EAAE,kCAAkC,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,qBAAa,8BAA+B,SAAQ,KAAK;IACvD,QAAQ,CAAC,MAAM,EAAE,8BAA8B,EAAE,CAAC;gBAEtC,MAAM,EAAE,8BAA8B,EAAE;CAKrD;AAED,wBAAgB,sCAAsC,CACpD,QAAQ,EAAE,eAAe,EACzB,QAAQ,EAAE,aAAa,GACtB,8BAA8B,EAAE,CAqElC;AAwBD,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,eAAe,EACzB,QAAQ,EAAE,aAAa,GACtB,IAAI,CAKN;AAED,wBAAgB,qCAAqC,CACnD,MAAM,EAAE,8BAA8B,EAAE,GACvC,MAAM,CAQR"}
|
package/dist/validation.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mergeCustomPagesIntoManifest } from "./custom-pages.js";
|
|
1
2
|
import { ThemeStyleSlotIdSchema } from "./style-slots.js";
|
|
2
3
|
export class BuilderManifestValidationError extends Error {
|
|
3
4
|
issues;
|
|
@@ -8,10 +9,11 @@ export class BuilderManifestValidationError extends Error {
|
|
|
8
9
|
}
|
|
9
10
|
}
|
|
10
11
|
export function validateBuilderSettingsAgainstManifest(settings, manifest) {
|
|
12
|
+
const effectiveManifest = mergeCustomPagesIntoManifest(manifest, settings);
|
|
11
13
|
const issues = [];
|
|
12
|
-
validateGlobalStyleSlots(settings,
|
|
14
|
+
validateGlobalStyleSlots(settings, effectiveManifest, issues);
|
|
13
15
|
for (const [pageId, layout] of Object.entries(settings.theme.layout)) {
|
|
14
|
-
const page =
|
|
16
|
+
const page = effectiveManifest.pages[pageId];
|
|
15
17
|
if (!page) {
|
|
16
18
|
issues.push({
|
|
17
19
|
code: 'unknown_page',
|
|
@@ -32,7 +34,7 @@ export function validateBuilderSettingsAgainstManifest(settings, manifest) {
|
|
|
32
34
|
});
|
|
33
35
|
}
|
|
34
36
|
blockIds.add(block.id);
|
|
35
|
-
const blockDefinition =
|
|
37
|
+
const blockDefinition = effectiveManifest.blocks[block.type];
|
|
36
38
|
if (!blockDefinition) {
|
|
37
39
|
issues.push({
|
|
38
40
|
code: 'unknown_block_type',
|
|
@@ -54,7 +56,7 @@ export function validateBuilderSettingsAgainstManifest(settings, manifest) {
|
|
|
54
56
|
validateBlockStyleOverrides(block, blockDefinition.exposedStyleSlots, blockPath, issues);
|
|
55
57
|
}
|
|
56
58
|
for (const [blockType, count] of blockTypeCounts.entries()) {
|
|
57
|
-
const maxInstances =
|
|
59
|
+
const maxInstances = effectiveManifest.blocks[blockType]?.maxInstances;
|
|
58
60
|
if (maxInstances !== undefined && count > maxInstances) {
|
|
59
61
|
issues.push({
|
|
60
62
|
code: 'too_many_block_instances',
|
package/package.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
import {
|
|
3
5
|
BuilderEventSchema,
|
|
4
6
|
BuilderSettingsSchema,
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
migrateLegacyBuilderSettings,
|
|
18
20
|
listThemeManifestPresets,
|
|
19
21
|
} from './index.ts';
|
|
22
|
+
import { ColorSchema } from './style-slots.ts';
|
|
20
23
|
|
|
21
24
|
describe('@shoppex/builder-contracts', () => {
|
|
22
25
|
test('accepts an empty builder settings document', () => {
|
|
@@ -797,4 +800,19 @@ describe('@shoppex/builder-contracts', () => {
|
|
|
797
800
|
});
|
|
798
801
|
expect(validateBuilderSettingsAgainstManifest(result.settings, manifest)).toEqual([]);
|
|
799
802
|
});
|
|
803
|
+
|
|
804
|
+
test('accepts transparent color literals used by theme manifests', () => {
|
|
805
|
+
expect(ColorSchema.parse('transparent')).toBe('transparent');
|
|
806
|
+
expect(ColorSchema.parse('#ffffff')).toBe('#ffffff');
|
|
807
|
+
expect(ColorSchema.parse('#00000080')).toBe('#00000080');
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
test('parses official theme manifests with transparent contact-form backgrounds', () => {
|
|
811
|
+
const repoRoot = join(import.meta.dir, '../../..');
|
|
812
|
+
for (const theme of ['default', 'nebula', 'classic', 'pulse', 'starlight', 'vault', 'phantom', 'apex']) {
|
|
813
|
+
const manifestPath = join(repoRoot, 'themes', theme, 'theme.manifest.json');
|
|
814
|
+
const raw = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
815
|
+
expect(ThemeManifestSchema.safeParse(raw).success, `${theme} manifest should validate`).toBe(true);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
800
818
|
});
|
package/src/builder-settings.ts
CHANGED
|
@@ -31,11 +31,14 @@ export const PageLayoutSchema = z
|
|
|
31
31
|
.strict();
|
|
32
32
|
export type PageLayout = z.infer<typeof PageLayoutSchema>;
|
|
33
33
|
|
|
34
|
+
export const CustomPageSlugSchema = z.string().min(1).regex(/^[a-z0-9][a-z0-9-_]*$/);
|
|
35
|
+
export type CustomPageSlug = z.infer<typeof CustomPageSlugSchema>;
|
|
36
|
+
|
|
34
37
|
export const CustomPageSchema = z
|
|
35
38
|
.object({
|
|
36
39
|
id: PageIdSchema,
|
|
37
40
|
title: z.string().min(1),
|
|
38
|
-
slug:
|
|
41
|
+
slug: CustomPageSlugSchema,
|
|
39
42
|
visible: z.boolean().default(true),
|
|
40
43
|
layout: PageLayoutSchema.default({ blocks: [] }),
|
|
41
44
|
seo: z
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
MERCHANT_CUSTOM_PAGE_ALLOWED_BLOCKS,
|
|
6
|
+
mergeCustomPagesIntoManifest,
|
|
7
|
+
} from './custom-pages.ts';
|
|
8
|
+
import { CustomPageSchema } from './builder-settings.ts';
|
|
9
|
+
import { createEmptyBuilderSettings } from './migrations.ts';
|
|
10
|
+
import { convertLegacyThemeManifest } from './legacy-manifest.ts';
|
|
11
|
+
import { validateBuilderSettingsAgainstManifest } from './validation.ts';
|
|
12
|
+
|
|
13
|
+
function loadDefaultManifest() {
|
|
14
|
+
const manifestPath = join(import.meta.dir, '../../../themes/default/theme.manifest.json');
|
|
15
|
+
return convertLegacyThemeManifest(JSON.parse(readFileSync(manifestPath, 'utf8')) as unknown);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('custom pages manifest merge', () => {
|
|
19
|
+
test('keeps MERCHANT_CUSTOM_PAGE_ALLOWED_BLOCKS aligned with config SSOT', () => {
|
|
20
|
+
const configPath = join(import.meta.dir, '../../../config/builder-shared-manifest-blocks.json');
|
|
21
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8')) as {
|
|
22
|
+
customPageAllowedBlocks: string[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
expect([...MERCHANT_CUSTOM_PAGE_ALLOWED_BLOCKS]).toEqual(config.customPageAllowedBlocks);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('merges merchant custom pages into the effective manifest', () => {
|
|
29
|
+
const manifest = loadDefaultManifest();
|
|
30
|
+
const settings = createEmptyBuilderSettings(1);
|
|
31
|
+
settings.theme.pages = [
|
|
32
|
+
{
|
|
33
|
+
id: 'custom-about',
|
|
34
|
+
title: 'About Us',
|
|
35
|
+
slug: 'about-us',
|
|
36
|
+
visible: true,
|
|
37
|
+
layout: { blocks: [] },
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
settings.theme.layout['custom-about'] = {
|
|
41
|
+
blocks: [
|
|
42
|
+
{
|
|
43
|
+
id: 'youtube-1',
|
|
44
|
+
type: 'youtube-embed',
|
|
45
|
+
visible: true,
|
|
46
|
+
settings: {
|
|
47
|
+
videoUrl: 'https://youtu.be/dQw4w9WgXcQ',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const effective = mergeCustomPagesIntoManifest(manifest, settings);
|
|
54
|
+
expect(effective.pages['custom-about']).toEqual({
|
|
55
|
+
label: 'About Us',
|
|
56
|
+
previewPath: '/page/about-us',
|
|
57
|
+
allowedBlocks: expect.arrayContaining(['youtube-embed', 'custom-html']),
|
|
58
|
+
defaultBlocks: [],
|
|
59
|
+
});
|
|
60
|
+
expect(validateBuilderSettingsAgainstManifest(settings, manifest)).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('rejects multi-segment custom page slugs', () => {
|
|
64
|
+
const result = CustomPageSchema.safeParse({
|
|
65
|
+
id: 'custom-help-faq',
|
|
66
|
+
title: 'Help FAQ',
|
|
67
|
+
slug: 'help/faq',
|
|
68
|
+
visible: true,
|
|
69
|
+
layout: { blocks: [] },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(result.success).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { BuilderSettings } from './builder-settings.ts';
|
|
2
|
+
import type { ManifestPage, ThemeManifest } from './theme-manifest.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Allowed embed/content blocks on merchant custom pages (`theme.pages[]`).
|
|
6
|
+
* Keep aligned with `customPageAllowedBlocks` in
|
|
7
|
+
* `config/builder-shared-manifest-blocks.json`.
|
|
8
|
+
*/
|
|
9
|
+
export const MERCHANT_CUSTOM_PAGE_ALLOWED_BLOCKS = [
|
|
10
|
+
'custom-html',
|
|
11
|
+
'youtube-embed',
|
|
12
|
+
'text-block',
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export type MerchantCustomPageAllowedBlock =
|
|
16
|
+
(typeof MERCHANT_CUSTOM_PAGE_ALLOWED_BLOCKS)[number];
|
|
17
|
+
|
|
18
|
+
export function resolveMerchantCustomPageAllowedBlocks(
|
|
19
|
+
manifest: ThemeManifest,
|
|
20
|
+
): string[] {
|
|
21
|
+
return MERCHANT_CUSTOM_PAGE_ALLOWED_BLOCKS.filter(
|
|
22
|
+
(blockType) => blockType in manifest.blocks,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildMerchantCustomPageManifestEntry(
|
|
27
|
+
customPage: BuilderSettings['theme']['pages'][number],
|
|
28
|
+
manifest: ThemeManifest,
|
|
29
|
+
): ManifestPage {
|
|
30
|
+
return {
|
|
31
|
+
label: customPage.title,
|
|
32
|
+
previewPath: `/page/${customPage.slug}`,
|
|
33
|
+
allowedBlocks: resolveMerchantCustomPageAllowedBlocks(manifest),
|
|
34
|
+
defaultBlocks: [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function mergeCustomPagesIntoManifest(
|
|
39
|
+
manifest: ThemeManifest,
|
|
40
|
+
settings: BuilderSettings,
|
|
41
|
+
): ThemeManifest {
|
|
42
|
+
if (settings.theme.pages.length === 0) {
|
|
43
|
+
return manifest;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const pages = { ...manifest.pages };
|
|
47
|
+
for (const customPage of settings.theme.pages) {
|
|
48
|
+
if (pages[customPage.id]) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
pages[customPage.id] = buildMerchantCustomPageManifestEntry(customPage, manifest);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { ...manifest, pages };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function findCustomPageBySlug(
|
|
59
|
+
settings: BuilderSettings,
|
|
60
|
+
slug: string,
|
|
61
|
+
): BuilderSettings['theme']['pages'][number] | undefined {
|
|
62
|
+
return settings.theme.pages.find((page) => page.slug === slug);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function findCustomPageById(
|
|
66
|
+
settings: BuilderSettings,
|
|
67
|
+
pageId: string,
|
|
68
|
+
): BuilderSettings['theme']['pages'][number] | undefined {
|
|
69
|
+
return settings.theme.pages.find((page) => page.id === pageId);
|
|
70
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
createDedicatedPageBlockType,
|
|
4
|
+
dedicatedPageContentKey,
|
|
5
|
+
dedicatedPagePreviewPath,
|
|
6
|
+
migrateDedicatedPageContent,
|
|
7
|
+
migrateDedicatedPageLayout,
|
|
8
|
+
normalizeDedicatedPageId,
|
|
9
|
+
} from './dedicated-pages.ts';
|
|
10
|
+
|
|
11
|
+
describe('dedicated-pages', () => {
|
|
12
|
+
test('normalizeDedicatedPageId maps legacy reviews id', () => {
|
|
13
|
+
expect(normalizeDedicatedPageId('reviews')).toBe('reviews-page');
|
|
14
|
+
expect(normalizeDedicatedPageId('faq-page')).toBe('faq-page');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('createDedicatedPageBlockType follows page-{pageId} rule', () => {
|
|
18
|
+
expect(createDedicatedPageBlockType('faq-page')).toBe('page-faq-page');
|
|
19
|
+
expect(createDedicatedPageBlockType('reviews-page')).toBe('page-reviews-page');
|
|
20
|
+
expect(createDedicatedPageBlockType('reviews')).toBe('page-reviews');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('dedicatedPagePreviewPath resolves known storefront paths', () => {
|
|
24
|
+
expect(dedicatedPagePreviewPath('contact-page')).toBe('/contact');
|
|
25
|
+
expect(dedicatedPagePreviewPath('reviews')).toBe('/reviews');
|
|
26
|
+
expect(dedicatedPagePreviewPath('home')).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('dedicatedPageContentKey uses canonical page id', () => {
|
|
30
|
+
expect(dedicatedPageContentKey('reviews', 'title')).toBe('pages.reviews-page.title');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('migrateDedicatedPageLayout renames legacy reviews page layout', () => {
|
|
34
|
+
const migrated = migrateDedicatedPageLayout({
|
|
35
|
+
home: { blocks: [] },
|
|
36
|
+
reviews: {
|
|
37
|
+
blocks: [{ id: 'reviews-1', type: 'page-reviews', visible: true, settings: {} }],
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(migrated.reviews).toBeUndefined();
|
|
42
|
+
expect(migrated['reviews-page']).toMatchObject({
|
|
43
|
+
blocks: [{ id: 'reviews-1', type: 'page-reviews-page', visible: true, settings: {} }],
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('migrateDedicatedPageContent renames legacy content keys', () => {
|
|
48
|
+
const migrated = migrateDedicatedPageContent({
|
|
49
|
+
'pages.reviews.title': 'Reviews',
|
|
50
|
+
'tool.title': 'Free Tool',
|
|
51
|
+
'hero.title': 'Home',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(migrated['pages.reviews.title']).toBeUndefined();
|
|
55
|
+
expect(migrated['pages.reviews-page.title']).toBe('Reviews');
|
|
56
|
+
expect(migrated['tool.title']).toBeUndefined();
|
|
57
|
+
expect(migrated['pages.tool-page.title']).toBe('Free Tool');
|
|
58
|
+
expect(migrated['hero.title']).toBe('Home');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('migrateDedicatedPageLayout renames legacy vault tool page', () => {
|
|
62
|
+
const migrated = migrateDedicatedPageLayout({
|
|
63
|
+
tool: {
|
|
64
|
+
blocks: [{
|
|
65
|
+
id: 'tool-1',
|
|
66
|
+
type: 'tool',
|
|
67
|
+
visible: true,
|
|
68
|
+
settings: { 'tool.title': 'Free Tool' },
|
|
69
|
+
}],
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(migrated.tool).toBeUndefined();
|
|
74
|
+
expect(migrated['tool-page']).toMatchObject({
|
|
75
|
+
blocks: [{
|
|
76
|
+
id: 'tool-1',
|
|
77
|
+
type: 'page-tool-page',
|
|
78
|
+
visible: true,
|
|
79
|
+
settings: { 'pages.tool-page.title': 'Free Tool' },
|
|
80
|
+
}],
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('dedicatedPagePreviewPath resolves tool-page', () => {
|
|
85
|
+
expect(dedicatedPagePreviewPath('tool-page')).toBe('/tool');
|
|
86
|
+
expect(dedicatedPagePreviewPath('tool')).toBe('/tool');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
export const BUILDER_LAYOUT_PAGE_IDS = [
|
|
2
|
+
'home',
|
|
3
|
+
'navigation',
|
|
4
|
+
'product',
|
|
5
|
+
'footer',
|
|
6
|
+
'terms',
|
|
7
|
+
] as const;
|
|
8
|
+
|
|
9
|
+
export type BuilderLayoutPageId = (typeof BUILDER_LAYOUT_PAGE_IDS)[number];
|
|
10
|
+
|
|
11
|
+
export const BUILDER_DEDICATED_PAGE_IDS = [
|
|
12
|
+
'contact-page',
|
|
13
|
+
'faq-page',
|
|
14
|
+
'reviews-page',
|
|
15
|
+
'feedback-page',
|
|
16
|
+
'guide-page',
|
|
17
|
+
'status-page',
|
|
18
|
+
'checkout-page',
|
|
19
|
+
'tool-page',
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export type BuilderDedicatedPageId = (typeof BUILDER_DEDICATED_PAGE_IDS)[number];
|
|
23
|
+
|
|
24
|
+
/** @deprecated Legacy manifest page ids migrated on settings load. */
|
|
25
|
+
export const DEDICATED_PAGE_ID_ALIASES: Readonly<Record<string, BuilderDedicatedPageId>> = {
|
|
26
|
+
reviews: 'reviews-page',
|
|
27
|
+
tool: 'tool-page',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const DEDICATED_PAGE_PREVIEW_PATHS: Readonly<Record<BuilderDedicatedPageId, string>> = {
|
|
31
|
+
'contact-page': '/contact',
|
|
32
|
+
'faq-page': '/faq',
|
|
33
|
+
'reviews-page': '/reviews',
|
|
34
|
+
'feedback-page': '/feedback',
|
|
35
|
+
'guide-page': '/guide',
|
|
36
|
+
'status-page': '/status',
|
|
37
|
+
'checkout-page': '/checkout',
|
|
38
|
+
'tool-page': '/tool',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function normalizeDedicatedPageId(pageId: string): string {
|
|
42
|
+
return DEDICATED_PAGE_ID_ALIASES[pageId] ?? pageId;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function isDedicatedPageId(pageId: string): pageId is BuilderDedicatedPageId {
|
|
46
|
+
return (BUILDER_DEDICATED_PAGE_IDS as readonly string[]).includes(pageId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function dedicatedPagePreviewPath(pageId: string): string | null {
|
|
50
|
+
const normalized = normalizeDedicatedPageId(pageId);
|
|
51
|
+
if (!isDedicatedPageId(normalized)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return DEDICATED_PAGE_PREVIEW_PATHS[normalized];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function dedicatedPageContentKey(pageId: string, field: string): string {
|
|
58
|
+
return `pages.${normalizeDedicatedPageId(pageId)}.${field}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function normalizeDedicatedPageBlockType(pageId: string): string {
|
|
62
|
+
return createDedicatedPageBlockType(normalizeDedicatedPageId(pageId));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createDedicatedPageBlockType(pageId: string): string {
|
|
66
|
+
const normalizedPageId = pageId
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.replace(/[^a-z0-9\-_.]+/g, '-')
|
|
69
|
+
.replace(/^[-_.]+/, '');
|
|
70
|
+
return `page-${normalizedPageId || 'custom'}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function migrateDedicatedPageBlockSettings(settings: unknown): Record<string, unknown> {
|
|
74
|
+
if (typeof settings !== 'object' || settings === null || Array.isArray(settings)) {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const next: Record<string, unknown> = {};
|
|
79
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
80
|
+
if (key.startsWith('tool.')) {
|
|
81
|
+
next[`pages.tool-page.${key.slice('tool.'.length)}`] = value;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
next[key] = value;
|
|
85
|
+
}
|
|
86
|
+
return next;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function migrateDedicatedPageLayout(layout: Record<string, unknown>): Record<string, unknown> {
|
|
90
|
+
const next: Record<string, unknown> = { ...layout };
|
|
91
|
+
|
|
92
|
+
for (const [legacyId, canonicalId] of Object.entries(DEDICATED_PAGE_ID_ALIASES)) {
|
|
93
|
+
if (!Object.prototype.hasOwnProperty.call(next, legacyId)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const legacyLayout = next[legacyId];
|
|
98
|
+
delete next[legacyId];
|
|
99
|
+
|
|
100
|
+
if (Object.prototype.hasOwnProperty.call(next, canonicalId)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof legacyLayout !== 'object' || legacyLayout === null || Array.isArray(legacyLayout)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const blocks = (legacyLayout as { blocks?: unknown }).blocks;
|
|
109
|
+
if (!Array.isArray(blocks)) {
|
|
110
|
+
next[canonicalId] = legacyLayout;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const legacyBlockType = createDedicatedPageBlockType(legacyId);
|
|
115
|
+
const canonicalBlockType = createDedicatedPageBlockType(canonicalId);
|
|
116
|
+
|
|
117
|
+
next[canonicalId] = {
|
|
118
|
+
...legacyLayout,
|
|
119
|
+
blocks: blocks.map((block) => {
|
|
120
|
+
if (typeof block !== 'object' || block === null || Array.isArray(block)) {
|
|
121
|
+
return block;
|
|
122
|
+
}
|
|
123
|
+
const typed = block as { type?: unknown };
|
|
124
|
+
const isLegacyBlockType = typed.type === legacyBlockType
|
|
125
|
+
|| (legacyId === 'tool' && typed.type === 'tool');
|
|
126
|
+
if (!isLegacyBlockType) {
|
|
127
|
+
return block;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
...typed,
|
|
131
|
+
type: canonicalBlockType,
|
|
132
|
+
settings: migrateDedicatedPageBlockSettings(
|
|
133
|
+
(typed as { settings?: unknown }).settings,
|
|
134
|
+
),
|
|
135
|
+
};
|
|
136
|
+
}),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return next;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function migrateDedicatedPageContent(content: Record<string, unknown>): Record<string, unknown> {
|
|
144
|
+
const next: Record<string, unknown> = { ...content };
|
|
145
|
+
|
|
146
|
+
for (const [key, value] of Object.entries(next)) {
|
|
147
|
+
if (key.startsWith('tool.') && !key.startsWith('tool-page.')) {
|
|
148
|
+
const canonicalKey = `pages.tool-page.${key.slice('tool.'.length)}`;
|
|
149
|
+
if (!Object.prototype.hasOwnProperty.call(next, canonicalKey)) {
|
|
150
|
+
next[canonicalKey] = value;
|
|
151
|
+
}
|
|
152
|
+
delete next[key];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const [legacyId, canonicalId] of Object.entries(DEDICATED_PAGE_ID_ALIASES)) {
|
|
157
|
+
const legacyPrefix = `pages.${legacyId}.`;
|
|
158
|
+
const canonicalPrefix = `pages.${canonicalId}.`;
|
|
159
|
+
|
|
160
|
+
for (const [key, value] of Object.entries(next)) {
|
|
161
|
+
if (!key.startsWith(legacyPrefix)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const canonicalKey = `${canonicalPrefix}${key.slice(legacyPrefix.length)}`;
|
|
165
|
+
if (!Object.prototype.hasOwnProperty.call(next, canonicalKey)) {
|
|
166
|
+
next[canonicalKey] = value;
|
|
167
|
+
}
|
|
168
|
+
delete next[key];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return next;
|
|
173
|
+
}
|
package/src/fields.ts
CHANGED
|
@@ -8,10 +8,10 @@ const FieldBaseSchema = z
|
|
|
8
8
|
defaultValue: z.unknown().optional(),
|
|
9
9
|
required: z.boolean().optional(),
|
|
10
10
|
// Groups settings within the Inspector. "content" (default) shows
|
|
11
|
-
// under the Block Settings collapsible; "
|
|
12
|
-
//
|
|
13
|
-
// existing block.settings.path lookup
|
|
14
|
-
group: z.enum(['content', 'style']).optional(),
|
|
11
|
+
// under the Block Settings collapsible; "profile" shows under Profile
|
|
12
|
+
// banner; "style" shows under Style; "search" shows under Search bar.
|
|
13
|
+
// Themes pick up the value via the existing block.settings.path lookup.
|
|
14
|
+
group: z.enum(['content', 'profile', 'style', 'search']).optional(),
|
|
15
15
|
})
|
|
16
16
|
.strict();
|
|
17
17
|
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export * from './custom-pages.ts';
|
|
2
|
+
export * from './dedicated-pages.ts';
|
|
1
3
|
export * from './builder-settings.ts';
|
|
2
4
|
export * from './canonical-settings.ts';
|
|
3
5
|
export * from './events.ts';
|
|
@@ -10,6 +12,7 @@ export * from './preview-protocol.ts';
|
|
|
10
12
|
export * from './preview-session-resolve.ts';
|
|
11
13
|
export * from './preview-trusted-origins.ts';
|
|
12
14
|
export * from './storefront-initial-data-html.ts';
|
|
15
|
+
export * from './storefront-typography-fonts.ts';
|
|
13
16
|
export * from './style-slots.ts';
|
|
14
17
|
export * from './theme-manifest.ts';
|
|
15
18
|
export * from './theme-schemes.ts';
|
package/src/legacy-manifest.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as z from 'zod/v4';
|
|
2
|
+
import { createDedicatedPageBlockType } from './dedicated-pages.ts';
|
|
2
3
|
import { BuilderFieldSchema, type BuilderField } from './fields.ts';
|
|
3
4
|
import { ThemeManifestSchema, type ThemeManifest } from './theme-manifest.ts';
|
|
4
5
|
import { StyleSlotDefaultsSchema } from './style-slots.ts';
|
|
@@ -129,7 +130,7 @@ export function convertLegacyThemeManifest(input: unknown): ThemeManifest {
|
|
|
129
130
|
);
|
|
130
131
|
|
|
131
132
|
for (const page of legacy.builder.pages) {
|
|
132
|
-
const syntheticBlockType =
|
|
133
|
+
const syntheticBlockType = createDedicatedPageBlockType(page.id);
|
|
133
134
|
if ((page.fields?.length ?? 0) === 0 && (page.lists?.length ?? 0) === 0) {
|
|
134
135
|
continue;
|
|
135
136
|
}
|
|
@@ -161,7 +162,7 @@ export function convertLegacyThemeManifest(input: unknown): ThemeManifest {
|
|
|
161
162
|
hotfixPaths: legacy.hotfixPaths,
|
|
162
163
|
pages: Object.fromEntries(
|
|
163
164
|
legacy.builder.pages.map((page) => {
|
|
164
|
-
const syntheticBlockType =
|
|
165
|
+
const syntheticBlockType = createDedicatedPageBlockType(page.id);
|
|
165
166
|
const hasSyntheticBlock = blocks[syntheticBlockType] !== undefined;
|
|
166
167
|
const allowedBlocks = page.blocks.length > 0
|
|
167
168
|
? page.blocks
|
|
@@ -201,11 +202,6 @@ function normalizeLegacyThemeId(rawId: string): string {
|
|
|
201
202
|
return `legacy-${lowered || 'theme'}`;
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
function createSyntheticPageBlockType(pageId: string): string {
|
|
205
|
-
const normalizedPageId = pageId.toLowerCase().replace(/[^a-z0-9\-_.]+/g, '-').replace(/^[-_.]+/, '');
|
|
206
|
-
return `page-${normalizedPageId || 'custom'}`;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
205
|
function convertLegacyThemePresets(presets: LegacyThemePreset[]): ThemeManifest['presets'] {
|
|
210
206
|
return Object.fromEntries(
|
|
211
207
|
presets.map((preset) => {
|
|
@@ -410,3 +406,40 @@ function createListItemShape(kind: string): Extract<BuilderField, { type: 'list'
|
|
|
410
406
|
body: { type: 'richtext', label: 'Body' },
|
|
411
407
|
};
|
|
412
408
|
}
|
|
409
|
+
|
|
410
|
+
export type ThemePageBlockOrderPage = {
|
|
411
|
+
allowedBlocks?: string[];
|
|
412
|
+
defaultBlocks?: Array<{ type?: string }>;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
function resolveManifestPagesForBlockOrder(manifest: unknown): Record<string, ThemePageBlockOrderPage> {
|
|
416
|
+
const canonical = ThemeManifestSchema.safeParse(manifest);
|
|
417
|
+
if (canonical.success) {
|
|
418
|
+
return canonical.data.pages;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
return convertLegacyThemeManifest(manifest).pages;
|
|
423
|
+
} catch {
|
|
424
|
+
if (typeof manifest === 'object' && manifest !== null && 'pages' in manifest) {
|
|
425
|
+
const pages = (manifest as { pages?: unknown }).pages;
|
|
426
|
+
if (typeof pages === 'object' && pages !== null && !Array.isArray(pages)) {
|
|
427
|
+
return pages as Record<string, ThemePageBlockOrderPage>;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return {};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function getThemePageBlockOrderFromManifest(manifest: unknown, pageId: string): string[] {
|
|
435
|
+
const page = resolveManifestPagesForBlockOrder(manifest)[pageId];
|
|
436
|
+
if (!page) {
|
|
437
|
+
return [];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const defaultBlockTypes = (Array.isArray(page.defaultBlocks) ? page.defaultBlocks : [])
|
|
441
|
+
.map((block) => (typeof block === 'object' && block !== null ? block.type : undefined))
|
|
442
|
+
.filter((blockType): blockType is string => typeof blockType === 'string' && blockType.length > 0);
|
|
443
|
+
|
|
444
|
+
return defaultBlockTypes.length > 0 ? defaultBlockTypes : page.allowedBlocks ?? [];
|
|
445
|
+
}
|