@toaster-builder/site-runtime 0.1.0

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.
Files changed (70) hide show
  1. package/README.md +38 -0
  2. package/dist/module.d.mts +7 -0
  3. package/dist/module.json +12 -0
  4. package/dist/module.mjs +21 -0
  5. package/dist/runtime/assets/editor/add-block.svg +5 -0
  6. package/dist/runtime/assets/editor/ai-assistant.svg +5 -0
  7. package/dist/runtime/assets/editor/block-settings-center.svg +3 -0
  8. package/dist/runtime/assets/editor/block-settings-path.svg +3 -0
  9. package/dist/runtime/assets/editor/copy-block.svg +10 -0
  10. package/dist/runtime/assets/editor/delete-block.svg +4 -0
  11. package/dist/runtime/assets/editor/hide-block.svg +6 -0
  12. package/dist/runtime/assets/editor/move-block-down.svg +3 -0
  13. package/dist/runtime/assets/editor/move-block-up.svg +3 -0
  14. package/dist/runtime/assets/editor/show-block.svg +4 -0
  15. package/dist/runtime/components/BlockEditorFrame.d.vue.ts +16 -0
  16. package/dist/runtime/components/BlockEditorFrame.vue +418 -0
  17. package/dist/runtime/components/BlockEditorFrame.vue.d.ts +16 -0
  18. package/dist/runtime/components/BlockPlacementProvider.d.vue.ts +16 -0
  19. package/dist/runtime/components/BlockPlacementProvider.vue +12 -0
  20. package/dist/runtime/components/BlockPlacementProvider.vue.d.ts +16 -0
  21. package/dist/runtime/components/PageRegionRenderer.d.vue.ts +16 -0
  22. package/dist/runtime/components/PageRegionRenderer.vue +50 -0
  23. package/dist/runtime/components/PageRegionRenderer.vue.d.ts +16 -0
  24. package/dist/runtime/components/PageRenderer.d.vue.ts +3 -0
  25. package/dist/runtime/components/PageRenderer.vue +65 -0
  26. package/dist/runtime/components/PageRenderer.vue.d.ts +3 -0
  27. package/dist/runtime/composables/useBuilder.d.ts +15 -0
  28. package/dist/runtime/composables/useBuilder.js +27 -0
  29. package/dist/runtime/composables/useBuilderEventService.d.ts +3 -0
  30. package/dist/runtime/composables/useBuilderEventService.js +14 -0
  31. package/dist/runtime/composables/useBuilderState.d.ts +8 -0
  32. package/dist/runtime/composables/useBuilderState.js +21 -0
  33. package/dist/runtime/composables/usePageRegistry.d.ts +6 -0
  34. package/dist/runtime/composables/usePageRegistry.js +76 -0
  35. package/dist/runtime/composables/usePageSettings.d.ts +21 -0
  36. package/dist/runtime/composables/usePageSettings.js +90 -0
  37. package/dist/runtime/events/builder-event.d.ts +9 -0
  38. package/dist/runtime/events/builder-event.js +14 -0
  39. package/dist/runtime/events/builder-form-preview-token-event.d.ts +7 -0
  40. package/dist/runtime/events/builder-form-preview-token-event.js +19 -0
  41. package/dist/runtime/events/builder-page-state-event.d.ts +9 -0
  42. package/dist/runtime/events/builder-page-state-event.js +62 -0
  43. package/dist/runtime/events/builder-ready-ping-event.d.ts +6 -0
  44. package/dist/runtime/events/builder-ready-ping-event.js +10 -0
  45. package/dist/runtime/events/site-options-updated-event.d.ts +8 -0
  46. package/dist/runtime/events/site-options-updated-event.js +18 -0
  47. package/dist/runtime/plugin.d.ts +10 -0
  48. package/dist/runtime/plugin.js +10 -0
  49. package/dist/runtime/server/tsconfig.json +3 -0
  50. package/dist/runtime/services/builder-event-service.d.ts +37 -0
  51. package/dist/runtime/services/builder-event-service.js +104 -0
  52. package/dist/runtime/types/assets.d.ts +5 -0
  53. package/dist/runtime/types/page.d.ts +37 -0
  54. package/dist/runtime/types/page.js +9 -0
  55. package/dist/runtime/utils/block-options.d.ts +2 -0
  56. package/dist/runtime/utils/block-options.js +9 -0
  57. package/dist/runtime/utils/block-placement.d.ts +2 -0
  58. package/dist/runtime/utils/block-placement.js +1 -0
  59. package/dist/runtime/utils/common.d.ts +2 -0
  60. package/dist/runtime/utils/common.js +6 -0
  61. package/dist/runtime/utils/css-variables.d.ts +7 -0
  62. package/dist/runtime/utils/css-variables.js +35 -0
  63. package/dist/runtime/utils/fonts.d.ts +8 -0
  64. package/dist/runtime/utils/fonts.js +80 -0
  65. package/dist/runtime/utils/page-config.d.ts +3 -0
  66. package/dist/runtime/utils/page-config.js +114 -0
  67. package/dist/runtime/utils/page-options.d.ts +3 -0
  68. package/dist/runtime/utils/page-options.js +26 -0
  69. package/dist/types.d.mts +3 -0
  70. package/package.json +65 -0
