@tanstack/cta-ui-base 0.29.1 → 0.30.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.
@@ -5,8 +5,11 @@ import { useApplicationMode, useDryRun, useFilters, useOriginalOutput, useProjec
5
5
  import { getFileClass, twClasses } from '../file-classes';
6
6
  import FileViewer from './file-viewer';
7
7
  import FileTree from './file-tree';
8
+ import WebContainerProvider from './web-container-provider';
9
+ import { WebContainerPreview } from './webcontainer-preview';
8
10
  import { Label } from './ui/label';
9
11
  import { Switch } from './ui/switch';
12
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from './ui/tabs';
10
13
  export function Filters() {
11
14
  const { includedFiles, toggleFilter } = useFilters();
12
15
  return (_jsxs("div", { className: "bg-white dark:bg-black/40 shadow-xl p-4 rounded-lg flex flex-row items-center gap-4 mb-2", children: [_jsx("h3", { className: "font-medium whitespace-nowrap", children: "File Filters" }), _jsxs("div", { className: "flex flex-row items-center", children: [_jsx(Switch, { id: "unchanged", checked: includedFiles.includes('unchanged'), onCheckedChange: () => toggleFilter('unchanged'), className: "mr-2" }), _jsx(Label, { htmlFor: "unchanged", className: twClasses.unchanged, children: "Unchanged" })] }), _jsxs("div", { className: "flex flex-row items-center", children: [_jsx(Switch, { id: "added", checked: includedFiles.includes('added'), onCheckedChange: () => toggleFilter('added'), className: "mr-2" }), _jsx(Label, { htmlFor: "added", className: twClasses.added, children: "Added" })] }), _jsxs("div", { className: "flex flex-row items-center", children: [_jsx(Switch, { id: "modified", checked: includedFiles.includes('modified'), onCheckedChange: () => toggleFilter('modified'), className: "mr-2" }), _jsx(Label, { htmlFor: "modified", className: twClasses.modified, children: "Modified" })] }), _jsxs("div", { className: "flex flex-row items-center", children: [_jsx(Switch, { id: "deleted", checked: includedFiles.includes('deleted'), onCheckedChange: () => toggleFilter('deleted'), className: "mr-2" }), _jsx(Label, { htmlFor: "deleted", className: twClasses.deleted, children: "Deleted" })] }), _jsxs("div", { className: "flex flex-row items-center", children: [_jsx(Switch, { id: "overwritten", checked: includedFiles.includes('overwritten'), onCheckedChange: () => toggleFilter('overwritten'), className: "mr-2" }), _jsx(Label, { htmlFor: "overwritten", className: twClasses.overwritten, children: "Overwritten" })] })] }));
@@ -79,8 +82,22 @@ export default function FileNavigator() {
79
82
  return treeData;
80
83
  }, [tree, originalTree, localTree, includedFiles]);
81
84
  const ready = useReady();
85
+ // Prepare project files for WebContainer
86
+ const webContainerFiles = useMemo(() => {
87
+ console.log('Preparing WebContainer files, tree:', tree);
88
+ if (!tree) {
89
+ console.log('Tree is empty, returning empty array');
90
+ return [];
91
+ }
92
+ const files = Object.entries(tree).map(([path, content]) => ({
93
+ path,
94
+ content,
95
+ }));
96
+ console.log('WebContainer files prepared:', files.length, 'files');
97
+ return files;
98
+ }, [tree]);
82
99
  if (!ready) {
83
100
  return null;
84
101
  }
85
- return (_jsxs("div", { className: "bg-white dark:bg-black/50 rounded-lg p-2 sm:p-4", children: [mode === 'add' && _jsx(Filters, {}), _jsxs("div", { className: "flex flex-row @container", children: [_jsx("div", { className: "w-1/3 @6xl:w-1/4 bg-gray-500/10 rounded-l-lg", children: _jsx(FileTree, { selectedFile: selectedFile, tree: fileTree }) }), _jsx("div", { className: "w-2/3 @6xl:w-3/4", children: selectedFile && modifiedFileContents ? (_jsx(FileViewer, { filePath: selectedFile, originalFile: originalFileContents, modifiedFile: modifiedFileContents })) : null })] })] }));
102
+ return (_jsx(WebContainerProvider, { projectFiles: webContainerFiles, children: _jsxs("div", { className: "bg-white dark:bg-black/50 rounded-lg p-2 sm:p-4", children: [mode === 'add' && _jsx(Filters, {}), _jsxs(Tabs, { defaultValue: "files", className: "w-full", children: [_jsxs(TabsList, { className: "mb-1 h-7 p-0.5 bg-transparent border border-gray-300 dark:border-gray-700", children: [_jsx(TabsTrigger, { value: "files", className: "text-xs h-6 px-3 py-0 data-[state=active]:bg-gray-200 dark:data-[state=active]:bg-gray-800", children: "Files" }), _jsx(TabsTrigger, { value: "preview", className: "text-xs h-6 px-3 py-0 data-[state=active]:bg-gray-200 dark:data-[state=active]:bg-gray-800", children: "Preview" })] }), _jsx(TabsContent, { value: "files", className: "mt-0", children: _jsxs("div", { className: "flex flex-row @container", children: [_jsx("div", { className: "w-1/3 @6xl:w-1/4 bg-gray-500/10 rounded-l-lg", children: _jsx(FileTree, { selectedFile: selectedFile, tree: fileTree }) }), _jsx("div", { className: "w-2/3 @6xl:w-3/4", children: selectedFile && modifiedFileContents ? (_jsx(FileViewer, { filePath: selectedFile, originalFile: originalFileContents, modifiedFile: modifiedFileContents })) : null })] }) }), _jsx(TabsContent, { value: "preview", className: "mt-0", children: _jsx("div", { className: "h-[800px]", children: _jsx(WebContainerPreview, {}) }) })] })] }) }));
86
103
  }
