@illuma-ai/code-sandbox 1.0.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.
- package/LICENSE +15 -0
- package/dist/__sw__.js +712 -0
- package/dist/code-sandbox.css +1 -0
- package/dist/components/BootOverlay.d.ts +17 -0
- package/dist/components/CodeEditor.d.ts +11 -0
- package/dist/components/FileTree.d.ts +19 -0
- package/dist/components/Preview.d.ts +15 -0
- package/dist/components/Terminal.d.ts +15 -0
- package/dist/components/ViewSlider.d.ts +25 -0
- package/dist/components/Workbench.d.ts +28 -0
- package/dist/hooks/useRuntime.d.ts +25 -0
- package/dist/index.cjs +50074 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +77335 -0
- package/dist/index.js.map +1 -0
- package/dist/services/git.d.ts +57 -0
- package/dist/services/runtime.d.ts +119 -0
- package/dist/templates/fullstack-starter.d.ts +38 -0
- package/dist/templates/index.d.ts +38 -0
- package/dist/types.d.ts +137 -0
- package/package.json +69 -0
- package/src/components/BootOverlay.tsx +145 -0
- package/src/components/CodeEditor.tsx +168 -0
- package/src/components/FileTree.tsx +286 -0
- package/src/components/Preview.tsx +50 -0
- package/src/components/Terminal.tsx +68 -0
- package/src/components/ViewSlider.tsx +87 -0
- package/src/components/Workbench.tsx +301 -0
- package/src/hooks/useRuntime.ts +236 -0
- package/src/index.ts +48 -0
- package/src/services/git.ts +415 -0
- package/src/services/runtime.ts +536 -0
- package/src/styles.css +24 -0
- package/src/templates/fullstack-starter.ts +3297 -0
- package/src/templates/index.ts +607 -0
- package/src/types.ts +179 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workbench — Main layout component for the code sandbox.
|
|
3
|
+
*
|
|
4
|
+
* Two full-width views toggled via a pill slider in the toolbar:
|
|
5
|
+
* - Code: FileTree (left) + Editor (top-right) + Terminal (bottom-right)
|
|
6
|
+
* - Preview: Full-width browser iframe with URL bar
|
|
7
|
+
*
|
|
8
|
+
* The toggle lives inside a compact toolbar row shared with the URL bar.
|
|
9
|
+
* Both views are full-width — Preview has no file tree, Code has no iframe.
|
|
10
|
+
*
|
|
11
|
+
* Responsive: works from ~320px up. On narrow screens the file tree
|
|
12
|
+
* collapses to a smaller width, and the allotment handles overflow.
|
|
13
|
+
*
|
|
14
|
+
* @see bolt.diy: app/components/workbench/Workbench.client.tsx (reference)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React, {
|
|
18
|
+
useMemo,
|
|
19
|
+
useState,
|
|
20
|
+
useEffect,
|
|
21
|
+
useCallback,
|
|
22
|
+
useRef,
|
|
23
|
+
} from "react";
|
|
24
|
+
import { Allotment } from "allotment";
|
|
25
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
26
|
+
import "allotment/dist/style.css";
|
|
27
|
+
|
|
28
|
+
import { buildFileTree, FileTree } from "./FileTree";
|
|
29
|
+
import { CodeEditor } from "./CodeEditor";
|
|
30
|
+
import { Terminal } from "./Terminal";
|
|
31
|
+
import { Preview } from "./Preview";
|
|
32
|
+
import { BootOverlay } from "./BootOverlay";
|
|
33
|
+
import { ViewSlider, type WorkbenchView } from "./ViewSlider";
|
|
34
|
+
import { useRuntime } from "../hooks/useRuntime";
|
|
35
|
+
import type { CodeSandboxProps } from "../types";
|
|
36
|
+
|
|
37
|
+
/** Transition for view crossfade */
|
|
38
|
+
const VIEW_TRANSITION = {
|
|
39
|
+
duration: 0.2,
|
|
40
|
+
ease: [0.4, 0, 0.2, 1] as [number, number, number, number],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* CodeSandbox — The main entrypoint component.
|
|
45
|
+
*
|
|
46
|
+
* Renders a complete IDE-like workbench with two full-width views:
|
|
47
|
+
* - Code: file tree + editor + terminal (split panes)
|
|
48
|
+
* - Preview: full-width browser iframe
|
|
49
|
+
*
|
|
50
|
+
* A compact toolbar row contains the view toggle and (in preview mode)
|
|
51
|
+
* a URL bar with refresh button.
|
|
52
|
+
*/
|
|
53
|
+
export function CodeSandbox(props: CodeSandboxProps) {
|
|
54
|
+
const { className = "", height = "100vh", port = 3000 } = props;
|
|
55
|
+
|
|
56
|
+
const {
|
|
57
|
+
state,
|
|
58
|
+
selectedFile,
|
|
59
|
+
openFiles,
|
|
60
|
+
handleFileChange,
|
|
61
|
+
handleSelectFile,
|
|
62
|
+
handleCloseFile,
|
|
63
|
+
restart,
|
|
64
|
+
} = useRuntime(props);
|
|
65
|
+
|
|
66
|
+
const fileTree = useMemo(() => buildFileTree(state.files), [state.files]);
|
|
67
|
+
const isBooting = state.status !== "ready" && state.status !== "error";
|
|
68
|
+
|
|
69
|
+
// Active view: code or preview
|
|
70
|
+
const [activeView, setActiveView] = useState<WorkbenchView>("code");
|
|
71
|
+
|
|
72
|
+
// Auto-switch to preview when server becomes ready
|
|
73
|
+
const hasPreview = !!state.previewUrl;
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (hasPreview) {
|
|
76
|
+
setActiveView("preview");
|
|
77
|
+
}
|
|
78
|
+
}, [hasPreview]);
|
|
79
|
+
|
|
80
|
+
// Switch to code view when a file is selected from the tree
|
|
81
|
+
const onFileSelect = useCallback(
|
|
82
|
+
(path: string) => {
|
|
83
|
+
handleSelectFile(path);
|
|
84
|
+
setActiveView("code");
|
|
85
|
+
},
|
|
86
|
+
[handleSelectFile],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
className={`sb-root relative w-full bg-sb-bg text-sb-text overflow-hidden flex flex-col ${className}`}
|
|
92
|
+
style={{ height }}
|
|
93
|
+
>
|
|
94
|
+
{/* Boot overlay */}
|
|
95
|
+
{isBooting && <BootOverlay progress={state.progress} />}
|
|
96
|
+
|
|
97
|
+
{/* Toolbar — compact row with view toggle + URL bar */}
|
|
98
|
+
<Toolbar
|
|
99
|
+
activeView={activeView}
|
|
100
|
+
onViewChange={setActiveView}
|
|
101
|
+
previewUrl={state.previewUrl}
|
|
102
|
+
port={port}
|
|
103
|
+
onRefresh={restart}
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
{/* Views — full width, toggled */}
|
|
107
|
+
<div className="flex-1 relative overflow-hidden min-h-0">
|
|
108
|
+
{/* Code view */}
|
|
109
|
+
<motion.div
|
|
110
|
+
className="absolute inset-0"
|
|
111
|
+
initial={false}
|
|
112
|
+
animate={{
|
|
113
|
+
opacity: activeView === "code" ? 1 : 0,
|
|
114
|
+
pointerEvents: activeView === "code" ? "auto" : "none",
|
|
115
|
+
}}
|
|
116
|
+
transition={{ duration: 0.15 }}
|
|
117
|
+
>
|
|
118
|
+
<CodeView
|
|
119
|
+
fileTree={fileTree}
|
|
120
|
+
files={state.files}
|
|
121
|
+
selectedFile={selectedFile}
|
|
122
|
+
openFiles={openFiles}
|
|
123
|
+
terminalOutput={state.terminalOutput}
|
|
124
|
+
onSelectFile={onFileSelect}
|
|
125
|
+
onCloseFile={handleCloseFile}
|
|
126
|
+
onFileChange={handleFileChange}
|
|
127
|
+
/>
|
|
128
|
+
</motion.div>
|
|
129
|
+
|
|
130
|
+
{/* Preview view */}
|
|
131
|
+
<motion.div
|
|
132
|
+
className="absolute inset-0"
|
|
133
|
+
initial={false}
|
|
134
|
+
animate={{
|
|
135
|
+
opacity: activeView === "preview" ? 1 : 0,
|
|
136
|
+
pointerEvents: activeView === "preview" ? "auto" : "none",
|
|
137
|
+
}}
|
|
138
|
+
transition={{ duration: 0.15 }}
|
|
139
|
+
>
|
|
140
|
+
<Preview url={state.previewUrl} />
|
|
141
|
+
</motion.div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Toolbar
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
interface ToolbarProps {
|
|
152
|
+
activeView: WorkbenchView;
|
|
153
|
+
onViewChange: (view: WorkbenchView) => void;
|
|
154
|
+
previewUrl: string | null;
|
|
155
|
+
port: number;
|
|
156
|
+
onRefresh?: () => void;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Formats the internal preview URL into a friendly display URL.
|
|
161
|
+
*
|
|
162
|
+
* The actual preview URL is something like `https://host/__preview__/3000/path`
|
|
163
|
+
* (a Service Worker proxy URL). This is meaningless to the user — the app runs
|
|
164
|
+
* entirely in their browser. We show `localhost:{port}/path` instead, which
|
|
165
|
+
* matches what they'd see from a real local dev server.
|
|
166
|
+
*/
|
|
167
|
+
function formatDisplayUrl(previewUrl: string | null, port: number): string {
|
|
168
|
+
if (!previewUrl) return "";
|
|
169
|
+
|
|
170
|
+
// Extract the path portion after /__preview__/PORT or /__virtual__/PORT
|
|
171
|
+
const match = previewUrl.match(/\/__(?:preview|virtual)__\/\d+(\/.*)?$/);
|
|
172
|
+
const path = match?.[1] ?? "/";
|
|
173
|
+
return `localhost:${port}${path}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Toolbar — single compact row with view toggle + URL bar.
|
|
178
|
+
* URL bar only shows in preview mode. In code mode the space is
|
|
179
|
+
* used for a subtle status label.
|
|
180
|
+
*/
|
|
181
|
+
function Toolbar({
|
|
182
|
+
activeView,
|
|
183
|
+
onViewChange,
|
|
184
|
+
previewUrl,
|
|
185
|
+
port,
|
|
186
|
+
onRefresh,
|
|
187
|
+
}: ToolbarProps) {
|
|
188
|
+
const displayUrl = formatDisplayUrl(previewUrl, port);
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div className="flex items-center gap-2 px-2 py-1 bg-sb-bg-alt border-b border-sb-border min-h-[36px] shrink-0">
|
|
192
|
+
{/* View toggle — always visible */}
|
|
193
|
+
<ViewSlider selected={activeView} onSelect={onViewChange} />
|
|
194
|
+
|
|
195
|
+
{/* Right side: URL bar in preview mode, status in code mode */}
|
|
196
|
+
<div className="flex-1 min-w-0">
|
|
197
|
+
{activeView === "preview" ? (
|
|
198
|
+
<div className="flex items-center gap-1.5">
|
|
199
|
+
<button
|
|
200
|
+
className="shrink-0 p-1 rounded text-sb-text-muted hover:text-sb-text-active hover:bg-sb-bg-hover transition-colors text-xs"
|
|
201
|
+
onClick={onRefresh}
|
|
202
|
+
title="Refresh preview"
|
|
203
|
+
>
|
|
204
|
+
<RefreshIcon />
|
|
205
|
+
</button>
|
|
206
|
+
<div className="flex-1 min-w-0 bg-sb-bg rounded px-2 py-0.5 text-xs text-sb-text-muted font-mono truncate">
|
|
207
|
+
{previewUrl ? displayUrl : "Waiting for server..."}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
) : (
|
|
211
|
+
<div className="text-xs text-sb-text-muted px-1 truncate">Editor</div>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Small inline refresh icon (SVG) */
|
|
219
|
+
function RefreshIcon() {
|
|
220
|
+
return (
|
|
221
|
+
<svg
|
|
222
|
+
width="14"
|
|
223
|
+
height="14"
|
|
224
|
+
viewBox="0 0 24 24"
|
|
225
|
+
fill="none"
|
|
226
|
+
stroke="currentColor"
|
|
227
|
+
strokeWidth="2"
|
|
228
|
+
strokeLinecap="round"
|
|
229
|
+
strokeLinejoin="round"
|
|
230
|
+
>
|
|
231
|
+
<polyline points="23 4 23 10 17 10" />
|
|
232
|
+
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
|
|
233
|
+
</svg>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// CodeView — full-width code editing layout
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
interface CodeViewProps {
|
|
242
|
+
fileTree: ReturnType<typeof buildFileTree>;
|
|
243
|
+
files: Record<string, string>;
|
|
244
|
+
selectedFile: string | null;
|
|
245
|
+
openFiles: string[];
|
|
246
|
+
terminalOutput: string[];
|
|
247
|
+
onSelectFile: (path: string) => void;
|
|
248
|
+
onCloseFile: (path: string) => void;
|
|
249
|
+
onFileChange: (path: string, content: string) => void;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* CodeView — FileTree (left) + Editor (top-right) + Terminal (bottom-right).
|
|
254
|
+
* Uses Allotment for resizable split panes.
|
|
255
|
+
* Responsive: file tree shrinks on narrow containers.
|
|
256
|
+
*/
|
|
257
|
+
function CodeView({
|
|
258
|
+
fileTree,
|
|
259
|
+
files,
|
|
260
|
+
selectedFile,
|
|
261
|
+
openFiles,
|
|
262
|
+
terminalOutput,
|
|
263
|
+
onSelectFile,
|
|
264
|
+
onCloseFile,
|
|
265
|
+
onFileChange,
|
|
266
|
+
}: CodeViewProps) {
|
|
267
|
+
return (
|
|
268
|
+
<div className="h-full w-full">
|
|
269
|
+
<Allotment>
|
|
270
|
+
{/* File tree — left */}
|
|
271
|
+
<Allotment.Pane preferredSize={200} minSize={120} maxSize={350}>
|
|
272
|
+
<FileTree
|
|
273
|
+
files={fileTree}
|
|
274
|
+
selectedFile={selectedFile}
|
|
275
|
+
onSelectFile={onSelectFile}
|
|
276
|
+
/>
|
|
277
|
+
</Allotment.Pane>
|
|
278
|
+
|
|
279
|
+
{/* Editor + Terminal — right */}
|
|
280
|
+
<Allotment.Pane>
|
|
281
|
+
<Allotment vertical>
|
|
282
|
+
<Allotment.Pane preferredSize="70%">
|
|
283
|
+
<CodeEditor
|
|
284
|
+
files={files}
|
|
285
|
+
activeFile={selectedFile}
|
|
286
|
+
openFiles={openFiles}
|
|
287
|
+
onSelectFile={onSelectFile}
|
|
288
|
+
onCloseFile={onCloseFile}
|
|
289
|
+
onFileChange={onFileChange}
|
|
290
|
+
/>
|
|
291
|
+
</Allotment.Pane>
|
|
292
|
+
|
|
293
|
+
<Allotment.Pane preferredSize="30%" minSize={60}>
|
|
294
|
+
<Terminal output={terminalOutput} />
|
|
295
|
+
</Allotment.Pane>
|
|
296
|
+
</Allotment>
|
|
297
|
+
</Allotment.Pane>
|
|
298
|
+
</Allotment>
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useRuntime — React hook wrapping NodepodRuntime lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Manages the Nodepod runtime instance and exposes reactive state
|
|
5
|
+
* for all UI components (progress, files, terminal output, preview URL).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
|
+
import { NodepodRuntime } from "../services/runtime";
|
|
10
|
+
import { getTemplate } from "../templates";
|
|
11
|
+
import type {
|
|
12
|
+
BootProgress,
|
|
13
|
+
CodeSandboxProps,
|
|
14
|
+
FileMap,
|
|
15
|
+
RuntimeState,
|
|
16
|
+
} from "../types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hook that manages the full Nodepod runtime lifecycle.
|
|
20
|
+
*
|
|
21
|
+
* @param props - CodeSandbox component props
|
|
22
|
+
* @returns Reactive runtime state + control functions
|
|
23
|
+
*/
|
|
24
|
+
export function useRuntime(props: CodeSandboxProps) {
|
|
25
|
+
const {
|
|
26
|
+
files: propFiles,
|
|
27
|
+
template,
|
|
28
|
+
entryCommand: propEntryCommand,
|
|
29
|
+
port: propPort,
|
|
30
|
+
env,
|
|
31
|
+
onFileChange,
|
|
32
|
+
onServerReady,
|
|
33
|
+
onProgress,
|
|
34
|
+
onError,
|
|
35
|
+
} = props;
|
|
36
|
+
|
|
37
|
+
const runtimeRef = useRef<NodepodRuntime | null>(null);
|
|
38
|
+
const bootedRef = useRef(false);
|
|
39
|
+
|
|
40
|
+
const [state, setState] = useState<RuntimeState>({
|
|
41
|
+
status: "initializing",
|
|
42
|
+
progress: { stage: "initializing", message: "Preparing...", percent: 0 },
|
|
43
|
+
previewUrl: null,
|
|
44
|
+
terminalOutput: [],
|
|
45
|
+
files: {},
|
|
46
|
+
error: null,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
|
50
|
+
const [openFiles, setOpenFiles] = useState<string[]>([]);
|
|
51
|
+
|
|
52
|
+
// Resolve files + entry command from props or template
|
|
53
|
+
const resolvedConfig = useCallback(() => {
|
|
54
|
+
if (propFiles) {
|
|
55
|
+
return {
|
|
56
|
+
files: propFiles,
|
|
57
|
+
entryCommand: propEntryCommand || "node server.js",
|
|
58
|
+
port: propPort || 3000,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (template) {
|
|
63
|
+
const tpl = getTemplate(template);
|
|
64
|
+
if (tpl) {
|
|
65
|
+
return {
|
|
66
|
+
files: tpl.files,
|
|
67
|
+
entryCommand: propEntryCommand || tpl.entryCommand,
|
|
68
|
+
port: propPort || tpl.port,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Default: empty project
|
|
74
|
+
return {
|
|
75
|
+
files: {},
|
|
76
|
+
entryCommand: propEntryCommand || "node server.js",
|
|
77
|
+
port: propPort || 3000,
|
|
78
|
+
};
|
|
79
|
+
}, [propFiles, template, propEntryCommand, propPort]);
|
|
80
|
+
|
|
81
|
+
// Boot the runtime on mount
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (bootedRef.current) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
bootedRef.current = true;
|
|
87
|
+
|
|
88
|
+
const config = resolvedConfig();
|
|
89
|
+
if (Object.keys(config.files).length === 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const runtime = new NodepodRuntime({
|
|
94
|
+
files: config.files,
|
|
95
|
+
entryCommand: config.entryCommand,
|
|
96
|
+
port: config.port,
|
|
97
|
+
env,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
runtimeRef.current = runtime;
|
|
101
|
+
|
|
102
|
+
// Set initial files state + open the first file
|
|
103
|
+
setState((prev) => ({
|
|
104
|
+
...prev,
|
|
105
|
+
files: config.files,
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
const fileNames = Object.keys(config.files);
|
|
109
|
+
// Auto-select entry file or first file
|
|
110
|
+
const entryFile =
|
|
111
|
+
fileNames.find((f) => f === "server.js") ||
|
|
112
|
+
fileNames.find((f) => f === "index.js") ||
|
|
113
|
+
fileNames.find((f) => f.endsWith(".js")) ||
|
|
114
|
+
fileNames[0];
|
|
115
|
+
|
|
116
|
+
if (entryFile) {
|
|
117
|
+
setSelectedFile(entryFile);
|
|
118
|
+
setOpenFiles([entryFile]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Wire callbacks
|
|
122
|
+
runtime.setProgressCallback((progress: BootProgress) => {
|
|
123
|
+
setState((prev) => ({
|
|
124
|
+
...prev,
|
|
125
|
+
status: progress.stage,
|
|
126
|
+
progress,
|
|
127
|
+
previewUrl: runtime.getPreviewUrl(),
|
|
128
|
+
error: progress.stage === "error" ? progress.message : prev.error,
|
|
129
|
+
}));
|
|
130
|
+
onProgress?.(progress);
|
|
131
|
+
if (progress.stage === "error") {
|
|
132
|
+
onError?.(progress.message);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
runtime.setOutputCallback((line: string) => {
|
|
137
|
+
setState((prev) => ({
|
|
138
|
+
...prev,
|
|
139
|
+
terminalOutput: [...prev.terminalOutput, line],
|
|
140
|
+
}));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
runtime.setServerReadyCallback((port: number, url: string) => {
|
|
144
|
+
setState((prev) => ({
|
|
145
|
+
...prev,
|
|
146
|
+
previewUrl: url,
|
|
147
|
+
status: "ready",
|
|
148
|
+
}));
|
|
149
|
+
onServerReady?.(port, url);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Boot
|
|
153
|
+
runtime.boot().catch((err) => {
|
|
154
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
155
|
+
onError?.(msg);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Cleanup on unmount
|
|
159
|
+
return () => {
|
|
160
|
+
runtime.teardown();
|
|
161
|
+
runtimeRef.current = null;
|
|
162
|
+
};
|
|
163
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
164
|
+
|
|
165
|
+
// File change handler
|
|
166
|
+
const handleFileChange = useCallback(
|
|
167
|
+
async (path: string, content: string) => {
|
|
168
|
+
const runtime = runtimeRef.current;
|
|
169
|
+
if (!runtime) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Write to Nodepod FS
|
|
174
|
+
try {
|
|
175
|
+
await runtime.writeFile(path, content);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error(`Failed to write file ${path}:`, err);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Update state
|
|
181
|
+
setState((prev) => ({
|
|
182
|
+
...prev,
|
|
183
|
+
files: { ...prev.files, [path]: content },
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
onFileChange?.(path, content);
|
|
187
|
+
},
|
|
188
|
+
[onFileChange],
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Select a file
|
|
192
|
+
const handleSelectFile = useCallback((path: string) => {
|
|
193
|
+
setSelectedFile(path);
|
|
194
|
+
setOpenFiles((prev) => (prev.includes(path) ? prev : [...prev, path]));
|
|
195
|
+
}, []);
|
|
196
|
+
|
|
197
|
+
// Close a file tab
|
|
198
|
+
const handleCloseFile = useCallback(
|
|
199
|
+
(path: string) => {
|
|
200
|
+
setOpenFiles((prev) => {
|
|
201
|
+
const next = prev.filter((p) => p !== path);
|
|
202
|
+
if (selectedFile === path) {
|
|
203
|
+
setSelectedFile(next.length > 0 ? next[next.length - 1] : null);
|
|
204
|
+
}
|
|
205
|
+
return next;
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
[selectedFile],
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Restart the server
|
|
212
|
+
const restart = useCallback(async () => {
|
|
213
|
+
const runtime = runtimeRef.current;
|
|
214
|
+
if (!runtime) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await runtime.restart();
|
|
218
|
+
}, []);
|
|
219
|
+
|
|
220
|
+
// Get changed files for git operations
|
|
221
|
+
const getChangedFiles = useCallback((): FileMap => {
|
|
222
|
+
return runtimeRef.current?.getChangedFiles() ?? {};
|
|
223
|
+
}, []);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
state,
|
|
227
|
+
selectedFile,
|
|
228
|
+
openFiles,
|
|
229
|
+
handleFileChange,
|
|
230
|
+
handleSelectFile,
|
|
231
|
+
handleCloseFile,
|
|
232
|
+
restart,
|
|
233
|
+
getChangedFiles,
|
|
234
|
+
runtime: runtimeRef,
|
|
235
|
+
};
|
|
236
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @illuma-ai/code-sandbox — Public API
|
|
3
|
+
*
|
|
4
|
+
* This is the main entry point for the library.
|
|
5
|
+
* Consumers import from here.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Main component
|
|
9
|
+
export { CodeSandbox } from "./components/Workbench";
|
|
10
|
+
|
|
11
|
+
// Individual components (for custom layouts)
|
|
12
|
+
export { FileTree, buildFileTree } from "./components/FileTree";
|
|
13
|
+
export { CodeEditor } from "./components/CodeEditor";
|
|
14
|
+
export { Terminal } from "./components/Terminal";
|
|
15
|
+
export { Preview } from "./components/Preview";
|
|
16
|
+
export { BootOverlay } from "./components/BootOverlay";
|
|
17
|
+
export { ViewSlider } from "./components/ViewSlider";
|
|
18
|
+
export type { WorkbenchView } from "./components/ViewSlider";
|
|
19
|
+
|
|
20
|
+
// Services
|
|
21
|
+
export { NodepodRuntime } from "./services/runtime";
|
|
22
|
+
export { GitService } from "./services/git";
|
|
23
|
+
|
|
24
|
+
// Hooks
|
|
25
|
+
export { useRuntime } from "./hooks/useRuntime";
|
|
26
|
+
|
|
27
|
+
// Templates
|
|
28
|
+
export { getTemplate, listTemplates } from "./templates";
|
|
29
|
+
|
|
30
|
+
// Types
|
|
31
|
+
export type {
|
|
32
|
+
FileMap,
|
|
33
|
+
FileNode,
|
|
34
|
+
BootStage,
|
|
35
|
+
BootProgress,
|
|
36
|
+
RuntimeConfig,
|
|
37
|
+
RuntimeState,
|
|
38
|
+
GitHubRepo,
|
|
39
|
+
GitCommitRequest,
|
|
40
|
+
GitPRRequest,
|
|
41
|
+
GitPRResult,
|
|
42
|
+
CodeSandboxProps,
|
|
43
|
+
FileTreeProps,
|
|
44
|
+
CodeEditorProps,
|
|
45
|
+
TerminalProps,
|
|
46
|
+
PreviewProps,
|
|
47
|
+
BootOverlayProps,
|
|
48
|
+
} from "./types";
|