@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
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scripts injected into preview pages that let them reach virtual servers the
|
|
3
|
+
* way a page reaches a real one, where a service worker cannot help:
|
|
4
|
+
*
|
|
5
|
+
* • WebSockets. A service worker never sees a WebSocket, so the page's
|
|
6
|
+
* `WebSocket` is replaced by one that, for URLs naming a virtual server
|
|
7
|
+
* (`ws://localhost:3000/…`, `/__bfwc/<id>/__port/3000/…`, or a path on the
|
|
8
|
+
* page's own host from a page that server served), opens a MessageChannel
|
|
9
|
+
* to the runtime's host page (`parent`, else `opener`). The host connects
|
|
10
|
+
* to the server and relays frames both ways. Every other URL gets the real
|
|
11
|
+
* `WebSocket`, untouched.
|
|
12
|
+
* • `document.cookie` on pages a virtual server served. The host keeps those
|
|
13
|
+
* cookies (a service worker cannot set `Set-Cookie`); this mirrors the
|
|
14
|
+
* non-HttpOnly ones into `document.cookie` and sends assignments back.
|
|
15
|
+
*
|
|
16
|
+
* Messages on a socket's channel — page → host: `{ kind: 'send', data }`,
|
|
17
|
+
* `{ kind: 'close', code, reason }`; host → page: `{ kind: 'open', protocol }`,
|
|
18
|
+
* `{ kind: 'message', data }` (string or ArrayBuffer), `{ kind: 'close', code, reason, wasClean }`.
|
|
19
|
+
* On the cookie channel — page → host `{ kind: 'set-cookie', value }`, host → page `{ kind: 'cookies', value }`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** page → host, with a MessagePort: "connect a WebSocket to `port` at `path`". */
|
|
23
|
+
export const SOCKET_MESSAGE_TYPE = 'bfwc:socket';
|
|
24
|
+
/** page → host, with a MessagePort: "keep my `document.cookie` in sync" (pages served by a virtual server). */
|
|
25
|
+
export const COOKIE_MESSAGE_TYPE = 'bfwc:cookies';
|
|
26
|
+
/** How long a page waits for the host to answer a socket before failing it (code 1006). */
|
|
27
|
+
export const SOCKET_CONNECT_TIMEOUT_MS = 15_000;
|
|
28
|
+
|
|
29
|
+
/** The `WebSocket` replacement; runs in every preview page (dev-server documents and virtual-server HTML). */
|
|
30
|
+
export function previewSocketScript(): string {
|
|
31
|
+
return `<script>(function(){
|
|
32
|
+
var Native = window.WebSocket;
|
|
33
|
+
if (!Native || Native.__bfwc) return;
|
|
34
|
+
var runtime = window.parent !== window ? window.parent : window.opener;
|
|
35
|
+
var m = /^(.*\\/__bfwc\\/[A-Za-z0-9_-]+\\/)(?:__port\\/(\\d{1,5})(?:\\/|$))?/.exec(location.pathname);
|
|
36
|
+
if (!runtime || !m || typeof MessageChannel === 'undefined') return;
|
|
37
|
+
var base = m[1], ownPort = m[2] ? Number(m[2]) : 0;
|
|
38
|
+
var LOCAL = /^(localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|\\[::1\\])$/i;
|
|
39
|
+
function target(url) {
|
|
40
|
+
var u;
|
|
41
|
+
try { u = new URL(String(url), location.href); } catch (_) { return null; }
|
|
42
|
+
if (!/^(wss?|https?):$/.test(u.protocol)) return null;
|
|
43
|
+
if (u.host === location.host) {
|
|
44
|
+
if (u.pathname.indexOf(base) === 0) {
|
|
45
|
+
var p = /^__port\\/(\\d{1,5})(\\/.*)?$/.exec(u.pathname.slice(base.length));
|
|
46
|
+
return p ? { port: Number(p[1]), path: (p[2] || '/') + u.search } : null;
|
|
47
|
+
}
|
|
48
|
+
return ownPort ? { port: ownPort, path: u.pathname + u.search } : null;
|
|
49
|
+
}
|
|
50
|
+
if (LOCAL.test(u.hostname)) return { port: Number(u.port || (/^(wss|https):$/.test(u.protocol) ? 443 : 80)), path: u.pathname + u.search };
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
var CONNECTING = 0, OPEN = 1, CLOSING = 2, CLOSED = 3;
|
|
54
|
+
class VirtualWebSocket extends EventTarget {
|
|
55
|
+
constructor(url, protocols) {
|
|
56
|
+
var t = target(url);
|
|
57
|
+
if (!t) return protocols === undefined ? new Native(url) : new Native(url, protocols);
|
|
58
|
+
super();
|
|
59
|
+
var self = this, channel = new MessageChannel();
|
|
60
|
+
this._url = new URL(String(url), location.href).href.replace(/^http/, 'ws');
|
|
61
|
+
this._state = CONNECTING;
|
|
62
|
+
this._protocol = '';
|
|
63
|
+
this._binaryType = 'blob';
|
|
64
|
+
this._queue = Promise.resolve();
|
|
65
|
+
this._port = channel.port1;
|
|
66
|
+
this._port.onmessage = function (e) { self._receive(e.data); };
|
|
67
|
+
this._timer = setTimeout(function () { self._receive({ kind: 'close', code: 1006, reason: '', wasClean: false }); }, ${SOCKET_CONNECT_TIMEOUT_MS});
|
|
68
|
+
var list = protocols === undefined ? [] : typeof protocols === 'string' ? [protocols] : Array.from(protocols, String);
|
|
69
|
+
runtime.postMessage({ type: ${JSON.stringify(SOCKET_MESSAGE_TYPE)}, href: location.href, port: t.port, path: t.path, protocols: list }, '*', [channel.port2]);
|
|
70
|
+
}
|
|
71
|
+
get url() { return this._url; }
|
|
72
|
+
get readyState() { return this._state; }
|
|
73
|
+
get protocol() { return this._protocol; }
|
|
74
|
+
get extensions() { return ''; }
|
|
75
|
+
get bufferedAmount() { return 0; }
|
|
76
|
+
get binaryType() { return this._binaryType; }
|
|
77
|
+
set binaryType(v) { if (v === 'blob' || v === 'arraybuffer') this._binaryType = v; }
|
|
78
|
+
send(data) {
|
|
79
|
+
if (this._state === CONNECTING) throw new DOMException("Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.", 'InvalidStateError');
|
|
80
|
+
if (this._state !== OPEN) return;
|
|
81
|
+
var port = this._port;
|
|
82
|
+
var value = typeof data === 'string' ? data
|
|
83
|
+
: data instanceof Blob ? data.arrayBuffer()
|
|
84
|
+
: ArrayBuffer.isView(data) ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
|
|
85
|
+
: data instanceof ArrayBuffer ? data.slice(0) : String(data);
|
|
86
|
+
this._queue = this._queue.then(function () { return value; }).then(function (v) { port.postMessage({ kind: 'send', data: v }); });
|
|
87
|
+
}
|
|
88
|
+
close(code, reason) {
|
|
89
|
+
if (code !== undefined && code !== 1000 && (code < 3000 || code > 4999)) throw new DOMException("Failed to execute 'close' on 'WebSocket': The code must be either 1000, or between 3000 and 4999.", 'InvalidAccessError');
|
|
90
|
+
if (this._state === CLOSING || this._state === CLOSED) return;
|
|
91
|
+
this._state = CLOSING;
|
|
92
|
+
var port = this._port;
|
|
93
|
+
this._queue.then(function () { port.postMessage({ kind: 'close', code: code, reason: reason === undefined ? '' : String(reason) }); });
|
|
94
|
+
}
|
|
95
|
+
_receive(m) {
|
|
96
|
+
if (!m || this._state === CLOSED) return;
|
|
97
|
+
if (m.kind === 'open' && this._state === CONNECTING) {
|
|
98
|
+
clearTimeout(this._timer);
|
|
99
|
+
this._state = OPEN;
|
|
100
|
+
this._protocol = m.protocol || '';
|
|
101
|
+
this._emit(new Event('open'));
|
|
102
|
+
} else if (m.kind === 'message' && this._state !== CONNECTING) {
|
|
103
|
+
var d = m.data;
|
|
104
|
+
if (typeof d !== 'string' && this._binaryType === 'blob') d = new Blob([d]);
|
|
105
|
+
this._emit(new MessageEvent('message', { data: d, origin: new URL(this._url).origin }));
|
|
106
|
+
} else if (m.kind === 'close') {
|
|
107
|
+
clearTimeout(this._timer);
|
|
108
|
+
this._state = CLOSED;
|
|
109
|
+
this._port.close();
|
|
110
|
+
if (!m.wasClean) this._emit(new Event('error'));
|
|
111
|
+
this._emit(new CloseEvent('close', { code: m.code, reason: m.reason || '', wasClean: !!m.wasClean }));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
_emit(event) {
|
|
115
|
+
var handler = this['on' + event.type];
|
|
116
|
+
if (typeof handler === 'function') handler.call(this, event);
|
|
117
|
+
this.dispatchEvent(event);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
[['CONNECTING', CONNECTING], ['OPEN', OPEN], ['CLOSING', CLOSING], ['CLOSED', CLOSED]].forEach(function (c) {
|
|
121
|
+
Object.defineProperty(VirtualWebSocket, c[0], { value: c[1] });
|
|
122
|
+
Object.defineProperty(VirtualWebSocket.prototype, c[0], { value: c[1] });
|
|
123
|
+
});
|
|
124
|
+
Object.defineProperty(VirtualWebSocket, Symbol.hasInstance, { value: function (o) { return o instanceof Native || Function.prototype[Symbol.hasInstance].call(VirtualWebSocket, o); } });
|
|
125
|
+
VirtualWebSocket.__bfwc = true;
|
|
126
|
+
window.WebSocket = VirtualWebSocket;
|
|
127
|
+
})();</script>`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** `JSON.stringify`, safe inside an inline script. */
|
|
131
|
+
const scriptLiteral = (value: unknown) => JSON.stringify(value).replace(/</g, '\\u003c');
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* `document.cookie` for a page a virtual server served: starts from the host's
|
|
135
|
+
* jar (`initial`: the non-HttpOnly cookies for this page's path), stays in sync
|
|
136
|
+
* over a channel, and sends assignments to the host's jar.
|
|
137
|
+
*/
|
|
138
|
+
export function previewCookieScript(initial: string): string {
|
|
139
|
+
return `<script>(function(){
|
|
140
|
+
var runtime = window.parent !== window ? window.parent : window.opener;
|
|
141
|
+
var cookies = ${scriptLiteral(initial)};
|
|
142
|
+
var port = null;
|
|
143
|
+
if (runtime && typeof MessageChannel !== 'undefined') {
|
|
144
|
+
var channel = new MessageChannel();
|
|
145
|
+
port = channel.port1;
|
|
146
|
+
port.onmessage = function (e) { if (e.data && e.data.kind === 'cookies') cookies = String(e.data.value); };
|
|
147
|
+
runtime.postMessage({ type: ${JSON.stringify(COOKIE_MESSAGE_TYPE)}, href: location.href }, '*', [channel.port2]);
|
|
148
|
+
}
|
|
149
|
+
function apply(value) {
|
|
150
|
+
var parts = value.split(';'), pair = parts[0], eq = pair.indexOf('=');
|
|
151
|
+
if (eq < 0) return;
|
|
152
|
+
var name = pair.slice(0, eq).trim(), remove = false;
|
|
153
|
+
if (!name) return;
|
|
154
|
+
for (var i = 1; i < parts.length; i++) {
|
|
155
|
+
var a = parts[i].trim(), k = a.split('=')[0].trim().toLowerCase(), v = a.slice(a.indexOf('=') + 1).trim();
|
|
156
|
+
if (k === 'httponly') return;
|
|
157
|
+
if (k === 'max-age' && /^-?\\d+$/.test(v) && Number(v) <= 0) remove = true;
|
|
158
|
+
if (k === 'expires' && Date.parse(v) <= Date.now()) remove = true;
|
|
159
|
+
}
|
|
160
|
+
var list = cookies ? cookies.split('; ').filter(function (c) { return c.slice(0, c.indexOf('=')) !== name; }) : [];
|
|
161
|
+
if (!remove) list.push(name + '=' + pair.slice(eq + 1).trim());
|
|
162
|
+
cookies = list.join('; ');
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
Object.defineProperty(document, 'cookie', {
|
|
166
|
+
configurable: true,
|
|
167
|
+
get: function () { return cookies; },
|
|
168
|
+
set: function (value) { value = String(value); apply(value); if (port) port.postMessage({ kind: 'set-cookie', value: value }); },
|
|
169
|
+
});
|
|
170
|
+
} catch (_) {}
|
|
171
|
+
})();</script>`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const HEAD_OPEN = /<head(?:\s[^>]*)?>/i;
|
|
175
|
+
const HTML_OPEN = /<html(?:\s[^>]*)?>/i;
|
|
176
|
+
const DOCTYPE = /<!doctype[^>]*>/i;
|
|
177
|
+
/** Content past which `<head>` can no longer appear. */
|
|
178
|
+
const BODY_STARTED = /<(?:body|script|link|meta|style|title|div|p)[\s>/]/i;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Where to inject the bridge into a streamed document, from its first bytes:
|
|
182
|
+
* just after `<head …>`, else after `<html …>` or the doctype (never before the
|
|
183
|
+
* doctype, which would switch the page to quirks mode), else the start.
|
|
184
|
+
* Undefined while the prefix is too short to tell; pass `complete` at the end of the body.
|
|
185
|
+
*/
|
|
186
|
+
export function bridgeInsertionPoint(prefix: string, complete = false): number | undefined {
|
|
187
|
+
const head = HEAD_OPEN.exec(prefix);
|
|
188
|
+
if (head) return head.index + head[0].length;
|
|
189
|
+
if (!complete && !BODY_STARTED.test(prefix) && prefix.length < 4096) return undefined;
|
|
190
|
+
const html = HTML_OPEN.exec(prefix);
|
|
191
|
+
if (html) return html.index + html[0].length;
|
|
192
|
+
const doctype = DOCTYPE.exec(prefix);
|
|
193
|
+
return doctype ? doctype.index + doctype[0].length : 0;
|
|
194
|
+
}
|
package/src/shell/ast.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shell's syntax tree. Words keep their quoting as segments, because
|
|
3
|
+
* expansion (variables, `$(…)`, `~`, globs, field splitting) happens at run
|
|
4
|
+
* time and depends on it.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type Segment =
|
|
8
|
+
| { kind: 'text'; value: string; quoted: boolean }
|
|
9
|
+
| { kind: 'var'; name: string; quoted: boolean; op?: ':-' | '-' | ':=' | ':+'; arg?: Word }
|
|
10
|
+
| { kind: 'cmd'; source: string; quoted: boolean }
|
|
11
|
+
| { kind: 'tilde' };
|
|
12
|
+
export type Word = Segment[];
|
|
13
|
+
|
|
14
|
+
export type RedirectOp = '>' | '>>' | '<' | '2>' | '2>>' | '&>' | '2>&1' | '>&2' | '<<' | '<<-';
|
|
15
|
+
export interface Redirect {
|
|
16
|
+
op: RedirectOp;
|
|
17
|
+
target?: Word;
|
|
18
|
+
/** A here-document's body; expanded like a double-quoted string unless the delimiter was quoted. */
|
|
19
|
+
heredoc?: { body: Word };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SimpleCommand {
|
|
23
|
+
type: 'simple';
|
|
24
|
+
assignments: Array<{ name: string; value: Word }>;
|
|
25
|
+
words: Word[];
|
|
26
|
+
redirects: Redirect[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface IfCommand {
|
|
30
|
+
type: 'if';
|
|
31
|
+
clauses: Array<{ condition: Script; body: Script }>;
|
|
32
|
+
otherwise?: Script;
|
|
33
|
+
redirects: Redirect[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface LoopCommand {
|
|
37
|
+
type: 'while' | 'until';
|
|
38
|
+
condition: Script;
|
|
39
|
+
body: Script;
|
|
40
|
+
redirects: Redirect[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ForCommand {
|
|
44
|
+
type: 'for';
|
|
45
|
+
name: string;
|
|
46
|
+
/** Absent: iterate the positional parameters (`for x; do …`). */
|
|
47
|
+
items?: Word[];
|
|
48
|
+
body: Script;
|
|
49
|
+
redirects: Redirect[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CaseCommand {
|
|
53
|
+
type: 'case';
|
|
54
|
+
subject: Word;
|
|
55
|
+
arms: Array<{ patterns: Word[]; body: Script }>;
|
|
56
|
+
redirects: Redirect[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `{ list; }` runs in this shell; `( list )` in a copy of it. */
|
|
60
|
+
export interface GroupCommand {
|
|
61
|
+
type: 'group' | 'subshell';
|
|
62
|
+
body: Script;
|
|
63
|
+
redirects: Redirect[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface FunctionDefinition {
|
|
67
|
+
type: 'function';
|
|
68
|
+
name: string;
|
|
69
|
+
body: CompoundCommand;
|
|
70
|
+
redirects: Redirect[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type CompoundCommand = IfCommand | LoopCommand | ForCommand | CaseCommand | GroupCommand;
|
|
74
|
+
export type Command = SimpleCommand | CompoundCommand | FunctionDefinition;
|
|
75
|
+
|
|
76
|
+
export interface Pipeline {
|
|
77
|
+
negate: boolean;
|
|
78
|
+
commands: Command[];
|
|
79
|
+
}
|
|
80
|
+
export interface AndOrList {
|
|
81
|
+
first: Pipeline;
|
|
82
|
+
rest: Array<{ op: '&&' | '||'; pipeline: Pipeline }>;
|
|
83
|
+
background: boolean;
|
|
84
|
+
}
|
|
85
|
+
export type Script = AndOrList[];
|
package/src/shell/builtins.ts
CHANGED
|
@@ -8,6 +8,15 @@ import type { CommandContext, Command } from './context.js';
|
|
|
8
8
|
import { resolvePath } from './context.js';
|
|
9
9
|
import { cat, cp, ls, mkdir, mv, pwd, rm, touch } from './fileCommands.js';
|
|
10
10
|
import { basename, clear, dirname, echo, env, falseCommand, grep, head, printf, sleep, tail, trueCommand, wc } from './textCommands.js';
|
|
11
|
+
import { bracket, test } from './testCommand.js';
|
|
12
|
+
import { readLine } from './lineReader.js';
|
|
13
|
+
import type { Flow } from './compound.js';
|
|
14
|
+
|
|
15
|
+
/** A command started with `&`. */
|
|
16
|
+
export interface BackgroundJob {
|
|
17
|
+
id: number;
|
|
18
|
+
done: Promise<number>;
|
|
19
|
+
}
|
|
11
20
|
|
|
12
21
|
/** The slice of the shell a builtin may change. */
|
|
13
22
|
export interface ShellState {
|
|
@@ -17,6 +26,14 @@ export interface ShellState {
|
|
|
17
26
|
lastStatus: number;
|
|
18
27
|
/** Set by `exit`: the shell stops after the current command. */
|
|
19
28
|
exitRequested?: number;
|
|
29
|
+
/** Set by `break`, `continue` and `return`; the loop or function it targets clears it. */
|
|
30
|
+
flow?: Flow;
|
|
31
|
+
/** `$1…`, `$@`; a function call swaps them for its arguments. */
|
|
32
|
+
positional: string[];
|
|
33
|
+
readonly functions: ReadonlyMap<string, unknown>;
|
|
34
|
+
readonly jobs: ReadonlyMap<number, BackgroundJob>;
|
|
35
|
+
/** One frame per running function (for `local`). */
|
|
36
|
+
readonly localFrames: Array<Map<string, string | undefined>>;
|
|
20
37
|
/** Run a script in this shell (for `source`). */
|
|
21
38
|
run(source: string): Promise<number>;
|
|
22
39
|
}
|
|
@@ -25,21 +42,25 @@ export type Builtin = (ctx: CommandContext, shell: ShellState) => Promise<number
|
|
|
25
42
|
|
|
26
43
|
/** Coreutils: plain commands, also spawnable as standalone programs. */
|
|
27
44
|
export const COREUTILS: Record<string, Command> = {
|
|
28
|
-
ls, cat, mkdir, rm, cp, mv, touch, pwd, echo, printf, head, tail, grep, wc, env, clear, sleep, basename, dirname,
|
|
45
|
+
ls, cat, mkdir, rm, cp, mv, touch, pwd, echo, printf, head, tail, grep, wc, env, clear, sleep, basename, dirname, test,
|
|
46
|
+
'[': bracket,
|
|
29
47
|
true: trueCommand,
|
|
30
48
|
false: falseCommand,
|
|
31
49
|
};
|
|
32
50
|
|
|
33
51
|
/** Programs the runtime provides besides coreutils (reported by `which`/`type`). */
|
|
34
|
-
export const SYSTEM_PROGRAMS = ['node', 'npm', 'npx', 'bfsh', 'sh', 'bash'];
|
|
52
|
+
export const SYSTEM_PROGRAMS = ['node', 'npm', 'npx', 'pnpm', 'yarn', 'bfsh', 'sh', 'bash'];
|
|
35
53
|
|
|
36
54
|
const HELP = `bfsh — BuilderForce WebContainers shell
|
|
37
55
|
|
|
38
|
-
Builtins: cd, pwd, export, unset, exit, source, history, type, which, help
|
|
39
|
-
|
|
56
|
+
Builtins: cd, pwd, export, unset, exit, source, history, type, which, help,
|
|
57
|
+
read, local, shift, return, break, continue, jobs, wait
|
|
58
|
+
Files: ls [-la], cat, mkdir [-p], rm [-rf], cp [-r], mv, touch, test / [ … ]
|
|
40
59
|
Text: echo, printf, head, tail, grep, wc, env, clear, sleep, basename, dirname
|
|
41
|
-
Node: node <file>, npm install|ci|run|start|test, npx <bin
|
|
60
|
+
Node: node <file>, npm install|ci|run|start|test, npx <bin>, pnpm, yarn
|
|
42
61
|
Syntax: 'quotes' "\$VARS" a && b || c; a | b > out.txt >> log 2>&1 2> err; FOO=bar cmd; $(cmd); *.js
|
|
62
|
+
if …; then …; elif …; else …; fi while|until …; do …; done for x in …; do …; done
|
|
63
|
+
case $x in a|b) …;; *) …;; esac name() { …; } { …; } ( … ) cat <<EOF … EOF
|
|
43
64
|
|
|
44
65
|
`;
|
|
45
66
|
|
|
@@ -116,10 +137,11 @@ export const BUILTINS: Record<string, Builtin> = {
|
|
|
116
137
|
}
|
|
117
138
|
return status;
|
|
118
139
|
},
|
|
119
|
-
async type(ctx) {
|
|
140
|
+
async type(ctx, shell) {
|
|
120
141
|
let status = 0;
|
|
121
142
|
for (const name of ctx.args) {
|
|
122
|
-
if (name
|
|
143
|
+
if (shell.functions.has(name)) ctx.io.stdout(`${name} is a function\n`);
|
|
144
|
+
else if (name in BUILTINS) ctx.io.stdout(`${name} is a shell builtin\n`);
|
|
123
145
|
else if (name in COREUTILS || SYSTEM_PROGRAMS.includes(name)) ctx.io.stdout(`${name} is /usr/local/bin/${name}\n`);
|
|
124
146
|
else {
|
|
125
147
|
const file = findExecutable(ctx.fs, name, ctx.cwd, ctx.env);
|
|
@@ -135,7 +157,75 @@ export const BUILTINS: Record<string, Builtin> = {
|
|
|
135
157
|
async set() {
|
|
136
158
|
return 0; // options (-e, -x) are accepted and ignored
|
|
137
159
|
},
|
|
160
|
+
async break(ctx, shell) {
|
|
161
|
+
return loopControl(ctx, shell, 'break');
|
|
162
|
+
},
|
|
163
|
+
async continue(ctx, shell) {
|
|
164
|
+
return loopControl(ctx, shell, 'continue');
|
|
165
|
+
},
|
|
166
|
+
async return(ctx, shell) {
|
|
167
|
+
if (!shell.localFrames.length) {
|
|
168
|
+
ctx.io.stderr('return: can only `return\' from a function or sourced script\n');
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
const status = ctx.args[0] === undefined ? shell.lastStatus : Number(ctx.args[0]) & 255;
|
|
172
|
+
shell.flow = { kind: 'return', status };
|
|
173
|
+
return status;
|
|
174
|
+
},
|
|
175
|
+
async local(ctx, shell) {
|
|
176
|
+
const frame = shell.localFrames[shell.localFrames.length - 1];
|
|
177
|
+
if (!frame) {
|
|
178
|
+
ctx.io.stderr('local: can only be used in a function\n');
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
for (const arg of ctx.args) {
|
|
182
|
+
const eq = arg.indexOf('=');
|
|
183
|
+
const name = eq > 0 ? arg.slice(0, eq) : arg;
|
|
184
|
+
if (!frame.has(name)) frame.set(name, shell.env[name]);
|
|
185
|
+
shell.env[name] = eq > 0 ? arg.slice(eq + 1) : '';
|
|
186
|
+
}
|
|
187
|
+
return 0;
|
|
188
|
+
},
|
|
189
|
+
async shift(ctx, shell) {
|
|
190
|
+
const count = ctx.args[0] === undefined ? 1 : Number(ctx.args[0]);
|
|
191
|
+
if (!Number.isInteger(count) || count < 0 || count > shell.positional.length) return 1;
|
|
192
|
+
shell.positional = shell.positional.slice(count);
|
|
193
|
+
return 0;
|
|
194
|
+
},
|
|
195
|
+
async read(ctx, shell) {
|
|
196
|
+
const raw = ctx.args.includes('-r');
|
|
197
|
+
const names = ctx.args.filter((a) => !a.startsWith('-'));
|
|
198
|
+
let line = await readLine(ctx.io.stdin, ctx.signal);
|
|
199
|
+
if (line === null) return 1;
|
|
200
|
+
if (!raw) line = line.replace(/\\(.)/g, '$1');
|
|
201
|
+
const targets = names.length ? names : ['REPLY'];
|
|
202
|
+
const fields = names.length ? line.trim().split(/\s+/) : [line];
|
|
203
|
+
targets.forEach((name, i) => {
|
|
204
|
+
shell.env[name] = i === targets.length - 1 ? fields.slice(i).join(' ') : fields[i] ?? '';
|
|
205
|
+
});
|
|
206
|
+
return 0;
|
|
207
|
+
},
|
|
208
|
+
async jobs(ctx, shell) {
|
|
209
|
+
for (const job of shell.jobs.values()) ctx.io.stdout(`[${job.id}] Running\n`);
|
|
210
|
+
return 0;
|
|
211
|
+
},
|
|
212
|
+
async wait(ctx, shell) {
|
|
213
|
+
const ids = ctx.args.map((a) => Number(a.replace(/^%/, '')));
|
|
214
|
+
const jobs = ids.length ? ids.map((id) => shell.jobs.get(id)).filter((j): j is BackgroundJob => !!j) : [...shell.jobs.values()];
|
|
215
|
+
const statuses = await Promise.all(jobs.map((job) => job.done));
|
|
216
|
+
return statuses.length ? statuses[statuses.length - 1]! : 0;
|
|
217
|
+
},
|
|
138
218
|
};
|
|
219
|
+
|
|
220
|
+
function loopControl(ctx: CommandContext, shell: ShellState, kind: 'break' | 'continue'): number {
|
|
221
|
+
const levels = ctx.args[0] === undefined ? 1 : Number(ctx.args[0]);
|
|
222
|
+
if (!Number.isInteger(levels) || levels < 1) {
|
|
223
|
+
ctx.io.stderr(`${kind}: ${ctx.args[0]}: loop count out of range\n`);
|
|
224
|
+
return 1;
|
|
225
|
+
}
|
|
226
|
+
shell.flow = { kind, levels };
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
139
229
|
BUILTINS['.'] = BUILTINS.source!;
|
|
140
230
|
BUILTINS[':'] = async () => 0;
|
|
141
231
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running compound commands (`if`, `while`/`until`, `for`, `case`, `{ }`,
|
|
3
|
+
* `( )`) against a narrow {@link ScriptRunner}, so the executor stays a
|
|
4
|
+
* dispatcher. Loop control (`break`, `continue`) and `return` travel as a
|
|
5
|
+
* {@link Flow} the runner holds: lists stop running while one is set, and the
|
|
6
|
+
* construct it targets clears it.
|
|
7
|
+
*/
|
|
8
|
+
import type { CompoundCommand, Script, Word } from './ast.js';
|
|
9
|
+
import type { JobInput, ShellIO } from './exec.js';
|
|
10
|
+
import { patternToRegExp } from './expand.js';
|
|
11
|
+
|
|
12
|
+
export type Flow = { kind: 'break' | 'continue'; levels: number } | { kind: 'return'; status: number };
|
|
13
|
+
|
|
14
|
+
export interface ScriptRunner {
|
|
15
|
+
flow?: Flow;
|
|
16
|
+
readonly positional: readonly string[];
|
|
17
|
+
runScript(script: Script, io: ShellIO, input: JobInput, signal: AbortSignal): Promise<number>;
|
|
18
|
+
/** Words with splitting and globbing, as command arguments. */
|
|
19
|
+
expandWords(words: Word[], io: ShellIO, signal: AbortSignal): Promise<string[]>;
|
|
20
|
+
/** One word as a single string: no splitting, no globbing. */
|
|
21
|
+
expandText(word: Word, io: ShellIO, signal: AbortSignal): Promise<string>;
|
|
22
|
+
setVar(name: string, value: string): void;
|
|
23
|
+
/** A copy of this shell for `( … )`: changes inside do not leak out. */
|
|
24
|
+
subshell(): ScriptRunner;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** After one pass of a loop body: whether the loop goes on. Consumes the flow aimed at this loop. */
|
|
28
|
+
function continueLoop(runner: ScriptRunner): boolean {
|
|
29
|
+
const flow = runner.flow;
|
|
30
|
+
if (!flow) return true;
|
|
31
|
+
if (flow.kind === 'return') return false;
|
|
32
|
+
if (flow.levels > 1) {
|
|
33
|
+
runner.flow = { kind: flow.kind, levels: flow.levels - 1 };
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
runner.flow = undefined;
|
|
37
|
+
return flow.kind === 'continue';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function runCompound(node: CompoundCommand, runner: ScriptRunner, io: ShellIO, input: JobInput, signal: AbortSignal): Promise<number> {
|
|
41
|
+
switch (node.type) {
|
|
42
|
+
case 'group':
|
|
43
|
+
return runner.runScript(node.body, io, input, signal);
|
|
44
|
+
case 'subshell':
|
|
45
|
+
return runner.subshell().runScript(node.body, io, input, signal);
|
|
46
|
+
case 'if': {
|
|
47
|
+
for (const clause of node.clauses) {
|
|
48
|
+
const condition = await runner.runScript(clause.condition, io, input, signal);
|
|
49
|
+
if (runner.flow) return condition;
|
|
50
|
+
if (condition === 0) return runner.runScript(clause.body, io, input, signal);
|
|
51
|
+
}
|
|
52
|
+
return node.otherwise ? runner.runScript(node.otherwise, io, input, signal) : 0;
|
|
53
|
+
}
|
|
54
|
+
case 'while':
|
|
55
|
+
case 'until': {
|
|
56
|
+
let status = 0;
|
|
57
|
+
while (!signal.aborted) {
|
|
58
|
+
const condition = await runner.runScript(node.condition, io, input, signal);
|
|
59
|
+
if (runner.flow && !continueLoop(runner)) break;
|
|
60
|
+
if ((condition === 0) !== (node.type === 'while')) break;
|
|
61
|
+
status = await runner.runScript(node.body, io, input, signal);
|
|
62
|
+
if (!continueLoop(runner)) break;
|
|
63
|
+
}
|
|
64
|
+
return status;
|
|
65
|
+
}
|
|
66
|
+
case 'for': {
|
|
67
|
+
const items = node.items ? await runner.expandWords(node.items, io, signal) : [...runner.positional];
|
|
68
|
+
let status = 0;
|
|
69
|
+
for (const item of items) {
|
|
70
|
+
if (signal.aborted) break;
|
|
71
|
+
runner.setVar(node.name, item);
|
|
72
|
+
status = await runner.runScript(node.body, io, input, signal);
|
|
73
|
+
if (!continueLoop(runner)) break;
|
|
74
|
+
}
|
|
75
|
+
return status;
|
|
76
|
+
}
|
|
77
|
+
case 'case': {
|
|
78
|
+
const subject = await runner.expandText(node.subject, io, signal);
|
|
79
|
+
for (const arm of node.arms) {
|
|
80
|
+
for (const pattern of arm.patterns) {
|
|
81
|
+
if (patternToRegExp(await runner.expandText(pattern, io, signal), { slash: true }).test(subject)) {
|
|
82
|
+
return runner.runScript(arm.body, io, input, signal);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|