@@ -0,0 +1,37 @@
1
+ import type { Ref } from 'vue';
2
+ import type { BlockOptions, PageRegion, PageRuntimeState } from '#toaster-site-runtime/types/page';
3
+ export type BuilderBlockEvent = 'add-block' | 'change-block-version' | 'copy-block' | 'delete-block' | 'hide-block' | 'move-block' | 'open-assistant-block' | 'open-create-block-assistant' | 'open-setting-block';
4
+ interface BuilderBlockEventTarget {
5
+ blockId: string;
6
+ placementId: string;
7
+ region: PageRegion;
8
+ }
9
+ interface BuilderEventServiceOptions {
10
+ builderOrigin: Ref<string | null>;
11
+ builderReady: Ref<boolean>;
12
+ siteOptions: Ref<BlockOptions>;
13
+ }
14
+ export interface BuilderPageContext {
15
+ currentPath: () => string;
16
+ formPreviewToken: Ref<string | null>;
17
+ isKnownBlock: (blockId: string) => boolean;
18
+ pageRuntimeState: Ref<PageRuntimeState | null>;
19
+ }
20
+ export declare class BuilderEventService {
21
+ private readonly options;
22
+ private static readonly processedMutationLimit;
23
+ private readonly handlers;
24
+ private readonly processedMutationIds;
25
+ private pageContext;
26
+ constructor(options: BuilderEventServiceOptions);
27
+ setPageContext(context: BuilderPageContext): () => void;
28
+ readonly handleMessage: (messageEvent: MessageEvent<unknown>) => void;
29
+ sendBlockEvent(event: BuilderBlockEvent, target: BuilderBlockEventTarget, payload?: Record<string, unknown>): void;
30
+ private readonly handleBuilderReadyPing;
31
+ private readonly handleSiteOptionsUpdated;
32
+ private readonly handleFormPreviewToken;
33
+ private readonly handlePageState;
34
+ private applyMutation;
35
+ private acknowledgeMutation;
36
+ }
37
+ export {};
@@ -0,0 +1,104 @@
1
+ import { BuilderFormPreviewTokenEvent } from "#toaster-site-runtime/events/builder-form-preview-token-event";
2
+ import { BuilderPageStateEvent } from "#toaster-site-runtime/events/builder-page-state-event";
3
+ import { BuilderReadyPingEvent } from "#toaster-site-runtime/events/builder-ready-ping-event";
4
+ import { SiteOptionsUpdatedEvent } from "#toaster-site-runtime/events/site-options-updated-event";
5
+ import { isRecord } from "#toaster-site-runtime/utils/common";
6
+ export class BuilderEventService {
7
+ constructor(options) {
8
+ this.options = options;
9
+ this.handlers = /* @__PURE__ */ new Map([
10
+ [BuilderReadyPingEvent.event, (_payload, origin) => {
11
+ this.handleBuilderReadyPing(BuilderReadyPingEvent.fromOrigin(origin));
12
+ }],
13
+ [SiteOptionsUpdatedEvent.event, (payload, origin) => {
14
+ const siteOptionsUpdatedEvent = SiteOptionsUpdatedEvent.fromPayload(payload, origin);
15
+ if (!siteOptionsUpdatedEvent) return;
16
+ this.applyMutation(siteOptionsUpdatedEvent, () => this.handleSiteOptionsUpdated(siteOptionsUpdatedEvent));
17
+ }],
18
+ [BuilderFormPreviewTokenEvent.event, (payload, origin) => {
19
+ const formPreviewTokenEvent = BuilderFormPreviewTokenEvent.fromPayload(payload, origin);
20
+ if (!formPreviewTokenEvent) return;
21
+ this.applyMutation(formPreviewTokenEvent, () => this.handleFormPreviewToken(formPreviewTokenEvent));
22
+ }],
23
+ [BuilderPageStateEvent.event, (payload, origin) => {
24
+ if (!this.pageContext) return;
25
+ const pageStateEvent = BuilderPageStateEvent.fromPayload(
26
+ payload,
27
+ origin,
28
+ this.pageContext.isKnownBlock
29
+ );
30
+ if (!pageStateEvent) return;
31
+ this.applyMutation(pageStateEvent, () => this.handlePageState(pageStateEvent));
32
+ }]
33
+ ]);
34
+ }
35
+ static processedMutationLimit = 200;
36
+ handlers;
37
+ processedMutationIds = /* @__PURE__ */ new Set();
38
+ pageContext = null;
39
+ setPageContext(context) {
40
+ this.pageContext = context;
41
+ return () => {
42
+ if (this.pageContext === context) this.pageContext = null;
43
+ };
44
+ }
45
+ handleMessage = (messageEvent) => {
46
+ if (window.parent === window || messageEvent.source !== window.parent || messageEvent.origin === "null" || !isRecord(messageEvent.data) || typeof messageEvent.data.event !== "string") {
47
+ return;
48
+ }
49
+ if (messageEvent.data.event !== BuilderReadyPingEvent.event && messageEvent.origin !== this.options.builderOrigin.value) return;
50
+ this.handlers.get(messageEvent.data.event)?.(
51
+ messageEvent.data,
52
+ messageEvent.origin
53
+ );
54
+ };
55
+ sendBlockEvent(event, target, payload = {}) {
56
+ const origin = this.options.builderOrigin.value;
57
+ if (!this.options.builderReady.value || origin === null || window.parent === window) return;
58
+ window.parent.postMessage({
59
+ event,
60
+ blockId: target.blockId,
61
+ placementId: target.placementId,
62
+ region: target.region,
63
+ ...payload
64
+ }, origin);
65
+ }
66
+ handleBuilderReadyPing = (event) => {
67
+ this.options.builderOrigin.value = event.origin;
68
+ this.options.builderReady.value = true;
69
+ window.parent.postMessage({ event: "builder-ready-pong" }, event.origin);
70
+ };
71
+ handleSiteOptionsUpdated = (event) => {
72
+ this.options.siteOptions.value = event.options;
73
+ return true;
74
+ };
75
+ handleFormPreviewToken = (event) => {
76
+ if (!this.pageContext) return false;
77
+ this.pageContext.formPreviewToken.value = event.token;
78
+ return true;
79
+ };
80
+ handlePageState = (event) => {
81
+ if (!this.pageContext || event.state.page.options.path !== this.pageContext.currentPath()) return false;
82
+ this.pageContext.pageRuntimeState.value = event.state;
83
+ return true;
84
+ };
85
+ applyMutation(event, apply) {
86
+ if (this.processedMutationIds.has(event.mutationId)) {
87
+ this.acknowledgeMutation(event);
88
+ return;
89
+ }
90
+ if (!apply()) return;
91
+ this.processedMutationIds.add(event.mutationId);
92
+ if (this.processedMutationIds.size > BuilderEventService.processedMutationLimit) {
93
+ const oldestMutationId = this.processedMutationIds.values().next().value;
94
+ if (oldestMutationId !== void 0) this.processedMutationIds.delete(oldestMutationId);
95
+ }
96
+ this.acknowledgeMutation(event);
97
+ }
98
+ acknowledgeMutation(event) {
99
+ window.parent.postMessage({
100
+ event: "builder-mutation-applied",
101
+ mutationId: event.mutationId
102
+ }, event.origin);
103
+ }
104
+ }
@@ -0,0 +1,5 @@
1
+ declare module '*.svg' {
2
+ const source: string
3
+
4
+ export default source
5
+ }
@@ -0,0 +1,37 @@
1
+ export declare const pageRegions: readonly ["header", "main", "footer"];
2
+ export declare const blockIdPattern: RegExp;
3
+ export declare const placementIdPattern: RegExp;
4
+ export type PageRegion = (typeof pageRegions)[number];
5
+ export type BlockOptions = Record<string, unknown>;
6
+ export interface BlockPlacement {
7
+ id: string;
8
+ enable: boolean;
9
+ placementId: string;
10
+ }
11
+ export type PageBlocks = Record<PageRegion, BlockPlacement[]>;
12
+ export interface PageOptions {
13
+ publish: boolean;
14
+ path: string;
15
+ seoTitle?: string;
16
+ seoDescription?: string;
17
+ [key: string]: unknown;
18
+ }
19
+ export interface PageConfig {
20
+ version: 1;
21
+ options: PageOptions;
22
+ blocks: PageBlocks;
23
+ }
24
+ export interface PageRuntimePlacement extends BlockPlacement {
25
+ options: BlockOptions;
26
+ }
27
+ export interface PageRuntimeDocument {
28
+ version: 1;
29
+ options: PageOptions;
30
+ blocks: Record<PageRegion, PageRuntimePlacement[]>;
31
+ }
32
+ export interface PageRuntimeState {
33
+ revision: string;
34
+ page: PageRuntimeDocument;
35
+ }
36
+ export declare function isBlockId(value: string): boolean;
37
+ export declare function isPlacementId(value: string): boolean;
@@ -0,0 +1,9 @@
1
+ export const pageRegions = ["header", "main", "footer"];
2
+ export const blockIdPattern = /^[A-Z][\w-]*\/[A-Z][\w-]*$/i;
3
+ export const placementIdPattern = /^[A-Z0-9][\w-]{0,127}$/i;
4
+ export function isBlockId(value) {
5
+ return blockIdPattern.test(value);
6
+ }
7
+ export function isPlacementId(value) {
8
+ return placementIdPattern.test(value);
9
+ }
@@ -0,0 +1,2 @@
1
+ import type { BlockOptions } from '#toaster-site-runtime/types/page';
2
+ export declare function getBlockElementId(options: BlockOptions, blockId: string): string;
@@ -0,0 +1,9 @@
1
+ export function getBlockElementId(options, blockId) {
2
+ const value = options["element-id"];
3
+ if (typeof value !== "string" || value.trim() === "" || /\s/.test(value)) {
4
+ throw new Error(
5
+ `Block "${blockId}" option "element-id" must contain a non-empty ID without spaces.`
6
+ );
7
+ }
8
+ return value;
9
+ }
@@ -0,0 +1,2 @@
1
+ import type { InjectionKey, Ref } from 'vue';
2
+ export declare const blockPlacementIdKey: InjectionKey<Readonly<Ref<string>>>;
@@ -0,0 +1 @@
1
+ export const blockPlacementIdKey = Symbol("toaster-block-placement-id");
@@ -0,0 +1,2 @@
1
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
2
+ export declare function isDev(): boolean;
@@ -0,0 +1,6 @@
1
+ export function isRecord(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ export function isDev() {
5
+ return import.meta.dev;
6
+ }
@@ -0,0 +1,7 @@
1
+ export interface CssVariableControl {
2
+ id: string;
3
+ type: string;
4
+ }
5
+ export declare function controlIdToCssVariable(id: string): string;
6
+ export declare function collectCssVariables(controls: readonly CssVariableControl[], options: Readonly<Record<string, unknown>>): Record<string, string>;
7
+ export declare function serializeCssVariables(variables: Readonly<Record<string, string>>): string;
@@ -0,0 +1,35 @@
1
+ import { resolveFontFamily } from "#toaster-site-runtime/utils/fonts";
2
+ const cssVariableControlTypes = /* @__PURE__ */ new Set(["color", "font"]);
3
+ const controlIdPattern = /^[a-z][a-zA-Z0-9]*$/;
4
+ const unsafeCssValuePattern = /[;{}]/;
5
+ export function controlIdToCssVariable(id) {
6
+ if (!controlIdPattern.test(id)) {
7
+ throw new Error(`Control id "${id}" cannot be converted to a CSS variable name.`);
8
+ }
9
+ return `--${id.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()}`;
10
+ }
11
+ export function collectCssVariables(controls, options) {
12
+ const variables = {};
13
+ for (const control of controls) {
14
+ if (!cssVariableControlTypes.has(control.type)) {
15
+ continue;
16
+ }
17
+ const variableName = controlIdToCssVariable(control.id);
18
+ if (variables[variableName] !== void 0) {
19
+ throw new Error(`CSS variable "${variableName}" is declared more than once.`);
20
+ }
21
+ const value = options[control.id];
22
+ if (control.type === "font") {
23
+ variables[variableName] = resolveFontFamily(value, control.id);
24
+ continue;
25
+ }
26
+ if (typeof value !== "string" || value.trim() === "" || unsafeCssValuePattern.test(value)) {
27
+ throw new Error(`Option "${control.id}" must contain a safe non-empty CSS value.`);
28
+ }
29
+ variables[variableName] = value.trim();
30
+ }
31
+ return variables;
32
+ }
33
+ export function serializeCssVariables(variables) {
34
+ return Object.entries(variables).map(([name, value]) => `${name}: ${value}`).join("; ");
35
+ }
@@ -0,0 +1,8 @@
1
+ interface FontControl {
2
+ id: string;
3
+ type: string;
4
+ }
5
+ export declare function resolveFontFamily(value: unknown, optionId: string): string;
6
+ export declare function collectFontStyleSheets(controls: readonly FontControl[], options: Readonly<Record<string, unknown>>): string[];
7
+ export declare function collectFontFacesCss(controls: readonly FontControl[], options: Readonly<Record<string, unknown>>): string;
8
+ export {};
@@ -0,0 +1,80 @@
1
+ import { isRecord } from "#toaster-site-runtime/utils/common";
2
+ const fallbackNames = /* @__PURE__ */ new Set([
3
+ "serif",
4
+ "sans-serif",
5
+ "monospace",
6
+ "cursive",
7
+ "fantasy",
8
+ "system-ui"
9
+ ]);
10
+ const fontStyles = /* @__PURE__ */ new Set(["normal", "italic", "oblique"]);
11
+ function isGoogleFontsStylesheet(value) {
12
+ try {
13
+ const url = new URL(value);
14
+ return url.protocol === "https:" && url.hostname === "fonts.googleapis.com" && url.pathname === "/css2";
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+ function fontFaceFormat(value) {
20
+ try {
21
+ const url = new URL(value);
22
+ if (!["http:", "https:"].includes(url.protocol) || url.username !== "" || url.password !== "") return null;
23
+ const extension = url.pathname.split(".").at(-1)?.toLowerCase();
24
+ return {
25
+ woff2: "woff2",
26
+ woff: "woff",
27
+ ttf: "truetype",
28
+ otf: "opentype"
29
+ }[extension ?? ""] ?? null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+ function getFontSelection(value, optionId) {
35
+ if (!isRecord(value) || !["google_fonts", "custom_fonts"].includes(String(value.provider)) || typeof value.key !== "string" || value.key.trim() === "" || value.key.length > 100 || typeof value.family !== "string" || value.family.trim() === "" || value.family.length > 100 || !/^[\p{L}\p{N} ._-]+$/u.test(value.family) || typeof value.fallback !== "string" || !fallbackNames.has(value.fallback) || !isRecord(value.installation)) {
36
+ throw new Error(`Font option "${optionId}" does not contain a valid font selection.`);
37
+ }
38
+ if (value.provider === "google_fonts" && value.installation.type === "stylesheet" && typeof value.installation.url === "string" && isGoogleFontsStylesheet(value.installation.url)) {
39
+ return value;
40
+ }
41
+ if (value.provider === "custom_fonts" && value.installation.type === "font_faces" && Array.isArray(value.installation.faces) && value.installation.faces.length > 0 && value.installation.faces.every((face) => isRecord(face) && Number.isInteger(face.weight) && Number(face.weight) >= 1 && Number(face.weight) <= 1e3 && typeof face.style === "string" && fontStyles.has(face.style) && typeof face.url === "string" && fontFaceFormat(face.url) !== null)) {
42
+ return value;
43
+ }
44
+ throw new Error(`Font option "${optionId}" contains an invalid provider installation.`);
45
+ }
46
+ export function resolveFontFamily(value, optionId) {
47
+ const font = getFontSelection(value, optionId);
48
+ return `"${font.family}", ${font.fallback}`;
49
+ }
50
+ export function collectFontStyleSheets(controls, options) {
51
+ const styleSheets = /* @__PURE__ */ new Set();
52
+ for (const control of controls) {
53
+ if (control.type !== "font") continue;
54
+ const font = getFontSelection(options[control.id], control.id);
55
+ if (font.installation.type === "stylesheet") {
56
+ styleSheets.add(font.installation.url);
57
+ }
58
+ }
59
+ return [...styleSheets];
60
+ }
61
+ export function collectFontFacesCss(controls, options) {
62
+ const rules = /* @__PURE__ */ new Set();
63
+ for (const control of controls) {
64
+ if (control.type !== "font") continue;
65
+ const font = getFontSelection(options[control.id], control.id);
66
+ if (font.installation.type !== "font_faces") continue;
67
+ for (const face of font.installation.faces) {
68
+ const format = fontFaceFormat(face.url);
69
+ if (!format) continue;
70
+ rules.add(`@font-face {
71
+ font-family: ${JSON.stringify(font.family)};
72
+ font-style: ${face.style};
73
+ font-weight: ${face.weight};
74
+ font-display: swap;
75
+ src: url(${JSON.stringify(face.url)}) format(${JSON.stringify(format)});
76
+ }`);
77
+ }
78
+ }
79
+ return [...rules].join("\n");
80
+ }
@@ -0,0 +1,3 @@
1
+ import { type BlockOptions, type BlockPlacement, type PageConfig } from '#toaster-site-runtime/types/page';
2
+ export declare function getPageConfig(path: string): PageConfig | undefined;
3
+ export declare function getBlockPlacementOptions(placement: BlockPlacement): BlockOptions;
@@ -0,0 +1,114 @@
1
+ import {
2
+ isBlockId,
3
+ isPlacementId,
4
+ pageRegions
5
+ } from "#toaster-site-runtime/types/page";
6
+ import { normalizePagePath, parsePageOptions } from "#toaster-site-runtime/utils/page-options";
7
+ import { isDev, isRecord } from "#toaster-site-runtime/utils/common";
8
+ const pageConfigModules = import.meta.glob("/pages/*/config.json", {
9
+ eager: true,
10
+ import: "default"
11
+ });
12
+ const pageSchemaModules = import.meta.glob("/pages/*/schema.json", {
13
+ eager: true,
14
+ import: "default"
15
+ });
16
+ const placementOptionsModules = import.meta.glob("/pages/*/blocks/*.options.json", {
17
+ eager: true,
18
+ import: "default"
19
+ });
20
+ function parsePlacement(value, source, region) {
21
+ if (!isRecord(value) || typeof value.id !== "string" || !isBlockId(value.id) || typeof value.enable !== "boolean" || typeof value.placementId !== "string" || !isPlacementId(value.placementId)) {
22
+ throw new Error(`Invalid block placement in ${source}, region "${region}"`);
23
+ }
24
+ return {
25
+ id: value.id,
26
+ enable: value.enable,
27
+ placementId: value.placementId
28
+ };
29
+ }
30
+ function parsePageConfig(value, source) {
31
+ if (!isRecord(value) || value.version !== 1) {
32
+ throw new Error(`Unsupported or missing page config version in ${source}`);
33
+ }
34
+ if (!isRecord(value.blocks)) {
35
+ throw new Error(`Page config must contain a blocks object in ${source}`);
36
+ }
37
+ const blocks = {};
38
+ for (const region of pageRegions) {
39
+ const placements = value.blocks[region];
40
+ if (!Array.isArray(placements)) {
41
+ throw new TypeError(`Page config blocks.${region} must be an array in ${source}`);
42
+ }
43
+ blocks[region] = placements.map((placement) => parsePlacement(placement, source, region));
44
+ }
45
+ const options = parsePageOptions(value.options, source);
46
+ return {
47
+ version: 1,
48
+ options,
49
+ blocks
50
+ };
51
+ }
52
+ const pageConfigsByPath = /* @__PURE__ */ new Map();
53
+ const placementOptionsById = /* @__PURE__ */ new Map();
54
+ const placementIds = /* @__PURE__ */ new Set();
55
+ const referencedSchemaSources = /* @__PURE__ */ new Set();
56
+ function placementOptionsSource(pageConfigSource, placement) {
57
+ const [blockName, blockVersion] = placement.id.split("/");
58
+ return pageConfigSource.replace(
59
+ /\/config\.json$/,
60
+ `/blocks/${blockName}.${blockVersion}.${placement.placementId}.options.json`
61
+ );
62
+ }
63
+ for (const [source, value] of Object.entries(pageConfigModules)) {
64
+ const config = parsePageConfig(value, source);
65
+ const schemaSource = source.replace(/\/config\.json$/, "/schema.json");
66
+ const schema = pageSchemaModules[schemaSource];
67
+ if (!isRecord(schema) || schema.version !== 1 || !Array.isArray(schema.tabs) || !Array.isArray(schema.controls)) {
68
+ throw new Error(`Missing or invalid page schema: ${schemaSource}`);
69
+ }
70
+ referencedSchemaSources.add(schemaSource);
71
+ const duplicate = pageConfigsByPath.get(config.options.path);
72
+ if (duplicate) {
73
+ throw new Error(`Duplicate page path "${config.options.path}" in ${source}`);
74
+ }
75
+ for (const region of pageRegions) {
76
+ for (const placement of config.blocks[region]) {
77
+ if (placementIds.has(placement.placementId)) {
78
+ throw new Error(`Duplicate placementId "${placement.placementId}" in ${source}`);
79
+ }
80
+ placementIds.add(placement.placementId);
81
+ const optionsSource = placementOptionsSource(source, placement);
82
+ const optionsDocument = placementOptionsModules[optionsSource];
83
+ if (!optionsDocument) {
84
+ if (isDev()) {
85
+ placementOptionsById.set(placement.placementId, {
86
+ "element-id": `placement-${placement.placementId}`
87
+ });
88
+ continue;
89
+ }
90
+ throw new Error(`Missing placement options: ${optionsSource}`);
91
+ }
92
+ if (!isRecord(optionsDocument) || optionsDocument.version !== 1 || !isRecord(optionsDocument.options)) {
93
+ throw new Error(`Missing or invalid placement options: ${optionsSource}`);
94
+ }
95
+ placementOptionsById.set(placement.placementId, optionsDocument.options);
96
+ }
97
+ }
98
+ pageConfigsByPath.set(config.options.path, config);
99
+ }
100
+ for (const source of Object.keys(pageSchemaModules)) {
101
+ if (!referencedSchemaSources.has(source)) {
102
+ throw new Error(`Orphan page schema: ${source}`);
103
+ }
104
+ }
105
+ export function getPageConfig(path) {
106
+ return pageConfigsByPath.get(normalizePagePath(path));
107
+ }
108
+ export function getBlockPlacementOptions(placement) {
109
+ const options = placementOptionsById.get(placement.placementId);
110
+ if (!options) {
111
+ throw new Error(`Options for placement "${placement.placementId}" were not loaded`);
112
+ }
113
+ return options;
114
+ }
@@ -0,0 +1,3 @@
1
+ import type { PageOptions } from '#toaster-site-runtime/types/page';
2
+ export declare function normalizePagePath(path: string): string;
3
+ export declare function parsePageOptions(value: unknown, source: string): PageOptions;
@@ -0,0 +1,26 @@
1
+ import { isRecord } from "#toaster-site-runtime/utils/common";
2
+ export function normalizePagePath(path) {
3
+ const normalized = path.trim();
4
+ if (!normalized.startsWith("/")) {
5
+ throw new Error(`Page path must start with "/": ${path}`);
6
+ }
7
+ if (normalized.includes("?") || normalized.includes("#")) {
8
+ throw new Error(`Page path must not contain query or hash fragments: ${path}`);
9
+ }
10
+ return normalized === "/" ? normalized : normalized.replace(/\/+$/, "");
11
+ }
12
+ export function parsePageOptions(value, source) {
13
+ if (!isRecord(value) || typeof value.publish !== "boolean" || typeof value.path !== "string") {
14
+ throw new Error(`Page options must contain boolean publish and string path in ${source}`);
15
+ }
16
+ if (value.seoTitle !== void 0 && typeof value.seoTitle !== "string") {
17
+ throw new Error(`Page options seoTitle must be a string in ${source}`);
18
+ }
19
+ if (value.seoDescription !== void 0 && typeof value.seoDescription !== "string") {
20
+ throw new Error(`Page options seoDescription must be a string in ${source}`);
21
+ }
22
+ return {
23
+ ...value,
24
+ path: normalizePagePath(value.path)
25
+ };
26
+ }
@@ -0,0 +1,3 @@
1
+ export { default } from './module.mjs'
2
+
3
+ export { type ModuleOptions } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@toaster-builder/site-runtime",
3
+ "version": "0.1.0",
4
+ "description": "Shared Nuxt runtime for Toaster sites and Builder integration",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/types.d.mts",
10
+ "import": "./dist/module.mjs"
11
+ }
12
+ },
13
+ "main": "./dist/module.mjs",
14
+ "typesVersions": {
15
+ "*": {
16
+ ".": [
17
+ "./dist/types.d.mts"
18
+ ]
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "engines": {
25
+ "node": ">=22"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public",
29
+ "registry": "https://registry.npmjs.org"
30
+ },
31
+ "workspaces": [
32
+ "playground"
33
+ ],
34
+ "scripts": {
35
+ "prepack": "nuxt-module-build build",
36
+ "dev": "npm run dev:prepare && nuxt dev playground",
37
+ "dev:build": "nuxt build playground",
38
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
39
+ "release": "npm run lint && npm run test && npm run test:types && npm run prepack && changelogen --release && npm publish --access public && git push --follow-tags",
40
+ "lint": "eslint .",
41
+ "pretest": "npm run dev:prepare",
42
+ "test": "vitest run",
43
+ "test:watch": "vitest watch",
44
+ "pretest:types": "npm run dev:prepare",
45
+ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
46
+ },
47
+ "dependencies": {
48
+ "@nuxt/kit": "^4.5.2"
49
+ },
50
+ "devDependencies": {
51
+ "@nuxt/devtools": "^3.4.2",
52
+ "@nuxt/eslint-config": "^1.17.0",
53
+ "@nuxt/module-builder": "^1.0.3",
54
+ "@nuxt/schema": "^4.5.2",
55
+ "@nuxt/test-utils": "^4.3.2",
56
+ "@types/node": "latest",
57
+ "changelogen": "^0.6.2",
58
+ "eslint": "^10.10.0",
59
+ "nuxt": "^4.5.2",
60
+ "sass": "^1.105.0",
61
+ "typescript": "^5.9.3",
62
+ "vitest": "^5.0.1",
63
+ "vue-tsc": "^3.3.11"
64
+ }
65
+ }