@@ -33,8 +33,15 @@ export default function FileViewer({ originalFile, modifiedFile, filePath, }) {
33
33
  return javascript();
34
34
  }
35
35
  const language = getLanguage(filePath);
36
- if (!originalFile || originalFile === modifiedFile) {
37
- return (_jsx(CodeMirror, { value: modifiedFile, theme: theme, height: "100vh", width: "100%", readOnly: true, extensions: [language], className: "text-lg" }));
36
+ // Display placeholder for binary files
37
+ const displayModified = modifiedFile.startsWith('base64::')
38
+ ? '<binary file>'
39
+ : modifiedFile;
40
+ const displayOriginal = originalFile?.startsWith('base64::')
41
+ ? '<binary file>'
42
+ : originalFile;
43
+ if (!displayOriginal || displayOriginal === displayModified) {
44
+ return (_jsx(CodeMirror, { value: displayModified, theme: theme, height: "100vh", width: "100%", readOnly: true, extensions: [language], className: "text-lg" }));
38
45
  }
39
- return (_jsxs(CodeMirrorMerge, { orientation: "a-b", theme: theme, className: "text-lg", children: [_jsx(CodeMirrorMerge.Original, { value: originalFile, extensions: [language] }), _jsx(CodeMirrorMerge.Modified, { value: modifiedFile, extensions: [language] })] }));
46
+ return (_jsxs(CodeMirrorMerge, { orientation: "a-b", theme: theme, className: "text-lg", children: [_jsx(CodeMirrorMerge.Original, { value: displayOriginal, extensions: [language] }), _jsx(CodeMirrorMerge.Modified, { value: displayModified, extensions: [language] })] }));
40
47
  }
