@statewalker/webrun-modules 0.1.0

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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +356 -0
  3. package/dist/index.d.ts +9 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +809 -0
  6. package/dist/resolution/node-builtins.d.ts +24 -0
  7. package/dist/resolution/node-builtins.d.ts.map +1 -0
  8. package/dist/resolution/resolve-entry.d.ts +9 -0
  9. package/dist/resolution/resolve-entry.d.ts.map +1 -0
  10. package/dist/server/new-module-server.d.ts +4 -0
  11. package/dist/server/new-module-server.d.ts.map +1 -0
  12. package/dist/server/specifiers.d.ts +11 -0
  13. package/dist/server/specifiers.d.ts.map +1 -0
  14. package/dist/sources/npm-registry-source.d.ts +29 -0
  15. package/dist/sources/npm-registry-source.d.ts.map +1 -0
  16. package/dist/sources/untar.d.ts +9 -0
  17. package/dist/sources/untar.d.ts.map +1 -0
  18. package/dist/transform/index.d.ts +15 -0
  19. package/dist/transform/index.d.ts.map +1 -0
  20. package/dist/transform/scan.d.ts +11 -0
  21. package/dist/transform/scan.d.ts.map +1 -0
  22. package/dist/transform/to-js.d.ts +10 -0
  23. package/dist/transform/to-js.d.ts.map +1 -0
  24. package/dist/transform/transform-cjs.d.ts +13 -0
  25. package/dist/transform/transform-cjs.d.ts.map +1 -0
  26. package/dist/transform/transform-esm.d.ts +9 -0
  27. package/dist/transform/transform-esm.d.ts.map +1 -0
  28. package/dist/types.d.ts +127 -0
  29. package/dist/types.d.ts.map +1 -0
  30. package/package.json +66 -0
  31. package/src/index.ts +26 -0
  32. package/src/resolution/node-builtins.ts +79 -0
  33. package/src/resolution/resolve-entry.ts +65 -0
  34. package/src/server/new-module-server.ts +416 -0
  35. package/src/server/specifiers.ts +26 -0
  36. package/src/sources/npm-registry-source.ts +86 -0
  37. package/src/sources/untar.ts +37 -0
  38. package/src/transform/index.ts +52 -0
  39. package/src/transform/scan.ts +32 -0
  40. package/src/transform/to-js.ts +18 -0
  41. package/src/transform/transform-cjs.ts +85 -0
  42. package/src/transform/transform-esm.ts +35 -0
  43. package/src/types.ts +143 -0
