@vesk/compiler 0.1.14 → 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.
Files changed (50) hide show
  1. package/dist/api-routes.d.ts +6 -1
  2. package/dist/api-routes.d.ts.map +1 -1
  3. package/dist/api-routes.js +52 -7
  4. package/dist/client-codegen.d.ts +15 -0
  5. package/dist/client-codegen.d.ts.map +1 -1
  6. package/dist/client-codegen.js +84 -11
  7. package/dist/config.d.ts +2 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +25 -0
  10. package/dist/errors.d.ts.map +1 -1
  11. package/dist/errors.js +33 -4
  12. package/dist/ir-generator.d.ts.map +1 -1
  13. package/dist/ir-generator.js +208 -9
  14. package/dist/md-inline.d.ts +17 -0
  15. package/dist/md-inline.d.ts.map +1 -0
  16. package/dist/md-inline.js +149 -0
  17. package/dist/middleware.d.ts.map +1 -1
  18. package/dist/middleware.js +42 -7
  19. package/dist/module-imports.d.ts +51 -0
  20. package/dist/module-imports.d.ts.map +1 -0
  21. package/dist/module-imports.js +715 -0
  22. package/dist/router.d.ts +2 -0
  23. package/dist/router.d.ts.map +1 -1
  24. package/dist/router.js +48 -3
  25. package/dist/server-codegen.d.ts +1 -1
  26. package/dist/server-codegen.d.ts.map +1 -1
  27. package/dist/server-codegen.js +1 -1
  28. package/dist/server-jsgen.d.ts.map +1 -1
  29. package/dist/server-jsgen.js +9 -4
  30. package/dist/server-render.d.ts.map +1 -1
  31. package/dist/server-render.js +30 -10
  32. package/dist/server-utils.d.ts +31 -0
  33. package/dist/server-utils.d.ts.map +1 -1
  34. package/dist/server-utils.js +478 -52
  35. package/dist/tokens.d.ts.map +1 -1
  36. package/dist/tokens.js +24 -1
  37. package/dist/typecheck.d.ts +2 -2
  38. package/dist/typecheck.d.ts.map +1 -1
  39. package/dist/typecheck.js +71 -27
  40. package/dist/types.d.ts +4 -129
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/types.js +1 -0
  43. package/dist/vesk-plugin.d.ts +3 -0
  44. package/dist/vesk-plugin.d.ts.map +1 -1
  45. package/dist/vesk-plugin.js +518 -1
  46. package/dist/vsk-imports.d.ts.map +1 -1
  47. package/dist/vsk-imports.js +3 -1
  48. package/dist/vsk-tsx.d.ts.map +1 -1
  49. package/dist/vsk-tsx.js +27 -5
  50. package/package.json +2 -1
@@ -4,6 +4,7 @@ import { createBaseParser } from '@vesk/compiler/src/parser';
4
4
  import { skipWhitespace, findBalancedEnd, splitTopLevel, startsWithIdentifier, stripDeclKeyword, isWhitespaceChar, collapseNewlineWhitespace } from '@vesk/compiler/src/scan';
5
5
  import { stripCodeTypes } from '@vesk/compiler/src/strip-ts';
6
6
  import { stripTypeImport } from '@vesk/compiler/src/vsk-imports';
7
+ import { importBindingPairs } from '@vesk/compiler/src/module-imports';
7
8
  import { collectCalledIdentifiers, extractImportNames, importModuleTarget } from '@vesk/compiler/src/tokens';
8
9
  let __vskAnnotations = [];