@@ -0,0 +1,31 @@
1
+ export declare const WebContainerContext: import("react").Context<import("zustand").StoreApi<{
2
+ webContainer: Promise<import("@webcontainer/api").WebContainer> | null;
3
+ ready: boolean;
4
+ setupStep: import("../hooks/use-webcontainer-store").SetupStep;
5
+ statusMessage: string;
6
+ terminalOutput: Array<string>;
7
+ previewUrl: string | null;
8
+ error: string | null;
9
+ devProcess: import("@webcontainer/api").WebContainerProcess | null;
10
+ projectFiles: Array<{
11
+ path: string;
12
+ content: string;
13
+ }>;
14
+ isInstalling: boolean;
15
+ teardown: () => void;
16
+ updateProjectFiles: (projectFiles: Array<{
17
+ path: string;
18
+ content: string;
19
+ }>) => Promise<void>;
20
+ startDevServer: () => Promise<void>;
21
+ addTerminalOutput: (output: string) => void;
22
+ installDependencies: () => Promise<void>;
23
+ setTerminalOutput: (output: Array<string>) => void;
24
+ }> | null>;
25
+ export default function WebContainerProvider({ children, projectFiles, }: {
26
+ children: React.ReactNode;
27
+ projectFiles: Array<{
28
+ path: string;
29
+ content: string;
30
+ }>;
31
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useEffect, useState } from 'react';
3
+ import { useStore } from 'zustand';
4
+ import createWebContainerStore from '../hooks/use-webcontainer-store';
5
+ export const WebContainerContext = createContext(null);
6
+ export default function WebContainerProvider({ children, projectFiles, }) {
7
+ console.log('WebContainerProvider rendering with', projectFiles.length, 'files');
8
+ const [containerStore] = useState(() => createWebContainerStore(true));
9
+ const updateProjectFiles = useStore(containerStore, (state) => state.updateProjectFiles);
10
+ useEffect(() => {
11
+ console.log('WebContainerProvider useEffect triggered with', projectFiles.length, 'files');
12
+ updateProjectFiles(projectFiles);
13
+ }, [updateProjectFiles, projectFiles]);
14
+ return (_jsx(WebContainerContext.Provider, { value: containerStore, children: children }));
15
+ }
@@ -0,0 +1 @@
1
+ export declare function WebContainerPreview(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useContext, useEffect, useRef, useState } from 'react';
3
+ import { useStore } from 'zustand';
4
+ import { ChevronDown, ChevronUp } from 'lucide-react';
5
+ import { WebContainerContext } from './web-container-provider';
6
+ export function WebContainerPreview() {
7
+ const containerStore = useContext(WebContainerContext);
8
+ if (!containerStore) {
9
+ throw new Error('WebContainerContext not found');
10
+ }
11
+ const webContainer = useStore(containerStore, (state) => state.webContainer);
12
+ const setupStep = useStore(containerStore, (state) => state.setupStep);
13
+ const statusMessage = useStore(containerStore, (state) => state.statusMessage);
14
+ const terminalOutput = useStore(containerStore, (state) => state.terminalOutput);
15
+ const error = useStore(containerStore, (state) => state.error);
16
+ const previewUrl = useStore(containerStore, (state) => state.previewUrl);
17
+ const startDevServer = useStore(containerStore, (state) => state.startDevServer);
18
+ const setTerminalOutput = useStore(containerStore, (state) => state.setTerminalOutput);
19
+ const [isTerminalOpen, setIsTerminalOpen] = useState(false);
20
+ // Auto-scroll terminal to bottom when new output arrives
21
+ const terminalRef = useRef(null);
22
+ useEffect(() => {
23
+ if (terminalRef.current) {
24
+ terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
25
+ }
26
+ }, [terminalOutput]);
27
+ const getStepIcon = (step) => {
28
+ switch (step) {
29
+ case 'mounting':
30
+ return '📁';
31
+ case 'installing':
32
+ return '📦';
33
+ case 'starting':
34
+ return '🚀';
35
+ case 'ready':
36
+ return '✅';
37
+ case 'error':
38
+ return '❌';
39
+ }
40
+ };
41
+ const getStepColor = (step) => {
42
+ switch (step) {
43
+ case 'error':
44
+ return 'text-red-500';
45
+ case 'ready':
46
+ return 'text-green-500';
47
+ default:
48
+ return 'text-blue-500';
49
+ }
50
+ };
51
+ // Show progress dialog during setup (similar to "Creating Your Application")
52
+ if (!webContainer ||
53
+ setupStep === 'error' ||
54
+ setupStep !== 'ready' ||
55
+ !previewUrl) {
56
+ return (_jsx("div", { className: "flex items-center justify-center h-full bg-gray-50 dark:bg-gray-900", children: _jsxs("div", { className: "bg-white dark:bg-gray-800 rounded-lg shadow-xl p-8 max-w-2xl w-full mx-4 border-2 border-blue-500", children: [_jsx("h2", { className: "text-2xl font-bold mb-6 text-center", children: setupStep === 'error' ? 'Setup Failed' : 'Preparing Preview' }), setupStep === 'error' ? (_jsxs("div", { className: "text-center", children: [_jsx("div", { className: "text-4xl mb-4", children: "\u274C" }), _jsx("div", { className: "text-lg font-medium text-red-600 mb-2", children: "An error occurred" }), _jsx("div", { className: "text-sm text-gray-600 dark:text-gray-400 mb-4", children: error }), _jsx("button", { onClick: startDevServer, className: "px-4 py-2 text-sm bg-orange-500 text-white rounded hover:bg-orange-600 transition-colors", children: "\uD83D\uDD04 Retry" })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-4 mb-6", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "text-2xl", children: getStepIcon('mounting') }), _jsx("div", { className: `flex-1 ${getStepColor(setupStep === 'mounting' ? 'mounting' : setupStep === 'installing' || setupStep === 'starting' || setupStep === 'ready' ? 'ready' : 'mounting')}`, children: "Mount Files" }), (setupStep === 'installing' ||
57
+ setupStep === 'starting' ||
58
+ setupStep === 'ready') &&
59
+ '✓'] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "text-2xl", children: getStepIcon('installing') }), _jsx("div", { className: `flex-1 ${getStepColor(setupStep === 'installing' ? 'installing' : setupStep === 'starting' || setupStep === 'ready' ? 'ready' : 'mounting')}`, children: "Install Dependencies" }), (setupStep === 'starting' || setupStep === 'ready') && '✓'] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "text-2xl", children: getStepIcon('starting') }), _jsx("div", { className: `flex-1 ${getStepColor(setupStep === 'starting' ? 'starting' : setupStep === 'ready' ? 'ready' : 'mounting')}`, children: "Start Server" }), setupStep === 'ready' && '✓'] })] }), _jsx("div", { className: "text-center text-sm text-gray-600 dark:text-gray-400", children: statusMessage || 'Preparing your application...' })] }))] }) }));
60
+ }
61
+ // Show the running application with collapsible terminal
62
+ return (_jsxs("div", { className: "flex flex-col h-full", children: [_jsx("div", { className: "flex-1", children: previewUrl ? (_jsx("iframe", { src: previewUrl, className: "w-full h-full border-0", title: "Application Preview", onLoad: () => console.log('Iframe loaded successfully'), onError: (e) => console.error('Iframe load error:', e) })) : (_jsx("div", { className: "flex items-center justify-center h-full bg-gray-100 dark:bg-gray-800", children: _jsxs("div", { className: "text-center", children: [_jsx("div", { className: "text-2xl mb-2", children: "\uD83D\uDD04" }), _jsx("div", { className: "text-sm text-gray-600 dark:text-gray-400", children: "Preview not available" })] }) })) }), _jsxs("div", { className: "border-t border-gray-200 dark:border-gray-700 bg-black text-green-400 flex flex-col flex-shrink-0", children: [_jsxs("div", { className: "p-2 border-b border-gray-700 bg-gray-900 flex items-center justify-between cursor-pointer hover:bg-gray-800 transition-colors", onClick: () => setIsTerminalOpen(!isTerminalOpen), children: [_jsxs("div", { className: "flex items-center gap-2 flex-1", children: [_jsx("button", { className: "text-gray-400 hover:text-gray-200", children: isTerminalOpen ? (_jsx(ChevronDown, { className: "w-4 h-4" })) : (_jsx(ChevronUp, { className: "w-4 h-4" })) }), _jsx("div", { className: "text-xs font-medium text-gray-300", children: "Terminal Output" }), setupStep === 'ready' && previewUrl && (_jsx("div", { className: "text-xs text-green-500", children: "\u25CF Server Running" }))] }), _jsxs("div", { className: "flex items-center gap-2", onClick: (e) => e.stopPropagation(), children: [previewUrl && (_jsx("button", { onClick: () => window.open(previewUrl, '_blank'), className: "px-2 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors", children: "Open in New Tab" })), _jsx("button", { onClick: startDevServer, disabled: !webContainer, className: "px-2 py-1 text-xs bg-orange-500 text-white rounded hover:bg-orange-600 transition-colors disabled:opacity-50", children: "\uD83D\uDD04 Restart" }), _jsx("button", { onClick: () => setTerminalOutput([]), className: "text-xs text-gray-400 hover:text-gray-200 transition-colors", children: "Clear" })] })] }), isTerminalOpen && (_jsx("div", { ref: terminalRef, className: "font-mono text-xs p-2 overflow-y-auto overflow-x-hidden", style: { maxHeight: '200px' }, children: terminalOutput.length > 0 ? (terminalOutput.map((line, index) => (_jsx("div", { className: "mb-1 leading-tight whitespace-pre-wrap break-words", children: line }, index)))) : (_jsx("div", { className: "text-gray-500", children: "No output yet..." })) }))] })] }));
63
+ }
@@ -0,0 +1,24 @@
1
+ export declare function useWebContainer(): import("zustand").StoreApi<{
2
+ webContainer: Promise<import("@webcontainer/api").WebContainer> | null;
3
+ ready: boolean;
4
+ setupStep: import("./use-webcontainer-store").SetupStep;
5
+ statusMessage: string;
6
+ terminalOutput: Array<string>;
7
+ previewUrl: string | null;
8
+ error: string | null;
9
+ devProcess: import("@webcontainer/api").WebContainerProcess | null;
10
+ projectFiles: Array<{
11
+ path: string;
12
+ content: string;
13
+ }>;
14
+ isInstalling: boolean;
15
+ teardown: () => void;
16
+ updateProjectFiles: (projectFiles: Array<{
17
+ path: string;
18
+ content: string;
19
+ }>) => Promise<void>;
20
+ startDevServer: () => Promise<void>;
21
+ addTerminalOutput: (output: string) => void;
22
+ installDependencies: () => Promise<void>;
23
+ setTerminalOutput: (output: Array<string>) => void;
24
+ }> | null;
@@ -0,0 +1,6 @@
1
+ import { useContext } from 'react';
2
+ import { WebContainerContext } from '../components/web-container-provider';
3
+ export function useWebContainer() {
4
+ const webContainer = useContext(WebContainerContext);
5
+ return webContainer;
6
+ }
@@ -0,0 +1,29 @@
1
+ import { WebContainer } from '@webcontainer/api';
2
+ import type { WebContainerProcess } from '@webcontainer/api';
3
+ export type SetupStep = 'mounting' | 'installing' | 'starting' | 'ready' | 'error';
4
+ type WebContainerStore = {
5
+ webContainer: Promise<WebContainer> | null;
6
+ ready: boolean;
7
+ setupStep: SetupStep;
8
+ statusMessage: string;
9
+ terminalOutput: Array<string>;
10
+ previewUrl: string | null;
11
+ error: string | null;
12
+ devProcess: WebContainerProcess | null;
13
+ projectFiles: Array<{
14
+ path: string;
15
+ content: string;
16
+ }>;
17
+ isInstalling: boolean;
18
+ teardown: () => void;
19
+ updateProjectFiles: (projectFiles: Array<{
20
+ path: string;
21
+ content: string;
22
+ }>) => Promise<void>;
23
+ startDevServer: () => Promise<void>;
24
+ addTerminalOutput: (output: string) => void;
25
+ installDependencies: () => Promise<void>;
26
+ setTerminalOutput: (output: Array<string>) => void;
27
+ };
28
+ export default function createWebContainerStore(shouldShimALS: boolean): import("zustand").StoreApi<WebContainerStore>;
29
+ export {};
@@ -0,0 +1,334 @@
1
+ import { WebContainer } from '@webcontainer/api';
2
+ import { createStore } from 'zustand';
3
+ import shimALS from '../lib/als-shim';
4
+ console.log('>>> startup');
5
+ const processTerminalLine = (data) => {
6
+ // Clean up terminal output - remove ANSI codes and control characters
7
+ let cleaned = data;
8
+ // Remove all ANSI escape sequences (comprehensive)
9
+ cleaned = cleaned.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, ''); // Standard ANSI sequences
10
+ cleaned = cleaned.replace(/\u001b\][0-9;]*;[^\u0007]*\u0007/g, ''); // OSC sequences
11
+ cleaned = cleaned.replace(/\u001b[=>]/g, ''); // Other escape codes
12
+ // Remove carriage returns and other control characters
13
+ cleaned = cleaned.replace(/\r/g, '');
14
+ cleaned = cleaned.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, '');
15
+ // Remove spinner characters and progress bar artifacts
16
+ cleaned = cleaned.replace(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/g, '');
17
+ cleaned = cleaned.replace(/[▀▁▂▃▄▅▆▇█▉▊▋▌▍▎▏]/g, '');
18
+ cleaned = cleaned.replace(/[░▒▓]/g, '');
19
+ // Trim excessive whitespace
20
+ cleaned = cleaned.trim();
21
+ // Only return non-empty lines
22
+ return cleaned.length > 0 ? cleaned : '';
23
+ };
24
+ let webContainer = null;
25
+ export default function createWebContainerStore(shouldShimALS) {
26
+ if (!webContainer) {
27
+ webContainer = WebContainer.boot();
28
+ }
29
+ const store = createStore((set, get) => ({
30
+ webContainer,
31
+ ready: false,
32
+ setupStep: 'mounting',
33
+ statusMessage: '',
34
+ terminalOutput: [],
35
+ previewUrl: null,
36
+ error: null,
37
+ devProcess: null,
38
+ projectFiles: [],
39
+ isInstalling: false,
40
+ teardown: () => {
41
+ set({ webContainer: null, ready: false });
42
+ },
43
+ addTerminalOutput: (output) => {
44
+ set(({ terminalOutput }) => ({
45
+ terminalOutput: [...terminalOutput, output],
46
+ }));
47
+ },
48
+ setTerminalOutput: (output) => {
49
+ set({ terminalOutput: output });
50
+ },
51
+ startDevServer: async () => {
52
+ const { devProcess, webContainer, addTerminalOutput } = get();
53
+ if (!webContainer) {
54
+ throw new Error('WebContainer not found');
55
+ }
56
+ try {
57
+ const container = await webContainer;
58
+ if (!container) {
59
+ throw new Error('WebContainer not found');
60
+ }
61
+ if (devProcess) {
62
+ console.log('Killing existing dev process...');
63
+ devProcess.kill();
64
+ set({ devProcess: null });
65
+ }
66
+ set({
67
+ setupStep: 'starting',
68
+ statusMessage: 'Starting development server...',
69
+ });
70
+ addTerminalOutput('🚀 Starting dev server...');
71
+ // Wait for server to be ready (set up listener first)
72
+ container.on('server-ready', (port, url) => {
73
+ console.log('Server ready on port', port, 'at', url);
74
+ const currentState = get();
75
+ set({
76
+ previewUrl: url,
77
+ setupStep: 'ready',
78
+ statusMessage: 'Development server running',
79
+ terminalOutput: [
80
+ ...currentState.terminalOutput,
81
+ `✅ Server ready at ${url}`,
82
+ ],
83
+ });
84
+ });
85
+ // Start the dev server
86
+ const newDevProcess = await container.spawn('pnpm', ['dev']);
87
+ set({ devProcess: newDevProcess });
88
+ newDevProcess.output.pipeTo(new WritableStream({
89
+ write(data) {
90
+ const cleaned = processTerminalLine(data);
91
+ if (cleaned && cleaned.length > 3) {
92
+ console.log('[DEV]', cleaned);
93
+ const currentState = get();
94
+ set({
95
+ terminalOutput: [...currentState.terminalOutput, cleaned],
96
+ });
97
+ }
98
+ },
99
+ }));
100
+ // Check exit code
101
+ const exitCode = await newDevProcess.exit;
102
+ if (exitCode !== 0) {
103
+ addTerminalOutput(`❌ Dev server exited with code ${exitCode}`);
104
+ set({ error: `Dev server exited with code ${exitCode}` });
105
+ }
106
+ }
107
+ catch (error) {
108
+ console.error('Dev server start error:', error);
109
+ addTerminalOutput(`❌ Dev server error: ${error.message}`);
110
+ set({ error: error.message, setupStep: 'error' });
111
+ }
112
+ },
113
+ updateProjectFiles: async (projectFiles) => {
114
+ const { projectFiles: originalProjectFiles, addTerminalOutput, installDependencies, webContainer, } = get();
115
+ if (!webContainer) {
116
+ console.error('WebContainer not found in updateProjectFiles');
117
+ throw new Error('WebContainer not found');
118
+ }
119
+ try {
120
+ const container = await webContainer;
121
+ if (!container) {
122
+ console.error('WebContainer resolved to null');
123
+ throw new Error('WebContainer not found');
124
+ }
125
+ console.log('WebContainer booted successfully!', container);
126
+ }
127
+ catch (error) {
128
+ console.error('WebContainer boot failed:', error);
129
+ set({
130
+ error: `WebContainer boot failed: ${error.message}`,
131
+ setupStep: 'error',
132
+ });
133
+ throw error;
134
+ }
135
+ const container = await webContainer;
136
+ let packageJSONChanged = false;
137
+ const binaryFiles = {};
138
+ if (originalProjectFiles.length === 0) {
139
+ const fileSystemTree = {};
140
+ let base64FileCount = 0;
141
+ for (const { path, content } of projectFiles) {
142
+ const cleanPath = path.replace(/^\.?\//, '');
143
+ const pathParts = cleanPath.split('/');
144
+ let current = fileSystemTree;
145
+ for (let i = 0; i < pathParts.length - 1; i++) {
146
+ const part = pathParts[i];
147
+ if (!current[part]) {
148
+ current[part] = { directory: {} };
149
+ }
150
+ current = current[part].directory;
151
+ }
152
+ const fileName = pathParts[pathParts.length - 1];
153
+ const adjustedContent = shouldShimALS
154
+ ? shimALS(fileName, content)
155
+ : content;
156
+ if (adjustedContent.startsWith('base64::')) {
157
+ base64FileCount++;
158
+ const base64Content = adjustedContent.replace('base64::', '');
159
+ try {
160
+ const base64Cleaned = base64Content.replace(/\s/g, '');
161
+ const binaryString = atob(base64Cleaned);
162
+ const bytes = new Uint8Array(binaryString.length);
163
+ for (let i = 0; i < binaryString.length; i++) {
164
+ bytes[i] = binaryString.charCodeAt(i) & 0xff;
165
+ }
166
+ binaryFiles[cleanPath] = bytes;
167
+ }
168
+ catch (error) {
169
+ console.error(`[BINARY ERROR] Failed to convert ${cleanPath}:`, error);
170
+ }
171
+ }
172
+ else {
173
+ current[fileName] = {
174
+ file: {
175
+ contents: String(adjustedContent),
176
+ },
177
+ };
178
+ }
179
+ }
180
+ // Write the binary files on their own since mount doesn't support binary files correctly
181
+ await container.mount(fileSystemTree);
182
+ for (const [path, bytes] of Object.entries(binaryFiles)) {
183
+ await container.fs.writeFile(path, bytes);
184
+ }
185
+ packageJSONChanged = true;
186
+ }
187
+ else {
188
+ const originalMap = new Map();
189
+ for (const { path, content } of originalProjectFiles) {
190
+ originalMap.set(path, content);
191
+ }
192
+ const newMap = new Map();
193
+ for (const { path, content } of projectFiles) {
194
+ newMap.set(path, content);
195
+ }
196
+ const changedOrNewFiles = [];
197
+ for (const { path, content } of projectFiles) {
198
+ if (!originalMap.has(path)) {
199
+ changedOrNewFiles.push({ path, content });
200
+ }
201
+ else if (originalMap.get(path) !== content) {
202
+ changedOrNewFiles.push({ path, content });
203
+ }
204
+ }
205
+ const deletedFiles = [];
206
+ for (const { path } of originalProjectFiles) {
207
+ if (!newMap.has(path)) {
208
+ deletedFiles.push(path);
209
+ }
210
+ }
211
+ if (changedOrNewFiles.length > 0 || deletedFiles.length > 0) {
212
+ // Kill dev server before updating files to avoid HMR issues
213
+ const { devProcess } = get();
214
+ if (devProcess) {
215
+ console.log('Stopping dev server before file update...');
216
+ addTerminalOutput('⏸️ Stopping dev server before updating files...');
217
+ devProcess.kill();
218
+ set({ devProcess: null, previewUrl: null });
219
+ }
220
+ for (const { path, content } of changedOrNewFiles) {
221
+ await container.fs.writeFile(path, content);
222
+ }
223
+ for (const path of deletedFiles) {
224
+ await container.fs.rm(path);
225
+ }
226
+ addTerminalOutput('📁 Files updated successfully');
227
+ if (changedOrNewFiles.some(({ path }) => path === './package.json')) {
228
+ packageJSONChanged = true;
229
+ }
230
+ }
231
+ }
232
+ set({ projectFiles });
233
+ if (packageJSONChanged) {
234
+ addTerminalOutput('📦 Package.json changed, reinstalling dependencies...');
235
+ await installDependencies();
236
+ }
237
+ },
238
+ installDependencies: async () => {
239
+ const { webContainer, addTerminalOutput, startDevServer, isInstalling } = get();
240
+ if (isInstalling) {
241
+ console.log('Install already in progress, skipping');
242
+ return;
243
+ }
244
+ if (!webContainer) {
245
+ throw new Error('WebContainer not found');
246
+ }
247
+ set({ isInstalling: true });
248
+ try {
249
+ const container = await webContainer;
250
+ if (!container) {
251
+ set({ isInstalling: false });
252
+ throw new Error('WebContainer not found');
253
+ }
254
+ set({
255
+ setupStep: 'installing',
256
+ statusMessage: 'Installing dependencies...',
257
+ });
258
+ console.log('Starting pnpm install...');
259
+ addTerminalOutput('📦 Running pnpm install...');
260
+ addTerminalOutput('⏳ This may take a minute...');
261
+ let installProcess;
262
+ try {
263
+ installProcess = await container.spawn('pnpm', ['install']);
264
+ console.log('pnpm install process spawned successfully');
265
+ }
266
+ catch (spawnError) {
267
+ console.error('Failed to spawn pnpm install:', spawnError);
268
+ throw spawnError;
269
+ }
270
+ let outputCount = 0;
271
+ let lastProgressUpdate = Date.now();
272
+ let allOutput = [];
273
+ let progressInterval = setInterval(() => {
274
+ const elapsed = Math.floor((Date.now() - lastProgressUpdate) / 1000);
275
+ console.log(`[INSTALL] Still running... (${elapsed}s, ${outputCount} output chunks)`);
276
+ }, 5000);
277
+ installProcess.output.pipeTo(new WritableStream({
278
+ write(data) {
279
+ outputCount++;
280
+ allOutput.push(data);
281
+ const cleaned = processTerminalLine(data);
282
+ // Show meaningful output immediately
283
+ if (cleaned && cleaned.length > 3) {
284
+ const isImportant = cleaned.includes('added') ||
285
+ cleaned.includes('removed') ||
286
+ cleaned.includes('changed') ||
287
+ cleaned.includes('audited') ||
288
+ cleaned.includes('packages') ||
289
+ cleaned.includes('error') ||
290
+ cleaned.includes('warn') ||
291
+ cleaned.includes('ERR') ||
292
+ cleaned.includes('FAIL');
293
+ if (isImportant) {
294
+ console.log('[INSTALL]', cleaned);
295
+ addTerminalOutput(cleaned);
296
+ if (isImportant && progressInterval) {
297
+ clearInterval(progressInterval);
298
+ progressInterval = undefined;
299
+ }
300
+ }
301
+ }
302
+ },
303
+ }));
304
+ console.log('Waiting for install to complete...');
305
+ const installExitCode = await installProcess.exit;
306
+ if (progressInterval)
307
+ clearInterval(progressInterval);
308
+ console.log('Install exit code:', installExitCode);
309
+ console.log('Total output lines:', outputCount);
310
+ if (installExitCode !== 0) {
311
+ // Show all output for debugging
312
+ console.error('[INSTALL ERROR] All output:', allOutput.join('\n'));
313
+ const errorMsg = `pnpm install failed with exit code ${installExitCode}`;
314
+ addTerminalOutput(`❌ ${errorMsg}`);
315
+ addTerminalOutput('💡 Check console for detailed error output');
316
+ set({ error: errorMsg, setupStep: 'error' });
317
+ throw new Error(errorMsg);
318
+ }
319
+ addTerminalOutput('✅ Dependencies installed successfully');
320
+ await startDevServer();
321
+ }
322
+ catch (error) {
323
+ console.error('Install error:', error);
324
+ addTerminalOutput(`❌ Install error: ${error.message}`);
325
+ set({ error: error.message, setupStep: 'error' });
326
+ throw error;
327
+ }
328
+ finally {
329
+ set({ isInstalling: false });
330
+ }
331
+ },
332
+ }));
333
+ return store;
334
+ }
package/dist/index.d.ts CHANGED
@@ -15,6 +15,9 @@ import ModeSelector from './components/sidebar-items/mode-selector';
15
15
  import TypescriptSwitch from './components/sidebar-items/typescript-switch';
16
16
  import StarterDialog from './components/sidebar-items/starter';
17
17
  import SidebarGroup from './components/sidebar-items/sidebar-group';
18
+ import WebContainerProvider from './components/web-container-provider';
19
+ import { WebContainerPreview } from './components/webcontainer-preview';
18
20
  import { useApplicationMode, useManager, useReady } from './store/project';
19
- export { FileNavigator, AppSidebar, AppHeader, BackgroundAnimation, Toaster, StartupDialog, QueryProvider, CTAProvider, SelectedAddOns, RunAddOns, RunCreateApp, ProjectName, ModeSelector, TypescriptSwitch, StarterDialog, SidebarGroup, useApplicationMode, useManager, useReady, };
21
+ import { useWebContainer } from './hooks/use-web-container';
22
+ export { FileNavigator, AppSidebar, AppHeader, BackgroundAnimation, Toaster, StartupDialog, QueryProvider, CTAProvider, SelectedAddOns, RunAddOns, RunCreateApp, ProjectName, ModeSelector, TypescriptSwitch, StarterDialog, SidebarGroup, WebContainerProvider, WebContainerPreview, useApplicationMode, useManager, useReady, useWebContainer, };
20
23
  export default RootComponent;
package/dist/index.js CHANGED
@@ -15,6 +15,9 @@ import ModeSelector from './components/sidebar-items/mode-selector';
15
15
  import TypescriptSwitch from './components/sidebar-items/typescript-switch';
16
16
  import StarterDialog from './components/sidebar-items/starter';
17
17
  import SidebarGroup from './components/sidebar-items/sidebar-group';
18
+ import WebContainerProvider from './components/web-container-provider';
19
+ import { WebContainerPreview } from './components/webcontainer-preview';
18
20
  import { useApplicationMode, useManager, useReady } from './store/project';
19
- export { FileNavigator, AppSidebar, AppHeader, BackgroundAnimation, Toaster, StartupDialog, QueryProvider, CTAProvider, SelectedAddOns, RunAddOns, RunCreateApp, ProjectName, ModeSelector, TypescriptSwitch, StarterDialog, SidebarGroup, useApplicationMode, useManager, useReady, };
21
+ import { useWebContainer } from './hooks/use-web-container';
22
+ export { FileNavigator, AppSidebar, AppHeader, BackgroundAnimation, Toaster, StartupDialog, QueryProvider, CTAProvider, SelectedAddOns, RunAddOns, RunCreateApp, ProjectName, ModeSelector, TypescriptSwitch, StarterDialog, SidebarGroup, WebContainerProvider, WebContainerPreview, useApplicationMode, useManager, useReady, useWebContainer, };
20
23
  export default RootComponent;
@@ -0,0 +1 @@
1
+ export default function shimALS(fileName: string, content: string): string;