@hudhod/core 0.1.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 +21 -0
- package/README.md +165 -0
- package/dist/index.d.ts +1017 -0
- package/dist/index.js +2084 -0
- package/dist/paths-B3gr2EuI.js +282 -0
- package/dist/spawner-CqGITKbd.d.ts +150 -0
- package/dist/webcontainer/index.d.ts +67 -0
- package/dist/webcontainer/index.js +200 -0
- package/package.json +54 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
//#region src/base/disposable.ts
|
|
2
|
+
/**
|
|
3
|
+
* Wraps a callback as a {@link Disposable} that runs at most once.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const sub = toDisposable(() => clearInterval(timer));
|
|
8
|
+
* sub.dispose();
|
|
9
|
+
* sub.dispose(); // no-op
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
function toDisposable(onDispose) {
|
|
13
|
+
let disposed = false;
|
|
14
|
+
return { dispose() {
|
|
15
|
+
if (disposed) return;
|
|
16
|
+
disposed = true;
|
|
17
|
+
onDispose();
|
|
18
|
+
} };
|
|
19
|
+
}
|
|
20
|
+
/** A {@link Disposable} that does nothing. Useful as a default return value. */
|
|
21
|
+
const NO_OP_DISPOSABLE = Object.freeze({ dispose() {} });
|
|
22
|
+
/**
|
|
23
|
+
* Collects disposables and releases them together.
|
|
24
|
+
*
|
|
25
|
+
* Disposal runs in reverse insertion order, so resources are torn down in the
|
|
26
|
+
* opposite order they were set up. A failure in one disposable does not
|
|
27
|
+
* prevent the rest from running; all errors are collected and rethrown
|
|
28
|
+
* together via {@link AggregateError}.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const store = new DisposableStore();
|
|
33
|
+
* store.add(emitter.event(handler));
|
|
34
|
+
* store.add(toDisposable(() => socket.close()));
|
|
35
|
+
* store.dispose();
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
var DisposableStore = class {
|
|
39
|
+
#items = /* @__PURE__ */ new Set();
|
|
40
|
+
#disposed = false;
|
|
41
|
+
/** Whether {@link dispose} has already run. */
|
|
42
|
+
get isDisposed() {
|
|
43
|
+
return this.#disposed;
|
|
44
|
+
}
|
|
45
|
+
/** Number of disposables currently held. */
|
|
46
|
+
get size() {
|
|
47
|
+
return this.#items.size;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Registers a disposable.
|
|
51
|
+
*
|
|
52
|
+
* When the store is already disposed the argument is disposed immediately,
|
|
53
|
+
* which keeps late registrations from leaking.
|
|
54
|
+
*
|
|
55
|
+
* @returns The same disposable, for convenient chaining.
|
|
56
|
+
*/
|
|
57
|
+
add(disposable) {
|
|
58
|
+
if (this.#disposed) {
|
|
59
|
+
disposable.dispose();
|
|
60
|
+
return disposable;
|
|
61
|
+
}
|
|
62
|
+
this.#items.add(disposable);
|
|
63
|
+
return disposable;
|
|
64
|
+
}
|
|
65
|
+
/** Disposes and forgets a single entry. Returns `false` when not held. */
|
|
66
|
+
delete(disposable) {
|
|
67
|
+
if (!this.#items.delete(disposable)) return false;
|
|
68
|
+
disposable.dispose();
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
/** Disposes everything held without marking the store itself as disposed. */
|
|
72
|
+
clear() {
|
|
73
|
+
const errors = disposeAll(this.#items);
|
|
74
|
+
this.#items.clear();
|
|
75
|
+
if (errors.length > 0) throw new AggregateError(errors, "Failed to dispose one or more items");
|
|
76
|
+
}
|
|
77
|
+
/** Disposes everything held and blocks further use. Safe to call repeatedly. */
|
|
78
|
+
dispose() {
|
|
79
|
+
if (this.#disposed) return;
|
|
80
|
+
this.#disposed = true;
|
|
81
|
+
this.clear();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/** Disposes items newest-first, collecting rather than propagating errors. */
|
|
85
|
+
function disposeAll(items) {
|
|
86
|
+
const errors = [];
|
|
87
|
+
const snapshot = Array.from(items);
|
|
88
|
+
for (let index = snapshot.length - 1; index >= 0; index -= 1) {
|
|
89
|
+
const item = snapshot[index];
|
|
90
|
+
if (!item) continue;
|
|
91
|
+
try {
|
|
92
|
+
item.dispose();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
errors.push(error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return errors;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/base/errors.ts
|
|
102
|
+
/**
|
|
103
|
+
* Builds a {@link HudhodError}.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* throw createError("FileNotFound", "No such file: /a.ts", { path: "/a.ts" });
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
function createError(code, message, details = {}) {
|
|
111
|
+
const error = new Error(message, { cause: details.cause });
|
|
112
|
+
error.name = `HudhodError(${code})`;
|
|
113
|
+
error.code = code;
|
|
114
|
+
if (details.path !== void 0) error.path = details.path;
|
|
115
|
+
if (details.partialOutput !== void 0) error.partialOutput = details.partialOutput;
|
|
116
|
+
return error;
|
|
117
|
+
}
|
|
118
|
+
/** The path does not exist. */
|
|
119
|
+
function fileNotFound(path) {
|
|
120
|
+
return createError("FileNotFound", `File not found: ${path}`, { path });
|
|
121
|
+
}
|
|
122
|
+
/** The path exists and overwriting was not permitted. */
|
|
123
|
+
function fileExists(path) {
|
|
124
|
+
return createError("FileExists", `File already exists: ${path}`, { path });
|
|
125
|
+
}
|
|
126
|
+
/** A directory was required but the path is not one. */
|
|
127
|
+
function notADirectory(path) {
|
|
128
|
+
return createError("NotADirectory", `Not a directory: ${path}`, { path });
|
|
129
|
+
}
|
|
130
|
+
/** A file was required but the path is not one. */
|
|
131
|
+
function notAFile(path) {
|
|
132
|
+
return createError("NotAFile", `Not a file: ${path}`, { path });
|
|
133
|
+
}
|
|
134
|
+
/** The directory still has children and `recursive` was not set. */
|
|
135
|
+
function directoryNotEmpty(path) {
|
|
136
|
+
return createError("DirectoryNotEmpty", `Directory not empty: ${path}`, { path });
|
|
137
|
+
}
|
|
138
|
+
/** The path is malformed, relative, or escapes the workspace root. */
|
|
139
|
+
function invalidPath(path, reason) {
|
|
140
|
+
return createError("InvalidPath", `Invalid path "${path}": ${reason}`, { path });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/base/paths.ts
|
|
145
|
+
/** The workspace root. */
|
|
146
|
+
const ROOT = "/";
|
|
147
|
+
/**
|
|
148
|
+
* Normalises a path: collapses duplicate slashes, resolves `.` and `..`, and
|
|
149
|
+
* strips any trailing slash.
|
|
150
|
+
*
|
|
151
|
+
* @throws An `InvalidPath` error when the path is relative, empty, or traverses
|
|
152
|
+
* above the workspace root.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```ts
|
|
156
|
+
* normalizePath("/src//lib/../index.ts"); // "/src/index.ts"
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
function normalizePath(path) {
|
|
160
|
+
if (typeof path !== "string" || path.length === 0) throw invalidPath(String(path), "path must be a non-empty string");
|
|
161
|
+
if (path.includes("\0")) throw invalidPath(path, "path must not contain null bytes");
|
|
162
|
+
if (!path.startsWith("/")) throw invalidPath(path, "path must be absolute");
|
|
163
|
+
const resolved = [];
|
|
164
|
+
for (const segment of path.split("/")) {
|
|
165
|
+
if (segment === "" || segment === ".") continue;
|
|
166
|
+
if (segment === "..") {
|
|
167
|
+
if (resolved.length === 0) throw invalidPath(path, "path escapes the workspace root");
|
|
168
|
+
resolved.pop();
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
resolved.push(segment);
|
|
172
|
+
}
|
|
173
|
+
return resolved.length === 0 ? ROOT : `/${resolved.join("/")}`;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Joins segments onto a base path and normalises the result.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```ts
|
|
180
|
+
* joinPath("/src", "lib", "index.ts"); // "/src/lib/index.ts"
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
function joinPath(base, ...segments) {
|
|
184
|
+
const suffix = segments.filter((segment) => segment.length > 0).join("/");
|
|
185
|
+
return normalizePath(suffix.length === 0 ? base : `${base}/${suffix}`);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Returns the parent directory. The root is its own parent.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```ts
|
|
192
|
+
* dirname("/src/index.ts"); // "/src"
|
|
193
|
+
* dirname("/"); // "/"
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
function dirname(path) {
|
|
197
|
+
const normalized = normalizePath(path);
|
|
198
|
+
if (normalized === ROOT) return ROOT;
|
|
199
|
+
const index = normalized.lastIndexOf("/");
|
|
200
|
+
return index <= 0 ? ROOT : normalized.slice(0, index);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Returns the final segment. The root has an empty basename.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```ts
|
|
207
|
+
* basename("/src/index.ts"); // "index.ts"
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
function basename(path) {
|
|
211
|
+
const normalized = normalizePath(path);
|
|
212
|
+
if (normalized === ROOT) return "";
|
|
213
|
+
return normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Returns the lowercased extension including the leading dot, or an empty
|
|
217
|
+
* string when there is none.
|
|
218
|
+
*
|
|
219
|
+
* A leading dot marks a hidden file rather than an extension, so `.gitignore`
|
|
220
|
+
* has no extension.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```ts
|
|
224
|
+
* extname("/src/App.TSX"); // ".tsx"
|
|
225
|
+
* extname("/.gitignore"); // ""
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
function extname(path) {
|
|
229
|
+
const name = basename(path);
|
|
230
|
+
const index = name.lastIndexOf(".");
|
|
231
|
+
if (index <= 0) return "";
|
|
232
|
+
return name.slice(index).toLowerCase();
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Expresses `path` relative to `from`, without a leading slash.
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* ```ts
|
|
239
|
+
* relativePath("/src", "/src/lib/a.ts"); // "lib/a.ts"
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
function relativePath(from, path) {
|
|
243
|
+
const base = normalizePath(from);
|
|
244
|
+
const target = normalizePath(path);
|
|
245
|
+
if (base === ROOT) return target.slice(1);
|
|
246
|
+
if (target === base) return "";
|
|
247
|
+
if (target.startsWith(`${base}/`)) return target.slice(base.length + 1);
|
|
248
|
+
return target.slice(1);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Whether `path` is `parent` or sits underneath it.
|
|
252
|
+
*
|
|
253
|
+
* Compares whole segments, so `/src` does not contain `/src-old`.
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```ts
|
|
257
|
+
* isSubPath("/src", "/src/a.ts"); // true
|
|
258
|
+
* isSubPath("/src", "/src-old"); // false
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
function isSubPath(parent, path) {
|
|
262
|
+
const base = normalizePath(parent);
|
|
263
|
+
const target = normalizePath(path);
|
|
264
|
+
if (base === target) return true;
|
|
265
|
+
if (base === ROOT) return true;
|
|
266
|
+
return target.startsWith(`${base}/`);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Splits a path into its segments. The root yields an empty array.
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* pathSegments("/src/lib/a.ts"); // ["src", "lib", "a.ts"]
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
function pathSegments(path) {
|
|
277
|
+
const normalized = normalizePath(path);
|
|
278
|
+
return normalized === ROOT ? [] : normalized.slice(1).split("/");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
//#endregion
|
|
282
|
+
export { DisposableStore as _, isSubPath as a, pathSegments as c, directoryNotEmpty as d, fileExists as f, notAFile as g, notADirectory as h, extname as i, relativePath as l, invalidPath as m, basename as n, joinPath as o, fileNotFound as p, dirname as r, normalizePath as s, ROOT as t, createError as u, NO_OP_DISPOSABLE as v, toDisposable as y };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Disposable, FileChangeEvent, FileType } from "@hudhod/sdk";
|
|
2
|
+
|
|
3
|
+
//#region src/fs/provider.d.ts
|
|
4
|
+
|
|
5
|
+
/** Metadata a provider reports for a path. */
|
|
6
|
+
interface ProviderStat {
|
|
7
|
+
/** Whether the entry is a file, directory, or symlink. */
|
|
8
|
+
readonly type: FileType;
|
|
9
|
+
/** Size in bytes. Providers may report `0` for directories. */
|
|
10
|
+
readonly size: number;
|
|
11
|
+
/** Last-modified time, in milliseconds since the Unix epoch. */
|
|
12
|
+
readonly mtime: number;
|
|
13
|
+
}
|
|
14
|
+
/** A child entry reported by {@link FileSystemProvider.readDirectory}. */
|
|
15
|
+
interface ProviderEntry {
|
|
16
|
+
/** Entry name, with no directory component. */
|
|
17
|
+
readonly name: string;
|
|
18
|
+
/** Whether the entry is a file, directory, or symlink. */
|
|
19
|
+
readonly type: FileType;
|
|
20
|
+
}
|
|
21
|
+
/** Options for {@link FileSystemProvider.watch}. */
|
|
22
|
+
interface ProviderWatchOptions {
|
|
23
|
+
/** Whether to watch nested directories. */
|
|
24
|
+
readonly recursive: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A storage backend.
|
|
28
|
+
*
|
|
29
|
+
* ## Implementer contract
|
|
30
|
+
*
|
|
31
|
+
* - Every `path` argument is already normalised: absolute, POSIX-style, no
|
|
32
|
+
* trailing slash, no `.` or `..` segments. Providers must not re-normalise.
|
|
33
|
+
* - Throw the typed errors from `@hudhod/core` — `fileNotFound`,
|
|
34
|
+
* `notADirectory`, and friends — so callers can branch on `error.code`.
|
|
35
|
+
* - Do not create parent directories implicitly; {@link FileSystemService}
|
|
36
|
+
* handles that so the behaviour is identical across backends.
|
|
37
|
+
* - `watch` is best-effort. A provider that cannot watch may return a no-op
|
|
38
|
+
* disposable, at the cost of stale UI when files change outside the app.
|
|
39
|
+
*/
|
|
40
|
+
interface FileSystemProvider {
|
|
41
|
+
/** Human-readable backend name, used in diagnostics. */
|
|
42
|
+
readonly name: string;
|
|
43
|
+
/**
|
|
44
|
+
* Reads a file's raw bytes.
|
|
45
|
+
* @throws `FileNotFound` when the path does not exist.
|
|
46
|
+
* @throws `NotAFile` when the path is a directory.
|
|
47
|
+
*/
|
|
48
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
49
|
+
/**
|
|
50
|
+
* Writes a file's raw bytes, replacing any existing content.
|
|
51
|
+
* @throws `FileNotFound` when the parent directory does not exist.
|
|
52
|
+
*/
|
|
53
|
+
writeFile(path: string, data: Uint8Array): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Creates a directory. Implementations may assume the parent exists.
|
|
56
|
+
* @throws `FileExists` when a *file* already occupies the path.
|
|
57
|
+
*/
|
|
58
|
+
createDirectory(path: string): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Removes a file or directory.
|
|
61
|
+
* @throws `FileNotFound` when the path does not exist.
|
|
62
|
+
* @throws `DirectoryNotEmpty` when removing a non-empty directory without `recursive`.
|
|
63
|
+
*/
|
|
64
|
+
delete(path: string, options: {
|
|
65
|
+
recursive: boolean;
|
|
66
|
+
}): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Moves an entry.
|
|
69
|
+
* @throws `FileNotFound` when the source does not exist.
|
|
70
|
+
* @throws `FileExists` when the destination exists and `overwrite` is false.
|
|
71
|
+
*/
|
|
72
|
+
rename(from: string, to: string, options: {
|
|
73
|
+
overwrite: boolean;
|
|
74
|
+
}): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Reads metadata.
|
|
77
|
+
* @throws `FileNotFound` when the path does not exist.
|
|
78
|
+
*/
|
|
79
|
+
stat(path: string): Promise<ProviderStat>;
|
|
80
|
+
/**
|
|
81
|
+
* Lists a directory's immediate children, in any order.
|
|
82
|
+
* @throws `FileNotFound` when the path does not exist.
|
|
83
|
+
* @throws `NotADirectory` when the path is a file.
|
|
84
|
+
*/
|
|
85
|
+
readDirectory(path: string): Promise<ProviderEntry[]>;
|
|
86
|
+
/**
|
|
87
|
+
* Observes changes beneath a path.
|
|
88
|
+
*
|
|
89
|
+
* Events may be delivered individually or in batches; debouncing is the
|
|
90
|
+
* service's job. Returns a {@link Disposable} that stops the watch.
|
|
91
|
+
*/
|
|
92
|
+
watch(path: string, options: ProviderWatchOptions, listener: (events: readonly FileChangeEvent[]) => void): Disposable;
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/process/spawner.d.ts
|
|
96
|
+
/**
|
|
97
|
+
* The process spawning abstraction.
|
|
98
|
+
*
|
|
99
|
+
* Mirrors the {@link FileSystemProvider} pattern: services depend on this
|
|
100
|
+
* interface rather than on WebContainer directly, so process semantics —
|
|
101
|
+
* timeouts, output caps, lifecycle tracking — can be tested in Node against a
|
|
102
|
+
* fake.
|
|
103
|
+
*
|
|
104
|
+
* @packageDocumentation
|
|
105
|
+
*/
|
|
106
|
+
/** A process that has been started by a {@link ProcessSpawner}. */
|
|
107
|
+
interface SpawnedProcess {
|
|
108
|
+
/** Merged stdout and stderr, as decoded text chunks. */
|
|
109
|
+
readonly output: ReadableStream<string>;
|
|
110
|
+
/** Standard input. */
|
|
111
|
+
readonly input: WritableStream<string>;
|
|
112
|
+
/** Resolves with the exit code when the process finishes. */
|
|
113
|
+
readonly exit: Promise<number>;
|
|
114
|
+
/** Terminates the process. Must be safe to call after exit. */
|
|
115
|
+
kill(): void;
|
|
116
|
+
/** Resizes the pseudo-terminal, if the process has one. */
|
|
117
|
+
resize(dimensions: {
|
|
118
|
+
cols: number;
|
|
119
|
+
rows: number;
|
|
120
|
+
}): void;
|
|
121
|
+
}
|
|
122
|
+
/** Options passed through to the backend. */
|
|
123
|
+
interface SpawnerOptions {
|
|
124
|
+
/** Working directory. */
|
|
125
|
+
readonly cwd?: string;
|
|
126
|
+
/** Environment variables to merge over the backend defaults. */
|
|
127
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
128
|
+
/** Pseudo-terminal dimensions, when one is required. */
|
|
129
|
+
readonly terminal?: {
|
|
130
|
+
readonly cols: number;
|
|
131
|
+
readonly rows: number;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Starts processes.
|
|
136
|
+
*
|
|
137
|
+
* ## Implementer contract
|
|
138
|
+
*
|
|
139
|
+
* - `output` must close when the process exits, otherwise readers hang.
|
|
140
|
+
* - `exit` must resolve exactly once, including when the process is killed.
|
|
141
|
+
* - `kill()` must be idempotent.
|
|
142
|
+
*/
|
|
143
|
+
interface ProcessSpawner {
|
|
144
|
+
/** Human-readable backend name, used in diagnostics. */
|
|
145
|
+
readonly name: string;
|
|
146
|
+
/** Starts a process. */
|
|
147
|
+
spawn(command: string, args: readonly string[], options: SpawnerOptions): Promise<SpawnedProcess>;
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
export { ProviderEntry as a, FileSystemProvider as i, SpawnedProcess as n, ProviderStat as o, SpawnerOptions as r, ProviderWatchOptions as s, ProcessSpawner as t };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { a as ProviderEntry, i as FileSystemProvider, n as SpawnedProcess, o as ProviderStat, r as SpawnerOptions, s as ProviderWatchOptions, t as ProcessSpawner } from "../spawner-CqGITKbd.js";
|
|
2
|
+
import { Disposable, FileChangeEvent } from "@hudhod/sdk";
|
|
3
|
+
import { WebContainer } from "@webcontainer/api";
|
|
4
|
+
|
|
5
|
+
//#region src/webcontainer/file-system-provider.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Adapts WebContainer's file system to the provider contract.
|
|
9
|
+
*
|
|
10
|
+
* ## Known limitations
|
|
11
|
+
*
|
|
12
|
+
* WebContainer exposes no `stat` call, so {@link stat} is emulated by listing
|
|
13
|
+
* the parent directory. Two consequences follow, and both are deliberate:
|
|
14
|
+
*
|
|
15
|
+
* - `mtime` is always `0`. No modification time is available, and inventing one
|
|
16
|
+
* from `Date.now()` would be worse than reporting an obvious sentinel.
|
|
17
|
+
* - `size` requires reading the file. {@link FileSystemService} calls `stat`
|
|
18
|
+
* mainly through `exists()`, which discards the size, so this is acceptable —
|
|
19
|
+
* but avoid calling `stat` in a hot loop.
|
|
20
|
+
*/
|
|
21
|
+
declare class WebContainerFileSystemProvider implements FileSystemProvider {
|
|
22
|
+
#private;
|
|
23
|
+
readonly name = "webcontainer";
|
|
24
|
+
constructor(container: WebContainer);
|
|
25
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
26
|
+
writeFile(path: string, data: Uint8Array): Promise<void>;
|
|
27
|
+
createDirectory(path: string): Promise<void>;
|
|
28
|
+
delete(path: string, options: {
|
|
29
|
+
recursive: boolean;
|
|
30
|
+
}): Promise<void>;
|
|
31
|
+
rename(from: string, to: string, options: {
|
|
32
|
+
overwrite: boolean;
|
|
33
|
+
}): Promise<void>;
|
|
34
|
+
stat(path: string): Promise<ProviderStat>;
|
|
35
|
+
readDirectory(path: string): Promise<ProviderEntry[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Watches a subtree.
|
|
38
|
+
*
|
|
39
|
+
* WebContainer reports a `rename` event for both creation and deletion, so
|
|
40
|
+
* each one is resolved by probing for the path afterwards. The probe is why
|
|
41
|
+
* the listener is async and why events arrive slightly after the change.
|
|
42
|
+
*/
|
|
43
|
+
watch(path: string, options: ProviderWatchOptions, listener: (events: readonly FileChangeEvent[]) => void): Disposable;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/webcontainer/process-spawner.d.ts
|
|
47
|
+
/**
|
|
48
|
+
* Spawns processes inside a WebContainer.
|
|
49
|
+
*
|
|
50
|
+
* The mapping is close to one-to-one: WebContainer already exposes a merged
|
|
51
|
+
* output stream, a writable stdin, and an exit promise. The only adaptation is
|
|
52
|
+
* making `kill()` idempotent, which the provider contract requires.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* const spawner = new WebContainerProcessSpawner(container);
|
|
57
|
+
* const processes = new ProcessService(spawner);
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
declare class WebContainerProcessSpawner implements ProcessSpawner {
|
|
61
|
+
#private;
|
|
62
|
+
readonly name = "webcontainer";
|
|
63
|
+
constructor(container: WebContainer);
|
|
64
|
+
spawn(command: string, args: readonly string[], options: SpawnerOptions): Promise<SpawnedProcess>;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { WebContainerFileSystemProvider, WebContainerProcessSpawner };
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { d as directoryNotEmpty, f as fileExists, h as notADirectory, n as basename, o as joinPath, p as fileNotFound, r as dirname, t as ROOT, y as toDisposable } from "../paths-B3gr2EuI.js";
|
|
2
|
+
|
|
3
|
+
//#region src/webcontainer/file-system-provider.ts
|
|
4
|
+
/**
|
|
5
|
+
* Adapts WebContainer's file system to the provider contract.
|
|
6
|
+
*
|
|
7
|
+
* ## Known limitations
|
|
8
|
+
*
|
|
9
|
+
* WebContainer exposes no `stat` call, so {@link stat} is emulated by listing
|
|
10
|
+
* the parent directory. Two consequences follow, and both are deliberate:
|
|
11
|
+
*
|
|
12
|
+
* - `mtime` is always `0`. No modification time is available, and inventing one
|
|
13
|
+
* from `Date.now()` would be worse than reporting an obvious sentinel.
|
|
14
|
+
* - `size` requires reading the file. {@link FileSystemService} calls `stat`
|
|
15
|
+
* mainly through `exists()`, which discards the size, so this is acceptable —
|
|
16
|
+
* but avoid calling `stat` in a hot loop.
|
|
17
|
+
*/
|
|
18
|
+
var WebContainerFileSystemProvider = class {
|
|
19
|
+
name = "webcontainer";
|
|
20
|
+
#container;
|
|
21
|
+
constructor(container) {
|
|
22
|
+
this.#container = container;
|
|
23
|
+
}
|
|
24
|
+
async readFile(path) {
|
|
25
|
+
try {
|
|
26
|
+
return await this.#container.fs.readFile(path);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw translate(error, path);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async writeFile(path, data) {
|
|
32
|
+
try {
|
|
33
|
+
await this.#container.fs.writeFile(path, data);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw translate(error, path);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async createDirectory(path) {
|
|
39
|
+
try {
|
|
40
|
+
await this.#container.fs.mkdir(path, { recursive: true });
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (isErrno(error, "EEXIST")) {
|
|
43
|
+
if ((await this.stat(path)).type === "directory") return;
|
|
44
|
+
throw fileExists(path);
|
|
45
|
+
}
|
|
46
|
+
throw translate(error, path);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async delete(path, options) {
|
|
50
|
+
try {
|
|
51
|
+
await this.#container.fs.rm(path, { recursive: options.recursive });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (isErrno(error, "ENOTEMPTY")) throw directoryNotEmpty(path);
|
|
54
|
+
throw translate(error, path);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async rename(from, to, options) {
|
|
58
|
+
if (!options.overwrite && await this.#exists(to)) throw fileExists(to);
|
|
59
|
+
try {
|
|
60
|
+
await this.#container.fs.rename(from, to);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw translate(error, from);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async stat(path) {
|
|
66
|
+
if (path === ROOT) return {
|
|
67
|
+
type: "directory",
|
|
68
|
+
size: 0,
|
|
69
|
+
mtime: 0
|
|
70
|
+
};
|
|
71
|
+
const name = basename(path);
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = await this.readDirectory(dirname(path));
|
|
75
|
+
} catch {
|
|
76
|
+
throw fileNotFound(path);
|
|
77
|
+
}
|
|
78
|
+
const entry = entries.find((candidate) => candidate.name === name);
|
|
79
|
+
if (!entry) throw fileNotFound(path);
|
|
80
|
+
if (entry.type === "directory") return {
|
|
81
|
+
type: "directory",
|
|
82
|
+
size: 0,
|
|
83
|
+
mtime: 0
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
type: "file",
|
|
87
|
+
size: (await this.readFile(path)).byteLength,
|
|
88
|
+
mtime: 0
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async readDirectory(path) {
|
|
92
|
+
let entries;
|
|
93
|
+
try {
|
|
94
|
+
entries = await this.#container.fs.readdir(path, { withFileTypes: true });
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (isErrno(error, "ENOTDIR")) throw notADirectory(path);
|
|
97
|
+
throw translate(error, path);
|
|
98
|
+
}
|
|
99
|
+
return entries.map((entry) => ({
|
|
100
|
+
name: entry.name,
|
|
101
|
+
type: entry.isDirectory() ? "directory" : "file"
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Watches a subtree.
|
|
106
|
+
*
|
|
107
|
+
* WebContainer reports a `rename` event for both creation and deletion, so
|
|
108
|
+
* each one is resolved by probing for the path afterwards. The probe is why
|
|
109
|
+
* the listener is async and why events arrive slightly after the change.
|
|
110
|
+
*/
|
|
111
|
+
watch(path, options, listener) {
|
|
112
|
+
const watcher = this.#container.fs.watch(path, { recursive: options.recursive }, (event, filename) => {
|
|
113
|
+
if (typeof filename !== "string") return;
|
|
114
|
+
const changed = joinPath(path, filename);
|
|
115
|
+
if (event === "change") {
|
|
116
|
+
listener([{
|
|
117
|
+
type: "changed",
|
|
118
|
+
path: changed
|
|
119
|
+
}]);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.#exists(changed).then((exists) => {
|
|
123
|
+
listener([{
|
|
124
|
+
type: exists ? "created" : "deleted",
|
|
125
|
+
path: changed
|
|
126
|
+
}]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
return toDisposable(() => watcher.close());
|
|
130
|
+
}
|
|
131
|
+
/** Existence probe that never throws. */
|
|
132
|
+
async #exists(path) {
|
|
133
|
+
try {
|
|
134
|
+
await this.stat(path);
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
/** Whether an unknown thrown value is a Node-style error with `code`. */
|
|
142
|
+
function isErrno(error, code) {
|
|
143
|
+
return typeof error === "object" && error !== null && error.code === code;
|
|
144
|
+
}
|
|
145
|
+
/** Converts a WebContainer/Node error into a typed hudhod error. */
|
|
146
|
+
function translate(error, path) {
|
|
147
|
+
if (isErrno(error, "ENOENT")) return fileNotFound(path);
|
|
148
|
+
if (isErrno(error, "ENOTDIR")) return notADirectory(path);
|
|
149
|
+
if (isErrno(error, "EEXIST")) return fileExists(path);
|
|
150
|
+
if (isErrno(error, "ENOTEMPTY")) return directoryNotEmpty(path);
|
|
151
|
+
return error;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/webcontainer/process-spawner.ts
|
|
156
|
+
/**
|
|
157
|
+
* Spawns processes inside a WebContainer.
|
|
158
|
+
*
|
|
159
|
+
* The mapping is close to one-to-one: WebContainer already exposes a merged
|
|
160
|
+
* output stream, a writable stdin, and an exit promise. The only adaptation is
|
|
161
|
+
* making `kill()` idempotent, which the provider contract requires.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* const spawner = new WebContainerProcessSpawner(container);
|
|
166
|
+
* const processes = new ProcessService(spawner);
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
var WebContainerProcessSpawner = class {
|
|
170
|
+
name = "webcontainer";
|
|
171
|
+
#container;
|
|
172
|
+
constructor(container) {
|
|
173
|
+
this.#container = container;
|
|
174
|
+
}
|
|
175
|
+
async spawn(command, args, options) {
|
|
176
|
+
const process = await this.#container.spawn(command, [...args], {
|
|
177
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
178
|
+
...options.env ? { env: { ...options.env } } : {},
|
|
179
|
+
...options.terminal ? { terminal: { ...options.terminal } } : {}
|
|
180
|
+
});
|
|
181
|
+
let killed = false;
|
|
182
|
+
return {
|
|
183
|
+
output: process.output,
|
|
184
|
+
input: process.input,
|
|
185
|
+
exit: process.exit,
|
|
186
|
+
kill() {
|
|
187
|
+
if (killed) return;
|
|
188
|
+
killed = true;
|
|
189
|
+
process.kill();
|
|
190
|
+
},
|
|
191
|
+
resize(dimensions) {
|
|
192
|
+
if (!options.terminal) return;
|
|
193
|
+
process.resize(dimensions);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
//#endregion
|
|
200
|
+
export { WebContainerFileSystemProvider, WebContainerProcessSpawner };
|