@seanhogg/builderforce-webcontainers-core 2026.10.4 → 2026.10.6

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.
Files changed (91) hide show
  1. package/README.md +1 -1
  2. package/dist/build.d.ts +3 -3
  3. package/dist/build.d.ts.map +1 -1
  4. package/dist/build.js +1 -1
  5. package/dist/build.js.map +1 -1
  6. package/dist/buildHost.js +3 -3
  7. package/dist/buildHost.js.map +1 -1
  8. package/dist/check/typecheck.d.ts +2 -2
  9. package/dist/check/typecheck.d.ts.map +1 -1
  10. package/dist/check/typecheck.js +1 -1
  11. package/dist/check/typecheck.js.map +1 -1
  12. package/dist/compileScript.js +2 -2
  13. package/dist/compileScript.js.map +1 -1
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/node/childProcess.js +1 -1
  19. package/dist/node/childProcess.js.map +1 -1
  20. package/dist/node/misc.js +1 -1
  21. package/dist/node/misc.js.map +1 -1
  22. package/dist/projectConfig.d.ts +2 -0
  23. package/dist/projectConfig.d.ts.map +1 -1
  24. package/dist/projectConfig.js +20 -1
  25. package/dist/projectConfig.js.map +1 -1
  26. package/dist/resolve.d.ts +9 -0
  27. package/dist/resolve.d.ts.map +1 -1
  28. package/dist/resolve.js +9 -0
  29. package/dist/resolve.js.map +1 -1
  30. package/dist/shell/builtins.d.ts.map +1 -1
  31. package/dist/shell/builtins.js +2 -2
  32. package/dist/shell/builtins.js.map +1 -1
  33. package/dist/shell/devHandoff.d.ts +1 -1
  34. package/dist/shell/exec.js +4 -4
  35. package/dist/shell/exec.js.map +1 -1
  36. package/dist/shell/expand.js +1 -1
  37. package/dist/shell/expand.js.map +1 -1
  38. package/dist/shell/program.d.ts +1 -1
  39. package/dist/shell/program.js +3 -3
  40. package/dist/shell/program.js.map +1 -1
  41. package/dist/system/commands.d.ts +4 -4
  42. package/dist/system/commands.d.ts.map +1 -1
  43. package/dist/system/commands.js +3 -3
  44. package/dist/system/commands.js.map +1 -1
  45. package/dist/system/index.d.ts +6 -4
  46. package/dist/system/index.d.ts.map +1 -1
  47. package/dist/system/index.js +2 -1
  48. package/dist/system/index.js.map +1 -1
  49. package/dist/system/kernel.d.ts +9 -16
  50. package/dist/system/kernel.d.ts.map +1 -1
  51. package/dist/system/kernel.js +10 -23
  52. package/dist/system/kernel.js.map +1 -1
  53. package/dist/system/process.d.ts +18 -14
  54. package/dist/system/process.d.ts.map +1 -1
  55. package/dist/system/process.js +12 -10
  56. package/dist/system/process.js.map +1 -1
  57. package/dist/system/programs.js +1 -1
  58. package/dist/system/programs.js.map +1 -1
  59. package/dist/system/serverEvents.d.ts +27 -0
  60. package/dist/system/serverEvents.d.ts.map +1 -0
  61. package/dist/system/serverEvents.js +33 -0
  62. package/dist/system/serverEvents.js.map +1 -0
  63. package/dist/system/types.d.ts +4 -4
  64. package/dist/system/types.d.ts.map +1 -1
  65. package/dist/vfs.d.ts +11 -16
  66. package/dist/vfs.d.ts.map +1 -1
  67. package/dist/vfs.js +3 -8
  68. package/dist/vfs.js.map +1 -1
  69. package/package.json +1 -1
  70. package/src/build.ts +4 -4
  71. package/src/buildHost.ts +3 -3
  72. package/src/check/typecheck.ts +4 -4
  73. package/src/compileScript.ts +2 -2
  74. package/src/index.ts +2 -2
  75. package/src/node/childProcess.ts +1 -1
  76. package/src/node/misc.ts +1 -1
  77. package/src/projectConfig.ts +22 -1
  78. package/src/resolve.ts +10 -0
  79. package/src/shell/builtins.ts +2 -2
  80. package/src/shell/devHandoff.ts +1 -1
  81. package/src/shell/exec.ts +4 -4
  82. package/src/shell/expand.ts +1 -1
  83. package/src/shell/program.ts +4 -4
  84. package/src/system/commands.ts +6 -6
  85. package/src/system/index.ts +6 -4
  86. package/src/system/kernel.ts +15 -32
  87. package/src/system/process.ts +28 -20
  88. package/src/system/programs.ts +1 -1
  89. package/src/system/serverEvents.ts +44 -0
  90. package/src/system/types.ts +4 -4
  91. package/src/vfs.ts +13 -13
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The `jsh` program: `jsh -c "…"`, `jsh script.sh`, a script on stdin, or —
2
+ * The `bfsh` program: `bfsh -c "…"`, `bfsh script.sh`, a script on stdin, or —
3
3
  * attached to a terminal — the interactive shell. Interactively the shell owns
