@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.
Files changed (184) hide show
  1. package/dist/archive.d.ts +13 -0
  2. package/dist/archive.d.ts.map +1 -0
  3. package/dist/archive.js +53 -0
  4. package/dist/archive.js.map +1 -0
  5. package/dist/devServer.d.ts +5 -0
  6. package/dist/devServer.d.ts.map +1 -1
  7. package/dist/devServer.js +11 -2
  8. package/dist/devServer.js.map +1 -1
  9. package/dist/html.d.ts +12 -3
  10. package/dist/html.d.ts.map +1 -1
  11. package/dist/html.js +31 -5
  12. package/dist/html.js.map +1 -1
  13. package/dist/index.d.ts +5 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/installer/index.d.ts +2 -0
  18. package/dist/installer/index.d.ts.map +1 -1
  19. package/dist/installer/index.js +1 -0
  20. package/dist/installer/index.js.map +1 -1
  21. package/dist/installer/install.d.ts +3 -0
  22. package/dist/installer/install.d.ts.map +1 -1
  23. package/dist/installer/install.js +1 -1
  24. package/dist/installer/install.js.map +1 -1
  25. package/dist/installer/npmrc.d.ts +26 -0
  26. package/dist/installer/npmrc.d.ts.map +1 -0
  27. package/dist/installer/npmrc.js +55 -0
  28. package/dist/installer/npmrc.js.map +1 -0
  29. package/dist/installer/registry.d.ts +3 -0
  30. package/dist/installer/registry.d.ts.map +1 -1
  31. package/dist/installer/registry.js +10 -5
  32. package/dist/installer/registry.js.map +1 -1
  33. package/dist/node/http/client.d.ts +3 -1
  34. package/dist/node/http/client.d.ts.map +1 -1
  35. package/dist/node/http/client.js +74 -5
  36. package/dist/node/http/client.js.map +1 -1
  37. package/dist/node/http/index.d.ts.map +1 -1
  38. package/dist/node/http/index.js +2 -1
  39. package/dist/node/http/index.js.map +1 -1
  40. package/dist/node/http/messages.d.ts +87 -0
  41. package/dist/node/http/messages.d.ts.map +1 -0
  42. package/dist/node/http/messages.js +146 -0
  43. package/dist/node/http/messages.js.map +1 -0
  44. package/dist/node/http/server.d.ts +13 -98
  45. package/dist/node/http/server.d.ts.map +1 -1
  46. package/dist/node/http/server.js +34 -202
  47. package/dist/node/http/server.js.map +1 -1
  48. package/dist/node/http/serverResponse.d.ts +42 -0
  49. package/dist/node/http/serverResponse.d.ts.map +1 -0
  50. package/dist/node/http/serverResponse.js +89 -0
  51. package/dist/node/http/serverResponse.js.map +1 -0
  52. package/dist/node/http/upgradeSocket.d.ts +34 -0
  53. package/dist/node/http/upgradeSocket.d.ts.map +1 -0
  54. package/dist/node/http/upgradeSocket.js +73 -0
  55. package/dist/node/http/upgradeSocket.js.map +1 -0
  56. package/dist/node/runtime.js +2 -2
  57. package/dist/node/runtime.js.map +1 -1
  58. package/dist/previewBridge.d.ts +42 -0
  59. package/dist/previewBridge.d.ts.map +1 -0
  60. package/dist/previewBridge.js +192 -0
  61. package/dist/previewBridge.js.map +1 -0
  62. package/dist/shell/ast.d.ts +101 -0
  63. package/dist/shell/ast.d.ts.map +1 -0
  64. package/dist/shell/ast.js +7 -0
  65. package/dist/shell/ast.js.map +1 -0
  66. package/dist/shell/builtins.d.ts +14 -0
  67. package/dist/shell/builtins.d.ts.map +1 -1
  68. package/dist/shell/builtins.js +87 -7
  69. package/dist/shell/builtins.js.map +1 -1
  70. package/dist/shell/compound.d.ts +30 -0
  71. package/dist/shell/compound.d.ts.map +1 -0
  72. package/dist/shell/compound.js +73 -0
  73. package/dist/shell/compound.js.map +1 -0
  74. package/dist/shell/exec.d.ts +27 -6
  75. package/dist/shell/exec.d.ts.map +1 -1
  76. package/dist/shell/exec.js +127 -41
  77. package/dist/shell/exec.js.map +1 -1
  78. package/dist/shell/expand.d.ts +5 -1
  79. package/dist/shell/expand.d.ts.map +1 -1
  80. package/dist/shell/expand.js +7 -5
  81. package/dist/shell/expand.js.map +1 -1
  82. package/dist/shell/lineReader.d.ts +9 -0
  83. package/dist/shell/lineReader.d.ts.map +1 -0
  84. package/dist/shell/lineReader.js +42 -0
  85. package/dist/shell/lineReader.js.map +1 -0
  86. package/dist/shell/npm.d.ts.map +1 -1
  87. package/dist/shell/npm.js +5 -1
  88. package/dist/shell/npm.js.map +1 -1
  89. package/dist/shell/packageManagers.d.ts +20 -0
  90. package/dist/shell/packageManagers.d.ts.map +1 -0
  91. package/dist/shell/packageManagers.js +74 -0
  92. package/dist/shell/packageManagers.js.map +1 -0
  93. package/dist/shell/parse.d.ts +16 -66
  94. package/dist/shell/parse.d.ts.map +1 -1
  95. package/dist/shell/parse.js +244 -243
  96. package/dist/shell/parse.js.map +1 -1
  97. package/dist/shell/program.d.ts.map +1 -1
  98. package/dist/shell/program.js +2 -1
  99. package/dist/shell/program.js.map +1 -1
  100. package/dist/shell/testCommand.d.ts +9 -0
  101. package/dist/shell/testCommand.d.ts.map +1 -0
  102. package/dist/shell/testCommand.js +105 -0
  103. package/dist/shell/testCommand.js.map +1 -0
  104. package/dist/shell/tokenize.d.ts +25 -0
  105. package/dist/shell/tokenize.d.ts.map +1 -0
  106. package/dist/shell/tokenize.js +279 -0
  107. package/dist/shell/tokenize.js.map +1 -0
  108. package/dist/system/cookieJar.d.ts +59 -0
  109. package/dist/system/cookieJar.d.ts.map +1 -0
  110. package/dist/system/cookieJar.js +187 -0
  111. package/dist/system/cookieJar.js.map +1 -0
  112. package/dist/system/httpStream.d.ts +40 -0
  113. package/dist/system/httpStream.d.ts.map +1 -0
  114. package/dist/system/httpStream.js +156 -0
  115. package/dist/system/httpStream.js.map +1 -0
  116. package/dist/system/index.d.ts +9 -1
  117. package/dist/system/index.d.ts.map +1 -1
  118. package/dist/system/index.js +4 -0
  119. package/dist/system/index.js.map +1 -1
  120. package/dist/system/kernel.d.ts +10 -2
  121. package/dist/system/kernel.d.ts.map +1 -1
  122. package/dist/system/kernel.js +12 -3
  123. package/dist/system/kernel.js.map +1 -1
  124. package/dist/system/ports.d.ts +9 -4
  125. package/dist/system/ports.d.ts.map +1 -1
  126. package/dist/system/ports.js +21 -2
  127. package/dist/system/ports.js.map +1 -1
  128. package/dist/system/programs.d.ts +1 -1
  129. package/dist/system/programs.d.ts.map +1 -1
  130. package/dist/system/programs.js +2 -0
  131. package/dist/system/programs.js.map +1 -1
  132. package/dist/system/types.d.ts +36 -6
  133. package/dist/system/types.d.ts.map +1 -1
  134. package/dist/system/webSocketClient.d.ts +37 -0
  135. package/dist/system/webSocketClient.d.ts.map +1 -0
  136. package/dist/system/webSocketClient.js +152 -0
  137. package/dist/system/webSocketClient.js.map +1 -0
  138. package/dist/system/webSocketFrames.d.ts +39 -0
  139. package/dist/system/webSocketFrames.d.ts.map +1 -0
  140. package/dist/system/webSocketFrames.js +117 -0
  141. package/dist/system/webSocketFrames.js.map +1 -0
  142. package/dist/vfs.d.ts +16 -1
  143. package/dist/vfs.d.ts.map +1 -1
  144. package/dist/vfs.js +45 -3
  145. package/dist/vfs.js.map +1 -1
  146. package/package.json +1 -1
  147. package/src/archive.ts +63 -0
  148. package/src/devServer.ts +12 -2
  149. package/src/html.ts +34 -5
  150. package/src/index.ts +5 -1
  151. package/src/installer/index.ts +2 -0
  152. package/src/installer/install.ts +4 -1
  153. package/src/installer/npmrc.ts +69 -0
  154. package/src/installer/registry.ts +12 -5
  155. package/src/node/http/client.ts +69 -6
  156. package/src/node/http/index.ts +2 -1
  157. package/src/node/http/messages.ts +150 -0
  158. package/src/node/http/server.ts +37 -202
  159. package/src/node/http/serverResponse.ts +98 -0
  160. package/src/node/http/upgradeSocket.ts +79 -0
  161. package/src/node/runtime.ts +2 -2
  162. package/src/previewBridge.ts +194 -0
  163. package/src/shell/ast.ts +85 -0
  164. package/src/shell/builtins.ts +97 -7
  165. package/src/shell/compound.ts +89 -0
  166. package/src/shell/exec.ts +128 -40
  167. package/src/shell/expand.ts +8 -6
  168. package/src/shell/lineReader.ts +46 -0
  169. package/src/shell/npm.ts +5 -1
  170. package/src/shell/packageManagers.ts +75 -0
  171. package/src/shell/parse.ts +233 -236
  172. package/src/shell/program.ts +2 -1
  173. package/src/shell/testCommand.ts +105 -0
  174. package/src/shell/tokenize.ts +257 -0
  175. package/src/system/cookieJar.ts +187 -0
  176. package/src/system/httpStream.ts +168 -0
  177. package/src/system/index.ts +24 -1
  178. package/src/system/kernel.ts +16 -4
  179. package/src/system/ports.ts +23 -4
  180. package/src/system/programs.ts +3 -1
  181. package/src/system/types.ts +40 -6
  182. package/src/system/webSocketClient.ts +177 -0
  183. package/src/system/webSocketFrames.ts +118 -0
  184. 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
- watch(listener) {
129
- this.listeners.add(listener);
130
- return () => this.listeners.delete(listener);
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":"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"}
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.6",
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 to the host
17
- * page (`window.parent`), which is how an editor — or an agent — learns the app
18
- * crashed without scraping the iframe.
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 = `${errorBridgeScript()}\n${liveReloadScript(base)}`;
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';
@@ -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';
@@ -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 key = `packument:${this.registry}/${name}`;
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 = `${this.registry}/${name.startsWith('@') ? '@' + encodeURIComponent(name.slice(1)) : encodeURIComponent(name)}`;
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 ${this.registry}.`);
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
  }
@@ -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 './server.js';
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
- return new Response(empty ? null : (reply.body as unknown as BodyInit), { status: reply.status, statusText: reply.statusText, headers });
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
- super({ decodeStrings: true });
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._fill(headersToRecord(response.headers), new Uint8Array(await response.arrayBuffer()));
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
- return await sendRequest(deps.system, url, { method: request.method, url: url.pathname + url.search, headers: headersToRecord(request.headers), body });
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
+ }
@@ -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 () => ({ status: 502, headers: { 'content-type': 'text/plain' }, body: new TextEncoder().encode('Not an HTTP server') }));
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;