@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.
- package/dist/archive.d.ts +11 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +51 -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 +10 -3
- package/dist/html.d.ts.map +1 -1
- package/dist/html.js +29 -5
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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/shell/npm.d.ts.map +1 -1
- package/dist/shell/npm.js +5 -1
- package/dist/shell/npm.js.map +1 -1
- 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 +59 -0
- package/src/devServer.ts +12 -2
- package/src/html.ts +32 -5
- package/src/index.ts +4 -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/shell/npm.ts +5 -1
- 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
|
|
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/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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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 {
|