@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.
@@ -1 +1 @@
1
- {"version":3,"file":"module-imports.d.ts","sourceRoot":"","sources":["../src/module-imports.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"module-imports.d.ts","sourceRoot":"","sources":["../src/module-imports.ts"],"names":[],"mappings":"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"}
@@ -301,15 +301,70 @@ function depsFresh(cachedVal) {
301
301
  }
302
302
  return true;
303
303
  }
304
- export function readSsrSource(absPath) {
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 (err) {
310
- console.warn(`[vesk] SSR: failed to read ${absPath}: ${err?.message ?? String(err)}`);
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
- const src = readSsrSource(absPath);
1212
- if (!src || !src.ast) {
1213
- const raw = src ? src.raw : '// unreadable during build\nmodule.exports = {};';
1214
- collector.modules.push({ key, code: raw, dir, deps: {} });
1215
- return key;
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 collectModuleSpecifiers(src.ast)) {
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: esmToCjs(stripped.body), dir, deps });
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
@@ -1,5 +1,18 @@
1
1
  import type { ComponentIR } from '@vesk/compiler/src/ir';
2
- export declare function renderHeadHtml(comp: ComponentIR, props?: Record<string, unknown>): string;
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;AAyHzD,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,MAAM,CAe7F;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"}
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"}
@@ -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 evaluateLocals(comp, props) {
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
- const fn = new Function('props', 'return (' + initSrc + ')');
26
- locals[name] = fn(props);
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, props, locals) {
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(tryEvalExpr(child.expression.raw, props, locals)));
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, props, locals = {}, reconcileable = false) {
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
- const val = tryEvalExpr(child.expression.raw, props, locals);
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, props, locals, false))
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
- const val = tryEvalExpr(node.expression.raw, props, locals);
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
- export function renderHeadHtml(comp, props = {}) {
129
- const locals = evaluateLocals(comp, props);
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, props, locals);
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, props, locals, true));
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":"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"}
1
+ {"version":3,"file":"server-render.d.ts","sourceRoot":"","sources":["../src/server-render.ts"],"names":[],"mappings":"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"}
@@ -187,7 +187,9 @@ export function renderPage(source, componentName, props = {}, registry = new Map
187
187
  }
188
188
  return {
189
189
  body: bodyHtml,
190
- head: renderHeadHtml(targetComp, ssrProps),
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.46",
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.46",
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",