cms-renderer 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/block-renderer.js +10 -53
- package/dist/lib/block-renderer.js.map +1 -1
- package/dist/lib/client-editable-block.d.ts +23 -0
- package/dist/lib/client-editable-block.js +93 -0
- package/dist/lib/client-editable-block.js.map +1 -0
- package/dist/lib/cms-api.js.map +1 -1
- package/dist/lib/custom-schemas.js +165 -161
- package/dist/lib/custom-schemas.js.map +1 -1
- package/dist/lib/refresher.d.ts +9 -0
- package/dist/lib/refresher.js +47 -0
- package/dist/lib/refresher.js.map +1 -0
- package/dist/lib/renderer.d.ts +1 -6
- package/dist/lib/renderer.js +10 -57
- package/dist/lib/renderer.js.map +1 -1
- package/dist/lib/schema.js +1 -1
- package/dist/lib/schema.js.map +1 -1
- package/package.json +10 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// lib/block-renderer.tsx
|
|
2
2
|
import React from "react";
|
|
3
|
+
import { ClientEditableBlock } from "./client-editable-block.js";
|
|
3
4
|
import { BlockToolbar } from "./block-toolbar.js";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
function walkReactNode(node, visitors, ctx = {}) {
|
|
@@ -267,63 +268,19 @@ function BlockRenderer({
|
|
|
267
268
|
});
|
|
268
269
|
}
|
|
269
270
|
})();
|
|
270
|
-
${contentEntries.length > 0 ? `
|
|
271
|
-
(function() {
|
|
272
|
-
var blockId = ${JSON.stringify(block.id)};
|
|
273
|
-
var blockType = ${JSON.stringify(block.type)};
|
|
274
|
-
var entries = ${JSON.stringify(contentEntries)};
|
|
275
|
-
|
|
276
|
-
function injectSpans() {
|
|
277
|
-
var block = document.querySelector('[data-block-id="' + blockId + '"]');
|
|
278
|
-
if (!block || block.getAttribute('data-cms-spans-injected')) return;
|
|
279
|
-
block.setAttribute('data-cms-spans-injected', 'true');
|
|
280
|
-
|
|
281
|
-
var used = {};
|
|
282
|
-
var walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT, null);
|
|
283
|
-
var textNodes = [];
|
|
284
|
-
var n;
|
|
285
|
-
while (n = walker.nextNode()) {
|
|
286
|
-
if (n.parentNode && n.parentNode.closest && n.parentNode.closest('.cms-block-toolbar')) continue;
|
|
287
|
-
if (n.nodeValue && n.nodeValue.trim()) textNodes.push(n);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
for (var i = 0; i < textNodes.length; i++) {
|
|
291
|
-
var node = textNodes[i];
|
|
292
|
-
var text = node.nodeValue;
|
|
293
|
-
if (!text) continue;
|
|
294
|
-
|
|
295
|
-
for (var j = 0; j < entries.length; j++) {
|
|
296
|
-
var e = entries[j];
|
|
297
|
-
if (used[e.p]) continue;
|
|
298
|
-
if (text.indexOf(e.v) !== -1 && text.trim() === e.v.trim()) {
|
|
299
|
-
used[e.p] = true;
|
|
300
|
-
var span = document.createElement('span');
|
|
301
|
-
span.setAttribute('data-cms-editable', '');
|
|
302
|
-
span.setAttribute('data-block-id', blockId);
|
|
303
|
-
span.setAttribute('data-block-type', blockType);
|
|
304
|
-
span.setAttribute('data-content-path', e.p);
|
|
305
|
-
node.parentNode.insertBefore(span, node);
|
|
306
|
-
span.appendChild(node);
|
|
307
|
-
break;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (document.readyState === 'complete') {
|
|
314
|
-
requestAnimationFrame(injectSpans);
|
|
315
|
-
} else {
|
|
316
|
-
window.addEventListener('load', function() {
|
|
317
|
-
requestAnimationFrame(injectSpans);
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
})();
|
|
321
|
-
` : ""}
|
|
322
271
|
`
|
|
323
272
|
}
|
|
324
273
|
}
|
|
325
274
|
),
|
|
326
|
-
|
|
275
|
+
needsClientSideSpans && contentEntries.length > 0 ? /* @__PURE__ */ jsx(
|
|
276
|
+
ClientEditableBlock,
|
|
277
|
+
{
|
|
278
|
+
blockId: block.id,
|
|
279
|
+
blockType: block.type,
|
|
280
|
+
contentEntries,
|
|
281
|
+
children: renderedComponent
|
|
282
|
+
}
|
|
283
|
+
) : renderedComponent,
|
|
327
284
|
/* @__PURE__ */ jsx(BlockToolbar, { blockId: block.id }, "cms-toolbar")
|
|
328
285
|
]
|
|
329
286
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../lib/block-renderer.tsx"],"sourcesContent":["/**\n * Block Renderer Component\n *\n * Dispatches block data to the appropriate component using the ComponentMap pattern.\n * This is the main entry point for rendering blocks from the CMS.\n */\n\nimport React from 'react';\nimport { BlockToolbar } from './block-toolbar';\nimport type { BlockComponentRegistry, BlockData, ResolvedRouteParams } from './types';\n\ntype TextInfo = {\n value: string;\n path: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n};\n\ntype ElementInfo = {\n element: React.ReactElement;\n path: Array<string | number>;\n};\n\ntype WalkVisitors = {\n /**\n * Called for every string/number child encountered.\n * Return:\n * - same string (or modified)\n * - a ReactNode (e.g. wrap in <span/>)\n */\n onText?: (info: TextInfo) => React.ReactNode;\n\n /**\n * Called for every ReactElement encountered (after children are processed).\n * Return:\n * - same element\n * - a cloned/modified element\n */\n onElement?: (info: ElementInfo) => React.ReactElement;\n};\n\n/**\n * Recursively maps a ReactNode tree, allowing transformations of text nodes and/or elements.\n * SSR-safe: does not touch DOM APIs.\n */\nexport function walkReactNode(\n node: React.ReactNode,\n visitors: WalkVisitors,\n ctx: {\n path?: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n } = {}\n): React.ReactNode {\n const path = ctx.path ?? [];\n\n // Fast-path primitives\n if (node == null || typeof node === 'boolean') return node;\n\n if (typeof node === 'string' || typeof node === 'number') {\n const value = String(node);\n return visitors.onText\n ? visitors.onText({ value, path, parentType: ctx.parentType, key: ctx.key })\n : node;\n }\n\n // Arrays\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, i],\n parentType: ctx.parentType,\n key: childKey,\n });\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `arr-${path.join('-')}-${i}` });\n }\n return result;\n });\n }\n\n // ReactElement (including Fragment)\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // Recurse into children (if any)\n const hasChildren = elProps && 'children' in elProps;\n const nextChildren = hasChildren\n ? React.Children.map(elProps.children as React.ReactNode, (child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, 'children', i],\n parentType: el.type as React.ElementType,\n key: childKey,\n });\n // Ensure children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `child-${path.join('-')}-${i}` });\n }\n return result;\n })\n : (elProps?.children as React.ReactNode);\n\n // Only clone if children changed (or if you want to force a clone)\n const cloned = hasChildren\n ? React.cloneElement(el, undefined, nextChildren as React.ReactNode)\n : el;\n\n return visitors.onElement ? visitors.onElement({ element: cloned, path }) : cloned;\n }\n\n // Functions, symbols, portals, etc. are rare here; return as-is\n return node;\n}\n\n// -----------------------------------------------------------------------------\n// Content Value Extraction\n// -----------------------------------------------------------------------------\n\ntype ContentMatch = {\n contentPath: string;\n value: string;\n};\n\n/**\n * Extracts all string values from a content object with their paths.\n * Returns a Map where keys are string values and values are arrays of content paths.\n */\nfunction extractContentValues(\n content: Record<string, unknown>,\n basePath: string[] = []\n): Map<string, ContentMatch[]> {\n const map = new Map<string, ContentMatch[]>();\n\n function walk(obj: unknown, path: string[]) {\n if (typeof obj === 'string' && obj.trim() !== '') {\n const contentPath = path.join('.');\n const existing = map.get(obj) || [];\n existing.push({ contentPath, value: obj });\n map.set(obj, existing);\n } else if (Array.isArray(obj)) {\n for (let index = 0; index < obj.length; index++) {\n walk(obj[index], [...path, String(index)]);\n }\n } else if (obj && typeof obj === 'object') {\n for (const [key, value] of Object.entries(obj)) {\n walk(value, [...path, key]);\n }\n }\n }\n\n walk(content, basePath);\n return map;\n}\n\n// -----------------------------------------------------------------------------\n// Props\n// -----------------------------------------------------------------------------\n\ninterface BlockRendererProps {\n /**\n * The block data to render.\n * Must have a `type` field that maps to a registered component.\n */\n block: BlockData;\n registry: Partial<BlockComponentRegistry>;\n /**\n * If true, renders the component without any tree walking or editable wrappers.\n */\n disableEditable?: boolean;\n /**\n * Resolved route parameters from parametric routes.\n * Each key is a param name (e.g., \"country\") with its value, schema name, and full document.\n */\n routeParams?: ResolvedRouteParams;\n}\n\n// -----------------------------------------------------------------------------\n// Component\n// -----------------------------------------------------------------------------\n\n/**\n * Recursively renders a React node, invoking function components to get their output.\n * This allows us to walk the full rendered tree, not just the element wrappers.\n */\nfunction renderToWalkableTree(node: React.ReactNode, keyPrefix = ''): React.ReactNode {\n if (node == null || typeof node === 'boolean') return node;\n if (typeof node === 'string' || typeof node === 'number') return node;\n\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n const result = renderToWalkableTree(child, `${keyPrefix}${i}-`);\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n // biome-ignore lint/suspicious/noExplicitAny: Adding key to element\n const existingKey = (child as any)?.key;\n return React.cloneElement(result, { key: existingKey ?? `${keyPrefix}${i}` });\n }\n return result;\n });\n }\n\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // If it's a function component, invoke it to get the rendered output\n if (typeof el.type === 'function') {\n try {\n // biome-ignore lint/complexity/noBannedTypes: Need to invoke React function component\n const rendered = (el.type as Function)(el.props);\n return renderToWalkableTree(rendered, keyPrefix);\n } catch {\n // If component throws (e.g., uses hooks), return as-is\n return node;\n }\n }\n\n // For host elements (div, span, etc.), recurse into children\n if (elProps && 'children' in elProps) {\n const newChildren = renderToWalkableTree(elProps.children as React.ReactNode, keyPrefix);\n return React.cloneElement(el, undefined, newChildren);\n }\n\n return node;\n }\n\n return node;\n}\n\n/**\n * Renders a single block by dispatching to the appropriate component.\n *\n * Uses the ComponentMap pattern: the block's `type` field determines which\n * component renders the block's `content`.\n *\n * Internally, it:\n * 1. Renders the component tree by invoking function components\n * 2. Extracts all string values from block.content\n * 3. Walks the rendered tree and wraps matching text nodes with spans\n */\nexport function BlockRenderer({\n block,\n registry,\n disableEditable,\n routeParams,\n}: BlockRendererProps) {\n const Component = registry[block.type];\n\n if (!Component) {\n // Log warning in development, render nothing in production\n if (process.env.NODE_ENV === 'development') {\n console.warn(`[BlockRenderer] Unknown block type: ${block.type}`);\n }\n return null;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type safety ensured by BlockData discriminated union\n const component = <Component content={block.content as any} routeParams={routeParams} />;\n\n if (disableEditable) {\n return component;\n }\n\n // Extract all string values from content with their paths\n const contentValueMap = extractContentValues(block.content as Record<string, unknown>);\n\n // Try server-side tree walking for text span injection (best-effort).\n // This works for sync server components but fails for client components,\n // async components, and components using hooks.\n let renderedComponent: React.ReactNode = component;\n let needsClientSideSpans = true;\n\n try {\n const renderedTree = renderToWalkableTree(component);\n\n // Check if tree walking produced host elements (div, section, etc.)\n // vs the original component reference (function type = couldn't be invoked)\n const isWalkable =\n React.isValidElement(renderedTree) &&\n typeof (renderedTree as React.ReactElement<Record<string, unknown>>).type === 'string';\n\n if (isWalkable) {\n const usedPaths = new Set<string>();\n\n renderedComponent = walkReactNode(renderedTree, {\n onText: ({ value, key, path }) => {\n const matches = contentValueMap.get(value);\n if (!matches || matches.length === 0) return value;\n\n const match = matches.find((m) => !usedPaths.has(m.contentPath)) ?? matches[0];\n if (!match) return value;\n\n usedPaths.add(match.contentPath);\n const spanKey = key ?? `${block.id}-${match.contentPath}-${path.join('-')}`;\n\n return (\n <span\n key={spanKey}\n data-cms-editable\n data-block-id={block.id}\n data-block-type={block.type}\n data-content-path={match.contentPath}\n >\n {value}\n </span>\n );\n },\n });\n\n needsClientSideSpans = false;\n }\n } catch {\n // Tree walking failed entirely, fall back to client-side injection\n }\n\n // Build content entries for client-side text matching when server-side fails\n const contentEntries = needsClientSideSpans\n ? Array.from(contentValueMap.entries())\n .map(([value, matches]) => ({ v: value, p: matches[0]?.contentPath }))\n .filter((e): e is { v: string; p: string } => !!e.p)\n : [];\n\n return (\n <div\n key={block.id}\n data-cms-block\n data-block-id={block.id}\n data-block-type={block.type}\n style={{ position: 'relative' }}\n >\n <style>{`\n [data-cms-block] {\n position: relative;\n }\n [data-cms-block]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 4px;\n }\n [data-cms-editable] {\n cursor: pointer;\n border-radius: 2px;\n }\n [data-cms-editable]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 2px;\n }\n .cms-block-toolbar {\n position: absolute;\n bottom: 8px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n gap: 4px;\n background: #1f2937;\n border-radius: 6px;\n padding: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.15s ease;\n z-index: 1000;\n }\n [data-cms-block]:hover .cms-block-toolbar {\n opacity: 1;\n pointer-events: auto;\n }\n .cms-block-toolbar button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #9ca3af;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n }\n .cms-block-toolbar button:hover {\n background: #374151;\n color: #fff;\n }\n .cms-block-toolbar button.delete:hover {\n background: #dc2626;\n color: #fff;\n }\n .cms-block-toolbar button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n .cms-block-toolbar button:disabled:hover {\n background: transparent;\n color: #9ca3af;\n }\n .cms-block-toolbar svg {\n width: 16px;\n height: 16px;\n }\n `}</style>\n <script\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Inline script for iframe postMessage and client-side text matching\n dangerouslySetInnerHTML={{\n __html: `\n (function() {\n if (!window.__cmsEditableInitialized) {\n window.__cmsEditableInitialized = true;\n\n document.addEventListener('click', function(e) {\n if (e.target.closest('.cms-block-toolbar')) return;\n\n var editableTarget = e.target.closest('[data-cms-editable]');\n if (editableTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: editableTarget.getAttribute('data-block-id'),\n blockType: editableTarget.getAttribute('data-block-type'),\n contentPath: editableTarget.getAttribute('data-content-path')\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n return;\n }\n\n var blockTarget = e.target.closest('[data-cms-block]');\n if (blockTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: blockTarget.getAttribute('data-block-id'),\n blockType: blockTarget.getAttribute('data-block-type'),\n contentPath: null\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n }\n });\n }\n })();\n ${\n contentEntries.length > 0\n ? `\n (function() {\n var blockId = ${JSON.stringify(block.id)};\n var blockType = ${JSON.stringify(block.type)};\n var entries = ${JSON.stringify(contentEntries)};\n\n function injectSpans() {\n var block = document.querySelector('[data-block-id=\"' + blockId + '\"]');\n if (!block || block.getAttribute('data-cms-spans-injected')) return;\n block.setAttribute('data-cms-spans-injected', 'true');\n\n var used = {};\n var walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT, null);\n var textNodes = [];\n var n;\n while (n = walker.nextNode()) {\n if (n.parentNode && n.parentNode.closest && n.parentNode.closest('.cms-block-toolbar')) continue;\n if (n.nodeValue && n.nodeValue.trim()) textNodes.push(n);\n }\n\n for (var i = 0; i < textNodes.length; i++) {\n var node = textNodes[i];\n var text = node.nodeValue;\n if (!text) continue;\n\n for (var j = 0; j < entries.length; j++) {\n var e = entries[j];\n if (used[e.p]) continue;\n if (text.indexOf(e.v) !== -1 && text.trim() === e.v.trim()) {\n used[e.p] = true;\n var span = document.createElement('span');\n span.setAttribute('data-cms-editable', '');\n span.setAttribute('data-block-id', blockId);\n span.setAttribute('data-block-type', blockType);\n span.setAttribute('data-content-path', e.p);\n node.parentNode.insertBefore(span, node);\n span.appendChild(node);\n break;\n }\n }\n }\n }\n\n if (document.readyState === 'complete') {\n requestAnimationFrame(injectSpans);\n } else {\n window.addEventListener('load', function() {\n requestAnimationFrame(injectSpans);\n });\n }\n })();\n `\n : ''\n }\n `,\n }}\n />\n {renderedComponent}\n <BlockToolbar key=\"cms-toolbar\" blockId={block.id} />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../lib/block-renderer.tsx"],"sourcesContent":["/**\n * Block Renderer Component\n *\n * Dispatches block data to the appropriate component using the ComponentMap pattern.\n * This is the main entry point for rendering blocks from the CMS.\n */\n\nimport React from 'react';\nimport { ClientEditableBlock } from './client-editable-block';\nimport { BlockToolbar } from './block-toolbar';\nimport type { BlockComponentRegistry, BlockData, ResolvedRouteParams } from './types';\n\ntype TextInfo = {\n value: string;\n path: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n};\n\ntype ElementInfo = {\n element: React.ReactElement;\n path: Array<string | number>;\n};\n\ntype WalkVisitors = {\n /**\n * Called for every string/number child encountered.\n * Return:\n * - same string (or modified)\n * - a ReactNode (e.g. wrap in <span/>)\n */\n onText?: (info: TextInfo) => React.ReactNode;\n\n /**\n * Called for every ReactElement encountered (after children are processed).\n * Return:\n * - same element\n * - a cloned/modified element\n */\n onElement?: (info: ElementInfo) => React.ReactElement;\n};\n\n/**\n * Recursively maps a ReactNode tree, allowing transformations of text nodes and/or elements.\n * SSR-safe: does not touch DOM APIs.\n */\nexport function walkReactNode(\n node: React.ReactNode,\n visitors: WalkVisitors,\n ctx: {\n path?: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n } = {}\n): React.ReactNode {\n const path = ctx.path ?? [];\n\n // Fast-path primitives\n if (node == null || typeof node === 'boolean') return node;\n\n if (typeof node === 'string' || typeof node === 'number') {\n const value = String(node);\n return visitors.onText\n ? visitors.onText({ value, path, parentType: ctx.parentType, key: ctx.key })\n : node;\n }\n\n // Arrays\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, i],\n parentType: ctx.parentType,\n key: childKey,\n });\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `arr-${path.join('-')}-${i}` });\n }\n return result;\n });\n }\n\n // ReactElement (including Fragment)\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // Recurse into children (if any)\n const hasChildren = elProps && 'children' in elProps;\n const nextChildren = hasChildren\n ? React.Children.map(elProps.children as React.ReactNode, (child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, 'children', i],\n parentType: el.type as React.ElementType,\n key: childKey,\n });\n // Ensure children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `child-${path.join('-')}-${i}` });\n }\n return result;\n })\n : (elProps?.children as React.ReactNode);\n\n // Only clone if children changed (or if you want to force a clone)\n const cloned = hasChildren\n ? React.cloneElement(el, undefined, nextChildren as React.ReactNode)\n : el;\n\n return visitors.onElement ? visitors.onElement({ element: cloned, path }) : cloned;\n }\n\n // Functions, symbols, portals, etc. are rare here; return as-is\n return node;\n}\n\n// -----------------------------------------------------------------------------\n// Content Value Extraction\n// -----------------------------------------------------------------------------\n\ntype ContentMatch = {\n contentPath: string;\n value: string;\n};\n\n/**\n * Extracts all string values from a content object with their paths.\n * Returns a Map where keys are string values and values are arrays of content paths.\n */\nfunction extractContentValues(\n content: Record<string, unknown>,\n basePath: string[] = []\n): Map<string, ContentMatch[]> {\n const map = new Map<string, ContentMatch[]>();\n\n function walk(obj: unknown, path: string[]) {\n if (typeof obj === 'string' && obj.trim() !== '') {\n const contentPath = path.join('.');\n const existing = map.get(obj) || [];\n existing.push({ contentPath, value: obj });\n map.set(obj, existing);\n } else if (Array.isArray(obj)) {\n for (let index = 0; index < obj.length; index++) {\n walk(obj[index], [...path, String(index)]);\n }\n } else if (obj && typeof obj === 'object') {\n for (const [key, value] of Object.entries(obj)) {\n walk(value, [...path, key]);\n }\n }\n }\n\n walk(content, basePath);\n return map;\n}\n\n// -----------------------------------------------------------------------------\n// Props\n// -----------------------------------------------------------------------------\n\ninterface BlockRendererProps {\n /**\n * The block data to render.\n * Must have a `type` field that maps to a registered component.\n */\n block: BlockData;\n registry: Partial<BlockComponentRegistry>;\n /**\n * If true, renders the component without any tree walking or editable wrappers.\n */\n disableEditable?: boolean;\n /**\n * Resolved route parameters from parametric routes.\n * Each key is a param name (e.g., \"country\") with its value, schema name, and full document.\n */\n routeParams?: ResolvedRouteParams;\n}\n\n// -----------------------------------------------------------------------------\n// Component\n// -----------------------------------------------------------------------------\n\n/**\n * Recursively renders a React node, invoking function components to get their output.\n * This allows us to walk the full rendered tree, not just the element wrappers.\n */\nfunction renderToWalkableTree(node: React.ReactNode, keyPrefix = ''): React.ReactNode {\n if (node == null || typeof node === 'boolean') return node;\n if (typeof node === 'string' || typeof node === 'number') return node;\n\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n const result = renderToWalkableTree(child, `${keyPrefix}${i}-`);\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n // biome-ignore lint/suspicious/noExplicitAny: Adding key to element\n const existingKey = (child as any)?.key;\n return React.cloneElement(result, { key: existingKey ?? `${keyPrefix}${i}` });\n }\n return result;\n });\n }\n\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // If it's a function component, invoke it to get the rendered output\n if (typeof el.type === 'function') {\n try {\n // biome-ignore lint/complexity/noBannedTypes: Need to invoke React function component\n const rendered = (el.type as Function)(el.props);\n return renderToWalkableTree(rendered, keyPrefix);\n } catch {\n // If component throws (e.g., uses hooks), return as-is\n return node;\n }\n }\n\n // For host elements (div, span, etc.), recurse into children\n if (elProps && 'children' in elProps) {\n const newChildren = renderToWalkableTree(elProps.children as React.ReactNode, keyPrefix);\n return React.cloneElement(el, undefined, newChildren);\n }\n\n return node;\n }\n\n return node;\n}\n\n/**\n * Renders a single block by dispatching to the appropriate component.\n *\n * Uses the ComponentMap pattern: the block's `type` field determines which\n * component renders the block's `content`.\n *\n * Internally, it:\n * 1. Renders the component tree by invoking function components\n * 2. Extracts all string values from block.content\n * 3. Walks the rendered tree and wraps matching text nodes with spans\n */\nexport function BlockRenderer({\n block,\n registry,\n disableEditable,\n routeParams,\n}: BlockRendererProps) {\n const Component = registry[block.type];\n\n if (!Component) {\n // Log warning in development, render nothing in production\n if (process.env.NODE_ENV === 'development') {\n console.warn(`[BlockRenderer] Unknown block type: ${block.type}`);\n }\n return null;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type safety ensured by BlockData discriminated union\n const component = <Component content={block.content as any} routeParams={routeParams} />;\n\n if (disableEditable) {\n return component;\n }\n\n // Extract all string values from content with their paths\n const contentValueMap = extractContentValues(block.content as Record<string, unknown>);\n\n // Try server-side tree walking for text span injection (best-effort).\n // This works for sync server components but fails for client components,\n // async components, and components using hooks.\n let renderedComponent: React.ReactNode = component;\n let needsClientSideSpans = true;\n\n try {\n const renderedTree = renderToWalkableTree(component);\n\n // Check if tree walking produced host elements (div, section, etc.)\n // vs the original component reference (function type = couldn't be invoked)\n const isWalkable =\n React.isValidElement(renderedTree) &&\n typeof (renderedTree as React.ReactElement<Record<string, unknown>>).type === 'string';\n\n if (isWalkable) {\n const usedPaths = new Set<string>();\n\n renderedComponent = walkReactNode(renderedTree, {\n onText: ({ value, key, path }) => {\n const matches = contentValueMap.get(value);\n if (!matches || matches.length === 0) return value;\n\n const match = matches.find((m) => !usedPaths.has(m.contentPath)) ?? matches[0];\n if (!match) return value;\n\n usedPaths.add(match.contentPath);\n const spanKey = key ?? `${block.id}-${match.contentPath}-${path.join('-')}`;\n\n return (\n <span\n key={spanKey}\n data-cms-editable\n data-block-id={block.id}\n data-block-type={block.type}\n data-content-path={match.contentPath}\n >\n {value}\n </span>\n );\n },\n });\n\n needsClientSideSpans = false;\n }\n } catch {\n // Tree walking failed entirely, fall back to client-side injection\n }\n\n // Build content entries for client-side text matching when server-side fails\n const contentEntries = needsClientSideSpans\n ? Array.from(contentValueMap.entries())\n .map(([value, matches]) => ({ v: value, p: matches[0]?.contentPath }))\n .filter((e): e is { v: string; p: string } => !!e.p)\n : [];\n\n return (\n <div\n key={block.id}\n data-cms-block\n data-block-id={block.id}\n data-block-type={block.type}\n style={{ position: 'relative' }}\n >\n <style>{`\n [data-cms-block] {\n position: relative;\n }\n [data-cms-block]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 4px;\n }\n [data-cms-editable] {\n cursor: pointer;\n border-radius: 2px;\n }\n [data-cms-editable]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 2px;\n }\n .cms-block-toolbar {\n position: absolute;\n bottom: 8px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n gap: 4px;\n background: #1f2937;\n border-radius: 6px;\n padding: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.15s ease;\n z-index: 1000;\n }\n [data-cms-block]:hover .cms-block-toolbar {\n opacity: 1;\n pointer-events: auto;\n }\n .cms-block-toolbar button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #9ca3af;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n }\n .cms-block-toolbar button:hover {\n background: #374151;\n color: #fff;\n }\n .cms-block-toolbar button.delete:hover {\n background: #dc2626;\n color: #fff;\n }\n .cms-block-toolbar button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n .cms-block-toolbar button:disabled:hover {\n background: transparent;\n color: #9ca3af;\n }\n .cms-block-toolbar svg {\n width: 16px;\n height: 16px;\n }\n `}</style>\n <script\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Inline script for iframe postMessage click routing\n dangerouslySetInnerHTML={{\n __html: `\n (function() {\n if (!window.__cmsEditableInitialized) {\n window.__cmsEditableInitialized = true;\n\n document.addEventListener('click', function(e) {\n if (e.target.closest('.cms-block-toolbar')) return;\n\n var editableTarget = e.target.closest('[data-cms-editable]');\n if (editableTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: editableTarget.getAttribute('data-block-id'),\n blockType: editableTarget.getAttribute('data-block-type'),\n contentPath: editableTarget.getAttribute('data-content-path')\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n return;\n }\n\n var blockTarget = e.target.closest('[data-cms-block]');\n if (blockTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: blockTarget.getAttribute('data-block-id'),\n blockType: blockTarget.getAttribute('data-block-type'),\n contentPath: null\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n }\n });\n }\n })();\n `,\n }}\n />\n {needsClientSideSpans && contentEntries.length > 0 ? (\n <ClientEditableBlock\n blockId={block.id}\n blockType={block.type}\n contentEntries={contentEntries}\n >\n {renderedComponent}\n </ClientEditableBlock>\n ) : (\n renderedComponent\n )}\n <BlockToolbar key=\"cms-toolbar\" blockId={block.id} />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
interface ContentEntry {
|
|
4
|
+
v: string;
|
|
5
|
+
p: string;
|
|
6
|
+
}
|
|
7
|
+
interface ClientEditableBlockProps {
|
|
8
|
+
blockId: string;
|
|
9
|
+
blockType: string;
|
|
10
|
+
contentEntries: ContentEntry[];
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Client-side span injector for blocks whose components use React hooks
|
|
15
|
+
* and therefore can't be walked server-side.
|
|
16
|
+
*
|
|
17
|
+
* Uses a MutationObserver to re-inject [data-cms-editable] spans after every
|
|
18
|
+
* React commit (including state-driven re-renders like animations), so the
|
|
19
|
+
* overlays survive React's reconciliation.
|
|
20
|
+
*/
|
|
21
|
+
declare function ClientEditableBlock({ blockId, blockType, contentEntries, children, }: ClientEditableBlockProps): react.JSX.Element;
|
|
22
|
+
|
|
23
|
+
export { ClientEditableBlock };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// lib/client-editable-block.tsx
|
|
5
|
+
import { useCallback, useLayoutEffect, useRef } from "react";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
function ClientEditableBlock({
|
|
8
|
+
blockId,
|
|
9
|
+
blockType,
|
|
10
|
+
contentEntries,
|
|
11
|
+
children
|
|
12
|
+
}) {
|
|
13
|
+
const containerRef = useRef(null);
|
|
14
|
+
const observerRef = useRef(null);
|
|
15
|
+
const isInjectingRef = useRef(false);
|
|
16
|
+
const injectSpans = useCallback(() => {
|
|
17
|
+
if (isInjectingRef.current) return;
|
|
18
|
+
const container = containerRef.current;
|
|
19
|
+
if (!container) return;
|
|
20
|
+
isInjectingRef.current = true;
|
|
21
|
+
observerRef.current?.disconnect();
|
|
22
|
+
try {
|
|
23
|
+
const existing = Array.from(
|
|
24
|
+
container.querySelectorAll("span[data-cms-editable][data-content-path]")
|
|
25
|
+
);
|
|
26
|
+
for (const span of existing) {
|
|
27
|
+
const parent = span.parentNode;
|
|
28
|
+
if (!parent) continue;
|
|
29
|
+
while (span.firstChild) parent.insertBefore(span.firstChild, span);
|
|
30
|
+
parent.removeChild(span);
|
|
31
|
+
}
|
|
32
|
+
const used = /* @__PURE__ */ new Set();
|
|
33
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null);
|
|
34
|
+
const textNodes = [];
|
|
35
|
+
let n;
|
|
36
|
+
while (n = walker.nextNode()) {
|
|
37
|
+
const textNode = n;
|
|
38
|
+
if (textNode.parentElement?.closest(".cms-block-toolbar") == null && textNode.nodeValue?.trim()) {
|
|
39
|
+
textNodes.push(textNode);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const textNode of textNodes) {
|
|
43
|
+
const text = textNode.nodeValue;
|
|
44
|
+
if (!text) continue;
|
|
45
|
+
for (const entry of contentEntries) {
|
|
46
|
+
if (used.has(entry.p)) continue;
|
|
47
|
+
if (text.indexOf(entry.v) !== -1 && text.trim() === entry.v.trim()) {
|
|
48
|
+
used.add(entry.p);
|
|
49
|
+
const span = document.createElement("span");
|
|
50
|
+
span.setAttribute("data-cms-editable", "");
|
|
51
|
+
span.setAttribute("data-block-id", blockId);
|
|
52
|
+
span.setAttribute("data-block-type", blockType);
|
|
53
|
+
span.setAttribute("data-content-path", entry.p);
|
|
54
|
+
textNode.parentNode?.insertBefore(span, textNode);
|
|
55
|
+
span.appendChild(textNode);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} finally {
|
|
61
|
+
isInjectingRef.current = false;
|
|
62
|
+
const current = containerRef.current;
|
|
63
|
+
if (current && observerRef.current) {
|
|
64
|
+
observerRef.current.observe(current, {
|
|
65
|
+
childList: true,
|
|
66
|
+
subtree: true,
|
|
67
|
+
characterData: true
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}, [blockId, blockType, contentEntries]);
|
|
72
|
+
useLayoutEffect(() => {
|
|
73
|
+
injectSpans();
|
|
74
|
+
const observer = new MutationObserver(injectSpans);
|
|
75
|
+
observerRef.current = observer;
|
|
76
|
+
if (containerRef.current) {
|
|
77
|
+
observer.observe(containerRef.current, {
|
|
78
|
+
childList: true,
|
|
79
|
+
subtree: true,
|
|
80
|
+
characterData: true
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return () => {
|
|
84
|
+
observer.disconnect();
|
|
85
|
+
observerRef.current = null;
|
|
86
|
+
};
|
|
87
|
+
}, [injectSpans]);
|
|
88
|
+
return /* @__PURE__ */ jsx("div", { ref: containerRef, children });
|
|
89
|
+
}
|
|
90
|
+
export {
|
|
91
|
+
ClientEditableBlock
|
|
92
|
+
};
|
|
93
|
+
//# sourceMappingURL=client-editable-block.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../lib/client-editable-block.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useLayoutEffect, useRef } from 'react';\n\ninterface ContentEntry {\n v: string;\n p: string;\n}\n\ninterface ClientEditableBlockProps {\n blockId: string;\n blockType: string;\n contentEntries: ContentEntry[];\n children: React.ReactNode;\n}\n\n/**\n * Client-side span injector for blocks whose components use React hooks\n * and therefore can't be walked server-side.\n *\n * Uses a MutationObserver to re-inject [data-cms-editable] spans after every\n * React commit (including state-driven re-renders like animations), so the\n * overlays survive React's reconciliation.\n */\nexport function ClientEditableBlock({\n blockId,\n blockType,\n contentEntries,\n children,\n}: ClientEditableBlockProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const observerRef = useRef<MutationObserver | null>(null);\n const isInjectingRef = useRef(false);\n\n const injectSpans = useCallback(() => {\n if (isInjectingRef.current) return;\n const container = containerRef.current;\n if (!container) return;\n\n isInjectingRef.current = true;\n // Disconnect observer while we mutate the DOM to avoid re-entrant calls.\n observerRef.current?.disconnect();\n\n try {\n // Remove previously injected text-level spans, restoring the original text nodes.\n const existing = Array.from(\n container.querySelectorAll<HTMLElement>('span[data-cms-editable][data-content-path]')\n );\n for (const span of existing) {\n const parent = span.parentNode;\n if (!parent) continue;\n while (span.firstChild) parent.insertBefore(span.firstChild, span);\n parent.removeChild(span);\n }\n\n // Walk all visible text nodes and wrap the ones that match content values.\n const used = new Set<string>();\n const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null);\n const textNodes: Text[] = [];\n let n: Node | null;\n while ((n = walker.nextNode())) {\n const textNode = n as Text;\n if (\n textNode.parentElement?.closest('.cms-block-toolbar') == null &&\n textNode.nodeValue?.trim()\n ) {\n textNodes.push(textNode);\n }\n }\n\n for (const textNode of textNodes) {\n const text = textNode.nodeValue;\n if (!text) continue;\n for (const entry of contentEntries) {\n if (used.has(entry.p)) continue;\n if (text.indexOf(entry.v) !== -1 && text.trim() === entry.v.trim()) {\n used.add(entry.p);\n const span = document.createElement('span');\n span.setAttribute('data-cms-editable', '');\n span.setAttribute('data-block-id', blockId);\n span.setAttribute('data-block-type', blockType);\n span.setAttribute('data-content-path', entry.p);\n textNode.parentNode?.insertBefore(span, textNode);\n span.appendChild(textNode);\n break;\n }\n }\n }\n } finally {\n isInjectingRef.current = false;\n // Reconnect after our mutations are done.\n const current = containerRef.current;\n if (current && observerRef.current) {\n observerRef.current.observe(current, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n }\n }, [blockId, blockType, contentEntries]);\n\n useLayoutEffect(() => {\n // Initial injection after React's first commit.\n injectSpans();\n\n // Re-inject whenever the child component mutates the DOM\n // (e.g. animated state changes that swap visible elements).\n const observer = new MutationObserver(injectSpans);\n observerRef.current = observer;\n\n if (containerRef.current) {\n observer.observe(containerRef.current, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n\n return () => {\n observer.disconnect();\n observerRef.current = null;\n };\n }, [injectSpans]);\n\n return <div ref={containerRef}>{children}</div>;\n}\n"],"mappings":";;;;AAEA,SAAS,aAAa,iBAAiB,cAAc;AA2H5C;AArGF,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6B;AAC3B,QAAM,eAAe,OAAuB,IAAI;AAChD,QAAM,cAAc,OAAgC,IAAI;AACxD,QAAM,iBAAiB,OAAO,KAAK;AAEnC,QAAM,cAAc,YAAY,MAAM;AACpC,QAAI,eAAe,QAAS;AAC5B,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,UAAW;AAEhB,mBAAe,UAAU;AAEzB,gBAAY,SAAS,WAAW;AAEhC,QAAI;AAEF,YAAM,WAAW,MAAM;AAAA,QACrB,UAAU,iBAA8B,4CAA4C;AAAA,MACtF;AACA,iBAAW,QAAQ,UAAU;AAC3B,cAAM,SAAS,KAAK;AACpB,YAAI,CAAC,OAAQ;AACb,eAAO,KAAK,WAAY,QAAO,aAAa,KAAK,YAAY,IAAI;AACjE,eAAO,YAAY,IAAI;AAAA,MACzB;AAGA,YAAM,OAAO,oBAAI,IAAY;AAC7B,YAAM,SAAS,SAAS,iBAAiB,WAAW,WAAW,WAAW,IAAI;AAC9E,YAAM,YAAoB,CAAC;AAC3B,UAAI;AACJ,aAAQ,IAAI,OAAO,SAAS,GAAI;AAC9B,cAAM,WAAW;AACjB,YACE,SAAS,eAAe,QAAQ,oBAAoB,KAAK,QACzD,SAAS,WAAW,KAAK,GACzB;AACA,oBAAU,KAAK,QAAQ;AAAA,QACzB;AAAA,MACF;AAEA,iBAAW,YAAY,WAAW;AAChC,cAAM,OAAO,SAAS;AACtB,YAAI,CAAC,KAAM;AACX,mBAAW,SAAS,gBAAgB;AAClC,cAAI,KAAK,IAAI,MAAM,CAAC,EAAG;AACvB,cAAI,KAAK,QAAQ,MAAM,CAAC,MAAM,MAAM,KAAK,KAAK,MAAM,MAAM,EAAE,KAAK,GAAG;AAClE,iBAAK,IAAI,MAAM,CAAC;AAChB,kBAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,iBAAK,aAAa,qBAAqB,EAAE;AACzC,iBAAK,aAAa,iBAAiB,OAAO;AAC1C,iBAAK,aAAa,mBAAmB,SAAS;AAC9C,iBAAK,aAAa,qBAAqB,MAAM,CAAC;AAC9C,qBAAS,YAAY,aAAa,MAAM,QAAQ;AAChD,iBAAK,YAAY,QAAQ;AACzB;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF,UAAE;AACA,qBAAe,UAAU;AAEzB,YAAM,UAAU,aAAa;AAC7B,UAAI,WAAW,YAAY,SAAS;AAClC,oBAAY,QAAQ,QAAQ,SAAS;AAAA,UACnC,WAAW;AAAA,UACX,SAAS;AAAA,UACT,eAAe;AAAA,QACjB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,GAAG,CAAC,SAAS,WAAW,cAAc,CAAC;AAEvC,kBAAgB,MAAM;AAEpB,gBAAY;AAIZ,UAAM,WAAW,IAAI,iBAAiB,WAAW;AACjD,gBAAY,UAAU;AAEtB,QAAI,aAAa,SAAS;AACxB,eAAS,QAAQ,aAAa,SAAS;AAAA,QACrC,WAAW;AAAA,QACX,SAAS;AAAA,QACT,eAAe;AAAA,MACjB,CAAC;AAAA,IACH;AAEA,WAAO,MAAM;AACX,eAAS,WAAW;AACpB,kBAAY,UAAU;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,WAAW,CAAC;AAEhB,SAAO,oBAAC,SAAI,KAAK,cAAe,UAAS;AAC3C;","names":[]}
|
package/dist/lib/cms-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../lib/cms-api.ts"],"sourcesContent":["/**\n * CMS API Client\n *\n * Creates an HTTP-based tRPC client for calling the CMS API.\n * Used in Server Components to fetch routes and blocks from the CMS.\n */\n\nimport type { AppRouter } from '@repo/cms-schema/trpc';\nimport { type CreateTRPCClient, createTRPCClient, httpBatchLink } from '@trpc/client';\nimport superjson from 'superjson';\n\n/** Type alias for the CMS API client */\ntype CmsClient = CreateTRPCClient<AppRouter>;\n\n/**\n * Get the CMS API URL from the provided base URL.\n */\nfunction getCmsApiUrl(cmsUrl: string): string {\n return new URL('/api/trpc', cmsUrl).toString();\n}\n\n/** Unified configuration for all CMS API access */\nexport interface CmsConfig {\n /** CMS API base URL (e.g., 'http://localhost:3000') */\n cmsUrl: string;\n /** API key for authentication */\n apiKey?: string;\n /** Website ID to scope API requests */\n websiteId?: string;\n}\n\n/**\n * Create a custom fetch function that appends API key as query parameter.\n */\nfunction createFetchWithApiKey(apiKey?: string, websiteId?: string) {\n return async (url: URL | RequestInfo, options?: RequestInit): Promise<Response> => {\n let finalUrl = url;\n\n const urlObj = new URL(url.toString());\n if (apiKey) {\n urlObj.searchParams.set('api_key', apiKey);\n }\n if (websiteId) {\n urlObj.searchParams.set('website_id', websiteId);\n }\n if (apiKey || websiteId) {\n finalUrl = urlObj.toString();\n }\n\n
|
|
1
|
+
{"version":3,"sources":["../../lib/cms-api.ts"],"sourcesContent":["/**\n * CMS API Client\n *\n * Creates an HTTP-based tRPC client for calling the CMS API.\n * Used in Server Components to fetch routes and blocks from the CMS.\n */\n\nimport type { AppRouter } from '@repo/cms-schema/trpc';\nimport { type CreateTRPCClient, createTRPCClient, httpBatchLink } from '@trpc/client';\nimport superjson from 'superjson';\n\n/** Type alias for the CMS API client */\ntype CmsClient = CreateTRPCClient<AppRouter>;\n\n/**\n * Get the CMS API URL from the provided base URL.\n */\nfunction getCmsApiUrl(cmsUrl: string): string {\n return new URL('/api/trpc', cmsUrl).toString();\n}\n\n/** Unified configuration for all CMS API access */\nexport interface CmsConfig {\n /** CMS API base URL (e.g., 'http://localhost:3000') */\n cmsUrl: string;\n /** API key for authentication */\n apiKey?: string;\n /** Website ID to scope API requests */\n websiteId?: string;\n}\n\n/**\n * Create a custom fetch function that appends API key as query parameter.\n */\nfunction createFetchWithApiKey(apiKey?: string, websiteId?: string) {\n return async (url: URL | RequestInfo, options?: RequestInit): Promise<Response> => {\n let finalUrl = url;\n\n const urlObj = new URL(url.toString());\n if (apiKey) {\n urlObj.searchParams.set('api_key', apiKey);\n }\n if (websiteId) {\n urlObj.searchParams.set('website_id', websiteId);\n }\n if (apiKey || websiteId) {\n finalUrl = urlObj.toString();\n }\n\n const response = await fetch(finalUrl, options);\n\n return response;\n };\n}\n\n/**\n * Create a tRPC client for the CMS API.\n */\nfunction createCmsClient(options: CmsConfig): CmsClient {\n const url = getCmsApiUrl(options.cmsUrl);\n\n return createTRPCClient<AppRouter>({\n links: [\n httpBatchLink({\n url,\n transformer: superjson,\n fetch: createFetchWithApiKey(options.apiKey, options.websiteId),\n }),\n ],\n });\n}\n\n/**\n * Get a CMS client for the specified CMS URL.\n */\nexport function getCmsClient(options: CmsConfig): CmsClient {\n return createCmsClient(options);\n}\n"],"mappings":";AAQA,SAAgC,kBAAkB,qBAAqB;AACvE,OAAO,eAAe;AAQtB,SAAS,aAAa,QAAwB;AAC5C,SAAO,IAAI,IAAI,aAAa,MAAM,EAAE,SAAS;AAC/C;AAeA,SAAS,sBAAsB,QAAiB,WAAoB;AAClE,SAAO,OAAO,KAAwB,YAA6C;AACjF,QAAI,WAAW;AAEf,UAAM,SAAS,IAAI,IAAI,IAAI,SAAS,CAAC;AACrC,QAAI,QAAQ;AACV,aAAO,aAAa,IAAI,WAAW,MAAM;AAAA,IAC3C;AACA,QAAI,WAAW;AACb,aAAO,aAAa,IAAI,cAAc,SAAS;AAAA,IACjD;AACA,QAAI,UAAU,WAAW;AACvB,iBAAW,OAAO,SAAS;AAAA,IAC7B;AAEA,UAAM,WAAW,MAAM,MAAM,UAAU,OAAO;AAE9C,WAAO;AAAA,EACT;AACF;AAKA,SAAS,gBAAgB,SAA+B;AACtD,QAAM,MAAM,aAAa,QAAQ,MAAM;AAEvC,SAAO,iBAA4B;AAAA,IACjC,OAAO;AAAA,MACL,cAAc;AAAA,QACZ;AAAA,QACA,aAAa;AAAA,QACb,OAAO,sBAAsB,QAAQ,QAAQ,QAAQ,SAAS;AAAA,MAChE,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;AAKO,SAAS,aAAa,SAA+B;AAC1D,SAAO,gBAAgB,OAAO;AAChC;","names":[]}
|