9
10
  function parseExprNode(text) {
@@ -89,11 +90,11 @@ function componentUsesFetch(nodes) {
89
90
  return true;
90
91
  }
91
92
  else if (node instanceof RuntimeStatement) {
92
- if (node.raw.includes('useFetch('))
93
+ if (node.raw.includes('useFetch(') || node.raw.includes('useFetch.'))
93
94
  return true;
94
95
  }
95
96
  else if (node instanceof DynamicBinding) {
96
- if (node.expression.raw.includes('useFetch('))
97
+ if (node.expression.raw.includes('useFetch(') || node.expression.raw.includes('useFetch.'))
97
98
  return true;
98
99
  }
99
100
  else if (node instanceof MapRegion) {
@@ -238,6 +239,20 @@ function processAttribute(source, attr) {
238
239
  }
239
240
  return { name, value: '' };
240
241
  }
242
+ /**
243
+ * Recognizes a statement-mode `if (cond)` opening among JSX children.
244
+ * Returns the condition source, or null when `text` does not open an if
245
+ * header (keyword + balanced parens).
246
+ */
247
+ function extractIfHeader(text) {
248
+ if (!startsWithIdentifier(text, 'if'))
249
+ return null;
250
+ let i = skipWhitespace(text, 2);
251
+ if (text[i] !== '(')
252
+ return null;
253
+ const end = findBalancedEnd(text, i);
254
+ return text.slice(i + 1, end).trim();
255
+ }
241
256
  function extractForHeader(text) {
242
257
  if (!startsWithIdentifier(text, 'for'))
243
258
  return null;
@@ -271,6 +286,67 @@ function processJSXChildren(source, children) {
271
286
  i++;
272
287
  continue;
273
288
  }
289
+ // ── statement-mode if / else-if / else among element children ──
290
+ const ifCond = extractIfHeader(trimmed);
291
+ const seqNext = children[i + 1];
292
+ if (ifCond !== null &&
293
+ seqNext !== undefined &&
294
+ seqNext.type === 'JSXExpressionContainer' &&
295
+ seqNext.expression.type !== 'JSXEmptyExpression') {
296
+ const links = [];
297
+ let alternate = null;
298
+ const containerToNodes = (container) => {
299
+ const exprNode = container.expression;
300
+ if (exprNode.type === 'JSXFragment') {
301
+ const inner = [];
302
+ for (const c of exprNode.children)
303
+ inner.push(...processJSXChildren(source, [c]));
304
+ return inner;
305
+ }
306
+ if (exprNode.type === 'JSXElement')
307
+ return processJSXElement(source, exprNode);
308
+ return exprToIR(source, exprNode);
309
+ };
310
+ links.push({
311
+ cond: new Expression(ifCond, [], parseExprNode(ifCond), null),
312
+ nodes: containerToNodes(seqNext),
313
+ });
314
+ let j = i + 2;
315
+ while (j < children.length) {
316
+ const sep = children[j];
317
+ if (sep === undefined || sep.type !== 'JSXText')
318
+ break;
319
+ const sv = sep.value.trim();
320
+ const cont = children[j + 1];
321
+ if (sv.startsWith('} else if') || sv.startsWith('else if')) {
322
+ const ifIdx = sv.indexOf('if');
323
+ const header = extractIfHeader(sv.slice(ifIdx));
324
+ if (header === null || cont === undefined || cont.type !== 'JSXExpressionContainer')
325
+ break;
326
+ links.push({
327
+ cond: new Expression(header, [], parseExprNode(header), null),
328
+ nodes: containerToNodes(cont),
329
+ });
330
+ j += 2;
331
+ continue;
332
+ }
333
+ if (sv.startsWith('} else') || sv === 'else') {
334
+ if (cont === undefined || cont.type !== 'JSXExpressionContainer')
335
+ break;
336
+ alternate = containerToNodes(cont);
337
+ j += 2;
338
+ }
339
+ break;
340
+ }
341
+ // Fold right-to-left: earlier conditions wrap later ones as alternate.
342
+ let tail = alternate || [];
343
+ for (let k = links.length - 1; k >= 0; k--) {
344
+ tail = [new OpaqueDynamicRegion(links[k].cond, links[k].nodes, tail)];
345
+ }
346
+ result.push(...tail);
347
+ i = j;
348
+ continue;
349
+ }
274
350
  const forDecl = extractForHeader(trimmed);
275
351
  const nextChild = children[i + 1];
276
352
  if (forDecl !== null && nextChild !== undefined && (nextChild.type === 'JSXExpressionContainer' ||
@@ -392,6 +468,32 @@ function processJSXChildren(source, children) {
392
468
  result.push(...processJSXChildren(source, [c]));
393
469
  i++;
394
470
  }
471
+ else if (child.type === 'ForOfStatement') {
472
+ let alternate = [];
473
+ let consumed = 1;
474
+ const emptyText = children[i + 1];
475
+ const emptyContainer = children[i + 2];
476
+ if (emptyText && emptyText.type === 'JSXText' &&
477
+ ['#empty', 'empty'].includes(emptyText.value.trim()) &&
478
+ emptyContainer && emptyContainer.type === 'JSXExpressionContainer' &&
479
+ emptyContainer.expression.type !== 'JSXEmptyExpression') {
480
+ alternate = exprToIR(source, emptyContainer.expression);
481
+ consumed = 3;
482
+ }
483
+ result.push(...processForStatement(source, child, alternate));
484
+ i += consumed;
485
+ continue;
486
+ }
487
+ else if (child.type === 'IfStatement' || child.type === 'ForStatement' ||
488
+ child.type === 'ForInStatement' ||
489
+ child.type === 'WhileStatement' || child.type === 'DoWhileStatement' ||
490
+ child.type === 'SwitchStatement' || child.type === 'TryStatement' ||
491
+ child.type === 'VariableDeclaration' || child.type === 'ExpressionStatement' ||
492
+ child.type === 'ReturnStatement' || child.type === 'WithStatement' ||
493
+ child.type === 'LabeledStatement') {
494
+ result.push(...processStatementModeBody(source, [child]));
495
+ i++;
496
+ }
395
497
  else {
396
498
  i++;
397
499
  }
@@ -434,6 +536,30 @@ function exprToIR(source, expr) {
434
536
  }
435
537
  return [new DynamicBinding(toExpression(source, expr))];
436
538
  }
539
+ /**
540
+ * True when an `ExpressionStatement` expression is a pure value that should
541
+ * render as output (a bare reference, member access, literal, template,
542
+ * conditional, etc.) rather than a side-effecting statement (call, assignment,
543
+ * update, `new`, `await`, `yield`, tagged template, `delete`/`void`). Side
544
+ * effects stay as runtime statements; pure values become dynamic bindings.
545
+ */
546
+ function isRenderableExpression(expr) {
547
+ const t = expr.type;
548
+ if (t === 'CallExpression' || t === 'NewExpression' ||
549
+ t === 'AssignmentExpression' || t === 'UpdateExpression' ||
550
+ t === 'AwaitExpression' || t === 'YieldExpression' ||
551
+ t === 'TaggedTemplateExpression' || t === 'ImportExpression' ||
552
+ t === 'MetaProperty') {
553
+ return false;
554
+ }
555
+ if (t === 'UnaryExpression') {
556
+ return expr.operator !== 'delete' && expr.operator !== 'void';
557
+ }
558
+ if (t === 'SequenceExpression') {
559
+ return isRenderableExpression(expr.expressions[expr.expressions.length - 1]);
560
+ }
561
+ return true;
562
+ }
437
563
  function processJSXCallbackBody(source, body) {
438
564
  if (body.type === 'JSXElement')
439
565
  return processJSXElement(source, body);
@@ -531,12 +657,17 @@ function buildGuardChain(source, guardClauses, mainReturn) {
531
657
  const guard = guardClauses[i];
532
658
  const condExpr = toExpression(source, guard.test);
533
659
  const consequent = [];
534
- if (guard.consequent.type === 'ReturnStatement' && guard.consequent.argument) {
535
- if (guard.consequent.argument.type === 'JSXElement') {
536
- consequent.push(...processJSXElement(source, guard.consequent.argument));
660
+ const guardReturn = getReturnArgument(guard.consequent);
661
+ if (guardReturn) {
662
+ if (guardReturn.type === 'JSXElement') {
663
+ consequent.push(...processJSXElement(source, guardReturn));
664
+ }
665
+ else if (guardReturn.type === 'JSXFragment') {
666
+ for (const c of guardReturn.children)
667
+ consequent.push(...processJSXChildren(source, [c]));
537
668
  }
538
669
  else {
539
- consequent.push(new DynamicBinding(toExpression(source, guard.consequent.argument)));
670
+ consequent.push(new DynamicBinding(toExpression(source, guardReturn)));
540
671
  }
541
672
  }
542
673
  currentAlternate = [new OpaqueDynamicRegion(condExpr, consequent, currentAlternate)];
@@ -580,7 +711,24 @@ function hasJSXInSubtree(node) {
580
711
  return false;
581
712
  }
582
713
  function isGuardClause(node) {
583
- return node.type === 'IfStatement' && node.consequent.type === 'ReturnStatement' && hasJSXInSubtree(node.consequent);
714
+ return (node.type === 'IfStatement' &&
715
+ !node.alternate &&
716
+ getReturnArgument(node.consequent) !== null &&
717
+ hasJSXInSubtree(node.consequent));
718
+ }
719
+ /**
720
+ * Returns the `return` argument of a statement that represents an early
721
+ * return — either a bare `ReturnStatement` or a `BlockStatement` wrapping a
722
+ * single `ReturnStatement`. Returns `null` when the statement is not such a
723
+ * return (including `return null` / `return;`, whose argument is absent).
724
+ */
725
+ function getReturnArgument(node) {
726
+ if (node.type === 'ReturnStatement')
727
+ return node.argument ?? null;
728
+ if (node.type === 'BlockStatement' && node.body.length === 1 && node.body[0].type === 'ReturnStatement') {
729
+ return node.body[0].argument ?? null;
730
+ }
731
+ return null;
584
732
  }
585
733
  function isStatementMode(bodyStmts) {
586
734
  if (bodyStmts.some((s) => s.type === 'JSXElement' || s.type === 'JSXExpressionContainer' || s.type === 'JSXFragment'))
@@ -767,6 +915,16 @@ function processStatementModeBody(source, bodyStmts) {
767
915
  else if (stmt.type === 'ForStatement') {
768
916
  nodes.push(...processForStatement(source, stmt));
769
917
  }
918
+ else if (stmt.type === 'ExpressionStatement') {
919
+ if (isRenderableExpression(stmt.expression)) {
920
+ nodes.push(...exprToIR(source, stmt.expression));
921
+ }
922
+ else {
923
+ const raw = getSource(source, stmt);
924
+ if (raw)
925
+ nodes.push(new RuntimeStatement(raw, stmt, source));
926
+ }
927
+ }
770
928
  else if (stmt.type === 'ClassDeclaration') {
771
929
  throw VeskError.classDecl();
772
930
  }
@@ -835,6 +993,37 @@ function processEnum(node, source, exported) {
835
993
  const prefix = exported ? `export const ${name}` : `const ${name}`;
836
994
  return `${prefix} = { ${allPairs} };`;
837
995
  }
996
+ /**
997
+ * Normalizes any `@vesk/runtime/<subpath>` import specifier to the bare
998
+ * `'@vesk/runtime'` form. The runtime resolves all subpaths onto the same
999
+ * module graph, and downstream consumers (client scope injection, chunk
1000
+ * stripping) key off the canonical bare specifier. Char-scan based — no
1001
+ * regex, per repo rule.
1002
+ */
1003
+ function normalizeRuntimeSpecifier(stmt) {
1004
+ const fromIdx = stmt.indexOf(' from ');
1005
+ if (fromIdx === -1)
1006
+ return stmt;
1007
+ const q1 = idxOfQuote(stmt, fromIdx);
1008
+ if (q1 === -1)
1009
+ return stmt;
1010
+ const quote = stmt[q1];
1011
+ const q2 = stmt.indexOf(quote, q1 + 1);
1012
+ if (q2 === -1)
1013
+ return stmt;
1014
+ const spec = stmt.slice(q1 + 1, q2);
1015
+ if (spec === '@vesk/runtime' || !spec.startsWith('@vesk/runtime/'))
1016
+ return stmt;
1017
+ return stmt.slice(0, q1 + 1) + '@vesk/runtime' + stmt.slice(q2);
1018
+ }
1019
+ function idxOfQuote(s, from) {
1020
+ for (let i = from; i < s.length; i++) {
1021
+ const c = s[i];
1022
+ if (c === "'" || c === '"')
1023
+ return i;
1024
+ }
1025
+ return -1;
1026
+ }
838
1027
  export function generateIR(ast, source) {
839
1028
  __vskAnnotations = ast.__vskAnnotations ?? [];
840
1029
  const components = [];
@@ -846,9 +1035,10 @@ export function generateIR(ast, source) {
846
1035
  for (const node of ast.body) {
847
1036
  if (node.type === 'ImportDeclaration') {
848
1037
  const raw = getSource(source, node);
849
- const cleaned = stripTypeImport(raw);
1038
+ let cleaned = stripTypeImport(raw);
850
1039
  if (cleaned === null)
851
1040
  continue;
1041
+ cleaned = normalizeRuntimeSpecifier(cleaned);
852
1042
  imports.push(cleaned);
853
1043
  for (const spec of node.specifiers) {
854
1044
  if (spec.importKind === 'type')
@@ -991,6 +1181,7 @@ export function generateIR(ast, source) {
991
1181
  'Link', 'NavLink', 'Outlet', 'Redirect',
992
1182
  'Image', 'Portal',
993
1183
  'Experiment',
1184
+ 'LoadingIndicator', 'useLoadingIndicator',
994
1185
  'JsonLd', 'ArticleSchema', 'ProductSchema', 'FAQPageSchema', 'BreadcrumbListSchema',
995
1186
  'OrganizationSchema', 'LocalBusinessSchema', 'VideoSchema',
996
1187
  'effect', 'derived', 'untrack', 'peek', 'tick', 'flushSync', 'on_destroy',
@@ -1053,7 +1244,15 @@ export function generateIR(ast, source) {
1053
1244
  for (const n of extractImportNames(imp))
1054
1245
  existing.add(n);
1055
1246
  }
1056
- const missing = [...usedFunctions].filter(f => !existing.has(f));
1247
+ // A name already bound by the file's own imports (any target — including a
1248
+ // local module with the same export) wins; never auto-import a duplicate
1249
+ // binding. Mirrors client shadowing so SSR and client agree.
1250
+ const boundLocally = new Set();
1251
+ for (const imp of imports) {
1252
+ for (const pair of importBindingPairs(imp))
1253
+ boundLocally.add(pair.local);
1254
+ }
1255
+ const missing = [...usedFunctions].filter(f => !existing.has(f) && !boundLocally.has(f));
1057
1256
  if (missing.length > 0) {
1058
1257
  imports.push(`import { ${missing.join(', ')} } from '@vesk/runtime';`);
1059
1258
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Rewrites static `content="<file>.md"` attributes on <Md> elements into
3
+ * inlined constants. Returns the transformed source; when nothing matches,
4
+ * the original string is returned untouched.
5
+ *
6
+ * `mdRoots` are candidate project roots for absolute-style specifiers
7
+ * ("/x.md"); each root is tried as <root>/public<x> and <root><x>.
8
+ */
9
+ export declare function inlineMdContentAttrs(source: string, importerDir: string | null, mdRoots?: string[]): string;
10
+ /**
11
+ * Candidate project roots for absolute-style specifiers: walks up from
12
+ * `dir` collecting directories that contain a package.json (bounded).
13
+ */
14
+ export declare function guessProjectRoots(dir: string | null): string[];
15
+ /** Convenience wrapper used by pipelines that already know the file dir. */
16
+ export declare function inlineMdImportsFrom(source: string, importerFile: string | null, mdRoots?: string[]): string;
17
+ //# sourceMappingURL=md-inline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"md-inline.d.ts","sourceRoot":"","sources":["../src/md-inline.ts"],"names":[],"mappings":"AA2CA;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CACnC,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,OAAO,GAAE,MAAM,EAAO,GACpB,MAAM,CAoDR;AAoBD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,EAAE,CAY9D;AAED,4EAA4E;AAC5E,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,GAAE,MAAM,EAAO,GAAG,MAAM,CAG/G"}
@@ -0,0 +1,149 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ // =============================================================
4
+ // Inline resolution of <Md content="path/to/file.md"> attributes.
5
+ //
6
+ // When a `content="..."` attribute on an <Md> element is a static string
7
+ // ending in .md/.markdown, the file is read at COMPILE time and inlined
8
+ // into the source as a module-level const, so SSR, client chunks, SSG and
9
+ // production all work without runtime filesystem access.
10
+ //
11
+ // Resolution order for the specifier:
12
+ // - './x.md' / '../x.md' → relative to the importing .vsk file
13
+ // - '/x.md' → candidate roots (project dir / public),
14
+ // tried as <root>/public/x.md then <root>/x.md
15
+ // =============================================================
16
+ const MD_EXT_MARKER = '.md';
17
+ function looksLikeMarkdownPath(value) {
18
+ const v = value.trim();
19
+ if (v.length === 0 || v.length > 4096)
20
+ return false;
21
+ if (v.includes('\n') || v.includes('\r'))
22
+ return false;
23
+ const lower = v.toLowerCase();
24
+ return lower.endsWith(MD_EXT_MARKER) || lower.endsWith('.markdown');
25
+ }
26
+ function isRelativeSpecifier(v) {
27
+ return v.startsWith('./') || v.startsWith('../');
28
+ }
29
+ /** Walks up from `start` looking for the first directory containing `marker`. */
30
+ function findUp(start, marker) {
31
+ let cur = start;
32
+ for (let i = 0; i < 32; i++) {
33
+ if (existsSync(join(cur, marker)))
34
+ return cur;
35
+ const parent = dirname(cur);
36
+ if (parent === cur)
37
+ return null;
38
+ cur = parent;
39
+ }
40
+ return null;
41
+ }
42
+ /**
43
+ * Rewrites static `content="<file>.md"` attributes on <Md> elements into
44
+ * inlined constants. Returns the transformed source; when nothing matches,
45
+ * the original string is returned untouched.
46
+ *
47
+ * `mdRoots` are candidate project roots for absolute-style specifiers
48
+ * ("/x.md"); each root is tried as <root>/public<x> and <root><x>.
49
+ */
50
+ export function inlineMdContentAttrs(source, importerDir, mdRoots = []) {
51
+ if (!source.includes('content=') && !source.includes('content ='))
52
+ return source;
53
+ if (!importerDir && mdRoots.length === 0)
54
+ return source;
55
+ let out = '';
56
+ let i = 0;
57
+ const n = source.length;
58
+ let counter = 0;
59
+ const decls = [];
60
+ const prefix = 'content=';
61
+ while (i < n) {
62
+ if (source.startsWith(prefix, i)) {
63
+ // attribute position: ensure this is a JSX-ish attribute (preceded by whitespace/start)
64
+ const prev = i > 0 ? source[i - 1] : ' ';
65
+ if (prev === ' ' || prev === '\t' || prev === '\n' || prev === '\r') {
66
+ let j = i + prefix.length;
67
+ const quote = source[j];
68
+ if (quote === '"' || quote === "'") {
69
+ j++;
70
+ const valStart = j;
71
+ while (j < n && source[j] !== quote)
72
+ j++;
73
+ if (j < n) {
74
+ const value = source.slice(valStart, j);
75
+ if (looksLikeMarkdownPath(value)) {
76
+ const abs = resolveMdPath(value, importerDir, mdRoots);
77
+ if (abs !== null) {
78
+ let contents;
79
+ try {
80
+ contents = readFileSync(abs, 'utf-8');
81
+ }
82
+ catch {
83
+ out += source[i];
84
+ i++;
85
+ continue;
86
+ }
87
+ const varName = `__vesk_md_${counter++}`;
88
+ decls.push(`const ${varName} = ${JSON.stringify(contents)};`);
89
+ out += `${prefix}{${varName}}`;
90
+ i = j + 1;
91
+ continue;
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ out += source[i];
99
+ i++;
100
+ }
101
+ if (decls.length === 0)
102
+ return source;
103
+ return decls.join('\n') + '\n' + out;
104
+ }
105
+ function resolveMdPath(specifier, importerDir, roots) {
106
+ const spec = specifier.trim();
107
+ if (isRelativeSpecifier(spec)) {
108
+ if (!importerDir)
109
+ return null;
110
+ const abs = resolve(importerDir, spec);
111
+ return existsSync(abs) ? abs : null;
112
+ }
113
+ if (spec.startsWith('/')) {
114
+ for (const root of roots) {
115
+ const pub = join(root, 'public', spec);
116
+ if (existsSync(pub))
117
+ return pub;
118
+ const direct = join(root, spec);
119
+ if (existsSync(direct))
120
+ return direct;
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+ /**
126
+ * Candidate project roots for absolute-style specifiers: walks up from
127
+ * `dir` collecting directories that contain a package.json (bounded).
128
+ */
129
+ export function guessProjectRoots(dir) {
130
+ if (!dir)
131
+ return [];
132
+ const roots = [];
133
+ let cur = dir;
134
+ for (let i = 0; i < 6; i++) {
135
+ roots.push(cur);
136
+ if (existsSync(join(cur, 'package.json')))
137
+ break;
138
+ const parent = dirname(cur);
139
+ if (parent === cur)
140
+ break;
141
+ cur = parent;
142
+ }
143
+ return roots;
144
+ }
145
+ /** Convenience wrapper used by pipelines that already know the file dir. */
146
+ export function inlineMdImportsFrom(source, importerFile, mdRoots = []) {
147
+ const dir = importerFile ? dirname(importerFile) : null;
148
+ return inlineMdContentAttrs(source, dir, mdRoots.length > 0 ? mdRoots : [dir || process.cwd()]);
149
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAiC,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAEtG,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,eAAe,EAAE,CAsE7G;AAED,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,CAevG;AAED,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,eAAe,EAAE,EACxB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAAC,UAAU,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAoGzH"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAiC,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAEtG,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,eAAe,EAAE,CAsE7G;AAED,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,CAiBvG;AAED,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,eAAe,EAAE,EACxB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAAC,UAAU,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAkIzH"}
@@ -75,7 +75,9 @@ export async function loadMiddleware(sourcePath) {
75
75
  }
76
76
  if (sourcePath.endsWith('.js') || sourcePath.endsWith('.ts')) {
77
77
  try {
78
- const mod = await import(sourcePath);
78
+ const url = new URL('file://' + sourcePath);
79
+ url.searchParams.set('t', String(Date.now()) + Math.random().toString(36).slice(2));
80
+ const mod = await import(url.href);
79
81
  return (mod.middleware || mod.default || null);
80
82
  }
81
83
  catch {
@@ -132,21 +134,37 @@ export async function executeMiddlewareChain(chain, request, params, options = {
132
134
  }
133
135
  let rewriteUrl = null;
134
136
  if (chain.length === 0) {
137
+ if (onLast) {
138
+ const prev = globalThis.__vesk_request;
139
+ globalThis.__vesk_request = ctx;
140
+ try {
141
+ const loneResponse = await onLast(rewriteUrl, ctx);
142
+ return { response: loneResponse ?? null, redirected: !!loneResponse && loneResponse.status >= 300 && loneResponse.status < 400, locals, rewriteUrl };
143
+ }
144
+ finally {
145
+ globalThis.__vesk_request = prev;
146
+ }
147
+ }
135
148
  return { response: null, redirected: false, locals, rewriteUrl: null };
136
149
  }
137
150
  async function runMiddleware(index) {
138
151
  if (index >= chain.length) {
139
152
  if (onLast) {
140
- return onLast(rewriteUrl);
153
+ return onLast(rewriteUrl, ctx);
141
154
  }
142
- return new Response(null, { status: 204 });
155
+ return null;
143
156
  }
144
157
  const { sourcePath } = chain[index];
145
158
  const fn = await loadMiddleware(sourcePath);
146
159
  if (!fn) {
147
160
  return runMiddleware(index + 1);
148
161
  }
162
+ let nextCalled = false;
163
+ let nextResult = null;
149
164
  async function next(rewrite) {
165
+ if (nextCalled)
166
+ return null;
167
+ nextCalled = true;
150
168
  if (rewrite) {
151
169
  rewriteUrl = rewrite;
152
170
  try {
@@ -158,14 +176,21 @@ export async function executeMiddlewareChain(chain, request, params, options = {
158
176
  ctx.url = new URL(rewrite, base.origin);
159
177
  }
160
178
  }
161
- return runMiddleware(index + 1);
179
+ nextResult = await runMiddleware(index + 1);
180
+ return nextResult;
162
181
  }
163
182
  try {
164
183
  const result = await fn(ctx, next);
165
184
  if (result instanceof Response) {
166
185
  return result;
167
186
  }
168
- return new Response(null, { status: 204 });
187
+ if (nextResult !== null && nextResult instanceof Response) {
188
+ return nextResult;
189
+ }
190
+ if (!nextCalled) {
191
+ return runMiddleware(index + 1);
192
+ }
193
+ return null;
169
194
  }
170
195
  catch (e) {
171
196
  const err = e;
@@ -178,6 +203,16 @@ export async function executeMiddlewareChain(chain, request, params, options = {
178
203
  throw e;
179
204
  }
180
205
  }
181
- const response = await runMiddleware(0);
182
- return { response, redirected: response?.status >= 300 && response?.status < 400, locals, rewriteUrl };
206
+ // Ensure the outermost ctx is available as __vesk_request during the chain,
207
+ // so that server locals / onLast rendering sees middleware locals.
208
+ const prevOuter = globalThis.__vesk_request;
209
+ globalThis.__vesk_request = ctx;
210
+ let response;
211
+ try {
212
+ response = await runMiddleware(0);
213
+ }
214
+ finally {
215
+ globalThis.__vesk_request = prevOuter;
216
+ }
217
+ return { response, redirected: !!response && response.status >= 300 && response.status < 400, locals, rewriteUrl };
183
218
  }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * AST-driven extraction of the value bindings introduced by one import
3
+ * statement (`import { a as b } from 'm'` → `[{ local: 'b', imported: 'a' }]`;
4
+ * `import X from 'm'` → `[{ local: 'X', imported: 'default' }]`;
5
+ * `import * as X from 'm'` → `[{ local: 'X', imported: '*' }]`). Type-only
6
+ * specifiers are skipped. Side-effect imports return `[]`.
7
+ */
8
+ export declare function importBindingPairs(imp: string): Array<{
9
+ local: string;
10
+ imported: string;
11
+ }>;
12
+ /** Local binding names imported from non-runtime, non-`.vsk` modules. */
13
+ export declare function localValueImportNames(importStrs: string[]): string[];
14
+ /** True when an import line should be SSR-loaded (non-runtime, non-empty, non-`.vsk`). */
15
+ export declare function isLocalValueImport(imp: string): boolean;
16
+ /**
17
+ * Loads every local value import and merges its exports into `__vesk` so SSR
18
+ * component bodies can reference them. Pure side-effect imports (`import './x'`)
19
+ * are also resolved and executed so their module-level setup runs client AND
20
+ * server. Resolution is relative to `sourcePath` (the importing `.vsk` file).
21
+ *
22
+ * Best-effort: unresolvable/unloadable modules warn and are skipped, matching
23
+ * how unresolvable `.vsk` imports are handled. Unsupported ESM constructs
24
+ * (import.meta/top-level await) THROW a specific error — loading them would
25
+ * silently yield `undefined` at render.
26
+ */
27
+ export declare function applyLocalModuleImports(__vesk: Record<string, unknown>, importStrs: string[], sourcePath: string | undefined): void;
28
+ /**
29
+ * Loads (and caches) a module's export object. Cache entries track both the
30
+ * module's own mtime and the mtimes of everything it transitively resolved,
31
+ * so a change to any dependency invalidates every module that pulls it in.
32
+ * Builtin markers load through Node and are cached separately (never stale).
33
+ */
34
+ export declare function loadSsrModule(absPath: string): Record<string, unknown> | null;
35
+ /**
36
+ * Resolves a specifier to an absolute file path (or a `BUILTIN_PREFIX` marker
37
+ * for Node builtins). Relative/absolute localities use extension probing;
38
+ * bare specifiers (`npm-package`, `pkg/subpath`, `node:fs`, `fs`) prefer
39
+ * Node's own resolver — which understands `exports` maps, conditions, scoped
40
+ * packages and builtins — and fall back to a `node_modules` walk-up.
41
+ *
42
+ * Every resolved file path is normalized through `realpathSync` so two
43
+ * specifiers pointing at the same physical file (e.g. pnpm/yarn symlinked
44
+ * `node_modules`) share one cache entry and one module instance.
45
+ */
46
+ export declare function resolveSsrModule(specifier: string, fromDir: string): string | null;
47
+ /** Rewrites an ESM program body (imports/exports removed) into CJS statements. */
48
+ export declare function esmToCjs(body: Array<{
49
+ type: string;
50
+ } & Record<string, unknown>>): string;
51
+ //# sourceMappingURL=module-imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module-imports.d.ts","sourceRoot":"","sources":["../src/module-imports.ts"],"names":[],"mappings":"AAmGA;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAgC1F;AAED,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAQpE;AAED,0FAA0F;AAC1F,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAIvD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,UAAU,EAAE,MAAM,EAAE,EACpB,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,IAAI,CAuBN;AAiBD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAqD7E;AAqJD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAyBlF;AAoJD,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAiGxF"}