4
4
  * the keyboard (raw mode) while it edits a line, and hands it to the running
5
5
  * job otherwise: straight through to a terminal-attached child (which has its
@@ -22,7 +22,7 @@ function createShell(ctx: ProgramContext, positional: string[] = []): Shell {
22
22
  return new Shell({
23
23
  system: ctx.system,
24
24
  cwd: ctx.cwd,
25
- env: { ...ctx.env, SHELL: '/bin/jsh' },
25
+ env: { ...ctx.env, SHELL: '/bin/bfsh' },
26
26
  positional,
27
27
  terminalSize: () => (ctx.io.terminal ? { cols: ctx.io.terminal.cols, rows: ctx.io.terminal.rows } : undefined),
28
28
  commands: SHELL_COMMANDS,
@@ -131,7 +131,7 @@ async function interactive(ctx: ProgramContext): Promise<number> {
131
131
  try {
132
132
  await shell.exec(line, io, current.input, controller.signal);
133
133
  } catch (error) {
134
- ctx.io.stderr(`jsh: ${error instanceof Error ? error.message : String(error)}\n`);
134
+ ctx.io.stderr(`bfsh: ${error instanceof Error ? error.message : String(error)}\n`);
135
135
  }
136
136
  job = undefined;
137
137
  if (shell.exitRequested !== undefined) return finish(shell.exitRequested);
@@ -206,7 +206,7 @@ export const shellProgram: Program = async (ctx) => {
206
206
  const path = args[0].startsWith('/') ? args[0] : join(ctx.cwd, args[0]);
207
207
  const source = ctx.system.fs.readText(path);
208
208
  if (source === undefined) {
209
- ctx.io.stderr(`jsh: ${args[0]}: No such file or directory\n`);
209
+ ctx.io.stderr(`bfsh: ${args[0]}: No such file or directory\n`);
210
210
  return 127;
211
211
  }
212
212
  return runOnce(ctx, createShell(ctx, args.slice(1)), source.replace(/^#!.*\n/, ''));
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Command lookup, shared by the kernel, the browser host and the shell:
3
- * registered programs first (node, npm, jsh, coreutils), then a path or a
3
+ * registered programs first (node, npm, bfsh, coreutils), then a path or a
4
4
  * `PATH`/`node_modules/.bin` lookup. A found file runs under `node` (a `.bin`
5
- * shim runs its target directly) or, with a shell shebang, under `jsh`.
5
+ * shim runs its target directly) or, with a shell shebang, under `bfsh`.
6
6
  */
7
7
  import type { VirtualFileSystem } from '../vfs.js';
8
8
  import { dirname, extname, join, normalizePath } from '../paths.js';
@@ -10,7 +10,7 @@ import { readBinLink } from '../installer/bins.js';
10
10
  import type { Program } from './types.js';
11
11
 
12
12
  export interface ResolvedCommand {
13
- /** The registered program that will run (`node`, `jsh`, `ls`…). */
13
+ /** The registered program that will run (`node`, `bfsh`, `ls`…). */
14
14
  name: string;
15
15
  program: Program;
16
16
  args: string[];
@@ -40,12 +40,12 @@ export function findExecutable(fs: VirtualFileSystem, command: string, cwd: stri
40
40
  }
41
41
 
42
42
  /** How to run a script file: its interpreter and arguments. */
43
- export function interpreterFor(fs: VirtualFileSystem, path: string): { name: 'node' | 'jsh'; args: string[] } {
43
+ export function interpreterFor(fs: VirtualFileSystem, path: string): { name: 'node' | 'bfsh'; args: string[] } {
44
44
  const target = readBinLink(fs, path);
45
45
  if (target) return { name: 'node', args: [target] };
46
46
  const firstLine = (fs.readText(path) ?? '').split('\n', 1)[0] ?? '';
47
- if (/^#!.*\b(?:sh|bash|jsh|zsh|dash)\b/.test(firstLine)) return { name: 'jsh', args: [path] };
48
- if (['.sh', '.bash'].includes(extname(path))) return { name: 'jsh', args: [path] };
47
+ if (/^#!.*\b(?:sh|bash|bfsh|zsh|dash)\b/.test(firstLine)) return { name: 'bfsh', args: [path] };
48
+ if (['.sh', '.bash'].includes(extname(path))) return { name: 'bfsh', args: [path] };
49
49
  return { name: 'node', args: [path] };
50
50
  }
51
51
 
@@ -1,10 +1,12 @@
1
1
  export { Kernel, DEFAULT_ENV } from './kernel.js';
2
- export type { KernelOptions, KernelSpawnOptions } from './kernel.js';
3
- export type { System, Program, ProgramContext, ProgramIO, TerminalPort, SpawnOptions, ChildHandle, HttpRequestData, HttpResponseData, HttpHandler } from './types.js';
2
+ export type { KernelOptions } from './kernel.js';
3
+ export type { System, Program, ProgramContext, ProgramIO, TerminalPort, ProcessOptions, ChildHandle, HttpRequestData, HttpResponseData, HttpHandler } from './types.js';
4
4
  export { PortRegistry, parsePortPath } from './ports.js';
5
5
  export type { PortEvent } from './ports.js';
6
- export { runProgram, toWebContainerProcess, allocatePid } from './process.js';
7
- export type { WebContainerProcess, RunProgramOptions } from './process.js';
6
+ export { runProgram, runArguments, toRuntimeProcess, allocatePid } from './process.js';
7
+ export type { RuntimeProcess, RunProgramOptions } from './process.js';
8
+ export { ServerEvents } from './serverEvents.js';
9
+ export type { ServerAddress, PortChange } from './serverEvents.js';
8
10
  export { resolveCommand, findExecutable, binDirectories, interpreterFor, commandNotFound } from './commands.js';
9
11
  export type { ResolvedCommand } from './commands.js';
10
12
  export { InputQueue, LineDiscipline, toTerminal } from './terminal.js';
@@ -7,10 +7,11 @@
7
7
  */
8
8
  import { VirtualFileSystem } from '../vfs.js';
9
9
  import type { PackageCache } from '../installer/registry.js';
10
- import type { ChildHandle, HttpRequestData, HttpResponseData, Program, SpawnOptions, System } from './types.js';
10
+ import type { ChildHandle, HttpRequestData, HttpResponseData, Program, ProcessOptions, System } from './types.js';
11
11
  import { PortRegistry } from './ports.js';
12
+ import { ServerEvents } from './serverEvents.js';
12
13
  import { commandNotFound, resolveCommand } from './commands.js';
13
- import { runProgram, toWebContainerProcess, type WebContainerProcess } from './process.js';
14
+ import { runArguments, runProgram, toRuntimeProcess, type RuntimeProcess } from './process.js';
14
15
 
15
16
  export interface KernelOptions {
16
17
  fs?: VirtualFileSystem;
@@ -26,19 +27,11 @@ export interface KernelOptions {
26
27
  packageCache?: PackageCache;
27
28
  }
28
29
 
29
- export interface KernelSpawnOptions extends SpawnOptions {
30
- /** Kept for `@webcontainer/api` parity; output is always delivered. */
31
- output?: boolean;
32
- }
33
-
34
- type ServerReadyListener = (port: number, url: string) => void;
35
- type PortListener = (port: number, type: 'open' | 'close', url: string) => void;
36
-
37
30
  export const DEFAULT_ENV: Record<string, string> = {
38
31
  PATH: '/usr/local/bin:/usr/bin:/bin',
39
32
  HOME: '/home/user',
40
33
  USER: 'user',
41
- SHELL: '/bin/jsh',
34
+ SHELL: '/bin/bfsh',
42
35
  TERM: 'xterm-256color',
43
36
  COLORTERM: 'truecolor',
44
37
  LANG: 'en_US.UTF-8',
@@ -48,20 +41,16 @@ export const DEFAULT_ENV: Record<string, string> = {
48
41
  export class Kernel {
49
42
  readonly fs: VirtualFileSystem;
50
43
  readonly ports = new PortRegistry();
44
+ /** Servers listening, ports opening and closing. */
45
+ readonly servers = new ServerEvents();
51
46
  readonly env: Record<string, string>;
52
- private readonly serverReady = new Set<ServerReadyListener>();
53
- private readonly portListeners = new Set<PortListener>();
54
47
  private readonly system: System;
55
48
 
56
49
  constructor(private readonly options: KernelOptions) {
57
50
  this.fs = options.fs ?? new VirtualFileSystem();
58
51
  this.env = { ...DEFAULT_ENV, ...options.env };
59
52
  const serverUrl = (port: number) => options.serverUrl?.(port) ?? `http://localhost:${port}/`;
60
- this.ports.watch(({ type, port }) => {
61
- const url = serverUrl(port);
62
- for (const l of this.portListeners) l(port, type, url);
63
- if (type === 'open') for (const l of this.serverReady) l(port, url);
64
- });
53
+ this.ports.watch(({ type, port }) => this.servers.portChanged({ type, port, url: serverUrl(port) }));
65
54
  this.system = {
66
55
  fs: this.fs,
67
56
  fetch: options.fetch ?? ((input, init) => globalThis.fetch(input, init)),
@@ -69,21 +58,23 @@ export class Kernel {
69
58
  listen: (port, handler) => this.ports.listen(port, handler),
70
59
  loopback: (port, request) => this.ports.request(port, request),
71
60
  serverUrl,
72
- announceServer: (port, url) => this.serverReady.forEach((l) => l(port, url)),
61
+ announceServer: (port, url) => this.servers.announce({ port, url }),
73
62
  previewUrl: options.previewUrl,
74
63
  registry: options.registry,
75
64
  packageCache: options.packageCache,
76
65
  };
77
66
  }
78
67
 
79
- /** Same shape as `WebContainer#spawn`. */
80
- spawn(command: string, args: string[] | KernelSpawnOptions = [], options: KernelSpawnOptions = {}): WebContainerProcess {
81
- if (!Array.isArray(args)) [options, args] = [args, []];
82
- return toWebContainerProcess(this.spawnChild(command, args, options), !!options.terminal);
68
+ /** Start a command — `run('npm', ['install'])`, `run('bfsh', { terminal })` — as a stream-based {@link RuntimeProcess}. */
69
+ run(command: string, args?: string[], options?: ProcessOptions): RuntimeProcess;
70
+ run(command: string, options?: ProcessOptions): RuntimeProcess;
71
+ run(command: string, argsOrOptions?: string[] | ProcessOptions, maybeOptions?: ProcessOptions): RuntimeProcess {
72
+ const [args, options] = runArguments(argsOrOptions, maybeOptions);
73
+ return toRuntimeProcess(this.spawnChild(command, args, options), !!options.terminal);
83
74
  }
84
75
 
85
76
  /** A child as a program's parent sees it — what `System.spawn` returns. */
86
- spawnChild(command: string, args: string[] = [], options: SpawnOptions = {}): ChildHandle {
77
+ spawnChild(command: string, args: string[] = [], options: ProcessOptions = {}): ChildHandle {
87
78
  const cwd = options.cwd ?? '/';
88
79
  const env = { ...this.env, ...options.env };
89
80
  const resolved = resolveCommand(this.fs, this.options.programs, command, args, cwd, env);
@@ -95,12 +86,4 @@ export class Kernel {
95
86
  request(port: number, request: Partial<HttpRequestData> & { url: string }): Promise<HttpResponseData | undefined> {
96
87
  return this.ports.request(port, { method: 'GET', headers: {}, body: null, ...request });
97
88
  }
98
-
99
- on(event: 'server-ready', listener: ServerReadyListener): () => void;
100
- on(event: 'port', listener: PortListener): () => void;
101
- on(event: 'server-ready' | 'port', listener: ServerReadyListener | PortListener): () => void {
102
- const set = (event === 'server-ready' ? this.serverReady : this.portListeners) as Set<typeof listener>;
103
- set.add(listener);
104
- return () => set.delete(listener);
105
- }
106
89
  }
@@ -2,10 +2,10 @@
2
2
  * Running one program in-process: its stdin queue, TTY line discipline,
3
3
  * output listeners and kill signal, packaged as a {@link ChildHandle}. The core
4
4
  * kernel uses it for every process; the browser worker uses it for the one
5
- * program it hosts. Also the WebContainer-shaped wrapper (`output`/`input`
6
- * streams) that public `spawn` returns.
5
+ * program it hosts. Also {@link RuntimeProcess}, the stream-based handle the
6
+ * public `run()` returns.
7
7
  */
8
- import type { ChildHandle, Program, ProgramIO, SpawnOptions, System } from './types.js';
8
+ import type { ChildHandle, Program, ProgramIO, ProcessOptions, System } from './types.js';
9
9
  import { InputQueue, LineDiscipline, toTerminal } from './terminal.js';
10
10
 
11
11
  let nextPid = 1000;
@@ -14,7 +14,7 @@ export function allocatePid(): number {
14
14
  return nextPid++;
15
15
  }
16
16
 
17
- export interface RunProgramOptions extends SpawnOptions {
17
+ export interface RunProgramOptions extends ProcessOptions {
18
18
  program: Program;
19
19
  args: string[];
20
20
  system: System;
@@ -122,18 +122,28 @@ export function runProgram(options: RunProgramOptions): ChildHandle {
122
122
  };
123
123
  }
124
124
 
125
- /** The public process shape, deliberately the same as `@webcontainer/api`'s `WebContainerProcess`. */
126
- export interface WebContainerProcess {
125
+ /** A running process as a host page sees it: streams for xterm.js or a log, and its exit code. */
126
+ export interface RuntimeProcess {
127
+ readonly pid: number;
127
128
  /** stdout and stderr, interleaved as written (`\r\n` line endings when attached to a terminal). */
128
- readonly output: ReadableStream<string>;
129
- readonly input: WritableStream<string>;
130
- readonly exit: Promise<number>;
129
+ readonly combinedOutput: ReadableStream<string>;
130
+ /** Keystrokes or piped input; closing it is end-of-file. */
131
+ readonly stdin: WritableStream<string>;
132
+ /** Resolves with the exit code (128 + signal number when killed). */
133
+ wait(): Promise<number>;
134
+ /** Default `SIGTERM`; `SIGKILL` cannot be caught. */
131
135
  kill(signal?: string): void;
132
- resize(dimensions: { cols: number; rows: number } | number, rows?: number): void;
136
+ /** Resize the attached terminal. A no-op for a process started without one. */
137
+ resizeTerminal(cols: number, rows: number): void;
133
138
  }
134
139
 
135
- export function toWebContainerProcess(child: ChildHandle, terminal: boolean): WebContainerProcess {
136
- const output = new ReadableStream<string>({
140
+ /** `run(command, args?, options?)` and `run(command, options)` — the same call either way. */
141
+ export function runArguments(args: string[] | ProcessOptions = [], options: ProcessOptions = {}): [string[], ProcessOptions] {
142
+ return Array.isArray(args) ? [args, options] : [[], args];
143
+ }
144
+
145
+ export function toRuntimeProcess(child: ChildHandle, terminal: boolean): RuntimeProcess {
146
+ const combinedOutput = new ReadableStream<string>({
137
147
  start(controller) {
138
148
  const push = (chunk: string) => controller.enqueue(terminal ? toTerminal(chunk) : chunk);
139
149
  child.onStdout(push);
@@ -141,18 +151,16 @@ export function toWebContainerProcess(child: ChildHandle, terminal: boolean): We
141
151
  void child.exit.then(() => controller.close());
142
152
  },
143
153
  });
144
- const input = new WritableStream<string>({
154
+ const stdin = new WritableStream<string>({
145
155
  write: (chunk) => child.write(chunk),
146
156
  close: () => child.closeStdin(),
147
157
  });
148
158
  return {
149
- output,
150
- input,
151
- exit: child.exit,
159
+ pid: child.pid,
160
+ combinedOutput,
161
+ stdin,
162
+ wait: () => child.exit,
152
163
  kill: (signal) => child.kill(signal),
153
- resize(dimensions, rows) {
154
- const size = typeof dimensions === 'number' ? { cols: dimensions, rows: rows ?? 24 } : dimensions;
155
- child.resize?.(size.cols, size.rows);
156
- },
164
+ resizeTerminal: (cols, rows) => child.resize?.(cols, rows),
157
165
  };
158
166
  }
@@ -20,7 +20,7 @@ function viaShell(command: string): Program {
20
20
  export function defaultPrograms(): Record<string, Program> {
21
21
  const programs: Record<string, Program> = {
22
22
  node: nodeProgram,
23
- jsh: shellProgram,
23
+ bfsh: shellProgram,
24
24
  sh: shellProgram,
25
25
  bash: shellProgram,
26
26
  npm: viaShell('npm'),
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Who hears about servers in a runtime: a port opening or closing, and a
3
+ * server becoming reachable (which a dev-server handoff announces without
4
+ * opening a port). One instance per runtime — the core `Kernel` and the
5
+ * browser's process host both own one, so the listener contract is the same.
6
+ */
7
+
8
+ export interface ServerAddress {
9
+ port: number;
10
+ /** Where a browser reaches it (the preview URL for that port). */
11
+ url: string;
12
+ }
13
+
14
+ export interface PortChange extends ServerAddress {
15
+ type: 'open' | 'close';
16
+ }
17
+
18
+ export class ServerEvents {
19
+ private readonly listening = new Set<(server: ServerAddress) => void>();
20
+ private readonly changes = new Set<(change: PortChange) => void>();
21
+
22
+ /** A server started listening (or a dev script handed off to the preview); `url` loads it in an iframe. */
23
+ onListening(listener: (server: ServerAddress) => void): () => void {
24
+ this.listening.add(listener);
25
+ return () => this.listening.delete(listener);
26
+ }
27
+
28
+ /** A virtual server opened or closed its port. */
29
+ onPortChange(listener: (change: PortChange) => void): () => void {
30
+ this.changes.add(listener);
31
+ return () => this.changes.delete(listener);
32
+ }
33
+
34
+ /** Report a port table change; an opened port is also a server listening. */
35
+ portChanged(change: PortChange): void {
36
+ for (const listener of this.changes) listener(change);
37
+ if (change.type === 'open') this.announce({ port: change.port, url: change.url });
38
+ }
39
+
40
+ /** Report a reachable server that did not open a port of its own. */
41
+ announce(server: ServerAddress): void {
42
+ for (const listener of this.listening) listener(server);
43
+ }
44
+ }
@@ -24,14 +24,14 @@ export interface ProgramIO {
24
24
  terminal?: TerminalPort;
25
25
  }
26
26
 
27
- export interface SpawnOptions {
27
+ export interface ProcessOptions {
28
28
  cwd?: string;
29
29
  env?: Record<string, string>;
30
30
  /** Attach to a terminal of this size (`isTTY`, line discipline, `\r\n` output). */
31
31
  terminal?: { cols: number; rows: number };
32
32
  }
33
33
 
34
- /** A child as its parent sees it (separate stdout/stderr, unlike the public API). */
34
+ /** A child as its parent sees it (separate stdout/stderr listeners, unlike the streams of the public `RuntimeProcess`). */
35
35
  export interface ChildHandle {
36
36
  readonly pid: number;
37
37
  onStdout(listener: (chunk: string) => void): () => void;
@@ -64,14 +64,14 @@ export interface System {
64
64
  fs: VirtualFileSystem;
65
65
  /** Outbound network (registry, `http.request`, `fetch`). */
66
66
  fetch(input: string, init?: RequestInit): Promise<Response>;
67
- spawn(command: string, args: string[], options?: SpawnOptions): ChildHandle;
67
+ spawn(command: string, args: string[], options?: ProcessOptions): ChildHandle;
68
68
  /** Register a virtual server; throws `EADDRINUSE`. Port 0 picks a free one. Returns the port and an unlisten. */
69
69
  listen(port: number, handler: HttpHandler): { port: number; close(): void };
70
70
  /** Send a request to a virtual server in this runtime (`http.get('http://localhost:3000')`); undefined when nothing listens. */
71
71
  loopback(port: number, request: HttpRequestData): Promise<HttpResponseData | undefined>;
72
72
  /** Where a browser reaches a server on `port` (the preview URL), if known. */
73
73
  serverUrl(port: number): string | undefined;
74
- /** Tell the host a server is ready (`server-ready`); used by the dev-server handoff. */
74
+ /** Tell the host a server is reachable without opening a port; used by the dev-server handoff. */
75
75
  announceServer(port: number, url: string): void;
76
76
  /** The in-browser dev server's preview URL, for `vite`/`react-scripts start` handoff. */
77
77
  previewUrl?: string;
package/src/vfs.ts CHANGED
@@ -2,14 +2,18 @@ import { normalizePath, join, dirname } from './paths.js';
2
2
 
3
3
  export type FileContent = string | Uint8Array;
4
4
 
5
- /** The nested tree shape `@webcontainer/api`'s `mount()` takes — accepted as-is. */
6
- export interface FileSystemTree {
7
- [name: string]: { file: { contents: FileContent } } | { directory: FileSystemTree };
8
- }
9
-
10
- /** A flat `{ 'src/main.tsx': '...' }` map, the other shape callers commonly hold. */
5
+ /** A flat `{ 'src/main.tsx': '...' }` map: path → contents. */
11
6
  export type FlatFiles = Record<string, FileContent>;
12
7
 
8
+ /**
9
+ * Files to write: a name maps to contents (a file) or to another tree (a
10
+ * directory). Names may hold slashes, so a {@link FlatFiles} map is a tree too,
11
+ * and the two styles mix freely: `{ src: { 'main.tsx': '…' }, 'public/a.svg': '…' }`.
12
+ */
13
+ export interface FileTree {
14
+ [name: string]: FileContent | FileTree;
15
+ }
16
+
13
17
  export interface FsChange {
14
18
  /** `mkdir` is an explicitly created (possibly empty) directory. */
15
19
  type: 'write' | 'remove' | 'mkdir';
@@ -129,18 +133,14 @@ export class VirtualFileSystem {
129
133
  return this.mtimes.get(normalizePath(path));
130
134
  }
131
135
 
132
- /**
133
- * Write many files at once. Takes either the nested WebContainer tree or a flat
134
- * path→contents map — or a mix, since each entry is judged on its own.
135
- */
136
- mount(files: FileSystemTree | FlatFiles, at = '/'): void {
136
+ /** Write many files at once, under `at` (default `/`). A nested object is a directory, created even when empty. */
137
+ writeTree(files: FileTree, at = '/'): void {
137
138
  for (const [name, entry] of Object.entries(files)) {
138
139
  const path = join(at, name);
139
140
  if (typeof entry === 'string' || entry instanceof Uint8Array) this.writeFile(path, entry);
140
- else if ('file' in entry) this.writeFile(path, entry.file.contents);
141
141
  else {
142
142
  this.mkdir(path);
143
- this.mount(entry.directory, path);
143
+ this.writeTree(entry, path);
144
144
  }
145
145
  }
146
146
  }