const PREVIEW_RUNTIME_QUERY_KEY = 'appPreviewMode'; const PREVIEW_RUNTIME_PARENT_ORIGIN_KEY = 'previewParentOrigin'; const PREVIEW_RUNTIME_TOKEN_QUERY_KEY = 'registeredAccessToken'; const PREVIEW_RUNTIME_DEVICE_MODE_QUERY_KEY = 'previewDeviceMode'; const PREVIEW_RUNTIME_CONSOLE_BRIDGE_EVENT = 'sm-home.preview-runtime.console'; const PREVIEW_TARGET_TYPE_QUERY_KEY = 'previewTargetType'; const PREVIEW_TARGET_COMPONENT_ID_QUERY_KEY = 'previewComponentId'; const PREVIEW_TARGET_SAMPLE_ID_QUERY_KEY = 'previewSampleId'; const PREVIEW_RUNTIME_CACHE_RESET_MARKER_KEY = 'work-app.preview-runtime.cache-reset.v1'; const PREVIEW_RUNTIME_CACHE_RESET_PARAM_KEY = '__previewRuntimeCacheReset'; const PREVIEW_RUNTIME_CLEANUP_TIMEOUT_MS = 2500; const PREVIEW_RUNTIME_PRESERVED_QUERY_KEYS = [ PREVIEW_RUNTIME_QUERY_KEY, PREVIEW_RUNTIME_PARENT_ORIGIN_KEY, PREVIEW_RUNTIME_TOKEN_QUERY_KEY, PREVIEW_RUNTIME_DEVICE_MODE_QUERY_KEY, ] as const; export type PreviewTargetDescriptor = | { type: 'widget'; componentId: string; sampleId?: string; } | null; export type PreviewRuntimeConsoleLevel = 'log' | 'info' | 'warn' | 'error' | 'debug'; export type PreviewRuntimeConsoleBridgeMessage = { source: typeof PREVIEW_RUNTIME_CONSOLE_BRIDGE_EVENT; level: PreviewRuntimeConsoleLevel; args: string[]; timestamp: string; href: string; }; export function isPreviewRuntime() { if (typeof window === 'undefined') { return false; } return new URLSearchParams(window.location.search).get(PREVIEW_RUNTIME_QUERY_KEY) === '1'; } export function isPreviewAppOrigin() { if (typeof window === 'undefined') { return false; } return window.location.origin === resolvePreviewAppOrigin(); } function readPreviewRuntimeCacheResetMarker() { if (typeof window === 'undefined') { return ''; } try { return window.sessionStorage.getItem(PREVIEW_RUNTIME_CACHE_RESET_MARKER_KEY)?.trim() ?? ''; } catch { return ''; } } function writePreviewRuntimeCacheResetMarker(value: string) { if (typeof window === 'undefined') { return; } try { if (value) { window.sessionStorage.setItem(PREVIEW_RUNTIME_CACHE_RESET_MARKER_KEY, value); } else { window.sessionStorage.removeItem(PREVIEW_RUNTIME_CACHE_RESET_MARKER_KEY); } } catch { // Ignore storage access failures in restricted runtimes. } } function buildPreviewRuntimeLocationKey() { if (typeof window === 'undefined') { return ''; } return `${window.location.origin}${window.location.pathname}`; } function buildPreviewRuntimeCacheResetUrl() { const nextUrl = new URL(window.location.href); nextUrl.searchParams.set(PREVIEW_RUNTIME_CACHE_RESET_PARAM_KEY, `${Date.now()}`); return nextUrl.toString(); } async function clearPreviewRuntimeServiceWorkersAndCaches() { if (typeof window === 'undefined') { return false; } let changed = false; if (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) { try { const registrations = await navigator.serviceWorker.getRegistrations(); if (registrations.length > 0) { changed = true; } await Promise.all(registrations.map((registration) => registration.unregister().catch(() => false))); } catch { // Ignore cleanup failure and continue. } } if ('caches' in window) { try { const cacheKeys = await caches.keys(); if (cacheKeys.length > 0) { changed = true; } await Promise.all(cacheKeys.map((cacheKey) => caches.delete(cacheKey).catch(() => false))); } catch { // Ignore cache cleanup failure and continue. } } return changed; } async function withTimeout(task: Promise, timeoutMs: number, fallbackValue: T) { return await Promise.race([ task, new Promise((resolve) => { window.setTimeout(() => resolve(fallbackValue), timeoutMs); }), ]); } export async function ensurePreviewRuntimeFreshState() { if (typeof window === 'undefined' || !isPreviewRuntime()) { return; } const currentLocationKey = buildPreviewRuntimeLocationKey(); const cleanedLocationKey = readPreviewRuntimeCacheResetMarker(); const resetSearchParam = new URL(window.location.href).searchParams.get(PREVIEW_RUNTIME_CACHE_RESET_PARAM_KEY)?.trim() ?? ''; if (cleanedLocationKey === currentLocationKey && !resetSearchParam) { return; } if (resetSearchParam) { const nextUrl = new URL(window.location.href); nextUrl.searchParams.delete(PREVIEW_RUNTIME_CACHE_RESET_PARAM_KEY); window.history.replaceState(window.history.state, '', nextUrl.pathname + nextUrl.search + nextUrl.hash); writePreviewRuntimeCacheResetMarker(currentLocationKey); return; } const changed = await withTimeout(clearPreviewRuntimeServiceWorkersAndCaches(), PREVIEW_RUNTIME_CLEANUP_TIMEOUT_MS, false); if (!changed) { writePreviewRuntimeCacheResetMarker(currentLocationKey); return; } writePreviewRuntimeCacheResetMarker(currentLocationKey); window.location.replace(buildPreviewRuntimeCacheResetUrl()); } export function getPreviewRuntimeParentOrigin() { if (typeof window === 'undefined') { return ''; } return new URLSearchParams(window.location.search).get(PREVIEW_RUNTIME_PARENT_ORIGIN_KEY)?.trim() ?? ''; } function stringifyPreviewRuntimeConsoleArg(value: unknown, seen = new WeakSet()): string { if (typeof value === 'string') { return value; } if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') { return String(value); } if (typeof value === 'undefined') { return 'undefined'; } if (value === null) { return 'null'; } if (typeof value === 'function') { return `[function ${value.name || 'anonymous'}]`; } if (value instanceof Error) { return value.stack || `${value.name}: ${value.message}`; } if (value instanceof URL) { return value.toString(); } if (typeof value === 'object') { if (seen.has(value)) { return '[circular]'; } seen.add(value); try { return JSON.stringify( value, (_key, nestedValue) => { if (typeof nestedValue === 'bigint') { return nestedValue.toString(); } if (nestedValue instanceof Error) { return { name: nestedValue.name, message: nestedValue.message, stack: nestedValue.stack, }; } if (nestedValue instanceof URL) { return nestedValue.toString(); } if (typeof nestedValue === 'function') { return `[function ${nestedValue.name || 'anonymous'}]`; } if (nestedValue && typeof nestedValue === 'object') { if (seen.has(nestedValue)) { return '[circular]'; } seen.add(nestedValue); } return nestedValue; }, 2, ); } catch { return Object.prototype.toString.call(value); } } return String(value); } function postPreviewRuntimeConsoleMessage(level: PreviewRuntimeConsoleLevel, args: unknown[]) { if (typeof window === 'undefined') { return; } const parentOrigin = getPreviewRuntimeParentOrigin(); if (!parentOrigin || window.parent === window) { return; } const payload: PreviewRuntimeConsoleBridgeMessage = { source: PREVIEW_RUNTIME_CONSOLE_BRIDGE_EVENT, level, args: args.map((arg) => stringifyPreviewRuntimeConsoleArg(arg)), timestamp: new Date().toISOString(), href: window.location.href, }; window.parent.postMessage(payload, parentOrigin); } export function installPreviewRuntimeConsoleBridge() { if (typeof window === 'undefined' || !isPreviewRuntime()) { return; } const consoleMethods: PreviewRuntimeConsoleLevel[] = ['log', 'info', 'warn', 'error', 'debug']; const previewConsole = window.console as Console & { __smHomePreviewConsoleBridgeInstalled?: boolean; }; if (previewConsole.__smHomePreviewConsoleBridgeInstalled) { return; } previewConsole.__smHomePreviewConsoleBridgeInstalled = true; consoleMethods.forEach((level) => { const originalMethod = previewConsole[level].bind(previewConsole); previewConsole[level] = (...args: unknown[]) => { postPreviewRuntimeConsoleMessage(level, args); originalMethod(...args); }; }); window.addEventListener('error', (event) => { postPreviewRuntimeConsoleMessage('error', [ event.message || 'Unknown error', event.filename ? `${event.filename}:${event.lineno}:${event.colno}` : '', event.error ?? '', ]); }); window.addEventListener('unhandledrejection', (event) => { postPreviewRuntimeConsoleMessage('error', ['Unhandled promise rejection', event.reason ?? '']); }); postPreviewRuntimeConsoleMessage('info', ['Preview runtime console bridge connected']); } export function readPreviewRuntimeDeviceModeFromUrl(): 'desktop' | 'mobile' | null { if (typeof window === 'undefined') { return null; } const value = new URLSearchParams(window.location.search).get(PREVIEW_RUNTIME_DEVICE_MODE_QUERY_KEY)?.trim() ?? ''; if (value === 'desktop' || value === 'mobile') { return value; } return null; } export function readPreviewRuntimeTokenFromUrl() { if (typeof window === 'undefined') { return ''; } return new URLSearchParams(window.location.search).get(PREVIEW_RUNTIME_TOKEN_QUERY_KEY)?.trim() ?? ''; } export function clearPreviewRuntimeTokenFromUrl() { if (typeof window === 'undefined') { return; } const url = new URL(window.location.href); if (!url.searchParams.has(PREVIEW_RUNTIME_TOKEN_QUERY_KEY)) { return; } url.searchParams.delete(PREVIEW_RUNTIME_TOKEN_QUERY_KEY); window.history.replaceState(window.history.state, '', `${url.pathname}${url.search}${url.hash}`); } export function resolvePreviewAppOrigin() { return 'https://preview.sm-home.cloud'; } export function appendPreviewRuntimeSearch(path: string, sourceSearch = '') { if (!path) { return path; } const [rawPathname, rawHash = ''] = path.split('#', 2); const [pathname, rawSearch = ''] = rawPathname.split('?', 2); const nextSearchParams = new URLSearchParams(rawSearch); const sourceSearchParams = new URLSearchParams(sourceSearch.startsWith('?') ? sourceSearch.slice(1) : sourceSearch); PREVIEW_RUNTIME_PRESERVED_QUERY_KEYS.forEach((key) => { const value = sourceSearchParams.get(key)?.trim() ?? ''; if (value) { nextSearchParams.set(key, value); return; } nextSearchParams.delete(key); }); const nextSearch = nextSearchParams.toString(); const nextHash = rawHash ? `#${rawHash}` : ''; return `${pathname}${nextSearch ? `?${nextSearch}` : ''}${nextHash}`; } export function readPreviewTargetDescriptorFromUrl(): PreviewTargetDescriptor { if (typeof window === 'undefined') { return null; } const searchParams = new URLSearchParams(window.location.search); const targetType = searchParams.get(PREVIEW_TARGET_TYPE_QUERY_KEY)?.trim() ?? ''; if (targetType !== 'widget') { return null; } const componentId = searchParams.get(PREVIEW_TARGET_COMPONENT_ID_QUERY_KEY)?.trim() ?? ''; const sampleId = searchParams.get(PREVIEW_TARGET_SAMPLE_ID_QUERY_KEY)?.trim() ?? ''; if (!componentId) { return null; } return { type: 'widget', componentId, sampleId: sampleId || undefined, }; } export function buildPreviewRuntimeUrl( pathname: string, search = '', token = '', targetDescriptor: PreviewTargetDescriptor = null, deviceMode: 'desktop' | 'mobile' = 'desktop', ) { const targetUrl = new URL(pathname || '/', resolvePreviewAppOrigin()); if (search) { const sourceParams = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search); sourceParams.forEach((value, key) => { targetUrl.searchParams.set(key, value); }); } targetUrl.searchParams.set(PREVIEW_RUNTIME_QUERY_KEY, '1'); if (typeof window !== 'undefined') { targetUrl.searchParams.set(PREVIEW_RUNTIME_PARENT_ORIGIN_KEY, window.location.origin); } targetUrl.searchParams.set(PREVIEW_RUNTIME_DEVICE_MODE_QUERY_KEY, deviceMode); if (token.trim()) { targetUrl.searchParams.set(PREVIEW_RUNTIME_TOKEN_QUERY_KEY, token.trim()); } if (targetDescriptor?.type === 'widget') { targetUrl.searchParams.set(PREVIEW_TARGET_TYPE_QUERY_KEY, 'widget'); targetUrl.searchParams.set(PREVIEW_TARGET_COMPONENT_ID_QUERY_KEY, targetDescriptor.componentId); if (targetDescriptor.sampleId?.trim()) { targetUrl.searchParams.set(PREVIEW_TARGET_SAMPLE_ID_QUERY_KEY, targetDescriptor.sampleId.trim()); } else { targetUrl.searchParams.delete(PREVIEW_TARGET_SAMPLE_ID_QUERY_KEY); } } return targetUrl.toString(); }