import { deleteArtifactWorkspace, normalizeArtifactFiles, persistArtifactWorkspace, publishArtifactWorkspace, readArtifactWorkspace, type ArtifactFile, type ArtifactFileInput, type ArtifactWorkspacePersistence, type ArtifactWorkspaceSnapshot, } from './artifact-workspace'; export type { ArtifactFile, ArtifactFileInput } from './artifact-workspace'; export type ArtifactConsoleLevel = 'log' | 'info' | 'warn' | 'error'; export interface ArtifactConsoleEntry { level: ArtifactConsoleLevel; message: string; source?: string; line?: number; column?: number; } export interface ArtifactEvaluation { status: 'passed' | 'failed' | 'timeout'; entries: ArtifactConsoleEntry[]; dom?: { title: string; textPreview: string; elementCount: number; }; durationMs: number; testedAt: number; } export interface ArtifactDocument { id: string; title: string; source: string; sandboxedSource: string; runtimeToken: string; createdAt: number; entryPath?: string; files?: ArtifactFile[]; schemaVersion?: 2; workspaceStorage?: ArtifactWorkspacePersistence['backend']; workspaceWarning?: string; evaluation?: ArtifactEvaluation; } interface RuntimeDiagnostics { token: string; evaluation: ArtifactEvaluation; } const MAX_ARTIFACT_BYTES = 256 * 1024; const MAX_CONSOLE_ENTRIES = 100; const ARTIFACT_DIAGNOSTICS_EVENT = 'bonsai-artifact-diagnostics'; const diagnostics = new Map(); const META_REFRESH = /]*\bhttp-equiv\s*=\s*["']?refresh\b)[^>]*>/iu; const JAVASCRIPT_URL = /\b(?:href|src)\s*=\s*["']\s*javascript:/iu; const SCRIPT_BLOCK = /]*>([\s\S]*?)<\/script\s*>/giu; const SCRIPT_NAVIGATION = /(?:\b(?:window\s*\.\s*)?location\s*(?:=|\.\s*href\s*=|\.\s*(?:assign|replace|reload)\s*\()|\bwindow\s*\.\s*open\s*\()/iu; const ROOT_ABSOLUTE_HTML_RESOURCE = /\b(?:src|href|action)\s*=\s*["']\/(?!\/)/iu; const ROOT_ABSOLUTE_CSS_RESOURCE = /\burl\s*\(\s*["']?\/(?!\/)/iu; const ROOT_ABSOLUTE_MODULE_RESOURCE = /\b(?:from\s*|import\s*(?:\(\s*)?)["']\/(?!\/)/iu; const BASE_ELEMENT = / 0 ? segments.join('/') : null; } function rewriteVfsFetches(source: string, fromPath: string): string { return source.replace( /\bfetch\s*\(\s*(["'])(\.{1,2}\/[^"']+)\1/gu, (match, _quote: string, specifier: string) => { const path = resolveWorkspaceSpecifier(fromPath, specifier); return path ? `__bonsaiVfsFetch(${JSON.stringify(path)}` : match; }, ); } function rewriteModuleSource(source: string, fromPath: string): string { const rewrite = (match: string, prefix: string, quote: string, specifier: string) => { const path = resolveWorkspaceSpecifier(fromPath, specifier); return path ? `${prefix}${quote}artifact:${path}${quote}` : match; }; return rewriteVfsFetches(source, fromPath) .replace( /(\b(?:import|export)\s+(?:[^"'`]*?\s+from\s*)?)(["'])(\.{1,2}\/[^"']+)\2/gu, rewrite, ) .replace( /(\bimport\s*\(\s*)(["'])(\.{1,2}\/[^"']+)\2/gu, rewrite, ); } function textDataUrl(file: Pick, content = file.content): string { return `data:${file.mime},${encodeURIComponent(content)}`; } function buildArtifactSrcdoc( files: ArtifactFile[], entryPath: string, artifactId: string, runtimeToken: string, ): string { const fileMap = new Map(files.map((file) => [file.path, file])); const entry = fileMap.get(entryPath); if (!entry) throw new Error(`artifact entry file is missing: ${entryPath}`); const rewriteCss = (source: string, fromPath: string, stack = new Set()): string => { const withImports = source.replace( /@import\s+(?:url\(\s*)?(["'])([^"']+)\1\s*\)?\s*;/giu, (match, _quote: string, specifier: string) => { const path = resolveWorkspaceSpecifier(fromPath, specifier); const imported = path ? fileMap.get(path) : undefined; if (!path || !imported?.mime.startsWith('text/css') || stack.has(path)) return match; return rewriteCss(imported.content, path, new Set([...stack, path])); }, ); return withImports.replace( /url\(\s*(["']?)([^"')]+)\1\s*\)/giu, (match, _quote: string, specifier: string) => { const path = resolveWorkspaceSpecifier(fromPath, specifier.trim()); const asset = path ? fileMap.get(path) : undefined; return asset ? `url("${textDataUrl(asset)}")` : match; }, ); }; const moduleImports = Object.fromEntries( files .filter((file) => file.mime.startsWith('text/javascript')) .map((file) => [ `artifact:${file.path}`, textDataUrl(file, `${rewriteModuleSource(file.content, file.path)}\n//# sourceURL=artifact:${file.path}`), ]), ); let source = entry.content; source = source.replace(/]*)>([\s\S]*?)<\/style\s*>/giu, (_match, attributes: string, css: string) => ( `${rewriteCss(css, entryPath, new Set([entryPath]))}` )); source = source.replace(/]*)>/giu, (match, attributes: string) => { const rel = /\brel\s*=\s*(["'])([^"']+)\1/iu.exec(attributes)?.[2]?.toLowerCase() ?? ''; const hrefMatch = /\bhref\s*=\s*(["'])([^"']+)\1/iu.exec(attributes); if (!hrefMatch) return match; const path = resolveWorkspaceSpecifier(entryPath, hrefMatch[2]!); const file = path ? fileMap.get(path) : undefined; if (path && rel.split(/\s+/u).includes('stylesheet') && file?.mime.startsWith('text/css')) { return ``; } if (rel.split(/\s+/u).includes('icon') && file) { return match.replace(hrefMatch[0], `href="${textDataUrl(file)}"`); } return match; }); source = source.replace(/<(img|source|video|audio|track|input)\b([^>]*)>/giu, (match, tag: string, attributes: string) => { const rewritten = attributes.replace( /\b(src|poster)\s*=\s*(["'])([^"']+)\2/giu, (attribute, name: string, _quote: string, specifier: string) => { const path = resolveWorkspaceSpecifier(entryPath, specifier); const file = path ? fileMap.get(path) : undefined; return file ? `${name}="${textDataUrl(file)}"` : attribute; }, ); return `<${tag}${rewritten}>`; }); source = source.replace(/]*)>([\s\S]*?)<\/script\s*>/giu, (match, attributes: string, inline: string) => { const srcMatch = /\bsrc\s*=\s*(["'])([^"']+)\1/iu.exec(attributes); const moduleScript = /\btype\s*=\s*(["'])module\1/iu.test(attributes); const fromPath = srcMatch ? resolveWorkspaceSpecifier(entryPath, srcMatch[2]!) ?? entryPath : entryPath; const external = srcMatch ? fileMap.get(fromPath) : undefined; if (srcMatch && !external) return match; const code = external?.content ?? inline; const rewritten = moduleScript ? rewriteModuleSource(code, fromPath) : rewriteVfsFetches(code, fromPath); const cleanAttributes = srcMatch ? attributes .replace(srcMatch[0], '') .replace(/\b(?:integrity|crossorigin|referrerpolicy)\s*=\s*(["'])[^"']*\1/giu, '') : attributes; return `${rewritten}<\/script>`; }); const importMap = `