@illuma-ai/code-sandbox 1.1.0 → 1.2.1
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/dist/components/Workbench.d.ts +3 -2
- package/dist/hooks/useRuntime.d.ts +24 -14
- package/dist/index.cjs +81 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +6121 -6364
- package/dist/index.js.map +1 -1
- package/dist/services/runtime.d.ts +1 -1
- package/dist/types.d.ts +104 -41
- package/package.json +1 -1
- package/src/components/Workbench.tsx +133 -87
- package/src/hooks/useRuntime.ts +182 -216
- package/src/index.ts +1 -5
- package/src/services/runtime.ts +1 -1
- package/src/types.ts +110 -42
- package/dist/services/git.d.ts +0 -57
- package/src/services/git.ts +0 -415
package/src/hooks/useRuntime.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Manages the Nodepod runtime instance and exposes reactive state
|
|
5
5
|
* for all UI components (progress, files, terminal output, preview URL).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* The sandbox is a pure renderer — it receives files via props (initial
|
|
8
|
+
* load) and via the imperative handle (updateFiles/updateFile for live
|
|
9
|
+
* updates). It does NOT interact with any storage backend directly.
|
|
10
10
|
*
|
|
11
11
|
* Error exposure: wires the runtime's structured error system and the
|
|
12
12
|
* preview iframe's browser error capture into a unified `errors[]` array
|
|
@@ -16,14 +16,12 @@
|
|
|
16
16
|
|
|
17
17
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
18
18
|
import { NodepodRuntime } from "../services/runtime";
|
|
19
|
-
import { GitService } from "../services/git";
|
|
20
19
|
import { getTemplate } from "../templates";
|
|
21
20
|
import type {
|
|
22
21
|
BootProgress,
|
|
23
22
|
CodeSandboxProps,
|
|
24
23
|
FileChangeStatus,
|
|
25
24
|
FileMap,
|
|
26
|
-
GitHubRepo,
|
|
27
25
|
RuntimeState,
|
|
28
26
|
SandboxError,
|
|
29
27
|
} from "../types";
|
|
@@ -31,9 +29,6 @@ import type {
|
|
|
31
29
|
/** Debug log prefix for easy filtering in DevTools */
|
|
32
30
|
const DBG = "[CodeSandbox:useRuntime]";
|
|
33
31
|
|
|
34
|
-
/** Default polling interval for GitHub branch changes (ms) */
|
|
35
|
-
const DEFAULT_POLL_INTERVAL = 5_000;
|
|
36
|
-
|
|
37
32
|
/**
|
|
38
33
|
* Compute per-file change statuses between an original and current file set.
|
|
39
34
|
*
|
|
@@ -69,27 +64,27 @@ function computeFileChanges(
|
|
|
69
64
|
/**
|
|
70
65
|
* Hook that manages the full Nodepod runtime lifecycle.
|
|
71
66
|
*
|
|
67
|
+
* The sandbox is a pure renderer — it receives files via props or the
|
|
68
|
+
* imperative handle. No polling, no storage backends.
|
|
69
|
+
*
|
|
72
70
|
* @param props - CodeSandbox component props
|
|
73
|
-
* @returns Reactive runtime state + control functions
|
|
71
|
+
* @returns Reactive runtime state + control functions + imperative methods
|
|
74
72
|
*
|
|
75
73
|
* @example
|
|
76
74
|
* ```tsx
|
|
77
|
-
* // Direct files
|
|
78
|
-
* const
|
|
75
|
+
* // Direct files (fetched from any source by the host app)
|
|
76
|
+
* const runtime = useRuntime({ files: myFiles, entryCommand: 'node server.js' });
|
|
77
|
+
*
|
|
78
|
+
* // Later, push updated files from the host:
|
|
79
|
+
* await runtime.updateFiles(newFiles);
|
|
79
80
|
*
|
|
80
|
-
* //
|
|
81
|
-
*
|
|
82
|
-
* github: { owner: 'illuma-ai', repo: 'fullstack-starter', branch: 'main' },
|
|
83
|
-
* gitToken: 'ghp_xxx',
|
|
84
|
-
* onSandboxError: (err) => agent.fix(err),
|
|
85
|
-
* });
|
|
81
|
+
* // Or update a single file:
|
|
82
|
+
* await runtime.updateFile('server.js', newContent);
|
|
86
83
|
* ```
|
|
87
84
|
*/
|
|
88
85
|
export function useRuntime(props: CodeSandboxProps) {
|
|
89
86
|
const {
|
|
90
87
|
files: propFiles,
|
|
91
|
-
github,
|
|
92
|
-
gitToken,
|
|
93
88
|
template,
|
|
94
89
|
entryCommand: propEntryCommand,
|
|
95
90
|
port: propPort,
|
|
@@ -99,6 +94,7 @@ export function useRuntime(props: CodeSandboxProps) {
|
|
|
99
94
|
onProgress,
|
|
100
95
|
onError,
|
|
101
96
|
onSandboxError,
|
|
97
|
+
onFilesUpdated,
|
|
102
98
|
} = props;
|
|
103
99
|
|
|
104
100
|
const runtimeRef = useRef<NodepodRuntime | null>(null);
|
|
@@ -107,10 +103,9 @@ export function useRuntime(props: CodeSandboxProps) {
|
|
|
107
103
|
const onSandboxErrorRef = useRef(onSandboxError);
|
|
108
104
|
onSandboxErrorRef.current = onSandboxError;
|
|
109
105
|
|
|
110
|
-
/**
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
106
|
+
/** Ref for latest onFilesUpdated callback */
|
|
107
|
+
const onFilesUpdatedRef = useRef(onFilesUpdated);
|
|
108
|
+
onFilesUpdatedRef.current = onFilesUpdated;
|
|
114
109
|
|
|
115
110
|
const [state, setState] = useState<RuntimeState>({
|
|
116
111
|
status: "initializing",
|
|
@@ -172,13 +167,24 @@ export function useRuntime(props: CodeSandboxProps) {
|
|
|
172
167
|
|
|
173
168
|
/**
|
|
174
169
|
* Resolve files + entry command from props or template.
|
|
175
|
-
* Returns null if using GitHub (files loaded async).
|
|
176
170
|
*/
|
|
177
171
|
const resolvedConfig = useCallback(() => {
|
|
178
|
-
if (propFiles) {
|
|
172
|
+
if (propFiles && Object.keys(propFiles).length > 0) {
|
|
173
|
+
// Infer entry command from package.json if not provided
|
|
174
|
+
let entryCommand = propEntryCommand || "node server.js";
|
|
175
|
+
if (!propEntryCommand && propFiles["package.json"]) {
|
|
176
|
+
try {
|
|
177
|
+
const pkg = JSON.parse(propFiles["package.json"]);
|
|
178
|
+
if (pkg.scripts?.start) {
|
|
179
|
+
entryCommand = pkg.scripts.start;
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
/* use default */
|
|
183
|
+
}
|
|
184
|
+
}
|
|
179
185
|
return {
|
|
180
186
|
files: propFiles,
|
|
181
|
-
entryCommand
|
|
187
|
+
entryCommand,
|
|
182
188
|
port: propPort || 3000,
|
|
183
189
|
};
|
|
184
190
|
}
|
|
@@ -194,18 +200,9 @@ export function useRuntime(props: CodeSandboxProps) {
|
|
|
194
200
|
}
|
|
195
201
|
}
|
|
196
202
|
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Default: empty project
|
|
203
|
-
return {
|
|
204
|
-
files: {},
|
|
205
|
-
entryCommand: propEntryCommand || "node server.js",
|
|
206
|
-
port: propPort || 3000,
|
|
207
|
-
};
|
|
208
|
-
}, [propFiles, template, github, propEntryCommand, propPort]);
|
|
203
|
+
// No files provided — return null (sandbox waits for updateFiles)
|
|
204
|
+
return null;
|
|
205
|
+
}, [propFiles, template, propEntryCommand, propPort]);
|
|
209
206
|
|
|
210
207
|
// -------------------------------------------------------------------------
|
|
211
208
|
// Boot helpers
|
|
@@ -213,8 +210,6 @@ export function useRuntime(props: CodeSandboxProps) {
|
|
|
213
210
|
|
|
214
211
|
/**
|
|
215
212
|
* Boot the runtime with a resolved file set.
|
|
216
|
-
*
|
|
217
|
-
* Shared between direct-file mode and GitHub mode.
|
|
218
213
|
*/
|
|
219
214
|
const bootWithFiles = useCallback(
|
|
220
215
|
(files: FileMap, entryCommand: string, port: number) => {
|
|
@@ -293,143 +288,145 @@ export function useRuntime(props: CodeSandboxProps) {
|
|
|
293
288
|
);
|
|
294
289
|
|
|
295
290
|
// -------------------------------------------------------------------------
|
|
296
|
-
//
|
|
291
|
+
// Imperative file update methods
|
|
297
292
|
// -------------------------------------------------------------------------
|
|
298
293
|
|
|
299
294
|
/**
|
|
300
|
-
*
|
|
301
|
-
* Used to detect when the agent pushes new commits.
|
|
302
|
-
*/
|
|
303
|
-
const fetchLatestSha = useCallback(
|
|
304
|
-
async (repo: GitHubRepo, token: string): Promise<string | null> => {
|
|
305
|
-
try {
|
|
306
|
-
const { owner, repo: repoName, branch = "main" } = repo;
|
|
307
|
-
const res = await fetch(
|
|
308
|
-
`https://api.github.com/repos/${owner}/${repoName}/git/ref/heads/${branch}`,
|
|
309
|
-
{
|
|
310
|
-
headers: {
|
|
311
|
-
Authorization: `Bearer ${token}`,
|
|
312
|
-
Accept: "application/vnd.github.v3+json",
|
|
313
|
-
},
|
|
314
|
-
},
|
|
315
|
-
);
|
|
316
|
-
if (!res.ok) return null;
|
|
317
|
-
const data = await res.json();
|
|
318
|
-
return data.object?.sha ?? null;
|
|
319
|
-
} catch {
|
|
320
|
-
return null;
|
|
321
|
-
}
|
|
322
|
-
},
|
|
323
|
-
[],
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Update files in the running sandbox when changes are detected.
|
|
295
|
+
* Push a complete new file set into the sandbox.
|
|
328
296
|
*
|
|
329
|
-
*
|
|
330
|
-
* the server
|
|
331
|
-
* doesn't support delete yet — they just become stale).
|
|
297
|
+
* Diffs against current files, writes only changed files to Nodepod FS,
|
|
298
|
+
* and restarts the server if anything changed (unless restartServer=false).
|
|
332
299
|
*
|
|
333
|
-
*
|
|
334
|
-
* viewer can show what changed.
|
|
300
|
+
* The previous file set becomes `originalFiles` for diff tracking.
|
|
335
301
|
*
|
|
336
|
-
*
|
|
302
|
+
* Called by the imperative handle's updateFiles() method.
|
|
303
|
+
* Also usable as the initial boot path when files arrive asynchronously
|
|
304
|
+
* (e.g., host fetches from a remote source, then calls updateFiles).
|
|
337
305
|
*/
|
|
338
|
-
const updateFiles = useCallback(
|
|
339
|
-
|
|
340
|
-
|
|
306
|
+
const updateFiles = useCallback(
|
|
307
|
+
async (newFiles: FileMap, options?: { restartServer?: boolean }) => {
|
|
308
|
+
const shouldRestart = options?.restartServer !== false;
|
|
309
|
+
const runtime = runtimeRef.current;
|
|
341
310
|
|
|
342
|
-
|
|
343
|
-
|
|
311
|
+
// If runtime hasn't booted yet, boot with these files
|
|
312
|
+
if (!runtime) {
|
|
313
|
+
if (bootedRef.current) {
|
|
314
|
+
console.warn(
|
|
315
|
+
DBG,
|
|
316
|
+
"updateFiles called but runtime is gone (already torn down)",
|
|
317
|
+
);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
bootedRef.current = true;
|
|
344
321
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
322
|
+
// Infer entry command from package.json
|
|
323
|
+
let entryCommand = propEntryCommand || "node server.js";
|
|
324
|
+
if (!propEntryCommand && newFiles["package.json"]) {
|
|
325
|
+
try {
|
|
326
|
+
const pkg = JSON.parse(newFiles["package.json"]);
|
|
327
|
+
if (pkg.scripts?.start) {
|
|
328
|
+
entryCommand = pkg.scripts.start;
|
|
329
|
+
}
|
|
330
|
+
} catch {
|
|
331
|
+
/* use default */
|
|
332
|
+
}
|
|
353
333
|
}
|
|
334
|
+
const port = propPort || 3000;
|
|
335
|
+
|
|
336
|
+
bootWithFiles(newFiles, entryCommand, port);
|
|
337
|
+
return;
|
|
354
338
|
}
|
|
355
|
-
}
|
|
356
339
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
340
|
+
// Runtime exists — diff and write changed files
|
|
341
|
+
const currentFiles = runtime.getCurrentFiles();
|
|
342
|
+
let changed = false;
|
|
360
343
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
344
|
+
for (const [path, content] of Object.entries(newFiles)) {
|
|
345
|
+
if (currentFiles[path] !== content) {
|
|
346
|
+
try {
|
|
347
|
+
await runtime.writeFile(path, content);
|
|
348
|
+
changed = true;
|
|
349
|
+
console.log(DBG, `updated file: ${path}`);
|
|
350
|
+
} catch (err) {
|
|
351
|
+
console.warn(DBG, `failed to write ${path}:`, err);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
368
355
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
356
|
+
// Compute change status — "original" is what we had before this update
|
|
357
|
+
const fileChanges = computeFileChanges(currentFiles, newFiles);
|
|
358
|
+
|
|
359
|
+
// Update React state with new file set + diff info
|
|
360
|
+
setState((prev) => ({
|
|
361
|
+
...prev,
|
|
362
|
+
originalFiles: currentFiles,
|
|
363
|
+
files: newFiles,
|
|
364
|
+
fileChanges,
|
|
365
|
+
}));
|
|
366
|
+
|
|
367
|
+
// Restart server if any files changed
|
|
368
|
+
if (changed && shouldRestart) {
|
|
369
|
+
console.log(
|
|
370
|
+
DBG,
|
|
371
|
+
`files changed — restarting server (${Object.keys(fileChanges).length} files differ)`,
|
|
372
|
+
);
|
|
373
|
+
try {
|
|
374
|
+
await runtime.restart();
|
|
375
|
+
} catch (err) {
|
|
376
|
+
console.error(DBG, "restart failed:", err);
|
|
377
|
+
}
|
|
379
378
|
}
|
|
380
|
-
|
|
381
|
-
|
|
379
|
+
|
|
380
|
+
// Notify host that files have been processed
|
|
381
|
+
onFilesUpdatedRef.current?.(fileChanges);
|
|
382
|
+
},
|
|
383
|
+
[propEntryCommand, propPort, bootWithFiles],
|
|
384
|
+
);
|
|
382
385
|
|
|
383
386
|
/**
|
|
384
|
-
*
|
|
387
|
+
* Push a single file update into the sandbox.
|
|
388
|
+
*
|
|
389
|
+
* Writes to Nodepod FS and updates React state. Does NOT restart
|
|
390
|
+
* the server — use restart() if needed, or updateFiles() for bulk
|
|
391
|
+
* updates with auto-restart.
|
|
385
392
|
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
* 2. Diff against current files
|
|
389
|
-
* 3. Write changed files to Nodepod FS
|
|
390
|
-
* 4. Restart the server
|
|
393
|
+
* This is the same path as editor edits, but callable imperatively
|
|
394
|
+
* (e.g., when the agent modifies a single file).
|
|
391
395
|
*/
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
repo: repo.repo,
|
|
402
|
-
branch: repo.branch || "main",
|
|
403
|
-
interval: DEFAULT_POLL_INTERVAL,
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
pollIntervalRef.current = setInterval(async () => {
|
|
407
|
-
const latestSha = await fetchLatestSha(repo, token);
|
|
408
|
-
if (!latestSha) return;
|
|
396
|
+
const updateFile = useCallback(async (path: string, content: string) => {
|
|
397
|
+
const runtime = runtimeRef.current;
|
|
398
|
+
if (!runtime) {
|
|
399
|
+
console.warn(
|
|
400
|
+
DBG,
|
|
401
|
+
"updateFile called but runtime not ready. Use updateFiles() for initial load.",
|
|
402
|
+
);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
409
405
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
lastCommitShaRef.current = latestSha;
|
|
406
|
+
try {
|
|
407
|
+
await runtime.writeFile(path, content);
|
|
408
|
+
} catch (err) {
|
|
409
|
+
console.error(`Failed to write file ${path}:`, err);
|
|
410
|
+
}
|
|
416
411
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
412
|
+
setState((prev) => {
|
|
413
|
+
const newFiles = { ...prev.files, [path]: content };
|
|
414
|
+
const newChanges = { ...prev.fileChanges };
|
|
415
|
+
if (!(path in prev.originalFiles)) {
|
|
416
|
+
newChanges[path] = "new";
|
|
417
|
+
} else if (prev.originalFiles[path] !== content) {
|
|
418
|
+
newChanges[path] = "modified";
|
|
419
|
+
} else {
|
|
420
|
+
// Reverted back to original — remove from changes
|
|
421
|
+
delete newChanges[path];
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
...prev,
|
|
425
|
+
files: newFiles,
|
|
426
|
+
fileChanges: newChanges,
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
}, []);
|
|
433
430
|
|
|
434
431
|
// -------------------------------------------------------------------------
|
|
435
432
|
// Boot on mount
|
|
@@ -437,82 +434,27 @@ export function useRuntime(props: CodeSandboxProps) {
|
|
|
437
434
|
|
|
438
435
|
useEffect(() => {
|
|
439
436
|
if (bootedRef.current) return;
|
|
440
|
-
bootedRef.current = true;
|
|
441
437
|
|
|
442
438
|
const config = resolvedConfig();
|
|
443
439
|
|
|
444
440
|
if (config) {
|
|
445
441
|
// Direct files or template — boot immediately
|
|
442
|
+
bootedRef.current = true;
|
|
446
443
|
if (Object.keys(config.files).length === 0) return;
|
|
447
444
|
bootWithFiles(config.files, config.entryCommand, config.port);
|
|
448
|
-
} else if (github && gitToken) {
|
|
449
|
-
// GitHub mode — fetch files then boot, then start polling
|
|
450
|
-
const entryCommand = propEntryCommand || "node server.js";
|
|
451
|
-
const port = propPort || 3000;
|
|
452
|
-
|
|
453
|
-
setState((prev) => ({
|
|
454
|
-
...prev,
|
|
455
|
-
progress: {
|
|
456
|
-
stage: "initializing",
|
|
457
|
-
message: "Cloning repository from GitHub...",
|
|
458
|
-
percent: 5,
|
|
459
|
-
},
|
|
460
|
-
}));
|
|
461
|
-
|
|
462
|
-
const git = new GitService(gitToken);
|
|
463
|
-
git
|
|
464
|
-
.cloneRepo(github, (percent, message) => {
|
|
465
|
-
setState((prev) => ({
|
|
466
|
-
...prev,
|
|
467
|
-
progress: { stage: "initializing", message, percent },
|
|
468
|
-
}));
|
|
469
|
-
})
|
|
470
|
-
.then((files) => {
|
|
471
|
-
// Infer entry command from package.json if not provided
|
|
472
|
-
let resolvedEntryCommand = entryCommand;
|
|
473
|
-
if (!propEntryCommand && files["package.json"]) {
|
|
474
|
-
try {
|
|
475
|
-
const pkg = JSON.parse(files["package.json"]);
|
|
476
|
-
if (pkg.scripts?.start) {
|
|
477
|
-
// Convert "node server.js" style commands
|
|
478
|
-
resolvedEntryCommand = pkg.scripts.start;
|
|
479
|
-
}
|
|
480
|
-
} catch {
|
|
481
|
-
/* use default */
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const runtime = bootWithFiles(files, resolvedEntryCommand, port);
|
|
486
|
-
|
|
487
|
-
// Start polling for branch changes
|
|
488
|
-
startPolling(github, gitToken);
|
|
489
|
-
})
|
|
490
|
-
.catch((err) => {
|
|
491
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
492
|
-
console.error(DBG, "GitHub clone failed:", err);
|
|
493
|
-
setState((prev) => ({
|
|
494
|
-
...prev,
|
|
495
|
-
status: "error",
|
|
496
|
-
error: msg,
|
|
497
|
-
progress: { stage: "error", message: msg, percent: 0 },
|
|
498
|
-
}));
|
|
499
|
-
onError?.(msg);
|
|
500
|
-
});
|
|
501
445
|
}
|
|
446
|
+
// If no config (no files, no template), sandbox waits for
|
|
447
|
+
// updateFiles() to be called via the imperative handle.
|
|
502
448
|
|
|
503
449
|
// Cleanup on unmount
|
|
504
450
|
return () => {
|
|
505
451
|
runtimeRef.current?.teardown();
|
|
506
452
|
runtimeRef.current = null;
|
|
507
|
-
if (pollIntervalRef.current) {
|
|
508
|
-
clearInterval(pollIntervalRef.current);
|
|
509
|
-
pollIntervalRef.current = null;
|
|
510
|
-
}
|
|
511
453
|
};
|
|
512
454
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
513
455
|
|
|
514
456
|
// -------------------------------------------------------------------------
|
|
515
|
-
// File editing
|
|
457
|
+
// File editing (from the CodeEditor UI)
|
|
516
458
|
// -------------------------------------------------------------------------
|
|
517
459
|
|
|
518
460
|
const handleFileChange = useCallback(
|
|
@@ -585,13 +527,30 @@ export function useRuntime(props: CodeSandboxProps) {
|
|
|
585
527
|
}, []);
|
|
586
528
|
|
|
587
529
|
// -------------------------------------------------------------------------
|
|
588
|
-
//
|
|
530
|
+
// State readers (for imperative handle)
|
|
589
531
|
// -------------------------------------------------------------------------
|
|
590
532
|
|
|
533
|
+
const getFiles = useCallback((): FileMap => {
|
|
534
|
+
return runtimeRef.current?.getCurrentFiles() ?? {};
|
|
535
|
+
}, []);
|
|
536
|
+
|
|
591
537
|
const getChangedFiles = useCallback((): FileMap => {
|
|
592
538
|
return runtimeRef.current?.getChangedFiles() ?? {};
|
|
593
539
|
}, []);
|
|
594
540
|
|
|
541
|
+
const getFileChanges = useCallback((): Record<string, FileChangeStatus> => {
|
|
542
|
+
// Read from React state since it's the source of truth for changes
|
|
543
|
+
return state.fileChanges;
|
|
544
|
+
}, [state.fileChanges]);
|
|
545
|
+
|
|
546
|
+
const getErrors = useCallback((): SandboxError[] => {
|
|
547
|
+
return state.errors;
|
|
548
|
+
}, [state.errors]);
|
|
549
|
+
|
|
550
|
+
const getState = useCallback((): RuntimeState => {
|
|
551
|
+
return state;
|
|
552
|
+
}, [state]);
|
|
553
|
+
|
|
595
554
|
return {
|
|
596
555
|
state,
|
|
597
556
|
selectedFile,
|
|
@@ -601,7 +560,14 @@ export function useRuntime(props: CodeSandboxProps) {
|
|
|
601
560
|
handleCloseFile,
|
|
602
561
|
handleBrowserError,
|
|
603
562
|
restart,
|
|
563
|
+
// Imperative methods (exposed via handle)
|
|
564
|
+
updateFiles,
|
|
565
|
+
updateFile,
|
|
566
|
+
getFiles,
|
|
604
567
|
getChangedFiles,
|
|
568
|
+
getFileChanges,
|
|
569
|
+
getErrors,
|
|
570
|
+
getState,
|
|
605
571
|
runtime: runtimeRef,
|
|
606
572
|
};
|
|
607
573
|
}
|
package/src/index.ts
CHANGED
|
@@ -19,7 +19,6 @@ export type { WorkbenchView } from "./components/ViewSlider";
|
|
|
19
19
|
|
|
20
20
|
// Services
|
|
21
21
|
export { NodepodRuntime } from "./services/runtime";
|
|
22
|
-
export { GitService } from "./services/git";
|
|
23
22
|
|
|
24
23
|
// Hooks
|
|
25
24
|
export { useRuntime } from "./hooks/useRuntime";
|
|
@@ -38,11 +37,8 @@ export type {
|
|
|
38
37
|
BootProgress,
|
|
39
38
|
RuntimeConfig,
|
|
40
39
|
RuntimeState,
|
|
41
|
-
GitHubRepo,
|
|
42
|
-
GitCommitRequest,
|
|
43
|
-
GitPRRequest,
|
|
44
|
-
GitPRResult,
|
|
45
40
|
CodeSandboxProps,
|
|
41
|
+
CodeSandboxHandle,
|
|
46
42
|
FileTreeProps,
|
|
47
43
|
CodeEditorProps,
|
|
48
44
|
TerminalProps,
|
package/src/services/runtime.ts
CHANGED
|
@@ -38,7 +38,7 @@ const DBG = "[CodeSandbox:Runtime]";
|
|
|
38
38
|
* - Write project files into the virtual filesystem
|
|
39
39
|
* - Install npm dependencies from package.json
|
|
40
40
|
* - Start the entry command (e.g., "node server.js")
|
|
41
|
-
* - Track file modifications for
|
|
41
|
+
* - Track file modifications for diffing
|
|
42
42
|
* - Provide preview URL for the iframe
|
|
43
43
|
*/
|
|
44
44
|
export class NodepodRuntime {
|