package/dist/index.js ADDED
@@ -0,0 +1,809 @@
1
+ import { readText, writeText } from "@statewalker/webrun-files";
2
+ import semver from "semver";
3
+ import { legacy, resolve } from "resolve.exports";
4
+ import { MemFilesApi } from "@statewalker/webrun-files-mem";
5
+ import { gunzipSync } from "fflate";
6
+ import { init, parse } from "cjs-module-lexer";
7
+ import { init as init$1, parse as parse$1 } from "es-module-lexer";
8
+ import { transform } from "sucrase";
9
+ //#region src/resolution/node-builtins.ts
10
+ /** Node core module names (static list — an isomorphic lib can't read `node:module`). */
11
+ const NODE_BUILTINS = new Set([
12
+ "assert",
13
+ "async_hooks",
14
+ "buffer",
15
+ "child_process",
16
+ "cluster",
17
+ "console",
18
+ "constants",
19
+ "crypto",
20
+ "dgram",
21
+ "diagnostics_channel",
22
+ "dns",
23
+ "domain",
24
+ "events",
25
+ "fs",
26
+ "http",
27
+ "http2",
28
+ "https",
29
+ "inspector",
30
+ "module",
31
+ "net",
32
+ "os",
33
+ "path",
34
+ "perf_hooks",
35
+ "process",
36
+ "punycode",
37
+ "querystring",
38
+ "readline",
39
+ "repl",
40
+ "stream",
41
+ "string_decoder",
42
+ "sys",
43
+ "timers",
44
+ "tls",
45
+ "trace_events",
46
+ "tty",
47
+ "url",
48
+ "util",
49
+ "v8",
50
+ "vm",
51
+ "wasi",
52
+ "worker_threads",
53
+ "zlib"
54
+ ]);
55
+ /** Strip a `node:` prefix and any subpath, returning the core module name. */
56
+ function nodeBuiltinName(spec) {
57
+ const bare = spec.replace(/^node:/, "");
58
+ const root = bare.split("/")[0];
59
+ return NODE_BUILTINS.has(root) ? bare : void 0;
60
+ }
61
+ /**
62
+ * Map a Node-builtin specifier under a given target:
63
+ * - `browser` → a `@jspm/core` browser-polyfill package ref (served locally).
64
+ * - `node` → `{ external }` (leave the `node:`-prefixed specifier in place).
65
+ *
66
+ * The subpath is `nodelibs/${name}` (not `nodelibs/browser/…`): `@jspm/core`'s
67
+ * `exports` map (`"./nodelibs/*"` → `default: "./nodelibs/browser/*.js"`) supplies
68
+ * the `browser` segment via the export condition. Hard-coding it here would make
69
+ * the wildcard expand to `nodelibs/browser/browser/${name}.js` — a path that
70
+ * doesn't exist.
71
+ */
72
+ function resolveNodeBuiltin(spec, target) {
73
+ const name = nodeBuiltinName(spec);
74
+ if (name === void 0) return void 0;
75
+ if (target === "node") return { external: `node:${name}` };
76
+ return { ref: {
77
+ pkg: "@jspm/core",
78
+ subpath: `nodelibs/${name}`
79
+ } };
80
+ }
81
+ //#endregion
82
+ //#region src/resolution/resolve-entry.ts
83
+ /** Strip a leading `./` or `/` so the result is a package-relative file path. */
84
+ function norm(p) {
85
+ return p.replace(/^\.?\//, "");
86
+ }
87
+ /** Call resolve.exports, swallowing its "no known conditions" throw. */
88
+ function tryResolve(manifest, entry, opts) {
89
+ try {
90
+ return resolve(manifest, entry, opts);
91
+ } catch {
92
+ return;
93
+ }
94
+ }
95
+ /**
96
+ * Resolve a package + subpath to the concrete file path inside the package,
97
+ * honoring `package.json` `exports` conditions for the given `target`, with a
98
+ * `main`/`module`/`browser` legacy fallback. Returns a package-relative path
99
+ * (e.g. `"dist/index.js"`).
100
+ */
101
+ function resolveEntry(manifest, subpath, target) {
102
+ const browser = target === "browser";
103
+ const clean = (subpath ?? "").replace(/^\.?\//, "");
104
+ const entry = clean ? `./${clean}` : ".";
105
+ if (manifest.exports !== void 0) {
106
+ const conditions = browser ? ["browser"] : ["node"];
107
+ const out = tryResolve(manifest, entry, {
108
+ browser,
109
+ conditions
110
+ }) ?? tryResolve(manifest, entry, {
111
+ browser,
112
+ conditions,
113
+ require: true
114
+ });
115
+ if (out?.length) return norm(out[0]);
116
+ }
117
+ if (entry === ".") {
118
+ const leg = legacy(manifest, {
119
+ browser,
120
+ fields: browser ? [
121
+ "browser",
122
+ "module",
123
+ "main"
124
+ ] : ["module", "main"]
125
+ });
126
+ if (typeof leg === "string") return norm(leg);
127
+ if (manifest.exports !== void 0) throw new Error(`${manifest.name}: package has no root entry ("." is not exported)`);
128
+ }
129
+ return clean || "index.js";
130
+ }
131
+ //#endregion
132
+ //#region src/types.ts
133
+ /** Thrown when a package / version / subpath cannot be resolved. */
134
+ var ModuleResolveError = class extends Error {
135
+ constructor(ref, reason) {
136
+ super(`Cannot resolve ${JSON.stringify(ref)}: ${reason}`);
137
+ this.ref = ref;
138
+ this.reason = reason;
139
+ this.name = "ModuleResolveError";
140
+ }
141
+ };
142
+ /** Thrown when a file cannot be transformed to runnable ESM. */
143
+ var ModuleTransformError = class extends Error {
144
+ constructor(path, reason) {
145
+ super(`Cannot transform ${path}: ${reason}`);
146
+ this.path = path;
147
+ this.reason = reason;
148
+ this.name = "ModuleTransformError";
149
+ }
150
+ };
151
+ //#endregion
152
+ //#region src/sources/untar.ts
153
+ /**
154
+ * Untar an npm `.tgz` (gzipped ustar) into a map of path → bytes, isomorphically
155
+ * (no `node:zlib`, no `Buffer`). npm tarballs prefix every entry with `package/`;
156
+ * that prefix is stripped so keys are relative to the package root.
157
+ */
158
+ function untarTgz(tgz) {
159
+ return untar(gunzipSync(tgz));
160
+ }
161
+ const td = new TextDecoder();
162
+ function str(buf, start, end) {
163
+ return td.decode(buf.subarray(start, end)).replace(/\0.*/s, "");
164
+ }
165
+ /** Parse a raw ustar archive (512-byte blocks). */
166
+ function untar(buf) {
167
+ const out = /* @__PURE__ */ new Map();
168
+ let off = 0;
169
+ while (off + 512 <= buf.length) {
170
+ const name = str(buf, off, off + 100);
171
+ if (!name) break;
172
+ const size = Number.parseInt(str(buf, off + 124, off + 136).trim() || "0", 8);
173
+ const type = str(buf, off + 156, off + 157);
174
+ const prefix = str(buf, off + 345, off + 500);
175
+ const full = prefix ? `${prefix}/${name}` : name;
176
+ const start = off + 512;
177
+ if (type === "0" || type === "") out.set(full.replace(/^package\//, ""), buf.subarray(start, start + size));
178
+ off = start + Math.ceil(size / 512) * 512;
179
+ }
180
+ return out;
181
+ }
182
+ //#endregion
183
+ //#region src/sources/npm-registry-source.ts
184
+ /**
185
+ * The default acquisition `Source`: resolves a version from npm registry
186
+ * metadata, downloads the tarball, untars it into a fresh `FilesApi`, and returns
187
+ * the package files + manifest. Fully isomorphic (`fetch` + pure-JS untar).
188
+ */
189
+ function npmRegistrySource(opts = {}) {
190
+ const registry = (opts.registryUrl ?? "https://registry.npmjs.org").replace(/\/+$/, "");
191
+ const doFetch = opts.fetch ?? globalThis.fetch;
192
+ const createFiles = opts.createFiles ?? (() => new MemFilesApi());
193
+ return {
194
+ matches(ref) {
195
+ return "pkg" in ref;
196
+ },
197
+ async load(ref) {
198
+ if (!("pkg" in ref)) throw new ModuleResolveError(ref, "not an npm reference");
199
+ const name = ref.pkg;
200
+ const metaRes = await doFetch(`${registry}/${encodeName(name)}`);
201
+ if (!metaRes.ok) throw new ModuleResolveError(ref, `registry ${metaRes.status} for ${name}`);
202
+ const meta = await metaRes.json();
203
+ const version = resolveVersion(meta, ref.version, ref);
204
+ const vmeta = meta.versions?.[version];
205
+ const tarball = vmeta?.dist?.tarball;
206
+ if (!tarball) throw new ModuleResolveError(ref, `no tarball for ${name}@${version}`);
207
+ const tgzRes = await doFetch(tarball);
208
+ if (!tgzRes.ok) throw new ModuleResolveError(ref, `tarball ${tgzRes.status} for ${name}@${version}`);
209
+ const entries = untarTgz(new Uint8Array(await tgzRes.arrayBuffer()));
210
+ const files = createFiles();
211
+ for (const [path, bytes] of entries) await files.write(`/${path}`, [bytes]);
212
+ return {
213
+ name,
214
+ version,
215
+ files,
216
+ manifest: {
217
+ ...vmeta,
218
+ name,
219
+ version
220
+ }
221
+ };
222
+ }
223
+ };
224
+ }
225
+ /** Resolve a dist-tag, exact version, or semver range to a concrete version. */
226
+ function resolveVersion(meta, spec, ref) {
227
+ const versions = Object.keys(meta.versions ?? {});
228
+ const tags = meta["dist-tags"] ?? {};
229
+ const s = spec ?? "latest";
230
+ if (tags[s]) return tags[s];
231
+ if (versions.includes(s)) return s;
232
+ const range = semver.validRange(s);
233
+ if (range) {
234
+ const best = semver.maxSatisfying(versions, range);
235
+ if (best) return best;
236
+ }
237
+ throw new ModuleResolveError(ref, `no version satisfies "${s}"`);
238
+ }
239
+ /** Encode a package name for a registry URL (scoped names need the `/` escaped). */
240
+ function encodeName(name) {
241
+ return name.startsWith("@") ? name.replace("/", "%2F") : name;
242
+ }
243
+ //#endregion
244
+ //#region src/transform/transform-cjs.ts
245
+ let lexerReady$1;
246
+ const REQUIRE_RE$1 = /require\(\s*(['"])((?:(?!\1)[^\\]|\\.)*)\1\s*\)/g;
247
+ const IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
248
+ /** Collect the unique static-string `require(...)` specifiers in a CJS source. */
249
+ function findRequires(source) {
250
+ const set = /* @__PURE__ */ new Set();
251
+ for (const m of source.matchAll(REQUIRE_RE$1)) set.add(m[2]);
252
+ return [...set];
253
+ }
254
+ /**
255
+ * Transform a CommonJS file into served, browser-runnable ESM — no global runtime
256
+ * registry: each static `require` target is a static namespace import, so the ESM
257
+ * module graph itself provides load ordering and circular-dep handling. The CJS
258
+ * body runs synchronously against a synthetic `require` that maps specifiers to
259
+ * those namespaces; named exports (via `cjs-module-lexer`) are re-exported as
260
+ * eval-time snapshots (valid because the body executed synchronously above).
261
+ * Computed `require(expr)` hits the synthetic require's miss path (throws) — the
262
+ * declared `esbuild-wasm` fallback's job.
263
+ */
264
+ function newCjsTransform() {
265
+ return { async transform(file, rewrite) {
266
+ lexerReady$1 ??= init();
267
+ await lexerReady$1;
268
+ const specs = findRequires(file.source);
269
+ const importLines = [];
270
+ const mapEntries = [];
271
+ specs.forEach((spec, i) => {
272
+ importLines.push(`import * as __d${i} from ${JSON.stringify(rewrite(spec))};`);
273
+ mapEntries.push(` ${JSON.stringify(spec)}: __d${i},`);
274
+ });
275
+ let names = [];
276
+ let reexports = [];
277
+ try {
278
+ const parsed = parse(file.source);
279
+ names = parsed.exports.filter((n) => IDENT_RE.test(n) && n !== "default");
280
+ reexports = parsed.reexports;
281
+ } catch {
282
+ names = [];
283
+ }
284
+ const namedExports = [...new Set(names)].map((n) => `export const ${n} = module.exports.${n};`).join("\n");
285
+ const reexportLines = [...new Set(reexports)].map((spec) => `export * from ${JSON.stringify(rewrite(spec))};`).join("\n");
286
+ const dir = file.path.replace(/\/[^/]*$/, "");
287
+ return [
288
+ ...importLines,
289
+ `const __ns = {\n${mapEntries.join("\n")}\n};`,
290
+ `const module = { exports: {} };`,
291
+ `const require = (s) => {`,
292
+ ` const m = __ns[s];`,
293
+ ` if (!m) throw new Error("Cannot require (computed/unresolved): " + s);`,
294
+ ` return m.default !== undefined ? m.default : m;`,
295
+ `};`,
296
+ `(function (module, exports, require, __filename, __dirname) {`,
297
+ file.source,
298
+ `}).call(module.exports, module, module.exports, require, ${JSON.stringify(file.path)}, ${JSON.stringify(dir)});`,
299
+ `export default module.exports;`,
300
+ namedExports,
301
+ reexportLines
302
+ ].filter(Boolean).join("\n");
303
+ } };
304
+ }
305
+ //#endregion
306
+ //#region src/transform/to-js.ts
307
+ /**
308
+ * Strip TS/JSX to plain JS (leave already-ESM/CJS JS untouched). Shared by the
309
+ * ESM transform and the specifier scanner so both see the *same* JS — raw TS/JSX
310
+ * cannot be lexed directly (`es-module-lexer` throws on JSX). JSX uses the
311
+ * automatic runtime, so scanning the output also surfaces the `react/jsx-runtime`
312
+ * import the runtime injects.
313
+ */
314
+ function toJs(source, format, path) {
315
+ if (format === "ts" || format === "tsx") return transform(source, {
316
+ transforms: format === "tsx" ? ["typescript", "jsx"] : ["typescript"],
317
+ jsxRuntime: "automatic",
318
+ filePath: path
319
+ }).code;
320
+ return source;
321
+ }
322
+ //#endregion
323
+ //#region src/transform/transform-esm.ts
324
+ let lexerReady;
325
+ /**
326
+ * The default per-file transform for ESM (and TS/JSX) sources: transpile to plain
327
+ * JS, then rewrite every static/dynamic-string import & re-export specifier in
328
+ * place via `rewrite`, leaving quotes and everything else byte-for-byte intact.
329
+ * Computed dynamic specifiers (no static string) are left untouched.
330
+ */
331
+ function newEsmTransform() {
332
+ return { async transform(file, rewrite) {
333
+ lexerReady ??= init$1;
334
+ await lexerReady;
335
+ const js = toJs(file.source, file.format, file.path);
336
+ const [imports] = parse$1(js, file.path);
337
+ let out = js;
338
+ for (let i = imports.length - 1; i >= 0; i--) {
339
+ const imp = imports[i];
340
+ if (imp.n == null) continue;
341
+ const q = out[imp.s];
342
+ const replacement = q === "\"" || q === "'" || q === "`" ? q + rewrite(imp.n) + q : rewrite(imp.n);
343
+ out = out.slice(0, imp.s) + replacement + out.slice(imp.e);
344
+ }
345
+ return out;
346
+ } };
347
+ }
348
+ //#endregion
349
+ //#region src/transform/index.ts
350
+ /** The default per-file transform: dispatch ESM/TS/JSX vs CJS by `file.format`. */
351
+ function newDefaultTransform() {
352
+ const esm = newEsmTransform();
353
+ const cjs = newCjsTransform();
354
+ return { transform(file, rewrite) {
355
+ return file.format === "cjs" ? cjs.transform(file, rewrite) : esm.transform(file, rewrite);
356
+ } };
357
+ }
358
+ const CJS_EXPORTS = /\bmodule\.exports\b|\bexports\s*[.[]/;
359
+ const ESM_STMT = /(^|[\s;])(import|export)[\s{*'"]/;
360
+ const REQUIRE_CALL = /(^|[^.\w])require\s*\(/;
361
+ /**
362
+ * Decide a file's `SourceFormat` from its extension, the package `type`, and
363
+ * (for ambiguous `.js`) a content sniff. Mirrors Node: `.mjs`=ESM, `.cjs`=CJS,
364
+ * `.js` follows `package.json#type`; with no `type`, a package `.js` is CJS
365
+ * (Node's default) unless real ESM syntax is present. Definitive CJS markers
366
+ * (`module.exports`/`exports.x`) win over a loose `import`/`export` word-match,
367
+ * so a CJS file with "export" in a string is not mis-read as ESM.
368
+ */
369
+ function detectFormat(path, source, manifest) {
370
+ if (path.endsWith(".ts")) return "ts";
371
+ if (path.endsWith(".tsx") || path.endsWith(".jsx")) return "tsx";
372
+ if (path.endsWith(".mjs")) return "esm";
373
+ if (path.endsWith(".cjs")) return "cjs";
374
+ if (manifest?.type === "module") return "esm";
375
+ if (manifest?.type === "commonjs") return "cjs";
376
+ if (CJS_EXPORTS.test(source)) return "cjs";
377
+ if (ESM_STMT.test(source)) return "esm";
378
+ if (REQUIRE_CALL.test(source)) return "cjs";
379
+ return manifest ? "cjs" : "esm";
380
+ }
381
+ //#endregion
382
+ //#region src/transform/scan.ts
383
+ const REQUIRE_RE = /require\(\s*(['"])((?:(?!\1)[^\\]|\\.)*)\1\s*\)/g;
384
+ let esmReady;
385
+ let cjsReady;
386
+ /**
387
+ * List the import specifiers a file's transform will rewrite — the exact same set
388
+ * the ESM/CJS transforms discover — so the server can pre-resolve them (async)
389
+ * before running the synchronous rewrite. For TS/JSX the raw source is scanned;
390
+ * `es-module-lexer` tolerates types well enough to find every specifier.
391
+ */
392
+ async function scanSpecifiers(source, format) {
393
+ if (format === "cjs") {
394
+ cjsReady ??= init();
395
+ await cjsReady;
396
+ return [...new Set([...source.matchAll(REQUIRE_RE)].map((m) => m[2]))];
397
+ }
398
+ esmReady ??= init$1;
399
+ await esmReady;
400
+ const [imports] = parse$1(toJs(source, format));
401
+ const out = /* @__PURE__ */ new Set();
402
+ for (const imp of imports) if (imp.n != null) out.add(imp.n);
403
+ return [...out];
404
+ }
405
+ //#endregion
406
+ //#region src/server/specifiers.ts
407
+ /** Split a bare specifier into package name + optional subpath (scope-aware). */
408
+ function parseSpecifier(spec) {
409
+ const parts = spec.split("/");
410
+ if (spec.startsWith("@")) {
411
+ const pkg = parts.slice(0, 2).join("/");
412
+ const subpath = parts.slice(2).join("/");
413
+ return subpath ? {
414
+ pkg,
415
+ subpath
416
+ } : { pkg };
417
+ }
418
+ const [pkg, ...rest] = parts;
419
+ const subpath = rest.join("/");
420
+ return subpath ? {
421
+ pkg,
422
+ subpath
423
+ } : { pkg };
424
+ }
425
+ /**
426
+ * Relative URL from one canonical module id to another (both are slash paths with
427
+ * no leading slash, filename last). Keeps served imports mount-prefix-portable.
428
+ */
429
+ function relativeUrl(fromId, toId) {
430
+ const from = fromId.split("/").slice(0, -1);
431
+ const to = toId.split("/");
432
+ let i = 0;
433
+ while (i < from.length && i < to.length && from[i] === to[i]) i++;
434
+ const ups = from.length - i;
435
+ const rel = [...Array(ups).fill(".."), ...to.slice(i)].join("/");
436
+ return rel.startsWith(".") ? rel : `./${rel}`;
437
+ }
438
+ //#endregion
439
+ //#region src/server/new-module-server.ts
440
+ const RAW_EXT = [
441
+ "",
442
+ ".js",
443
+ ".mjs",
444
+ ".cjs",
445
+ ".json",
446
+ "/index.js",
447
+ "/index.mjs"
448
+ ];
449
+ /** JS/TS module files — the only ones the transform touches. */
450
+ const MODULE_EXT = /\.(?:m|c)?[jt]sx?$/;
451
+ const isModuleFile = (path) => MODULE_EXT.test(path);
452
+ const CONTENT_TYPES = {
453
+ json: "application/json",
454
+ css: "text/css",
455
+ md: "text/markdown",
456
+ html: "text/html",
457
+ svg: "image/svg+xml",
458
+ wasm: "application/wasm",
459
+ map: "application/json",
460
+ txt: "text/plain"
461
+ };
462
+ /** Guess a content-type from a file path (non-module resources). */
463
+ function contentType(path) {
464
+ return CONTENT_TYPES[path.split(".").pop()?.toLowerCase() ?? ""] ?? "application/octet-stream";
465
+ }
466
+ /** Create a module/dependency server over an injected `FilesApi` cache. */
467
+ function newModuleServer(options) {
468
+ const cache = options.cache;
469
+ const project = options.project;
470
+ const sources = options.sources ?? [npmRegistrySource()];
471
+ const transformer = options.transform ?? newDefaultTransform();
472
+ const target = options.target ?? "browser";
473
+ const basePath = normalizeBase(options.basePath ?? "/");
474
+ const depsPrefix = normalizeDeps(options.depsPath ?? "");
475
+ const lock = { ...options.lock ?? {} };
476
+ const tRoot = `/t/${target}`;
477
+ let ready;
478
+ const init = () => {
479
+ ready ??= (async () => {
480
+ if (await cache.exists("/lock.json")) Object.assign(lock, JSON.parse(await readText(cache, "/lock.json")), options.lock ?? {});
481
+ })();
482
+ return ready;
483
+ };
484
+ const urlPath = (id) => id.startsWith("~/") ? id : depsPrefix + id;
485
+ const urlFor = (id) => basePath + urlPath(id);
486
+ const idFromPath = (pathname) => {
487
+ let p = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname.replace(/^\//, "");
488
+ if (depsPrefix && p.startsWith(depsPrefix)) p = p.slice(depsPrefix.length);
489
+ return p;
490
+ };
491
+ function matchSource(ref) {
492
+ const s = sources.find((x) => x.matches(ref));
493
+ if (!s) throw new ModuleResolveError(ref, "no source matches");
494
+ return s;
495
+ }
496
+ function reusable(locked, spec) {
497
+ if (!spec) return true;
498
+ if (semver.valid(spec)) return locked === spec;
499
+ if (semver.validRange(spec)) return semver.satisfies(locked, spec);
500
+ return true;
501
+ }
502
+ async function cachedManifest(pkgKey) {
503
+ return JSON.parse(await readText(cache, `/raw/${pkgKey}/package.json`));
504
+ }
505
+ /** Persist a loaded package's files under `/raw/{name}@{version}/…` (idempotent).
506
+ * The manifest is written as `package.json` (the authority — a Source's files
507
+ * are not required to include one). */
508
+ async function cacheRaw(name, version, files, manifest) {
509
+ const key = `${name}@${version}`;
510
+ if (await cache.exists(`/raw/${key}/package.json`)) return;
511
+ for await (const info of files.list("/", { recursive: true })) if (info.kind === "file") await cache.write(`/raw/${key}${info.path}`, [await collect(files.read(info.path))]);
512
+ if (!await cache.exists(`/raw/${key}/package.json`)) await writeText(cache, `/raw/${key}/package.json`, JSON.stringify(manifest));
513
+ }
514
+ /** Lazily load a package's raw files by its `name@version` cache key (idempotent). */
515
+ async function ensureRawByKey(pkgKey) {
516
+ if (await cache.exists(`/raw/${pkgKey}/package.json`)) return;
517
+ const at = pkgKey.lastIndexOf("@");
518
+ const ref = {
519
+ pkg: pkgKey.slice(0, at),
520
+ version: pkgKey.slice(at + 1)
521
+ };
522
+ const loaded = await matchSource(ref).load(ref);
523
+ await cacheRaw(loaded.name, loaded.version, loaded.files, loaded.manifest);
524
+ }
525
+ /** Ensure a package's raw files + manifest are cached; return its pinned id. */
526
+ async function ensurePackage(ref) {
527
+ const name = ref.pkg;
528
+ const locked = lock[name];
529
+ let version;
530
+ let manifest;
531
+ if (locked && reusable(locked, ref.version)) {
532
+ version = locked;
533
+ await ensureRawByKey(`${name}@${version}`);
534
+ manifest = await cachedManifest(`${name}@${version}`);
535
+ } else {
536
+ const loaded = await matchSource(ref).load({
537
+ pkg: name,
538
+ version: ref.version
539
+ });
540
+ version = loaded.version;
541
+ await cacheRaw(name, version, loaded.files, loaded.manifest);
542
+ manifest = loaded.manifest;
543
+ if (!locked) lock[name] = version;
544
+ }
545
+ const entry = resolveEntry(manifest, ref.subpath, target);
546
+ const file = await resolveRawFile(`${name}@${version}`, entry);
547
+ return {
548
+ name,
549
+ version,
550
+ file,
551
+ id: `${name}@${version}/${file}`,
552
+ manifest
553
+ };
554
+ }
555
+ /** Resolve an import specifier (from `fromId`) to what to emit + its cache id. */
556
+ async function resolveSpec(spec, fromId) {
557
+ if (/^(https?:|data:)/.test(spec)) return { url: spec };
558
+ const builtin = resolveNodeBuiltin(spec, target);
559
+ if (builtin) {
560
+ if ("external" in builtin) return { url: builtin.external };
561
+ const t = await ensurePackage(builtin.ref);
562
+ return {
563
+ url: relativeUrl(urlPath(fromId), urlPath(t.id)),
564
+ id: t.id
565
+ };
566
+ }
567
+ if (spec.startsWith(".")) {
568
+ const id = await resolveRelativeId(fromId, spec);
569
+ return {
570
+ url: jsonModuleUrl(relativeUrl(urlPath(fromId), urlPath(id)), id),
571
+ id
572
+ };
573
+ }
574
+ const { pkg, subpath } = parseSpecifier(spec);
575
+ const t = await ensurePackage({
576
+ pkg,
577
+ version: await importerVersion(pkg, fromId),
578
+ subpath
579
+ });
580
+ return {
581
+ url: jsonModuleUrl(relativeUrl(urlPath(fromId), urlPath(t.id)), t.id),
582
+ id: t.id
583
+ };
584
+ }
585
+ /** The version constraint a bare specifier should resolve to, from the importer's
586
+ * package: self-reference → the importer's own version; a dependency → the
587
+ * importer's `package.json` range; otherwise undefined (global lock/latest). */
588
+ async function importerVersion(pkg, fromId) {
589
+ const m = fromId.match(/^((?:@[^/]+\/)?[^/]+)@([^/]+)\//);
590
+ if (!m) return void 0;
591
+ const [, impName, impVersion] = m;
592
+ if (pkg === impName) return impVersion;
593
+ const manifest = await cachedManifest(`${impName}@${impVersion}`).catch(() => void 0);
594
+ return manifest?.dependencies?.[pkg] ?? manifest?.peerDependencies?.[pkg] ?? (manifest?.optionalDependencies)?.[pkg];
595
+ }
596
+ /** A `.json` imported by a JS module is served as an ESM (`export default …`);
597
+ * mark its URL with `?module` so `fetch` wraps it instead of serving raw JSON. */
598
+ function jsonModuleUrl(url, id) {
599
+ return id.endsWith(".json") ? `${url}?module` : url;
600
+ }
601
+ /** Resolve a relative specifier against an importer id to a concrete cache id. */
602
+ async function resolveRelativeId(fromId, spec) {
603
+ const dir = fromId.split("/").slice(0, -1);
604
+ for (const part of spec.split("/")) {
605
+ if (part === "." || part === "") continue;
606
+ if (part === "..") dir.pop();
607
+ else dir.push(part);
608
+ }
609
+ const base = dir.join("/");
610
+ const pkgKey = base.match(/^(?:@[^/]+\/)?[^/]+@[^/]+/)?.[0];
611
+ if (pkgKey) return `${pkgKey}/${await resolveRawFile(pkgKey, base.slice(pkgKey.length + 1))}`;
612
+ return base;
613
+ }
614
+ /** Try extension/index variants for a package-relative file; return what exists. */
615
+ async function resolveRawFile(pkgKey, file) {
616
+ for (const ext of RAW_EXT) if (await cache.exists(`/raw/${pkgKey}/${file}${ext}`)) return file + ext;
617
+ return file;
618
+ }
619
+ /** Load a canonical id's raw source + format context. */
620
+ async function loadRaw(id) {
621
+ if (id.startsWith("~/")) {
622
+ if (!project) throw new ModuleResolveError({ url: id }, "no project FilesApi");
623
+ const path = `/${id.slice(2)}`;
624
+ const source = await readText(project, path);
625
+ return {
626
+ path,
627
+ source,
628
+ format: detectFormat(path, source)
629
+ };
630
+ }
631
+ const m = id.match(/^((?:@[^/]+\/)?[^/]+@[^/]+)\/(.+)$/);
632
+ if (!m) throw new ModuleResolveError({ url: id }, "not a module id");
633
+ const [, pkgKey, rawFile] = m;
634
+ await ensureRawByKey(pkgKey);
635
+ const file = await resolveRawFile(pkgKey, rawFile);
636
+ const source = await readText(cache, `/raw/${pkgKey}/${file}`);
637
+ const manifest = await cachedManifest(pkgKey).catch(() => void 0);
638
+ return {
639
+ path: `/${pkgKey}/${file}`,
640
+ source,
641
+ format: detectFormat(file, source, manifest)
642
+ };
643
+ }
644
+ /** Transform a module (pre-resolving its specifiers) and cache the ESM output. */
645
+ async function transformAndCache(id) {
646
+ const { path, source, format } = await loadRaw(id);
647
+ const specs = await scanSpecifiers(source, format);
648
+ const map = /* @__PURE__ */ new Map();
649
+ for (const spec of specs) map.set(spec, (await resolveSpec(spec, id)).url);
650
+ const out = await transformer.transform({
651
+ path,
652
+ source,
653
+ format
654
+ }, (s) => map.get(s) ?? s);
655
+ await writeText(cache, `${tRoot}/${id}`, out);
656
+ return out;
657
+ }
658
+ /** Resolve a file id (project `~/…` or `{pkg}@{ver}/{file}`) to its raw bytes. */
659
+ async function rawBytes(id) {
660
+ if (id.startsWith("~/")) {
661
+ const path = `/${id.slice(2)}`;
662
+ if (!project || !await project.exists(path)) return void 0;
663
+ return collect(project.read(path));
664
+ }
665
+ const m = id.match(/^((?:@[^/]+\/)?[^/]+@[^/]+)\/(.+)$/);
666
+ if (!m) return void 0;
667
+ await ensureRawByKey(m[1]);
668
+ const file = await resolveRawFile(m[1], m[2]);
669
+ if (!await cache.exists(`/raw/${m[1]}/${file}`)) return void 0;
670
+ return collect(cache.read(`/raw/${m[1]}/${file}`));
671
+ }
672
+ /** Serve a file's raw bytes (non-module resources, or `?raw`). */
673
+ async function serveRaw(id, asOctet) {
674
+ const bytes = await rawBytes(id);
675
+ if (!bytes) return new Response(null, { status: 404 });
676
+ return new Response(bytes, {
677
+ status: 200,
678
+ headers: { "content-type": asOctet ? "application/octet-stream" : contentType(id) }
679
+ });
680
+ }
681
+ /** Serve a JSON file as an ESM module (`export default …`) — how a JS `import`
682
+ * of a `.json` is satisfied without relying on browser JSON-import attributes. */
683
+ async function serveJsonModule(id) {
684
+ const bytes = await rawBytes(id);
685
+ if (!bytes) return new Response(null, { status: 404 });
686
+ const json = new TextDecoder().decode(bytes);
687
+ return new Response(`export default ${json};`, {
688
+ status: 200,
689
+ headers: { "content-type": "text/javascript" }
690
+ });
691
+ }
692
+ async function resolveEntryId(ref) {
693
+ if ("url" in ref) {
694
+ if (/^https?:/.test(ref.url)) throw new ModuleResolveError(ref, "absolute URL is not local");
695
+ const p = idFromPath(ref.url);
696
+ return p.startsWith("~/") ? p : `~/${p.replace(/^\//, "")}`;
697
+ }
698
+ return (await ensurePackage(ref)).id;
699
+ }
700
+ /** Walk + transform + cache the whole graph from an entry; persist the lockfile.
701
+ * Returns the entry id and every reachable module id. */
702
+ async function walkGraph(entry) {
703
+ const rootId = await resolveEntryId(entry);
704
+ const seen = /* @__PURE__ */ new Set();
705
+ const queue = [rootId];
706
+ while (queue.length) {
707
+ const id = queue.shift();
708
+ if (id === void 0 || seen.has(id)) continue;
709
+ seen.add(id);
710
+ if (!isModuleFile(id)) {
711
+ const m = id.match(/^((?:@[^/]+\/)?[^/]+@[^/]+)\//);
712
+ if (m) await ensureRawByKey(m[1]);
713
+ continue;
714
+ }
715
+ const { source, format } = await loadRaw(id);
716
+ for (const spec of await scanSpecifiers(source, format)) {
717
+ const { id: childId } = await resolveSpec(spec, id);
718
+ if (childId && !seen.has(childId)) queue.push(childId);
719
+ }
720
+ await transformAndCache(id);
721
+ }
722
+ await writeText(cache, "/lock.json", JSON.stringify(lock));
723
+ return {
724
+ rootId,
725
+ ids: [...seen]
726
+ };
727
+ }
728
+ return {
729
+ get lock() {
730
+ return lock;
731
+ },
732
+ async resolve(ref) {
733
+ await init();
734
+ return {
735
+ url: urlFor(await resolveEntryId(ref)),
736
+ target
737
+ };
738
+ },
739
+ async prime(entry) {
740
+ await init();
741
+ const { rootId } = await walkGraph(entry);
742
+ return {
743
+ url: urlFor(rootId),
744
+ target
745
+ };
746
+ },
747
+ async listResources(entry) {
748
+ await init();
749
+ const { ids } = await walkGraph(entry);
750
+ return ids.map(urlFor).sort();
751
+ },
752
+ async listPackageFiles(ref) {
753
+ await init();
754
+ if ("url" in ref) throw new ModuleResolveError(ref, "not a package reference");
755
+ const { name, version } = await ensurePackage({
756
+ pkg: ref.pkg,
757
+ version: ref.version
758
+ });
759
+ const key = `${name}@${version}`;
760
+ const files = [];
761
+ for await (const info of cache.list(`/raw/${key}`, { recursive: true })) if (info.kind === "file") files.push(info.path.replace(`/raw/${key}/`, ""));
762
+ return files.sort();
763
+ },
764
+ async fetch(request) {
765
+ await init();
766
+ const url = new URL(request.url);
767
+ const id = idFromPath(url.pathname);
768
+ try {
769
+ if (url.searchParams.has("raw")) return await serveRaw(id, true);
770
+ if (url.searchParams.has("module") && id.endsWith(".json")) return await serveJsonModule(id);
771
+ if (!isModuleFile(id)) return await serveRaw(id, false);
772
+ const body = await cache.exists(`${tRoot}/${id}`) ? await readText(cache, `${tRoot}/${id}`) : await transformAndCache(id);
773
+ return new Response(body, {
774
+ status: 200,
775
+ headers: { "content-type": "text/javascript" }
776
+ });
777
+ } catch {
778
+ return new Response(null, { status: 404 });
779
+ }
780
+ }
781
+ };
782
+ }
783
+ function normalizeBase(base) {
784
+ let b = base.startsWith("/") ? base : `/${base}`;
785
+ if (!b.endsWith("/")) b += "/";
786
+ return b;
787
+ }
788
+ function normalizeDeps(prefix) {
789
+ if (!prefix) return "";
790
+ const s = prefix.replace(/^\/+/, "").replace(/\/+$/, "");
791
+ return s ? `${s}/` : "";
792
+ }
793
+ async function collect(chunks) {
794
+ const parts = [];
795
+ let n = 0;
796
+ for await (const c of chunks) {
797
+ parts.push(c);
798
+ n += c.length;
799
+ }
800
+ const out = new Uint8Array(n);
801
+ let off = 0;
802
+ for (const p of parts) {
803
+ out.set(p, off);
804
+ off += p.length;
805
+ }
806
+ return out;
807
+ }
808
+ //#endregion
809
+ export { ModuleResolveError, ModuleTransformError, detectFormat, newCjsTransform, newDefaultTransform, newEsmTransform, newModuleServer, npmRegistrySource, parseSpecifier, relativeUrl, untarTgz };