@shoppexio/builder-contracts 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder-settings.d.ts +11 -666
- package/dist/builder-settings.d.ts.map +1 -1
- package/dist/builder-settings.js +2 -1
- package/dist/canonical-settings.d.ts +18 -0
- package/dist/canonical-settings.d.ts.map +1 -0
- package/dist/canonical-settings.js +106 -0
- 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/events.d.ts +35 -240
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +7 -0
- package/dist/fields.d.ts +229 -10
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +27 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/legacy-manifest.d.ts +18 -0
- package/dist/legacy-manifest.d.ts.map +1 -1
- package/dist/legacy-manifest.js +137 -22
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +55 -6
- package/dist/persistence.d.ts +7 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +58 -0
- package/dist/preview-boot.d.ts +68 -0
- package/dist/preview-boot.d.ts.map +1 -0
- package/dist/preview-boot.js +38 -0
- package/dist/preview-protocol.d.ts +227 -459
- package/dist/preview-protocol.d.ts.map +1 -1
- package/dist/preview-protocol.js +112 -0
- package/dist/preview-session-resolve.d.ts +115 -0
- package/dist/preview-session-resolve.d.ts.map +1 -0
- package/dist/preview-session-resolve.js +25 -0
- package/dist/preview-trusted-origins.d.ts +4 -0
- package/dist/preview-trusted-origins.d.ts.map +1 -0
- package/dist/preview-trusted-origins.js +28 -0
- package/dist/storefront-initial-data-html.d.ts +17 -0
- package/dist/storefront-initial-data-html.d.ts.map +1 -0
- package/dist/storefront-initial-data-html.js +83 -0
- 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 +50 -152
- package/dist/style-slots.d.ts.map +1 -1
- package/dist/style-slots.js +80 -32
- package/dist/theme-manifest.d.ts +287 -456
- package/dist/theme-manifest.d.ts.map +1 -1
- package/dist/theme-manifest.js +92 -0
- package/dist/theme-schemes.d.ts +10 -0
- package/dist/theme-schemes.d.ts.map +1 -0
- package/dist/theme-schemes.js +25 -0
- package/dist/validation.d.ts +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +23 -12
- package/package.json +43 -1
- package/src/builder-contracts.test.ts +416 -3
- package/src/builder-settings.ts +4 -1
- package/src/canonical-settings.ts +156 -0
- 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/events.ts +8 -0
- package/src/fields.ts +30 -0
- package/src/index.ts +10 -0
- package/src/legacy-manifest.ts +147 -23
- package/src/migrations.ts +70 -6
- package/src/persistence.ts +77 -0
- package/src/preview-boot.test.ts +72 -0
- package/src/preview-boot.ts +49 -0
- package/src/preview-protocol.test.ts +132 -0
- package/src/preview-protocol.ts +122 -0
- package/src/preview-session-resolve.test.ts +37 -0
- package/src/preview-session-resolve.ts +34 -0
- package/src/preview-trusted-origins.test.ts +24 -0
- package/src/preview-trusted-origins.ts +35 -0
- package/src/storefront-initial-data-html.test.ts +73 -0
- package/src/storefront-initial-data-html.ts +112 -0
- package/src/storefront-typography-fonts.test.ts +48 -0
- package/src/storefront-typography-fonts.ts +108 -0
- package/src/style-slots.ts +102 -34
- package/src/theme-manifest.ts +118 -1
- package/src/theme-schemes.ts +34 -0
- package/src/validation.ts +32 -13
- 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 -361
|
@@ -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/events.ts
CHANGED
|
@@ -102,6 +102,13 @@ const TermsSetEventSchema = EventBaseSchema.extend({
|
|
|
102
102
|
value: z.string(),
|
|
103
103
|
}).strict();
|
|
104
104
|
|
|
105
|
+
const PresetApplyEventSchema = EventBaseSchema.extend({
|
|
106
|
+
type: z.literal('preset.apply'),
|
|
107
|
+
pageId: PageIdSchema,
|
|
108
|
+
presetId: z.string().min(1),
|
|
109
|
+
blockId: z.string().min(1).optional(),
|
|
110
|
+
}).strict();
|
|
111
|
+
|
|
105
112
|
export const BuilderEventSchema = z.discriminatedUnion('type', [
|
|
106
113
|
ContentSetEventSchema,
|
|
107
114
|
ListSetEventSchema,
|
|
@@ -117,5 +124,6 @@ export const BuilderEventSchema = z.discriminatedUnion('type', [
|
|
|
117
124
|
PageUpsertEventSchema,
|
|
118
125
|
PageRemoveEventSchema,
|
|
119
126
|
TermsSetEventSchema,
|
|
127
|
+
PresetApplyEventSchema,
|
|
120
128
|
]);
|
|
121
129
|
export type BuilderEvent = z.infer<typeof BuilderEventSchema>;
|
package/src/fields.ts
CHANGED
|
@@ -7,6 +7,11 @@ const FieldBaseSchema = z
|
|
|
7
7
|
description: z.string().min(1).optional(),
|
|
8
8
|
defaultValue: z.unknown().optional(),
|
|
9
9
|
required: z.boolean().optional(),
|
|
10
|
+
// Groups settings within the Inspector. "content" (default) shows
|
|
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(),
|
|
10
15
|
})
|
|
11
16
|
.strict();
|
|
12
17
|
|
|
@@ -30,6 +35,30 @@ const RichTextFieldSchema = FieldBaseSchema.extend({
|
|
|
30
35
|
allowedMarks: z.array(z.enum(['bold', 'italic', 'link', 'code'])).optional(),
|
|
31
36
|
}).strict();
|
|
32
37
|
|
|
38
|
+
// Plain-text source code field. Renders a Monaco editor in the
|
|
39
|
+
// Inspector and persists the raw string back into block.settings.
|
|
40
|
+
// Used by the `custom-html` block (and any future block that needs
|
|
41
|
+
// a code-shaped setting). Themes interpret the string however they
|
|
42
|
+
// see fit — sandboxed iframe srcDoc for HTML, etc.
|
|
43
|
+
const CodeFieldTemplateSchema = z
|
|
44
|
+
.object({
|
|
45
|
+
label: z.string().min(1),
|
|
46
|
+
snippet: z.string().min(1),
|
|
47
|
+
description: z.string().min(1).optional(),
|
|
48
|
+
})
|
|
49
|
+
.strict();
|
|
50
|
+
export type CodeFieldTemplate = z.infer<typeof CodeFieldTemplateSchema>;
|
|
51
|
+
|
|
52
|
+
const CodeFieldSchema = FieldBaseSchema.extend({
|
|
53
|
+
type: z.literal('code'),
|
|
54
|
+
language: z.enum(['html', 'css', 'javascript', 'json']).optional(),
|
|
55
|
+
placeholder: z.string().optional(),
|
|
56
|
+
maxLength: z.number().int().positive().optional(),
|
|
57
|
+
// Optional quick-insert chips rendered above the editor. Clicking a
|
|
58
|
+
// template replaces the field value with the snippet.
|
|
59
|
+
templates: z.array(CodeFieldTemplateSchema).optional(),
|
|
60
|
+
}).strict();
|
|
61
|
+
|
|
33
62
|
const ImageFieldSchema = FieldBaseSchema.extend({
|
|
34
63
|
type: z.literal('image'),
|
|
35
64
|
aspectRatio: z.string().min(1).optional(),
|
|
@@ -117,6 +146,7 @@ const ListFieldSchema = FieldBaseSchema.extend({
|
|
|
117
146
|
export const BuilderFieldSchema = z.discriminatedUnion('type', [
|
|
118
147
|
TextFieldSchema,
|
|
119
148
|
RichTextFieldSchema,
|
|
149
|
+
CodeFieldSchema,
|
|
120
150
|
ImageFieldSchema,
|
|
121
151
|
LinkFieldSchema,
|
|
122
152
|
BooleanFieldSchema,
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
export * from './custom-pages.ts';
|
|
2
|
+
export * from './dedicated-pages.ts';
|
|
1
3
|
export * from './builder-settings.ts';
|
|
4
|
+
export * from './canonical-settings.ts';
|
|
2
5
|
export * from './events.ts';
|
|
3
6
|
export * from './fields.ts';
|
|
4
7
|
export * from './legacy-manifest.ts';
|
|
5
8
|
export * from './migrations.ts';
|
|
9
|
+
export * from './persistence.ts';
|
|
10
|
+
export * from './preview-boot.ts';
|
|
6
11
|
export * from './preview-protocol.ts';
|
|
12
|
+
export * from './preview-session-resolve.ts';
|
|
13
|
+
export * from './preview-trusted-origins.ts';
|
|
14
|
+
export * from './storefront-initial-data-html.ts';
|
|
15
|
+
export * from './storefront-typography-fonts.ts';
|
|
7
16
|
export * from './style-slots.ts';
|
|
8
17
|
export * from './theme-manifest.ts';
|
|
18
|
+
export * from './theme-schemes.ts';
|
|
9
19
|
export * from './validation.ts';
|