@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.
- package/LICENSE +21 -0
- package/README.md +356 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +809 -0
- package/dist/resolution/node-builtins.d.ts +24 -0
- package/dist/resolution/node-builtins.d.ts.map +1 -0
- package/dist/resolution/resolve-entry.d.ts +9 -0
- package/dist/resolution/resolve-entry.d.ts.map +1 -0
- package/dist/server/new-module-server.d.ts +4 -0
- package/dist/server/new-module-server.d.ts.map +1 -0
- package/dist/server/specifiers.d.ts +11 -0
- package/dist/server/specifiers.d.ts.map +1 -0
- package/dist/sources/npm-registry-source.d.ts +29 -0
- package/dist/sources/npm-registry-source.d.ts.map +1 -0
- package/dist/sources/untar.d.ts +9 -0
- package/dist/sources/untar.d.ts.map +1 -0
- package/dist/transform/index.d.ts +15 -0
- package/dist/transform/index.d.ts.map +1 -0
- package/dist/transform/scan.d.ts +11 -0
- package/dist/transform/scan.d.ts.map +1 -0
- package/dist/transform/to-js.d.ts +10 -0
- package/dist/transform/to-js.d.ts.map +1 -0
- package/dist/transform/transform-cjs.d.ts +13 -0
- package/dist/transform/transform-cjs.d.ts.map +1 -0
- package/dist/transform/transform-esm.d.ts +9 -0
- package/dist/transform/transform-esm.d.ts.map +1 -0
- package/dist/types.d.ts +127 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +66 -0
- package/src/index.ts +26 -0
- package/src/resolution/node-builtins.ts +79 -0
- package/src/resolution/resolve-entry.ts +65 -0
- package/src/server/new-module-server.ts +416 -0
- package/src/server/specifiers.ts +26 -0
- package/src/sources/npm-registry-source.ts +86 -0
- package/src/sources/untar.ts +37 -0
- package/src/transform/index.ts +52 -0
- package/src/transform/scan.ts +32 -0
- package/src/transform/to-js.ts +18 -0
- package/src/transform/transform-cjs.ts +85 -0
- package/src/transform/transform-esm.ts +35 -0
- package/src/types.ts +143 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { init, parse } from "cjs-module-lexer";
|
|
2
|
+
import type { SourceFile, Transform } from "../types.js";
|
|
3
|
+
|
|
4
|
+
let lexerReady: Promise<unknown> | undefined;
|
|
5
|
+
|
|
6
|
+
// Matches `require("x")` / `require('x')` with a static string literal argument.
|
|
7
|
+
const REQUIRE_RE = /require\(\s*(['"])((?:(?!\1)[^\\]|\\.)*)\1\s*\)/g;
|
|
8
|
+
|
|
9
|
+
const IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
10
|
+
|
|
11
|
+
/** Collect the unique static-string `require(...)` specifiers in a CJS source. */
|
|
12
|
+
function findRequires(source: string): string[] {
|
|
13
|
+
const set = new Set<string>();
|
|
14
|
+
for (const m of source.matchAll(REQUIRE_RE)) set.add(m[2]);
|
|
15
|
+
return [...set];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Transform a CommonJS file into served, browser-runnable ESM — no global runtime
|
|
20
|
+
* registry: each static `require` target is a static namespace import, so the ESM
|
|
21
|
+
* module graph itself provides load ordering and circular-dep handling. The CJS
|
|
22
|
+
* body runs synchronously against a synthetic `require` that maps specifiers to
|
|
23
|
+
* those namespaces; named exports (via `cjs-module-lexer`) are re-exported as
|
|
24
|
+
* eval-time snapshots (valid because the body executed synchronously above).
|
|
25
|
+
* Computed `require(expr)` hits the synthetic require's miss path (throws) — the
|
|
26
|
+
* declared `esbuild-wasm` fallback's job.
|
|
27
|
+
*/
|
|
28
|
+
export function newCjsTransform(): Transform {
|
|
29
|
+
return {
|
|
30
|
+
async transform(file: SourceFile, rewrite: (specifier: string) => string): Promise<string> {
|
|
31
|
+
lexerReady ??= init();
|
|
32
|
+
await lexerReady;
|
|
33
|
+
|
|
34
|
+
const specs = findRequires(file.source);
|
|
35
|
+
const importLines: string[] = [];
|
|
36
|
+
const mapEntries: string[] = [];
|
|
37
|
+
specs.forEach((spec, i) => {
|
|
38
|
+
importLines.push(`import * as __d${i} from ${JSON.stringify(rewrite(spec))};`);
|
|
39
|
+
mapEntries.push(` ${JSON.stringify(spec)}: __d${i},`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
let names: string[] = [];
|
|
43
|
+
let reexports: string[] = [];
|
|
44
|
+
try {
|
|
45
|
+
const parsed = parse(file.source);
|
|
46
|
+
names = parsed.exports.filter((n) => IDENT_RE.test(n) && n !== "default");
|
|
47
|
+
reexports = parsed.reexports;
|
|
48
|
+
} catch {
|
|
49
|
+
names = []; // lexer can't parse → default-only interop
|
|
50
|
+
}
|
|
51
|
+
const namedExports = [...new Set(names)]
|
|
52
|
+
.map((n) => `export const ${n} = module.exports.${n};`)
|
|
53
|
+
.join("\n");
|
|
54
|
+
// A `module.exports = require("x")` entry (e.g. React's `index.js`) has no
|
|
55
|
+
// own statically-lexable names — only a reexport. Surface x's named exports
|
|
56
|
+
// by re-exporting its already-served namespace, so `import { StrictMode }
|
|
57
|
+
// from "react"` (and the automatic-JSX `jsxDEV` import) resolve. `export *`
|
|
58
|
+
// never re-exports `default`, so the `export default module.exports` above
|
|
59
|
+
// stays authoritative.
|
|
60
|
+
const reexportLines = [...new Set(reexports)]
|
|
61
|
+
.map((spec) => `export * from ${JSON.stringify(rewrite(spec))};`)
|
|
62
|
+
.join("\n");
|
|
63
|
+
|
|
64
|
+
const dir = file.path.replace(/\/[^/]*$/, "");
|
|
65
|
+
return [
|
|
66
|
+
...importLines,
|
|
67
|
+
`const __ns = {\n${mapEntries.join("\n")}\n};`,
|
|
68
|
+
`const module = { exports: {} };`,
|
|
69
|
+
`const require = (s) => {`,
|
|
70
|
+
` const m = __ns[s];`,
|
|
71
|
+
` if (!m) throw new Error("Cannot require (computed/unresolved): " + s);`,
|
|
72
|
+
` return m.default !== undefined ? m.default : m;`,
|
|
73
|
+
`};`,
|
|
74
|
+
`(function (module, exports, require, __filename, __dirname) {`,
|
|
75
|
+
file.source,
|
|
76
|
+
`}).call(module.exports, module, module.exports, require, ${JSON.stringify(file.path)}, ${JSON.stringify(dir)});`,
|
|
77
|
+
`export default module.exports;`,
|
|
78
|
+
namedExports,
|
|
79
|
+
reexportLines,
|
|
80
|
+
]
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
.join("\n");
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { init, parse } from "es-module-lexer";
|
|
2
|
+
import type { SourceFile, Transform } from "../types.js";
|
|
3
|
+
import { toJs } from "./to-js.js";
|
|
4
|
+
|
|
5
|
+
let lexerReady: Promise<unknown> | undefined;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The default per-file transform for ESM (and TS/JSX) sources: transpile to plain
|
|
9
|
+
* JS, then rewrite every static/dynamic-string import & re-export specifier in
|
|
10
|
+
* place via `rewrite`, leaving quotes and everything else byte-for-byte intact.
|
|
11
|
+
* Computed dynamic specifiers (no static string) are left untouched.
|
|
12
|
+
*/
|
|
13
|
+
export function newEsmTransform(): Transform {
|
|
14
|
+
return {
|
|
15
|
+
async transform(file: SourceFile, rewrite: (specifier: string) => string): Promise<string> {
|
|
16
|
+
lexerReady ??= init;
|
|
17
|
+
await lexerReady;
|
|
18
|
+
const js = toJs(file.source, file.format, file.path);
|
|
19
|
+
const [imports] = parse(js, file.path);
|
|
20
|
+
let out = js;
|
|
21
|
+
// Splice from the end so earlier offsets stay valid. For static imports the
|
|
22
|
+
// [s,e) span sits *inside* the quotes; for dynamic `import("x")` it *includes*
|
|
23
|
+
// them — detect a leading quote and preserve it either way.
|
|
24
|
+
for (let i = imports.length - 1; i >= 0; i--) {
|
|
25
|
+
const imp = imports[i];
|
|
26
|
+
if (imp.n == null) continue; // computed/dynamic specifier — cannot rewrite statically
|
|
27
|
+
const q = out[imp.s];
|
|
28
|
+
const quoted = q === '"' || q === "'" || q === "`";
|
|
29
|
+
const replacement = quoted ? q + rewrite(imp.n) + q : rewrite(imp.n);
|
|
30
|
+
out = out.slice(0, imp.s) + replacement + out.slice(imp.e);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { FilesApi } from "@statewalker/webrun-files";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public contract for `@statewalker/webrun-modules`. Types only — the resolver
|
|
5
|
+
* core, the default npm `Source`, and the transforms are implemented separately.
|
|
6
|
+
* Contract source: notes/2026/2026-07/2026-07-23/grill-module-webrun-modules.md
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The build target — selects `exports` conditions and is part of the cache key. */
|
|
10
|
+
export type ModuleTarget = "browser" | "node";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A reference to resolve: either an npm package (with optional pinned version and
|
|
14
|
+
* subpath) or a URL (a local project script served by this server, or an already
|
|
15
|
+
* resolved local URL).
|
|
16
|
+
*/
|
|
17
|
+
export type ModuleRef = { pkg: string; version?: string; subpath?: string } | { url: string };
|
|
18
|
+
|
|
19
|
+
/** The result of resolving a `ModuleRef`: a directly `import`-able local URL. */
|
|
20
|
+
export interface ResolvedModule {
|
|
21
|
+
/** Importable URL — `/{name}@{version}/{subpath}` for a package, carries `basePath`. */
|
|
22
|
+
url: string;
|
|
23
|
+
target: ModuleTarget;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The version-dedupe map produced by `prime` and reusable as a pin input. One
|
|
28
|
+
* artifact, two roles: a partial lockfile pins only the names it lists; `prime`
|
|
29
|
+
* writes the complete one back to the cache.
|
|
30
|
+
*/
|
|
31
|
+
export type Lockfile = Record<string, string /* resolved version */>;
|
|
32
|
+
|
|
33
|
+
/** A parsed `package.json` (only the fields resolution reads). */
|
|
34
|
+
export interface PackageManifest {
|
|
35
|
+
name: string;
|
|
36
|
+
version: string;
|
|
37
|
+
main?: string;
|
|
38
|
+
module?: string;
|
|
39
|
+
browser?: string | Record<string, string | false>;
|
|
40
|
+
exports?: unknown;
|
|
41
|
+
imports?: unknown;
|
|
42
|
+
dependencies?: Record<string, string>;
|
|
43
|
+
peerDependencies?: Record<string, string>;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A package acquired by a `Source`: its untarred files plus parsed manifest. */
|
|
48
|
+
export interface LoadedPackage {
|
|
49
|
+
name: string;
|
|
50
|
+
version: string;
|
|
51
|
+
/** The untarred package tree — every file individually addressable. */
|
|
52
|
+
files: FilesApi;
|
|
53
|
+
manifest: PackageManifest;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Pluggable acquisition seam. The default impl fetches an npm registry tarball and
|
|
58
|
+
* untars it in memory; JSR and direct-URL are alternative impls.
|
|
59
|
+
*/
|
|
60
|
+
export interface Source {
|
|
61
|
+
/** Whether this source handles the given reference (npm | jsr | url). */
|
|
62
|
+
matches(ref: ModuleRef): boolean;
|
|
63
|
+
/** Resolve a concrete version, fetch bytes, untar, parse the manifest. */
|
|
64
|
+
load(ref: ModuleRef): Promise<LoadedPackage>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The source-file kinds the transform distinguishes. */
|
|
68
|
+
export type SourceFormat = "esm" | "cjs" | "ts" | "tsx";
|
|
69
|
+
|
|
70
|
+
export interface SourceFile {
|
|
71
|
+
path: string;
|
|
72
|
+
source: string;
|
|
73
|
+
format: SourceFormat;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Pluggable per-file transform seam. Turns one raw file into browser-runnable ESM,
|
|
78
|
+
* passing every discovered specifier through `rewrite` (which returns the
|
|
79
|
+
* same-origin, prefix-portable relative URL to emit).
|
|
80
|
+
*/
|
|
81
|
+
export interface Transform {
|
|
82
|
+
transform(file: SourceFile, rewrite: (specifier: string) => string): Promise<string>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ModuleServerOptions {
|
|
86
|
+
/** Injected cache — NodeFilesApi / browser-OPFS / MemFilesApi. Never node:fs. */
|
|
87
|
+
cache: FilesApi;
|
|
88
|
+
/** Optional: local project files served under their own paths (authored scripts). */
|
|
89
|
+
project?: FilesApi;
|
|
90
|
+
/** Acquisition sources; defaults to `[npmRegistrySource()]`. */
|
|
91
|
+
sources?: Source[];
|
|
92
|
+
/** Per-file transform; defaults to the sucrase-based ESM + CJS-interop transform. */
|
|
93
|
+
transform?: Transform;
|
|
94
|
+
/** Selects `exports` conditions and cache key. Defaults to `"browser"`. */
|
|
95
|
+
target?: ModuleTarget;
|
|
96
|
+
/** Optional lockfile input for reproducible resolution; also written by `prime`. */
|
|
97
|
+
lock?: Lockfile;
|
|
98
|
+
/** Mount prefix, e.g. `"/deps/v1/"`. Defaults to `"/"`. */
|
|
99
|
+
basePath?: string;
|
|
100
|
+
/** Prefix (relative to `basePath`) for external package URLs, isolating npm
|
|
101
|
+
* deps under e.g. `"deps/"` while authored project files stay at `~/`.
|
|
102
|
+
* Defaults to `""` — packages served alongside project files. */
|
|
103
|
+
depsPath?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ModuleServer {
|
|
107
|
+
/** Resolve a single reference to an importable local URL (resolve-on-miss). */
|
|
108
|
+
resolve(ref: ModuleRef, importer?: string): Promise<ResolvedModule>;
|
|
109
|
+
/** Walk the entry's whole graph, warm the cache, write `lock`. */
|
|
110
|
+
prime(entry: ModuleRef): Promise<ResolvedModule>;
|
|
111
|
+
/** Prime an entry and return every reachable module URL (the exact set of
|
|
112
|
+
* scripts required to run it, `basePath`-prefixed, sorted). */
|
|
113
|
+
listResources(entry: ModuleRef): Promise<string[]>;
|
|
114
|
+
/** List every file of a package (raw, package-relative paths), loading it if
|
|
115
|
+
* needed — the full package contents, not just the reachable subset. */
|
|
116
|
+
listPackageFiles(ref: ModuleRef): Promise<string[]>;
|
|
117
|
+
/** Standard Web handler; serves the cache and matches under `basePath`. */
|
|
118
|
+
fetch(request: Request): Promise<Response>;
|
|
119
|
+
/** The resolved graph — also the persisted lockfile. */
|
|
120
|
+
readonly lock: Lockfile;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Thrown when a package / version / subpath cannot be resolved. */
|
|
124
|
+
export class ModuleResolveError extends Error {
|
|
125
|
+
constructor(
|
|
126
|
+
readonly ref: ModuleRef,
|
|
127
|
+
readonly reason: string,
|
|
128
|
+
) {
|
|
129
|
+
super(`Cannot resolve ${JSON.stringify(ref)}: ${reason}`);
|
|
130
|
+
this.name = "ModuleResolveError";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Thrown when a file cannot be transformed to runnable ESM. */
|
|
135
|
+
export class ModuleTransformError extends Error {
|
|
136
|
+
constructor(
|
|
137
|
+
readonly path: string,
|
|
138
|
+
readonly reason: string,
|
|
139
|
+
) {
|
|
140
|
+
super(`Cannot transform ${path}: ${reason}`);
|
|
141
|
+
this.name = "ModuleTransformError";
|
|
142
|
+
}
|
|
143
|
+
}
|