@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 +13 -0
- package/README.md +159 -0
- package/dist/index.cjs +150 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +94 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +145 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|