@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.
- package/dist/api-routes.d.ts +6 -1
- package/dist/api-routes.d.ts.map +1 -1
- package/dist/api-routes.js +52 -7
- package/dist/client-codegen.d.ts +15 -0
- package/dist/client-codegen.d.ts.map +1 -1
- package/dist/client-codegen.js +84 -11
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +25 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +33 -4
- package/dist/ir-generator.d.ts.map +1 -1
- package/dist/ir-generator.js +208 -9
- package/dist/md-inline.d.ts +17 -0
- package/dist/md-inline.d.ts.map +1 -0
- package/dist/md-inline.js +149 -0
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +42 -7
- 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/router.d.ts +2 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +48 -3
- package/dist/server-codegen.d.ts +1 -1
- package/dist/server-codegen.d.ts.map +1 -1
- package/dist/server-codegen.js +1 -1
- package/dist/server-jsgen.d.ts.map +1 -1
- package/dist/server-jsgen.js +9 -4
- package/dist/server-render.d.ts.map +1 -1
- package/dist/server-render.js +30 -10
- package/dist/server-utils.d.ts +31 -0
- package/dist/server-utils.d.ts.map +1 -1
- package/dist/server-utils.js +478 -52
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +24 -1
- package/dist/typecheck.d.ts +2 -2
- package/dist/typecheck.d.ts.map +1 -1
- package/dist/typecheck.js +71 -27
- package/dist/types.d.ts +4 -129
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/vesk-plugin.d.ts +3 -0
- package/dist/vesk-plugin.d.ts.map +1 -1
- package/dist/vesk-plugin.js +518 -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 +27 -5
- package/package.json +2 -1
package/dist/ir-generator.js
CHANGED
|
@@ -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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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,
|
|
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' &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/middleware.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/middleware.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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"}
|