= {\n`;
+ for (const comp of components) {
+ if (comp.type === 'component') {
+ registryCode += ` "${comp.name}": lazy(() => import('${comp.path}').then(m => ({ default: m["${comp.name}"] || m.default }))),\n`;
+ }
+ }
+ registryCode += `};\n`;
+ writeFileSync(join(pbDir, 'virtualRegistry.tsx'), registryCode);
+
+ // Build CSS import lines for the preview entry
+ const cssImportLines = cssImports.map(p => `import '/${p}';`).join('\n');
+
+ const previewEntryCode = `
+import React, { Suspense, useEffect, useState } from 'react';
+import { createRoot } from 'react-dom/client';
+import { COMPONENT_REGISTRY } from './virtualRegistry';
+import { LiveProvider, LivePreview, LiveError } from 'react-live';
+import { themes } from 'prism-react-renderer';
+${cssImportLines}
+
+const urlParams = new URLSearchParams(window.location.search);
+const componentName = urlParams.get('component');
+const initialCode = urlParams.get('code') || ('<' + componentName + ' />');
+
+function PreviewApp() {
+ const [code, setCode] = useState(initialCode);
+ const Component = COMPONENT_REGISTRY[componentName];
+
+ useEffect(() => {
+ const handleMessage = (event) => {
+ if (event.data?.type === 'UPDATE_CODE') setCode(event.data.code);
+ };
+ window.addEventListener('message', handleMessage);
+ return () => window.removeEventListener('message', handleMessage);
+ }, []);
+
+ if (!componentName || !Component) {
+ return {'Component not found: ' + componentName}
;
+ }
+
+ const scope = { React, [componentName]: Component };
+
+ return (
+ Loading...}>
+
+
+
+
+
+ );
+}
+
+const root = createRoot(document.getElementById('root'));
+root.render();
+`;
+ writeFileSync(join(pbDir, 'preview-entry.tsx'), previewEntryCode);
+
+ const previewHtmlCode = `
+
+
+
+
+ PatternBook Preview
+
+
+
+
+
+
+`;
+ writeFileSync(join(pbDir, 'preview.html'), previewHtmlCode);
+ };
+
+ generatePreviewApp();
+
+ watchFile(MANIFEST_PATH, { interval: 1000 }, () => {
+ manifestCache = loadManifest(MANIFEST_PATH);
+ generatePreviewApp(); // regenerate virtual registry when manifest updates
+ console.log(chalk.blue(`š Reloaded manifest.json and virtual registry`));
+ });
+
+ watchFile(DEPENDENCY_GRAPH_PATH, { interval: 1000 }, () => {
+ dependencyGraphCache = loadManifest(DEPENDENCY_GRAPH_PATH);
+ });
+
+ // Try to attach Vite middleware for dynamic component bundling.
+ // Wrapped in try/catch so a port collision or misconfiguration never
+ // prevents the Express dashboard from starting.
+ try {
+ const vite = await createViteServer({
+ root: targetDir,
+ server: {
+ middlewareMode: true,
+ hmr: false,
+ },
+ appType: 'custom',
+ // Use Vite 8's native OXC transformer for JSX ā no plugin needed, no preamble issues.
+ plugins: [],
+ oxc: {
+ jsx: { runtime: 'automatic' },
+ },
+ optimizeDeps: {
+ include: ['react', 'react-dom', 'react-live', 'prism-react-renderer'],
+ },
+ resolve: {
+ dedupe: ['react', 'react-dom'],
+ },
+ logLevel: 'warn',
+ });
+ app.use(vite.middlewares);
+ console.log(chalk.green('ā Vite component bundler attached.'));
+ } catch (err) {
+ console.warn(chalk.yellow('ā ļø Vite bundler failed to start ā live component preview will be unavailable.'));
+ console.warn(chalk.gray(err instanceof Error ? err.message : String(err)));
+ }
+
+ // API Routes
+ app.get('/api/health', (req, res) => res.json({ status: 'ok' }));
+
+ const addNoCacheHeaders = (res: Response) => {
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, private');
+ };
+
+ app.get('/api/manifest', (req, res) => {
+ addNoCacheHeaders(res);
+ if (!manifestCache) return res.status(404).json({ error: 'Manifest not found' });
+ res.json(manifestCache);
+ });
+
+ app.get('/api/search', (req, res) => {
+ addNoCacheHeaders(res);
+ if (!manifestCache || !manifestCache.components) return res.status(404).json({ error: 'No components' });
+ const query = String(req.query.q || '').toLowerCase();
+ const results = manifestCache.components.filter((c: any) =>
+ c.name.toLowerCase().includes(query) ||
+ c.tags?.some((t: string) => t.toLowerCase().includes(query)) ||
+ c.type.toLowerCase().includes(query)
+ );
+ res.json({ query: req.query.q, results, total: results.length });
+ });
+
+ app.get('/api/graph', (req, res) => {
+ addNoCacheHeaders(res);
+ if (!dependencyGraphCache) return res.status(404).json({ error: 'Graph not found' });
+ res.json(dependencyGraphCache);
+ });
+
+ app.get('/api/stats', (req, res) => {
+ addNoCacheHeaders(res);
+ if (!manifestCache) return res.status(404).json({ error: 'Manifest not found' });
+ res.json({ totalComponents: manifestCache.components?.length || 0 });
+ });
+
+ // Serve preview.html explicitly ā Vite handles all .tsx script requests from there
+ app.get('/patternbook-preview/preview.html', (req: Request, res: Response) => {
+ const previewHtmlPath = join(pbDir, 'preview.html');
+ if (existsSync(previewHtmlPath)) {
+ res.sendFile(previewHtmlPath);
+ } else {
+ res.status(404).send('Preview not generated yet. Is there a manifest?');
+ }
+ });
+
+ // Send all non-API, non-vite requests to the dashboard SPA index.html
+ app.use((req: Request, res: Response, next: NextFunction) => {
+ if (
+ req.path.startsWith('/patternbook-preview/') ||
+ req.path.startsWith('/@vite/') ||
+ req.path.startsWith('/@fs/') ||
+ req.path.startsWith('/node_modules/')
+ ) {
+ return next();
+ }
+ if (existsSync(join(publicPath, 'index.html'))) {
+ res.sendFile(join(publicPath, 'index.html'));
+ } else {
+ res.status(404).send('Dashboard not bundled. Run npm run build in client/.');
+ }
+ });
+
+ app.listen(PORT, HOST, () => {
+ console.log(chalk.green(`\nš PatternBook Dashboard & Dynamic Bundler running!`));
+ console.log(chalk.cyan(` http://${HOST}:${PORT}`));
+ console.log(chalk.gray(` Serving project: ${targetDir}\n`));
+ });
+}
+