cms-renderer 0.6.5 → 0.6.6
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/README.md +11 -11
- package/dist/lib/cms-api.d.ts +1 -0
- package/dist/lib/cms-api.js +11 -1
- package/dist/lib/cms-api.js.map +1 -1
- package/dist/lib/custom-schemas.js +17 -2
- package/dist/lib/custom-schemas.js.map +1 -1
- package/dist/lib/docs-markdown.d.ts +1 -0
- package/dist/lib/docs-markdown.js +97 -12
- package/dist/lib/docs-markdown.js.map +1 -1
- package/dist/lib/proxy.js +8 -20
- package/dist/lib/proxy.js.map +1 -1
- package/dist/lib/renderer.js +15 -10
- package/dist/lib/renderer.js.map +1 -1
- package/dist/lib/schema.d.ts +19 -7
- package/dist/lib/schema.js +41 -7
- package/dist/lib/schema.js.map +1 -1
- package/package.json +1 -1
|
@@ -146,10 +146,81 @@ async function initMarkdown() {
|
|
|
146
146
|
|
|
147
147
|
// lib/docs-markdown.tsx
|
|
148
148
|
import { Fragment as Fragment3 } from "react";
|
|
149
|
-
import {
|
|
149
|
+
import { bundledLanguages, createHighlighter } from "shiki";
|
|
150
150
|
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
151
151
|
var defaultClassName = "cms-docs-markdown";
|
|
152
152
|
var markdownInitPromise;
|
|
153
|
+
var PRELOADED_LANGS = [
|
|
154
|
+
// Web
|
|
155
|
+
"typescript",
|
|
156
|
+
"javascript",
|
|
157
|
+
"tsx",
|
|
158
|
+
"jsx",
|
|
159
|
+
"html",
|
|
160
|
+
"css",
|
|
161
|
+
"scss",
|
|
162
|
+
// Systems
|
|
163
|
+
"c",
|
|
164
|
+
"cpp",
|
|
165
|
+
"rust",
|
|
166
|
+
"go",
|
|
167
|
+
// Enterprise
|
|
168
|
+
"java",
|
|
169
|
+
"csharp",
|
|
170
|
+
"kotlin",
|
|
171
|
+
"swift",
|
|
172
|
+
// Scripting
|
|
173
|
+
"python",
|
|
174
|
+
"ruby",
|
|
175
|
+
"php",
|
|
176
|
+
"bash",
|
|
177
|
+
"shellscript",
|
|
178
|
+
"powershell",
|
|
179
|
+
// Data
|
|
180
|
+
"json",
|
|
181
|
+
"yaml",
|
|
182
|
+
"toml",
|
|
183
|
+
"xml",
|
|
184
|
+
"sql",
|
|
185
|
+
"graphql",
|
|
186
|
+
// Other
|
|
187
|
+
"markdown",
|
|
188
|
+
"dockerfile"
|
|
189
|
+
];
|
|
190
|
+
var highlighterPromise;
|
|
191
|
+
var loadedLanguages = new Set(PRELOADED_LANGS);
|
|
192
|
+
async function getHighlighter() {
|
|
193
|
+
highlighterPromise ??= createHighlighter({
|
|
194
|
+
themes: ["github-dark"],
|
|
195
|
+
langs: PRELOADED_LANGS
|
|
196
|
+
});
|
|
197
|
+
return highlighterPromise;
|
|
198
|
+
}
|
|
199
|
+
async function ensureLanguageLoaded(highlighter, lang) {
|
|
200
|
+
if (loadedLanguages.has(lang)) {
|
|
201
|
+
return lang;
|
|
202
|
+
}
|
|
203
|
+
if (lang in bundledLanguages) {
|
|
204
|
+
try {
|
|
205
|
+
await highlighter.loadLanguage(lang);
|
|
206
|
+
loadedLanguages.add(lang);
|
|
207
|
+
return lang;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
if (typeof window === "undefined") {
|
|
215
|
+
markdownInitPromise = initMarkdown();
|
|
216
|
+
highlighterPromise = createHighlighter({
|
|
217
|
+
themes: ["github-dark"],
|
|
218
|
+
langs: PRELOADED_LANGS
|
|
219
|
+
});
|
|
220
|
+
Promise.all([markdownInitPromise, highlighterPromise]).then(
|
|
221
|
+
() => console.log(`[docs-markdown] WASM and Shiki pre-warmed (${PRELOADED_LANGS.length} languages)`)
|
|
222
|
+
).catch((err) => console.error("[docs-markdown] Pre-warm failed:", err));
|
|
223
|
+
}
|
|
153
224
|
function markdownStartsWithHeading(markdown) {
|
|
154
225
|
return /^\s*#\s+/.test(markdown);
|
|
155
226
|
}
|
|
@@ -170,24 +241,30 @@ function buildComponentProps(type, props) {
|
|
|
170
241
|
}
|
|
171
242
|
function normalizeLanguage(lang) {
|
|
172
243
|
if (typeof lang !== "string") {
|
|
173
|
-
return "
|
|
244
|
+
return "";
|
|
174
245
|
}
|
|
175
246
|
const normalized = lang.trim().toLowerCase();
|
|
176
|
-
return normalized
|
|
247
|
+
return normalized;
|
|
177
248
|
}
|
|
178
249
|
async function renderHighlightedCode(code, lang) {
|
|
179
250
|
const language = normalizeLanguage(lang);
|
|
251
|
+
const displayLanguage = language || "text";
|
|
180
252
|
try {
|
|
181
|
-
const
|
|
182
|
-
|
|
253
|
+
const highlighter = await getHighlighter();
|
|
254
|
+
const resolvedLang = await ensureLanguageLoaded(highlighter, language);
|
|
255
|
+
if (!resolvedLang) {
|
|
256
|
+
return /* @__PURE__ */ jsx3("pre", { className: `language-${displayLanguage}`, children: /* @__PURE__ */ jsx3("code", { className: `language-${displayLanguage}`, children: code }) });
|
|
257
|
+
}
|
|
258
|
+
const result = highlighter.codeToTokens(code, {
|
|
259
|
+
lang: resolvedLang,
|
|
183
260
|
theme: "github-dark"
|
|
184
261
|
});
|
|
185
262
|
return /* @__PURE__ */ jsx3(
|
|
186
263
|
"pre",
|
|
187
264
|
{
|
|
188
|
-
className: `shiki language-${
|
|
265
|
+
className: `shiki language-${displayLanguage}`,
|
|
189
266
|
style: { backgroundColor: result.bg, color: result.fg },
|
|
190
|
-
children: /* @__PURE__ */ jsx3("code", { className: `shiki_code language-${
|
|
267
|
+
children: /* @__PURE__ */ jsx3("code", { className: `shiki_code language-${displayLanguage}`, "data-language": displayLanguage, children: (() => {
|
|
191
268
|
const lineOccurrences = /* @__PURE__ */ new Map();
|
|
192
269
|
return result.tokens.map((line, lineIndex) => {
|
|
193
270
|
const isLastLine = lineIndex === result.tokens.length - 1;
|
|
@@ -208,19 +285,22 @@ async function renderHighlightedCode(code, lang) {
|
|
|
208
285
|
}
|
|
209
286
|
);
|
|
210
287
|
} catch {
|
|
211
|
-
return /* @__PURE__ */ jsx3("pre", { className: `language-${
|
|
288
|
+
return /* @__PURE__ */ jsx3("pre", { className: `language-${displayLanguage}`, children: /* @__PURE__ */ jsx3("code", { className: `language-${displayLanguage}`, children: code }) });
|
|
212
289
|
}
|
|
213
290
|
}
|
|
214
|
-
async function renderNode(node, components, renderImage, key) {
|
|
291
|
+
async function renderNode(node, components, renderImage, imageIndexRef, key) {
|
|
215
292
|
if (isTextNode2(node)) {
|
|
216
293
|
return node;
|
|
217
294
|
}
|
|
218
295
|
const { type, props, children } = node;
|
|
219
296
|
if (type === NodeType3.IMG && renderImage) {
|
|
297
|
+
const imageIndex = imageIndexRef.current;
|
|
298
|
+
imageIndexRef.current += 1;
|
|
220
299
|
return /* @__PURE__ */ jsx3(Fragment3, { children: renderImage({
|
|
221
300
|
src: typeof props?.src === "string" ? props.src : "",
|
|
222
301
|
alt: typeof props?.alt === "string" ? props.alt : "",
|
|
223
|
-
title: typeof props?.title === "string" ? props.title : void 0
|
|
302
|
+
title: typeof props?.title === "string" ? props.title : void 0,
|
|
303
|
+
loading: imageIndex === 0 ? "eager" : "lazy"
|
|
224
304
|
}) }, key);
|
|
225
305
|
}
|
|
226
306
|
if (type === NodeType3.CODE_BLOCK) {
|
|
@@ -228,7 +308,9 @@ async function renderNode(node, components, renderImage, key) {
|
|
|
228
308
|
return /* @__PURE__ */ jsx3(Fragment3, { children: await renderHighlightedCode(code, props?.lang) }, key);
|
|
229
309
|
}
|
|
230
310
|
const renderedChildren = children ? await Promise.all(
|
|
231
|
-
children.map(
|
|
311
|
+
children.map(
|
|
312
|
+
(child, index) => renderNode(child, components, renderImage, imageIndexRef, index)
|
|
313
|
+
)
|
|
232
314
|
) : void 0;
|
|
233
315
|
const Component = components[type];
|
|
234
316
|
if (Component) {
|
|
@@ -244,8 +326,11 @@ async function DocsMarkdown({
|
|
|
244
326
|
}) {
|
|
245
327
|
await initMarkdown2();
|
|
246
328
|
const ast = mdToJSON2(content);
|
|
329
|
+
const imageIndexRef = { current: 0 };
|
|
247
330
|
const rendered = await Promise.all(
|
|
248
|
-
ast.children.map(
|
|
331
|
+
ast.children.map(
|
|
332
|
+
(node, index) => renderNode(node, defaultComponents, renderImage, imageIndexRef, index)
|
|
333
|
+
)
|
|
249
334
|
);
|
|
250
335
|
const resolvedClassName = className === defaultClassName ? defaultClassName : `${defaultClassName} ${className}`.trim();
|
|
251
336
|
return /* @__PURE__ */ jsx3("div", { className: resolvedClassName, children: rendered });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../packages/markdown-wasm/src/index.ts","../../../../packages/markdown-wasm/src/components.tsx","../../../../packages/markdown-wasm/src/renderer.tsx","../../../../packages/markdown-wasm/src/types.ts","../../lib/docs-markdown.tsx"],"sourcesContent":["/**\n * @repo/markdown-wasm\n *\n * High-performance markdown rendering using md4w WASM engine\n * with React component mapping support.\n *\n * @example\n * ```tsx\n * import { MarkdownRenderer, createMarkdownRenderer, NodeType } from \"@repo/markdown-wasm\";\n *\n * // Basic usage\n * <MarkdownRenderer content=\"# Hello **world**\" />\n *\n * // With custom components\n * <MarkdownRenderer\n * content={markdown}\n * components={{\n * [NodeType.H1]: ({ children }) => <MyHeading>{children}</MyHeading>,\n * }}\n * />\n *\n * // Factory pattern for reusable renderer\n * const CustomRenderer = createMarkdownRenderer({\n * components: { ... },\n * className: \"prose\",\n * });\n * <CustomRenderer content={markdown} />\n * ```\n */\n\n// Re-export md4w utilities for advanced usage\nexport { init, mdToHtml, mdToJSON, mdToReadableHtml, ParseFlags as Md4wParseFlags } from 'md4w';\n\n// Component exports\nexport { defaultComponents, minimalComponents } from './components';\n// Main renderer exports\nexport {\n createMarkdownRenderer,\n initMarkdown,\n MarkdownRenderer,\n NodeType,\n renderMarkdown,\n} from './renderer';\n// Type exports\nexport type {\n ComponentMap,\n CreateRendererOptions,\n GenericNodeComponent,\n HeadingLevel,\n MarkdownRendererProps,\n MDNode,\n MDTree,\n NodeComponent,\n NodePropsFor,\n NodeRenderProps,\n Options,\n ParseFlags,\n} from './types';\n// Utility exports\nexport { getHeadingLevel, isElementNode, isTextNode } from './types';\n","/**\n * @repo/markdown-wasm - Default Component Mappings\n *\n * Provides semantic HTML components for all md4w node types.\n * Override any of these by passing custom components to MarkdownRenderer.\n */\n\nimport { NodeType } from 'md4w';\nimport type { ReactNode } from 'react';\n\nimport type { ComponentMap, HeadingLevel } from './types';\n\n/**\n * Default heading component.\n * Renders H1-H6 based on level prop.\n */\nfunction Heading({ level, children }: { level: HeadingLevel; children: ReactNode }) {\n const Tag = `h${level}` as const;\n return <Tag>{children}</Tag>;\n}\n\n/**\n * Default components for all md4w node types.\n * These render semantic HTML elements that can be styled with CSS.\n *\n * Props are accessed from a generic props object to satisfy GenericNodeComponent type.\n * Runtime guarantees from md4w ensure the props exist for each node type.\n */\nexport const defaultComponents: ComponentMap = {\n // Block elements\n [NodeType.QUOTE]: (props) => <blockquote>{props.children}</blockquote>,\n [NodeType.UL]: (props) => <ul>{props.children}</ul>,\n [NodeType.OL]: (props) => <ol start={props.start as number | undefined}>{props.children}</ol>,\n [NodeType.LI]: (props) => {\n const isTask = props.isTask as boolean | undefined;\n const done = props.done as boolean | undefined;\n if (isTask) {\n return (\n <li>\n <input type=\"checkbox\" checked={done} disabled readOnly />\n {props.children}\n </li>\n );\n }\n return <li>{props.children}</li>;\n },\n [NodeType.HR]: () => <hr />,\n [NodeType.CODE_BLOCK]: (props: { lang?: string; children: React.ReactNode }) => {\n const language = props.lang?.toLowerCase() || 'plaintext';\n const languageClass = language ? `language-${language}` : undefined;\n return (\n <pre className={languageClass}>\n <code className={languageClass} data-language={language}>\n {props.children}\n </code>\n </pre>\n );\n },\n [NodeType.HTML]: (props) => {\n const html = getHtmlString(props.children);\n if (html && isLineBreakHtml(html)) {\n return <br />;\n }\n // HTML blocks are rendered as-is inside a div\n // Note: This is raw HTML from markdown, use dangerouslySetInnerHTML if needed\n return <div data-markdown-html>{props.children}</div>;\n },\n [NodeType.P]: (props) => <p>{props.children}</p>,\n\n // Table elements\n [NodeType.TABLE]: (props) => <table>{props.children}</table>,\n [NodeType.THEAD]: (props) => <thead>{props.children}</thead>,\n [NodeType.TBODY]: (props) => <tbody>{props.children}</tbody>,\n [NodeType.TR]: (props) => <tr>{props.children}</tr>,\n [NodeType.TH]: (props) => {\n const align = props.align as 'left' | 'center' | 'right' | '' | undefined;\n return <th style={align ? { textAlign: align } : undefined}>{props.children}</th>;\n },\n [NodeType.TD]: (props) => {\n const align = props.align as 'left' | 'center' | 'right' | '' | undefined;\n return <td style={align ? { textAlign: align } : undefined}>{props.children}</td>;\n },\n\n // Headings (H1-H6 share the same component with level prop injected by renderer)\n [NodeType.H1]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 1}>{props.children}</Heading>\n ),\n [NodeType.H2]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 2}>{props.children}</Heading>\n ),\n [NodeType.H3]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 3}>{props.children}</Heading>\n ),\n [NodeType.H4]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 4}>{props.children}</Heading>\n ),\n [NodeType.H5]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 5}>{props.children}</Heading>\n ),\n [NodeType.H6]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 6}>{props.children}</Heading>\n ),\n\n // Inline elements\n [NodeType.EM]: (props) => <em>{props.children}</em>,\n [NodeType.STRONG]: (props) => <strong>{props.children}</strong>,\n [NodeType.A]: (props) => (\n <a href={props.href as string} title={props.title as string | undefined}>\n {props.children}\n </a>\n ),\n [NodeType.IMG]: (props) => (\n // biome-ignore lint/performance/noImgElement: Generic markdown package, not Next.js specific. Users can override with next/image.\n <img\n src={props.src as string}\n alt={props.alt as string}\n title={props.title as string | undefined}\n />\n ),\n [NodeType.CODE_SPAN]: (props) => <code>{props.children}</code>,\n [NodeType.DEL]: (props) => <del>{props.children}</del>,\n\n // Math (LaTeX)\n [NodeType.LATEXMATH]: (props) => <span data-math=\"inline\">{props.children}</span>,\n [NodeType.LATEXMATH_DISPLAY]: (props) => <div data-math=\"display\">{props.children}</div>,\n\n // Wiki links\n [NodeType.WIKILINK]: (props) => {\n const target = props.target as string;\n return (\n <a href={`/wiki/${encodeURIComponent(target)}`} data-wikilink={target}>\n {props.children}\n </a>\n );\n },\n\n // Underline (when UNDERLINE parse flag is enabled)\n [NodeType.U]: (props) => <u>{props.children}</u>,\n};\n\nfunction getHtmlString(children: ReactNode): string | null {\n if (typeof children === 'string') {\n return children;\n }\n if (Array.isArray(children) && children.length === 1 && typeof children[0] === 'string') {\n return children[0];\n }\n return null;\n}\n\nfunction isLineBreakHtml(html: string): boolean {\n const trimmed = html.trim().toLowerCase();\n return trimmed === '<br>' || trimmed === '<br/>' || trimmed === '<br />';\n}\n\n/**\n * Minimal component set - only essential elements.\n * Use this when you want to style everything yourself.\n */\nexport const minimalComponents: ComponentMap = {\n [NodeType.P]: (props) => <p>{props.children}</p>,\n [NodeType.H1]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 1}>{props.children}</Heading>\n ),\n [NodeType.H2]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 2}>{props.children}</Heading>\n ),\n [NodeType.H3]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 3}>{props.children}</Heading>\n ),\n [NodeType.H4]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 4}>{props.children}</Heading>\n ),\n [NodeType.H5]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 5}>{props.children}</Heading>\n ),\n [NodeType.H6]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 6}>{props.children}</Heading>\n ),\n [NodeType.STRONG]: (props) => <strong>{props.children}</strong>,\n [NodeType.EM]: (props) => <em>{props.children}</em>,\n [NodeType.A]: (props) => <a href={props.href as string}>{props.children}</a>,\n [NodeType.CODE_SPAN]: (props) => <code>{props.children}</code>,\n};\n","/**\n * @repo/markdown-wasm - AST to React Renderer\n *\n * Converts md4w JSON AST to React components.\n * Pattern B from research: Direct AST-to-React mapping for maximum performance.\n *\n * IMPORTANT: md4w is a WASM module that requires async initialization.\n * This module uses a singleton pattern with cached promise to ensure\n * init() is called exactly once before any parsing.\n *\n * NOTE: For Vercel/Next.js deployment, ensure `serverExternalPackages: ['md4w']`\n * is set in next.config.ts so md4w can properly resolve its WASM files.\n */\n\nimport { init, mdToJSON, NodeType } from 'md4w';\nimport { Fragment, type ReactNode } from 'react';\n\nimport { defaultComponents } from './components';\nimport type { ComponentMap, CreateRendererOptions, MarkdownRendererProps, MDNode } from './types';\nimport { getHeadingLevel, isTextNode } from './types';\n\n// -----------------------------------------------------------------------------\n// WASM Initialization (Singleton Pattern)\n// -----------------------------------------------------------------------------\n\n/**\n * Cached initialization promise.\n * Ensures init() is called exactly once, even with concurrent calls.\n */\nlet initPromise: Promise<void> | null = null;\n\n/**\n * Ensures md4w WASM is initialized before use.\n * Uses singleton pattern - multiple calls return the same promise.\n *\n * When md4w is listed in `serverExternalPackages`, it will be kept as an\n * external package and can properly resolve its WASM file using import.meta.url.\n *\n * @returns Promise that resolves when WASM is ready\n */\nasync function ensureInitialized(): Promise<void> {\n if (!initPromise) {\n // Use \"small\" variant (28KB gzipped) for faster loading\n initPromise = init('small');\n }\n return initPromise;\n}\n\n// -----------------------------------------------------------------------------\n// Internal Helpers\n// -----------------------------------------------------------------------------\n\n/**\n * Recursively renders an AST node to React elements.\n *\n * @param node - The AST node (MDNode or string)\n * @param components - Component map for rendering\n * @param key - React key for list rendering\n * @returns React element or text\n */\nfunction renderNode(\n node: string | MDNode,\n components: ComponentMap,\n key?: string | number\n): ReactNode {\n // Text nodes render as-is\n if (isTextNode(node)) {\n return node;\n }\n\n const { type, props, children } = node;\n\n // Recursively render children first\n const renderedChildren = children?.map((child, index) => renderNode(child, components, index));\n\n // Get component for this node type\n const Component = components[type as NodeType];\n\n if (Component) {\n // Build props based on node type\n const componentProps = buildComponentProps(type, props);\n\n return (\n <Fragment key={key}>{Component({ ...componentProps, children: renderedChildren })}</Fragment>\n );\n }\n\n // Unmapped node types: warn in development and render children only\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n `[@repo/markdown-wasm] Unmapped node type: ${type} (${NodeType[type] ?? 'unknown'})`\n );\n }\n\n return <Fragment key={key}>{renderedChildren}</Fragment>;\n}\n\n/**\n * Builds component props from AST node props.\n * Handles special cases like heading levels.\n */\nfunction buildComponentProps(\n type: number,\n props?: Record<string, unknown>\n): Record<string, unknown> {\n const result = { ...props };\n\n // Add heading level for H1-H6 nodes\n const headingLevel = getHeadingLevel(type as NodeType);\n if (headingLevel !== undefined) {\n result.level = headingLevel;\n }\n\n return result;\n}\n\n// -----------------------------------------------------------------------------\n// Public API\n// -----------------------------------------------------------------------------\n\n/**\n * Renders markdown content to React elements.\n *\n * This is an async Server Component that initializes the WASM module\n * on first use, then parses and renders markdown content.\n *\n * @example\n * ```tsx\n * import { MarkdownRenderer } from \"@repo/markdown-wasm\";\n *\n * // In a Server Component (async)\n * async function MyComponent() {\n * return <MarkdownRenderer content=\"# Hello **world**\" />;\n * }\n *\n * // Or with await\n * export default async function Page() {\n * return (\n * <article>\n * {await MarkdownRenderer({ content: markdown })}\n * </article>\n * );\n * }\n * ```\n */\nexport async function MarkdownRenderer({\n content,\n components: componentOverrides,\n parseFlags,\n className,\n}: MarkdownRendererProps): Promise<ReactNode> {\n // Ensure WASM is initialized before parsing\n await ensureInitialized();\n\n // Parse markdown to AST\n const ast = mdToJSON(content, { parseFlags });\n\n // Merge default components with overrides\n const components: ComponentMap = {\n ...defaultComponents,\n ...componentOverrides,\n };\n\n // Render AST children\n const rendered = ast.children.map((node, index) => renderNode(node, components, index));\n\n // Wrap in container if className provided, otherwise return fragment\n if (className) {\n return <div className={className}>{rendered}</div>;\n }\n\n return <>{rendered}</>;\n}\n\n/**\n * Creates a pre-configured markdown renderer with default settings.\n *\n * Returns an async function suitable for Server Components.\n *\n * @example\n * ```tsx\n * import { createMarkdownRenderer } from \"@repo/markdown-wasm\";\n * import { MyHeading, MyText, MyLink } from \"./components\";\n *\n * const CustomRenderer = createMarkdownRenderer({\n * components: {\n * [NodeType.H1]: ({ level, children }) => <MyHeading level={level}>{children}</MyHeading>,\n * [NodeType.P]: ({ children }) => <MyText>{children}</MyText>,\n * [NodeType.A]: ({ href, children }) => <MyLink href={href}>{children}</MyLink>,\n * },\n * className: \"prose prose-lg\",\n * });\n *\n * async function Article({ markdown }: { markdown: string }) {\n * return <CustomRenderer content={markdown} />;\n * }\n * ```\n */\nexport function createMarkdownRenderer(options: CreateRendererOptions) {\n const {\n components: defaultOverrides,\n parseFlags: defaultParseFlags,\n className: defaultClassName,\n } = options;\n\n return async function ConfiguredMarkdownRenderer({\n content,\n components: instanceOverrides,\n parseFlags: instanceParseFlags,\n className: instanceClassName,\n }: MarkdownRendererProps): Promise<ReactNode> {\n return MarkdownRenderer({\n content,\n components: { ...defaultOverrides, ...instanceOverrides },\n parseFlags: instanceParseFlags ?? defaultParseFlags,\n className: instanceClassName ?? defaultClassName,\n });\n };\n}\n\n/**\n * Renders markdown to React elements without a wrapper component.\n * Useful for embedding markdown content inline.\n *\n * @example\n * ```tsx\n * import { renderMarkdown } from \"@repo/markdown-wasm\";\n *\n * // In an async Server Component\n * const elements = await renderMarkdown(\"**bold** and *italic*\");\n * ```\n */\nexport async function renderMarkdown(\n content: string,\n options?: Omit<MarkdownRendererProps, 'content'>\n): Promise<ReactNode> {\n return MarkdownRenderer({ content, ...options });\n}\n\n/**\n * Explicitly initialize the md4w WASM module.\n * Useful for pre-warming in middleware or layout components.\n *\n * @example\n * ```tsx\n * // In layout.tsx\n * import { initMarkdown } from \"@repo/markdown-wasm\";\n *\n * export default async function RootLayout({ children }) {\n * // Pre-warm WASM for faster first render\n * await initMarkdown();\n * return <html>{children}</html>;\n * }\n * ```\n */\nexport async function initMarkdown(): Promise<void> {\n await ensureInitialized();\n}\n\n// Re-export for convenience\nexport { NodeType } from 'md4w';\n","/**\n * @repo/markdown-wasm - Type definitions\n *\n * Re-exports md4w types with additional utility types for React rendering.\n */\n\nimport type { MDNode, MDTree, NodeType, Options, ParseFlags } from 'md4w';\nimport type { ReactNode } from 'react';\n\n// Re-export md4w types\nexport type { MDNode, MDTree, NodeType, Options, ParseFlags };\n\n/**\n * Props passed to a custom component for an AST node.\n * Each node type has different props available.\n */\nexport interface NodeRenderProps {\n /** The original AST node */\n node: MDNode;\n /** Rendered children (already converted to React elements) */\n children: ReactNode;\n}\n\n/**\n * A component that renders a specific node type.\n * Receives the node props and pre-rendered children.\n */\nexport type NodeComponent<TProps = Record<string, unknown>> = (\n props: TProps & { children: ReactNode }\n) => ReactNode;\n\n/**\n * Generic component function used internally.\n * Accepts any props and children.\n */\nexport type GenericNodeComponent = (\n props: Record<string, unknown> & { children: ReactNode }\n) => ReactNode;\n\n/**\n * Component overrides map keyed by NodeType.\n * Each entry maps a node type number to a component function.\n * Uses GenericNodeComponent internally for flexibility.\n */\nexport type ComponentMap = Partial<Record<NodeType, GenericNodeComponent>>;\n\n/**\n * Utility type to extract props for a specific node type.\n * This provides type-safe access to node-specific properties.\n */\nexport type NodePropsFor<T extends NodeType> = T extends NodeType.CODE_BLOCK\n ? { lang?: string }\n : T extends NodeType.OL\n ? { start?: number }\n : T extends NodeType.LI\n ? { isTask?: boolean; done?: boolean }\n : T extends NodeType.TH | NodeType.TD\n ? { align?: 'left' | 'center' | 'right' | '' }\n : T extends NodeType.A\n ? { href: string; title?: string }\n : T extends NodeType.IMG\n ? { src: string; alt: string; title?: string }\n : T extends NodeType.WIKILINK\n ? { target: string }\n : T extends\n | NodeType.H1\n | NodeType.H2\n | NodeType.H3\n | NodeType.H4\n | NodeType.H5\n | NodeType.H6\n ? { level: 1 | 2 | 3 | 4 | 5 | 6 }\n : Record<string, unknown>;\n\n/**\n * Props for the MarkdownRenderer component.\n */\nexport interface MarkdownRendererProps {\n /** Markdown content to render */\n content: string;\n /** Optional component overrides */\n components?: ComponentMap;\n /** Optional parse flags for md4w */\n parseFlags?: Options['parseFlags'];\n /** Optional className for the wrapper element */\n className?: string;\n}\n\n/**\n * Factory function options for creating a custom renderer.\n */\nexport interface CreateRendererOptions {\n /** Component overrides */\n components?: ComponentMap;\n /** Default parse flags */\n parseFlags?: Options['parseFlags'];\n /** Default wrapper className */\n className?: string;\n}\n\n/**\n * Heading level type (1-6).\n */\nexport type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;\n\n/**\n * Maps NodeType.H1-H6 to numeric heading level.\n */\nexport function getHeadingLevel(type: NodeType): HeadingLevel | undefined {\n const levelMap: Record<number, HeadingLevel> = {\n 21: 1,\n 22: 2,\n 23: 3,\n 24: 4,\n 25: 5,\n 26: 6,\n };\n return levelMap[type];\n}\n\n/**\n * Type guard to check if a node is a text node (string).\n */\nexport function isTextNode(node: string | MDNode): node is string {\n return typeof node === 'string';\n}\n\n/**\n * Type guard to check if a node is an element node (MDNode).\n */\nexport function isElementNode(node: string | MDNode): node is MDNode {\n return typeof node !== 'string' && typeof node === 'object' && 'type' in node;\n}\n","import {\n type ComponentMap,\n defaultComponents,\n getHeadingLevel,\n initMarkdown as initMarkdownWasm,\n type MDNode,\n mdToJSON,\n NodeType,\n} from '@repo/markdown-wasm';\nimport { Fragment, type ReactNode } from 'react';\nimport { type BuiltinLanguage, codeToTokens } from 'shiki';\n\nexport interface DocsMarkdownProps {\n content: string;\n className?: string;\n renderImage?: (props: { src: string; alt: string; title?: string }) => ReactNode;\n}\n\nconst defaultClassName = 'cms-docs-markdown';\nlet markdownInitPromise: Promise<void> | undefined;\n\nexport function markdownStartsWithHeading(markdown: string): boolean {\n return /^\\s*#\\s+/.test(markdown);\n}\n\nexport async function initMarkdown(): Promise<void> {\n markdownInitPromise ??= initMarkdownWasm();\n await markdownInitPromise;\n}\n\nfunction isTextNode(node: string | MDNode): node is string {\n return typeof node === 'string';\n}\n\nfunction buildComponentProps(\n type: number,\n props?: Record<string, unknown>\n): Record<string, unknown> {\n const result = { ...props };\n const headingLevel = getHeadingLevel(type as NodeType);\n\n if (headingLevel !== undefined) {\n result.level = headingLevel;\n }\n\n return result;\n}\n\nfunction normalizeLanguage(lang: unknown): string {\n if (typeof lang !== 'string') {\n return 'text';\n }\n\n const normalized = lang.trim().toLowerCase();\n return normalized || 'text';\n}\n\nasync function renderHighlightedCode(code: string, lang: unknown): Promise<ReactNode> {\n const language = normalizeLanguage(lang);\n\n try {\n const result = await codeToTokens(code, {\n lang: language as BuiltinLanguage,\n theme: 'github-dark',\n });\n\n return (\n <pre\n className={`shiki language-${language}`}\n style={{ backgroundColor: result.bg, color: result.fg }}\n >\n <code className={`shiki_code language-${language}`} data-language={language}>\n {(() => {\n const lineOccurrences = new Map<string, number>();\n\n return result.tokens.map((line, lineIndex) => {\n const isLastLine = lineIndex === result.tokens.length - 1;\n const lineSignature = line\n .map((token) => `${token.offset}:${token.color ?? ''}:${token.content}`)\n .join('|');\n const lineOccurrence = lineOccurrences.get(lineSignature) ?? 0;\n\n lineOccurrences.set(lineSignature, lineOccurrence + 1);\n\n let tokenCursor = 0;\n\n return (\n <Fragment key={`${lineSignature}#${lineOccurrence}`}>\n {line.map((token) => {\n const tokenKey = `${token.offset}:${token.color ?? ''}:${tokenCursor}:${token.content}`;\n tokenCursor += token.content.length;\n\n return (\n <span key={tokenKey} style={{ color: token.color }}>\n {token.content}\n </span>\n );\n })}\n {isLastLine ? null : '\\n'}\n </Fragment>\n );\n });\n })()}\n </code>\n </pre>\n );\n } catch {\n return (\n <pre className={`language-${language}`}>\n <code className={`language-${language}`}>{code}</code>\n </pre>\n );\n }\n}\n\nasync function renderNode(\n node: string | MDNode,\n components: ComponentMap,\n renderImage: DocsMarkdownProps['renderImage'],\n key?: string | number\n): Promise<ReactNode> {\n if (isTextNode(node)) {\n return node;\n }\n\n const { type, props, children } = node;\n\n if (type === NodeType.IMG && renderImage) {\n return (\n <Fragment key={key}>\n {renderImage({\n src: typeof props?.src === 'string' ? props.src : '',\n alt: typeof props?.alt === 'string' ? props.alt : '',\n title: typeof props?.title === 'string' ? props.title : undefined,\n })}\n </Fragment>\n );\n }\n\n if (type === NodeType.CODE_BLOCK) {\n const code = children?.filter(isTextNode).join('') ?? '';\n return <Fragment key={key}>{await renderHighlightedCode(code, props?.lang)}</Fragment>;\n }\n\n const renderedChildren = children\n ? await Promise.all(\n children.map((child, index) => renderNode(child, components, renderImage, index))\n )\n : undefined;\n\n const Component = components[type as NodeType];\n\n if (Component) {\n const componentProps = buildComponentProps(type, props);\n return (\n <Fragment key={key}>{Component({ ...componentProps, children: renderedChildren })}</Fragment>\n );\n }\n\n return <Fragment key={key}>{renderedChildren}</Fragment>;\n}\n\nexport async function DocsMarkdown({\n content,\n className = defaultClassName,\n renderImage,\n}: DocsMarkdownProps) {\n await initMarkdown();\n\n const ast = mdToJSON(content);\n const rendered = await Promise.all(\n ast.children.map((node, index) => renderNode(node, defaultComponents, renderImage, index))\n );\n\n const resolvedClassName =\n className === defaultClassName ? defaultClassName : `${defaultClassName} ${className}`.trim();\n\n return <div className={resolvedClassName}>{rendered}</div>;\n}\n"],"mappings":"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","names":["init","mdToJSON","NodeType","NodeType","Fragment","jsx","Fragment","jsx","jsxs","initMarkdown","isTextNode","NodeType","mdToJSON"]}
|
|
1
|
+
{"version":3,"sources":["../../../../packages/markdown-wasm/src/index.ts","../../../../packages/markdown-wasm/src/components.tsx","../../../../packages/markdown-wasm/src/renderer.tsx","../../../../packages/markdown-wasm/src/types.ts","../../lib/docs-markdown.tsx"],"sourcesContent":["/**\n * @repo/markdown-wasm\n *\n * High-performance markdown rendering using md4w WASM engine\n * with React component mapping support.\n *\n * @example\n * ```tsx\n * import { MarkdownRenderer, createMarkdownRenderer, NodeType } from \"@repo/markdown-wasm\";\n *\n * // Basic usage\n * <MarkdownRenderer content=\"# Hello **world**\" />\n *\n * // With custom components\n * <MarkdownRenderer\n * content={markdown}\n * components={{\n * [NodeType.H1]: ({ children }) => <MyHeading>{children}</MyHeading>,\n * }}\n * />\n *\n * // Factory pattern for reusable renderer\n * const CustomRenderer = createMarkdownRenderer({\n * components: { ... },\n * className: \"prose\",\n * });\n * <CustomRenderer content={markdown} />\n * ```\n */\n\n// Re-export md4w utilities for advanced usage\nexport { init, mdToHtml, mdToJSON, mdToReadableHtml, ParseFlags as Md4wParseFlags } from 'md4w';\n\n// Component exports\nexport { defaultComponents, minimalComponents } from './components';\n// Main renderer exports\nexport {\n createMarkdownRenderer,\n initMarkdown,\n MarkdownRenderer,\n NodeType,\n renderMarkdown,\n} from './renderer';\n// Type exports\nexport type {\n ComponentMap,\n CreateRendererOptions,\n GenericNodeComponent,\n HeadingLevel,\n MarkdownRendererProps,\n MDNode,\n MDTree,\n NodeComponent,\n NodePropsFor,\n NodeRenderProps,\n Options,\n ParseFlags,\n} from './types';\n// Utility exports\nexport { getHeadingLevel, isElementNode, isTextNode } from './types';\n","/**\n * @repo/markdown-wasm - Default Component Mappings\n *\n * Provides semantic HTML components for all md4w node types.\n * Override any of these by passing custom components to MarkdownRenderer.\n */\n\nimport { NodeType } from 'md4w';\nimport type { ReactNode } from 'react';\n\nimport type { ComponentMap, HeadingLevel } from './types';\n\n/**\n * Default heading component.\n * Renders H1-H6 based on level prop.\n */\nfunction Heading({ level, children }: { level: HeadingLevel; children: ReactNode }) {\n const Tag = `h${level}` as const;\n return <Tag>{children}</Tag>;\n}\n\n/**\n * Default components for all md4w node types.\n * These render semantic HTML elements that can be styled with CSS.\n *\n * Props are accessed from a generic props object to satisfy GenericNodeComponent type.\n * Runtime guarantees from md4w ensure the props exist for each node type.\n */\nexport const defaultComponents: ComponentMap = {\n // Block elements\n [NodeType.QUOTE]: (props) => <blockquote>{props.children}</blockquote>,\n [NodeType.UL]: (props) => <ul>{props.children}</ul>,\n [NodeType.OL]: (props) => <ol start={props.start as number | undefined}>{props.children}</ol>,\n [NodeType.LI]: (props) => {\n const isTask = props.isTask as boolean | undefined;\n const done = props.done as boolean | undefined;\n if (isTask) {\n return (\n <li>\n <input type=\"checkbox\" checked={done} disabled readOnly />\n {props.children}\n </li>\n );\n }\n return <li>{props.children}</li>;\n },\n [NodeType.HR]: () => <hr />,\n [NodeType.CODE_BLOCK]: (props: { lang?: string; children: React.ReactNode }) => {\n const language = props.lang?.toLowerCase() || 'plaintext';\n const languageClass = language ? `language-${language}` : undefined;\n return (\n <pre className={languageClass}>\n <code className={languageClass} data-language={language}>\n {props.children}\n </code>\n </pre>\n );\n },\n [NodeType.HTML]: (props) => {\n const html = getHtmlString(props.children);\n if (html && isLineBreakHtml(html)) {\n return <br />;\n }\n // HTML blocks are rendered as-is inside a div\n // Note: This is raw HTML from markdown, use dangerouslySetInnerHTML if needed\n return <div data-markdown-html>{props.children}</div>;\n },\n [NodeType.P]: (props) => <p>{props.children}</p>,\n\n // Table elements\n [NodeType.TABLE]: (props) => <table>{props.children}</table>,\n [NodeType.THEAD]: (props) => <thead>{props.children}</thead>,\n [NodeType.TBODY]: (props) => <tbody>{props.children}</tbody>,\n [NodeType.TR]: (props) => <tr>{props.children}</tr>,\n [NodeType.TH]: (props) => {\n const align = props.align as 'left' | 'center' | 'right' | '' | undefined;\n return <th style={align ? { textAlign: align } : undefined}>{props.children}</th>;\n },\n [NodeType.TD]: (props) => {\n const align = props.align as 'left' | 'center' | 'right' | '' | undefined;\n return <td style={align ? { textAlign: align } : undefined}>{props.children}</td>;\n },\n\n // Headings (H1-H6 share the same component with level prop injected by renderer)\n [NodeType.H1]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 1}>{props.children}</Heading>\n ),\n [NodeType.H2]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 2}>{props.children}</Heading>\n ),\n [NodeType.H3]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 3}>{props.children}</Heading>\n ),\n [NodeType.H4]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 4}>{props.children}</Heading>\n ),\n [NodeType.H5]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 5}>{props.children}</Heading>\n ),\n [NodeType.H6]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 6}>{props.children}</Heading>\n ),\n\n // Inline elements\n [NodeType.EM]: (props) => <em>{props.children}</em>,\n [NodeType.STRONG]: (props) => <strong>{props.children}</strong>,\n [NodeType.A]: (props) => (\n <a href={props.href as string} title={props.title as string | undefined}>\n {props.children}\n </a>\n ),\n [NodeType.IMG]: (props) => (\n // biome-ignore lint/performance/noImgElement: Generic markdown package, not Next.js specific. Users can override with next/image.\n <img\n src={props.src as string}\n alt={props.alt as string}\n title={props.title as string | undefined}\n />\n ),\n [NodeType.CODE_SPAN]: (props) => <code>{props.children}</code>,\n [NodeType.DEL]: (props) => <del>{props.children}</del>,\n\n // Math (LaTeX)\n [NodeType.LATEXMATH]: (props) => <span data-math=\"inline\">{props.children}</span>,\n [NodeType.LATEXMATH_DISPLAY]: (props) => <div data-math=\"display\">{props.children}</div>,\n\n // Wiki links\n [NodeType.WIKILINK]: (props) => {\n const target = props.target as string;\n return (\n <a href={`/wiki/${encodeURIComponent(target)}`} data-wikilink={target}>\n {props.children}\n </a>\n );\n },\n\n // Underline (when UNDERLINE parse flag is enabled)\n [NodeType.U]: (props) => <u>{props.children}</u>,\n};\n\nfunction getHtmlString(children: ReactNode): string | null {\n if (typeof children === 'string') {\n return children;\n }\n if (Array.isArray(children) && children.length === 1 && typeof children[0] === 'string') {\n return children[0];\n }\n return null;\n}\n\nfunction isLineBreakHtml(html: string): boolean {\n const trimmed = html.trim().toLowerCase();\n return trimmed === '<br>' || trimmed === '<br/>' || trimmed === '<br />';\n}\n\n/**\n * Minimal component set - only essential elements.\n * Use this when you want to style everything yourself.\n */\nexport const minimalComponents: ComponentMap = {\n [NodeType.P]: (props) => <p>{props.children}</p>,\n [NodeType.H1]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 1}>{props.children}</Heading>\n ),\n [NodeType.H2]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 2}>{props.children}</Heading>\n ),\n [NodeType.H3]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 3}>{props.children}</Heading>\n ),\n [NodeType.H4]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 4}>{props.children}</Heading>\n ),\n [NodeType.H5]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 5}>{props.children}</Heading>\n ),\n [NodeType.H6]: (props) => (\n <Heading level={(props.level as HeadingLevel) ?? 6}>{props.children}</Heading>\n ),\n [NodeType.STRONG]: (props) => <strong>{props.children}</strong>,\n [NodeType.EM]: (props) => <em>{props.children}</em>,\n [NodeType.A]: (props) => <a href={props.href as string}>{props.children}</a>,\n [NodeType.CODE_SPAN]: (props) => <code>{props.children}</code>,\n};\n","/**\n * @repo/markdown-wasm - AST to React Renderer\n *\n * Converts md4w JSON AST to React components.\n * Pattern B from research: Direct AST-to-React mapping for maximum performance.\n *\n * IMPORTANT: md4w is a WASM module that requires async initialization.\n * This module uses a singleton pattern with cached promise to ensure\n * init() is called exactly once before any parsing.\n *\n * NOTE: For Vercel/Next.js deployment, ensure `serverExternalPackages: ['md4w']`\n * is set in next.config.ts so md4w can properly resolve its WASM files.\n */\n\nimport { init, mdToJSON, NodeType } from 'md4w';\nimport { Fragment, type ReactNode } from 'react';\n\nimport { defaultComponents } from './components';\nimport type { ComponentMap, CreateRendererOptions, MarkdownRendererProps, MDNode } from './types';\nimport { getHeadingLevel, isTextNode } from './types';\n\n// -----------------------------------------------------------------------------\n// WASM Initialization (Singleton Pattern)\n// -----------------------------------------------------------------------------\n\n/**\n * Cached initialization promise.\n * Ensures init() is called exactly once, even with concurrent calls.\n */\nlet initPromise: Promise<void> | null = null;\n\n/**\n * Ensures md4w WASM is initialized before use.\n * Uses singleton pattern - multiple calls return the same promise.\n *\n * When md4w is listed in `serverExternalPackages`, it will be kept as an\n * external package and can properly resolve its WASM file using import.meta.url.\n *\n * @returns Promise that resolves when WASM is ready\n */\nasync function ensureInitialized(): Promise<void> {\n if (!initPromise) {\n // Use \"small\" variant (28KB gzipped) for faster loading\n initPromise = init('small');\n }\n return initPromise;\n}\n\n// -----------------------------------------------------------------------------\n// Internal Helpers\n// -----------------------------------------------------------------------------\n\n/**\n * Recursively renders an AST node to React elements.\n *\n * @param node - The AST node (MDNode or string)\n * @param components - Component map for rendering\n * @param key - React key for list rendering\n * @returns React element or text\n */\nfunction renderNode(\n node: string | MDNode,\n components: ComponentMap,\n key?: string | number\n): ReactNode {\n // Text nodes render as-is\n if (isTextNode(node)) {\n return node;\n }\n\n const { type, props, children } = node;\n\n // Recursively render children first\n const renderedChildren = children?.map((child, index) => renderNode(child, components, index));\n\n // Get component for this node type\n const Component = components[type as NodeType];\n\n if (Component) {\n // Build props based on node type\n const componentProps = buildComponentProps(type, props);\n\n return (\n <Fragment key={key}>{Component({ ...componentProps, children: renderedChildren })}</Fragment>\n );\n }\n\n // Unmapped node types: warn in development and render children only\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n `[@repo/markdown-wasm] Unmapped node type: ${type} (${NodeType[type] ?? 'unknown'})`\n );\n }\n\n return <Fragment key={key}>{renderedChildren}</Fragment>;\n}\n\n/**\n * Builds component props from AST node props.\n * Handles special cases like heading levels.\n */\nfunction buildComponentProps(\n type: number,\n props?: Record<string, unknown>\n): Record<string, unknown> {\n const result = { ...props };\n\n // Add heading level for H1-H6 nodes\n const headingLevel = getHeadingLevel(type as NodeType);\n if (headingLevel !== undefined) {\n result.level = headingLevel;\n }\n\n return result;\n}\n\n// -----------------------------------------------------------------------------\n// Public API\n// -----------------------------------------------------------------------------\n\n/**\n * Renders markdown content to React elements.\n *\n * This is an async Server Component that initializes the WASM module\n * on first use, then parses and renders markdown content.\n *\n * @example\n * ```tsx\n * import { MarkdownRenderer } from \"@repo/markdown-wasm\";\n *\n * // In a Server Component (async)\n * async function MyComponent() {\n * return <MarkdownRenderer content=\"# Hello **world**\" />;\n * }\n *\n * // Or with await\n * export default async function Page() {\n * return (\n * <article>\n * {await MarkdownRenderer({ content: markdown })}\n * </article>\n * );\n * }\n * ```\n */\nexport async function MarkdownRenderer({\n content,\n components: componentOverrides,\n parseFlags,\n className,\n}: MarkdownRendererProps): Promise<ReactNode> {\n // Ensure WASM is initialized before parsing\n await ensureInitialized();\n\n // Parse markdown to AST\n const ast = mdToJSON(content, { parseFlags });\n\n // Merge default components with overrides\n const components: ComponentMap = {\n ...defaultComponents,\n ...componentOverrides,\n };\n\n // Render AST children\n const rendered = ast.children.map((node, index) => renderNode(node, components, index));\n\n // Wrap in container if className provided, otherwise return fragment\n if (className) {\n return <div className={className}>{rendered}</div>;\n }\n\n return <>{rendered}</>;\n}\n\n/**\n * Creates a pre-configured markdown renderer with default settings.\n *\n * Returns an async function suitable for Server Components.\n *\n * @example\n * ```tsx\n * import { createMarkdownRenderer } from \"@repo/markdown-wasm\";\n * import { MyHeading, MyText, MyLink } from \"./components\";\n *\n * const CustomRenderer = createMarkdownRenderer({\n * components: {\n * [NodeType.H1]: ({ level, children }) => <MyHeading level={level}>{children}</MyHeading>,\n * [NodeType.P]: ({ children }) => <MyText>{children}</MyText>,\n * [NodeType.A]: ({ href, children }) => <MyLink href={href}>{children}</MyLink>,\n * },\n * className: \"prose prose-lg\",\n * });\n *\n * async function Article({ markdown }: { markdown: string }) {\n * return <CustomRenderer content={markdown} />;\n * }\n * ```\n */\nexport function createMarkdownRenderer(options: CreateRendererOptions) {\n const {\n components: defaultOverrides,\n parseFlags: defaultParseFlags,\n className: defaultClassName,\n } = options;\n\n return async function ConfiguredMarkdownRenderer({\n content,\n components: instanceOverrides,\n parseFlags: instanceParseFlags,\n className: instanceClassName,\n }: MarkdownRendererProps): Promise<ReactNode> {\n return MarkdownRenderer({\n content,\n components: { ...defaultOverrides, ...instanceOverrides },\n parseFlags: instanceParseFlags ?? defaultParseFlags,\n className: instanceClassName ?? defaultClassName,\n });\n };\n}\n\n/**\n * Renders markdown to React elements without a wrapper component.\n * Useful for embedding markdown content inline.\n *\n * @example\n * ```tsx\n * import { renderMarkdown } from \"@repo/markdown-wasm\";\n *\n * // In an async Server Component\n * const elements = await renderMarkdown(\"**bold** and *italic*\");\n * ```\n */\nexport async function renderMarkdown(\n content: string,\n options?: Omit<MarkdownRendererProps, 'content'>\n): Promise<ReactNode> {\n return MarkdownRenderer({ content, ...options });\n}\n\n/**\n * Explicitly initialize the md4w WASM module.\n * Useful for pre-warming in middleware or layout components.\n *\n * @example\n * ```tsx\n * // In layout.tsx\n * import { initMarkdown } from \"@repo/markdown-wasm\";\n *\n * export default async function RootLayout({ children }) {\n * // Pre-warm WASM for faster first render\n * await initMarkdown();\n * return <html>{children}</html>;\n * }\n * ```\n */\nexport async function initMarkdown(): Promise<void> {\n await ensureInitialized();\n}\n\n// Re-export for convenience\nexport { NodeType } from 'md4w';\n","/**\n * @repo/markdown-wasm - Type definitions\n *\n * Re-exports md4w types with additional utility types for React rendering.\n */\n\nimport type { MDNode, MDTree, NodeType, Options, ParseFlags } from 'md4w';\nimport type { ReactNode } from 'react';\n\n// Re-export md4w types\nexport type { MDNode, MDTree, NodeType, Options, ParseFlags };\n\n/**\n * Props passed to a custom component for an AST node.\n * Each node type has different props available.\n */\nexport interface NodeRenderProps {\n /** The original AST node */\n node: MDNode;\n /** Rendered children (already converted to React elements) */\n children: ReactNode;\n}\n\n/**\n * A component that renders a specific node type.\n * Receives the node props and pre-rendered children.\n */\nexport type NodeComponent<TProps = Record<string, unknown>> = (\n props: TProps & { children: ReactNode }\n) => ReactNode;\n\n/**\n * Generic component function used internally.\n * Accepts any props and children.\n */\nexport type GenericNodeComponent = (\n props: Record<string, unknown> & { children: ReactNode }\n) => ReactNode;\n\n/**\n * Component overrides map keyed by NodeType.\n * Each entry maps a node type number to a component function.\n * Uses GenericNodeComponent internally for flexibility.\n */\nexport type ComponentMap = Partial<Record<NodeType, GenericNodeComponent>>;\n\n/**\n * Utility type to extract props for a specific node type.\n * This provides type-safe access to node-specific properties.\n */\nexport type NodePropsFor<T extends NodeType> = T extends NodeType.CODE_BLOCK\n ? { lang?: string }\n : T extends NodeType.OL\n ? { start?: number }\n : T extends NodeType.LI\n ? { isTask?: boolean; done?: boolean }\n : T extends NodeType.TH | NodeType.TD\n ? { align?: 'left' | 'center' | 'right' | '' }\n : T extends NodeType.A\n ? { href: string; title?: string }\n : T extends NodeType.IMG\n ? { src: string; alt: string; title?: string }\n : T extends NodeType.WIKILINK\n ? { target: string }\n : T extends\n | NodeType.H1\n | NodeType.H2\n | NodeType.H3\n | NodeType.H4\n | NodeType.H5\n | NodeType.H6\n ? { level: 1 | 2 | 3 | 4 | 5 | 6 }\n : Record<string, unknown>;\n\n/**\n * Props for the MarkdownRenderer component.\n */\nexport interface MarkdownRendererProps {\n /** Markdown content to render */\n content: string;\n /** Optional component overrides */\n components?: ComponentMap;\n /** Optional parse flags for md4w */\n parseFlags?: Options['parseFlags'];\n /** Optional className for the wrapper element */\n className?: string;\n}\n\n/**\n * Factory function options for creating a custom renderer.\n */\nexport interface CreateRendererOptions {\n /** Component overrides */\n components?: ComponentMap;\n /** Default parse flags */\n parseFlags?: Options['parseFlags'];\n /** Default wrapper className */\n className?: string;\n}\n\n/**\n * Heading level type (1-6).\n */\nexport type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;\n\n/**\n * Maps NodeType.H1-H6 to numeric heading level.\n */\nexport function getHeadingLevel(type: NodeType): HeadingLevel | undefined {\n const levelMap: Record<number, HeadingLevel> = {\n 21: 1,\n 22: 2,\n 23: 3,\n 24: 4,\n 25: 5,\n 26: 6,\n };\n return levelMap[type];\n}\n\n/**\n * Type guard to check if a node is a text node (string).\n */\nexport function isTextNode(node: string | MDNode): node is string {\n return typeof node === 'string';\n}\n\n/**\n * Type guard to check if a node is an element node (MDNode).\n */\nexport function isElementNode(node: string | MDNode): node is MDNode {\n return typeof node !== 'string' && typeof node === 'object' && 'type' in node;\n}\n","import {\n type ComponentMap,\n defaultComponents,\n getHeadingLevel,\n initMarkdown as initMarkdownWasm,\n type MDNode,\n mdToJSON,\n NodeType,\n} from '@repo/markdown-wasm';\nimport { Fragment, type ReactNode } from 'react';\nimport { type BundledLanguage, bundledLanguages, createHighlighter, type Highlighter } from 'shiki';\n\nexport interface DocsMarkdownProps {\n content: string;\n className?: string;\n renderImage?: (props: {\n src: string;\n alt: string;\n title?: string;\n loading?: 'eager' | 'lazy';\n }) => ReactNode;\n}\n\nconst defaultClassName = 'cms-docs-markdown';\nlet markdownInitPromise: Promise<void> | undefined;\n\n// Pre-load common languages for fast cold-start\n// Less common languages are lazy-loaded on demand\nconst PRELOADED_LANGS: BundledLanguage[] = [\n // Web\n 'typescript',\n 'javascript',\n 'tsx',\n 'jsx',\n 'html',\n 'css',\n 'scss',\n // Systems\n 'c',\n 'cpp',\n 'rust',\n 'go',\n // Enterprise\n 'java',\n 'csharp',\n 'kotlin',\n 'swift',\n // Scripting\n 'python',\n 'ruby',\n 'php',\n 'bash',\n 'shellscript',\n 'powershell',\n // Data\n 'json',\n 'yaml',\n 'toml',\n 'xml',\n 'sql',\n 'graphql',\n // Other\n 'markdown',\n 'dockerfile',\n];\n\n// Cache the highlighter instance to avoid re-loading languages/themes\nlet highlighterPromise: Promise<Highlighter> | undefined;\n\n// Track which languages have been loaded\nconst loadedLanguages = new Set<string>(PRELOADED_LANGS);\n\nasync function getHighlighter(): Promise<Highlighter> {\n highlighterPromise ??= createHighlighter({\n themes: ['github-dark'],\n langs: PRELOADED_LANGS,\n });\n return highlighterPromise;\n}\n\n/**\n * Ensures a language is loaded, lazy-loading if needed.\n * Returns the resolved language name, or null if not a valid language.\n */\nasync function ensureLanguageLoaded(\n highlighter: Highlighter,\n lang: string\n): Promise<BundledLanguage | null> {\n // Already loaded\n if (loadedLanguages.has(lang)) {\n return lang as BundledLanguage;\n }\n\n // Check if it's a valid bundled language\n if (lang in bundledLanguages) {\n try {\n await highlighter.loadLanguage(lang as BundledLanguage);\n loadedLanguages.add(lang);\n return lang as BundledLanguage;\n } catch {\n // Loading failed, will render as plain code\n return null;\n }\n }\n\n // Unknown language, will render as plain code\n return null;\n}\n\n// Pre-warm WASM and Shiki at module load to avoid cold start penalty\n// This runs once when the module is first imported (server startup)\nif (typeof window === 'undefined') {\n // Server-side only: trigger initialization immediately\n markdownInitPromise = initMarkdownWasm();\n highlighterPromise = createHighlighter({\n themes: ['github-dark'],\n langs: PRELOADED_LANGS,\n });\n // Log pre-warming for debugging\n Promise.all([markdownInitPromise, highlighterPromise])\n .then(() =>\n console.log(`[docs-markdown] WASM and Shiki pre-warmed (${PRELOADED_LANGS.length} languages)`)\n )\n .catch((err) => console.error('[docs-markdown] Pre-warm failed:', err));\n}\n\nexport function markdownStartsWithHeading(markdown: string): boolean {\n return /^\\s*#\\s+/.test(markdown);\n}\n\nexport async function initMarkdown(): Promise<void> {\n markdownInitPromise ??= initMarkdownWasm();\n await markdownInitPromise;\n}\n\nfunction isTextNode(node: string | MDNode): node is string {\n return typeof node === 'string';\n}\n\nfunction buildComponentProps(\n type: number,\n props?: Record<string, unknown>\n): Record<string, unknown> {\n const result = { ...props };\n const headingLevel = getHeadingLevel(type as NodeType);\n\n if (headingLevel !== undefined) {\n result.level = headingLevel;\n }\n\n return result;\n}\n\nfunction normalizeLanguage(lang: unknown): string {\n if (typeof lang !== 'string') {\n return '';\n }\n\n const normalized = lang.trim().toLowerCase();\n return normalized;\n}\n\nasync function renderHighlightedCode(code: string, lang: unknown): Promise<ReactNode> {\n const language = normalizeLanguage(lang);\n const displayLanguage = language || 'text';\n\n try {\n const highlighter = await getHighlighter();\n // Ensure language is loaded (lazy-load if needed)\n const resolvedLang = await ensureLanguageLoaded(highlighter, language);\n\n // If language not supported, render plain code\n if (!resolvedLang) {\n return (\n <pre className={`language-${displayLanguage}`}>\n <code className={`language-${displayLanguage}`}>{code}</code>\n </pre>\n );\n }\n\n const result = highlighter.codeToTokens(code, {\n lang: resolvedLang,\n theme: 'github-dark',\n });\n\n return (\n <pre\n className={`shiki language-${displayLanguage}`}\n style={{ backgroundColor: result.bg, color: result.fg }}\n >\n <code className={`shiki_code language-${displayLanguage}`} data-language={displayLanguage}>\n {(() => {\n const lineOccurrences = new Map<string, number>();\n\n return result.tokens.map((line, lineIndex) => {\n const isLastLine = lineIndex === result.tokens.length - 1;\n const lineSignature = line\n .map((token) => `${token.offset}:${token.color ?? ''}:${token.content}`)\n .join('|');\n const lineOccurrence = lineOccurrences.get(lineSignature) ?? 0;\n\n lineOccurrences.set(lineSignature, lineOccurrence + 1);\n\n let tokenCursor = 0;\n\n return (\n <Fragment key={`${lineSignature}#${lineOccurrence}`}>\n {line.map((token) => {\n const tokenKey = `${token.offset}:${token.color ?? ''}:${tokenCursor}:${token.content}`;\n tokenCursor += token.content.length;\n\n return (\n <span key={tokenKey} style={{ color: token.color }}>\n {token.content}\n </span>\n );\n })}\n {isLastLine ? null : '\\n'}\n </Fragment>\n );\n });\n })()}\n </code>\n </pre>\n );\n } catch {\n return (\n <pre className={`language-${displayLanguage}`}>\n <code className={`language-${displayLanguage}`}>{code}</code>\n </pre>\n );\n }\n}\n\nasync function renderNode(\n node: string | MDNode,\n components: ComponentMap,\n renderImage: DocsMarkdownProps['renderImage'],\n imageIndexRef: { current: number },\n key?: string | number\n): Promise<ReactNode> {\n if (isTextNode(node)) {\n return node;\n }\n\n const { type, props, children } = node;\n\n if (type === NodeType.IMG && renderImage) {\n const imageIndex = imageIndexRef.current;\n imageIndexRef.current += 1;\n\n return (\n <Fragment key={key}>\n {renderImage({\n src: typeof props?.src === 'string' ? props.src : '',\n alt: typeof props?.alt === 'string' ? props.alt : '',\n title: typeof props?.title === 'string' ? props.title : undefined,\n loading: imageIndex === 0 ? 'eager' : 'lazy',\n })}\n </Fragment>\n );\n }\n\n if (type === NodeType.CODE_BLOCK) {\n const code = children?.filter(isTextNode).join('') ?? '';\n return <Fragment key={key}>{await renderHighlightedCode(code, props?.lang)}</Fragment>;\n }\n\n const renderedChildren = children\n ? await Promise.all(\n children.map((child, index) =>\n renderNode(child, components, renderImage, imageIndexRef, index)\n )\n )\n : undefined;\n\n const Component = components[type as NodeType];\n\n if (Component) {\n const componentProps = buildComponentProps(type, props);\n return (\n <Fragment key={key}>{Component({ ...componentProps, children: renderedChildren })}</Fragment>\n );\n }\n\n return <Fragment key={key}>{renderedChildren}</Fragment>;\n}\n\nexport async function DocsMarkdown({\n content,\n className = defaultClassName,\n renderImage,\n}: DocsMarkdownProps) {\n await initMarkdown();\n\n const ast = mdToJSON(content);\n const imageIndexRef = { current: 0 };\n const rendered = await Promise.all(\n ast.children.map((node, index) =>\n renderNode(node, defaultComponents, renderImage, imageIndexRef, index)\n )\n );\n\n const resolvedClassName =\n className === defaultClassName ? defaultClassName : `${defaultClassName} ${className}`.trim();\n\n return <div className={resolvedClassName}>{rendered}</div>;\n}\n"],"mappings":";AA+BA,SAAS,QAAAA,OAAM,UAAU,YAAAC,WAAU,kBAAgC,kBAAsB;;;ACxBzF,SAAS,gBAAgB;AAWhB,cAoBD,YApBC;AAFT,SAAS,QAAQ,EAAE,OAAO,SAAS,GAAiD;AAClF,QAAM,MAAM,IAAI,KAAK;AACrB,SAAO,oBAAC,OAAK,UAAS;AACxB;AASO,IAAM,oBAAkC;AAAA;AAAA,EAE7C,CAAC,SAAS,KAAK,GAAG,CAAC,UAAU,oBAAC,gBAAY,gBAAM,UAAS;AAAA,EACzD,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU,oBAAC,QAAI,gBAAM,UAAS;AAAA,EAC9C,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU,oBAAC,QAAG,OAAO,MAAM,OAA8B,gBAAM,UAAS;AAAA,EACxF,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU;AACxB,UAAM,SAAS,MAAM;AACrB,UAAM,OAAO,MAAM;AACnB,QAAI,QAAQ;AACV,aACE,qBAAC,QACC;AAAA,4BAAC,WAAM,MAAK,YAAW,SAAS,MAAM,UAAQ,MAAC,UAAQ,MAAC;AAAA,QACvD,MAAM;AAAA,SACT;AAAA,IAEJ;AACA,WAAO,oBAAC,QAAI,gBAAM,UAAS;AAAA,EAC7B;AAAA,EACA,CAAC,SAAS,EAAE,GAAG,MAAM,oBAAC,QAAG;AAAA,EACzB,CAAC,SAAS,UAAU,GAAG,CAAC,UAAwD;AAC9E,UAAM,WAAW,MAAM,MAAM,YAAY,KAAK;AAC9C,UAAM,gBAAgB,WAAW,YAAY,QAAQ,KAAK;AAC1D,WACE,oBAAC,SAAI,WAAW,eACd,8BAAC,UAAK,WAAW,eAAe,iBAAe,UAC5C,gBAAM,UACT,GACF;AAAA,EAEJ;AAAA,EACA,CAAC,SAAS,IAAI,GAAG,CAAC,UAAU;AAC1B,UAAM,OAAO,cAAc,MAAM,QAAQ;AACzC,QAAI,QAAQ,gBAAgB,IAAI,GAAG;AACjC,aAAO,oBAAC,QAAG;AAAA,IACb;AAGA,WAAO,oBAAC,SAAI,sBAAkB,MAAE,gBAAM,UAAS;AAAA,EACjD;AAAA,EACA,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,oBAAC,OAAG,gBAAM,UAAS;AAAA;AAAA,EAG5C,CAAC,SAAS,KAAK,GAAG,CAAC,UAAU,oBAAC,WAAO,gBAAM,UAAS;AAAA,EACpD,CAAC,SAAS,KAAK,GAAG,CAAC,UAAU,oBAAC,WAAO,gBAAM,UAAS;AAAA,EACpD,CAAC,SAAS,KAAK,GAAG,CAAC,UAAU,oBAAC,WAAO,gBAAM,UAAS;AAAA,EACpD,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU,oBAAC,QAAI,gBAAM,UAAS;AAAA,EAC9C,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU;AACxB,UAAM,QAAQ,MAAM;AACpB,WAAO,oBAAC,QAAG,OAAO,QAAQ,EAAE,WAAW,MAAM,IAAI,QAAY,gBAAM,UAAS;AAAA,EAC9E;AAAA,EACA,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU;AACxB,UAAM,QAAQ,MAAM;AACpB,WAAO,oBAAC,QAAG,OAAO,QAAQ,EAAE,WAAW,MAAM,IAAI,QAAY,gBAAM,UAAS;AAAA,EAC9E;AAAA;AAAA,EAGA,CAAC,SAAS,EAAE,GAAG,CAAC,UACd,oBAAC,WAAQ,OAAQ,MAAM,SAA0B,GAAI,gBAAM,UAAS;AAAA,EAEtE,CAAC,SAAS,EAAE,GAAG,CAAC,UACd,oBAAC,WAAQ,OAAQ,MAAM,SAA0B,GAAI,gBAAM,UAAS;AAAA,EAEtE,CAAC,SAAS,EAAE,GAAG,CAAC,UACd,oBAAC,WAAQ,OAAQ,MAAM,SAA0B,GAAI,gBAAM,UAAS;AAAA,EAEtE,CAAC,SAAS,EAAE,GAAG,CAAC,UACd,oBAAC,WAAQ,OAAQ,MAAM,SAA0B,GAAI,gBAAM,UAAS;AAAA,EAEtE,CAAC,SAAS,EAAE,GAAG,CAAC,UACd,oBAAC,WAAQ,OAAQ,MAAM,SAA0B,GAAI,gBAAM,UAAS;AAAA,EAEtE,CAAC,SAAS,EAAE,GAAG,CAAC,UACd,oBAAC,WAAQ,OAAQ,MAAM,SAA0B,GAAI,gBAAM,UAAS;AAAA;AAAA,EAItE,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU,oBAAC,QAAI,gBAAM,UAAS;AAAA,EAC9C,CAAC,SAAS,MAAM,GAAG,CAAC,UAAU,oBAAC,YAAQ,gBAAM,UAAS;AAAA,EACtD,CAAC,SAAS,CAAC,GAAG,CAAC,UACb,oBAAC,OAAE,MAAM,MAAM,MAAgB,OAAO,MAAM,OACzC,gBAAM,UACT;AAAA,EAEF,CAAC,SAAS,GAAG,GAAG,CAAC;AAAA;AAAA,IAEf;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,MAAM;AAAA,QACX,KAAK,MAAM;AAAA,QACX,OAAO,MAAM;AAAA;AAAA,IACf;AAAA;AAAA,EAEF,CAAC,SAAS,SAAS,GAAG,CAAC,UAAU,oBAAC,UAAM,gBAAM,UAAS;AAAA,EACvD,CAAC,SAAS,GAAG,GAAG,CAAC,UAAU,oBAAC,SAAK,gBAAM,UAAS;AAAA;AAAA,EAGhD,CAAC,SAAS,SAAS,GAAG,CAAC,UAAU,oBAAC,UAAK,aAAU,UAAU,gBAAM,UAAS;AAAA,EAC1E,CAAC,SAAS,iBAAiB,GAAG,CAAC,UAAU,oBAAC,SAAI,aAAU,WAAW,gBAAM,UAAS;AAAA;AAAA,EAGlF,CAAC,SAAS,QAAQ,GAAG,CAAC,UAAU;AAC9B,UAAM,SAAS,MAAM;AACrB,WACE,oBAAC,OAAE,MAAM,SAAS,mBAAmB,MAAM,CAAC,IAAI,iBAAe,QAC5D,gBAAM,UACT;AAAA,EAEJ;AAAA;AAAA,EAGA,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,oBAAC,OAAG,gBAAM,UAAS;AAC9C;AAEA,SAAS,cAAc,UAAoC;AACzD,MAAI,OAAO,aAAa,UAAU;AAChC,WAAO;AAAA,EACT;AACA,MAAI,MAAM,QAAQ,QAAQ,KAAK,SAAS,WAAW,KAAK,OAAO,SAAS,CAAC,MAAM,UAAU;AACvF,WAAO,SAAS,CAAC;AAAA,EACnB;AACA,SAAO;AACT;AAEA,SAAS,gBAAgB,MAAuB;AAC9C,QAAM,UAAU,KAAK,KAAK,EAAE,YAAY;AACxC,SAAO,YAAY,UAAU,YAAY,WAAW,YAAY;AAClE;AAMO,IAAM,oBAAkC;AAAA,EAC7C,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,oBAAC,OAAG,gBAAM,UAAS;AAAA,EAC5C,CAAC,SAAS,EAAE,GAAG,CAAC,UACd,oBAAC,WAAQ,OAAQ,MAAM,SAA0B,GAAI,gBAAM,UAAS;AAAA,EAEtE,CAAC,SAAS,EAAE,GAAG,CAAC,UACd,oBAAC,WAAQ,OAAQ,MAAM,SAA0B,GAAI,gBAAM,UAAS;AAAA,EAEtE,CAAC,SAAS,EAAE,GAAG,CAAC,UACd,oBAAC,WAAQ,OAAQ,MAAM,SAA0B,GAAI,gBAAM,UAAS;AAAA,EAEtE,CAAC,SAAS,EAAE,GAAG,CAAC,UACd,oBAAC,WAAQ,OAAQ,MAAM,SAA0B,GAAI,gBAAM,UAAS;AAAA,EAEtE,CAAC,SAAS,EAAE,GAAG,CAAC,UACd,oBAAC,WAAQ,OAAQ,MAAM,SAA0B,GAAI,gBAAM,UAAS;AAAA,EAEtE,CAAC,SAAS,EAAE,GAAG,CAAC,UACd,oBAAC,WAAQ,OAAQ,MAAM,SAA0B,GAAI,gBAAM,UAAS;AAAA,EAEtE,CAAC,SAAS,MAAM,GAAG,CAAC,UAAU,oBAAC,YAAQ,gBAAM,UAAS;AAAA,EACtD,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU,oBAAC,QAAI,gBAAM,UAAS;AAAA,EAC9C,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,oBAAC,OAAE,MAAM,MAAM,MAAiB,gBAAM,UAAS;AAAA,EACxE,CAAC,SAAS,SAAS,GAAG,CAAC,UAAU,oBAAC,UAAM,gBAAM,UAAS;AACzD;;;ACzKA,SAAS,MAAM,UAAU,YAAAC,iBAAgB;AACzC,SAAS,gBAAgC;;;AC6FlC,SAAS,gBAAgB,MAA0C;AACxE,QAAM,WAAyC;AAAA,IAC7C,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AACA,SAAO,SAAS,IAAI;AACtB;;;AD8IA,SAAS,YAAAC,iBAAgB;AAjLnB,SAwFG,YAAAC,WAxFH,OAAAC,YAAA;AAtDN,IAAI,cAAoC;AAWxC,eAAe,oBAAmC;AAChD,MAAI,CAAC,aAAa;AAEhB,kBAAc,KAAK,OAAO;AAAA,EAC5B;AACA,SAAO;AACT;AAiNA,eAAsB,eAA8B;AAClD,QAAM,kBAAkB;AAC1B;;;AExPA,SAAS,YAAAC,iBAAgC;AACzC,SAA+B,kBAAkB,yBAA2C;AAqKlF,gBAAAC,MA+BM,QAAAC,aA/BN;AAxJV,IAAM,mBAAmB;AACzB,IAAI;AAIJ,IAAM,kBAAqC;AAAA;AAAA,EAEzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AACF;AAGA,IAAI;AAGJ,IAAM,kBAAkB,IAAI,IAAY,eAAe;AAEvD,eAAe,iBAAuC;AACpD,yBAAuB,kBAAkB;AAAA,IACvC,QAAQ,CAAC,aAAa;AAAA,IACtB,OAAO;AAAA,EACT,CAAC;AACD,SAAO;AACT;AAMA,eAAe,qBACb,aACA,MACiC;AAEjC,MAAI,gBAAgB,IAAI,IAAI,GAAG;AAC7B,WAAO;AAAA,EACT;AAGA,MAAI,QAAQ,kBAAkB;AAC5B,QAAI;AACF,YAAM,YAAY,aAAa,IAAuB;AACtD,sBAAgB,IAAI,IAAI;AACxB,aAAO;AAAA,IACT,QAAQ;AAEN,aAAO;AAAA,IACT;AAAA,EACF;AAGA,SAAO;AACT;AAIA,IAAI,OAAO,WAAW,aAAa;AAEjC,wBAAsB,aAAiB;AACvC,uBAAqB,kBAAkB;AAAA,IACrC,QAAQ,CAAC,aAAa;AAAA,IACtB,OAAO;AAAA,EACT,CAAC;AAED,UAAQ,IAAI,CAAC,qBAAqB,kBAAkB,CAAC,EAClD;AAAA,IAAK,MACJ,QAAQ,IAAI,8CAA8C,gBAAgB,MAAM,aAAa;AAAA,EAC/F,EACC,MAAM,CAAC,QAAQ,QAAQ,MAAM,oCAAoC,GAAG,CAAC;AAC1E;AAEO,SAAS,0BAA0B,UAA2B;AACnE,SAAO,WAAW,KAAK,QAAQ;AACjC;AAEA,eAAsBC,gBAA8B;AAClD,0BAAwB,aAAiB;AACzC,QAAM;AACR;AAEA,SAASC,YAAW,MAAuC;AACzD,SAAO,OAAO,SAAS;AACzB;AAEA,SAAS,oBACP,MACA,OACyB;AACzB,QAAM,SAAS,EAAE,GAAG,MAAM;AAC1B,QAAM,eAAe,gBAAgB,IAAgB;AAErD,MAAI,iBAAiB,QAAW;AAC9B,WAAO,QAAQ;AAAA,EACjB;AAEA,SAAO;AACT;AAEA,SAAS,kBAAkB,MAAuB;AAChD,MAAI,OAAO,SAAS,UAAU;AAC5B,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,KAAK,KAAK,EAAE,YAAY;AAC3C,SAAO;AACT;AAEA,eAAe,sBAAsB,MAAc,MAAmC;AACpF,QAAM,WAAW,kBAAkB,IAAI;AACvC,QAAM,kBAAkB,YAAY;AAEpC,MAAI;AACF,UAAM,cAAc,MAAM,eAAe;AAEzC,UAAM,eAAe,MAAM,qBAAqB,aAAa,QAAQ;AAGrE,QAAI,CAAC,cAAc;AACjB,aACE,gBAAAH,KAAC,SAAI,WAAW,YAAY,eAAe,IACzC,0BAAAA,KAAC,UAAK,WAAW,YAAY,eAAe,IAAK,gBAAK,GACxD;AAAA,IAEJ;AAEA,UAAM,SAAS,YAAY,aAAa,MAAM;AAAA,MAC5C,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AAED,WACE,gBAAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,kBAAkB,eAAe;AAAA,QAC5C,OAAO,EAAE,iBAAiB,OAAO,IAAI,OAAO,OAAO,GAAG;AAAA,QAEtD,0BAAAA,KAAC,UAAK,WAAW,uBAAuB,eAAe,IAAI,iBAAe,iBACtE,iBAAM;AACN,gBAAM,kBAAkB,oBAAI,IAAoB;AAEhD,iBAAO,OAAO,OAAO,IAAI,CAAC,MAAM,cAAc;AAC5C,kBAAM,aAAa,cAAc,OAAO,OAAO,SAAS;AACxD,kBAAM,gBAAgB,KACnB,IAAI,CAAC,UAAU,GAAG,MAAM,MAAM,IAAI,MAAM,SAAS,EAAE,IAAI,MAAM,OAAO,EAAE,EACtE,KAAK,GAAG;AACX,kBAAM,iBAAiB,gBAAgB,IAAI,aAAa,KAAK;AAE7D,4BAAgB,IAAI,eAAe,iBAAiB,CAAC;AAErD,gBAAI,cAAc;AAElB,mBACE,gBAAAC,MAACF,WAAA,EACE;AAAA,mBAAK,IAAI,CAAC,UAAU;AACnB,sBAAM,WAAW,GAAG,MAAM,MAAM,IAAI,MAAM,SAAS,EAAE,IAAI,WAAW,IAAI,MAAM,OAAO;AACrF,+BAAe,MAAM,QAAQ;AAE7B,uBACE,gBAAAC,KAAC,UAAoB,OAAO,EAAE,OAAO,MAAM,MAAM,GAC9C,gBAAM,WADE,QAEX;AAAA,cAEJ,CAAC;AAAA,cACA,aAAa,OAAO;AAAA,iBAXR,GAAG,aAAa,IAAI,cAAc,EAYjD;AAAA,UAEJ,CAAC;AAAA,QACH,GAAG,GACL;AAAA;AAAA,IACF;AAAA,EAEJ,QAAQ;AACN,WACE,gBAAAA,KAAC,SAAI,WAAW,YAAY,eAAe,IACzC,0BAAAA,KAAC,UAAK,WAAW,YAAY,eAAe,IAAK,gBAAK,GACxD;AAAA,EAEJ;AACF;AAEA,eAAe,WACb,MACA,YACA,aACA,eACA,KACoB;AACpB,MAAIG,YAAW,IAAI,GAAG;AACpB,WAAO;AAAA,EACT;AAEA,QAAM,EAAE,MAAM,OAAO,SAAS,IAAI;AAElC,MAAI,SAASC,UAAS,OAAO,aAAa;AACxC,UAAM,aAAa,cAAc;AACjC,kBAAc,WAAW;AAEzB,WACE,gBAAAJ,KAACD,WAAA,EACE,sBAAY;AAAA,MACX,KAAK,OAAO,OAAO,QAAQ,WAAW,MAAM,MAAM;AAAA,MAClD,KAAK,OAAO,OAAO,QAAQ,WAAW,MAAM,MAAM;AAAA,MAClD,OAAO,OAAO,OAAO,UAAU,WAAW,MAAM,QAAQ;AAAA,MACxD,SAAS,eAAe,IAAI,UAAU;AAAA,IACxC,CAAC,KANY,GAOf;AAAA,EAEJ;AAEA,MAAI,SAASK,UAAS,YAAY;AAChC,UAAM,OAAO,UAAU,OAAOD,WAAU,EAAE,KAAK,EAAE,KAAK;AACtD,WAAO,gBAAAH,KAACD,WAAA,EAAoB,gBAAM,sBAAsB,MAAM,OAAO,IAAI,KAAnD,GAAqD;AAAA,EAC7E;AAEA,QAAM,mBAAmB,WACrB,MAAM,QAAQ;AAAA,IACZ,SAAS;AAAA,MAAI,CAAC,OAAO,UACnB,WAAW,OAAO,YAAY,aAAa,eAAe,KAAK;AAAA,IACjE;AAAA,EACF,IACA;AAEJ,QAAM,YAAY,WAAW,IAAgB;AAE7C,MAAI,WAAW;AACb,UAAM,iBAAiB,oBAAoB,MAAM,KAAK;AACtD,WACE,gBAAAC,KAACD,WAAA,EAAoB,oBAAU,EAAE,GAAG,gBAAgB,UAAU,iBAAiB,CAAC,KAAjE,GAAmE;AAAA,EAEtF;AAEA,SAAO,gBAAAC,KAACD,WAAA,EAAoB,8BAAN,GAAuB;AAC/C;AAEA,eAAsB,aAAa;AAAA,EACjC;AAAA,EACA,YAAY;AAAA,EACZ;AACF,GAAsB;AACpB,QAAMG,cAAa;AAEnB,QAAM,MAAMG,UAAS,OAAO;AAC5B,QAAM,gBAAgB,EAAE,SAAS,EAAE;AACnC,QAAM,WAAW,MAAM,QAAQ;AAAA,IAC7B,IAAI,SAAS;AAAA,MAAI,CAAC,MAAM,UACtB,WAAW,MAAM,mBAAmB,aAAa,eAAe,KAAK;AAAA,IACvE;AAAA,EACF;AAEA,QAAM,oBACJ,cAAc,mBAAmB,mBAAmB,GAAG,gBAAgB,IAAI,SAAS,GAAG,KAAK;AAE9F,SAAO,gBAAAL,KAAC,SAAI,WAAW,mBAAoB,oBAAS;AACtD;","names":["init","mdToJSON","NodeType","NodeType","Fragment","jsx","Fragment","jsx","jsxs","initMarkdown","isTextNode","NodeType","mdToJSON"]}
|
package/dist/lib/proxy.js
CHANGED
|
@@ -3,13 +3,15 @@ import { NextResponse } from "next/server";
|
|
|
3
3
|
var STATIC_FILE_REGEX = /\.(css|js|map|png|jpg|jpeg|gif|svg|ico|webp|avif|woff|woff2|ttf|eot|txt|xml)$/;
|
|
4
4
|
function isFromAdminPage(request) {
|
|
5
5
|
const referer = request.headers.get("referer");
|
|
6
|
-
if (
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
if (referer) {
|
|
7
|
+
try {
|
|
8
|
+
const refererUrl = new URL(referer);
|
|
9
|
+
return refererUrl.pathname.startsWith("/admin");
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
12
13
|
}
|
|
14
|
+
return request.cookies.get("cms_admin_assets")?.value === "1";
|
|
13
15
|
}
|
|
14
16
|
async function proxyToUpstream(request, pathname, upstream) {
|
|
15
17
|
const upstreamUrl = new URL(pathname, upstream);
|
|
@@ -66,20 +68,6 @@ async function proxyToUpstream(request, pathname, upstream) {
|
|
|
66
68
|
}
|
|
67
69
|
});
|
|
68
70
|
responseHeaders.set("x-proxied-by", "cms-proxy");
|
|
69
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
70
|
-
if (contentType.includes("text/html") && response.body) {
|
|
71
|
-
let text = await response.text();
|
|
72
|
-
const upstreamHost = upstreamUrlObj.host;
|
|
73
|
-
text = text.replaceAll(upstreamOrigin, currentOrigin);
|
|
74
|
-
text = text.replaceAll(`//${upstreamHost}`, `//${request.nextUrl.host}`);
|
|
75
|
-
text = text.replaceAll(`https://${upstreamHost}`, currentOrigin);
|
|
76
|
-
text = text.replaceAll(`http://${upstreamHost}`, currentOrigin);
|
|
77
|
-
return new NextResponse(text, {
|
|
78
|
-
status: response.status,
|
|
79
|
-
statusText: response.statusText,
|
|
80
|
-
headers: responseHeaders
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
71
|
return new NextResponse(response.body, {
|
|
84
72
|
status: response.status,
|
|
85
73
|
statusText: response.statusText,
|
package/dist/lib/proxy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../lib/proxy.ts"],"sourcesContent":["import { type NextRequest, NextResponse } from 'next/server';\n\n/**\n * Configuration options for the CMS proxy.\n */\nexport interface ProxyConfig {\n /**\n * The upstream CMS server URL (e.g., 'https://cms.example.com').\n * Defaults to ADMIN_UPSTREAM_ORIGIN environment variable.\n */\n upstream?: string;\n /**\n * Additional path prefixes to proxy (beyond /admin, /api, /auth).\n */\n additionalPaths?: string[];\n}\n\n// Static file extensions to proxy to upstream\nconst STATIC_FILE_REGEX =\n /\\.(css|js|map|png|jpg|jpeg|gif|svg|ico|webp|avif|woff|woff2|ttf|eot|txt|xml)$/;\n\n/**\n * Check if the request originates from an admin page (via Referer header).\n */\nfunction isFromAdminPage(request: NextRequest): boolean {\n const referer = request.headers.get('referer');\n if (!referer) return false;\n\n try {\n const refererUrl = new URL(referer);\n return refererUrl.pathname.startsWith('/admin');\n } catch {\n return false;\n }\n}\n\n/**\n * Proxy a request to the upstream CMS server with proper cookie handling.\n */\nasync function proxyToUpstream(\n request: NextRequest,\n pathname: string,\n upstream: string\n): Promise<NextResponse> {\n const upstreamUrl = new URL(pathname, upstream);\n upstreamUrl.search = request.nextUrl.search;\n\n // Clone all headers from the request\n const headers = new Headers(request.headers);\n\n // Keep the original host header so the upstream app knows the real origin\n // This is important for auth redirects (WorkOS) to use the correct domain\n // The x-forwarded-* headers provide additional context\n // Fall back to nextUrl.host since the Fetch API treats 'host' as a\n // forbidden request header and NextRequest may not expose it directly.\n headers.set('x-forwarded-host', request.headers.get('host') ?? request.nextUrl.host);\n headers.set('x-forwarded-proto', request.nextUrl.protocol.replace(':', ''));\n headers.set('x-forwarded-for', request.headers.get('x-forwarded-for') ?? '');\n\n let response: Response;\n try {\n response = await fetch(upstreamUrl.toString(), {\n method: request.method,\n headers,\n body: request.body,\n // @ts-expect-error - duplex is required for streaming bodies\n duplex: 'half',\n redirect: 'manual', // Don't follow redirects, let the client handle them\n });\n } catch (err) {\n // Upstream is unreachable (connection refused, DNS failure, timeout, etc.).\n // Return a 503 so the caller receives an explicit signal instead of an\n // unhandled exception — prevents cross-tenant information leakage and keeps\n // the Next.js runtime stable.\n console.error('[cms-proxy] upstream unreachable', upstreamUrl.toString(), err);\n return new NextResponse('Upstream unavailable', { status: 503 });\n }\n\n // Create response with proper header handling\n const responseHeaders = new Headers();\n\n const upstreamUrlObj = new URL(upstream);\n const upstreamOrigin = upstreamUrlObj.origin;\n const currentOrigin = request.nextUrl.origin;\n\n // Copy headers from upstream response\n response.headers.forEach((value, key) => {\n const lowerKey = key.toLowerCase();\n\n // Handle Set-Cookie specially - rewrite domain to current host\n if (lowerKey === 'set-cookie') {\n let modifiedCookie = value;\n\n // Remove Domain attribute so cookie defaults to current host\n modifiedCookie = modifiedCookie.replace(/;\\s*Domain=[^;]*/gi, '');\n\n // Ensure Path is set (usually /admin or /)\n if (!/;\\s*Path=/i.test(modifiedCookie)) {\n modifiedCookie += '; Path=/';\n }\n\n // For secure cookies in production, ensure SameSite is appropriate\n if (!/;\\s*SameSite=/i.test(modifiedCookie)) {\n modifiedCookie += '; SameSite=Lax';\n }\n\n responseHeaders.append(key, modifiedCookie);\n }\n // Handle Location header - rewrite upstream URLs to current host\n else if (lowerKey === 'location') {\n try {\n // Parse the location (handles both absolute and relative URLs)\n const locationUrl = new URL(value, upstream);\n\n // If redirect points to upstream, rewrite to current origin\n if (locationUrl.origin === upstreamOrigin) {\n const newLocation = `${currentOrigin}${locationUrl.pathname}${locationUrl.search}`;\n responseHeaders.set(key, newLocation);\n } else {\n // External redirect (e.g., to WorkOS) — pass through unchanged.\n // The CMS encodes the frontend origin in the OAuth state parameter and\n // uses a handoff token to set the session cookie on the correct domain\n // after the WorkOS callback. Rewriting redirect_uri here would point\n // WorkOS at an unregistered per-deployment URL.\n responseHeaders.set(key, value);\n }\n } catch {\n // If URL parsing fails, keep original\n responseHeaders.set(key, value);\n }\n }\n // Skip headers that cause issues after fetch decompresses the body\n else if (\n lowerKey !== 'transfer-encoding' &&\n lowerKey !== 'content-encoding' &&\n lowerKey !== 'content-length'\n ) {\n responseHeaders.set(key, value);\n }\n });\n\n // Add debug header to verify middleware is running\n responseHeaders.set('x-proxied-by', 'cms-proxy');\n\n // For HTML responses, rewrite upstream URLs in the body\n const contentType = response.headers.get('content-type') ?? '';\n if (contentType.includes('text/html') && response.body) {\n let text = await response.text();\n\n // Get the upstream host for more comprehensive replacement\n const upstreamHost = upstreamUrlObj.host;\n\n // Replace full origin (https://cms.example.com)\n text = text.replaceAll(upstreamOrigin, currentOrigin);\n\n // Replace protocol-relative URLs (//cms.example.com)\n text = text.replaceAll(`//${upstreamHost}`, `//${request.nextUrl.host}`);\n\n // Replace any remaining absolute URLs with the upstream host\n // This catches cases where protocol might differ\n text = text.replaceAll(`https://${upstreamHost}`, currentOrigin);\n text = text.replaceAll(`http://${upstreamHost}`, currentOrigin);\n\n return new NextResponse(text, {\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n });\n }\n\n return new NextResponse(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n });\n}\n\n/**\n * Creates a proxy middleware function for Next.js.\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { createCmsProxy } from 'cms-renderer/lib/proxy';\n *\n * const cmsProxy = createCmsProxy({\n * upstream: process.env.ADMIN_UPSTREAM_ORIGIN,\n * });\n *\n * export async function middleware(request: NextRequest) {\n * return cmsProxy(request);\n * }\n *\n * export const config = {\n * matcher: cmsProxyMatcher,\n * };\n * ```\n */\nexport function createCmsProxy(config: ProxyConfig = {}) {\n const upstream = (config.upstream ?? process.env.ADMIN_UPSTREAM_ORIGIN ?? '').replace(/\\/$/, '');\n const additionalPaths = config.additionalPaths ?? [];\n\n if (!upstream) {\n console.warn(\n '[cms-proxy] No upstream URL configured. Set ADMIN_UPSTREAM_ORIGIN or pass upstream option.'\n );\n }\n\n return async function cmsProxy(request: NextRequest): Promise<NextResponse> {\n if (!upstream) {\n return NextResponse.next();\n }\n\n const { pathname } = request.nextUrl;\n\n // Proxy /admin routes to the upstream CMS\n if (pathname.startsWith('/admin')) {\n return proxyToUpstream(request, pathname, upstream);\n }\n\n // Proxy /api routes to the upstream CMS\n if (pathname.startsWith('/api')) {\n return proxyToUpstream(request, pathname, upstream);\n }\n\n // Proxy auth routes to the upstream CMS (WorkOS callbacks, signin, etc.)\n if (pathname.startsWith('/auth')) {\n return proxyToUpstream(request, pathname, upstream);\n }\n\n // Proxy additional custom paths\n for (const pathPrefix of additionalPaths) {\n if (pathname.startsWith(pathPrefix)) {\n return proxyToUpstream(request, pathname, upstream);\n }\n }\n\n // Only proxy /_next and static files if the request comes from an admin page\n // This prevents breaking the web app's own assets\n if (isFromAdminPage(request)) {\n if (pathname.startsWith('/_next') || STATIC_FILE_REGEX.test(pathname)) {\n return proxyToUpstream(request, pathname, upstream);\n }\n }\n\n return NextResponse.next();\n };\n}\n\n/**\n * Default matcher configuration for the CMS proxy middleware.\n * Use this in your middleware.ts config export.\n *\n * @example\n * ```ts\n * export const config = {\n * matcher: cmsProxyMatcher,\n * };\n * ```\n */\nexport const cmsProxyMatcher = [\n '/admin',\n '/admin/:path*',\n '/api/:path*',\n '/auth/:path*',\n '/_next/:path*',\n '/((?:.*\\\\.(?:css|js|map|png|jpg|jpeg|gif|svg|ico|webp|avif|woff|woff2|ttf|eot|txt|xml))$)',\n];\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../lib/proxy.ts"],"sourcesContent":["import { type NextRequest, NextResponse } from 'next/server';\n\n/**\n * Configuration options for the CMS proxy.\n */\nexport interface ProxyConfig {\n /**\n * The upstream CMS server URL (e.g., 'https://cms.example.com').\n * Defaults to ADMIN_UPSTREAM_ORIGIN environment variable.\n */\n upstream?: string;\n /**\n * Additional path prefixes to proxy (beyond /admin, /api, /auth).\n */\n additionalPaths?: string[];\n}\n\n// Static file extensions to proxy to upstream\nconst STATIC_FILE_REGEX =\n /\\.(css|js|map|png|jpg|jpeg|gif|svg|ico|webp|avif|woff|woff2|ttf|eot|txt|xml)$/;\n\n/**\n * Decide whether a framework/static asset request belongs to the proxied CMS UI.\n *\n * `Referer` wins when present because the docs site can render both the admin UI\n * and preview pages on the same host. The preview iframe shares the\n * `cms_admin_assets` cookie, so using the cookie first would incorrectly send\n * preview assets to the CMS upstream.\n */\nfunction isFromAdminPage(request: NextRequest): boolean {\n const referer = request.headers.get('referer');\n if (referer) {\n try {\n const refererUrl = new URL(referer);\n return refererUrl.pathname.startsWith('/admin');\n } catch {\n return false;\n }\n }\n\n // Fall back to the cookie only when the browser omits Referer entirely.\n return request.cookies.get('cms_admin_assets')?.value === '1';\n}\n\n/**\n * Proxy a request to the upstream CMS server with proper cookie handling.\n */\nasync function proxyToUpstream(\n request: NextRequest,\n pathname: string,\n upstream: string\n): Promise<NextResponse> {\n const upstreamUrl = new URL(pathname, upstream);\n upstreamUrl.search = request.nextUrl.search;\n\n // Clone all headers from the request\n const headers = new Headers(request.headers);\n\n // Keep the original host header so the upstream app knows the real origin\n // This is important for auth redirects (WorkOS) to use the correct domain\n // The x-forwarded-* headers provide additional context\n // Fall back to nextUrl.host since the Fetch API treats 'host' as a\n // forbidden request header and NextRequest may not expose it directly.\n headers.set('x-forwarded-host', request.headers.get('host') ?? request.nextUrl.host);\n headers.set('x-forwarded-proto', request.nextUrl.protocol.replace(':', ''));\n headers.set('x-forwarded-for', request.headers.get('x-forwarded-for') ?? '');\n\n let response: Response;\n try {\n response = await fetch(upstreamUrl.toString(), {\n method: request.method,\n headers,\n body: request.body,\n // @ts-expect-error - duplex is required for streaming bodies\n duplex: 'half',\n redirect: 'manual', // Don't follow redirects, let the client handle them\n });\n } catch (err) {\n // Upstream is unreachable (connection refused, DNS failure, timeout, etc.).\n // Return a 503 so the caller receives an explicit signal instead of an\n // unhandled exception — prevents cross-tenant information leakage and keeps\n // the Next.js runtime stable.\n console.error('[cms-proxy] upstream unreachable', upstreamUrl.toString(), err);\n return new NextResponse('Upstream unavailable', { status: 503 });\n }\n\n // Create response with proper header handling\n const responseHeaders = new Headers();\n\n const upstreamUrlObj = new URL(upstream);\n const upstreamOrigin = upstreamUrlObj.origin;\n const currentOrigin = request.nextUrl.origin;\n\n // Copy headers from upstream response\n response.headers.forEach((value, key) => {\n const lowerKey = key.toLowerCase();\n\n // Handle Set-Cookie specially - rewrite domain to current host\n if (lowerKey === 'set-cookie') {\n let modifiedCookie = value;\n\n // Remove Domain attribute so cookie defaults to current host\n modifiedCookie = modifiedCookie.replace(/;\\s*Domain=[^;]*/gi, '');\n\n // Ensure Path is set (usually /admin or /)\n if (!/;\\s*Path=/i.test(modifiedCookie)) {\n modifiedCookie += '; Path=/';\n }\n\n // For secure cookies in production, ensure SameSite is appropriate\n if (!/;\\s*SameSite=/i.test(modifiedCookie)) {\n modifiedCookie += '; SameSite=Lax';\n }\n\n responseHeaders.append(key, modifiedCookie);\n }\n // Handle Location header - rewrite upstream URLs to current host\n else if (lowerKey === 'location') {\n try {\n // Parse the location (handles both absolute and relative URLs)\n const locationUrl = new URL(value, upstream);\n\n // If redirect points to upstream, rewrite to current origin\n if (locationUrl.origin === upstreamOrigin) {\n const newLocation = `${currentOrigin}${locationUrl.pathname}${locationUrl.search}`;\n responseHeaders.set(key, newLocation);\n } else {\n // External redirect (e.g., to WorkOS) — pass through unchanged.\n // The CMS encodes the frontend origin in the OAuth state parameter and\n // uses a handoff token to set the session cookie on the correct domain\n // after the WorkOS callback. Rewriting redirect_uri here would point\n // WorkOS at an unregistered per-deployment URL.\n responseHeaders.set(key, value);\n }\n } catch {\n // If URL parsing fails, keep original\n responseHeaders.set(key, value);\n }\n }\n // Skip headers that cause issues after fetch decompresses the body\n else if (\n lowerKey !== 'transfer-encoding' &&\n lowerKey !== 'content-encoding' &&\n lowerKey !== 'content-length'\n ) {\n responseHeaders.set(key, value);\n }\n });\n\n // Add debug header to verify middleware is running\n responseHeaders.set('x-proxied-by', 'cms-proxy');\n\n return new NextResponse(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n });\n}\n\n/**\n * Creates a proxy middleware function for Next.js.\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { createCmsProxy } from 'cms-renderer/lib/proxy';\n *\n * const cmsProxy = createCmsProxy({\n * upstream: process.env.ADMIN_UPSTREAM_ORIGIN,\n * });\n *\n * export async function middleware(request: NextRequest) {\n * return cmsProxy(request);\n * }\n *\n * export const config = {\n * matcher: cmsProxyMatcher,\n * };\n * ```\n */\nexport function createCmsProxy(config: ProxyConfig = {}) {\n const upstream = (config.upstream ?? process.env.ADMIN_UPSTREAM_ORIGIN ?? '').replace(/\\/$/, '');\n const additionalPaths = config.additionalPaths ?? [];\n\n if (!upstream) {\n console.warn(\n '[cms-proxy] No upstream URL configured. Set ADMIN_UPSTREAM_ORIGIN or pass upstream option.'\n );\n }\n\n return async function cmsProxy(request: NextRequest): Promise<NextResponse> {\n if (!upstream) {\n return NextResponse.next();\n }\n\n const { pathname } = request.nextUrl;\n\n // Proxy /admin routes to the upstream CMS\n if (pathname.startsWith('/admin')) {\n return proxyToUpstream(request, pathname, upstream);\n }\n\n // Proxy /api routes to the upstream CMS\n if (pathname.startsWith('/api')) {\n return proxyToUpstream(request, pathname, upstream);\n }\n\n // Proxy auth routes to the upstream CMS (WorkOS callbacks, signin, etc.)\n if (pathname.startsWith('/auth')) {\n return proxyToUpstream(request, pathname, upstream);\n }\n\n // Proxy additional custom paths\n for (const pathPrefix of additionalPaths) {\n if (pathname.startsWith(pathPrefix)) {\n return proxyToUpstream(request, pathname, upstream);\n }\n }\n\n // Only proxy /_next and static files if the request comes from an admin page\n // This prevents breaking the web app's own assets\n if (isFromAdminPage(request)) {\n if (pathname.startsWith('/_next') || STATIC_FILE_REGEX.test(pathname)) {\n return proxyToUpstream(request, pathname, upstream);\n }\n }\n\n return NextResponse.next();\n };\n}\n\n/**\n * Default matcher configuration for the CMS proxy middleware.\n * Use this in your middleware.ts config export.\n *\n * @example\n * ```ts\n * export const config = {\n * matcher: cmsProxyMatcher,\n * };\n * ```\n */\nexport const cmsProxyMatcher = [\n '/admin',\n '/admin/:path*',\n '/api/:path*',\n '/auth/:path*',\n '/_next/:path*',\n '/((?:.*\\\\.(?:css|js|map|png|jpg|jpeg|gif|svg|ico|webp|avif|woff|woff2|ttf|eot|txt|xml))$)',\n];\n"],"mappings":"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","names":[]}
|
package/dist/lib/renderer.js
CHANGED
|
@@ -459,8 +459,18 @@ function createCmsClient(options) {
|
|
|
459
459
|
]
|
|
460
460
|
});
|
|
461
461
|
}
|
|
462
|
+
var clientCache = /* @__PURE__ */ new Map();
|
|
463
|
+
function getClientCacheKey(options) {
|
|
464
|
+
return `${options.cmsUrl}|${options.apiKey ?? ""}|${options.websiteId ?? ""}`;
|
|
465
|
+
}
|
|
462
466
|
function getCmsClient(options) {
|
|
463
|
-
|
|
467
|
+
const cacheKey = getClientCacheKey(options);
|
|
468
|
+
let client = clientCache.get(cacheKey);
|
|
469
|
+
if (!client) {
|
|
470
|
+
client = createCmsClient(options);
|
|
471
|
+
clientCache.set(cacheKey, client);
|
|
472
|
+
}
|
|
473
|
+
return client;
|
|
464
474
|
}
|
|
465
475
|
|
|
466
476
|
// lib/renderer.tsx
|
|
@@ -518,21 +528,16 @@ async function ParametricRoutePage({
|
|
|
518
528
|
if (route.state !== "Live") {
|
|
519
529
|
notFound();
|
|
520
530
|
}
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
return result.block;
|
|
525
|
-
} catch (error) {
|
|
526
|
-
console.error(`Failed to fetch block ${blockId}:`, error);
|
|
527
|
-
return null;
|
|
528
|
-
}
|
|
531
|
+
const blockResultsPromise = client.block.getByIds.query({ websiteId, ids: route.block_ids }).catch((error) => {
|
|
532
|
+
console.error("Failed to fetch blocks:", error);
|
|
533
|
+
return [];
|
|
529
534
|
});
|
|
530
535
|
const generatedBlocksPromise = aiPreviewIndex !== null ? client.block.getGeneratedByBlockIds.query({ websiteId, blockIds: route.block_ids }).catch((error) => {
|
|
531
536
|
console.error("Failed to fetch generated blocks:", error);
|
|
532
537
|
return { generatedBlocks: {} };
|
|
533
538
|
}) : Promise.resolve({ generatedBlocks: {} });
|
|
534
539
|
const [blockResults, { generatedBlocks }] = await Promise.all([
|
|
535
|
-
|
|
540
|
+
blockResultsPromise,
|
|
536
541
|
generatedBlocksPromise
|
|
537
542
|
]);
|
|
538
543
|
const blocks = [];
|