@typecms/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -0
- package/dist/client-BW6BWpfF.d.mts +92 -0
- package/dist/flags/index.d.mts +100 -0
- package/dist/flags/index.mjs +99 -0
- package/dist/flags/index.mjs.map +1 -0
- package/dist/index.d.mts +67 -0
- package/dist/index.mjs +174 -0
- package/dist/index.mjs.map +1 -0
- package/dist/overlay-CLYAeNwg.d.mts +43 -0
- package/dist/preview/dom.d.mts +76 -0
- package/dist/preview/dom.mjs +352 -0
- package/dist/preview/dom.mjs.map +1 -0
- package/dist/preview/index.d.mts +438 -0
- package/dist/preview/index.mjs +736 -0
- package/dist/preview/index.mjs.map +1 -0
- package/dist/preview/next.d.mts +99 -0
- package/dist/preview/next.mjs +31 -0
- package/dist/preview/next.mjs.map +1 -0
- package/dist/preview/svelte.d.mts +99 -0
- package/dist/preview/svelte.mjs +437 -0
- package/dist/preview/svelte.mjs.map +1 -0
- package/dist/preview/vue.d.mts +113 -0
- package/dist/preview/vue.mjs +587 -0
- package/dist/preview/vue.mjs.map +1 -0
- package/dist/types/index.d.mts +358 -0
- package/dist/types/index.mjs +8 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/types-BNHDdA2G.d.mts +117 -0
- package/package.json +90 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/preview/client.ts","../../src/preview/overlay.ts","../../src/preview/sourcemap.ts","../../src/preview/richtext.ts","../../src/preview/react.tsx"],"sourcesContent":["/**\n * Preview Client\n *\n * Core client for handling live preview PostMessage communication.\n * This runs on the preview site (your frontend) inside an iframe.\n */\n\nimport type {\n PreviewConfig,\n PreviewState,\n PreviewMessage,\n PreviewUpdateMessage,\n PreviewInitMessage,\n PreviewReadyMessage,\n PreviewFocusMessage,\n} from './types'\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nconst DEFAULT_ALLOWED_ORIGINS = [\n 'https://app.typecms.com',\n 'https://staging.typecms.com',\n 'http://localhost:3000',\n 'http://localhost:3001',\n]\n\n// =============================================================================\n// Preview Client\n// =============================================================================\n\nexport type PreviewListener = (state: PreviewState) => void\n\n/**\n * Create a preview client instance.\n *\n * This handles PostMessage communication between the Type CMS editor\n * and your preview site. The client runs in the preview iframe.\n *\n * @example\n * ```typescript\n * import { createPreviewClient } from '@typecms/sdk/preview'\n *\n * const preview = createPreviewClient({\n * debug: true,\n * onUpdate: (data) => {\n * console.log('Value changed:', data.path, data.value)\n * },\n * })\n *\n * // Start listening for messages\n * preview.connect()\n *\n * // Get current preview state\n * const state = preview.getState()\n *\n * // Cleanup when done\n * preview.disconnect()\n * ```\n */\nexport function createPreviewClient(config: PreviewConfig = {}) {\n const {\n allowedOrigins = DEFAULT_ALLOWED_ORIGINS,\n onInit,\n onUpdate,\n onNavigate,\n onFocus,\n debug = false,\n } = config\n\n // Internal state\n let state: PreviewState = {\n enabled: false,\n entryId: null,\n projectId: null,\n locale: null,\n typeId: null,\n values: {},\n focusedPath: null,\n }\n\n const listeners = new Set<PreviewListener>()\n let isConnected = false\n let parentOrigin: string | null = null\n\n // ==========================================================================\n // Internal Helpers\n // ==========================================================================\n\n function log(...args: unknown[]) {\n if (debug) {\n console.log('[TypeCMS Preview]', ...args)\n }\n }\n\n function setState(updates: Partial<PreviewState>) {\n state = { ...state, ...updates }\n listeners.forEach((listener) => listener(state))\n }\n\n function isAllowedOrigin(origin: string): boolean {\n return allowedOrigins.some((allowed) => {\n if (allowed === '*') return true\n // A trailing \"/*\" historically read as \"this origin\". Origins have no\n // path, so a startsWith() prefix match is unsafe — \"https://app.x.com/*\"\n // would also match the hostile origin \"https://app.x.com.evil.com\".\n // Treat \"/*\" as the exact origin (strip it) and always compare for\n // equality.\n const expected = allowed.endsWith('/*') ? allowed.slice(0, -2) : allowed\n return origin === expected\n })\n }\n\n function sendMessage(message: PreviewMessage) {\n if (!parentOrigin) {\n log('Cannot send message: no parent origin')\n return\n }\n\n if (typeof window === 'undefined' || !window.parent) {\n log('Cannot send message: not in iframe')\n return\n }\n\n log('Sending message:', message.type)\n window.parent.postMessage(message, parentOrigin)\n }\n\n // ==========================================================================\n // Message Handler\n // ==========================================================================\n\n function handleMessage(event: MessageEvent) {\n // Validate origin\n if (!isAllowedOrigin(event.origin)) {\n log('Ignored message from untrusted origin:', event.origin)\n return\n }\n\n // Validate message structure\n const message = event.data as PreviewMessage\n if (!message?.type?.startsWith('typecms:preview:')) {\n return\n }\n\n log('Received message:', message.type, message)\n parentOrigin = event.origin\n\n switch (message.type) {\n case 'typecms:preview:init': {\n const initMsg = message as PreviewInitMessage\n setState({\n enabled: true,\n entryId: initMsg.payload.entryId,\n projectId: initMsg.payload.projectId,\n locale: initMsg.payload.locale,\n typeId: initMsg.payload.typeId,\n values: {},\n focusedPath: null,\n })\n onInit?.(initMsg.payload)\n\n // Send ready response\n const readyMessage: PreviewReadyMessage = {\n type: 'typecms:preview:ready',\n timestamp: Date.now(),\n payload: {\n pathname: typeof window !== 'undefined' ? window.location.pathname : '/',\n },\n }\n sendMessage(readyMessage)\n break\n }\n\n case 'typecms:preview:update': {\n const updateMsg = message as PreviewUpdateMessage\n\n // If full values provided, replace entirely\n if (updateMsg.payload.values) {\n setState({ values: updateMsg.payload.values })\n } else {\n // Otherwise, merge the single path update\n const newValues = { ...state.values }\n setNestedValue(newValues, updateMsg.payload.path, updateMsg.payload.value)\n setState({ values: newValues })\n }\n\n onUpdate?.(updateMsg.payload)\n break\n }\n\n case 'typecms:preview:navigate': {\n onNavigate?.(message.payload.url)\n break\n }\n\n case 'typecms:preview:focus': {\n setState({ focusedPath: message.payload.path })\n onFocus?.(message.payload.path)\n break\n }\n }\n }\n\n // ==========================================================================\n // Public API\n // ==========================================================================\n\n return {\n /**\n * Start listening for preview messages.\n * Call this on mount.\n */\n connect() {\n if (typeof window === 'undefined') {\n log('Cannot connect: not in browser')\n return\n }\n\n if (isConnected) {\n log('Already connected')\n return\n }\n\n log('Connecting...')\n window.addEventListener('message', handleMessage)\n isConnected = true\n\n // Announce readiness to the embedding editor. The editor waits for a\n // `ready` before sending `init`, and we don't yet know its origin\n // (parentOrigin is only learned from an incoming message) — so without\n // this hello the handshake would deadlock. The payload is just our\n // pathname (non-sensitive), so it is safe to offer it to every\n // configured candidate origin; the browser delivers it only to the one\n // that actually matches the parent.\n if (window.parent && window.parent !== (window as unknown as Window)) {\n const hello: PreviewReadyMessage = {\n type: 'typecms:preview:ready',\n timestamp: Date.now(),\n payload: { pathname: window.location?.pathname ?? '/' },\n }\n for (const allowed of allowedOrigins) {\n const target = allowed === '*'\n ? '*'\n : allowed.endsWith('/*') ? allowed.slice(0, -2) : allowed\n try {\n window.parent.postMessage(hello, target)\n } catch {\n // Invalid target origin string — skip this candidate.\n }\n }\n }\n },\n\n /**\n * Stop listening for preview messages.\n * Call this on unmount.\n */\n disconnect() {\n if (typeof window === 'undefined') return\n\n log('Disconnecting...')\n window.removeEventListener('message', handleMessage)\n isConnected = false\n parentOrigin = null\n setState({\n enabled: false,\n entryId: null,\n projectId: null,\n locale: null,\n typeId: null,\n values: {},\n focusedPath: null,\n })\n },\n\n /**\n * Get current preview state.\n */\n getState(): PreviewState {\n return state\n },\n\n /**\n * Check if preview mode is active.\n */\n isEnabled(): boolean {\n return state.enabled\n },\n\n /**\n * Get a preview value by path.\n * Returns undefined if no preview value exists.\n */\n getValue<T = unknown>(path: string): T | undefined {\n return getNestedValue(state.values, path) as T | undefined\n },\n\n /**\n * Subscribe to state changes.\n */\n subscribe(listener: PreviewListener): () => void {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n\n /**\n * Notify the editor that the preview has navigated.\n */\n notifyNavigation(pathname: string) {\n const message: PreviewReadyMessage = {\n type: 'typecms:preview:ready',\n timestamp: Date.now(),\n payload: { pathname },\n }\n sendMessage(message)\n },\n\n /**\n * Notify the editor that a field was clicked in the preview.\n * The editor will scroll to the corresponding field.\n *\n * @example\n * ```typescript\n * // On click of an element with data-preview-field attribute:\n * const path = el.closest('[data-preview-field]')?.dataset.previewField\n * if (path) preview.notifyFieldFocus(path)\n * ```\n */\n notifyFieldFocus(path: string) {\n const message: PreviewFocusMessage = {\n type: 'typecms:preview:focus',\n timestamp: Date.now(),\n payload: { path },\n }\n sendMessage(message)\n },\n }\n}\n\n// =============================================================================\n// Utility Functions\n// =============================================================================\n\n/**\n * Set a nested value by path (e.g., \"content.title\")\n */\nconst UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])\n\nfunction setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {\n const parts = path.split('.')\n let current: Record<string, unknown> = obj\n\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]\n // Handle array notation like \"items[0]\"\n const arrayMatch = part.match(/^(\\w+)\\[(\\d+)\\]$/)\n\n if (arrayMatch) {\n const [, name, indexStr] = arrayMatch\n const index = parseInt(indexStr, 10)\n if (UNSAFE_KEYS.has(name)) continue\n if (!current[name]) {\n current[name] = []\n }\n const arr = current[name] as unknown[]\n if (!arr[index]) {\n arr[index] = {}\n }\n current = arr[index] as Record<string, unknown>\n } else {\n if (UNSAFE_KEYS.has(part)) continue\n if (!current[part] || typeof current[part] !== 'object') {\n current[part] = {}\n }\n current = current[part] as Record<string, unknown>\n }\n }\n\n const lastPart = parts[parts.length - 1]\n const arrayMatch = lastPart.match(/^(\\w+)\\[(\\d+)\\]$/)\n\n if (arrayMatch) {\n const [, name, indexStr] = arrayMatch\n const index = parseInt(indexStr, 10)\n if (UNSAFE_KEYS.has(name)) return\n if (!current[name]) {\n current[name] = []\n }\n (current[name] as unknown[])[index] = value\n } else {\n if (UNSAFE_KEYS.has(lastPart)) return\n current[lastPart] = value\n }\n}\n\n/**\n * Get a nested value by path (supports \"items[0].name\" array notation)\n */\nexport function getNestedValue(obj: Record<string, unknown>, path: string): unknown {\n const parts = path.split('.')\n let current: unknown = obj\n\n for (const part of parts) {\n if (current === null || current === undefined) {\n return undefined\n }\n\n // Handle array notation like \"items[0]\"\n const arrayMatch = part.match(/^(\\w+)\\[(\\d+)\\]$/)\n\n if (arrayMatch) {\n const [, name, indexStr] = arrayMatch\n const index = parseInt(indexStr, 10)\n const arr = (current as Record<string, unknown>)[name] as unknown[] | undefined\n if (!arr) return undefined\n current = arr[index]\n } else {\n current = (current as Record<string, unknown>)[part]\n }\n }\n\n return current\n}\n\n/**\n * Deep merge two objects\n */\nexport function deepMerge<T extends Record<string, unknown>>(\n target: T,\n source: Record<string, unknown>\n): T {\n const result = { ...target }\n\n for (const key of Object.keys(source)) {\n const sourceValue = source[key]\n const targetValue = result[key as keyof T]\n\n if (\n sourceValue &&\n typeof sourceValue === 'object' &&\n !Array.isArray(sourceValue) &&\n targetValue &&\n typeof targetValue === 'object' &&\n !Array.isArray(targetValue)\n ) {\n result[key as keyof T] = deepMerge(\n targetValue as Record<string, unknown>,\n sourceValue as Record<string, unknown>\n ) as T[keyof T]\n } else {\n result[key as keyof T] = sourceValue as T[keyof T]\n }\n }\n\n return result\n}\n","/**\n * Preview Overlay\n *\n * Draws a highlight box over the element whose `data-preview-field`\n * matches the field currently focused in the Type CMS editor.\n * Zero-markup alternative to hand-rolling highlights with useFocusedField().\n */\n\nimport type { PreviewState } from './types'\n\nexport interface PreviewOverlayOptions {\n /** Outline color of the highlight box */\n color?: string\n /** z-index of the highlight box */\n zIndex?: number\n /** Scroll the highlighted element into view when focus changes */\n scrollIntoView?: boolean\n /** Attribute used to locate field elements */\n attribute?: string\n}\n\ninterface OverlayClient {\n subscribe(listener: (state: PreviewState) => void): () => void\n getState(): PreviewState\n}\n\nconst DEFAULT_OPTIONS: Required<PreviewOverlayOptions> = {\n color: '#3b82f6',\n zIndex: 9999,\n scrollIntoView: true,\n attribute: 'data-preview-field',\n}\n\n/**\n * Attach a focus-highlight overlay to the document.\n *\n * Subscribes to the preview client's state; whenever `focusedPath` changes,\n * positions a non-interactive highlight box over the matching\n * `[data-preview-field]` element. Returns a detach function.\n *\n * @example\n * ```typescript\n * const preview = createPreviewClient()\n * preview.connect()\n * const detach = attachPreviewOverlay(preview)\n * // ...on teardown\n * detach()\n * ```\n */\nexport function attachPreviewOverlay(\n client: OverlayClient,\n options: PreviewOverlayOptions = {},\n): () => void {\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n return () => {}\n }\n\n const { color, zIndex, scrollIntoView, attribute } = { ...DEFAULT_OPTIONS, ...options }\n\n const box = document.createElement('div')\n box.setAttribute('data-typecms-preview-overlay', '')\n Object.assign(box.style, {\n position: 'fixed',\n display: 'none',\n pointerEvents: 'none',\n boxSizing: 'border-box',\n border: `2px solid ${color}`,\n borderRadius: '4px',\n zIndex: String(zIndex),\n transition: 'top 120ms ease, left 120ms ease, width 120ms ease, height 120ms ease',\n })\n document.body.appendChild(box)\n\n let target: Element | null = null\n let lastPath: string | null = null\n\n function position() {\n if (!target || !target.isConnected) {\n box.style.display = 'none'\n return\n }\n const rect = target.getBoundingClientRect()\n Object.assign(box.style, {\n display: 'block',\n top: `${rect.top - 2}px`,\n left: `${rect.left - 2}px`,\n width: `${rect.width + 4}px`,\n height: `${rect.height + 4}px`,\n })\n }\n\n function update(state: PreviewState) {\n const path = state.enabled ? state.focusedPath : null\n if (path === lastPath) return\n lastPath = path\n\n target = path\n ? document.querySelector(`[${attribute}=\"${CSS.escape(path)}\"]`)\n : null\n\n if (target && scrollIntoView) {\n target.scrollIntoView({ behavior: 'smooth', block: 'center' })\n }\n position()\n }\n\n // Capture-phase scroll listener catches scrolling in nested containers\n window.addEventListener('scroll', position, true)\n window.addEventListener('resize', position)\n\n const unsubscribe = client.subscribe(update)\n update(client.getState())\n\n return () => {\n unsubscribe()\n window.removeEventListener('scroll', position, true)\n window.removeEventListener('resize', position)\n box.remove()\n target = null\n }\n}\n","/**\n * Preview Content Source Mapping (v1)\n *\n * Automatically tags preview-site DOM elements with `data-preview-field`\n * attributes by matching element text against live preview field values,\n * so click-to-edit and focus overlays work without the site hand-tagging\n * every component.\n *\n * This is a deliberate alternative to stega-style source maps: no invisible\n * unicode is encoded into API responses. Instead, the DOM is scanned and\n * matched against the value map by plain string equality.\n *\n * HONEST LIMITS — this is heuristic value-matching:\n * - Only exact, unique string values of at least `minLength` characters are\n * matched. Values appearing under more than one field path are skipped\n * (they cannot be attributed safely), as are short values like \"OK\" or\n * \"Go\" that would tag buttons everywhere.\n * - Only an element's own direct text is compared — never text inside nested\n * elements — so the innermost rendering element is what gets tagged and\n * wrapper containers never are.\n * - Values the site transforms before rendering (uppercased, truncated,\n * interpolated, markdown-rendered) will not match.\n * - Framework re-renders are survived: when a tagged element is replaced,\n * the next pass (value update or DOM mutation) notices the remembered\n * element is disconnected and re-tags the replacement.\n *\n * Manual `data-preview-field` attributes remain the precise option and\n * always win: auto-tagging never overwrites an existing attribute, and\n * detach removes only the attributes it added.\n *\n * @example\n * ```typescript\n * import { createPreviewClient } from '@typecms/sdk/preview'\n * import { autoTagPreviewFields } from '@typecms/sdk/preview'\n *\n * const preview = createPreviewClient()\n * preview.connect()\n *\n * const detach = autoTagPreviewFields(preview)\n * // Elements whose text equals a field value now carry\n * // data-preview-field=\"path\" — click-to-edit and overlays just work.\n *\n * // ...on teardown\n * detach()\n * ```\n */\n\nimport type { PreviewState } from './types'\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * Minimal structural view of a DOM node — everything the tagger touches.\n * Real `Element`/`Document` objects satisfy this, and tests can provide\n * lightweight fakes without a DOM implementation.\n */\nexport interface AutoTagNode {\n nodeType: number\n textContent?: string | null\n childNodes?: ArrayLike<AutoTagNode>\n /**\n * Whether the node is still attached to the document. Real DOM elements\n * provide this; when absent the node is assumed connected. Used to detect\n * framework re-renders that replaced a tagged element.\n */\n isConnected?: boolean\n getAttribute?(name: string): string | null\n setAttribute?(name: string, value: string): void\n removeAttribute?(name: string): void\n}\n\nexport interface AutoTagOptions {\n /** Root node to scan for candidate elements (defaults to `document.body`) */\n root?: AutoTagNode\n /** Attribute to write the field path into (defaults to `data-preview-field`) */\n attribute?: string\n /**\n * Minimum (trimmed) string value length to consider for matching\n * (defaults to 3). Shorter values like \"OK\" would tag unrelated UI.\n */\n minLength?: number\n /**\n * Re-run the matching pass when new nodes are added under `root`\n * (client-side navigation, lazy rendering). Defaults to true; ignored\n * in environments without `MutationObserver`.\n */\n observe?: boolean\n}\n\ninterface SourceMapClient {\n subscribe(listener: (state: PreviewState) => void): () => void\n getState(): PreviewState\n}\n\nconst DEFAULT_ATTRIBUTE = 'data-preview-field'\nconst DEFAULT_MIN_LENGTH = 3\n\nconst ELEMENT_NODE = 1\nconst TEXT_NODE = 3\n\n// =============================================================================\n// Value Flattening\n// =============================================================================\n\n/**\n * Walk `state.values` and collect every STRING leaf as (trimmed text → paths).\n * Paths use the same notation `getNestedValue` reads: dots for objects and\n * `items[0]` for array indices, so tagged paths round-trip through the rest\n * of the preview system (focus overlay, DOM binding, click-to-edit).\n */\nfunction collectStringLeaves(\n value: unknown,\n prefix: string,\n out: Map<string, string[]>,\n): void {\n if (typeof value === 'string') {\n const text = value.trim()\n if (!text) return\n const paths = out.get(text)\n if (paths) {\n paths.push(prefix)\n } else {\n out.set(text, [prefix])\n }\n return\n }\n\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n collectStringLeaves(value[i], `${prefix}[${i}]`, out)\n }\n return\n }\n\n if (value && typeof value === 'object') {\n for (const key of Object.keys(value)) {\n const childPrefix = prefix ? `${prefix}.${key}` : key\n collectStringLeaves((value as Record<string, unknown>)[key], childPrefix, out)\n }\n }\n}\n\n// =============================================================================\n// DOM Helpers (kept thin & structural so they are testable without a DOM)\n// =============================================================================\n\n/**\n * An element's OWN text: the concatenation of its direct text-node children,\n * trimmed — deliberately excluding text inside nested elements. Comparing own\n * text (instead of `textContent`) gives innermost-match precision: the leaf\n * element actually rendering the value matches, while every wrapper up the\n * tree (whose `textContent` would also contain the value) does not.\n */\nfunction ownText(el: AutoTagNode): string {\n const kids = el.childNodes\n if (!kids) return ''\n let text = ''\n for (let i = 0; i < kids.length; i++) {\n const child = kids[i]\n if (child.nodeType === TEXT_NODE) {\n text += child.textContent ?? ''\n }\n }\n return text.trim()\n}\n\n/**\n * Depth-first walk over element nodes under (and including) `node`.\n * The visitor returns false to stop the walk early.\n */\nfunction walk(node: AutoTagNode, visit: (el: AutoTagNode) => boolean): boolean {\n if (node.nodeType === ELEMENT_NODE && typeof node.getAttribute === 'function') {\n if (!visit(node)) return false\n }\n const kids = node.childNodes\n if (!kids) return true\n for (let i = 0; i < kids.length; i++) {\n if (!walk(kids[i], visit)) return false\n }\n return true\n}\n\n// =============================================================================\n// Auto Tagging\n// =============================================================================\n\n/**\n * Automatically tag DOM elements with preview field paths.\n *\n * Subscribes to the preview client; whenever preview is enabled and values\n * are present (the editor's init sends a full values sync), string leaf\n * values are matched against element text and matching elements receive\n * `data-preview-field=\"<path>\"`. The pass re-runs on value updates (new\n * fields may have arrived) and — when `observe` is on — when new nodes are\n * added under `root`. Re-runs are cheap: disabled states bail immediately\n * and already-tagged paths are skipped.\n *\n * @returns A detach function that unsubscribes, stops observing, and removes\n * only the attributes this tagger added (manual tags are untouched).\n */\nexport function autoTagPreviewFields(\n client: SourceMapClient,\n options: AutoTagOptions = {},\n): () => void {\n // SSR guard: nothing to tag outside the browser\n if (typeof document === 'undefined') {\n return () => {}\n }\n\n const attribute = options.attribute ?? DEFAULT_ATTRIBUTE\n const minLength = options.minLength ?? DEFAULT_MIN_LENGTH\n const observe = options.observe ?? true\n const rootNode: AutoTagNode = options.root ?? document.body\n\n /**\n * Path → the element this tagger attributed it to. Detach removes only\n * these attributes; passes use it both to early-exit on paths that are\n * still live and to detect framework re-renders: when the remembered\n * element is no longer connected (or lost its attribute), the path is\n * forgotten and re-matched against the new DOM.\n */\n const taggedByPath = new Map<string, AutoTagNode>()\n\n /** A remembered tagging is live while its element is connected and tagged. */\n function isStillTagged(path: string): boolean {\n const el = taggedByPath.get(path)\n if (!el) return false\n if (el.isConnected === false || el.getAttribute?.(attribute) !== path) {\n taggedByPath.delete(path)\n return false\n }\n return true\n }\n\n let lastState = client.getState()\n\n function runPass(state: PreviewState): void {\n if (!state.enabled) return\n const values = state.values\n if (!values || Object.keys(values).length === 0) return\n\n // Flatten to (trimmed string value → every path carrying it)\n const pathsByValue = new Map<string, string[]>()\n collectStringLeaves(values, '', pathsByValue)\n\n // Candidate values: long enough, unambiguous, not already tagged\n const candidates = new Map<string, string>()\n pathsByValue.forEach((paths, text) => {\n if (text.length < minLength) return\n // Ambiguous: the same string under multiple paths cannot be\n // attributed to a single field safely — skip it entirely.\n if (paths.length !== 1) return\n if (isStillTagged(paths[0])) return\n candidates.set(text, paths[0])\n })\n if (candidates.size === 0) return\n\n walk(rootNode, (el) => {\n // Never overwrite an existing attribute — manual tags (and prior\n // auto-tags) always win.\n if (typeof el.getAttribute !== 'function' || typeof el.setAttribute !== 'function') {\n return true\n }\n if (el.getAttribute(attribute) !== null) return true\n\n const text = ownText(el)\n if (!text) return true\n\n const path = candidates.get(text)\n if (path === undefined) return true\n\n el.setAttribute(attribute, path)\n taggedByPath.set(path, el)\n candidates.delete(text)\n\n // Stop walking once every candidate has found its element\n return candidates.size > 0\n })\n }\n\n const unsubscribe = client.subscribe((state) => {\n lastState = state\n runPass(state)\n })\n runPass(lastState)\n\n // Re-run when nodes appear OR disappear under root: additions cover\n // client-side navigation and lazy-rendered sections; removals cover\n // framework re-renders that replaced a tagged element (isStillTagged\n // forgets the dead element so the pass re-matches its replacement).\n // Guarded: some environments lack MutationObserver.\n let observer: MutationObserver | null = null\n if (observe && typeof MutationObserver !== 'undefined') {\n observer = new MutationObserver((mutations) => {\n const hasNodeChanges = mutations.some(\n (m) =>\n (m.addedNodes && m.addedNodes.length > 0) ||\n (m.removedNodes && m.removedNodes.length > 0),\n )\n if (hasNodeChanges) runPass(lastState)\n })\n observer.observe(rootNode as unknown as Node, { childList: true, subtree: true })\n }\n\n return () => {\n unsubscribe()\n observer?.disconnect()\n observer = null\n taggedByPath.forEach((el) => {\n el.removeAttribute?.(attribute)\n })\n taggedByPath.clear()\n }\n}\n","/**\n * Rich-Text Live Preview Helpers\n *\n * Rich-text fields ('richtextv2' → TipTap, 'richtext' → Slate) store large\n * JSON documents. During live preview every keystroke sends the WHOLE new\n * document, and naive consumers re-render the entire rich-text tree — losing\n * performance and, in editable embeds, cursor state.\n *\n * These helpers apply STRUCTURAL SHARING: when a new document arrives, any\n * subtree that is deep-equal to the previous document's subtree at the same\n * position is returned as the SAME reference from the previous document.\n * React/Vue memoization and keyed renderers then only re-render the nodes\n * that actually changed.\n *\n * This module is framework-agnostic and dependency-free. Documents are\n * assumed to be plain JSON data (no cycles, no functions, no Dates).\n *\n * @example\n * ```typescript\n * import { createRichTextPreview } from '@typecms/sdk/preview'\n *\n * const preview = createRichTextPreview()\n * const docA = preview.apply(incomingDocA)\n * const docB = preview.apply(incomingDocB)\n * // Unchanged paragraphs in docB are the SAME object references as in docA.\n * ```\n */\n\n// =============================================================================\n// Document Shapes\n// =============================================================================\n\n/**\n * A TipTap/ProseMirror node. Kept permissive on purpose — the editor may\n * attach arbitrary attrs/marks and custom node types.\n */\nexport interface TipTapNode {\n type: string\n content?: TipTapNode[]\n text?: string\n marks?: Array<{ type: string; attrs?: Record<string, unknown> }>\n attrs?: Record<string, unknown>\n [key: string]: unknown\n}\n\n/**\n * A TipTap document: `{ type: 'doc', content: [...] }`.\n * Produced by 'richtextv2' fields.\n */\nexport interface TipTapDoc extends TipTapNode {\n type: 'doc'\n content: TipTapNode[]\n}\n\n/**\n * A Slate node. Element nodes carry `children`; text leaves carry `text`\n * (plus arbitrary mark properties like `bold`). Kept permissive on purpose.\n */\nexport interface SlateNode {\n type?: string\n children?: SlateNode[]\n text?: string\n [key: string]: unknown\n}\n\n// =============================================================================\n// Type Guards\n// =============================================================================\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * Check whether a value looks like a TipTap document\n * (`{ type: 'doc', content: [...] }`, produced by 'richtextv2' fields).\n *\n * Permissive: only the root shape is inspected, children are not validated.\n * Use it to decide which renderer to hand a preview value to.\n */\nexport function isTipTapDoc(value: unknown): value is TipTapDoc {\n return isPlainObject(value) && value.type === 'doc' && Array.isArray(value.content)\n}\n\n/**\n * Check whether a value looks like a Slate document — an array of nodes\n * where each node has `children` or is a text leaf with `text`\n * (produced by 'richtext' fields).\n *\n * Permissive: only one level is inspected, grandchildren are not validated.\n * An empty array is treated as a valid (empty) Slate fragment.\n */\nexport function isSlateDoc(value: unknown): value is SlateNode[] {\n return (\n Array.isArray(value) &&\n value.every((node) => isPlainObject(node) && ('children' in node || 'text' in node))\n )\n}\n\n// =============================================================================\n// Structural Sharing\n// =============================================================================\n\n/**\n * Structurally share `next` against `prev`.\n *\n * Returns `prev` itself when the two are deep-equal. Otherwise returns a copy\n * of `next` in which every child object/array is replaced by the structurally\n * shared version against the corresponding `prev` child.\n *\n * Guarantee: any subtree of the result that is deep-equal to the previous\n * document's subtree at the same position is the SAME reference from `prev`,\n * so memoized/keyed renderers skip it.\n *\n * Arrays are aligned from the tail first, then by index from the head, so a\n * mid-array insert or delete (the common rich-text edit) still preserves\n * reference equality for the deep-equal siblings around the edit point.\n *\n * Handles arrays, plain objects, primitives, `null`, and mismatched types\n * (mismatches simply return `next`). Input is assumed to be acyclic JSON.\n */\nexport function shareStructure<T>(prev: T | undefined, next: T): T {\n if (prev === next || prev === undefined) {\n return next\n }\n\n if (Array.isArray(prev) && Array.isArray(next)) {\n return shareArray(prev, next) as T\n }\n\n if (isPlainObject(prev) && isPlainObject(next)) {\n return shareObject(prev, next) as T\n }\n\n // Primitives, null, or mismatched types — nothing to share.\n return next\n}\n\nfunction shareArray(prev: unknown[], next: unknown[]): unknown[] {\n const prevLength = prev.length\n const nextLength = next.length\n const result: unknown[] = new Array(nextLength)\n\n // Match deep-equal elements from the tail first so mid-array inserts and\n // deletes don't break sharing for everything after the edit point.\n let tail = 0\n const maxTail = Math.min(prevLength, nextLength)\n while (tail < maxTail) {\n const prevItem = prev[prevLength - 1 - tail]\n const shared = shareStructure(prevItem, next[nextLength - 1 - tail])\n if (shared !== prevItem) break\n result[nextLength - 1 - tail] = shared\n tail++\n }\n\n // Align the remaining head region by index.\n const prevHeadLength = prevLength - tail\n for (let i = 0; i < nextLength - tail; i++) {\n result[i] = i < prevHeadLength ? shareStructure(prev[i], next[i]) : next[i]\n }\n\n // Fully deep-equal — hand back the previous array itself.\n if (prevLength === nextLength) {\n let allShared = true\n for (let i = 0; i < nextLength; i++) {\n if (result[i] !== prev[i]) {\n allShared = false\n break\n }\n }\n if (allShared) return prev\n }\n\n return result\n}\n\nfunction shareObject(\n prev: Record<string, unknown>,\n next: Record<string, unknown>\n): Record<string, unknown> {\n const nextKeys = Object.keys(next)\n const result: Record<string, unknown> = {}\n let allShared = nextKeys.length === Object.keys(prev).length\n\n for (const key of nextKeys) {\n const hasPrev = Object.prototype.hasOwnProperty.call(prev, key)\n const shared = hasPrev ? shareStructure(prev[key], next[key]) : next[key]\n result[key] = shared\n if (!hasPrev || shared !== prev[key]) {\n allShared = false\n }\n }\n\n // Fully deep-equal — hand back the previous object itself.\n return allShared ? prev : result\n}\n\n// =============================================================================\n// Stateful Wrapper\n// =============================================================================\n\nexport interface RichTextPreviewOptions {\n /**\n * Seed the \"previous document\" memory, e.g. with the published document\n * already rendered on the page, so the very first preview update can\n * share unchanged subtrees with it.\n */\n initial?: unknown\n}\n\nexport interface RichTextPreview {\n /**\n * Structurally share `nextDoc` against the last applied document and\n * remember the result for the next call.\n */\n apply<T>(nextDoc: T): T\n /** Forget the last document. The next apply() returns its input as-is. */\n reset(): void\n}\n\n/**\n * Create a small stateful structural-sharing instance for a single\n * rich-text field. Useful outside React/Vue (vanilla JS, Svelte stores,\n * custom render loops).\n *\n * @example\n * ```typescript\n * const preview = createRichTextPreview()\n *\n * client.subscribe((state) => {\n * const doc = preview.apply(state.values.body)\n * render(doc) // unchanged nodes keep their references\n * })\n * ```\n */\nexport function createRichTextPreview(options: RichTextPreviewOptions = {}): RichTextPreview {\n let lastDoc: unknown = options.initial\n\n return {\n apply<T>(nextDoc: T): T {\n const shared = shareStructure(lastDoc as T | undefined, nextDoc)\n lastDoc = shared\n return shared\n },\n\n reset() {\n lastDoc = undefined\n },\n }\n}\n","/**\n * Preview React Hooks\n *\n * React integration for Type CMS live preview.\n */\n\nimport {\n createContext,\n useContext,\n useEffect,\n useRef,\n useState,\n useMemo,\n useCallback,\n type ReactNode,\n} from 'react'\nimport { createPreviewClient, deepMerge } from './client'\nimport { attachPreviewOverlay, type PreviewOverlayOptions } from './overlay'\nimport { createRichTextPreview, type RichTextPreview } from './richtext'\nimport type { PreviewConfig, PreviewState } from './types'\n\n// =============================================================================\n// Context\n// =============================================================================\n\ninterface PreviewContextValue {\n state: PreviewState\n /** Check if running in preview mode */\n isPreview: boolean\n /** Get preview value for a field path */\n getValue: <T>(path: string) => T | undefined\n /** Merge published content with preview overrides */\n mergeContent: <T extends Record<string, unknown>>(content: T) => T\n /** Notify editor of navigation */\n notifyNavigation: (pathname: string) => void\n /** Notify editor that a field was clicked in the preview */\n notifyFieldFocus: (path: string) => void\n}\n\nconst PreviewContext = createContext<PreviewContextValue | null>(null)\n\n// =============================================================================\n// Provider\n// =============================================================================\n\ninterface PreviewProviderProps extends PreviewConfig {\n children: ReactNode\n /**\n * Draw an automatic highlight box over the `[data-preview-field]` element\n * matching the field focused in the editor. Pass options to customize.\n */\n overlay?: boolean | PreviewOverlayOptions\n}\n\n/**\n * Provider component for Type CMS live preview.\n *\n * Wrap your app (or specific pages) with this provider to enable\n * real-time preview updates from the Type CMS editor.\n *\n * @example\n * ```tsx\n * // app/layout.tsx or pages/_app.tsx\n * import { PreviewProvider } from '@typecms/sdk/preview'\n *\n * export default function RootLayout({ children }) {\n * return (\n * <PreviewProvider debug={process.env.NODE_ENV === 'development'}>\n * {children}\n * </PreviewProvider>\n * )\n * }\n * ```\n */\nexport function PreviewProvider({ children, overlay, ...config }: PreviewProviderProps) {\n const [state, setState] = useState<PreviewState>({\n enabled: false,\n entryId: null,\n projectId: null,\n locale: null,\n typeId: null,\n values: {},\n focusedPath: null,\n })\n\n const client = useMemo(() => createPreviewClient(config), [])\n\n useEffect(() => {\n // Subscribe to state changes\n const unsubscribe = client.subscribe(setState)\n\n // Start listening\n client.connect()\n\n return () => {\n unsubscribe()\n client.disconnect()\n }\n }, [client])\n\n // Ref keeps the latest options without re-attaching the overlay on every\n // render when consumers pass an inline options object.\n const overlayRef = useRef(overlay)\n overlayRef.current = overlay\n const overlayEnabled = !!overlay\n\n useEffect(() => {\n if (!overlayEnabled) return\n const current = overlayRef.current\n return attachPreviewOverlay(client, current === true || !current ? {} : current)\n }, [client, overlayEnabled])\n\n const getValue = useCallback(\n <T,>(path: string): T | undefined => {\n return client.getValue<T>(path)\n },\n [client]\n )\n\n const mergeContent = useCallback(\n <T extends Record<string, unknown>>(content: T): T => {\n if (!state.enabled || Object.keys(state.values).length === 0) {\n return content\n }\n return deepMerge(content, state.values as Record<string, unknown>) as T\n },\n [state.enabled, state.values]\n )\n\n const notifyNavigation = useCallback(\n (pathname: string) => {\n client.notifyNavigation(pathname)\n },\n [client]\n )\n\n const notifyFieldFocus = useCallback(\n (path: string) => {\n client.notifyFieldFocus(path)\n },\n [client]\n )\n\n const value: PreviewContextValue = useMemo(\n () => ({\n state,\n isPreview: state.enabled,\n getValue,\n mergeContent,\n notifyNavigation,\n notifyFieldFocus,\n }),\n [state, getValue, mergeContent, notifyNavigation, notifyFieldFocus]\n )\n\n return <PreviewContext.Provider value={value}>{children}</PreviewContext.Provider>\n}\n\n// =============================================================================\n// Hooks\n// =============================================================================\n\n/**\n * Access the preview context.\n *\n * @throws Error if used outside of PreviewProvider\n */\nexport function usePreviewContext(): PreviewContextValue {\n const context = useContext(PreviewContext)\n if (!context) {\n throw new Error('usePreviewContext must be used within a PreviewProvider')\n }\n return context\n}\n\n/**\n * Check if running in preview mode.\n *\n * @example\n * ```tsx\n * function Page() {\n * const isPreview = useIsPreview()\n *\n * return (\n * <div>\n * {isPreview && <div className=\"preview-badge\">Preview Mode</div>}\n * <Content />\n * </div>\n * )\n * }\n * ```\n */\nexport function useIsPreview(): boolean {\n const context = useContext(PreviewContext)\n return context?.state.enabled ?? false\n}\n\n/**\n * Get current preview state.\n */\nexport function usePreviewState(): PreviewState | null {\n const context = useContext(PreviewContext)\n return context?.state ?? null\n}\n\n/**\n * Get a preview value by field path, with fallback to published value.\n *\n * @example\n * ```tsx\n * function Title({ entry }) {\n * const title = usePreviewValue('title', entry.title)\n * return <h1>{title}</h1>\n * }\n * ```\n */\nexport function usePreviewValue<T>(path: string, fallback: T): T {\n const context = useContext(PreviewContext)\n\n if (!context || !context.state.enabled) {\n return fallback\n }\n\n const previewValue = context.getValue<T>(path)\n return previewValue !== undefined ? previewValue : fallback\n}\n\n/**\n * Merge published content with preview overrides.\n *\n * This is the primary hook for integrating preview data.\n * Pass your fetched/published content and get back a merged\n * version that includes any live preview changes.\n *\n * @example\n * ```tsx\n * // In your page component\n * function BlogPost({ post }) {\n * // post is fetched from the API (published content)\n * const content = usePreviewContent(post)\n *\n * // content now includes any live preview changes\n * return (\n * <article>\n * <h1>{content.title}</h1>\n * <div>{content.body}</div>\n * </article>\n * )\n * }\n * ```\n */\nexport function usePreviewContent<T extends Record<string, unknown>>(content: T): T {\n const context = useContext(PreviewContext)\n\n if (!context || !context.state.enabled) {\n return content\n }\n\n return context.mergeContent(content)\n}\n\n/**\n * Get the currently focused field path (if any).\n *\n * Use this to highlight fields in your preview when they're\n * being edited in the Type CMS editor.\n *\n * @example\n * ```tsx\n * function Field({ path, children }) {\n * const focusedPath = useFocusedField()\n * const isFocused = focusedPath === path\n *\n * return (\n * <div className={isFocused ? 'ring-2 ring-blue-500' : ''}>\n * {children}\n * </div>\n * )\n * }\n * ```\n */\nexport function useFocusedField(): string | null {\n const context = useContext(PreviewContext)\n return context?.state.focusedPath ?? null\n}\n\n/**\n * Hook that provides a function to notify the editor of navigation.\n *\n * Call this when your app navigates to a new page so the editor\n * can update its state accordingly.\n *\n * @example\n * ```tsx\n * function Page() {\n * const pathname = usePathname()\n * const notifyNavigation = usePreviewNavigation()\n *\n * useEffect(() => {\n * notifyNavigation(pathname)\n * }, [pathname, notifyNavigation])\n *\n * return <Content />\n * }\n * ```\n */\nexport function usePreviewNavigation(): (pathname: string) => void {\n const context = useContext(PreviewContext)\n return context?.notifyNavigation ?? (() => {})\n}\n\n/**\n * Hook that provides a function to notify the editor when a preview field is clicked.\n *\n * Add `data-preview-field=\"<path>\"` to elements in your preview site. When a user\n * clicks one, call this function with the path to highlight and scroll to the\n * corresponding field in the Type CMS editor.\n *\n * @example\n * ```tsx\n * function PreviewField({ path, children }) {\n * const notifyFieldFocus = usePreviewFieldFocus()\n *\n * return (\n * <div\n * data-preview-field={path}\n * onClick={() => notifyFieldFocus(path)}\n * >\n * {children}\n * </div>\n * )\n * }\n * ```\n */\nexport function usePreviewFieldFocus(): (path: string) => void {\n const context = useContext(PreviewContext)\n return context?.notifyFieldFocus ?? (() => {})\n}\n\n/**\n * Get a rich-text preview value by field path with STRUCTURAL SHARING.\n *\n * Like `usePreviewValue`, but pipes the document through a persistent\n * structural-sharing instance: subtrees that are deep-equal to the previous\n * update keep their object references, so memoized node renderers\n * (`React.memo`, keyed lists) only re-render nodes that actually changed —\n * instead of re-rendering the whole rich-text tree on every keystroke.\n *\n * Works for both TipTap ('richtextv2') and Slate ('richtext') documents —\n * or any JSON value. Returns the fallback (unshared) when preview is\n * disabled. Sharing memory resets when `path` changes.\n *\n * @example\n * ```tsx\n * function Body({ entry }) {\n * const doc = usePreviewRichText('body', entry.body)\n * return <RichTextRenderer doc={doc} />\n * }\n * ```\n */\nexport function usePreviewRichText<T>(path: string, fallback: T): T {\n const context = useContext(PreviewContext)\n\n // One sharing instance per hook instance, reset when the path changes.\n const previewRef = useRef<RichTextPreview | null>(null)\n const pathRef = useRef(path)\n if (previewRef.current === null) {\n previewRef.current = createRichTextPreview()\n }\n if (pathRef.current !== path) {\n pathRef.current = path\n previewRef.current.reset()\n }\n\n if (!context || !context.state.enabled) {\n return fallback\n }\n\n const previewValue = context.getValue<T>(path)\n return previewRef.current.apply(previewValue !== undefined ? previewValue : fallback)\n}\n"],"mappings":"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","names":["arrayMatch"]}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js Draft Mode Helpers
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic-at-runtime helpers for wiring the Type CMS live preview
|
|
5
|
+
* into Next.js Draft Mode. This module has no dependency on `next` — it only
|
|
6
|
+
* parses URLs — so it is safe to import from any runtime (Node, Edge, Bun).
|
|
7
|
+
*
|
|
8
|
+
* The Type CMS editor opens the preview site with `?preview=true` (plus
|
|
9
|
+
* optional `secret`, `slug`, and `locale` params). Your site's `/api/preview`
|
|
10
|
+
* route validates the request with {@link validatePreviewRequest}, enables
|
|
11
|
+
* Draft Mode, and redirects to the slug. Pages then fetch content with
|
|
12
|
+
* `draft: isEnabled` so the server returns draft-built content rows.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Options for {@link validatePreviewRequest}.
|
|
16
|
+
*/
|
|
17
|
+
interface ValidatePreviewRequestOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Shared preview secret. When provided, the request's `secret` query param
|
|
20
|
+
* must match it exactly, otherwise the request is invalid. When omitted,
|
|
21
|
+
* no secret check is performed.
|
|
22
|
+
*/
|
|
23
|
+
secret?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Result of {@link validatePreviewRequest}.
|
|
27
|
+
*/
|
|
28
|
+
interface ValidatePreviewRequestResult {
|
|
29
|
+
/** Whether the request is a valid preview request. */
|
|
30
|
+
valid: boolean;
|
|
31
|
+
/** The `slug` query param (redirect target), or null when absent or invalid. */
|
|
32
|
+
slug: string | null;
|
|
33
|
+
/** The `locale` query param, or null when absent or invalid. */
|
|
34
|
+
locale: string | null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validate a preview URL opened by the Type CMS editor.
|
|
38
|
+
*
|
|
39
|
+
* The editor's preview URL carries `?preview=true` plus optional `secret`,
|
|
40
|
+
* `slug`, and `locale` params. The request is valid when `preview=true` is
|
|
41
|
+
* present and, if a `secret` option is provided, the URL's `secret` param
|
|
42
|
+
* matches it. On an invalid request, `slug` and `locale` are returned as
|
|
43
|
+
* `null` so a failed validation can never leak a redirect target.
|
|
44
|
+
*
|
|
45
|
+
* The secret is compared with a full (non-constant-time) string comparison.
|
|
46
|
+
* Constant-time comparison is not required here: the secret only gates
|
|
47
|
+
* enabling draft mode, and brute-force attempts are rate-limited upstream;
|
|
48
|
+
* a full string comparison (never a prefix check) is still used.
|
|
49
|
+
*
|
|
50
|
+
* @param url - The incoming request URL (e.g. `request.url` in a route
|
|
51
|
+
* handler). Relative URLs are supported.
|
|
52
|
+
* @param options - Validation options (shared secret).
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* // app/api/preview/route.ts
|
|
57
|
+
* import { draftMode } from 'next/headers'
|
|
58
|
+
* import { redirect } from 'next/navigation'
|
|
59
|
+
* import { validatePreviewRequest } from '@typecms/sdk/preview/next'
|
|
60
|
+
*
|
|
61
|
+
* export async function GET(request: Request) {
|
|
62
|
+
* const { valid, slug } = validatePreviewRequest(request.url, {
|
|
63
|
+
* secret: process.env.TYPECMS_PREVIEW_SECRET,
|
|
64
|
+
* })
|
|
65
|
+
*
|
|
66
|
+
* if (!valid) {
|
|
67
|
+
* return new Response('Invalid preview request', { status: 401 })
|
|
68
|
+
* }
|
|
69
|
+
*
|
|
70
|
+
* const draft = await draftMode()
|
|
71
|
+
* draft.enable()
|
|
72
|
+
* redirect(slug ?? '/')
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* // app/[slug]/page.tsx — fetch draft content when Draft Mode is enabled
|
|
79
|
+
* import { draftMode } from 'next/headers'
|
|
80
|
+
* import { createClient } from '@typecms/sdk'
|
|
81
|
+
*
|
|
82
|
+
* const client = createClient({
|
|
83
|
+
* projectId: process.env.TYPECMS_PROJECT_ID!,
|
|
84
|
+
* // Preview-environment token — server-side only, never ship to browsers
|
|
85
|
+
* token: process.env.TYPECMS_PREVIEW_TOKEN!,
|
|
86
|
+
* })
|
|
87
|
+
*
|
|
88
|
+
* export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
|
89
|
+
* const { slug } = await params
|
|
90
|
+
* const { isEnabled } = await draftMode()
|
|
91
|
+
*
|
|
92
|
+
* const page = await client.get('page', { slug, draft: isEnabled })
|
|
93
|
+
* return <h1>{page.title}</h1>
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
declare function validatePreviewRequest(url: string | URL, options?: ValidatePreviewRequestOptions): ValidatePreviewRequestResult;
|
|
98
|
+
|
|
99
|
+
export { type ValidatePreviewRequestOptions, type ValidatePreviewRequestResult, validatePreviewRequest };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/preview/next.ts
|
|
2
|
+
function validatePreviewRequest(url, options = {}) {
|
|
3
|
+
const invalid = {
|
|
4
|
+
valid: false,
|
|
5
|
+
slug: null,
|
|
6
|
+
locale: null
|
|
7
|
+
};
|
|
8
|
+
let parsed;
|
|
9
|
+
try {
|
|
10
|
+
parsed = url instanceof URL ? url : new URL(url, "http://localhost");
|
|
11
|
+
} catch {
|
|
12
|
+
return invalid;
|
|
13
|
+
}
|
|
14
|
+
if (parsed.searchParams.get("preview") !== "true") {
|
|
15
|
+
return invalid;
|
|
16
|
+
}
|
|
17
|
+
if (options.secret !== void 0) {
|
|
18
|
+
if (parsed.searchParams.get("secret") !== options.secret) {
|
|
19
|
+
return invalid;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
valid: true,
|
|
24
|
+
slug: parsed.searchParams.get("slug"),
|
|
25
|
+
locale: parsed.searchParams.get("locale")
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
validatePreviewRequest
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=next.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/preview/next.ts"],"sourcesContent":["/**\n * Next.js Draft Mode Helpers\n *\n * Framework-agnostic-at-runtime helpers for wiring the Type CMS live preview\n * into Next.js Draft Mode. This module has no dependency on `next` — it only\n * parses URLs — so it is safe to import from any runtime (Node, Edge, Bun).\n *\n * The Type CMS editor opens the preview site with `?preview=true` (plus\n * optional `secret`, `slug`, and `locale` params). Your site's `/api/preview`\n * route validates the request with {@link validatePreviewRequest}, enables\n * Draft Mode, and redirects to the slug. Pages then fetch content with\n * `draft: isEnabled` so the server returns draft-built content rows.\n */\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * Options for {@link validatePreviewRequest}.\n */\nexport interface ValidatePreviewRequestOptions {\n /**\n * Shared preview secret. When provided, the request's `secret` query param\n * must match it exactly, otherwise the request is invalid. When omitted,\n * no secret check is performed.\n */\n secret?: string\n}\n\n/**\n * Result of {@link validatePreviewRequest}.\n */\nexport interface ValidatePreviewRequestResult {\n /** Whether the request is a valid preview request. */\n valid: boolean\n /** The `slug` query param (redirect target), or null when absent or invalid. */\n slug: string | null\n /** The `locale` query param, or null when absent or invalid. */\n locale: string | null\n}\n\n// =============================================================================\n// validatePreviewRequest\n// =============================================================================\n\n/**\n * Validate a preview URL opened by the Type CMS editor.\n *\n * The editor's preview URL carries `?preview=true` plus optional `secret`,\n * `slug`, and `locale` params. The request is valid when `preview=true` is\n * present and, if a `secret` option is provided, the URL's `secret` param\n * matches it. On an invalid request, `slug` and `locale` are returned as\n * `null` so a failed validation can never leak a redirect target.\n *\n * The secret is compared with a full (non-constant-time) string comparison.\n * Constant-time comparison is not required here: the secret only gates\n * enabling draft mode, and brute-force attempts are rate-limited upstream;\n * a full string comparison (never a prefix check) is still used.\n *\n * @param url - The incoming request URL (e.g. `request.url` in a route\n * handler). Relative URLs are supported.\n * @param options - Validation options (shared secret).\n *\n * @example\n * ```typescript\n * // app/api/preview/route.ts\n * import { draftMode } from 'next/headers'\n * import { redirect } from 'next/navigation'\n * import { validatePreviewRequest } from '@typecms/sdk/preview/next'\n *\n * export async function GET(request: Request) {\n * const { valid, slug } = validatePreviewRequest(request.url, {\n * secret: process.env.TYPECMS_PREVIEW_SECRET,\n * })\n *\n * if (!valid) {\n * return new Response('Invalid preview request', { status: 401 })\n * }\n *\n * const draft = await draftMode()\n * draft.enable()\n * redirect(slug ?? '/')\n * }\n * ```\n *\n * @example\n * ```typescript\n * // app/[slug]/page.tsx — fetch draft content when Draft Mode is enabled\n * import { draftMode } from 'next/headers'\n * import { createClient } from '@typecms/sdk'\n *\n * const client = createClient({\n * projectId: process.env.TYPECMS_PROJECT_ID!,\n * // Preview-environment token — server-side only, never ship to browsers\n * token: process.env.TYPECMS_PREVIEW_TOKEN!,\n * })\n *\n * export default async function Page({ params }: { params: Promise<{ slug: string }> }) {\n * const { slug } = await params\n * const { isEnabled } = await draftMode()\n *\n * const page = await client.get('page', { slug, draft: isEnabled })\n * return <h1>{page.title}</h1>\n * }\n * ```\n */\nexport function validatePreviewRequest(\n url: string | URL,\n options: ValidatePreviewRequestOptions = {}\n): ValidatePreviewRequestResult {\n const invalid: ValidatePreviewRequestResult = {\n valid: false,\n slug: null,\n locale: null,\n }\n\n let parsed: URL\n try {\n // Base is only used when `url` is relative (e.g. '/api/preview?...');\n // it never affects query-param parsing.\n parsed = url instanceof URL ? url : new URL(url, 'http://localhost')\n } catch {\n return invalid\n }\n\n if (parsed.searchParams.get('preview') !== 'true') {\n return invalid\n }\n\n if (options.secret !== undefined) {\n if (parsed.searchParams.get('secret') !== options.secret) {\n return invalid\n }\n }\n\n return {\n valid: true,\n slug: parsed.searchParams.get('slug'),\n locale: parsed.searchParams.get('locale'),\n }\n}\n"],"mappings":";AA2GO,SAAS,uBACd,KACA,UAAyC,CAAC,GACZ;AAC9B,QAAM,UAAwC;AAAA,IAC5C,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,EACV;AAEA,MAAI;AACJ,MAAI;AAGF,aAAS,eAAe,MAAM,MAAM,IAAI,IAAI,KAAK,kBAAkB;AAAA,EACrE,QAAQ;AACN,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,aAAa,IAAI,SAAS,MAAM,QAAQ;AACjD,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,WAAW,QAAW;AAChC,QAAI,OAAO,aAAa,IAAI,QAAQ,MAAM,QAAQ,QAAQ;AACxD,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO;AAAA,IACL,OAAO;AAAA,IACP,MAAM,OAAO,aAAa,IAAI,MAAM;AAAA,IACpC,QAAQ,OAAO,aAAa,IAAI,QAAQ;AAAA,EAC1C;AACF;","names":[]}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Readable } from 'svelte/store';
|
|
2
|
+
import { P as PreviewOverlayOptions } from '../overlay-CLYAeNwg.mjs';
|
|
3
|
+
import { P as PreviewState, a as PreviewConfig } from '../types-BNHDdA2G.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Preview Svelte Stores
|
|
7
|
+
*
|
|
8
|
+
* Svelte integration for Type CMS live preview.
|
|
9
|
+
* Mirrors the Vue layer (vue.ts) 1:1 on top of the
|
|
10
|
+
* framework-agnostic preview client.
|
|
11
|
+
*
|
|
12
|
+
* Built on `svelte/store` only (no component runtime), so it works
|
|
13
|
+
* in both Svelte 4 and Svelte 5, and in SvelteKit load/SSR contexts.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* // src/routes/+layout.svelte
|
|
18
|
+
* <script>
|
|
19
|
+
* import { onMount } from 'svelte'
|
|
20
|
+
* import { initPreview } from '@typecms/sdk/preview/svelte'
|
|
21
|
+
* import { setContext } from 'svelte'
|
|
22
|
+
*
|
|
23
|
+
* onMount(() => {
|
|
24
|
+
* const preview = initPreview({ overlay: true })
|
|
25
|
+
* setContext('typecms-preview', preview)
|
|
26
|
+
* return () => preview.disconnect()
|
|
27
|
+
* })
|
|
28
|
+
* </script>
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* ```svelte
|
|
32
|
+
* <!-- src/routes/blog/[slug]/+page.svelte -->
|
|
33
|
+
* <script>
|
|
34
|
+
* import { getContext } from 'svelte'
|
|
35
|
+
*
|
|
36
|
+
* export let data
|
|
37
|
+
* const preview = getContext('typecms-preview')
|
|
38
|
+
* const content = preview.previewContent(data.post)
|
|
39
|
+
* const isPreview = preview.isPreview
|
|
40
|
+
* </script>
|
|
41
|
+
*
|
|
42
|
+
* {#if $isPreview}<span>Preview mode</span>{/if}
|
|
43
|
+
* <h1 data-preview-field="title">{$content.title}</h1>
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
interface SveltePreviewContext {
|
|
48
|
+
/** Reactive preview state */
|
|
49
|
+
state: Readable<PreviewState>;
|
|
50
|
+
/** Check if running in preview mode */
|
|
51
|
+
isPreview: Readable<boolean>;
|
|
52
|
+
/** Currently focused field path (if any) */
|
|
53
|
+
focusedField: Readable<string | null>;
|
|
54
|
+
/** Get preview value for a field path */
|
|
55
|
+
getValue: <T>(path: string) => T | undefined;
|
|
56
|
+
/** Preview value for a field path, with fallback to the published value */
|
|
57
|
+
previewValue: <T>(path: string, fallback: T) => Readable<T>;
|
|
58
|
+
/** Merge published content with preview overrides */
|
|
59
|
+
previewContent: <T extends Record<string, unknown>>(content: T) => Readable<T>;
|
|
60
|
+
/** Notify editor of navigation */
|
|
61
|
+
notifyNavigation: (pathname: string) => void;
|
|
62
|
+
/** Notify editor that a field was clicked in the preview */
|
|
63
|
+
notifyFieldFocus: (path: string) => void;
|
|
64
|
+
/** Start listening for preview messages */
|
|
65
|
+
connect: () => void;
|
|
66
|
+
/** Stop listening and reset state */
|
|
67
|
+
disconnect: () => void;
|
|
68
|
+
}
|
|
69
|
+
interface SveltePreviewOptions extends PreviewConfig {
|
|
70
|
+
/**
|
|
71
|
+
* Draw an automatic highlight box over the `[data-preview-field]` element
|
|
72
|
+
* matching the field focused in the editor. Pass options to customize.
|
|
73
|
+
*/
|
|
74
|
+
overlay?: boolean | PreviewOverlayOptions;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create a preview context backed by Svelte stores.
|
|
78
|
+
*
|
|
79
|
+
* Call `connect()` to start listening for editor messages (typically in
|
|
80
|
+
* onMount of a root +layout.svelte) and `disconnect()` on teardown.
|
|
81
|
+
* For the common case, use {@link initPreview} instead.
|
|
82
|
+
*/
|
|
83
|
+
declare function createPreviewContext(options?: SveltePreviewOptions): SveltePreviewContext;
|
|
84
|
+
/**
|
|
85
|
+
* Create a preview context and start listening for editor messages
|
|
86
|
+
* immediately. The typical entry point in a SvelteKit app: call from
|
|
87
|
+
* onMount in a root +layout.svelte and disconnect on teardown.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* onMount(() => {
|
|
92
|
+
* const preview = initPreview({ overlay: true })
|
|
93
|
+
* return () => preview.disconnect()
|
|
94
|
+
* })
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
declare function initPreview(options?: SveltePreviewOptions): SveltePreviewContext;
|
|
98
|
+
|
|
99
|
+
export { type SveltePreviewContext, type SveltePreviewOptions, createPreviewContext, initPreview };
|