sandlot 0.1.2 → 0.1.4
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/README.md +138 -408
- package/dist/build-emitter.d.ts +31 -13
- package/dist/build-emitter.d.ts.map +1 -1
- package/dist/builder.d.ts +370 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/bundler.d.ts +6 -2
- package/dist/bundler.d.ts.map +1 -1
- package/dist/commands/compile.d.ts +13 -0
- package/dist/commands/compile.d.ts.map +1 -0
- package/dist/commands/index.d.ts +17 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/packages.d.ts +17 -0
- package/dist/commands/packages.d.ts.map +1 -0
- package/dist/commands/run.d.ts +40 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/types.d.ts +141 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/fs.d.ts +53 -49
- package/dist/fs.d.ts.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +300 -511
- package/dist/internal.js +161 -171
- package/dist/runner.d.ts +314 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/sandbox-manager.d.ts +45 -21
- package/dist/sandbox-manager.d.ts.map +1 -1
- package/dist/sandbox.d.ts +144 -62
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/shared-modules.d.ts +22 -3
- package/dist/shared-modules.d.ts.map +1 -1
- package/dist/shared-resources.d.ts +0 -3
- package/dist/shared-resources.d.ts.map +1 -1
- package/dist/ts-libs.d.ts +7 -20
- package/dist/ts-libs.d.ts.map +1 -1
- package/dist/typechecker.d.ts +1 -1
- package/package.json +5 -5
- package/src/build-emitter.ts +32 -29
- package/src/builder.ts +498 -0
- package/src/bundler.ts +76 -55
- package/src/commands/compile.ts +236 -0
- package/src/commands/index.ts +51 -0
- package/src/commands/packages.ts +154 -0
- package/src/commands/run.ts +245 -0
- package/src/commands/types.ts +172 -0
- package/src/fs.ts +82 -221
- package/src/index.ts +17 -12
- package/src/sandbox.ts +219 -149
- package/src/shared-modules.ts +74 -4
- package/src/shared-resources.ts +0 -3
- package/src/ts-libs.ts +19 -121
- package/src/typechecker.ts +1 -1
- package/dist/react.d.ts +0 -159
- package/dist/react.d.ts.map +0 -1
- package/dist/react.js +0 -149
- package/src/commands.ts +0 -733
- package/src/sandbox-manager.ts +0 -409
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types and utilities for sandbox bash commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { IFileSystem } from "just-bash/browser";
|
|
6
|
+
import type { BundleResult } from "../bundler";
|
|
7
|
+
import type { TypesCache } from "../packages";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The result of a successful build, including the bundle and loaded module.
|
|
11
|
+
*/
|
|
12
|
+
export interface BuildOutput {
|
|
13
|
+
/**
|
|
14
|
+
* The compiled bundle (code, metadata, etc.)
|
|
15
|
+
*/
|
|
16
|
+
bundle: BundleResult;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The loaded module exports.
|
|
20
|
+
* If validation was provided, this is the validated module.
|
|
21
|
+
*/
|
|
22
|
+
module: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validation function type for module validation.
|
|
27
|
+
* Takes the raw module exports and returns validated exports (or throws).
|
|
28
|
+
*/
|
|
29
|
+
export type ValidateFn = (module: Record<string, unknown>) => Record<string, unknown>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Dependencies required by command factories
|
|
33
|
+
*/
|
|
34
|
+
export interface CommandDeps {
|
|
35
|
+
/**
|
|
36
|
+
* The virtual filesystem to operate on
|
|
37
|
+
*/
|
|
38
|
+
fs: IFileSystem;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Pre-loaded TypeScript lib files for type checking
|
|
42
|
+
*/
|
|
43
|
+
libFiles: Map<string, string>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Path to tsconfig.json in the virtual filesystem
|
|
47
|
+
*/
|
|
48
|
+
tsconfigPath: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Callback invoked when a build succeeds (after loading and validation).
|
|
52
|
+
*/
|
|
53
|
+
onBuild?: (result: BuildOutput) => void | Promise<void>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Getter for the current validation function.
|
|
57
|
+
* Called during build to check if validation should be performed.
|
|
58
|
+
*/
|
|
59
|
+
getValidation?: () => ValidateFn | null;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Cache for package type definitions.
|
|
63
|
+
* When provided, avoids redundant network fetches for packages
|
|
64
|
+
* that have already been installed in other sandboxes.
|
|
65
|
+
*/
|
|
66
|
+
typesCache?: TypesCache;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Options for the `run` command
|
|
70
|
+
*/
|
|
71
|
+
runOptions?: RunOptions;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Module IDs that should be resolved from the host's SharedModuleRegistry
|
|
75
|
+
* instead of esm.sh CDN. The host must have registered these modules.
|
|
76
|
+
*
|
|
77
|
+
* Example: ['react', 'react-dom/client']
|
|
78
|
+
*/
|
|
79
|
+
sharedModules?: string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Runtime context passed to the `main()` function when code is executed.
|
|
84
|
+
* This provides sandboxed code with access to sandbox capabilities.
|
|
85
|
+
*/
|
|
86
|
+
export interface RunContext {
|
|
87
|
+
/**
|
|
88
|
+
* The virtual filesystem - read/write files within the sandbox
|
|
89
|
+
*/
|
|
90
|
+
fs: IFileSystem;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Environment variables (configurable per-sandbox)
|
|
94
|
+
*/
|
|
95
|
+
env: Record<string, string>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Command-line arguments passed to `run`
|
|
99
|
+
*/
|
|
100
|
+
args: string[];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Explicit logging function (alternative to console.log)
|
|
104
|
+
*/
|
|
105
|
+
log: (...args: unknown[]) => void;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Explicit error logging function (alternative to console.error)
|
|
109
|
+
*/
|
|
110
|
+
error: (...args: unknown[]) => void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Options for configuring the `run` command behavior
|
|
115
|
+
*/
|
|
116
|
+
export interface RunOptions {
|
|
117
|
+
/**
|
|
118
|
+
* Environment variables available via ctx.env
|
|
119
|
+
*/
|
|
120
|
+
env?: Record<string, string>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Maximum execution time in milliseconds (default: 30000 = 30s)
|
|
124
|
+
* Set to 0 to disable timeout.
|
|
125
|
+
*/
|
|
126
|
+
timeout?: number;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Whether to skip type checking before running (default: false)
|
|
130
|
+
*/
|
|
131
|
+
skipTypecheck?: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Result of running code via the `run` command
|
|
136
|
+
*/
|
|
137
|
+
export interface RunResult {
|
|
138
|
+
/**
|
|
139
|
+
* Captured console output (log, warn, error)
|
|
140
|
+
*/
|
|
141
|
+
logs: string[];
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Return value from main() if present
|
|
145
|
+
*/
|
|
146
|
+
returnValue?: unknown;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Execution time in milliseconds
|
|
150
|
+
*/
|
|
151
|
+
executionTimeMs: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Format esbuild messages (warnings/errors) for display
|
|
156
|
+
*/
|
|
157
|
+
export function formatEsbuildMessages(
|
|
158
|
+
messages: { text: string; location?: { file?: string; line?: number; column?: number } | null }[]
|
|
159
|
+
): string {
|
|
160
|
+
if (messages.length === 0) return "";
|
|
161
|
+
|
|
162
|
+
return messages
|
|
163
|
+
.map((msg) => {
|
|
164
|
+
if (msg.location) {
|
|
165
|
+
const { file, line, column } = msg.location;
|
|
166
|
+
const loc = file ? `${file}${line ? `:${line}` : ""}${column ? `:${column}` : ""}` : "";
|
|
167
|
+
return loc ? `${loc}: ${msg.text}` : msg.text;
|
|
168
|
+
}
|
|
169
|
+
return msg.text;
|
|
170
|
+
})
|
|
171
|
+
.join("\n");
|
|
172
|
+
}
|
package/src/fs.ts
CHANGED
|
@@ -48,85 +48,54 @@ const DEFAULT_SYMLINK_MODE = 0o777;
|
|
|
48
48
|
const DEFAULT_MAX_SIZE_BYTES = 50 * 1024 * 1024; // 50MB default limit
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Options for creating
|
|
51
|
+
* Options for creating a Filesystem instance
|
|
52
52
|
*/
|
|
53
|
-
export interface
|
|
54
|
-
/** Database name in IndexedDB */
|
|
55
|
-
dbName?: string;
|
|
53
|
+
export interface FilesystemOptions {
|
|
56
54
|
/** Maximum total size in bytes (default: 50MB) */
|
|
57
55
|
maxSizeBytes?: number;
|
|
58
|
-
/** Initial files to populate
|
|
56
|
+
/** Initial files to populate */
|
|
59
57
|
initialFiles?: InitialFiles;
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
/**
|
|
63
|
-
* In-memory filesystem
|
|
64
|
-
*
|
|
61
|
+
* In-memory virtual filesystem for sandlot sandboxes.
|
|
62
|
+
*
|
|
63
|
+
* All operations are synchronous in-memory. Use `getFiles()` to export
|
|
64
|
+
* the current state for persistence, and `initialFiles` to restore.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* // Create filesystem
|
|
69
|
+
* const fs = Filesystem.create({ initialFiles: { '/src/index.ts': 'export const x = 1;' } });
|
|
70
|
+
*
|
|
71
|
+
* // Use filesystem
|
|
72
|
+
* await fs.writeFile('/src/app.ts', 'console.log("hello")');
|
|
73
|
+
*
|
|
74
|
+
* // Export for persistence
|
|
75
|
+
* const files = fs.getFiles();
|
|
76
|
+
* localStorage.setItem('my-project', JSON.stringify(files));
|
|
77
|
+
*
|
|
78
|
+
* // Later, restore
|
|
79
|
+
* const saved = JSON.parse(localStorage.getItem('my-project'));
|
|
80
|
+
* const fs2 = Filesystem.create({ initialFiles: saved });
|
|
81
|
+
* ```
|
|
65
82
|
*/
|
|
66
|
-
export class
|
|
83
|
+
export class Filesystem implements IFileSystem {
|
|
67
84
|
private entries: Map<string, FsEntry>;
|
|
68
|
-
private db: IDBDatabase | null = null;
|
|
69
|
-
private dbName: string;
|
|
70
85
|
private maxSizeBytes: number;
|
|
71
|
-
private dirty = false;
|
|
72
86
|
|
|
73
87
|
private constructor(
|
|
74
88
|
entries: Map<string, FsEntry>,
|
|
75
|
-
db: IDBDatabase | null,
|
|
76
|
-
dbName: string,
|
|
77
89
|
maxSizeBytes: number
|
|
78
90
|
) {
|
|
79
91
|
this.entries = entries;
|
|
80
|
-
this.db = db;
|
|
81
|
-
this.dbName = dbName;
|
|
82
92
|
this.maxSizeBytes = maxSizeBytes;
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
/**
|
|
86
|
-
* Create
|
|
96
|
+
* Create a new Filesystem instance
|
|
87
97
|
*/
|
|
88
|
-
static
|
|
89
|
-
const dbName = options.dbName ?? "sandlot-fs";
|
|
90
|
-
const maxSizeBytes = options.maxSizeBytes ?? DEFAULT_MAX_SIZE_BYTES;
|
|
91
|
-
|
|
92
|
-
const db = await IndexedDbFs.openDatabase(dbName);
|
|
93
|
-
const entries = await IndexedDbFs.loadEntries(db);
|
|
94
|
-
|
|
95
|
-
// If empty and initialFiles provided, populate
|
|
96
|
-
if (entries.size === 0) {
|
|
97
|
-
// Always ensure root exists
|
|
98
|
-
entries.set("/", {
|
|
99
|
-
type: "directory",
|
|
100
|
-
mode: DEFAULT_DIR_MODE,
|
|
101
|
-
mtime: new Date(),
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
if (options.initialFiles) {
|
|
105
|
-
for (const [path, value] of Object.entries(options.initialFiles)) {
|
|
106
|
-
const normalizedPath = IndexedDbFs.normalizePath(path);
|
|
107
|
-
const init = IndexedDbFs.parseFileInit(value);
|
|
108
|
-
|
|
109
|
-
// Ensure parent directories exist
|
|
110
|
-
IndexedDbFs.ensureParentDirs(entries, normalizedPath);
|
|
111
|
-
|
|
112
|
-
entries.set(normalizedPath, {
|
|
113
|
-
type: "file",
|
|
114
|
-
content: init.content,
|
|
115
|
-
mode: init.mode ?? DEFAULT_FILE_MODE,
|
|
116
|
-
mtime: init.mtime ?? new Date(),
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const fs = new IndexedDbFs(entries, db, dbName, maxSizeBytes);
|
|
123
|
-
return fs;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Create an in-memory only instance (no IndexedDB)
|
|
128
|
-
*/
|
|
129
|
-
static createInMemory(options: Omit<IndexedDbFsOptions, "dbName"> = {}): IndexedDbFs {
|
|
98
|
+
static create(options: FilesystemOptions = {}): Filesystem {
|
|
130
99
|
const maxSizeBytes = options.maxSizeBytes ?? DEFAULT_MAX_SIZE_BYTES;
|
|
131
100
|
const entries = new Map<string, FsEntry>();
|
|
132
101
|
|
|
@@ -139,11 +108,11 @@ export class IndexedDbFs implements IFileSystem {
|
|
|
139
108
|
|
|
140
109
|
if (options.initialFiles) {
|
|
141
110
|
for (const [path, value] of Object.entries(options.initialFiles)) {
|
|
142
|
-
const normalizedPath =
|
|
143
|
-
const init =
|
|
111
|
+
const normalizedPath = Filesystem.normalizePath(path);
|
|
112
|
+
const init = Filesystem.parseFileInit(value);
|
|
144
113
|
|
|
145
114
|
// Ensure parent directories exist
|
|
146
|
-
|
|
115
|
+
Filesystem.ensureParentDirs(entries, normalizedPath);
|
|
147
116
|
|
|
148
117
|
entries.set(normalizedPath, {
|
|
149
118
|
type: "file",
|
|
@@ -154,52 +123,50 @@ export class IndexedDbFs implements IFileSystem {
|
|
|
154
123
|
}
|
|
155
124
|
}
|
|
156
125
|
|
|
157
|
-
return new
|
|
126
|
+
return new Filesystem(entries, maxSizeBytes);
|
|
158
127
|
}
|
|
159
128
|
|
|
160
|
-
// ============
|
|
129
|
+
// ============ State Export ============
|
|
161
130
|
|
|
162
131
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
132
|
+
* Get all files as a serializable object.
|
|
133
|
+
*
|
|
134
|
+
* Returns a Record<string, string> that can be JSON-serialized and
|
|
135
|
+
* used as `initialFiles` when creating a new filesystem.
|
|
136
|
+
*
|
|
137
|
+
* Note: Only includes files, not directories (directories are
|
|
138
|
+
* automatically created from file paths). Binary files are
|
|
139
|
+
* base64-encoded with a `data:` prefix.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* const files = fs.getFiles();
|
|
144
|
+
* // { '/src/index.ts': 'export const x = 1;', '/package.json': '{"name":"app"}' }
|
|
145
|
+
*
|
|
146
|
+
* // Persist however you want
|
|
147
|
+
* localStorage.setItem('project', JSON.stringify(files));
|
|
148
|
+
*
|
|
149
|
+
* // Restore later
|
|
150
|
+
* const saved = JSON.parse(localStorage.getItem('project'));
|
|
151
|
+
* const fs2 = Filesystem.create({ initialFiles: saved });
|
|
152
|
+
* ```
|
|
165
153
|
*/
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const tx = this.db.transaction("entries", "readwrite");
|
|
172
|
-
const store = tx.objectStore("entries");
|
|
173
|
-
|
|
174
|
-
// Clear and rewrite all entries
|
|
175
|
-
await this.promisifyRequest(store.clear());
|
|
176
|
-
|
|
154
|
+
getFiles(): Record<string, string> {
|
|
155
|
+
const files: Record<string, string> = {};
|
|
156
|
+
|
|
177
157
|
for (const [path, entry] of this.entries) {
|
|
178
|
-
|
|
158
|
+
if (entry.type === "file") {
|
|
159
|
+
if (typeof entry.content === "string") {
|
|
160
|
+
files[path] = entry.content;
|
|
161
|
+
} else {
|
|
162
|
+
// Binary content - base64 encode with data URI prefix
|
|
163
|
+
const base64 = this.encodeBase64(entry.content);
|
|
164
|
+
files[path] = `data:application/octet-stream;base64,${base64}`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
179
167
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
this.dirty = false;
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Reload entries from IndexedDB, discarding unsaved changes
|
|
188
|
-
*/
|
|
189
|
-
async reload(): Promise<void> {
|
|
190
|
-
if (!this.db) {
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
this.entries = await IndexedDbFs.loadEntries(this.db);
|
|
195
|
-
this.dirty = false;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Check if there are unsaved changes
|
|
200
|
-
*/
|
|
201
|
-
isDirty(): boolean {
|
|
202
|
-
return this.dirty;
|
|
168
|
+
|
|
169
|
+
return files;
|
|
203
170
|
}
|
|
204
171
|
|
|
205
172
|
/**
|
|
@@ -220,16 +187,6 @@ export class IndexedDbFs implements IFileSystem {
|
|
|
220
187
|
return size;
|
|
221
188
|
}
|
|
222
189
|
|
|
223
|
-
/**
|
|
224
|
-
* Close the database connection
|
|
225
|
-
*/
|
|
226
|
-
close(): void {
|
|
227
|
-
if (this.db) {
|
|
228
|
-
this.db.close();
|
|
229
|
-
this.db = null;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
190
|
// ============ IFileSystem Implementation ============
|
|
234
191
|
|
|
235
192
|
async readFile(
|
|
@@ -296,7 +253,6 @@ export class IndexedDbFs implements IFileSystem {
|
|
|
296
253
|
mode: existing?.mode ?? DEFAULT_FILE_MODE,
|
|
297
254
|
mtime: new Date(),
|
|
298
255
|
});
|
|
299
|
-
this.dirty = true;
|
|
300
256
|
}
|
|
301
257
|
|
|
302
258
|
async appendFile(
|
|
@@ -375,7 +331,6 @@ export class IndexedDbFs implements IFileSystem {
|
|
|
375
331
|
mode: DEFAULT_DIR_MODE,
|
|
376
332
|
mtime: new Date(),
|
|
377
333
|
});
|
|
378
|
-
this.dirty = true;
|
|
379
334
|
}
|
|
380
335
|
|
|
381
336
|
async readdir(path: string): Promise<string[]> {
|
|
@@ -464,7 +419,6 @@ export class IndexedDbFs implements IFileSystem {
|
|
|
464
419
|
}
|
|
465
420
|
|
|
466
421
|
this.entries.delete(normalizedPath);
|
|
467
|
-
this.dirty = true;
|
|
468
422
|
}
|
|
469
423
|
|
|
470
424
|
async cp(src: string, dest: string, options?: CpOptions): Promise<void> {
|
|
@@ -497,8 +451,6 @@ export class IndexedDbFs implements IFileSystem {
|
|
|
497
451
|
this.ensureParentDirs(destPath);
|
|
498
452
|
this.entries.set(destPath, this.cloneEntry(entry));
|
|
499
453
|
}
|
|
500
|
-
|
|
501
|
-
this.dirty = true;
|
|
502
454
|
}
|
|
503
455
|
|
|
504
456
|
async mv(src: string, dest: string): Promise<void> {
|
|
@@ -532,8 +484,6 @@ export class IndexedDbFs implements IFileSystem {
|
|
|
532
484
|
this.entries.delete(srcPath);
|
|
533
485
|
this.entries.set(destPath, entry);
|
|
534
486
|
}
|
|
535
|
-
|
|
536
|
-
this.dirty = true;
|
|
537
487
|
}
|
|
538
488
|
|
|
539
489
|
resolvePath(base: string, path: string): string {
|
|
@@ -571,7 +521,6 @@ export class IndexedDbFs implements IFileSystem {
|
|
|
571
521
|
|
|
572
522
|
entry.mode = mode;
|
|
573
523
|
entry.mtime = new Date();
|
|
574
|
-
this.dirty = true;
|
|
575
524
|
}
|
|
576
525
|
|
|
577
526
|
async symlink(target: string, linkPath: string): Promise<void> {
|
|
@@ -589,7 +538,6 @@ export class IndexedDbFs implements IFileSystem {
|
|
|
589
538
|
mode: DEFAULT_SYMLINK_MODE,
|
|
590
539
|
mtime: new Date(),
|
|
591
540
|
});
|
|
592
|
-
this.dirty = true;
|
|
593
541
|
}
|
|
594
542
|
|
|
595
543
|
async link(existingPath: string, newPath: string): Promise<void> {
|
|
@@ -616,7 +564,6 @@ export class IndexedDbFs implements IFileSystem {
|
|
|
616
564
|
mode: entry.mode,
|
|
617
565
|
mtime: new Date(),
|
|
618
566
|
});
|
|
619
|
-
this.dirty = true;
|
|
620
567
|
}
|
|
621
568
|
|
|
622
569
|
async readlink(path: string): Promise<string> {
|
|
@@ -680,13 +627,12 @@ export class IndexedDbFs implements IFileSystem {
|
|
|
680
627
|
|
|
681
628
|
// Update mtime (atime is ignored as per interface docs, kept for API compatibility)
|
|
682
629
|
entry.mtime = mtime;
|
|
683
|
-
this.dirty = true;
|
|
684
630
|
}
|
|
685
631
|
|
|
686
632
|
// ============ Private Helpers ============
|
|
687
633
|
|
|
688
634
|
private normalizePath(path: string): string {
|
|
689
|
-
return
|
|
635
|
+
return Filesystem.normalizePath(path);
|
|
690
636
|
}
|
|
691
637
|
|
|
692
638
|
private static normalizePath(path: string): string {
|
|
@@ -718,8 +664,7 @@ export class IndexedDbFs implements IFileSystem {
|
|
|
718
664
|
}
|
|
719
665
|
|
|
720
666
|
private ensureParentDirs(path: string): void {
|
|
721
|
-
|
|
722
|
-
this.dirty = true;
|
|
667
|
+
Filesystem.ensureParentDirs(this.entries, path);
|
|
723
668
|
}
|
|
724
669
|
|
|
725
670
|
private static ensureParentDirs(entries: Map<string, FsEntry>, path: string): void {
|
|
@@ -818,11 +763,7 @@ export class IndexedDbFs implements IFileSystem {
|
|
|
818
763
|
return new TextDecoder("utf-8").decode(buffer);
|
|
819
764
|
}
|
|
820
765
|
if (encoding === "base64") {
|
|
821
|
-
|
|
822
|
-
for (let i = 0; i < buffer.byteLength; i++) {
|
|
823
|
-
binary += String.fromCharCode(buffer[i]!);
|
|
824
|
-
}
|
|
825
|
-
return btoa(binary);
|
|
766
|
+
return this.encodeBase64(buffer);
|
|
826
767
|
}
|
|
827
768
|
if (encoding === "hex") {
|
|
828
769
|
return Array.from(buffer)
|
|
@@ -833,6 +774,14 @@ export class IndexedDbFs implements IFileSystem {
|
|
|
833
774
|
return new TextDecoder("utf-8").decode(buffer);
|
|
834
775
|
}
|
|
835
776
|
|
|
777
|
+
private encodeBase64(buffer: Uint8Array): string {
|
|
778
|
+
let binary = "";
|
|
779
|
+
for (let i = 0; i < buffer.byteLength; i++) {
|
|
780
|
+
binary += String.fromCharCode(buffer[i]!);
|
|
781
|
+
}
|
|
782
|
+
return btoa(binary);
|
|
783
|
+
}
|
|
784
|
+
|
|
836
785
|
private concatBuffers(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
837
786
|
const result = new Uint8Array(a.byteLength + b.byteLength);
|
|
838
787
|
result.set(a, 0);
|
|
@@ -846,103 +795,15 @@ export class IndexedDbFs implements IFileSystem {
|
|
|
846
795
|
}
|
|
847
796
|
return value;
|
|
848
797
|
}
|
|
849
|
-
|
|
850
|
-
// ============ IndexedDB Helpers ============
|
|
851
|
-
|
|
852
|
-
private static openDatabase(dbName: string): Promise<IDBDatabase> {
|
|
853
|
-
return new Promise((resolve, reject) => {
|
|
854
|
-
const request = indexedDB.open(dbName, 1);
|
|
855
|
-
|
|
856
|
-
request.onerror = () => reject(request.error);
|
|
857
|
-
request.onsuccess = () => resolve(request.result);
|
|
858
|
-
|
|
859
|
-
request.onupgradeneeded = (event) => {
|
|
860
|
-
const db = (event.target as IDBOpenDBRequest).result;
|
|
861
|
-
if (!db.objectStoreNames.contains("entries")) {
|
|
862
|
-
db.createObjectStore("entries", { keyPath: "path" });
|
|
863
|
-
}
|
|
864
|
-
};
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
private static async loadEntries(db: IDBDatabase): Promise<Map<string, FsEntry>> {
|
|
869
|
-
const tx = db.transaction("entries", "readonly");
|
|
870
|
-
const store = tx.objectStore("entries");
|
|
871
|
-
|
|
872
|
-
return new Promise((resolve, reject) => {
|
|
873
|
-
const request = store.getAll();
|
|
874
|
-
request.onerror = () => reject(request.error);
|
|
875
|
-
request.onsuccess = () => {
|
|
876
|
-
const entries = new Map<string, FsEntry>();
|
|
877
|
-
for (const record of request.result) {
|
|
878
|
-
entries.set(record.path, IndexedDbFs.deserializeEntry(record.entry));
|
|
879
|
-
}
|
|
880
|
-
resolve(entries);
|
|
881
|
-
};
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
private serializeEntry(entry: FsEntry): object {
|
|
886
|
-
if (entry.type === "file" && entry.content instanceof Uint8Array) {
|
|
887
|
-
return {
|
|
888
|
-
...entry,
|
|
889
|
-
content: Array.from(entry.content),
|
|
890
|
-
contentType: "uint8array",
|
|
891
|
-
mtime: entry.mtime.toISOString(),
|
|
892
|
-
};
|
|
893
|
-
}
|
|
894
|
-
return {
|
|
895
|
-
...entry,
|
|
896
|
-
mtime: entry.mtime.toISOString(),
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
private static deserializeEntry(data: any): FsEntry {
|
|
901
|
-
const mtime = new Date(data.mtime);
|
|
902
|
-
|
|
903
|
-
if (data.type === "file") {
|
|
904
|
-
let content = data.content;
|
|
905
|
-
if (data.contentType === "uint8array" && Array.isArray(content)) {
|
|
906
|
-
content = new Uint8Array(content);
|
|
907
|
-
}
|
|
908
|
-
return { type: "file", content, mode: data.mode, mtime };
|
|
909
|
-
}
|
|
910
|
-
if (data.type === "symlink") {
|
|
911
|
-
return { type: "symlink", target: data.target, mode: data.mode, mtime };
|
|
912
|
-
}
|
|
913
|
-
return { type: "directory", mode: data.mode, mtime };
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
private promisifyRequest<T>(request: IDBRequest<T>): Promise<T> {
|
|
917
|
-
return new Promise((resolve, reject) => {
|
|
918
|
-
request.onerror = () => reject(request.error);
|
|
919
|
-
request.onsuccess = () => resolve(request.result);
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
private promisifyTransaction(tx: IDBTransaction): Promise<void> {
|
|
924
|
-
return new Promise((resolve, reject) => {
|
|
925
|
-
tx.onerror = () => reject(tx.error);
|
|
926
|
-
tx.oncomplete = () => resolve();
|
|
927
|
-
});
|
|
928
|
-
}
|
|
929
798
|
}
|
|
930
799
|
|
|
931
800
|
/**
|
|
932
|
-
*
|
|
933
|
-
*
|
|
934
|
-
* Note: For IndexedDB-backed persistence, use `IndexedDbFs.create()` instead.
|
|
935
|
-
* This function exists for compatibility with sync factory patterns.
|
|
801
|
+
* Create an in-memory filesystem.
|
|
936
802
|
*
|
|
937
803
|
* @param initialFiles - Optional initial files to populate the filesystem
|
|
938
|
-
* @returns
|
|
804
|
+
* @returns A new Filesystem instance
|
|
939
805
|
*/
|
|
940
|
-
export function
|
|
941
|
-
return
|
|
806
|
+
export function createFilesystem(options?: FilesystemOptions): Filesystem {
|
|
807
|
+
return Filesystem.create(options);
|
|
942
808
|
}
|
|
943
809
|
|
|
944
|
-
/**
|
|
945
|
-
* @deprecated Use `createInMemoryFs` for in-memory filesystems or
|
|
946
|
-
* `IndexedDbFs.create()` for IndexedDB-backed persistence.
|
|
947
|
-
*/
|
|
948
|
-
export const createIndexedDbFs = createInMemoryFs;
|
package/src/index.ts
CHANGED
|
@@ -25,19 +25,23 @@ if (typeof window !== "undefined" && typeof globalThis.process === "undefined")
|
|
|
25
25
|
|
|
26
26
|
export {
|
|
27
27
|
createSandbox,
|
|
28
|
-
createInMemorySandbox,
|
|
29
28
|
type Sandbox,
|
|
30
29
|
type SandboxOptions,
|
|
30
|
+
type SandboxState,
|
|
31
|
+
type SandboxBashOptions,
|
|
31
32
|
} from "./sandbox";
|
|
32
33
|
|
|
34
|
+
// -----------------------------------------------------------------------------
|
|
35
|
+
// Builder (recommended for agent workflows)
|
|
36
|
+
// -----------------------------------------------------------------------------
|
|
37
|
+
|
|
33
38
|
export {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
type
|
|
37
|
-
type
|
|
38
|
-
type
|
|
39
|
-
|
|
40
|
-
} from "./sandbox-manager";
|
|
39
|
+
createBuilder,
|
|
40
|
+
type BuildResult,
|
|
41
|
+
type CreateBuilderOptions,
|
|
42
|
+
type BuildCallOptions,
|
|
43
|
+
type BuilderFn,
|
|
44
|
+
} from "./builder";
|
|
41
45
|
|
|
42
46
|
// -----------------------------------------------------------------------------
|
|
43
47
|
// Module Loading (use after build to get exports)
|
|
@@ -68,6 +72,7 @@ export {
|
|
|
68
72
|
// -----------------------------------------------------------------------------
|
|
69
73
|
|
|
70
74
|
export type { BundleResult } from "./bundler";
|
|
75
|
+
export type { BuildOutput, ValidateFn } from "./commands/types";
|
|
71
76
|
export type { TypecheckResult, Diagnostic } from "./typechecker";
|
|
72
77
|
export type { PackageManifest, InstallResult } from "./packages";
|
|
73
78
|
|
|
@@ -131,9 +136,9 @@ export {
|
|
|
131
136
|
// -----------------------------------------------------------------------------
|
|
132
137
|
|
|
133
138
|
export {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
type
|
|
139
|
+
Filesystem,
|
|
140
|
+
createFilesystem,
|
|
141
|
+
type FilesystemOptions,
|
|
137
142
|
} from "./fs";
|
|
138
143
|
|
|
139
144
|
export type { IFileSystem, FsEntry } from "just-bash/browser";
|
|
@@ -163,4 +168,4 @@ export {
|
|
|
163
168
|
type RunContext,
|
|
164
169
|
type RunOptions,
|
|
165
170
|
type RunResult,
|
|
166
|
-
} from "./commands";
|
|
171
|
+
} from "./commands/index";
|