@vesk/compiler 0.2.46 → 0.2.48
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/module-imports.d.ts.map +1 -1
- package/dist/module-imports.js +96 -14
- package/dist/server-head.d.ts +14 -1
- package/dist/server-head.d.ts.map +1 -1
- package/dist/server-head.js +218 -17
- package/dist/server-render.d.ts.map +1 -1
- package/dist/server-render.js +5 -3
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module-imports.d.ts","sourceRoot":"","sources":["../src/module-imports.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AA2ElD,gFAAgF;AAChF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAM7D;AAED,gEAAgE;AAChE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAOzD;AAED;;;;;;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,CAiE7E;
|
|
1
|
+
{"version":3,"file":"module-imports.d.ts","sourceRoot":"","sources":["../src/module-imports.ts"],"names":[],"mappings":"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"}
|
package/dist/module-imports.js
CHANGED
|
@@ -301,15 +301,70 @@ function depsFresh(cachedVal) {
|
|
|
301
301
|
}
|
|
302
302
|
return true;
|
|
303
303
|
}
|
|
304
|
-
|
|
304
|
+
/**
|
|
305
|
+
* Raw file contents, keyed by path and validated by mtime.
|
|
306
|
+
*
|
|
307
|
+
* `readSsrSource` is on the hot path of every `.vsk` compile: a page's module
|
|
308
|
+
* bundle pulls in its `.ts`/`.js` imports, and a docs-style app where 26 pages
|
|
309
|
+
* all import one content module read and re-parsed that module 26 times. The
|
|
310
|
+
* parsed AST is deliberately NOT shared (callers mutate it — `stripTsTypes`,
|
|
311
|
+
* `stripped.body = ...`) and is still produced per call; only the immutable
|
|
312
|
+
* text is reused, and only while the file's mtime is unchanged, so an edit in
|
|
313
|
+
* a watch session is picked up immediately.
|
|
314
|
+
*/
|
|
315
|
+
const rawSourceCache = new Map();
|
|
316
|
+
/**
|
|
317
|
+
* Bounded, insertion-ordered eviction.
|
|
318
|
+
*
|
|
319
|
+
* The cap has to clear the OLDEST entries, not the whole map: an app that
|
|
320
|
+
* imports an icon barrel walks 1,600+ modules, and a 512-entry cap with
|
|
321
|
+
* `clear()` on overflow thrashed itself into a 0% hit rate (11,235 reads of
|
|
322
|
+
* 1,620 distinct files on vesk-doc). Draining the front keeps the hot modules
|
|
323
|
+
* resident and bounds memory.
|
|
324
|
+
*/
|
|
325
|
+
const BUILD_CACHE_MAX = 8192;
|
|
326
|
+
const BUILD_CACHE_EVICT = 2048;
|
|
327
|
+
function evict(cache) {
|
|
328
|
+
if (cache.size <= BUILD_CACHE_MAX)
|
|
329
|
+
return;
|
|
330
|
+
let dropped = 0;
|
|
331
|
+
for (const key of cache.keys()) {
|
|
332
|
+
cache.delete(key);
|
|
333
|
+
if (++dropped >= BUILD_CACHE_EVICT)
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function readSourceCached(absPath) {
|
|
338
|
+
let mtimeMs;
|
|
339
|
+
try {
|
|
340
|
+
mtimeMs = statSync(absPath).mtimeMs;
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
const hit = rawSourceCache.get(absPath);
|
|
346
|
+
if (hit && hit.mtimeMs === mtimeMs)
|
|
347
|
+
return hit.raw;
|
|
305
348
|
let raw;
|
|
306
349
|
try {
|
|
307
350
|
raw = readFileSync(absPath, 'utf-8');
|
|
308
351
|
}
|
|
309
|
-
catch
|
|
310
|
-
|
|
352
|
+
catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
rawSourceCache.set(absPath, { mtimeMs, raw });
|
|
356
|
+
evict(rawSourceCache);
|
|
357
|
+
return raw;
|
|
358
|
+
}
|
|
359
|
+
export function readSsrSource(absPath) {
|
|
360
|
+
const cachedRaw = readSourceCached(absPath);
|
|
361
|
+
if (cachedRaw === null) {
|
|
362
|
+
// Keep the original warning: a missing module is a build-time signal, not
|
|
363
|
+
// a cache miss to swallow.
|
|
364
|
+
console.warn(`[vesk] SSR: failed to read ${absPath}`);
|
|
311
365
|
return null;
|
|
312
366
|
}
|
|
367
|
+
const raw = cachedRaw;
|
|
313
368
|
let ast = null;
|
|
314
369
|
try {
|
|
315
370
|
ast = parse(raw, { filename: absPath });
|
|
@@ -1208,18 +1263,44 @@ export function collectModuleBundled(absPath, collector) {
|
|
|
1208
1263
|
collector.modules.push({ key, code: `module.exports = ${raw};`, dir, deps: {} });
|
|
1209
1264
|
return key;
|
|
1210
1265
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1266
|
+
// The transpiled code and the relative specifiers of a module do not depend
|
|
1267
|
+
// on which collector is asking, only on the file — and every `.vsk` compile
|
|
1268
|
+
// builds its own collector, so a docs-style app whose 26 pages all import one
|
|
1269
|
+
// content module read, parsed, stripped and re-transpiled that module 26
|
|
1270
|
+
// times (51s of a 95s build). Cache the result against the file's mtime and
|
|
1271
|
+
// skip the read AND the parse on a hit: callers mutate the AST they get
|
|
1272
|
+
// (`stripTsTypes`, `stripped.body = ...`), so the AST itself stays private to
|
|
1273
|
+
// the call that created it.
|
|
1274
|
+
let mtimeMs = -1;
|
|
1275
|
+
try {
|
|
1276
|
+
mtimeMs = statSync(absPath).mtimeMs;
|
|
1277
|
+
}
|
|
1278
|
+
catch { /* unreadable: fall through to the uncached path */ }
|
|
1279
|
+
const cached = mtimeMs >= 0 ? bundledCodeCache.get(absPath) : undefined;
|
|
1280
|
+
let code;
|
|
1281
|
+
let specifiers;
|
|
1282
|
+
if (cached && cached.mtimeMs === mtimeMs) {
|
|
1283
|
+
code = cached.code;
|
|
1284
|
+
specifiers = cached.specifiers;
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
const src = readSsrSource(absPath);
|
|
1288
|
+
if (!src || !src.ast) {
|
|
1289
|
+
const raw = src ? src.raw : '// unreadable during build\nmodule.exports = {};';
|
|
1290
|
+
collector.modules.push({ key, code: raw, dir, deps: {} });
|
|
1291
|
+
return key;
|
|
1292
|
+
}
|
|
1293
|
+
let stripped = src.ast;
|
|
1294
|
+
if (hasTsSyntax(src.ast))
|
|
1295
|
+
stripped = stripTsTypes(src.ast);
|
|
1296
|
+
stripped.body = (stripped.body || []).filter((n) => (n ? !isTypeOnlyStatement(n) : false));
|
|
1297
|
+
specifiers = [...collectModuleSpecifiers(src.ast)];
|
|
1298
|
+
code = esmToCjs(stripped.body);
|
|
1299
|
+
bundledCodeCache.set(absPath, { mtimeMs, code, specifiers });
|
|
1300
|
+
evict(bundledCodeCache);
|
|
1216
1301
|
}
|
|
1217
|
-
let stripped = src.ast;
|
|
1218
|
-
if (hasTsSyntax(src.ast))
|
|
1219
|
-
stripped = stripTsTypes(src.ast);
|
|
1220
|
-
stripped.body = (stripped.body || []).filter((n) => (n ? !isTypeOnlyStatement(n) : false));
|
|
1221
1302
|
const deps = {};
|
|
1222
|
-
for (const spec of
|
|
1303
|
+
for (const spec of specifiers) {
|
|
1223
1304
|
if (!isRelativeSpec(spec))
|
|
1224
1305
|
continue;
|
|
1225
1306
|
const depPath = resolveSsrModule(spec, dir);
|
|
@@ -1227,9 +1308,10 @@ export function collectModuleBundled(absPath, collector) {
|
|
|
1227
1308
|
continue;
|
|
1228
1309
|
deps[spec] = collectModuleBundled(depPath, collector);
|
|
1229
1310
|
}
|
|
1230
|
-
collector.modules.push({ key, code
|
|
1311
|
+
collector.modules.push({ key, code, dir, deps });
|
|
1231
1312
|
return key;
|
|
1232
1313
|
}
|
|
1314
|
+
const bundledCodeCache = new Map();
|
|
1233
1315
|
/**
|
|
1234
1316
|
* Builds the module bundle + local-binding list for a set of import lines
|
|
1235
1317
|
* (a `.vsk` file's `imports`). Framework-owned, `.vsk`, `.css` and markdown
|
package/dist/server-head.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { ComponentIR } from '@vesk/compiler/src/ir';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Serialize one component's `<Head>` block.
|
|
4
|
+
*
|
|
5
|
+
* `scope` is the component's `__vesk` bindings (imports, runtime names, the
|
|
6
|
+
* sub-components its body destructures). The head is assembled by re-walking
|
|
7
|
+
* the IR and re-evaluating the expressions rather than by capturing the real
|
|
8
|
+
* render, so the re-evaluation needs the same bindings the component had —
|
|
9
|
+
* without them every expression that touched an import threw and was swallowed,
|
|
10
|
+
* and the served document shipped an empty `<title>`/`<meta>` while the page
|
|
11
|
+
* body rendered fine (and the client corrected it on hydration, which is why
|
|
12
|
+
* this only ever showed up in view-source, in crawlers, and after an SPA nav
|
|
13
|
+
* re-applied the server head).
|
|
14
|
+
*/
|
|
15
|
+
export declare function renderHeadHtml(comp: ComponentIR, props?: Record<string, unknown>, scope?: Record<string, unknown>): string;
|
|
3
16
|
export declare function mergeHeadHtml(pageHead: string, layoutHead: string): {
|
|
4
17
|
html: string;
|
|
5
18
|
conflicts: Array<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-head.d.ts","sourceRoot":"","sources":["../src/server-head.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"server-head.d.ts","sourceRoot":"","sources":["../src/server-head.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAgVzD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,WAAW,EACjB,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACnC,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAClC,MAAM,CAkBR;AAoFD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CAkExI"}
|
package/dist/server-head.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { StaticNode, TextNode, DynamicBinding, HeadBlock, RuntimeStatement } from '@vesk/compiler/src/ir';
|
|
1
|
+
import { StaticNode, TextNode, DynamicBinding, HeadBlock, RuntimeStatement, TrackDecl } from '@vesk/compiler/src/ir';
|
|
2
2
|
import { tryEvalExpr, escapeHtml } from '@vesk/compiler/src/server-utils';
|
|
3
3
|
import { isWhitespaceChar } from '@vesk/compiler/src/scan';
|
|
4
|
+
import { parse } from '@vesk/compiler/src/parser';
|
|
5
|
+
import { collectTrackedNames, transformTracked, transformTrackedInit } from '@vesk/compiler/src/client-codegen';
|
|
6
|
+
import { walk } from 'zimmerframe';
|
|
4
7
|
function isHtmlNameChar(ch) {
|
|
5
8
|
const code = ch.charCodeAt(0);
|
|
6
9
|
return ((code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || ch === '-');
|
|
@@ -11,22 +14,206 @@ function isAttrBoundaryChar(ch) {
|
|
|
11
14
|
function isValueBoundaryChar(ch) {
|
|
12
15
|
return isWhitespaceChar(ch) || ch === '/' || ch === '>';
|
|
13
16
|
}
|
|
14
|
-
function
|
|
17
|
+
function headCell(value) {
|
|
18
|
+
const cell = {
|
|
19
|
+
__vskHeadValue: value,
|
|
20
|
+
toString() { return String(cell.__vskHeadValue); },
|
|
21
|
+
// Present so `+count` / `String(count)` on the cell itself still yields the
|
|
22
|
+
// value; the return type is widened because `valueOf` must return `this`.
|
|
23
|
+
valueOf() { return cell; },
|
|
24
|
+
};
|
|
25
|
+
return cell;
|
|
26
|
+
}
|
|
27
|
+
const headRuntimeShims = () => ({
|
|
28
|
+
track: (init) => headCell(init),
|
|
29
|
+
derived: (fn) => headCell(typeof fn === 'function' ? fn() : fn),
|
|
30
|
+
get: (cell) => cell && typeof cell === 'object' && '__vskHeadValue' in cell
|
|
31
|
+
? cell.__vskHeadValue
|
|
32
|
+
: cell,
|
|
33
|
+
peek: (cell) => cell && typeof cell === 'object' && '__vskHeadValue' in cell
|
|
34
|
+
? cell.__vskHeadValue
|
|
35
|
+
: cell,
|
|
36
|
+
untrack: (fn) => (typeof fn === 'function' ? fn() : fn),
|
|
37
|
+
});
|
|
38
|
+
function headEvalScope(ctx) {
|
|
39
|
+
// The shims come LAST: they must win over the component's real `track`/`get`
|
|
40
|
+
// from `__vesk`, which would otherwise allocate real cells.
|
|
41
|
+
return { ...ctx.scope, ...ctx.locals, ...headRuntimeShims() };
|
|
42
|
+
}
|
|
43
|
+
function evalHeadExpression(expression, ctx) {
|
|
44
|
+
// `{count}` must resolve to the VALUE, not the cell: rewrite tracked reads the
|
|
45
|
+
// same way the client codegen does, then evaluate with the shims bound.
|
|
46
|
+
const code = transformTracked(expression, ctx.tracked);
|
|
47
|
+
return tryEvalExpr(code, ctx.props, headEvalScope(ctx));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Evaluate `raw` with `props` plus every name in `scope` bound to its value.
|
|
51
|
+
*
|
|
52
|
+
* The head pass re-evaluates a component's top-level declarations outside the
|
|
53
|
+
* component's own call frame, so it has to rebuild that frame. Passing the
|
|
54
|
+
* component's `__vesk` bindings as named parameters is what makes imports
|
|
55
|
+
* visible: a docs page's `const doc = getDoc(props.params.slug)` needs
|
|
56
|
+
* `getDoc`, which lives in `__vesk` (the generated body destructures it from
|
|
57
|
+
* there), and without it the head silently degraded to empty strings.
|
|
58
|
+
*/
|
|
59
|
+
function evalWithScope(raw, props, scope) {
|
|
60
|
+
const names = Object.keys(scope);
|
|
61
|
+
const fn = new Function('props', ...names, 'return (' + raw + ')');
|
|
62
|
+
return fn(props, ...names.map((n) => scope[n]));
|
|
63
|
+
}
|
|
64
|
+
/** Identifier reads of an ESTree expression (not property keys, not `obj.prop`). */
|
|
65
|
+
function identifiersOf(ast, into) {
|
|
66
|
+
if (!ast || typeof ast !== 'object')
|
|
67
|
+
return;
|
|
68
|
+
walk(ast, null, {
|
|
69
|
+
Identifier(n, context) {
|
|
70
|
+
const parent = context.path.at(-1);
|
|
71
|
+
if (parent) {
|
|
72
|
+
if (parent.type === 'MemberExpression' && !parent.computed && parent.property === n)
|
|
73
|
+
return context.next();
|
|
74
|
+
if (parent.type === 'Property' && parent.key === n)
|
|
75
|
+
return context.next();
|
|
76
|
+
}
|
|
77
|
+
into.add(n.name);
|
|
78
|
+
return context.next();
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** Every interpolated/attribute expression inside a `<Head>` subtree. */
|
|
83
|
+
function collectHeadExprs(node, out) {
|
|
84
|
+
if (node instanceof StaticNode) {
|
|
85
|
+
for (const child of node.children) {
|
|
86
|
+
if (child instanceof DynamicBinding)
|
|
87
|
+
out.push(child.expression);
|
|
88
|
+
else
|
|
89
|
+
collectHeadExprs(child, out);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Top-level declarations of a component: tracked cells and `const` locals. */
|
|
94
|
+
function declaredNames(comp) {
|
|
95
|
+
const out = new Map();
|
|
96
|
+
for (const node of comp.body) {
|
|
97
|
+
if (node instanceof TrackDecl) {
|
|
98
|
+
const cellName = node.rawName || node.name;
|
|
99
|
+
let initAst = null;
|
|
100
|
+
try {
|
|
101
|
+
initAst = parse(node.init);
|
|
102
|
+
}
|
|
103
|
+
catch { /* unparsable init: no deps to follow */ }
|
|
104
|
+
const entry = { kind: 'track', initSrc: node.init, initAst, names: [cellName] };
|
|
105
|
+
out.set(cellName, entry);
|
|
106
|
+
if (node.name !== cellName) {
|
|
107
|
+
out.set(node.name, entry);
|
|
108
|
+
entry.names.push(node.name);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (node instanceof RuntimeStatement && node.ast) {
|
|
112
|
+
const stmt = node.ast;
|
|
113
|
+
if (stmt.type !== 'VariableDeclaration')
|
|
114
|
+
continue;
|
|
115
|
+
for (const decl of stmt.declarations) {
|
|
116
|
+
if (decl.id.type !== 'Identifier' || !decl.init || !node.source)
|
|
117
|
+
continue;
|
|
118
|
+
out.set(decl.id.name, {
|
|
119
|
+
kind: 'const',
|
|
120
|
+
initSrc: node.source.slice(decl.init.start, decl.init.end),
|
|
121
|
+
initAst: decl.init,
|
|
122
|
+
names: [decl.id.name],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The transitive closure of top-level names the `<Head>` block reads.
|
|
131
|
+
*
|
|
132
|
+
* THE head pass must evaluate only what the head uses. Evaluating every
|
|
133
|
+
* top-level declaration (which is what it used to do) means re-running the
|
|
134
|
+
* component's SIDE EFFECTS in the rebuilt frame: a page whose body does
|
|
135
|
+
* `const data = useFetch('/api/fail')` fetched twice per render, because the
|
|
136
|
+
* head pass called `useFetch` a second time now that the runtime names are in
|
|
137
|
+
* scope. Imports and tracked reads are still resolved — just the declarations
|
|
138
|
+
* the head actually needs, plus the ones those depend on.
|
|
139
|
+
*/
|
|
140
|
+
function headNeededNames(comp) {
|
|
141
|
+
const wanted = new Set();
|
|
142
|
+
for (const node of comp.body) {
|
|
143
|
+
if (!(node instanceof HeadBlock))
|
|
144
|
+
continue;
|
|
145
|
+
const exprs = [];
|
|
146
|
+
for (const child of node.children)
|
|
147
|
+
collectHeadExprs(child, exprs);
|
|
148
|
+
for (const e of exprs)
|
|
149
|
+
identifiersOf(e.ast, wanted);
|
|
150
|
+
}
|
|
151
|
+
const declared = declaredNames(comp);
|
|
152
|
+
const needed = new Set();
|
|
153
|
+
const queue = [...wanted].filter((n) => declared.has(n));
|
|
154
|
+
while (queue.length > 0) {
|
|
155
|
+
const name = queue.shift();
|
|
156
|
+
if (needed.has(name))
|
|
157
|
+
continue;
|
|
158
|
+
needed.add(name);
|
|
159
|
+
const entry = declared.get(name);
|
|
160
|
+
if (!entry)
|
|
161
|
+
continue;
|
|
162
|
+
const deps = new Set();
|
|
163
|
+
identifiersOf(entry.initAst, deps);
|
|
164
|
+
for (const d of deps) {
|
|
165
|
+
// A tracked init reads other cells through the runtime, so follow the
|
|
166
|
+
// tracked names the init mentions.
|
|
167
|
+
if (declared.has(d) && !needed.has(d))
|
|
168
|
+
queue.push(d);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return needed;
|
|
172
|
+
}
|
|
173
|
+
function evaluateLocals(comp, props, scope = {}, tracked = new Map(), needed = null) {
|
|
15
174
|
const locals = {};
|
|
175
|
+
const wanted = (name) => needed === null || needed.has(name);
|
|
16
176
|
for (const node of comp.body) {
|
|
177
|
+
if (node instanceof TrackDecl) {
|
|
178
|
+
// `const [count, setCount] = track(0)`: bind every name the declaration
|
|
179
|
+
// introduces to a head cell, so a later local or head expression that
|
|
180
|
+
// reads it sees the current value through the shims.
|
|
181
|
+
const cellName = node.rawName || node.name;
|
|
182
|
+
if (!wanted(cellName) && !wanted(node.name))
|
|
183
|
+
continue;
|
|
184
|
+
const init = transformTrackedInit(node.init, tracked);
|
|
185
|
+
let value;
|
|
186
|
+
try {
|
|
187
|
+
value = evalWithScope(init, props, { ...scope, ...locals, ...headRuntimeShims() });
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
value = undefined;
|
|
191
|
+
}
|
|
192
|
+
const cell = headCell(value);
|
|
193
|
+
if (node.name !== cellName)
|
|
194
|
+
locals[node.name] = cell;
|
|
195
|
+
locals[cellName] = cell;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
17
198
|
if (node instanceof RuntimeStatement && node.ast) {
|
|
18
199
|
const stmt = node.ast;
|
|
19
200
|
if (stmt.type === 'VariableDeclaration') {
|
|
20
201
|
for (const decl of stmt.declarations) {
|
|
21
202
|
if (decl.id.type === 'Identifier' && decl.init && node.source) {
|
|
22
203
|
const name = decl.id.name;
|
|
204
|
+
if (!wanted(name))
|
|
205
|
+
continue;
|
|
23
206
|
const initSrc = node.source.slice(decl.init.start, decl.init.end);
|
|
24
207
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
208
|
+
locals[name] = evalWithScope(initSrc, props, {
|
|
209
|
+
...scope,
|
|
210
|
+
...locals,
|
|
211
|
+
...headRuntimeShims(),
|
|
212
|
+
});
|
|
27
213
|
}
|
|
28
214
|
catch {
|
|
29
|
-
// expression can't be evaluated — skip
|
|
215
|
+
// expression can't be evaluated — skip (its dependents degrade
|
|
216
|
+
// to empty, exactly as they did before the scope was passed)
|
|
30
217
|
}
|
|
31
218
|
}
|
|
32
219
|
}
|
|
@@ -35,7 +222,7 @@ function evaluateLocals(comp, props) {
|
|
|
35
222
|
}
|
|
36
223
|
return locals;
|
|
37
224
|
}
|
|
38
|
-
function headElementKey(node,
|
|
225
|
+
function headElementKey(node, ctx) {
|
|
39
226
|
if (!(node instanceof StaticNode))
|
|
40
227
|
return null;
|
|
41
228
|
const tag = node.tag;
|
|
@@ -47,7 +234,7 @@ function headElementKey(node, props, locals) {
|
|
|
47
234
|
for (const child of node.children) {
|
|
48
235
|
if (child instanceof DynamicBinding && child.kind === 'attribute' && child.target && child.target !== 'ref') {
|
|
49
236
|
try {
|
|
50
|
-
attrMap.set(child.target, String(
|
|
237
|
+
attrMap.set(child.target, String(evalHeadExpression(child.expression, ctx)));
|
|
51
238
|
}
|
|
52
239
|
catch { /* skip */ }
|
|
53
240
|
}
|
|
@@ -88,14 +275,13 @@ function headIsReconcilable(tag, attrMap) {
|
|
|
88
275
|
return false;
|
|
89
276
|
return true;
|
|
90
277
|
}
|
|
91
|
-
function irNodeToHeadHtml(node,
|
|
278
|
+
function irNodeToHeadHtml(node, ctx, reconcileable = false) {
|
|
92
279
|
if (node instanceof StaticNode) {
|
|
93
280
|
const attrMap = new Map(node.attributes.map((a) => [a.name, a.value]));
|
|
94
281
|
for (const child of node.children) {
|
|
95
282
|
if (child instanceof DynamicBinding && child.kind === 'attribute' && child.target && child.target !== 'ref') {
|
|
96
283
|
try {
|
|
97
|
-
|
|
98
|
-
attrMap.set(child.target, String(val));
|
|
284
|
+
attrMap.set(child.target, String(evalHeadExpression(child.expression, ctx)));
|
|
99
285
|
}
|
|
100
286
|
catch { /* skip */ }
|
|
101
287
|
}
|
|
@@ -108,7 +294,7 @@ function irNodeToHeadHtml(node, props, locals = {}, reconcileable = false) {
|
|
|
108
294
|
return `<${node.tag}${attrs}${marker} />`;
|
|
109
295
|
const inner = node.children
|
|
110
296
|
.filter((c) => !(c instanceof DynamicBinding && c.kind === 'attribute' && c.target !== 'ref'))
|
|
111
|
-
.map((c) => irNodeToHeadHtml(c,
|
|
297
|
+
.map((c) => irNodeToHeadHtml(c, ctx, false))
|
|
112
298
|
.join('');
|
|
113
299
|
return `<${node.tag}${attrs}${marker}>${inner}</${node.tag}>`;
|
|
114
300
|
}
|
|
@@ -116,8 +302,7 @@ function irNodeToHeadHtml(node, props, locals = {}, reconcileable = false) {
|
|
|
116
302
|
return node.value;
|
|
117
303
|
if (node instanceof DynamicBinding) {
|
|
118
304
|
try {
|
|
119
|
-
|
|
120
|
-
return escapeHtml(String(val));
|
|
305
|
+
return escapeHtml(String(evalHeadExpression(node.expression, ctx)));
|
|
121
306
|
}
|
|
122
307
|
catch {
|
|
123
308
|
return '';
|
|
@@ -125,19 +310,35 @@ function irNodeToHeadHtml(node, props, locals = {}, reconcileable = false) {
|
|
|
125
310
|
}
|
|
126
311
|
return '';
|
|
127
312
|
}
|
|
128
|
-
|
|
129
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Serialize one component's `<Head>` block.
|
|
315
|
+
*
|
|
316
|
+
* `scope` is the component's `__vesk` bindings (imports, runtime names, the
|
|
317
|
+
* sub-components its body destructures). The head is assembled by re-walking
|
|
318
|
+
* the IR and re-evaluating the expressions rather than by capturing the real
|
|
319
|
+
* render, so the re-evaluation needs the same bindings the component had —
|
|
320
|
+
* without them every expression that touched an import threw and was swallowed,
|
|
321
|
+
* and the served document shipped an empty `<title>`/`<meta>` while the page
|
|
322
|
+
* body rendered fine (and the client corrected it on hydration, which is why
|
|
323
|
+
* this only ever showed up in view-source, in crawlers, and after an SPA nav
|
|
324
|
+
* re-applied the server head).
|
|
325
|
+
*/
|
|
326
|
+
export function renderHeadHtml(comp, props = {}, scope = {}) {
|
|
327
|
+
const tracked = collectTrackedNames(comp.body);
|
|
328
|
+
const needed = headNeededNames(comp);
|
|
329
|
+
const locals = evaluateLocals(comp, props, scope, tracked, needed);
|
|
330
|
+
const ctx = { props, locals, tracked, scope };
|
|
130
331
|
const seen = new Set();
|
|
131
332
|
const parts = [];
|
|
132
333
|
for (const node of comp.body) {
|
|
133
334
|
if (node instanceof HeadBlock) {
|
|
134
335
|
for (const child of node.children) {
|
|
135
|
-
const key = headElementKey(child,
|
|
336
|
+
const key = headElementKey(child, ctx);
|
|
136
337
|
if (key !== null && seen.has(key))
|
|
137
338
|
continue;
|
|
138
339
|
if (key !== null)
|
|
139
340
|
seen.add(key);
|
|
140
|
-
parts.push(irNodeToHeadHtml(child,
|
|
341
|
+
parts.push(irNodeToHeadHtml(child, ctx, true));
|
|
141
342
|
}
|
|
142
343
|
}
|
|
143
344
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-render.d.ts","sourceRoot":"","sources":["../src/server-render.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,SAAS,EAAE,eAAe,EAAqB,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAuCvJ,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,iBAAiB,CAKhG;AA6DD,wBAAgB,MAAM,CACpB,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACnC,QAAQ,GAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAa,EAC3C,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACpC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA0C1B;AAED,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACnC,QAAQ,GAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAa,EAC3C,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACpC,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,
|
|
1
|
+
{"version":3,"file":"server-render.d.ts","sourceRoot":"","sources":["../src/server-render.ts"],"names":[],"mappings":"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"}
|
package/dist/server-render.js
CHANGED
|
@@ -187,7 +187,9 @@ export function renderPage(source, componentName, props = {}, registry = new Map
|
|
|
187
187
|
}
|
|
188
188
|
return {
|
|
189
189
|
body: bodyHtml,
|
|
190
|
-
|
|
190
|
+
// The component's own scope, so head expressions that read an import
|
|
191
|
+
// resolve (see renderHeadHtml).
|
|
192
|
+
head: renderHeadHtml(targetComp, ssrProps, scopedVesk(renderFn, __vesk)),
|
|
191
193
|
props: ssrProps
|
|
192
194
|
};
|
|
193
195
|
})();
|
|
@@ -199,7 +201,7 @@ export function renderPage(source, componentName, props = {}, registry = new Map
|
|
|
199
201
|
finally {
|
|
200
202
|
delete globalThis.__vsk_ssr;
|
|
201
203
|
}
|
|
202
|
-
const headHtml = targetComp ? renderHeadHtml(targetComp, ssrProps) : '';
|
|
204
|
+
const headHtml = targetComp ? renderHeadHtml(targetComp, ssrProps, scopedVesk(renderFn, __vesk)) : '';
|
|
203
205
|
const pending = globalThis[`__vsk_ssr_promises_${renderToken}`];
|
|
204
206
|
if (pending && pending.length > 0) {
|
|
205
207
|
// A sync component still started server resources (useFetch): settle them
|
|
@@ -616,7 +618,7 @@ export function renderPageStream(source, componentName, props = {}, registry = n
|
|
|
616
618
|
}
|
|
617
619
|
let streamHeadHtml = null;
|
|
618
620
|
if (targetComp) {
|
|
619
|
-
let headHtml = renderHeadHtml(targetComp, ssrProps);
|
|
621
|
+
let headHtml = renderHeadHtml(targetComp, ssrProps, scopedVesk(renderFn, __vesk));
|
|
620
622
|
if (options.pageHead) {
|
|
621
623
|
const merged = mergeHeadHtml(options.pageHead, headHtml);
|
|
622
624
|
headHtml = merged.html;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vesk/compiler",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.48",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vesk compiler — lexer, parser, semantic analysis, IR, codegen",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@sveltejs/acorn-typescript": "^1.0.0",
|
|
23
|
-
"@vesk/types": "^0.2.
|
|
23
|
+
"@vesk/types": "^0.2.48",
|
|
24
24
|
"acorn": "^8.15.0",
|
|
25
25
|
"esrap": "^2.3.0",
|
|
26
26
|
"is-reference": "^3.0.0",
|