From 0d3a560535ddbf4a2d020b442f3d9e46dccd3a1d Mon Sep 17 00:00:00 2001 From: Ashwin V C Date: Sat, 15 Nov 2025 18:53:00 -0800 Subject: [PATCH] pass all files to sandpack, [a lil slower to start though] --- src/app/api/components/all/route.ts | 121 ++++--------------------- src/components/component-viewer.tsx | 131 ++++++++++++++++++---------- src/hooks/use-components.ts | 2 +- 3 files changed, 99 insertions(+), 155 deletions(-) diff --git a/src/app/api/components/all/route.ts b/src/app/api/components/all/route.ts index c360944..7a81f4a 100644 --- a/src/app/api/components/all/route.ts +++ b/src/app/api/components/all/route.ts @@ -6,130 +6,39 @@ import { componentExamples } from "@/lib/component-registry"; type ComponentData = { name: string; fileName: string; - files: Record; + code: string; exampleCode: string; }; -/** - * Extracts @/components/ui/ imports from a code string - */ -function extractUIImports(code: string): string[] { - const importRegex = /from\s+["']@\/components\/ui\/([^"']+)["']/g; - const matches = [...code.matchAll(importRegex)]; - return matches.map((match) => match[1]); -} - -/** - * Recursively loads component dependencies by parsing imports - */ -async function loadComponentWithDependencies( - componentFileName: string, - componentsDir: string, - loadedComponents = new Set() -): Promise> { - // Prevent circular dependencies and duplicates - if (loadedComponents.has(componentFileName)) { - return {}; - } - - loadedComponents.add(componentFileName); - - const files: Record = {}; - const filePath = join(componentsDir, componentFileName); - - try { - const code = await readFile(filePath, "utf-8"); - - // Extract component name without extension for the key - const componentKey = componentFileName.replace(/\.tsx?$/, ""); - files[componentKey] = code; - - // Parse imports to find dependencies on @/components/ui/ - const dependencies = extractUIImports(code); - - // Load each dependency recursively - for (const dependencyName of dependencies) { - const dependencyFileName = `${dependencyName}.tsx`; - - const dependencyFiles = await loadComponentWithDependencies( - dependencyFileName, - componentsDir, - loadedComponents - ); - - // Merge dependency files - Object.assign(files, dependencyFiles); - } - - return files; - } catch (error) { - console.error(`Error loading component ${componentFileName}:`, error); - return files; - } -} - export async function GET() { try { const componentsDir = join(process.cwd(), "src", "components", "ui"); const files = await readdir(componentsDir); - // Get list of components - const componentsList = files - .filter((file) => file.endsWith(".tsx")) - .map((file) => ({ - fileName: file, - name: file - .replace(".tsx", "") - .split("-") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "), - })) - .sort((a, b) => a.name.localeCompare(b.name)); - - // Load all components with their dependencies const components: ComponentData[] = []; - for (const component of componentsList) { - const loadedComponents = new Set(); - const componentFiles = await loadComponentWithDependencies( - component.fileName, - componentsDir, - loadedComponents - ); + for (const file of files) { + if (!file.endsWith(".tsx")) continue; - const componentKey = component.fileName.replace(".tsx", ""); + const componentKey = file.replace(".tsx", ""); + const filePath = join(componentsDir, file); + const code = await readFile(filePath, "utf-8"); const exampleCode = componentExamples[componentKey] || ""; - // Extract dependencies from the example code - if (exampleCode) { - const exampleDependencies = extractUIImports(exampleCode); - - // Load any dependencies from the example code that weren't already loaded - for (const dependencyName of exampleDependencies) { - const dependencyFileName = `${dependencyName}.tsx`; - - // Only load if not already loaded - if (!loadedComponents.has(dependencyFileName)) { - const dependencyFiles = await loadComponentWithDependencies( - dependencyFileName, - componentsDir, - loadedComponents - ); - - // Merge dependency files - Object.assign(componentFiles, dependencyFiles); - } - } - } - components.push({ - name: component.name, - fileName: component.fileName, - files: componentFiles, + name: file + .replace(".tsx", "") + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "), + fileName: file, + code, exampleCode, }); } + components.sort((a, b) => a.name.localeCompare(b.name)); + return NextResponse.json({ components }); } catch (error) { console.error("Error loading all components:", error); diff --git a/src/components/component-viewer.tsx b/src/components/component-viewer.tsx index 4ceae9e..b296837 100644 --- a/src/components/component-viewer.tsx +++ b/src/components/component-viewer.tsx @@ -5,6 +5,7 @@ import { SandboxLayout, SandboxCodeEditor, SandboxPreview, + SandboxConsole, } from "@/components/kibo-ui/sandbox"; import { ComponentData, useComponents } from "@/hooks/use-components"; import { useActiveComponent } from "@/stores/use-active-component"; @@ -13,7 +14,7 @@ import { useSandpackNavigation, } from "@codesandbox/sandpack-react"; import { useTheme } from "next-themes"; -import { useEffect, useLayoutEffect, useMemo } from "react"; +import { useEffect, useLayoutEffect, useMemo, useState } from "react"; import { ALL_DEPENDENCIES } from "@/lib/component-registry"; function transformAbsoluteToRelativeImports(code: string): string { @@ -253,6 +254,20 @@ export default { }; `; +const getViteConfigTS = ( + dependencies: Record +) => `import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + optimizeDeps: { + disabled: true, + }, +}) + `; + const getIndexHTML = (isDark: boolean) => ` @@ -274,8 +289,7 @@ const getPackageJSON = (dependencies: Record) => type: "module", scripts: { dev: "vite", - build: - "npx tailwindcss -i ./index.css -o ./output.css && tsc && vite build", + build: "tsc && vite build", preview: "vite preview", }, dependencies: { @@ -335,6 +349,9 @@ export function ComponentViewer() { const { activeComponentName } = useActiveComponent(); const { data: allComponents = [] } = useComponents(); const { resolvedTheme } = useTheme(); + const [initialFiles, setInitialFiles] = useState< + Record + >({}); const activeComponent = useMemo(() => { return allComponents.find( @@ -342,27 +359,19 @@ export function ComponentViewer() { ) as ComponentData; }, [allComponents, activeComponentName]); - const files = useMemo(() => { - if (!activeComponent) return {}; - - const { fileName, exampleCode, files: componentFiles } = activeComponent; - const componentName = fileName.replace(".tsx", ""); - const componentCode = componentFiles[componentName] || ""; + // Setup all files once on mount + useEffect(() => { + if (allComponents.length === 0 || !activeComponent) return; - const transformedExampleCode = - transformAbsoluteToRelativeImports(exampleCode); - const transformedComponentCode = - transformAbsoluteToRelativeImports(componentCode); + const transformedExampleCode = transformAbsoluteToRelativeImports( + activeComponent.exampleCode + ); const setupFiles: Record = { "/App.tsx": { code: transformedExampleCode, readOnly: false, }, - [`/${componentName}.tsx`]: { - code: transformedComponentCode, - readOnly: false, - }, "/index.html": { code: getIndexHTML(resolvedTheme === "dark"), readOnly: false, @@ -371,24 +380,29 @@ export function ComponentViewer() { code: getPackageJSON(ALL_DEPENDENCIES), readOnly: true, }, + "/vite.config.ts": { + code: getViteConfigTS(ALL_DEPENDENCIES), + readOnly: false, + }, ...INITIAL_FILES, }; - for (const [fileName, code] of Object.entries(componentFiles)) { - if (fileName === componentName) continue; - - const transformedCode = transformAbsoluteToRelativeImports(code); + for (const component of allComponents) { + const componentName = component.fileName.replace(".tsx", ""); + const transformedCode = transformAbsoluteToRelativeImports( + component.code + ); - setupFiles[`/${fileName}.tsx`] = { + setupFiles[`/${componentName}.tsx`] = { code: transformedCode, readOnly: false, }; } - return setupFiles; - }, [activeComponent, resolvedTheme]); + setInitialFiles(setupFiles); + }, [allComponents]); - if (Object.keys(files).length === 0) { + if (Object.keys(initialFiles).length === 0) { return (

Loading component...

@@ -399,7 +413,7 @@ export function ComponentViewer() { return (
@@ -410,7 +424,8 @@ export function ComponentViewer() {
- + + Preview
- - - +
+ + + + + + +
@@ -442,25 +462,40 @@ export function ComponentViewer() { ); } -function UpdateDarkMode({ - files, -}: { - files: Record; -}) { - const { resolvedTheme } = useTheme(); +function UpdateActiveComponent() { + const { activeComponentName } = useActiveComponent(); + const { data: allComponents = [] } = useComponents(); const { sandpack } = useSandpack(); - const { refresh } = useSandpackNavigation(); useEffect(() => { - const timeout = setTimeout(() => { - refresh(); - }, 300); + console.log("running update active component hook"); + const activeComponent = allComponents.find( + (c) => c.fileName === activeComponentName + ); - return () => clearTimeout(timeout); - }, [files, refresh]); + if (!activeComponent) return; - useLayoutEffect(() => { - sandpack.updateFile("/index.html", getIndexHTML(resolvedTheme === "dark")); + const transformedExampleCode = transformAbsoluteToRelativeImports( + activeComponent.exampleCode + ); + + sandpack.updateFile("/App.tsx", transformedExampleCode, true); + }, [activeComponentName, allComponents]); + + return null; +} + +function UpdateDarkMode() { + const { resolvedTheme } = useTheme(); + const { sandpack } = useSandpack(); + + useEffect(() => { + console.log("running update dark mode hook"); + sandpack.updateFile( + "/index.html", + getIndexHTML(resolvedTheme === "dark"), + true + ); }, [resolvedTheme]); return null; diff --git a/src/hooks/use-components.ts b/src/hooks/use-components.ts index e1aee26..ebba04d 100644 --- a/src/hooks/use-components.ts +++ b/src/hooks/use-components.ts @@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query"; export type ComponentData = { name: string; fileName: string; - files: Record; + code: string; exampleCode: string; };