@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/dist/vfs.js
CHANGED
|
@@ -71,6 +71,43 @@ export class VirtualFileSystem {
|
|
|
71
71
|
readdir(path) {
|
|
72
72
|
return [...(this.children.get(normalizePath(path)) ?? [])].sort();
|
|
73
73
|
}
|
|
74
|
+
/** Direct children with their kind, sorted by name — `readdir` for callers that render a tree. */
|
|
75
|
+
entries(path) {
|
|
76
|
+
const dir = normalizePath(path);
|
|
77
|
+
return this.readdir(dir).map((name) => ({ name, kind: this.children.has(join(dir, name)) ? 'directory' : 'file' }));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Move a file or a directory (with everything under it). Throws when `from` is
|
|
81
|
+
* missing or `to` already exists. Watchers see the old paths removed and the
|
|
82
|
+
* new ones written, which is what every consumer (dev server, process sync) needs.
|
|
83
|
+
*/
|
|
84
|
+
rename(from, to) {
|
|
85
|
+
const source = normalizePath(from);
|
|
86
|
+
const target = normalizePath(to);
|
|
87
|
+
if (source === target)
|
|
88
|
+
return;
|
|
89
|
+
if (!this.exists(source))
|
|
90
|
+
throw new Error(`ENOENT: no such file or directory, rename '${source}' -> '${target}'`);
|
|
91
|
+
if (this.exists(target))
|
|
92
|
+
throw new Error(`EEXIST: file already exists, rename '${source}' -> '${target}'`);
|
|
93
|
+
if (target.startsWith(source + '/'))
|
|
94
|
+
throw new Error(`EINVAL: cannot move '${source}' into itself`);
|
|
95
|
+
if (this.files.has(source)) {
|
|
96
|
+
const contents = this.files.get(source);
|
|
97
|
+
this.removeFile(source);
|
|
98
|
+
this.writeFile(target, contents);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const prefix = source + '/';
|
|
102
|
+
const moved = [...this.files].filter(([path]) => path.startsWith(prefix));
|
|
103
|
+
const dirs = [...this.explicit].filter((dir) => dir === source || dir.startsWith(prefix));
|
|
104
|
+
this.rm(source);
|
|
105
|
+
this.mkdir(target);
|
|
106
|
+
for (const dir of dirs)
|
|
107
|
+
this.mkdir(target + dir.slice(source.length));
|
|
108
|
+
for (const [path, contents] of moved)
|
|
109
|
+
this.writeFile(target + path.slice(source.length), contents);
|
|
110
|
+
}
|
|
74
111
|
/** Every file path, sorted. */
|
|
75
112
|
list() {
|
|
76
113
|
return [...this.files.keys()].sort();
|
|
@@ -125,9 +162,14 @@ export class VirtualFileSystem {
|
|
|
125
162
|
out[path.slice(1)] = contents;
|
|
126
163
|
return out;
|
|
127
164
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
165
|
+
/** Every change, or with `under` only changes at or below that path. */
|
|
166
|
+
watch(listener, options = {}) {
|
|
167
|
+
const root = options.under === undefined ? undefined : normalizePath(options.under);
|
|
168
|
+
const filtered = root === undefined || root === '/'
|
|
169
|
+
? listener
|
|
170
|
+
: (change) => (change.path === root || change.path.startsWith(root + '/')) && listener(change);
|
|
171
|
+
this.listeners.add(filtered);
|
|
172
|
+
return () => this.listeners.delete(filtered);
|
|
131
173
|
}
|
|
132
174
|
removeFile(key) {
|
|
133
175
|
this.files.delete(key);
|
package/dist/vfs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vfs.js","sourceRoot":"","sources":["../src/vfs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"vfs.js","sourceRoot":"","sources":["../src/vfs.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seanhogg/builderforce-webcontainers-core",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.8",
|
|
4
4
|
"description": "BuilderForce WebContainers — core. A runtime-agnostic in-browser dev server: virtual file system, module resolution, import rewriting to a package CDN, CSS/JSON/asset modules and project detection. No COOP/COEP required.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
package/src/archive.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The file system as a zip: one format for "Download project" and for a
|
|
3
|
+
* snapshot a host stores and restores later (with `node_modules`, so a
|
|
4
|
+
* restored project needs no reinstall). Empty directories created with `mkdir`
|
|
5
|
+
* travel as `name/` entries; text comes back as strings, everything else as bytes.
|
|
6
|
+
*/
|
|
7
|
+
import { unzipSync, zipSync, type Zippable } from 'fflate';
|
|
8
|
+
import { VirtualFileSystem } from './vfs.js';
|
|
9
|
+
import { join } from './paths.js';
|
|
10
|
+
|
|
11
|
+
export interface ZipExportOptions {
|
|
12
|
+
/** Only these path prefixes (`node_modules`), relative to the root, no leading slash. Default: everything. */
|
|
13
|
+
include?: string[];
|
|
14
|
+
/** Path prefixes left out (`node_modules`, `dist`), relative to the root, no leading slash. */
|
|
15
|
+
exclude?: string[];
|
|
16
|
+
/** 0 (store, fastest) to 9. Default 6. */
|
|
17
|
+
level?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const encoder = new TextEncoder();
|
|
21
|
+
const strictUtf8 = new TextDecoder('utf-8', { fatal: true });
|
|
22
|
+
|
|
23
|
+
function under(relative: string, prefixes: string[]): boolean {
|
|
24
|
+
return prefixes.some((prefix) => {
|
|
25
|
+
const clean = prefix.replace(/^\/+|\/+$/g, '');
|
|
26
|
+
return relative === clean || relative.startsWith(clean + '/');
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function exportZip(fs: VirtualFileSystem, options: ZipExportOptions = {}): Uint8Array {
|
|
31
|
+
const exclude = options.exclude ?? [];
|
|
32
|
+
const include = options.include;
|
|
33
|
+
const excluded = (relative: string) => (include !== undefined && !under(relative, include)) || under(relative, exclude);
|
|
34
|
+
const entries: Zippable = {};
|
|
35
|
+
for (const dir of fs.explicitDirectories()) {
|
|
36
|
+
const relative = dir.slice(1);
|
|
37
|
+
if (!excluded(relative) && fs.readdir(dir).length === 0) entries[relative + '/'] = new Uint8Array(0);
|
|
38
|
+
}
|
|
39
|
+
for (const path of fs.list()) {
|
|
40
|
+
const relative = path.slice(1);
|
|
41
|
+
if (excluded(relative)) continue;
|
|
42
|
+
const contents = fs.readFile(path)!;
|
|
43
|
+
entries[relative] = typeof contents === 'string' ? encoder.encode(contents) : contents;
|
|
44
|
+
}
|
|
45
|
+
return zipSync(entries, { level: options.level ?? 6 });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Write a zip's contents into `fs` under `at` (default `/`), over whatever is there. */
|
|
49
|
+
export function importZip(fs: VirtualFileSystem, zip: Uint8Array, at = '/'): void {
|
|
50
|
+
for (const [name, bytes] of Object.entries(unzipSync(zip))) {
|
|
51
|
+
if (name.endsWith('/')) {
|
|
52
|
+
fs.mkdir(join(at, name.slice(0, -1)));
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
let contents: string | Uint8Array = bytes;
|
|
56
|
+
try {
|
|
57
|
+
contents = strictUtf8.decode(bytes);
|
|
58
|
+
} catch {
|
|
59
|
+
// binary: keep the bytes
|
|
60
|
+
}
|
|
61
|
+
fs.writeFile(join(at, name), contents);
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/devServer.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface DevServerOptions {
|
|
|
30
30
|
components?: ComponentCompilers;
|
|
31
31
|
/** The "Built with Builderforce.ai" badge in served pages. Default true. */
|
|
32
32
|
attribution?: boolean;
|
|
33
|
+
/** Host scripts (JavaScript source) run in every preview page before the app. */
|
|
34
|
+
previewScripts?: readonly string[];
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const NO_STORE = { 'cache-control': 'no-store' };
|
|
@@ -81,9 +83,11 @@ export class DevServer {
|
|
|
81
83
|
private profileCache?: ProjectProfile;
|
|
82
84
|
private readonly cache = new Map<string, { version: number; file: ServedFile }>();
|
|
83
85
|
private readonly unwatch: () => void;
|
|
86
|
+
private previewScripts: readonly string[];
|
|
84
87
|
|
|
85
88
|
constructor(private readonly options: DevServerOptions) {
|
|
86
89
|
if (!options.base.endsWith('/')) throw new Error('DevServer base must end with "/"');
|
|
90
|
+
this.previewScripts = options.previewScripts ?? [];
|
|
87
91
|
this.unwatch = options.fs.watch((change) => this.onChange(change));
|
|
88
92
|
}
|
|
89
93
|
|
|
@@ -92,6 +96,12 @@ export class DevServer {
|
|
|
92
96
|
this.cache.clear();
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
/** Replace the host scripts injected into preview pages; takes effect on the next page load. */
|
|
100
|
+
setPreviewScripts(scripts: readonly string[]): void {
|
|
101
|
+
this.previewScripts = [...scripts];
|
|
102
|
+
for (const key of [...this.cache.keys()]) if (/\.html?\?/.test(key)) this.cache.delete(key);
|
|
103
|
+
}
|
|
104
|
+
|
|
95
105
|
profile(): ProjectProfile {
|
|
96
106
|
return (this.profileCache ??= detectProject(this.options.fs));
|
|
97
107
|
}
|
|
@@ -136,7 +146,7 @@ export class DevServer {
|
|
|
136
146
|
if (loaderFor(ext)) return reply(200, JS_MIME, await compileScript(await this.context(), file));
|
|
137
147
|
if (isScriptPath(file)) return reply(200, JS_MIME, await compileComponent(await this.context(), file));
|
|
138
148
|
if (ext === '.html' || ext === '.htm') {
|
|
139
|
-
return reply(200, mimeFor(ext), transformHtml(fs.readText(file) ?? '', { base, attribution: this.options.attribution }));
|
|
149
|
+
return reply(200, mimeFor(ext), transformHtml(fs.readText(file) ?? '', { base, attribution: this.options.attribution, scripts: this.previewScripts }));
|
|
140
150
|
}
|
|
141
151
|
return reply(200, mimeFor(ext), fs.readFile(file) ?? '');
|
|
142
152
|
} catch (error) {
|
|
@@ -151,7 +161,7 @@ export class DevServer {
|
|
|
151
161
|
}
|
|
152
162
|
const html = this.options.fs.readText(profile.htmlPath) ?? '';
|
|
153
163
|
const { base, attribution } = this.options;
|
|
154
|
-
return reply(200, 'text/html; charset=utf-8', transformHtml(html, { base, entry: profile.entry, attribution }));
|
|
164
|
+
return reply(200, 'text/html; charset=utf-8', transformHtml(html, { base, entry: profile.entry, attribution, scripts: this.previewScripts }));
|
|
155
165
|
}
|
|
156
166
|
|
|
157
167
|
private async context(): Promise<CompileContext> {
|
package/src/html.ts
CHANGED
|
@@ -4,18 +4,26 @@
|
|
|
4
4
|
* base, since the preview is served from a sub-path, not the origin root;
|
|
5
5
|
* • CRA's `%PUBLIC_URL%` placeholder is filled;
|
|
6
6
|
* • a page with no module script (CRA) gets its entry injected;
|
|
7
|
-
* • the error bridge is prepended, so runtime errors reach the host;
|
|
7
|
+
* • the error bridge is prepended, so runtime errors and console output reach the host;
|
|
8
|
+
* • the WebSocket bridge follows, so `new WebSocket('ws://localhost:3000')` reaches a virtual server;
|
|
9
|
+
* • the host's preview scripts run next, before the app;
|
|
8
10
|
* • the live-reload listener is prepended, so an edit re-renders the preview;
|
|
9
11
|
* • the attribution badge is appended unless the host turned it off.
|
|
10
12
|
*/
|
|
11
13
|
import { injectAttribution } from './attribution.js';
|
|
14
|
+
import { previewSocketScript } from './previewBridge.js';
|
|
12
15
|
|
|
13
16
|
export const ERROR_MESSAGE_TYPE = 'bfwc:error';
|
|
17
|
+
export const CONSOLE_MESSAGE_TYPE = 'bfwc:console';
|
|
18
|
+
/** Console messages forwarded per page load; past this the bridge goes quiet (a log loop must not flood the host). */
|
|
19
|
+
export const CONSOLE_MESSAGE_LIMIT = 1000;
|
|
14
20
|
|
|
15
21
|
/**
|
|
16
|
-
* Runs first in the preview. Forwards uncaught errors and rejections
|
|
17
|
-
* page (`window.parent`)
|
|
18
|
-
*
|
|
22
|
+
* Runs first in the preview. Forwards uncaught errors and rejections, and the
|
|
23
|
+
* app's console output, to the host page (`window.parent`): how an editor — or
|
|
24
|
+
* an agent — learns what the app did without scraping the iframe. Console
|
|
25
|
+
* arguments are flattened to text (errors with their stack, objects as JSON),
|
|
26
|
+
* each message capped at 4,000 characters.
|
|
19
27
|
*/
|
|
20
28
|
export function errorBridgeScript(): string {
|
|
21
29
|
return `<script>(function(){
|
|
@@ -24,6 +32,22 @@ export function errorBridgeScript(): string {
|
|
|
24
32
|
};
|
|
25
33
|
addEventListener('error', function (e) { send(e.message, e.error && e.error.stack); });
|
|
26
34
|
addEventListener('unhandledrejection', function (e) { var r = e.reason; send(r && r.message ? r.message : r, r && r.stack); });
|
|
35
|
+
var sent = 0;
|
|
36
|
+
var text = function (value) {
|
|
37
|
+
if (typeof value === 'string') return value;
|
|
38
|
+
if (value instanceof Error) return value.stack || String(value);
|
|
39
|
+
try { var json = JSON.stringify(value); return json === undefined ? String(value) : json; } catch (_) { return String(value); }
|
|
40
|
+
};
|
|
41
|
+
['log', 'info', 'warn', 'error', 'debug'].forEach(function (level) {
|
|
42
|
+
var original = console[level];
|
|
43
|
+
console[level] = function () {
|
|
44
|
+
if (sent < ${CONSOLE_MESSAGE_LIMIT}) {
|
|
45
|
+
sent++;
|
|
46
|
+
try { parent.postMessage({ type: ${JSON.stringify(CONSOLE_MESSAGE_TYPE)}, level: level, text: Array.prototype.map.call(arguments, text).join(' ').slice(0, 4000), href: location.href }, '*'); } catch (_) {}
|
|
47
|
+
}
|
|
48
|
+
return original.apply(console, arguments);
|
|
49
|
+
};
|
|
50
|
+
});
|
|
27
51
|
window.process = window.process || { env: { NODE_ENV: 'development' } };
|
|
28
52
|
})();</script>`;
|
|
29
53
|
}
|
|
@@ -55,8 +79,13 @@ export interface HtmlOptions {
|
|
|
55
79
|
entry?: string;
|
|
56
80
|
/** The "Built with Builderforce.ai" badge. On unless explicitly `false`. */
|
|
57
81
|
attribution?: boolean;
|
|
82
|
+
/** Host scripts (JavaScript source) run in every preview page before the app. */
|
|
83
|
+
scripts?: readonly string[];
|
|
58
84
|
}
|
|
59
85
|
|
|
86
|
+
/** A host script as a tag. `</script>` inside the body would end the tag early, so it is escaped. */
|
|
87
|
+
export const scriptTag = (source: string): string => `<script>${source.replace(/<\/script/gi, '<\\/script')}</script>`;
|
|
88
|
+
|
|
60
89
|
export function transformHtml(html: string, options: HtmlOptions): string {
|
|
61
90
|
const { base } = options;
|
|
62
91
|
// `%PUBLIC_URL%/logo.png` becomes root-absolute here and is re-rooted once below;
|
|
@@ -70,7 +99,7 @@ export function transformHtml(html: string, options: HtmlOptions): string {
|
|
|
70
99
|
}
|
|
71
100
|
if (options.attribution !== false) out = injectAttribution(out);
|
|
72
101
|
|
|
73
|
-
const bridge =
|
|
102
|
+
const bridge = [errorBridgeScript(), previewSocketScript(), liveReloadScript(base), ...(options.scripts ?? []).map(scriptTag)].join('\n');
|
|
74
103
|
if (/<head[^>]*>/i.test(out)) return out.replace(/<head[^>]*>/i, (head) => `${head}\n${bridge}`);
|
|
75
104
|
return bridge + out;
|
|
76
105
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { VirtualFileSystem } from './vfs.js';
|
|
2
2
|
export type { FileContent, FileTree, FlatFiles, FsChange } from './vfs.js';
|
|
3
|
+
export type { DirectoryEntry } from './vfs.js';
|
|
4
|
+
export { exportZip, importZip } from './archive.js';
|
|
5
|
+
export type { ZipExportOptions } from './archive.js';
|
|
3
6
|
export { normalizePath, dirname, basename, join, extname } from './paths.js';
|
|
4
7
|
export { DevServer } from './devServer.js';
|
|
5
8
|
export type { DevServerOptions, ServedFile } from './devServer.js';
|
|
@@ -41,7 +44,8 @@ export { rewriteImports } from './rewriteImports.js';
|
|
|
41
44
|
export { resolveLocal, resolveAlias, isBareSpecifier, splitPackageSpecifier, resolvePackage, RESOLVE_EXTENSIONS } from './resolve.js';
|
|
42
45
|
export { compileScript, compileModule, isScriptPath, mapSpecifier, moduleUrl, buildDefine } from './compileScript.js';
|
|
43
46
|
export { compileComponent } from './compileComponent.js';
|
|
44
|
-
export { transformHtml, errorBridgeScript, liveReloadScript, reloadChannelName, ERROR_MESSAGE_TYPE } from './html.js';
|
|
47
|
+
export { transformHtml, scriptTag, errorBridgeScript, liveReloadScript, reloadChannelName, ERROR_MESSAGE_TYPE, CONSOLE_MESSAGE_TYPE, CONSOLE_MESSAGE_LIMIT } from './html.js';
|
|
48
|
+
export { previewSocketScript, previewCookieScript, bridgeInsertionPoint, SOCKET_MESSAGE_TYPE, COOKIE_MESSAGE_TYPE, SOCKET_CONNECT_TIMEOUT_MS } from './previewBridge.js';
|
|
45
49
|
export { mimeFor } from './mime.js';
|
|
46
50
|
export { attributionScript, injectAttribution, ATTRIBUTION_URL, ATTRIBUTION_LABEL } from './attribution.js';
|
|
47
51
|
export type { AttributionOptions } from './attribution.js';
|
package/src/installer/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { installPackages, chooseVersion } from './install.js';
|
|
2
2
|
export type { InstallOptions, InstallResult, InstallProgress } from './install.js';
|
|
3
|
+
export { parseNpmrc, readNpmConfig, registryFor, authHeaders, emptyNpmConfig } from './npmrc.js';
|
|
4
|
+
export type { NpmConfig } from './npmrc.js';
|
|
3
5
|
export { RegistryClient, createMemoryCache, verifyIntegrity, DEFAULT_REGISTRY } from './registry.js';
|
|
4
6
|
export type { FetchLike, PackageCache, PackageManifest, Packument, RegistryOptions, BinField } from './registry.js';
|
|
5
7
|
export { resolveTree, childLocation, parentLocation, PLATFORM } from './tree.js';
|
package/src/installer/install.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { gunzipSync } from 'fflate';
|
|
|
13
13
|
import type { VirtualFileSystem } from '../vfs.js';
|
|
14
14
|
import { join } from '../paths.js';
|
|
15
15
|
import { RegistryClient, type FetchLike, type PackageCache, type PackageManifest, type Packument } from './registry.js';
|
|
16
|
+
import type { NpmConfig } from './npmrc.js';
|
|
16
17
|
import { parseInstallSpec, sortObject, type PackageJson, type ParsedSpec } from './spec.js';
|
|
17
18
|
import { childLocation, parentLocation, resolveTree, type ResolvedPackage } from './tree.js';
|
|
18
19
|
import { buildLockfile, lockMatchesPackage, lockedManifests, parseLockfile, pickLocked, type Lockfile } from './lockfile.js';
|
|
@@ -32,6 +33,8 @@ export interface InstallOptions {
|
|
|
32
33
|
fs: VirtualFileSystem;
|
|
33
34
|
fetch: FetchLike;
|
|
34
35
|
registry?: string;
|
|
36
|
+
/** `.npmrc` settings: scoped registries and auth tokens. */
|
|
37
|
+
npmrc?: NpmConfig;
|
|
35
38
|
cache?: PackageCache;
|
|
36
39
|
/** The project directory holding package.json. Default `/`. */
|
|
37
40
|
cwd?: string;
|
|
@@ -158,7 +161,7 @@ export async function installPackages(options: InstallOptions): Promise<InstallR
|
|
|
158
161
|
const pkgPath = join(base, 'package.json');
|
|
159
162
|
const lockPath = join(base, 'package-lock.json');
|
|
160
163
|
const warnings: string[] = [];
|
|
161
|
-
const client = new RegistryClient({ fetch: options.fetch, registry: options.registry, cache: options.cache, signal });
|
|
164
|
+
const client = new RegistryClient({ fetch: options.fetch, registry: options.registry ?? options.npmrc?.registry, npmrc: options.npmrc, cache: options.cache, signal });
|
|
162
165
|
const progress = (p: InstallProgress) => options.onProgress?.(p);
|
|
163
166
|
|
|
164
167
|
let pkg = readJson<PackageJson>(fs, pkgPath);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.npmrc`: the default registry, per-scope registries (`@acme:registry=…`) and
|
|
3
|
+
* auth tokens keyed by registry path (`//npm.acme.dev/:_authToken=${NPM_TOKEN}`).
|
|
4
|
+
* Project and user files are read like npm does — the project's wins — and
|
|
5
|
+
* `${VAR}` is filled from the process environment.
|
|
6
|
+
*
|
|
7
|
+
* A private registry is reached with the browser's `fetch`, so it must answer
|
|
8
|
+
* CORS requests that carry an `Authorization` header.
|
|
9
|
+
*/
|
|
10
|
+
import type { VirtualFileSystem } from '../vfs.js';
|
|
11
|
+
|
|
12
|
+
export interface NpmConfig {
|
|
13
|
+
registry?: string;
|
|
14
|
+
/** `@scope` → registry URL. */
|
|
15
|
+
scopes: Record<string, string>;
|
|
16
|
+
/** `//host/path/` → bearer token. */
|
|
17
|
+
tokens: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function emptyNpmConfig(): NpmConfig {
|
|
21
|
+
return { scopes: {}, tokens: {} };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const trimSlash = (url: string) => url.replace(/\/+$/, '');
|
|
25
|
+
|
|
26
|
+
export function parseNpmrc(text: string, env: Record<string, string | undefined> = {}, into: NpmConfig = emptyNpmConfig()): NpmConfig {
|
|
27
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
28
|
+
const line = raw.trim();
|
|
29
|
+
if (!line || line.startsWith('#') || line.startsWith(';')) continue;
|
|
30
|
+
const eq = line.indexOf('=');
|
|
31
|
+
if (eq < 0) continue;
|
|
32
|
+
const key = line.slice(0, eq).trim();
|
|
33
|
+
const value = line.slice(eq + 1).trim().replace(/^(["'])(.*)\1$/, '$2').replace(/\$\{([^}]+)\}/g, (_m, name: string) => env[name] ?? '');
|
|
34
|
+
if (key === 'registry') into.registry = trimSlash(value);
|
|
35
|
+
else if (/^@[^:]+:registry$/.test(key)) into.scopes[key.slice(0, key.indexOf(':'))] = trimSlash(value);
|
|
36
|
+
else if (key.startsWith('//') && key.endsWith(':_authToken')) into.tokens[key.slice(0, -':_authToken'.length).replace(/\/?$/, '/')] = value;
|
|
37
|
+
}
|
|
38
|
+
return into;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Read `.npmrc` files, lowest precedence first (user, then project). Missing files are skipped. */
|
|
42
|
+
export function readNpmConfig(fs: VirtualFileSystem, paths: string[], env: Record<string, string | undefined> = {}): NpmConfig {
|
|
43
|
+
const config = emptyNpmConfig();
|
|
44
|
+
for (const path of paths) {
|
|
45
|
+
const text = fs.readText(path);
|
|
46
|
+
if (text !== undefined) parseNpmrc(text, env, config);
|
|
47
|
+
}
|
|
48
|
+
return config;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The registry a package is fetched from: its scope's, else the default. */
|
|
52
|
+
export function registryFor(config: NpmConfig | undefined, name: string, fallback: string): string {
|
|
53
|
+
if (name.startsWith('@')) {
|
|
54
|
+
const scoped = config?.scopes[name.slice(0, name.indexOf('/'))];
|
|
55
|
+
if (scoped) return scoped;
|
|
56
|
+
}
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `Authorization` for a URL: the token whose registry path is the longest prefix of it. */
|
|
61
|
+
export function authHeaders(config: NpmConfig | undefined, url: string): Record<string, string> {
|
|
62
|
+
if (!config) return {};
|
|
63
|
+
const target = url.replace(/^https?:/, '');
|
|
64
|
+
let best = '';
|
|
65
|
+
for (const prefix of Object.keys(config.tokens)) {
|
|
66
|
+
if ((target.startsWith(prefix) || target + '/' === prefix) && prefix.length > best.length) best = prefix;
|
|
67
|
+
}
|
|
68
|
+
return best ? { authorization: `Bearer ${config.tokens[best]}` } : {};
|
|
69
|
+
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* caller injects one, which is what keeps this testable against a fake registry.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { authHeaders, registryFor, type NpmConfig } from './npmrc.js';
|
|
9
|
+
|
|
8
10
|
export type FetchLike = (url: string, init?: { headers?: Record<string, string>; signal?: AbortSignal }) => Promise<{
|
|
9
11
|
ok: boolean;
|
|
10
12
|
status: number;
|
|
@@ -60,6 +62,8 @@ export interface RegistryOptions {
|
|
|
60
62
|
fetch: FetchLike;
|
|
61
63
|
registry?: string;
|
|
62
64
|
cache?: PackageCache;
|
|
65
|
+
/** `.npmrc` settings: scoped registries and auth tokens. */
|
|
66
|
+
npmrc?: NpmConfig;
|
|
63
67
|
/** How long a cached packument is trusted. Default 10 minutes. */
|
|
64
68
|
packumentMaxAgeMs?: number;
|
|
65
69
|
signal?: AbortSignal;
|
|
@@ -136,7 +140,8 @@ export class RegistryClient {
|
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
private async loadPackument(name: string): Promise<Packument> {
|
|
139
|
-
const
|
|
143
|
+
const registry = registryFor(this.options.npmrc, name, this.registry);
|
|
144
|
+
const key = `packument:${registry}/${name}`;
|
|
140
145
|
const maxAge = this.options.packumentMaxAgeMs ?? 10 * 60_000;
|
|
141
146
|
const cached = await this.options.cache?.get(key);
|
|
142
147
|
if (cached) {
|
|
@@ -147,15 +152,16 @@ export class RegistryClient {
|
|
|
147
152
|
// a corrupt entry is just a miss
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
|
-
const url = `${
|
|
151
|
-
const bytes = await this.get(url, { accept: ABBREVIATED }, name);
|
|
155
|
+
const url = `${registry}/${name.startsWith('@') ? '@' + encodeURIComponent(name.slice(1)) : encodeURIComponent(name)}`;
|
|
156
|
+
const bytes = await this.get(url, { accept: ABBREVIATED }, name, registry);
|
|
152
157
|
const packument = JSON.parse(decoder.decode(bytes)) as Packument;
|
|
153
158
|
if (!packument.versions) throw new Error(`The registry returned no versions for "${name}".`);
|
|
154
159
|
await this.options.cache?.put(key, encoder.encode(JSON.stringify({ t: Date.now(), p: packument })));
|
|
155
160
|
return packument;
|
|
156
161
|
}
|
|
157
162
|
|
|
158
|
-
private async get(url: string, headers: Record<string, string>, packageName?: string): Promise<Uint8Array> {
|
|
163
|
+
private async get(url: string, headers: Record<string, string>, packageName?: string, registry = this.registry): Promise<Uint8Array> {
|
|
164
|
+
headers = { ...headers, ...authHeaders(this.options.npmrc, url) };
|
|
159
165
|
let lastError: unknown;
|
|
160
166
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
161
167
|
let response: Awaited<ReturnType<FetchLike>>;
|
|
@@ -166,7 +172,8 @@ export class RegistryClient {
|
|
|
166
172
|
lastError = error; // network blip: retry once
|
|
167
173
|
continue;
|
|
168
174
|
}
|
|
169
|
-
if (response.status === 404 && packageName) throw new Error(`Package "${packageName}" was not found in ${
|
|
175
|
+
if (response.status === 404 && packageName) throw new Error(`Package "${packageName}" was not found in ${registry}.`);
|
|
176
|
+
if ((response.status === 401 || response.status === 403) && packageName) throw new Error(`${registry} refused "${packageName}" (HTTP ${response.status}). Add an auth token to .npmrc.`);
|
|
170
177
|
if (!response.ok) throw new Error(`GET ${url} failed with HTTP ${response.status}.`);
|
|
171
178
|
return new Uint8Array(await response.arrayBuffer());
|
|
172
179
|
}
|
package/src/node/http/client.ts
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* the `fetch` global a program sees. Requests to localhost reach this runtime's
|
|
4
4
|
* own virtual servers (`loopback`); everything else goes through the browser's
|
|
5
5
|
* fetch, so CORS applies — a public API without CORS headers is unreachable
|
|
6
|
-
* from a browser page, and no shim can change that.
|
|
6
|
+
* from a browser page, and no shim can change that. Response bodies stream:
|
|
7
|
+
* `res.on('data')` and `response.body` see chunks as the server writes them.
|
|
7
8
|
*/
|
|
8
9
|
import { Buffer } from '../buffer.js';
|
|
9
10
|
import type { EventLoop } from '../loop.js';
|
|
10
11
|
import type { HttpRequestData, System } from '../../system/types.js';
|
|
11
|
-
import { FakeSocket, IncomingMessage, OutgoingHeaders } from './
|
|
12
|
+
import { FakeSocket, IncomingMessage, OutgoingHeaders } from './messages.js';
|
|
12
13
|
|
|
13
14
|
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', '[::1]']);
|
|
14
15
|
/** Headers a browser refuses to let script set; dropping them beats a TypeError. */
|
|
@@ -70,7 +71,8 @@ export async function sendRequest(system: System, url: URL, request: HttpRequest
|
|
|
70
71
|
const headers = new Headers();
|
|
71
72
|
for (const [k, v] of Object.entries(reply.headers)) for (const item of Array.isArray(v) ? v : [v]) headers.append(k, String(item));
|
|
72
73
|
const empty = reply.status === 204 || reply.status === 304 || request.method === 'HEAD';
|
|
73
|
-
|
|
74
|
+
if (empty) void reply.body.cancel().catch(() => undefined);
|
|
75
|
+
return new Response(empty ? null : reply.body, { status: reply.status, statusText: reply.statusText, headers });
|
|
74
76
|
}
|
|
75
77
|
const headers: Record<string, string> = {};
|
|
76
78
|
for (const [k, v] of Object.entries(request.headers)) if (!FORBIDDEN.has(k.toLowerCase())) headers[k] = v;
|
|
@@ -92,7 +94,9 @@ export class ClientRequest extends OutgoingHeaders {
|
|
|
92
94
|
private readonly options: RequestOptions;
|
|
93
95
|
|
|
94
96
|
constructor(private readonly deps: ClientDeps, input: unknown, extra: unknown, callback: ((res: IncomingMessage) => void) | undefined, defaultProtocol: string) {
|
|
95
|
-
|
|
97
|
+
// Not auto-destroyed on 'finish' (as in Node): the request body is done long before the
|
|
98
|
+
// response streams in, and destroy() aborts the request — which would cut that response off.
|
|
99
|
+
super({ decodeStrings: true, autoDestroy: false });
|
|
96
100
|
if (typeof extra === 'function') [callback, extra] = [extra as typeof callback, undefined];
|
|
97
101
|
this.options = toOptions(input, extra, defaultProtocol);
|
|
98
102
|
this.method = this.options.method!;
|
|
@@ -131,6 +135,33 @@ export class ClientRequest extends OutgoingHeaders {
|
|
|
131
135
|
setNoDelay(): void {}
|
|
132
136
|
setSocketKeepAlive(): void {}
|
|
133
137
|
|
|
138
|
+
/** Push the body into `res` chunk by chunk; aborting the request hangs up on the server. */
|
|
139
|
+
private async streamBody(response: Response, res: IncomingMessage): Promise<void> {
|
|
140
|
+
const reader = response.body?.getReader();
|
|
141
|
+
if (reader) {
|
|
142
|
+
const stop = () => void reader.cancel().catch(() => undefined);
|
|
143
|
+
this.controller.signal.addEventListener('abort', stop);
|
|
144
|
+
try {
|
|
145
|
+
for (;;) {
|
|
146
|
+
const { value, done } = await reader.read();
|
|
147
|
+
if (done || this.controller.signal.aborted) break;
|
|
148
|
+
this.deps.loop._run(() => res.push(Buffer.from(value.buffer as ArrayBuffer, value.byteOffset, value.byteLength)));
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (this.controller.signal.aborted) return;
|
|
152
|
+
this.deps.loop._run(() => {
|
|
153
|
+
res.aborted = true;
|
|
154
|
+
res.destroy(error as Error);
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
} finally {
|
|
158
|
+
this.controller.signal.removeEventListener('abort', stop);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (this.controller.signal.aborted) return;
|
|
162
|
+
this.deps.loop._run(() => res._complete());
|
|
163
|
+
}
|
|
164
|
+
|
|
134
165
|
private async perform(): Promise<void> {
|
|
135
166
|
const release = this.deps.loop.hold();
|
|
136
167
|
try {
|
|
@@ -144,10 +175,11 @@ export class ClientRequest extends OutgoingHeaders {
|
|
|
144
175
|
const res = new IncomingMessage(this.socket);
|
|
145
176
|
res.statusCode = response.status;
|
|
146
177
|
res.statusMessage = response.statusText;
|
|
147
|
-
res.
|
|
178
|
+
res._setHeaders(headersToRecord(response.headers));
|
|
148
179
|
this.deps.loop._run(() => {
|
|
149
180
|
if (!this.emit('response', res)) res.resume();
|
|
150
181
|
});
|
|
182
|
+
await this.streamBody(response, res);
|
|
151
183
|
} catch (error) {
|
|
152
184
|
if (!this.aborted) this.deps.loop._run(() => this.emit('error', error));
|
|
153
185
|
} finally {
|
|
@@ -165,9 +197,40 @@ export function createFetch(deps: ClientDeps): typeof fetch {
|
|
|
165
197
|
const url = new URL(request.url);
|
|
166
198
|
if (!isLocalHost(url.hostname)) return await deps.system.fetch(url.href, init ?? (typeof input === 'object' && !(input instanceof URL) ? { method: request.method, headers: request.headers, body: request.body, signal: request.signal } : undefined));
|
|
167
199
|
const body = request.method === 'GET' || request.method === 'HEAD' ? null : new Uint8Array(await request.arrayBuffer());
|
|
168
|
-
|
|
200
|
+
const response = await sendRequest(deps.system, url, { method: request.method, url: url.pathname + url.search, headers: headersToRecord(request.headers), body });
|
|
201
|
+
return holdWhileReading(response, () => deps.loop.hold());
|
|
169
202
|
} finally {
|
|
170
203
|
release();
|
|
171
204
|
}
|
|
172
205
|
};
|
|
173
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* A loopback body streams from a server that may live in another process, so
|
|
210
|
+
* nothing else keeps this one alive while the program awaits the next chunk:
|
|
211
|
+
* hold the loop for each pending read — and only then, so a body nobody reads
|
|
212
|
+
* does not keep the process running.
|
|
213
|
+
*/
|
|
214
|
+
function holdWhileReading(response: Response, hold: () => () => void): Response {
|
|
215
|
+
if (!response.body) return response;
|
|
216
|
+
const reader = response.body.getReader();
|
|
217
|
+
const body = new ReadableStream<Uint8Array>(
|
|
218
|
+
{
|
|
219
|
+
async pull(controller) {
|
|
220
|
+
const release = hold();
|
|
221
|
+
try {
|
|
222
|
+
const { value, done } = await reader.read();
|
|
223
|
+
if (done) controller.close();
|
|
224
|
+
else controller.enqueue(value);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
controller.error(error);
|
|
227
|
+
} finally {
|
|
228
|
+
release();
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
cancel: (reason) => reader.cancel(reason),
|
|
232
|
+
},
|
|
233
|
+
{ highWaterMark: 0 },
|
|
234
|
+
);
|
|
235
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
236
|
+
}
|
package/src/node/http/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { EventEmitterBase } from '../events.js';
|
|
|
8
8
|
import { ClientRequest, createFetch, type ClientDeps } from './client.js';
|
|
9
9
|
import { FakeSocket, IncomingMessage, OutgoingHeaders, Server, ServerResponse } from './server.js';
|
|
10
10
|
import { METHODS, STATUS_CODES } from './status.js';
|
|
11
|
+
import { textResponse } from '../../system/httpStream.js';
|
|
11
12
|
|
|
12
13
|
export { createFetch };
|
|
13
14
|
|
|
@@ -74,7 +75,7 @@ export function createNetModule(deps: ClientDeps): Record<string, unknown> {
|
|
|
74
75
|
const port = typeof first === 'object' && first ? Number((first as { port?: number }).port ?? 0) : Number(first ?? 0);
|
|
75
76
|
if (callback) this.once('listening', callback);
|
|
76
77
|
try {
|
|
77
|
-
this.reservation = deps.system.listen(port, async () => (
|
|
78
|
+
this.reservation = deps.system.listen(port, async () => textResponse(502, 'Not an HTTP server'));
|
|
78
79
|
} catch (error) {
|
|
79
80
|
queueMicrotask(() => this.emit('error', error));
|
|
80
81
|
return this;
|