@vesk/compiler 0.2.8 → 0.2.9
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/client-codegen.d.ts.map +1 -1
- package/dist/client-codegen.js +11 -1
- package/dist/ir-generator.d.ts.map +1 -1
- package/dist/ir-generator.js +99 -8
- package/dist/module-imports.d.ts +51 -0
- package/dist/module-imports.d.ts.map +1 -0
- package/dist/module-imports.js +715 -0
- package/dist/server-jsgen.d.ts.map +1 -1
- package/dist/server-jsgen.js +8 -3
- package/dist/server-render.d.ts.map +1 -1
- package/dist/server-render.js +16 -2
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +24 -1
- package/dist/typecheck.d.ts +1 -1
- package/dist/typecheck.d.ts.map +1 -1
- package/dist/typecheck.js +5 -0
- package/dist/vesk-plugin.d.ts +1 -0
- package/dist/vesk-plugin.d.ts.map +1 -1
- package/dist/vesk-plugin.js +366 -1
- package/dist/vsk-imports.d.ts.map +1 -1
- package/dist/vsk-imports.js +3 -1
- package/dist/vsk-tsx.d.ts.map +1 -1
- package/dist/vsk-tsx.js +12 -0
- package/package.json +2 -2
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, extname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { print } from 'esrap';
|
|
5
|
+
import ts from 'esrap/languages/ts';
|
|
6
|
+
import { parse } from '@vesk/compiler/src/parser';
|
|
7
|
+
import { stripTsTypes, hasTsSyntax, isTypeOnlyStatement } from '@vesk/compiler/src/strip-ts';
|
|
8
|
+
import { importModuleTarget } from '@vesk/compiler/src/tokens';
|
|
9
|
+
// =============================================================
|
|
10
|
+
// SSR module-value imports.
|
|
11
|
+
//
|
|
12
|
+
// The SSR scope (`__vesk`) is built by `buildComponentMap`/`loadRuntimeImports`
|
|
13
|
+
// and only ever contained two kinds of identifiers:
|
|
14
|
+
// - runtime exports (from `@vesk/runtime` / `@vesk/reactivity`)
|
|
15
|
+
// - top-level declarations in the `.vsk` file itself
|
|
16
|
+
//
|
|
17
|
+
// A `.vsk` component that imported a *value* from a plain `.ts`/`.js` module
|
|
18
|
+
// (e.g. `import { GUIDE } from '../lib/guide.ts'`) got those names into the
|
|
19
|
+
// client bundle but NEVER into the SSR scope — so the server component threw
|
|
20
|
+
// `ReferenceError: GUIDE is not defined` even though the client build was fine.
|
|
21
|
+
//
|
|
22
|
+
// This module closes that gap with a synchronous, self-contained module loader
|
|
23
|
+
// (no esbuild, no native `require(esm)`, works on the repo's supported Node):
|
|
24
|
+
// - resolves local/relative/absolute/bare specifiers with extension probing
|
|
25
|
+
// - strips TS, rewrites ESM `import`/`export` to a CJS `new Function` body
|
|
26
|
+
// - evaluates with a recursive `require` so nested relative imports work
|
|
27
|
+
// - caches per absolute path keyed on mtime so dev edits are picked up on
|
|
28
|
+
// the next compile without a process restart
|
|
29
|
+
//
|
|
30
|
+
// Scope notes: `@vesk/*` targets stay compiler-controlled (runtime scope),
|
|
31
|
+
// `.vsk` targets resolve through the component registry, and `.css`/`.md`
|
|
32
|
+
// targets carry no runtime value — none of them are loaded here.
|
|
33
|
+
//
|
|
34
|
+
// Supported export forms: named/`default`/namespace imports, `export const`/
|
|
35
|
+
// `let`/`var`/`function`/`class`, `export { b as a }`, `export { x } from`,
|
|
36
|
+
// `export * from`, `export * as ns from`, `export default <expr>`. CJS files
|
|
37
|
+
// without import/export statements run verbatim. Everything unsupported falls
|
|
38
|
+
// back to native `require()` when available and otherwise warns + skips.
|
|
39
|
+
// =============================================================
|
|
40
|
+
const RUNTIME_PREFIXES = ['@vesk/runtime/', '@vesk/reactivity/', '@vesk/types', '@vesk/'];
|
|
41
|
+
const EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs', '.jsx', '.json'];
|
|
42
|
+
/** Marker prefix returned by `resolveSsrModule` for Node builtins. */
|
|
43
|
+
const BUILTIN_PREFIX = '\u0000builtin:';
|
|
44
|
+
/** Node builtins already loaded (never go stale; no mtime tracking). */
|
|
45
|
+
const BUILTIN_CACHE = new Map();
|
|
46
|
+
const MODULE_CACHE = new Map();
|
|
47
|
+
/** Hard cap on cached modules — bounds long dev-session growth (LRU-ish: evicts oldest). */
|
|
48
|
+
const MAX_CACHE_ENTRIES = 256;
|
|
49
|
+
function cacheModule(p, val) {
|
|
50
|
+
MODULE_CACHE.delete(p);
|
|
51
|
+
MODULE_CACHE.set(p, val);
|
|
52
|
+
if (MODULE_CACHE.size > MAX_CACHE_ENTRIES) {
|
|
53
|
+
MODULE_CACHE.delete(MODULE_CACHE.keys().next().value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Active evaluation frames — an array (not a single slot) so nested module
|
|
58
|
+
* evaluation keeps each ancestor's closure live. Every resolve performed by
|
|
59
|
+
* `createModuleRequire` is recorded into ALL frames, so a module's invalidation
|
|
60
|
+
* set is its full transitive dependency closure (editing a leaf invalidates
|
|
61
|
+
* every ancestor that transitively depends on it).
|
|
62
|
+
*/
|
|
63
|
+
const depStack = [];
|
|
64
|
+
/** True when the import target is owned by the framework (never SSR-loaded). */
|
|
65
|
+
function isCompilerOwnedTarget(target) {
|
|
66
|
+
if (target === '@vesk/runtime' || target === '@vesk/reactivity')
|
|
67
|
+
return true;
|
|
68
|
+
for (const prefix of RUNTIME_PREFIXES) {
|
|
69
|
+
if (target.startsWith(prefix))
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
/** True when the target carries no value into the SSR scope. */
|
|
75
|
+
function isValueLessTarget(target) {
|
|
76
|
+
return (target.endsWith('.vsk') ||
|
|
77
|
+
target.endsWith('.css') ||
|
|
78
|
+
target.endsWith('.md') ||
|
|
79
|
+
target.endsWith('.markdown'));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* AST-driven extraction of the value bindings introduced by one import
|
|
83
|
+
* statement (`import { a as b } from 'm'` → `[{ local: 'b', imported: 'a' }]`;
|
|
84
|
+
* `import X from 'm'` → `[{ local: 'X', imported: 'default' }]`;
|
|
85
|
+
* `import * as X from 'm'` → `[{ local: 'X', imported: '*' }]`). Type-only
|
|
86
|
+
* specifiers are skipped. Side-effect imports return `[]`.
|
|
87
|
+
*/
|
|
88
|
+
export function importBindingPairs(imp) {
|
|
89
|
+
const pairs = [];
|
|
90
|
+
let ast = null;
|
|
91
|
+
try {
|
|
92
|
+
ast = parse(imp, { filename: 'import.mjs' });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
ast = null;
|
|
96
|
+
}
|
|
97
|
+
if (!ast)
|
|
98
|
+
return pairs;
|
|
99
|
+
const stmt = (ast.body || []).find((n) => n.type === 'ImportDeclaration');
|
|
100
|
+
if (!stmt)
|
|
101
|
+
return pairs;
|
|
102
|
+
const specifiers = (stmt.specifiers || []);
|
|
103
|
+
for (const spec of specifiers) {
|
|
104
|
+
if (spec.importKind === 'type')
|
|
105
|
+
continue;
|
|
106
|
+
const local = spec.local?.name;
|
|
107
|
+
if (!local)
|
|
108
|
+
continue;
|
|
109
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
110
|
+
pairs.push({ local, imported: 'default' });
|
|
111
|
+
}
|
|
112
|
+
else if (spec.type === 'ImportNamespaceSpecifier') {
|
|
113
|
+
pairs.push({ local, imported: '*' });
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const importedSpec = (spec.imported || spec.local);
|
|
117
|
+
const imported = importedSpec.name ?? importedSpec.value;
|
|
118
|
+
if (imported)
|
|
119
|
+
pairs.push({ local, imported });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return pairs;
|
|
123
|
+
}
|
|
124
|
+
/** Local binding names imported from non-runtime, non-`.vsk` modules. */
|
|
125
|
+
export function localValueImportNames(importStrs) {
|
|
126
|
+
const names = [];
|
|
127
|
+
for (const imp of importStrs) {
|
|
128
|
+
const target = importModuleTarget(imp);
|
|
129
|
+
if (!target || isCompilerOwnedTarget(target) || isValueLessTarget(target))
|
|
130
|
+
continue;
|
|
131
|
+
for (const pair of importBindingPairs(imp))
|
|
132
|
+
names.push(pair.local);
|
|
133
|
+
}
|
|
134
|
+
return names;
|
|
135
|
+
}
|
|
136
|
+
/** True when an import line should be SSR-loaded (non-runtime, non-empty, non-`.vsk`). */
|
|
137
|
+
export function isLocalValueImport(imp) {
|
|
138
|
+
const target = importModuleTarget(imp);
|
|
139
|
+
if (!target || isCompilerOwnedTarget(target) || isValueLessTarget(target))
|
|
140
|
+
return false;
|
|
141
|
+
return importBindingPairs(imp).length > 0;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Loads every local value import and merges its exports into `__vesk` so SSR
|
|
145
|
+
* component bodies can reference them. Pure side-effect imports (`import './x'`)
|
|
146
|
+
* are also resolved and executed so their module-level setup runs client AND
|
|
147
|
+
* server. Resolution is relative to `sourcePath` (the importing `.vsk` file).
|
|
148
|
+
*
|
|
149
|
+
* Best-effort: unresolvable/unloadable modules warn and are skipped, matching
|
|
150
|
+
* how unresolvable `.vsk` imports are handled. Unsupported ESM constructs
|
|
151
|
+
* (import.meta/top-level await) THROW a specific error — loading them would
|
|
152
|
+
* silently yield `undefined` at render.
|
|
153
|
+
*/
|
|
154
|
+
export function applyLocalModuleImports(__vesk, importStrs, sourcePath) {
|
|
155
|
+
if (!sourcePath)
|
|
156
|
+
return;
|
|
157
|
+
const fromDir = dirname(sourcePath);
|
|
158
|
+
for (const imp of importStrs) {
|
|
159
|
+
const target = importModuleTarget(imp);
|
|
160
|
+
if (!target || isCompilerOwnedTarget(target) || isValueLessTarget(target))
|
|
161
|
+
continue;
|
|
162
|
+
const resolved = resolveSsrModule(target, fromDir);
|
|
163
|
+
if (!resolved) {
|
|
164
|
+
console.warn(`[vesk] SSR: cannot resolve "${target}" imported by ${sourcePath} — the imported name will be undefined during server render.`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// Load always (a side-effect import runs the module's top-level code);
|
|
168
|
+
// merge values only when the import actually binds names.
|
|
169
|
+
const mod = loadSsrModule(resolved);
|
|
170
|
+
if (!mod || typeof mod !== 'object')
|
|
171
|
+
continue;
|
|
172
|
+
for (const pair of importBindingPairs(imp)) {
|
|
173
|
+
if (pair.imported === '*') {
|
|
174
|
+
__vesk[pair.local] = mod;
|
|
175
|
+
}
|
|
176
|
+
else if (pair.imported in mod) {
|
|
177
|
+
__vesk[pair.local] = mod[pair.imported];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Best-effort: run the module through the native loader when possible. */
|
|
183
|
+
function nativeRequireFallback(absPath) {
|
|
184
|
+
try {
|
|
185
|
+
// resolution is by absolute path so a bare/relative `require()` inside the
|
|
186
|
+
// module is Node-handled (via createRequire ancestry).
|
|
187
|
+
const req = createRequire(absPath);
|
|
188
|
+
const loaded = req(absPath);
|
|
189
|
+
if (loaded && typeof loaded === 'object')
|
|
190
|
+
return loaded;
|
|
191
|
+
if (loaded !== null && loaded !== undefined)
|
|
192
|
+
return { default: loaded };
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Loads (and caches) a module's export object. Cache entries track both the
|
|
201
|
+
* module's own mtime and the mtimes of everything it transitively resolved,
|
|
202
|
+
* so a change to any dependency invalidates every module that pulls it in.
|
|
203
|
+
* Builtin markers load through Node and are cached separately (never stale).
|
|
204
|
+
*/
|
|
205
|
+
export function loadSsrModule(absPath) {
|
|
206
|
+
if (isBuiltinPath(absPath)) {
|
|
207
|
+
return loadBuiltin(absPath.slice(BUILTIN_PREFIX.length));
|
|
208
|
+
}
|
|
209
|
+
let mtimeMs = 0;
|
|
210
|
+
try {
|
|
211
|
+
mtimeMs = statSync(absPath).mtimeMs;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
// A module that is still evaluating links back to live partial exports —
|
|
217
|
+
// the CJS analogue of ESM circular-import tolerance (no infinite recursion).
|
|
218
|
+
const inFlight = EVALUATING.get(absPath);
|
|
219
|
+
if (inFlight !== undefined)
|
|
220
|
+
return inFlight;
|
|
221
|
+
const cachedVal = MODULE_CACHE.get(absPath);
|
|
222
|
+
if (cachedVal && cachedVal.mtimeMs === mtimeMs && depsFresh(cachedVal))
|
|
223
|
+
return cachedVal.exports;
|
|
224
|
+
// Capture the transitive closure of this evaluation into the live frames.
|
|
225
|
+
const frame = new Set();
|
|
226
|
+
depStack.push(frame);
|
|
227
|
+
try {
|
|
228
|
+
let exportsObj = null;
|
|
229
|
+
if (absPath.endsWith('.json')) {
|
|
230
|
+
try {
|
|
231
|
+
exportsObj = JSON.parse(readFileSync(absPath, 'utf-8'));
|
|
232
|
+
if (exportsObj && typeof exportsObj === 'object' && !('default' in exportsObj)) {
|
|
233
|
+
exportsObj.default = exportsObj;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
exportsObj = null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
exportsObj = {};
|
|
242
|
+
EVALUATING.set(absPath, exportsObj);
|
|
243
|
+
const ran = evaluateModuleFile(absPath, exportsObj);
|
|
244
|
+
// Runtime evaluation failure (already warned) — give Node's own loader
|
|
245
|
+
// a chance before giving up.
|
|
246
|
+
if (!ran)
|
|
247
|
+
exportsObj = null;
|
|
248
|
+
}
|
|
249
|
+
if (exportsObj === null) {
|
|
250
|
+
exportsObj = nativeRequireFallback(absPath);
|
|
251
|
+
}
|
|
252
|
+
if (exportsObj === null)
|
|
253
|
+
return null;
|
|
254
|
+
cacheModule(absPath, { mtimeMs, deps: captureClosure(frame), exports: exportsObj });
|
|
255
|
+
return exportsObj;
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
depStack.pop();
|
|
259
|
+
EVALUATING.delete(absPath);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** Snapshots the current stat of every file in a module's closure. */
|
|
263
|
+
function captureClosure(frame) {
|
|
264
|
+
const deps = [];
|
|
265
|
+
for (const dep of frame) {
|
|
266
|
+
try {
|
|
267
|
+
deps.push({ path: dep, mtimeMs: statSync(dep).mtimeMs });
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Missing now, but the parent re-check will hit the missing file and
|
|
271
|
+
// treat it as changed — record it so stale entries get dropped.
|
|
272
|
+
deps.push({ path: dep, mtimeMs: -1 });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return deps;
|
|
276
|
+
}
|
|
277
|
+
/** True when every file in the module's dependency closure still matches. */
|
|
278
|
+
function depsFresh(cachedVal) {
|
|
279
|
+
for (const dep of cachedVal.deps) {
|
|
280
|
+
try {
|
|
281
|
+
if (statSync(dep.path).mtimeMs !== dep.mtimeMs)
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Runs one module file. Returns `true` when the body executed, `false` when a
|
|
292
|
+
* runtime failure was already warned about. Unsupported ESM constructs throw —
|
|
293
|
+
* the only honest outcome is a loud, specific error, never silently undefined.
|
|
294
|
+
*/
|
|
295
|
+
function evaluateModuleFile(absPath, exportsObj) {
|
|
296
|
+
let raw;
|
|
297
|
+
try {
|
|
298
|
+
raw = readFileSync(absPath, 'utf-8');
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
console.warn(`[vesk] SSR: failed to read ${absPath}: ${err?.message ?? String(err)}`);
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
let ast = null;
|
|
305
|
+
try {
|
|
306
|
+
ast = parse(raw, { filename: absPath });
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
ast = null;
|
|
310
|
+
}
|
|
311
|
+
if (!ast) {
|
|
312
|
+
// Not parseable as ESM — run verbatim as CJS.
|
|
313
|
+
try {
|
|
314
|
+
const fn = new Function('require', 'module', 'exports', '__dirname', '__filename', raw);
|
|
315
|
+
fn(createModuleRequire(dirname(absPath)), { exports: exportsObj }, exportsObj, dirname(absPath), absPath);
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
console.warn(`[vesk] SSR: failed to load ${absPath}: ${err?.message ?? String(err)}`);
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
const unsupported = findUnsupportedEsm(ast.body);
|
|
324
|
+
if (unsupported) {
|
|
325
|
+
throw new Error(`${absPath} uses ${unsupported} — not representable in the SSR module loader. ` +
|
|
326
|
+
`Split it out of the module or avoid ${unsupported} in .vsk-imported code.`);
|
|
327
|
+
}
|
|
328
|
+
let stripped = ast;
|
|
329
|
+
if (hasTsSyntax(ast))
|
|
330
|
+
stripped = stripTsTypes(ast);
|
|
331
|
+
stripped.body = (stripped.body || []).filter((n) => {
|
|
332
|
+
if (!n)
|
|
333
|
+
return false;
|
|
334
|
+
return !isTypeOnlyStatement(n);
|
|
335
|
+
});
|
|
336
|
+
const body = esmToCjs(stripped.body);
|
|
337
|
+
// ESM modules are always strict; mirror per-spec `this === undefined`.
|
|
338
|
+
try {
|
|
339
|
+
const fn = new Function('require', 'module', 'exports', '__dirname', '__filename', "'use strict';\n" + body);
|
|
340
|
+
fn(createModuleRequire(dirname(absPath)), { exports: exportsObj }, exportsObj, dirname(absPath), absPath);
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
console.warn(`[vesk] SSR: failed to load ${absPath}: ${err?.message ?? String(err)}`);
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
const EVALUATING = new Map();
|
|
349
|
+
/**
|
|
350
|
+
* Detects ESM constructs the CJS rewrite cannot express, returning a
|
|
351
|
+
* human-readable name (`import.meta`, `top-level await`) or `null`.
|
|
352
|
+
* Nested usage inside function/class bodies is legal ESM and fine here.
|
|
353
|
+
*/
|
|
354
|
+
function findUnsupportedEsm(body) {
|
|
355
|
+
for (const stmt of body) {
|
|
356
|
+
const hit = scanUnsupportedNode(stmt);
|
|
357
|
+
if (hit)
|
|
358
|
+
return hit;
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
function scanUnsupportedNode(node, inFunction = false) {
|
|
363
|
+
if (!node || typeof node !== 'object')
|
|
364
|
+
return null;
|
|
365
|
+
const n = node;
|
|
366
|
+
const t = n.type;
|
|
367
|
+
if (t === 'MetaProperty' || (t === 'MetaProperty' && n.meta?.name === 'import')) {
|
|
368
|
+
return 'import.meta';
|
|
369
|
+
}
|
|
370
|
+
if (!inFunction && t === 'AwaitExpression')
|
|
371
|
+
return 'top-level await';
|
|
372
|
+
if (t === 'FunctionDeclaration' || t === 'FunctionExpression' ||
|
|
373
|
+
t === 'ArrowFunctionExpression' || t === 'ClassDeclaration' || t === 'ClassExpression') {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
for (const key of Object.keys(n)) {
|
|
377
|
+
const val = n[key];
|
|
378
|
+
if (Array.isArray(val)) {
|
|
379
|
+
for (const item of val) {
|
|
380
|
+
if (item && typeof item === 'object') {
|
|
381
|
+
const sub = scanUnsupportedNode(item, inFunction);
|
|
382
|
+
if (sub)
|
|
383
|
+
return sub;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
else if (val && typeof val === 'object') {
|
|
388
|
+
const sub = scanUnsupportedNode(val, inFunction);
|
|
389
|
+
if (sub)
|
|
390
|
+
return sub;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
/** Recursive `require` used inside evaluated modules, rooted at their dir. */
|
|
396
|
+
function createModuleRequire(fromDir) {
|
|
397
|
+
return (specifier) => {
|
|
398
|
+
const resolved = resolveSsrModule(specifier, fromDir);
|
|
399
|
+
if (!resolved)
|
|
400
|
+
throw new Error(`Cannot find module '${specifier}'`);
|
|
401
|
+
// Builtins are handled natively; files join every live evaluation frame so
|
|
402
|
+
// the transitive closure stays fresh.
|
|
403
|
+
for (const frame of depStack)
|
|
404
|
+
frame.add(resolved);
|
|
405
|
+
const loaded = loadSsrModule(resolved);
|
|
406
|
+
if (loaded === null)
|
|
407
|
+
throw new Error(`Cannot load module '${specifier}'`);
|
|
408
|
+
return loaded;
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Resolves a specifier to an absolute file path (or a `BUILTIN_PREFIX` marker
|
|
413
|
+
* for Node builtins). Relative/absolute localities use extension probing;
|
|
414
|
+
* bare specifiers (`npm-package`, `pkg/subpath`, `node:fs`, `fs`) prefer
|
|
415
|
+
* Node's own resolver — which understands `exports` maps, conditions, scoped
|
|
416
|
+
* packages and builtins — and fall back to a `node_modules` walk-up.
|
|
417
|
+
*
|
|
418
|
+
* Every resolved file path is normalized through `realpathSync` so two
|
|
419
|
+
* specifiers pointing at the same physical file (e.g. pnpm/yarn symlinked
|
|
420
|
+
* `node_modules`) share one cache entry and one module instance.
|
|
421
|
+
*/
|
|
422
|
+
export function resolveSsrModule(specifier, fromDir) {
|
|
423
|
+
let resolved = null;
|
|
424
|
+
if (specifier === '.' || specifier === '..' || isAbsolute(specifier)) {
|
|
425
|
+
resolved = probeFile(resolve(specifier));
|
|
426
|
+
}
|
|
427
|
+
else if (specifier.startsWith('./') || specifier.startsWith('../')) {
|
|
428
|
+
resolved = probeFile(resolve(fromDir, specifier));
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
// Bare specifier — prefer the native resolver (exports map, conditions,
|
|
432
|
+
// builtins, symlinks), then fall back to a node_modules walk-up.
|
|
433
|
+
const native = nativeResolve(specifier, fromDir);
|
|
434
|
+
if (native)
|
|
435
|
+
return native; // realpath'd inside nativeResolve / builtin marker
|
|
436
|
+
let dir = fromDir;
|
|
437
|
+
for (let depth = 0; depth < 64; depth++) {
|
|
438
|
+
const base = join(dir, 'node_modules', specifier);
|
|
439
|
+
const found = probeFile(base);
|
|
440
|
+
if (found) {
|
|
441
|
+
resolved = found;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
const parent = dirname(dir);
|
|
445
|
+
if (parent === dir)
|
|
446
|
+
break;
|
|
447
|
+
dir = parent;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return resolved ? toRealPath(resolved) : null;
|
|
451
|
+
}
|
|
452
|
+
/** Normalizes a resolved file path through `realpathSync` (no-op on failure). */
|
|
453
|
+
function toRealPath(p) {
|
|
454
|
+
try {
|
|
455
|
+
return realpathSync(p);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
return p;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Native resolution of a bare specifier. `createRequire.resolve` returns an
|
|
463
|
+
* absolute file path for packages; for builtins it returns the bare specifier
|
|
464
|
+
* itself (`'node:fs'` / `'fs'`), which `resolveSsrModule` marks as a builtin.
|
|
465
|
+
*/
|
|
466
|
+
function nativeResolve(specifier, fromDir) {
|
|
467
|
+
try {
|
|
468
|
+
const req = createRequire(join(fromDir, '__vesk_resolve__.js'));
|
|
469
|
+
const resolved = req.resolve(specifier);
|
|
470
|
+
if (isAbsolute(resolved))
|
|
471
|
+
return toRealPath(resolved);
|
|
472
|
+
return builtinMarker(resolved);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function builtinMarker(name) {
|
|
479
|
+
return BUILTIN_PREFIX + name;
|
|
480
|
+
}
|
|
481
|
+
function isBuiltinPath(p) {
|
|
482
|
+
return p.startsWith(BUILTIN_PREFIX);
|
|
483
|
+
}
|
|
484
|
+
/** Loads a Node builtin module (never cached in `MODULE_CACHE`, never stale). */
|
|
485
|
+
function loadBuiltin(name) {
|
|
486
|
+
const id = name.startsWith('node:') ? name : `node:${name}`;
|
|
487
|
+
const cachedVal = BUILTIN_CACHE.get(id);
|
|
488
|
+
if (cachedVal)
|
|
489
|
+
return cachedVal;
|
|
490
|
+
try {
|
|
491
|
+
const req = createRequire(join('/', '__vesk_builtin__.js'));
|
|
492
|
+
const loaded = req(id);
|
|
493
|
+
let mod = null;
|
|
494
|
+
if (loaded && typeof loaded === 'object')
|
|
495
|
+
mod = loaded;
|
|
496
|
+
else if (loaded !== null && loaded !== undefined)
|
|
497
|
+
mod = { default: loaded };
|
|
498
|
+
if (mod)
|
|
499
|
+
BUILTIN_CACHE.set(id, mod);
|
|
500
|
+
return mod;
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function statOrNull(p) {
|
|
507
|
+
try {
|
|
508
|
+
return statSync(p);
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function probeFile(base) {
|
|
515
|
+
const st = statOrNull(base);
|
|
516
|
+
if (st && st.isFile())
|
|
517
|
+
return base;
|
|
518
|
+
if (st && st.isDirectory()) {
|
|
519
|
+
const pkgPath = join(base, 'package.json');
|
|
520
|
+
const pkgSt = statOrNull(pkgPath);
|
|
521
|
+
if (pkgSt && pkgSt.isFile()) {
|
|
522
|
+
try {
|
|
523
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
524
|
+
if (typeof pkg.main === 'string' && pkg.main.length > 0) {
|
|
525
|
+
const viaMain = probeFile(resolve(base, pkg.main));
|
|
526
|
+
if (viaMain)
|
|
527
|
+
return viaMain;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
// ignore malformed package.json
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return probeFile(join(base, 'index'));
|
|
535
|
+
}
|
|
536
|
+
const ext = extname(base);
|
|
537
|
+
if (ext.length === 0 || '/\\'.includes(base[base.length - 1])) {
|
|
538
|
+
for (const suffix of EXTENSIONS) {
|
|
539
|
+
const candidate = base + suffix;
|
|
540
|
+
const cst = statOrNull(candidate);
|
|
541
|
+
if (cst && cst.isFile())
|
|
542
|
+
return candidate;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
// ---------------------------------------------------------------------------
|
|
548
|
+
// ESM -> CJS rewriting (AST-driven; never regex).
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
function astName(node) {
|
|
551
|
+
if (!node)
|
|
552
|
+
return 'undefined';
|
|
553
|
+
if (node.type === 'Identifier' && typeof node.name === 'string')
|
|
554
|
+
return node.name;
|
|
555
|
+
return printNode(node);
|
|
556
|
+
}
|
|
557
|
+
function printNode(node) {
|
|
558
|
+
try {
|
|
559
|
+
return print(node, ts()).code.trim();
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
return '';
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function memberAccess(obj, prop) {
|
|
566
|
+
if (prop.type === 'Identifier' && typeof prop.name === 'string')
|
|
567
|
+
return `${obj}.${prop.name}`;
|
|
568
|
+
if ((prop.type === 'Literal' || prop.type === 'StringLiteral') && typeof prop.value === 'string') {
|
|
569
|
+
return `${obj}[${JSON.stringify(prop.value)}]`;
|
|
570
|
+
}
|
|
571
|
+
return `${obj}[${astName(prop)}]`;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Unquoted string value of an export key node (Identifier name, string literal
|
|
575
|
+
* value, or the printed node with its surrounding quotes stripped).
|
|
576
|
+
*/
|
|
577
|
+
function exportKeyName(node) {
|
|
578
|
+
if (!node)
|
|
579
|
+
return '';
|
|
580
|
+
if (node.type === 'Identifier' && typeof node.name === 'string')
|
|
581
|
+
return node.name;
|
|
582
|
+
if ((node.type === 'Literal' || node.type === 'StringLiteral') && typeof node.value === 'string')
|
|
583
|
+
return node.value;
|
|
584
|
+
const printed = printNode(node);
|
|
585
|
+
return stripOuterQuotes(printed);
|
|
586
|
+
}
|
|
587
|
+
function stripOuterQuotes(s) {
|
|
588
|
+
if (s.length >= 2) {
|
|
589
|
+
const first = s[0];
|
|
590
|
+
const last = s[s.length - 1];
|
|
591
|
+
if ((first === '"' || first === "'" || first === '`') && first === last)
|
|
592
|
+
return s.slice(1, -1);
|
|
593
|
+
}
|
|
594
|
+
return s;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Emits a live-binding getter for an export, so consumers observe mutations
|
|
598
|
+
* the way real ESM live bindings do (a snapshot `exports.x = x` goes stale).
|
|
599
|
+
*/
|
|
600
|
+
function exportGetter(lines, key, valueExpr) {
|
|
601
|
+
lines.push(`Object.defineProperty(exports, ${JSON.stringify(key)}, { get: () => ${valueExpr}, enumerable: true });`);
|
|
602
|
+
}
|
|
603
|
+
let exportCounter = 0;
|
|
604
|
+
/** Rewrites an ESM program body (imports/exports removed) into CJS statements. */
|
|
605
|
+
export function esmToCjs(body) {
|
|
606
|
+
const lines = [];
|
|
607
|
+
for (const stmt of body) {
|
|
608
|
+
switch (stmt.type) {
|
|
609
|
+
case 'ImportDeclaration': {
|
|
610
|
+
const source = printNode(stmt.source);
|
|
611
|
+
const specifiers = stmt.specifiers || [];
|
|
612
|
+
if (specifiers.length === 0) {
|
|
613
|
+
lines.push(`require(${source});`);
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
for (const spec of specifiers) {
|
|
617
|
+
if (spec.type === 'ImportNamespaceSpecifier') {
|
|
618
|
+
lines.push(`const ${astName(spec.local)} = require(${source});`);
|
|
619
|
+
}
|
|
620
|
+
else if (spec.type === 'ImportDefaultSpecifier') {
|
|
621
|
+
lines.push(`const ${astName(spec.local)} = require(${source}).default;`);
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
// ImportSpecifier
|
|
625
|
+
const local = astName(spec.local);
|
|
626
|
+
const imported = spec.imported;
|
|
627
|
+
if (imported && spec.importKind === 'type')
|
|
628
|
+
continue;
|
|
629
|
+
lines.push(`const ${local} = ${memberAccess(`require(${source})`, imported || spec.local)};`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
case 'ExportNamedDeclaration': {
|
|
635
|
+
if (stmt.exportKind === 'type')
|
|
636
|
+
continue;
|
|
637
|
+
const declaration = stmt.declaration;
|
|
638
|
+
const source = stmt.source ? printNode(stmt.source) : null;
|
|
639
|
+
if (declaration) {
|
|
640
|
+
const printed = printNode(declaration);
|
|
641
|
+
if (printed) {
|
|
642
|
+
lines.push(printed);
|
|
643
|
+
if (declaration.type === 'VariableDeclaration') {
|
|
644
|
+
const declarators = declaration.declarations || [];
|
|
645
|
+
for (const d of declarators) {
|
|
646
|
+
if (d.id && d.id.type === 'Identifier')
|
|
647
|
+
exportGetter(lines, exportKeyName(d.id), d.id.name ?? '');
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
else if (declaration.id) {
|
|
651
|
+
const name = declaration.id;
|
|
652
|
+
exportGetter(lines, exportKeyName(name), name.name ?? '');
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
else if (source) {
|
|
657
|
+
const modVar = `__veskExport${exportCounter++}`;
|
|
658
|
+
lines.push(`const ${modVar} = require(${source});`);
|
|
659
|
+
const specifiers = stmt.specifiers || [];
|
|
660
|
+
for (const spec of specifiers) {
|
|
661
|
+
if (spec.exportKind === 'type')
|
|
662
|
+
continue;
|
|
663
|
+
const local = spec.local;
|
|
664
|
+
const exported = spec.exported;
|
|
665
|
+
exportGetter(lines, exportKeyName(exported), memberAccess(modVar, local));
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
const specifiers = stmt.specifiers || [];
|
|
670
|
+
for (const spec of specifiers) {
|
|
671
|
+
if (spec.exportKind === 'type')
|
|
672
|
+
continue;
|
|
673
|
+
const local = spec.local;
|
|
674
|
+
const exported = spec.exported;
|
|
675
|
+
exportGetter(lines, exportKeyName(exported), astName(local));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
case 'ExportDefaultDeclaration': {
|
|
681
|
+
const declaration = stmt.declaration;
|
|
682
|
+
if (declaration.type === 'FunctionDeclaration' || declaration.type === 'ClassDeclaration') {
|
|
683
|
+
if (declaration.id) {
|
|
684
|
+
const name = declaration.id.name;
|
|
685
|
+
lines.push(printNode(declaration));
|
|
686
|
+
lines.push(`exports.default = ${name};`);
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
const expr = { ...declaration, type: declaration.type === 'FunctionDeclaration' ? 'FunctionExpression' : 'ClassExpression' };
|
|
690
|
+
lines.push(`exports.default = ${printNode(expr)};`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
lines.push(`exports.default = ${printNode(declaration)};`);
|
|
695
|
+
}
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
case 'ExportAllDeclaration': {
|
|
699
|
+
const source = printNode(stmt.source);
|
|
700
|
+
const modVar = `__veskExport${exportCounter++}`;
|
|
701
|
+
lines.push(`const ${modVar} = require(${source});`);
|
|
702
|
+
if (stmt.exported) {
|
|
703
|
+
lines.push(`exports[${JSON.stringify(exportKeyName(stmt.exported))}] = ${modVar};`);
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
lines.push(`for (const __veskKey in ${modVar}) { if (__veskKey !== 'default' && __veskKey !== '__esModule' && !(__veskKey in exports)) exports[__veskKey] = ${modVar}[__veskKey]; }`);
|
|
707
|
+
}
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
default:
|
|
711
|
+
lines.push(printNode(stmt));
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return lines.join('\n');
|
|
715
|
+
}
|