@otl-core/block-registry 1.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.
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2025 OTL Core
2
+
3
+ Licensed under the PolyForm Shield License, Version 1.0.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may
5
+ obtain a copy of the License at
6
+
7
+ https://polyformproject.org/licenses/shield/1.0.0/
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+ License for the specific language governing permissions and limitations
13
+ under the License.
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # @otl-core/block-registry
2
+
3
+ Block registry infrastructure for OTL CMS. This package provides the core registry and rendering
4
+ system for block components.
5
+
6
+ ## Purpose
7
+
8
+ This package contains **ONLY infrastructure** - registry classes and renderers. Actual block
9
+ components (Markdown, Image, Video, etc.) remain in your application code for customization.
10
+
11
+ ## SSR Compatibility
12
+
13
+ All components in this package are **server-component safe** and work with Next.js App Router SSR:
14
+
15
+ - No client-only hooks (useState, useEffect, useMemo, etc.)
16
+ - Pure synchronous logic
17
+ - Deterministic rendering
18
+
19
+ Individual block components in your app can be client components if they need interactivity - just
20
+ add `"use client"` at the top of those files.
21
+
22
+ ## Installation
23
+
24
+ This package is part of the OTL CMS monorepo and uses workspace protocol:
25
+
26
+ ```json
27
+ {
28
+ "dependencies": {
29
+ "@otl-core/block-registry": "workspace:*"
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### 1. Create a Registry Instance
37
+
38
+ ```typescript
39
+ // In your app: src/lib/registries/block-registry.ts
40
+ import { BlockRegistry } from "@otl-core/block-registry";
41
+ import Markdown from "@/components/blocks/markdown";
42
+ import Image from "@/components/blocks/image";
43
+ // ... import all your block components
44
+
45
+ export const blockRegistry = new BlockRegistry();
46
+
47
+ // Register your blocks
48
+ blockRegistry.register("markdown", Markdown);
49
+ blockRegistry.register("image", Image);
50
+ // ... register all blocks
51
+ ```
52
+
53
+ ### 2. Use BlockRenderer
54
+
55
+ ```typescript
56
+ import { BlockRenderer } from '@otl-core/block-registry';
57
+ import { blockRegistry } from '@/lib/registries/block-registry';
58
+
59
+ export default function MyPage({ blocks }) {
60
+ return (
61
+ <div>
62
+ {blocks.map((block) => (
63
+ <BlockRenderer
64
+ key={block.id}
65
+ block={block}
66
+ blockRegistry={blockRegistry}
67
+ siteId="your-site-id" // Optional, for form blocks
68
+ />
69
+ ))}
70
+ </div>
71
+ );
72
+ }
73
+ ```
74
+
75
+ ### 3. Form Block Support
76
+
77
+ Form blocks receive special props:
78
+
79
+ ```typescript
80
+ // Your form block component
81
+ interface FormInputProps {
82
+ blockId: string;
83
+ siteId?: string;
84
+ }
85
+
86
+ export default function FormInput({ blockId, siteId }: FormInputProps) {
87
+ // Your form block implementation
88
+ }
89
+
90
+ // Register it with type starting with "form-"
91
+ blockRegistry.register("form-input", FormInput);
92
+ ```
93
+
94
+ The BlockRenderer automatically detects form blocks (types starting with `"form-"`) and passes
95
+ `blockId` and `siteId` instead of `config`.
96
+
97
+ ## API Reference
98
+
99
+ ### BlockRegistry
100
+
101
+ ```typescript
102
+ class BlockRegistry<TProps> {
103
+ register(type: string, component: ComponentType<TProps>): void;
104
+ get(type: string): ComponentType<TProps> | undefined;
105
+ has(type: string): boolean;
106
+ getAll(): string[];
107
+ size(): number;
108
+ }
109
+ ```
110
+
111
+ ### BlockRenderer Props
112
+
113
+ ```typescript
114
+ interface BlockRendererProps {
115
+ block: BlockInstance; // The block to render
116
+ blockRegistry: BlockRegistry; // Registry containing block components
117
+ siteId?: string; // Optional, for form blocks
118
+ }
119
+ ```
120
+
121
+ ### Block Component Props
122
+
123
+ Regular blocks receive:
124
+
125
+ ```typescript
126
+ interface BlockComponentProps {
127
+ config: Record<string, unknown>;
128
+ }
129
+ ```
130
+
131
+ Form blocks receive:
132
+
133
+ ```typescript
134
+ interface FormBlockComponentProps {
135
+ blockId: string;
136
+ siteId?: string;
137
+ }
138
+ ```
139
+
140
+ ## Error Handling
141
+
142
+ If a block type is not found in the registry, `ComponentNotFound` is rendered, which:
143
+
144
+ - Logs detailed error information in development
145
+ - Logs minimal error in production
146
+ - Calls global error handler if available (`window.__CMS_ERROR_HANDLER__`)
147
+ - Renders invisible placeholder to prevent layout breaks
148
+
149
+ ## Best Practices
150
+
151
+ 1. **Keep components in your app**: Don't put actual block components in this package
152
+ 2. **Server-first**: Use server components by default, only add `"use client"` when needed
153
+ 3. **Type safety**: Import types from this package for consistency
154
+ 4. **Registry singleton**: Create one registry instance and export it
155
+ 5. **Register all blocks**: Ensure all block types used in content are registered
156
+
157
+ ## Examples
158
+
159
+ See `frontend/engine/examples/custom-block-example.tsx` for a complete example.
package/dist/index.cjs ADDED
@@ -0,0 +1,150 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+
5
+ // src/registry/block-registry.ts
6
+ var BlockRegistry = class {
7
+ constructor() {
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ this.components = /* @__PURE__ */ new Map();
10
+ }
11
+ /**
12
+ * Register a block component
13
+ */
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ register(type, component) {
16
+ this.components.set(type, component);
17
+ }
18
+ /**
19
+ * Get a block component by type
20
+ */
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ get(type) {
23
+ return this.components.get(type);
24
+ }
25
+ /**
26
+ * Check if a block type is registered
27
+ */
28
+ has(type) {
29
+ return this.components.has(type);
30
+ }
31
+ /**
32
+ * Get all registered block types
33
+ */
34
+ getAll() {
35
+ return Array.from(this.components.keys());
36
+ }
37
+ /**
38
+ * Get count of registered components
39
+ */
40
+ size() {
41
+ return this.components.size;
42
+ }
43
+ };
44
+ function ComponentNotFound({
45
+ type,
46
+ config,
47
+ availableTypes,
48
+ componentKind
49
+ }) {
50
+ const isDev = process.env.NODE_ENV === "development";
51
+ const errorDetails = {
52
+ componentKind,
53
+ type,
54
+ config,
55
+ availableTypes,
56
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
57
+ suggestion: `Create components/${componentKind}s/${type}.tsx and register it in lib/registries/${componentKind}-registry.ts`
58
+ };
59
+ if (isDev) {
60
+ console.error(
61
+ `[${componentKind === "section" ? "SectionRenderer" : "BlockRenderer"}] Component not found: "${type}"`,
62
+ errorDetails
63
+ );
64
+ } else {
65
+ console.error(`[CMS] Unknown ${componentKind}: "${type}"`);
66
+ }
67
+ if (typeof window !== "undefined" && window.__CMS_ERROR_HANDLER__) {
68
+ try {
69
+ window.__CMS_ERROR_HANDLER__({
70
+ errorType: "COMPONENT_NOT_FOUND",
71
+ severity: "warning",
72
+ ...errorDetails
73
+ });
74
+ } catch {
75
+ }
76
+ }
77
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { minHeight: "1px" }, "aria-hidden": "true" });
78
+ }
79
+ var globalAnalyticsWrapper = null;
80
+ function registerAnalyticsWrapper(wrapper) {
81
+ globalAnalyticsWrapper = wrapper;
82
+ }
83
+ function BlockRenderer({
84
+ block,
85
+ blockRegistry,
86
+ siteId,
87
+ analyticsWrapper
88
+ }) {
89
+ const AnalyticsWrapper = analyticsWrapper ?? globalAnalyticsWrapper;
90
+ const { type, id } = block;
91
+ const BlockComponent = blockRegistry.get(type);
92
+ if (!BlockComponent) {
93
+ const config2 = "config" in block && block.config !== void 0 ? block.config : "data" in block && block.data !== void 0 ? block.data : {};
94
+ return /* @__PURE__ */ jsxRuntime.jsx(
95
+ ComponentNotFound,
96
+ {
97
+ type,
98
+ config: config2,
99
+ availableTypes: blockRegistry.getAll(),
100
+ componentKind: "block"
101
+ }
102
+ );
103
+ }
104
+ const blockAnalytics = block.config?.analytics;
105
+ const wrapWithAnalytics = (element) => {
106
+ if (AnalyticsWrapper) {
107
+ return /* @__PURE__ */ jsxRuntime.jsx(
108
+ AnalyticsWrapper,
109
+ {
110
+ analyticsConfig: blockAnalytics,
111
+ blockId: id,
112
+ blockType: type,
113
+ children: element
114
+ }
115
+ );
116
+ }
117
+ return element;
118
+ };
119
+ if (type.startsWith("form-")) {
120
+ return wrapWithAnalytics(/* @__PURE__ */ jsxRuntime.jsx(BlockComponent, { blockId: id, siteId }));
121
+ }
122
+ if (type === "form") {
123
+ const config2 = block.config || {};
124
+ const data = block.data;
125
+ return wrapWithAnalytics(
126
+ /* @__PURE__ */ jsxRuntime.jsx(BlockComponent, { config: config2, data, siteId })
127
+ );
128
+ }
129
+ const config = "config" in block && block.config !== void 0 ? block.config : "data" in block && block.data !== void 0 ? block.data : {};
130
+ if (type === "alert" || type === "card" || type === "modal" || type === "grid-layout" || type === "flexbox-layout" || type === "container-layout" || type === "entry-content") {
131
+ return wrapWithAnalytics(
132
+ /* @__PURE__ */ jsxRuntime.jsx(
133
+ BlockComponent,
134
+ {
135
+ config,
136
+ siteId,
137
+ blockRegistry
138
+ }
139
+ )
140
+ );
141
+ }
142
+ return wrapWithAnalytics(/* @__PURE__ */ jsxRuntime.jsx(BlockComponent, { config, siteId }));
143
+ }
144
+
145
+ exports.BlockRegistry = BlockRegistry;
146
+ exports.BlockRenderer = BlockRenderer;
147
+ exports.ComponentNotFound = ComponentNotFound;
148
+ exports.registerAnalyticsWrapper = registerAnalyticsWrapper;
149
+ //# sourceMappingURL=index.cjs.map
150
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/registry/block-registry.ts","../src/renderer/component-not-found.tsx","../src/renderer/block-renderer.tsx"],"names":["jsx","config"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Block Component Registry\n * Manages registration and retrieval of block components\n */\n\nimport { ComponentType } from \"react\";\n\nexport class BlockRegistry {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private components = new Map<string, ComponentType<any>>();\n\n /**\n * Register a block component\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n register(type: string, component: ComponentType<any>): void {\n this.components.set(type, component);\n }\n\n /**\n * Get a block component by type\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n get(type: string): ComponentType<any> | undefined {\n return this.components.get(type);\n }\n\n /**\n * Check if a block type is registered\n */\n has(type: string): boolean {\n return this.components.has(type);\n }\n\n /**\n * Get all registered block types\n */\n getAll(): string[] {\n return Array.from(this.components.keys());\n }\n\n /**\n * Get count of registered components\n */\n size(): number {\n return this.components.size;\n }\n}\n","/**\n * Component Not Found Fallback\n * Logs error when a component type is not registered in the registry\n */\n\ninterface ComponentNotFoundProps {\n type: string;\n config: Record<string, unknown>;\n availableTypes: string[];\n componentKind: \"section\" | \"block\";\n}\n\nexport default function ComponentNotFound({\n type,\n config,\n availableTypes,\n componentKind,\n}: ComponentNotFoundProps) {\n const isDev = process.env.NODE_ENV === \"development\";\n\n const errorDetails = {\n componentKind,\n type,\n config,\n availableTypes,\n timestamp: new Date().toISOString(),\n suggestion: `Create components/${componentKind}s/${type}.tsx and register it in lib/registries/${componentKind}-registry.ts`,\n };\n\n if (isDev) {\n console.error(\n `[${componentKind === \"section\" ? \"SectionRenderer\" : \"BlockRenderer\"}] Component not found: \"${type}\"`,\n errorDetails,\n );\n } else {\n console.error(`[CMS] Unknown ${componentKind}: \"${type}\"`);\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if (typeof window !== \"undefined\" && (window as any).__CMS_ERROR_HANDLER__) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (window as any).__CMS_ERROR_HANDLER__({\n errorType: \"COMPONENT_NOT_FOUND\",\n severity: \"warning\",\n ...errorDetails,\n });\n } catch {\n // Ignore errors from error handler\n }\n }\n\n return <div style={{ minHeight: \"1px\" }} aria-hidden=\"true\" />;\n}\n","/**\n * Block Renderer\n * Dynamically renders block components based on schema instance type\n *\n * SSR COMPATIBLE: This is a pure server component with no client hooks.\n * Analytics wrapper is a client component that is rendered by this server\n * component (valid in Next.js RSC architecture).\n */\n\nimport { BlockRegistry } from \"../registry/block-registry\";\nimport ComponentNotFound from \"./component-not-found\";\n\ninterface BlockAnalyticsConfig {\n enabled: boolean;\n event_label: string;\n track_type: \"click\" | \"visibility\" | \"both\";\n visibility_threshold?: number;\n fire_once?: boolean;\n target_providers?: \"all\" | string[];\n custom_params?: Record<string, string>;\n}\n\ntype AnalyticsWrapperComponent = React.ComponentType<{\n analyticsConfig: BlockAnalyticsConfig | undefined;\n blockId: string;\n blockType: string;\n children: React.ReactNode;\n}>;\n\nlet globalAnalyticsWrapper: AnalyticsWrapperComponent | null = null;\n\n/**\n * Register a global analytics wrapper component that will be applied to\n * all blocks rendered by BlockRenderer. Call once during app initialization.\n */\nexport function registerAnalyticsWrapper(\n wrapper: AnalyticsWrapperComponent,\n): void {\n globalAnalyticsWrapper = wrapper;\n}\n\ninterface BlockInstance {\n id: string;\n type: string;\n config?: Record<string, unknown>;\n data?: Record<string, unknown>;\n}\n\ninterface BlockRendererProps {\n block: BlockInstance;\n blockRegistry: BlockRegistry;\n siteId?: string;\n /** Optional wrapper component for block-level analytics (overrides global) */\n analyticsWrapper?: AnalyticsWrapperComponent;\n}\n\n// NO \"use client\" directive - this is a server component\nexport default function BlockRenderer({\n block,\n blockRegistry,\n siteId,\n analyticsWrapper,\n}: BlockRendererProps) {\n const AnalyticsWrapper = analyticsWrapper ?? globalAnalyticsWrapper;\n const { type, id } = block;\n\n const BlockComponent = blockRegistry.get(type);\n\n if (!BlockComponent) {\n const config =\n \"config\" in block && block.config !== undefined\n ? block.config\n : \"data\" in block && block.data !== undefined\n ? block.data\n : {};\n\n return (\n <ComponentNotFound\n type={type}\n config={config}\n availableTypes={blockRegistry.getAll()}\n componentKind=\"block\"\n />\n );\n }\n\n // Extract analytics config from the block config\n const blockAnalytics = block.config?.analytics as\n | BlockAnalyticsConfig\n | undefined;\n\n // Helper to wrap rendered block with analytics if wrapper is provided\n const wrapWithAnalytics = (element: React.ReactElement) => {\n if (AnalyticsWrapper) {\n return (\n <AnalyticsWrapper\n analyticsConfig={blockAnalytics}\n blockId={id}\n blockType={type}\n >\n {element}\n </AnalyticsWrapper>\n );\n }\n return element;\n };\n\n // Form blocks get blockId, siteId, and their embedded data\n if (type.startsWith(\"form-\")) {\n return wrapWithAnalytics(<BlockComponent blockId={id} siteId={siteId} />);\n }\n\n // Special handling for the \"form\" block (embedded form)\n // This block type receives both config AND data\n if (type === \"form\") {\n const config = block.config || {};\n const data = block.data; // This contains FormBlockData from backend\n return wrapWithAnalytics(\n <BlockComponent config={config} data={data} siteId={siteId} />,\n );\n }\n\n // Regular blocks get config (or data if config is not present)\n const config =\n \"config\" in block && block.config !== undefined\n ? block.config\n : \"data\" in block && block.data !== undefined\n ? block.data\n : {};\n\n // Blocks that can contain nested blocks need the blockRegistry and siteId\n if (\n type === \"alert\" ||\n type === \"card\" ||\n type === \"modal\" ||\n type === \"grid-layout\" ||\n type === \"flexbox-layout\" ||\n type === \"container-layout\" ||\n type === \"entry-content\"\n ) {\n return wrapWithAnalytics(\n <BlockComponent\n config={config}\n siteId={siteId}\n blockRegistry={blockRegistry}\n />,\n );\n }\n\n return wrapWithAnalytics(<BlockComponent config={config} siteId={siteId} />);\n}\n"]}
@@ -0,0 +1,94 @@
1
+ import { ComponentType } from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+
4
+ /**
5
+ * Block Component Registry
6
+ * Manages registration and retrieval of block components
7
+ */
8
+
9
+ declare class BlockRegistry {
10
+ private components;
11
+ /**
12
+ * Register a block component
13
+ */
14
+ register(type: string, component: ComponentType<any>): void;
15
+ /**
16
+ * Get a block component by type
17
+ */
18
+ get(type: string): ComponentType<any> | undefined;
19
+ /**
20
+ * Check if a block type is registered
21
+ */
22
+ has(type: string): boolean;
23
+ /**
24
+ * Get all registered block types
25
+ */
26
+ getAll(): string[];
27
+ /**
28
+ * Get count of registered components
29
+ */
30
+ size(): number;
31
+ }
32
+
33
+ /**
34
+ * Type definitions for Block Registry
35
+ */
36
+
37
+ interface BlockComponentProps<TConfig = Record<string, unknown>> {
38
+ config: TConfig;
39
+ siteId?: string;
40
+ }
41
+ interface FormBlockComponentProps {
42
+ blockId: string;
43
+ siteId?: string;
44
+ }
45
+ type BlockComponent = ComponentType<BlockComponentProps> | ComponentType<FormBlockComponentProps>;
46
+
47
+ interface BlockAnalyticsConfig {
48
+ enabled: boolean;
49
+ event_label: string;
50
+ track_type: "click" | "visibility" | "both";
51
+ visibility_threshold?: number;
52
+ fire_once?: boolean;
53
+ target_providers?: "all" | string[];
54
+ custom_params?: Record<string, string>;
55
+ }
56
+ type AnalyticsWrapperComponent = React.ComponentType<{
57
+ analyticsConfig: BlockAnalyticsConfig | undefined;
58
+ blockId: string;
59
+ blockType: string;
60
+ children: React.ReactNode;
61
+ }>;
62
+ /**
63
+ * Register a global analytics wrapper component that will be applied to
64
+ * all blocks rendered by BlockRenderer. Call once during app initialization.
65
+ */
66
+ declare function registerAnalyticsWrapper(wrapper: AnalyticsWrapperComponent): void;
67
+ interface BlockInstance {
68
+ id: string;
69
+ type: string;
70
+ config?: Record<string, unknown>;
71
+ data?: Record<string, unknown>;
72
+ }
73
+ interface BlockRendererProps {
74
+ block: BlockInstance;
75
+ blockRegistry: BlockRegistry;
76
+ siteId?: string;
77
+ /** Optional wrapper component for block-level analytics (overrides global) */
78
+ analyticsWrapper?: AnalyticsWrapperComponent;
79
+ }
80
+ declare function BlockRenderer({ block, blockRegistry, siteId, analyticsWrapper, }: BlockRendererProps): react_jsx_runtime.JSX.Element;
81
+
82
+ /**
83
+ * Component Not Found Fallback
84
+ * Logs error when a component type is not registered in the registry
85
+ */
86
+ interface ComponentNotFoundProps {
87
+ type: string;
88
+ config: Record<string, unknown>;
89
+ availableTypes: string[];
90
+ componentKind: "section" | "block";
91
+ }
92
+ declare function ComponentNotFound({ type, config, availableTypes, componentKind, }: ComponentNotFoundProps): react_jsx_runtime.JSX.Element;
93
+
94
+ export { type BlockComponent, type BlockComponentProps, BlockRegistry, BlockRenderer, ComponentNotFound, type FormBlockComponentProps, registerAnalyticsWrapper };
@@ -0,0 +1,94 @@
1
+ import { ComponentType } from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+
4
+ /**
5
+ * Block Component Registry
6
+ * Manages registration and retrieval of block components
7
+ */
8
+
9
+ declare class BlockRegistry {
10
+ private components;
11
+ /**
12
+ * Register a block component
13
+ */
14
+ register(type: string, component: ComponentType<any>): void;
15
+ /**
16
+ * Get a block component by type
17
+ */
18
+ get(type: string): ComponentType<any> | undefined;
19
+ /**
20
+ * Check if a block type is registered
21
+ */
22
+ has(type: string): boolean;
23
+ /**
24
+ * Get all registered block types
25
+ */
26
+ getAll(): string[];
27
+ /**
28
+ * Get count of registered components
29
+ */
30
+ size(): number;
31
+ }
32
+
33
+ /**
34
+ * Type definitions for Block Registry
35
+ */
36
+
37
+ interface BlockComponentProps<TConfig = Record<string, unknown>> {
38
+ config: TConfig;
39
+ siteId?: string;
40
+ }
41
+ interface FormBlockComponentProps {
42
+ blockId: string;
43
+ siteId?: string;
44
+ }
45
+ type BlockComponent = ComponentType<BlockComponentProps> | ComponentType<FormBlockComponentProps>;
46
+
47
+ interface BlockAnalyticsConfig {
48
+ enabled: boolean;
49
+ event_label: string;
50
+ track_type: "click" | "visibility" | "both";
51
+ visibility_threshold?: number;
52
+ fire_once?: boolean;
53
+ target_providers?: "all" | string[];
54
+ custom_params?: Record<string, string>;
55
+ }
56
+ type AnalyticsWrapperComponent = React.ComponentType<{
57
+ analyticsConfig: BlockAnalyticsConfig | undefined;
58
+ blockId: string;
59
+ blockType: string;
60
+ children: React.ReactNode;
61
+ }>;
62
+ /**
63
+ * Register a global analytics wrapper component that will be applied to
64
+ * all blocks rendered by BlockRenderer. Call once during app initialization.
65
+ */
66
+ declare function registerAnalyticsWrapper(wrapper: AnalyticsWrapperComponent): void;
67
+ interface BlockInstance {
68
+ id: string;
69
+ type: string;
70
+ config?: Record<string, unknown>;
71
+ data?: Record<string, unknown>;
72
+ }
73
+ interface BlockRendererProps {
74
+ block: BlockInstance;
75
+ blockRegistry: BlockRegistry;
76
+ siteId?: string;
77
+ /** Optional wrapper component for block-level analytics (overrides global) */
78
+ analyticsWrapper?: AnalyticsWrapperComponent;
79
+ }
80
+ declare function BlockRenderer({ block, blockRegistry, siteId, analyticsWrapper, }: BlockRendererProps): react_jsx_runtime.JSX.Element;
81
+
82
+ /**
83
+ * Component Not Found Fallback
84
+ * Logs error when a component type is not registered in the registry
85
+ */
86
+ interface ComponentNotFoundProps {
87
+ type: string;
88
+ config: Record<string, unknown>;
89
+ availableTypes: string[];
90
+ componentKind: "section" | "block";
91
+ }
92
+ declare function ComponentNotFound({ type, config, availableTypes, componentKind, }: ComponentNotFoundProps): react_jsx_runtime.JSX.Element;
93
+
94
+ export { type BlockComponent, type BlockComponentProps, BlockRegistry, BlockRenderer, ComponentNotFound, type FormBlockComponentProps, registerAnalyticsWrapper };
package/dist/index.js ADDED
@@ -0,0 +1,145 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+
3
+ // src/registry/block-registry.ts
4
+ var BlockRegistry = class {
5
+ constructor() {
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ this.components = /* @__PURE__ */ new Map();
8
+ }
9
+ /**
10
+ * Register a block component
11
+ */
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ register(type, component) {
14
+ this.components.set(type, component);
15
+ }
16
+ /**
17
+ * Get a block component by type
18
+ */
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ get(type) {
21
+ return this.components.get(type);
22
+ }
23
+ /**
24
+ * Check if a block type is registered
25
+ */
26
+ has(type) {
27
+ return this.components.has(type);
28
+ }
29
+ /**
30
+ * Get all registered block types
31
+ */
32
+ getAll() {
33
+ return Array.from(this.components.keys());
34
+ }
35
+ /**
36
+ * Get count of registered components
37
+ */
38
+ size() {
39
+ return this.components.size;
40
+ }
41
+ };
42
+ function ComponentNotFound({
43
+ type,
44
+ config,
45
+ availableTypes,
46
+ componentKind
47
+ }) {
48
+ const isDev = process.env.NODE_ENV === "development";
49
+ const errorDetails = {
50
+ componentKind,
51
+ type,
52
+ config,
53
+ availableTypes,
54
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
55
+ suggestion: `Create components/${componentKind}s/${type}.tsx and register it in lib/registries/${componentKind}-registry.ts`
56
+ };
57
+ if (isDev) {
58
+ console.error(
59
+ `[${componentKind === "section" ? "SectionRenderer" : "BlockRenderer"}] Component not found: "${type}"`,
60
+ errorDetails
61
+ );
62
+ } else {
63
+ console.error(`[CMS] Unknown ${componentKind}: "${type}"`);
64
+ }
65
+ if (typeof window !== "undefined" && window.__CMS_ERROR_HANDLER__) {
66
+ try {
67
+ window.__CMS_ERROR_HANDLER__({
68
+ errorType: "COMPONENT_NOT_FOUND",
69
+ severity: "warning",
70
+ ...errorDetails
71
+ });
72
+ } catch {
73
+ }
74
+ }
75
+ return /* @__PURE__ */ jsx("div", { style: { minHeight: "1px" }, "aria-hidden": "true" });
76
+ }
77
+ var globalAnalyticsWrapper = null;
78
+ function registerAnalyticsWrapper(wrapper) {
79
+ globalAnalyticsWrapper = wrapper;
80
+ }
81
+ function BlockRenderer({
82
+ block,
83
+ blockRegistry,
84
+ siteId,
85
+ analyticsWrapper
86
+ }) {
87
+ const AnalyticsWrapper = analyticsWrapper ?? globalAnalyticsWrapper;
88
+ const { type, id } = block;
89
+ const BlockComponent = blockRegistry.get(type);
90
+ if (!BlockComponent) {
91
+ const config2 = "config" in block && block.config !== void 0 ? block.config : "data" in block && block.data !== void 0 ? block.data : {};
92
+ return /* @__PURE__ */ jsx(
93
+ ComponentNotFound,
94
+ {
95
+ type,
96
+ config: config2,
97
+ availableTypes: blockRegistry.getAll(),
98
+ componentKind: "block"
99
+ }
100
+ );
101
+ }
102
+ const blockAnalytics = block.config?.analytics;
103
+ const wrapWithAnalytics = (element) => {
104
+ if (AnalyticsWrapper) {
105
+ return /* @__PURE__ */ jsx(
106
+ AnalyticsWrapper,
107
+ {
108
+ analyticsConfig: blockAnalytics,
109
+ blockId: id,
110
+ blockType: type,
111
+ children: element
112
+ }
113
+ );
114
+ }
115
+ return element;
116
+ };
117
+ if (type.startsWith("form-")) {
118
+ return wrapWithAnalytics(/* @__PURE__ */ jsx(BlockComponent, { blockId: id, siteId }));
119
+ }
120
+ if (type === "form") {
121
+ const config2 = block.config || {};
122
+ const data = block.data;
123
+ return wrapWithAnalytics(
124
+ /* @__PURE__ */ jsx(BlockComponent, { config: config2, data, siteId })
125
+ );
126
+ }
127
+ const config = "config" in block && block.config !== void 0 ? block.config : "data" in block && block.data !== void 0 ? block.data : {};
128
+ if (type === "alert" || type === "card" || type === "modal" || type === "grid-layout" || type === "flexbox-layout" || type === "container-layout" || type === "entry-content") {
129
+ return wrapWithAnalytics(
130
+ /* @__PURE__ */ jsx(
131
+ BlockComponent,
132
+ {
133
+ config,
134
+ siteId,
135
+ blockRegistry
136
+ }
137
+ )
138
+ );
139
+ }
140
+ return wrapWithAnalytics(/* @__PURE__ */ jsx(BlockComponent, { config, siteId }));
141
+ }
142
+
143
+ export { BlockRegistry, BlockRenderer, ComponentNotFound, registerAnalyticsWrapper };
144
+ //# sourceMappingURL=index.js.map
145
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/registry/block-registry.ts","../src/renderer/component-not-found.tsx","../src/renderer/block-renderer.tsx"],"names":["config","jsx"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Block Component Registry\n * Manages registration and retrieval of block components\n */\n\nimport { ComponentType } from \"react\";\n\nexport class BlockRegistry {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private components = new Map<string, ComponentType<any>>();\n\n /**\n * Register a block component\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n register(type: string, component: ComponentType<any>): void {\n this.components.set(type, component);\n }\n\n /**\n * Get a block component by type\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n get(type: string): ComponentType<any> | undefined {\n return this.components.get(type);\n }\n\n /**\n * Check if a block type is registered\n */\n has(type: string): boolean {\n return this.components.has(type);\n }\n\n /**\n * Get all registered block types\n */\n getAll(): string[] {\n return Array.from(this.components.keys());\n }\n\n /**\n * Get count of registered components\n */\n size(): number {\n return this.components.size;\n }\n}\n","/**\n * Component Not Found Fallback\n * Logs error when a component type is not registered in the registry\n */\n\ninterface ComponentNotFoundProps {\n type: string;\n config: Record<string, unknown>;\n availableTypes: string[];\n componentKind: \"section\" | \"block\";\n}\n\nexport default function ComponentNotFound({\n type,\n config,\n availableTypes,\n componentKind,\n}: ComponentNotFoundProps) {\n const isDev = process.env.NODE_ENV === \"development\";\n\n const errorDetails = {\n componentKind,\n type,\n config,\n availableTypes,\n timestamp: new Date().toISOString(),\n suggestion: `Create components/${componentKind}s/${type}.tsx and register it in lib/registries/${componentKind}-registry.ts`,\n };\n\n if (isDev) {\n console.error(\n `[${componentKind === \"section\" ? \"SectionRenderer\" : \"BlockRenderer\"}] Component not found: \"${type}\"`,\n errorDetails,\n );\n } else {\n console.error(`[CMS] Unknown ${componentKind}: \"${type}\"`);\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if (typeof window !== \"undefined\" && (window as any).__CMS_ERROR_HANDLER__) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (window as any).__CMS_ERROR_HANDLER__({\n errorType: \"COMPONENT_NOT_FOUND\",\n severity: \"warning\",\n ...errorDetails,\n });\n } catch {\n // Ignore errors from error handler\n }\n }\n\n return <div style={{ minHeight: \"1px\" }} aria-hidden=\"true\" />;\n}\n","/**\n * Block Renderer\n * Dynamically renders block components based on schema instance type\n *\n * SSR COMPATIBLE: This is a pure server component with no client hooks.\n * Analytics wrapper is a client component that is rendered by this server\n * component (valid in Next.js RSC architecture).\n */\n\nimport { BlockRegistry } from \"../registry/block-registry\";\nimport ComponentNotFound from \"./component-not-found\";\n\ninterface BlockAnalyticsConfig {\n enabled: boolean;\n event_label: string;\n track_type: \"click\" | \"visibility\" | \"both\";\n visibility_threshold?: number;\n fire_once?: boolean;\n target_providers?: \"all\" | string[];\n custom_params?: Record<string, string>;\n}\n\ntype AnalyticsWrapperComponent = React.ComponentType<{\n analyticsConfig: BlockAnalyticsConfig | undefined;\n blockId: string;\n blockType: string;\n children: React.ReactNode;\n}>;\n\nlet globalAnalyticsWrapper: AnalyticsWrapperComponent | null = null;\n\n/**\n * Register a global analytics wrapper component that will be applied to\n * all blocks rendered by BlockRenderer. Call once during app initialization.\n */\nexport function registerAnalyticsWrapper(\n wrapper: AnalyticsWrapperComponent,\n): void {\n globalAnalyticsWrapper = wrapper;\n}\n\ninterface BlockInstance {\n id: string;\n type: string;\n config?: Record<string, unknown>;\n data?: Record<string, unknown>;\n}\n\ninterface BlockRendererProps {\n block: BlockInstance;\n blockRegistry: BlockRegistry;\n siteId?: string;\n /** Optional wrapper component for block-level analytics (overrides global) */\n analyticsWrapper?: AnalyticsWrapperComponent;\n}\n\n// NO \"use client\" directive - this is a server component\nexport default function BlockRenderer({\n block,\n blockRegistry,\n siteId,\n analyticsWrapper,\n}: BlockRendererProps) {\n const AnalyticsWrapper = analyticsWrapper ?? globalAnalyticsWrapper;\n const { type, id } = block;\n\n const BlockComponent = blockRegistry.get(type);\n\n if (!BlockComponent) {\n const config =\n \"config\" in block && block.config !== undefined\n ? block.config\n : \"data\" in block && block.data !== undefined\n ? block.data\n : {};\n\n return (\n <ComponentNotFound\n type={type}\n config={config}\n availableTypes={blockRegistry.getAll()}\n componentKind=\"block\"\n />\n );\n }\n\n // Extract analytics config from the block config\n const blockAnalytics = block.config?.analytics as\n | BlockAnalyticsConfig\n | undefined;\n\n // Helper to wrap rendered block with analytics if wrapper is provided\n const wrapWithAnalytics = (element: React.ReactElement) => {\n if (AnalyticsWrapper) {\n return (\n <AnalyticsWrapper\n analyticsConfig={blockAnalytics}\n blockId={id}\n blockType={type}\n >\n {element}\n </AnalyticsWrapper>\n );\n }\n return element;\n };\n\n // Form blocks get blockId, siteId, and their embedded data\n if (type.startsWith(\"form-\")) {\n return wrapWithAnalytics(<BlockComponent blockId={id} siteId={siteId} />);\n }\n\n // Special handling for the \"form\" block (embedded form)\n // This block type receives both config AND data\n if (type === \"form\") {\n const config = block.config || {};\n const data = block.data; // This contains FormBlockData from backend\n return wrapWithAnalytics(\n <BlockComponent config={config} data={data} siteId={siteId} />,\n );\n }\n\n // Regular blocks get config (or data if config is not present)\n const config =\n \"config\" in block && block.config !== undefined\n ? block.config\n : \"data\" in block && block.data !== undefined\n ? block.data\n : {};\n\n // Blocks that can contain nested blocks need the blockRegistry and siteId\n if (\n type === \"alert\" ||\n type === \"card\" ||\n type === \"modal\" ||\n type === \"grid-layout\" ||\n type === \"flexbox-layout\" ||\n type === \"container-layout\" ||\n type === \"entry-content\"\n ) {\n return wrapWithAnalytics(\n <BlockComponent\n config={config}\n siteId={siteId}\n blockRegistry={blockRegistry}\n />,\n );\n }\n\n return wrapWithAnalytics(<BlockComponent config={config} siteId={siteId} />);\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@otl-core/block-registry",
3
+ "version": "1.1.0",
4
+ "type": "module",
5
+ "description": "Block registry and renderer for OTL CMS",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "clean": "rm -rf dist",
22
+ "rebuild": "npm run clean && npm run build"
23
+ },
24
+ "keywords": [
25
+ "blocks",
26
+ "registry",
27
+ "react",
28
+ "cms"
29
+ ],
30
+ "author": "OTL Core",
31
+ "license": "PolyForm-Shield-1.0.0",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/otl-core/block-registry.git"
35
+ },
36
+ "dependencies": {
37
+ "@otl-core/cms-types": "^1.1.0"
38
+ },
39
+ "peerDependencies": {
40
+ "react": "^18.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^25.3.3",
44
+ "@types/react": "^19.0.0",
45
+ "react": "^19.0.0",
46
+ "tsup": "^8.0.0",
47
+ "typescript": "^5.0.0"
48
+ }
49
+ }