@seanhogg/builderforce-webcontainers-core 2026.10.6 → 2026.10.7

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 (50) hide show
  1. package/dist/archive.d.ts +11 -0
  2. package/dist/archive.d.ts.map +1 -0
  3. package/dist/archive.js +51 -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 +10 -3
  10. package/dist/html.d.ts.map +1 -1
  11. package/dist/html.js +29 -5
  12. package/dist/html.js.map +1 -1
  13. package/dist/index.d.ts +4 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -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/shell/npm.d.ts.map +1 -1
  34. package/dist/shell/npm.js +5 -1
  35. package/dist/shell/npm.js.map +1 -1
  36. package/dist/vfs.d.ts +16 -1
  37. package/dist/vfs.d.ts.map +1 -1
  38. package/dist/vfs.js +45 -3
  39. package/dist/vfs.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/archive.ts +59 -0
  42. package/src/devServer.ts +12 -2
  43. package/src/html.ts +32 -5
  44. package/src/index.ts +4 -1
  45. package/src/installer/index.ts +2 -0
  46. package/src/installer/install.ts +4 -1
  47. package/src/installer/npmrc.ts +69 -0
  48. package/src/installer/registry.ts +12 -5
  49. package/src/shell/npm.ts +5 -1
  50. package/src/vfs.ts +46 -3
@@ -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
  }
package/src/shell/npm.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { join } from '../paths.js';
9
9
  import { installPackages, type InstallOptions } from '../installer/install.js';
10
+ import { readNpmConfig } from '../installer/npmrc.js';
10
11
  import type { PackageJson } from '../installer/spec.js';
11
12
  import { binDirectories, findExecutable } from '../system/commands.js';
12
13
  import { findPackageScope } from '../node/resolve.js';
@@ -34,10 +35,13 @@ async function install(ctx: CommandContext, options: Partial<InstallOptions>, di
34
35
  const started = performance.now();
35
36
  const progress = ctx.io.isTerminal;
36
37
  try {
38
+ // User file first, then the project's: later files win, as with npm.
39
+ const npmrc = readNpmConfig(ctx.fs, [join(ctx.env.HOME ?? '/home/user', '.npmrc'), join(dir, '.npmrc')], ctx.env);
37
40
  const result = await installPackages({
38
41
  fs: ctx.fs,
39
42
  fetch: (url, init) => ctx.system.fetch(url, init as RequestInit),
40
- registry: ctx.env.npm_config_registry ?? ctx.system.registry,
43
+ registry: ctx.env.npm_config_registry ?? npmrc.registry ?? ctx.system.registry,
44
+ npmrc,
41
45
  cache: ctx.system.packageCache,
42
46
  cwd: dir,
43
47
  signal: ctx.signal,
package/src/vfs.ts CHANGED
@@ -14,6 +14,11 @@ export interface FileTree {
14
14
  [name: string]: FileContent | FileTree;
15
15
  }
16
16
 
17
+ export interface DirectoryEntry {
18
+ name: string;
19
+ kind: 'file' | 'directory';
20
+ }
21
+
17
22
  export interface FsChange {
18
23
  /** `mkdir` is an explicitly created (possibly empty) directory. */
19
24
  type: 'write' | 'remove' | 'mkdir';
@@ -98,6 +103,39 @@ export class VirtualFileSystem {
98
103
  return [...(this.children.get(normalizePath(path)) ?? [])].sort();
99
104
  }
100
105
 
106
+ /** Direct children with their kind, sorted by name — `readdir` for callers that render a tree. */
107
+ entries(path: string): DirectoryEntry[] {
108
+ const dir = normalizePath(path);
109
+ return this.readdir(dir).map((name) => ({ name, kind: this.children.has(join(dir, name)) ? 'directory' : 'file' }));
110
+ }
111
+
112
+ /**
113
+ * Move a file or a directory (with everything under it). Throws when `from` is
114
+ * missing or `to` already exists. Watchers see the old paths removed and the
115
+ * new ones written, which is what every consumer (dev server, process sync) needs.
116
+ */
117
+ rename(from: string, to: string): void {
118
+ const source = normalizePath(from);
119
+ const target = normalizePath(to);
120
+ if (source === target) return;
121
+ if (!this.exists(source)) throw new Error(`ENOENT: no such file or directory, rename '${source}' -> '${target}'`);
122
+ if (this.exists(target)) throw new Error(`EEXIST: file already exists, rename '${source}' -> '${target}'`);
123
+ if (target.startsWith(source + '/')) throw new Error(`EINVAL: cannot move '${source}' into itself`);
124
+ if (this.files.has(source)) {
125
+ const contents = this.files.get(source)!;
126
+ this.removeFile(source);
127
+ this.writeFile(target, contents);
128
+ return;
129
+ }
130
+ const prefix = source + '/';
131
+ const moved = [...this.files].filter(([path]) => path.startsWith(prefix));
132
+ const dirs = [...this.explicit].filter((dir) => dir === source || dir.startsWith(prefix));
133
+ this.rm(source);
134
+ this.mkdir(target);
135
+ for (const dir of dirs) this.mkdir(target + dir.slice(source.length));
136
+ for (const [path, contents] of moved) this.writeFile(target + path.slice(source.length), contents);
137
+ }
138
+
101
139
  /** Every file path, sorted. */
102
140
  list(): string[] {
103
141
  return [...this.files.keys()].sort();
@@ -152,9 +190,14 @@ export class VirtualFileSystem {
152
190
  return out;
153
191
  }
154
192
 
155
- watch(listener: (change: FsChange) => void): () => void {
156
- this.listeners.add(listener);
157
- return () => this.listeners.delete(listener);
193
+ /** Every change, or with `under` only changes at or below that path. */
194
+ watch(listener: (change: FsChange) => void, options: { under?: string } = {}): () => void {
195
+ const root = options.under === undefined ? undefined : normalizePath(options.under);
196
+ const filtered = root === undefined || root === '/'
197
+ ? listener
198
+ : (change: FsChange) => (change.path === root || change.path.startsWith(root + '/')) && listener(change);
199
+ this.listeners.add(filtered);
200
+ return () => this.listeners.delete(filtered);
158
201
  }
159
202
 
160
203
  private removeFile(key: string): void {