@seanhogg/builderforce-webcontainers-core 2026.10.6 → 2026.10.8
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/archive.d.ts +13 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +53 -0
- package/dist/archive.js.map +1 -0
- package/dist/devServer.d.ts +5 -0
- package/dist/devServer.d.ts.map +1 -1
- package/dist/devServer.js +11 -2
- package/dist/devServer.js.map +1 -1
- package/dist/html.d.ts +12 -3
- package/dist/html.d.ts.map +1 -1
- package/dist/html.js +31 -5
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/installer/index.d.ts +2 -0
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +1 -0
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/install.d.ts +3 -0
- package/dist/installer/install.d.ts.map +1 -1
- package/dist/installer/install.js +1 -1
- package/dist/installer/install.js.map +1 -1
- package/dist/installer/npmrc.d.ts +26 -0
- package/dist/installer/npmrc.d.ts.map +1 -0
- package/dist/installer/npmrc.js +55 -0
- package/dist/installer/npmrc.js.map +1 -0
- package/dist/installer/registry.d.ts +3 -0
- package/dist/installer/registry.d.ts.map +1 -1
- package/dist/installer/registry.js +10 -5
- package/dist/installer/registry.js.map +1 -1
- package/dist/node/http/client.d.ts +3 -1
- package/dist/node/http/client.d.ts.map +1 -1
- package/dist/node/http/client.js +74 -5
- package/dist/node/http/client.js.map +1 -1
- package/dist/node/http/index.d.ts.map +1 -1
- package/dist/node/http/index.js +2 -1
- package/dist/node/http/index.js.map +1 -1
- package/dist/node/http/messages.d.ts +87 -0
- package/dist/node/http/messages.d.ts.map +1 -0
- package/dist/node/http/messages.js +146 -0
- package/dist/node/http/messages.js.map +1 -0
- package/dist/node/http/server.d.ts +13 -98
- package/dist/node/http/server.d.ts.map +1 -1
- package/dist/node/http/server.js +34 -202
- package/dist/node/http/server.js.map +1 -1
- package/dist/node/http/serverResponse.d.ts +42 -0
- package/dist/node/http/serverResponse.d.ts.map +1 -0
- package/dist/node/http/serverResponse.js +89 -0
- package/dist/node/http/serverResponse.js.map +1 -0
- package/dist/node/http/upgradeSocket.d.ts +34 -0
- package/dist/node/http/upgradeSocket.d.ts.map +1 -0
- package/dist/node/http/upgradeSocket.js +73 -0
- package/dist/node/http/upgradeSocket.js.map +1 -0
- package/dist/node/runtime.js +2 -2
- package/dist/node/runtime.js.map +1 -1
- package/dist/previewBridge.d.ts +42 -0
- package/dist/previewBridge.d.ts.map +1 -0
- package/dist/previewBridge.js +192 -0
- package/dist/previewBridge.js.map +1 -0
- package/dist/shell/ast.d.ts +101 -0
- package/dist/shell/ast.d.ts.map +1 -0
- package/dist/shell/ast.js +7 -0
- package/dist/shell/ast.js.map +1 -0
- package/dist/shell/builtins.d.ts +14 -0
- package/dist/shell/builtins.d.ts.map +1 -1
- package/dist/shell/builtins.js +87 -7
- package/dist/shell/builtins.js.map +1 -1
- package/dist/shell/compound.d.ts +30 -0
- package/dist/shell/compound.d.ts.map +1 -0
- package/dist/shell/compound.js +73 -0
- package/dist/shell/compound.js.map +1 -0
- package/dist/shell/exec.d.ts +27 -6
- package/dist/shell/exec.d.ts.map +1 -1
- package/dist/shell/exec.js +127 -41
- package/dist/shell/exec.js.map +1 -1
- package/dist/shell/expand.d.ts +5 -1
- package/dist/shell/expand.d.ts.map +1 -1
- package/dist/shell/expand.js +7 -5
- package/dist/shell/expand.js.map +1 -1
- package/dist/shell/lineReader.d.ts +9 -0
- package/dist/shell/lineReader.d.ts.map +1 -0
- package/dist/shell/lineReader.js +42 -0
- package/dist/shell/lineReader.js.map +1 -0
- package/dist/shell/npm.d.ts.map +1 -1
- package/dist/shell/npm.js +5 -1
- package/dist/shell/npm.js.map +1 -1
- package/dist/shell/packageManagers.d.ts +20 -0
- package/dist/shell/packageManagers.d.ts.map +1 -0
- package/dist/shell/packageManagers.js +74 -0
- package/dist/shell/packageManagers.js.map +1 -0
- package/dist/shell/parse.d.ts +16 -66
- package/dist/shell/parse.d.ts.map +1 -1
- package/dist/shell/parse.js +244 -243
- package/dist/shell/parse.js.map +1 -1
- package/dist/shell/program.d.ts.map +1 -1
- package/dist/shell/program.js +2 -1
- package/dist/shell/program.js.map +1 -1
- package/dist/shell/testCommand.d.ts +9 -0
- package/dist/shell/testCommand.d.ts.map +1 -0
- package/dist/shell/testCommand.js +105 -0
- package/dist/shell/testCommand.js.map +1 -0
- package/dist/shell/tokenize.d.ts +25 -0
- package/dist/shell/tokenize.d.ts.map +1 -0
- package/dist/shell/tokenize.js +279 -0
- package/dist/shell/tokenize.js.map +1 -0
- package/dist/system/cookieJar.d.ts +59 -0
- package/dist/system/cookieJar.d.ts.map +1 -0
- package/dist/system/cookieJar.js +187 -0
- package/dist/system/cookieJar.js.map +1 -0
- package/dist/system/httpStream.d.ts +40 -0
- package/dist/system/httpStream.d.ts.map +1 -0
- package/dist/system/httpStream.js +156 -0
- package/dist/system/httpStream.js.map +1 -0
- package/dist/system/index.d.ts +9 -1
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +4 -0
- package/dist/system/index.js.map +1 -1
- package/dist/system/kernel.d.ts +10 -2
- package/dist/system/kernel.d.ts.map +1 -1
- package/dist/system/kernel.js +12 -3
- package/dist/system/kernel.js.map +1 -1
- package/dist/system/ports.d.ts +9 -4
- package/dist/system/ports.d.ts.map +1 -1
- package/dist/system/ports.js +21 -2
- package/dist/system/ports.js.map +1 -1
- package/dist/system/programs.d.ts +1 -1
- package/dist/system/programs.d.ts.map +1 -1
- package/dist/system/programs.js +2 -0
- package/dist/system/programs.js.map +1 -1
- package/dist/system/types.d.ts +36 -6
- package/dist/system/types.d.ts.map +1 -1
- package/dist/system/webSocketClient.d.ts +37 -0
- package/dist/system/webSocketClient.d.ts.map +1 -0
- package/dist/system/webSocketClient.js +152 -0
- package/dist/system/webSocketClient.js.map +1 -0
- package/dist/system/webSocketFrames.d.ts +39 -0
- package/dist/system/webSocketFrames.d.ts.map +1 -0
- package/dist/system/webSocketFrames.js +117 -0
- package/dist/system/webSocketFrames.js.map +1 -0
- package/dist/vfs.d.ts +16 -1
- package/dist/vfs.d.ts.map +1 -1
- package/dist/vfs.js +45 -3
- package/dist/vfs.js.map +1 -1
- package/package.json +1 -1
- package/src/archive.ts +63 -0
- package/src/devServer.ts +12 -2
- package/src/html.ts +34 -5
- package/src/index.ts +5 -1
- package/src/installer/index.ts +2 -0
- package/src/installer/install.ts +4 -1
- package/src/installer/npmrc.ts +69 -0
- package/src/installer/registry.ts +12 -5
- package/src/node/http/client.ts +69 -6
- package/src/node/http/index.ts +2 -1
- package/src/node/http/messages.ts +150 -0
- package/src/node/http/server.ts +37 -202
- package/src/node/http/serverResponse.ts +98 -0
- package/src/node/http/upgradeSocket.ts +79 -0
- package/src/node/runtime.ts +2 -2
- package/src/previewBridge.ts +194 -0
- package/src/shell/ast.ts +85 -0
- package/src/shell/builtins.ts +97 -7
- package/src/shell/compound.ts +89 -0
- package/src/shell/exec.ts +128 -40
- package/src/shell/expand.ts +8 -6
- package/src/shell/lineReader.ts +46 -0
- package/src/shell/npm.ts +5 -1
- package/src/shell/packageManagers.ts +75 -0
- package/src/shell/parse.ts +233 -236
- package/src/shell/program.ts +2 -1
- package/src/shell/testCommand.ts +105 -0
- package/src/shell/tokenize.ts +257 -0
- package/src/system/cookieJar.ts +187 -0
- package/src/system/httpStream.ts +168 -0
- package/src/system/index.ts +24 -1
- package/src/system/kernel.ts +16 -4
- package/src/system/ports.ts +23 -4
- package/src/system/programs.ts +3 -1
- package/src/system/types.ts +40 -6
- package/src/system/webSocketClient.ts +177 -0
- package/src/system/webSocketFrames.ts +118 -0
- package/src/vfs.ts +46 -3
package/src/shell/exec.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The executor: runs a parsed script. Lists honour `&&`/`||`/`;`/`&`; pipeline
|
|
3
|
-
* stages run concurrently, joined by in-memory pipes; redirects
|
|
4
|
-
* stdin/stdout/stderr
|
|
3
|
+
* stages run concurrently, joined by in-memory pipes; redirects (here-documents
|
|
4
|
+
* included) swap a stage's stdin/stdout/stderr. Compound commands run through
|
|
5
|
+
* compound.ts. Dispatch order per simple command: shell functions, builtins,
|
|
5
6
|
* extra in-process commands (npm, npx), the dev-server handoff, coreutils, and
|
|
6
7
|
* finally `system.spawn` (node, scripts on PATH, `.bin` entries) — which in the
|
|
7
8
|
* browser is a separate Web Worker.
|
|
8
9
|
*/
|
|
9
10
|
import type { System } from '../system/types.js';
|
|
10
11
|
import { InputQueue } from '../system/terminal.js';
|
|
11
|
-
import {
|
|
12
|
+
import type { Command as CommandNode, FunctionDefinition, Pipeline, Redirect, Script, SimpleCommand, Word } from './ast.js';
|
|
13
|
+
import { parse, ShellSyntaxError } from './parse.js';
|
|
12
14
|
import { expandWord, expandWords, type ExpandContext } from './expand.js';
|
|
13
|
-
import { BUILTINS, COREUTILS, type ShellState } from './builtins.js';
|
|
15
|
+
import { BUILTINS, COREUTILS, type BackgroundJob, type ShellState } from './builtins.js';
|
|
14
16
|
import { resolvePath, type CommandContext } from './context.js';
|
|
15
17
|
import { matchDevServer, runDevServerHandoff } from './devHandoff.js';
|
|
18
|
+
import { runCompound, type Flow, type ScriptRunner } from './compound.js';
|
|
16
19
|
|
|
17
20
|
export interface ShellIO {
|
|
18
21
|
stdout(text: string): void;
|
|
@@ -39,31 +42,44 @@ export interface ShellOptions {
|
|
|
39
42
|
/** The terminal size, when attached to one. */
|
|
40
43
|
terminalSize?: () => { cols: number; rows: number } | undefined;
|
|
41
44
|
commands?: Record<string, ShellCommand>;
|
|
45
|
+
/** Functions inherited from a parent shell (subshells and `$(…)`). */
|
|
46
|
+
functions?: Map<string, FunctionDefinition>;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
let nextShellPid = 500;
|
|
45
50
|
|
|
46
|
-
export class Shell implements ShellState {
|
|
51
|
+
export class Shell implements ShellState, ScriptRunner {
|
|
47
52
|
cwd: string;
|
|
48
53
|
env: Record<string, string>;
|
|
49
54
|
lastStatus = 0;
|
|
50
55
|
history: string[] = [];
|
|
51
56
|
exitRequested?: number;
|
|
57
|
+
flow?: Flow;
|
|
58
|
+
positional: string[];
|
|
59
|
+
readonly functions: Map<string, FunctionDefinition>;
|
|
60
|
+
readonly jobs = new Map<number, BackgroundJob>();
|
|
61
|
+
/** One frame per running function: names declared `local`, with the value to restore. */
|
|
62
|
+
readonly localFrames: Array<Map<string, string | undefined>> = [];
|
|
52
63
|
readonly pid = nextShellPid++;
|
|
53
64
|
readonly system: System;
|
|
54
|
-
private
|
|
65
|
+
private nextJob = 1;
|
|
55
66
|
private current?: { io: ShellIO; input: JobInput; signal: AbortSignal };
|
|
56
67
|
|
|
57
68
|
constructor(readonly options: ShellOptions) {
|
|
58
69
|
this.system = options.system;
|
|
59
70
|
this.cwd = options.cwd;
|
|
60
71
|
this.env = { ...options.env, PWD: options.cwd };
|
|
61
|
-
this.positional = options.positional ?? [];
|
|
72
|
+
this.positional = [...(options.positional ?? [])];
|
|
73
|
+
this.functions = new Map(options.functions);
|
|
62
74
|
}
|
|
63
75
|
|
|
64
|
-
/** A child shell for `npm run` / subshells: same IO, its own cwd and
|
|
76
|
+
/** A child shell for `npm run` / subshells: same IO, its own cwd, environment and functions. */
|
|
65
77
|
fork(overrides: { cwd?: string; env?: Record<string, string>; positional?: string[] }): Shell {
|
|
66
|
-
return new Shell({ ...this.options, cwd: overrides.cwd ?? this.cwd, env: { ...this.env, ...overrides.env }, positional: overrides.positional });
|
|
78
|
+
return new Shell({ ...this.options, cwd: overrides.cwd ?? this.cwd, env: { ...this.env, ...overrides.env }, positional: overrides.positional ?? this.positional, functions: this.functions });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
subshell(): Shell {
|
|
82
|
+
return this.fork({});
|
|
67
83
|
}
|
|
68
84
|
|
|
69
85
|
/** Parse and run `source`. Syntax errors print and return 2. */
|
|
@@ -80,23 +96,11 @@ export class Shell implements ShellState {
|
|
|
80
96
|
const previous = this.current;
|
|
81
97
|
this.current = { io, input, signal };
|
|
82
98
|
try {
|
|
83
|
-
|
|
84
|
-
if (this.exitRequested !== undefined || signal.aborted) break;
|
|
85
|
-
const run = async () => {
|
|
86
|
-
let status = await this.runPipeline(list.first, io, input, signal);
|
|
87
|
-
for (const { op, pipeline } of list.rest) {
|
|
88
|
-
if (this.exitRequested !== undefined || signal.aborted) break;
|
|
89
|
-
if ((op === '&&') === (status === 0)) status = await this.runPipeline(pipeline, io, input, signal);
|
|
90
|
-
}
|
|
91
|
-
return status;
|
|
92
|
-
};
|
|
93
|
-
if (list.background) {
|
|
94
|
-
void run();
|
|
95
|
-
this.lastStatus = 0;
|
|
96
|
-
} else this.lastStatus = await run();
|
|
97
|
-
}
|
|
99
|
+
await this.runScript(script, io, input, signal);
|
|
98
100
|
} finally {
|
|
99
101
|
this.current = previous;
|
|
102
|
+
// A `break`/`continue` outside any loop (or `return` outside a function) must not stop later lines.
|
|
103
|
+
if (!this.localFrames.length) this.flow = undefined;
|
|
100
104
|
}
|
|
101
105
|
if (signal.aborted && this.lastStatus === 0) this.lastStatus = 130;
|
|
102
106
|
return this.exitRequested ?? this.lastStatus;
|
|
@@ -109,6 +113,40 @@ export class Shell implements ShellState {
|
|
|
109
113
|
return this.exec(source, job.io, job.input, job.signal);
|
|
110
114
|
}
|
|
111
115
|
|
|
116
|
+
async runScript(script: Script, io: ShellIO, input: JobInput, signal: AbortSignal): Promise<number> {
|
|
117
|
+
for (const list of script) {
|
|
118
|
+
if (this.exitRequested !== undefined || signal.aborted || this.flow) break;
|
|
119
|
+
const run = async () => {
|
|
120
|
+
let status = await this.runPipeline(list.first, io, input, signal);
|
|
121
|
+
for (const { op, pipeline } of list.rest) {
|
|
122
|
+
if (this.exitRequested !== undefined || signal.aborted || this.flow) break;
|
|
123
|
+
if ((op === '&&') === (status === 0)) status = await this.runPipeline(pipeline, io, input, signal);
|
|
124
|
+
}
|
|
125
|
+
return status;
|
|
126
|
+
};
|
|
127
|
+
if (list.background) {
|
|
128
|
+
const id = this.nextJob++;
|
|
129
|
+
const done = run();
|
|
130
|
+
this.jobs.set(id, { id, done });
|
|
131
|
+
void done.finally(() => this.jobs.delete(id));
|
|
132
|
+
this.lastStatus = 0;
|
|
133
|
+
} else this.lastStatus = await run();
|
|
134
|
+
}
|
|
135
|
+
return this.lastStatus;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
expandWords(words: Word[], io: ShellIO, signal: AbortSignal): Promise<string[]> {
|
|
139
|
+
return expandWords(words, this.expandContext(io, signal));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async expandText(word: Word, io: ShellIO, signal: AbortSignal): Promise<string> {
|
|
143
|
+
return (await expandWord(word, this.expandContext(io, signal), { split: false, glob: false })).join('');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
setVar(name: string, value: string): void {
|
|
147
|
+
this.env[name] = value;
|
|
148
|
+
}
|
|
149
|
+
|
|
112
150
|
private expandContext(io: ShellIO, signal: AbortSignal): ExpandContext {
|
|
113
151
|
return {
|
|
114
152
|
fs: this.system.fs,
|
|
@@ -147,6 +185,21 @@ export class Shell implements ShellState {
|
|
|
147
185
|
}
|
|
148
186
|
|
|
149
187
|
private async runCommand(node: CommandNode, io: ShellIO, input: JobInput, signal: AbortSignal, alone: boolean): Promise<number> {
|
|
188
|
+
if (node.type === 'function') {
|
|
189
|
+
this.functions.set(node.name, node);
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
if (node.type === 'simple') return this.runSimple(node, io, input, signal, alone);
|
|
193
|
+
const redirected = await this.applyRedirects(node.redirects, io, input, signal);
|
|
194
|
+
if (typeof redirected === 'number') return redirected;
|
|
195
|
+
try {
|
|
196
|
+
return await runCompound(node, this, redirected.io, redirected.input, signal);
|
|
197
|
+
} finally {
|
|
198
|
+
redirected.flush();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private async runSimple(node: SimpleCommand, io: ShellIO, input: JobInput, signal: AbortSignal, alone: boolean): Promise<number> {
|
|
150
203
|
const ectx = this.expandContext(io, signal);
|
|
151
204
|
const argv = await expandWords(node.words, ectx);
|
|
152
205
|
const assigned: Record<string, string> = {};
|
|
@@ -155,14 +208,27 @@ export class Shell implements ShellState {
|
|
|
155
208
|
Object.assign(this.env, assigned);
|
|
156
209
|
return 0;
|
|
157
210
|
}
|
|
211
|
+
const redirected = await this.applyRedirects(node.redirects, io, input, signal);
|
|
212
|
+
if (typeof redirected === 'number') return redirected;
|
|
213
|
+
try {
|
|
214
|
+
return await this.dispatch(argv, assigned, redirected.io, redirected.input, signal, alone && redirected.io.isTerminal);
|
|
215
|
+
} finally {
|
|
216
|
+
redirected.flush();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
158
219
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
let stderr = io
|
|
220
|
+
/** Apply redirects; returns the new IO, or an exit status when a redirect fails. */
|
|
221
|
+
private async applyRedirects(redirects: Redirect[], io: ShellIO, input: JobInput, signal: AbortSignal): Promise<number | { io: ShellIO; input: JobInput; flush(): void }> {
|
|
222
|
+
let { stdout, stderr, isTerminal } = io;
|
|
162
223
|
let stdin = input;
|
|
163
|
-
let isTerminal = io.isTerminal;
|
|
164
224
|
const flushes: Array<() => void> = [];
|
|
165
|
-
|
|
225
|
+
const fromText = (text: string): JobInput => {
|
|
226
|
+
const queue = new InputQueue();
|
|
227
|
+
queue.push(text);
|
|
228
|
+
queue.push(null);
|
|
229
|
+
return { queue };
|
|
230
|
+
};
|
|
231
|
+
for (const redirect of redirects) {
|
|
166
232
|
if (redirect.op === '2>&1') {
|
|
167
233
|
stderr = stdout;
|
|
168
234
|
continue;
|
|
@@ -171,7 +237,12 @@ export class Shell implements ShellState {
|
|
|
171
237
|
stdout = stderr;
|
|
172
238
|
continue;
|
|
173
239
|
}
|
|
174
|
-
|
|
240
|
+
if (redirect.heredoc) {
|
|
241
|
+
const text = await this.expandText(redirect.heredoc.body, io, signal);
|
|
242
|
+
stdin = fromText(text);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const target = (await expandWord(redirect.target!, this.expandContext(io, signal))).join(' ');
|
|
175
246
|
const path = resolvePath(this, target);
|
|
176
247
|
if (redirect.op === '<') {
|
|
177
248
|
const text = this.system.fs.readText(path);
|
|
@@ -179,10 +250,7 @@ export class Shell implements ShellState {
|
|
|
179
250
|
io.stderr(`bfsh: ${target}: No such file or directory\n`);
|
|
180
251
|
return 1;
|
|
181
252
|
}
|
|
182
|
-
|
|
183
|
-
queue.push(text);
|
|
184
|
-
queue.push(null);
|
|
185
|
-
stdin = { queue };
|
|
253
|
+
stdin = fromText(text);
|
|
186
254
|
continue;
|
|
187
255
|
}
|
|
188
256
|
if (this.system.fs.isDirectory(path)) {
|
|
@@ -201,16 +269,13 @@ export class Shell implements ShellState {
|
|
|
201
269
|
isTerminal = false;
|
|
202
270
|
}
|
|
203
271
|
}
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
return await this.dispatch(argv, assigned, { stdout, stderr, isTerminal }, stdin, signal, alone && isTerminal);
|
|
207
|
-
} finally {
|
|
208
|
-
for (const flush of flushes) flush();
|
|
209
|
-
}
|
|
272
|
+
return { io: { stdout, stderr, isTerminal }, input: stdin, flush: () => flushes.forEach((flush) => flush()) };
|
|
210
273
|
}
|
|
211
274
|
|
|
212
275
|
private async dispatch(argv: string[], assigned: Record<string, string>, io: ShellIO, input: JobInput, signal: AbortSignal, attachTerminal: boolean): Promise<number> {
|
|
213
276
|
const [name, ...args] = argv as [string, ...string[]];
|
|
277
|
+
const fn = this.functions.get(name);
|
|
278
|
+
if (fn) return this.callFunction(fn, args, io, input, signal);
|
|
214
279
|
const env = { ...this.env, ...assigned };
|
|
215
280
|
const ctx: CommandContext = {
|
|
216
281
|
args,
|
|
@@ -233,6 +298,29 @@ export class Shell implements ShellState {
|
|
|
233
298
|
return this.spawnExternal(name, args, env, io, input, signal, attachTerminal);
|
|
234
299
|
}
|
|
235
300
|
|
|
301
|
+
/** A shell function: its own positional parameters and `local` frame; `return` ends it. */
|
|
302
|
+
private async callFunction(fn: FunctionDefinition, args: string[], io: ShellIO, input: JobInput, signal: AbortSignal): Promise<number> {
|
|
303
|
+
const savedPositional = this.positional;
|
|
304
|
+
this.positional = args;
|
|
305
|
+
const frame = new Map<string, string | undefined>();
|
|
306
|
+
this.localFrames.push(frame);
|
|
307
|
+
try {
|
|
308
|
+
let status = await runCompound(fn.body, this, io, input, signal);
|
|
309
|
+
if (this.flow?.kind === 'return') {
|
|
310
|
+
status = this.flow.status;
|
|
311
|
+
this.flow = undefined;
|
|
312
|
+
}
|
|
313
|
+
return status;
|
|
314
|
+
} finally {
|
|
315
|
+
this.localFrames.pop();
|
|
316
|
+
for (const [name, value] of frame) {
|
|
317
|
+
if (value === undefined) delete this.env[name];
|
|
318
|
+
else this.env[name] = value;
|
|
319
|
+
}
|
|
320
|
+
this.positional = savedPositional;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
236
324
|
private async spawnExternal(name: string, args: string[], env: Record<string, string>, io: ShellIO, input: JobInput, signal: AbortSignal, attachTerminal: boolean): Promise<number> {
|
|
237
325
|
const size = attachTerminal ? this.options.terminalSize?.() : undefined;
|
|
238
326
|
let child;
|
package/src/shell/expand.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { VirtualFileSystem } from '../vfs.js';
|
|
8
8
|
import { join, normalizePath } from '../paths.js';
|
|
9
|
-
import type { Segment, Word } from './
|
|
9
|
+
import type { Segment, Word } from './ast.js';
|
|
10
10
|
|
|
11
11
|
export interface ExpandContext {
|
|
12
12
|
fs: VirtualFileSystem;
|
|
@@ -49,12 +49,14 @@ async function paramValue(seg: Extract<Segment, { kind: 'var' }>, ctx: ExpandCon
|
|
|
49
49
|
|
|
50
50
|
const GLOB_CHARS = /[*?[]/;
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
/** A glob as a regular expression. `slash: true` lets `*`/`?` cross `/` (a `case` pattern, not a path). */
|
|
53
|
+
export function patternToRegExp(pattern: string, options: { slash?: boolean } = {}): RegExp {
|
|
54
|
+
const any = options.slash ? '.' : '[^/]';
|
|
53
55
|
let source = '';
|
|
54
56
|
for (let i = 0; i < pattern.length; i++) {
|
|
55
57
|
const c = pattern[i]!;
|
|
56
|
-
if (c === '*') source +=
|
|
57
|
-
else if (c === '?') source +=
|
|
58
|
+
if (c === '*') source += `${any}*`;
|
|
59
|
+
else if (c === '?') source += any;
|
|
58
60
|
else if (c === '[') {
|
|
59
61
|
const end = pattern.indexOf(']', i + 1);
|
|
60
62
|
if (end < 0) source += '\\[';
|
|
@@ -64,7 +66,7 @@ function globToRegExp(pattern: string): RegExp {
|
|
|
64
66
|
}
|
|
65
67
|
} else source += c.replace(/[.+^${}()|\\]/g, '\\$&');
|
|
66
68
|
}
|
|
67
|
-
return new RegExp(`^${source}
|
|
69
|
+
return new RegExp(`^${source}$`, 's');
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
export function glob(fs: VirtualFileSystem, cwd: string, pattern: string): string[] {
|
|
@@ -80,7 +82,7 @@ export function glob(fs: VirtualFileSystem, cwd: string, pattern: string): strin
|
|
|
80
82
|
if (fs.exists(candidate.startsWith('/') ? candidate : join(cwd, candidate))) next.push(candidate);
|
|
81
83
|
continue;
|
|
82
84
|
}
|
|
83
|
-
const re =
|
|
85
|
+
const re = patternToRegExp(part);
|
|
84
86
|
for (const name of fs.readdir(normalizePath(dir))) {
|
|
85
87
|
if (name.startsWith('.') && !part.startsWith('.')) continue;
|
|
86
88
|
if (re.test(name)) next.push(base ? (base.endsWith('/') ? base + name : `${base}/${name}`) : name);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading stdin one line at a time (`read`, `while read line`). A chunk can
|
|
3
|
+
* hold several lines, so what follows the first newline is kept per queue and
|
|
4
|
+
* served to the next read before any new chunk.
|
|
5
|
+
*/
|
|
6
|
+
import type { InputQueue } from '../system/terminal.js';
|
|
7
|
+
|
|
8
|
+
const leftovers = new WeakMap<InputQueue, string>();
|
|
9
|
+
const finished = new WeakSet<InputQueue>();
|
|
10
|
+
|
|
11
|
+
/** The next line without its newline, or `null` at end of input. */
|
|
12
|
+
export function readLine(queue: InputQueue, signal?: AbortSignal): Promise<string | null> {
|
|
13
|
+
let buffer = leftovers.get(queue) ?? '';
|
|
14
|
+
const take = (): string | null => {
|
|
15
|
+
const newline = buffer.indexOf('\n');
|
|
16
|
+
if (newline < 0) return null;
|
|
17
|
+
leftovers.set(queue, buffer.slice(newline + 1));
|
|
18
|
+
return buffer.slice(0, newline);
|
|
19
|
+
};
|
|
20
|
+
const ready = take();
|
|
21
|
+
if (ready !== null) return Promise.resolve(ready);
|
|
22
|
+
if (finished.has(queue)) {
|
|
23
|
+
leftovers.delete(queue);
|
|
24
|
+
return Promise.resolve(buffer ? buffer : null);
|
|
25
|
+
}
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
const settle = (line: string | null) => {
|
|
28
|
+
stop();
|
|
29
|
+
signal?.removeEventListener('abort', onAbort);
|
|
30
|
+
resolve(line);
|
|
31
|
+
};
|
|
32
|
+
const onAbort = () => settle(null);
|
|
33
|
+
signal?.addEventListener('abort', onAbort);
|
|
34
|
+
const stop = queue.subscribe((chunk) => {
|
|
35
|
+
if (chunk === null) {
|
|
36
|
+
finished.add(queue);
|
|
37
|
+
leftovers.delete(queue);
|
|
38
|
+
settle(buffer ? buffer : null);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
buffer += chunk;
|
|
42
|
+
const line = take();
|
|
43
|
+
if (line !== null) settle(line);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
package/src/shell/npm.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { join } from '../paths.js';
|
|
9
9
|
import { installPackages, type InstallOptions } from '../installer/install.js';
|
|
10
|
+
import { readNpmConfig } from '../installer/npmrc.js';
|
|
10
11
|
import type { PackageJson } from '../installer/spec.js';
|
|
11
12
|
import { binDirectories, findExecutable } from '../system/commands.js';
|
|
12
13
|
import { findPackageScope } from '../node/resolve.js';
|
|
@@ -34,10 +35,13 @@ async function install(ctx: CommandContext, options: Partial<InstallOptions>, di
|
|
|
34
35
|
const started = performance.now();
|
|
35
36
|
const progress = ctx.io.isTerminal;
|
|
36
37
|
try {
|
|
38
|
+
// User file first, then the project's: later files win, as with npm.
|
|
39
|
+
const npmrc = readNpmConfig(ctx.fs, [join(ctx.env.HOME ?? '/home/user', '.npmrc'), join(dir, '.npmrc')], ctx.env);
|
|
37
40
|
const result = await installPackages({
|
|
38
41
|
fs: ctx.fs,
|
|
39
42
|
fetch: (url, init) => ctx.system.fetch(url, init as RequestInit),
|
|
40
|
-
registry: ctx.env.npm_config_registry ?? ctx.system.registry,
|
|
43
|
+
registry: ctx.env.npm_config_registry ?? npmrc.registry ?? ctx.system.registry,
|
|
44
|
+
npmrc,
|
|
41
45
|
cache: ctx.system.packageCache,
|
|
42
46
|
cwd: dir,
|
|
43
47
|
signal: ctx.signal,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pnpm` and `yarn`, as front ends to the same installer and script runner
|
|
3
|
+
* `npm` uses: their commands are translated to npm's, so `pnpm add -D x`,
|
|
4
|
+
* `yarn dev` or `pnpm dlx create-vite` behave as a project's README expects.
|
|
5
|
+
* The tree is npm-style (hoisted `node_modules`, `package-lock.json`);
|
|
6
|
+
* `pnpm-lock.yaml` and `yarn.lock` are not read.
|
|
7
|
+
*/
|
|
8
|
+
import { join } from '../paths.js';
|
|
9
|
+
import { findPackageScope } from '../node/resolve.js';
|
|
10
|
+
import { findExecutable } from '../system/commands.js';
|
|
11
|
+
import type { CommandContext } from './context.js';
|
|
12
|
+
import type { JobInput, Shell } from './exec.js';
|
|
13
|
+
import { npmCommand, npxCommand } from './npm.js';
|
|
14
|
+
|
|
15
|
+
const VERSIONS = { pnpm: '9.12.0', yarn: '1.22.22' } as const;
|
|
16
|
+
|
|
17
|
+
type Manager = keyof typeof VERSIONS;
|
|
18
|
+
|
|
19
|
+
const INSTALL = new Set(['install', 'i']);
|
|
20
|
+
const REMOVE = new Set(['remove', 'rm', 'uninstall', 'un']);
|
|
21
|
+
const NPM_PASSTHROUGH = new Set(['test', 't', 'start', 'init', 'ls', 'list', 'run', 'run-script']);
|
|
22
|
+
|
|
23
|
+
function hasScript(ctx: CommandContext, name: string): boolean {
|
|
24
|
+
const dir = findPackageScope(ctx.fs, ctx.cwd)?.dir ?? ctx.cwd;
|
|
25
|
+
try {
|
|
26
|
+
const pkg = JSON.parse(ctx.fs.readText(join(dir, 'package.json')) ?? '{}') as { scripts?: Record<string, string> };
|
|
27
|
+
return !!pkg.scripts?.[name];
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Map `--dev` / `-D` to npm's flag; drop flags npm has no use for here. */
|
|
34
|
+
function addFlags(args: string[]): string[] {
|
|
35
|
+
return args.flatMap((a) => (a === '--dev' || a === '-D' || a === '--save-dev' ? ['--save-dev'] : a === '--frozen-lockfile' || a === '--immutable' || a === '--prefer-offline' ? [] : [a]));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The npm (or npx) command line a pnpm/yarn command stands for, or a message when there is none. */
|
|
39
|
+
export function translate(manager: Manager, args: string[], ctx: CommandContext): { tool: 'npm' | 'npx'; args: string[] } | { version: string } | { error: string } {
|
|
40
|
+
const [sub, ...rest] = args;
|
|
41
|
+
if (sub === undefined) return { tool: 'npm', args: ['install'] };
|
|
42
|
+
if (sub === '-v' || sub === '--version') return { version: VERSIONS[manager] };
|
|
43
|
+
if (INSTALL.has(sub)) {
|
|
44
|
+
// `yarn install x` is an error in yarn; treat any operands as `add` for both.
|
|
45
|
+
const flags = addFlags(rest);
|
|
46
|
+
return { tool: 'npm', args: ['install', ...flags] };
|
|
47
|
+
}
|
|
48
|
+
if (sub === 'add') return { tool: 'npm', args: ['install', ...addFlags(rest)] };
|
|
49
|
+
if (REMOVE.has(sub)) return { tool: 'npm', args: ['uninstall', ...rest] };
|
|
50
|
+
if (sub === 'dlx' || sub === 'exec' || (manager === 'pnpm' && sub === 'x')) return { tool: 'npx', args: rest };
|
|
51
|
+
if (NPM_PASSTHROUGH.has(sub)) return { tool: 'npm', args: [sub, ...rest] };
|
|
52
|
+
// `pnpm dev` / `yarn build`: a script of that name, else an installed bin.
|
|
53
|
+
if (hasScript(ctx, sub)) return { tool: 'npm', args: ['run', sub, ...(rest.length ? ['--', ...rest] : [])] };
|
|
54
|
+
if (findExecutable(ctx.fs, sub, ctx.cwd, { PATH: '' })) return { tool: 'npx', args: [sub, ...rest] };
|
|
55
|
+
return { error: `${manager}: no script or command named "${sub}"` };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function frontEnd(manager: Manager) {
|
|
59
|
+
return async (ctx: CommandContext, shell: Shell, input: JobInput): Promise<number> => {
|
|
60
|
+
const plan = translate(manager, ctx.args, ctx);
|
|
61
|
+
if ('version' in plan) {
|
|
62
|
+
ctx.io.stdout(`${plan.version}\n`);
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
if ('error' in plan) {
|
|
66
|
+
ctx.io.stderr(`${plan.error}\n`);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
const next = { ...ctx, args: plan.args, name: plan.tool };
|
|
70
|
+
return plan.tool === 'npm' ? npmCommand(next, shell, input) : npxCommand(next, shell, input);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const pnpmCommand = frontEnd('pnpm');
|
|
75
|
+
export const yarnCommand = frontEnd('yarn');
|