@vesk/compiler 0.2.22 → 0.2.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client-codegen.d.ts.map +1 -1
- package/dist/client-codegen.js +231 -20
- package/dist/ir-generator.d.ts.map +1 -1
- package/dist/ir-generator.js +13 -0
- package/dist/module-imports.d.ts +43 -0
- package/dist/module-imports.d.ts.map +1 -1
- package/dist/module-imports.js +130 -3
- package/dist/precompile-runtime.d.ts +46 -0
- package/dist/precompile-runtime.d.ts.map +1 -0
- package/dist/precompile-runtime.js +211 -0
- package/dist/precompile.d.ts +13 -0
- package/dist/precompile.d.ts.map +1 -0
- package/dist/precompile.js +76 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +108 -7
- package/dist/server-codegen.d.ts +3 -1
- package/dist/server-codegen.d.ts.map +1 -1
- package/dist/server-codegen.js +3 -1
- package/dist/server-jsgen.d.ts +11 -0
- package/dist/server-jsgen.d.ts.map +1 -1
- package/dist/server-jsgen.js +75 -10
- package/dist/server-render.d.ts.map +1 -1
- package/dist/server-render.js +131 -13
- package/package.json +2 -2
package/dist/server-jsgen.js
CHANGED
|
@@ -3,6 +3,30 @@ import { isStaticIR, collectTrackedNames, transformTracked, transformTrackedInit
|
|
|
3
3
|
import { unwrapTrackCall, stripTrackGeneric, hasTopLevelComma, skipWhitespace, findBalancedEnd, startsWithIdentifier } from '@vesk/compiler/src/scan';
|
|
4
4
|
import { escapeHtml, indent, exprJS, extractTopLevelNames, extractRuntimeNames, buildParamInit, __vskHydrate, __vskImportedNames, setVskImportedNames, setVskForceClaim, takeVskForceClaim, nextVskId, } from '@vesk/compiler/src/server-utils';
|
|
5
5
|
import { localValueImportNames } from '@vesk/compiler/src/module-imports';
|
|
6
|
+
// Hydrate-mode component-boundary wrapper. Each server-rendered component call
|
|
7
|
+
// is preceded by a `<!--vsk-->` marker and a single container element: the
|
|
8
|
+
// hydration walker adopts that element as the component root
|
|
9
|
+
// (`walker.nextElement()`) and scopes interior claims via
|
|
10
|
+
// `subWalker(rootEl).contains(...)`, so fragment components with multiple root
|
|
11
|
+
// siblings need one shared container here.
|
|
12
|
+
//
|
|
13
|
+
// The container MUST NOT be a layout box. The client renderer has no such box
|
|
14
|
+
// — component nodes are appended straight into the parent — so a block wrapper
|
|
15
|
+
// that collapses to the component's own height confines every child that
|
|
16
|
+
// depends on the parent layout: `position: sticky`, `h-full`/`min-h-screen`,
|
|
17
|
+
// inset/percent offsets, flex/grid stretch. `display: contents` keeps the DOM
|
|
18
|
+
// node for claiming while suppressing its box, making SSR layout identical to
|
|
19
|
+
// the client's for every property, not just sticky.
|
|
20
|
+
//
|
|
21
|
+
// The wrapper tag must be `<span>`, not `<div>`. The HTML parser implicitly
|
|
22
|
+
// closes an open `<p>` on a flow-content start tag like `<div>`, so a `<div>`
|
|
23
|
+
// wrapper inside a `<p>` (a component called inside a paragraph, e.g. a `Link`
|
|
24
|
+
// in an eyebrow row) is hoisted out of the paragraph, stranding its `<!--vsk-->`
|
|
25
|
+
// marker in the now-empty `<p>` → `nextElementSibling` is null → the hydration
|
|
26
|
+
// claim misses and every later claim tag-shifts, wiping the SSR content. `<span>`
|
|
27
|
+
// is phrasing content (legal inside `<p>`) and never triggers an implicit close,
|
|
28
|
+
// while `display: contents` keeps it box-less everywhere a `<div>` would be.
|
|
29
|
+
const HYDRATE_COMPONENT_WRAPPER = '<!--vsk--><span style="display:contents">';
|
|
6
30
|
export function irNodeToJS(node, importedNames, isAsync = false, tracked) {
|
|
7
31
|
importedNames = importedNames || __vskImportedNames;
|
|
8
32
|
if (node instanceof StaticNode)
|
|
@@ -205,6 +229,12 @@ function mapRegionToJS(node, isAsync = false, tracked) {
|
|
|
205
229
|
const lines = [];
|
|
206
230
|
const arr = exprJSX(node.expression, tracked);
|
|
207
231
|
const item = node.itemVariable;
|
|
232
|
+
// In hydration SSR, stamp each keyed item root with `data-vsk-key` so the
|
|
233
|
+
// client can claim items by key instead of rebuilding them. The binding is
|
|
234
|
+
// injected into a shadow copy of the item root at codegen time — it flows
|
|
235
|
+
// through the existing dynamic-attribute machinery (which also forces a
|
|
236
|
+
// claim marker on otherwise-static roots).
|
|
237
|
+
const bodyForItem = __vskHydrate && node.keyExpr ? keyedItemTemplate(node) : node.bodyTemplate;
|
|
208
238
|
const hasAlternate = node.alternateNodes.length > 0;
|
|
209
239
|
const arrVar = hasAlternate ? `__a${nextVskId()}` : null;
|
|
210
240
|
if (hasAlternate) {
|
|
@@ -224,7 +254,7 @@ function mapRegionToJS(node, isAsync = false, tracked) {
|
|
|
224
254
|
lines.push('let __i = 0;');
|
|
225
255
|
lines.push(`for (const ${item} of ${loopArr}) {`);
|
|
226
256
|
lines.push(indent(`const ${node.indexVariable} = __i;`));
|
|
227
|
-
for (const n of
|
|
257
|
+
for (const n of bodyForItem) {
|
|
228
258
|
const code = irNodeToJS(n, null, isAsync, tracked);
|
|
229
259
|
if (code)
|
|
230
260
|
lines.push(indent(code));
|
|
@@ -234,7 +264,7 @@ function mapRegionToJS(node, isAsync = false, tracked) {
|
|
|
234
264
|
}
|
|
235
265
|
else {
|
|
236
266
|
lines.push(`for (const ${item} of ${loopArr}) {`);
|
|
237
|
-
for (const n of
|
|
267
|
+
for (const n of bodyForItem) {
|
|
238
268
|
const code = irNodeToJS(n, null, isAsync, tracked);
|
|
239
269
|
if (code)
|
|
240
270
|
lines.push(indent(code));
|
|
@@ -246,6 +276,27 @@ function mapRegionToJS(node, isAsync = false, tracked) {
|
|
|
246
276
|
}
|
|
247
277
|
return lines.join('\n');
|
|
248
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Returns a per-item body where the item root element (the first top-level
|
|
281
|
+
* StaticNode) carries an injected `data-vsk-key` dynamic attribute matching
|
|
282
|
+
* the map's `keyExpr`. Only the common case — an element as the item root —
|
|
283
|
+
* is supported; other root shapes (conditionals/components) render without a
|
|
284
|
+
* key and degrade to fresh rendering, exactly as before.
|
|
285
|
+
*/
|
|
286
|
+
function keyedItemTemplate(node) {
|
|
287
|
+
if (!node.keyExpr)
|
|
288
|
+
return node.bodyTemplate;
|
|
289
|
+
const template = node.bodyTemplate;
|
|
290
|
+
const idx = template.findIndex((n) => n instanceof StaticNode);
|
|
291
|
+
if (idx === -1)
|
|
292
|
+
return template;
|
|
293
|
+
const root = template[idx];
|
|
294
|
+
const copy = new StaticNode(root.tag, root.attributes, [...root.children, new DynamicBinding(node.keyExpr, 'attribute', 'data-vsk-key')], root.keyExpr);
|
|
295
|
+
copy.selfClosing = root.selfClosing;
|
|
296
|
+
const out = template.slice();
|
|
297
|
+
out[idx] = copy;
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
249
300
|
function whileLoopToJS(node, isAsync = false, tracked) {
|
|
250
301
|
const lines = [];
|
|
251
302
|
if (node.isDoWhile) {
|
|
@@ -388,7 +439,9 @@ function componentCallToJS(node, importedNames, isAsync = false, tracked) {
|
|
|
388
439
|
lines.push(`const ${calleeVar} = ${callee};`);
|
|
389
440
|
const callExpr = `${awaitKw}${calleeVar}(${propsObj}, __registry, (${calleeVar}.__veskScope || __vesk))`;
|
|
390
441
|
if (__vskHydrate) {
|
|
391
|
-
|
|
442
|
+
// Wrapper purpose documented at HYDRATE_COMPONENT_WRAPPER; it must never
|
|
443
|
+
// become a layout box (see the constant's comment for why).
|
|
444
|
+
lines.push(`__out.push(${JSON.stringify(HYDRATE_COMPONENT_WRAPPER)} + (${callExpr} || '') + '</span>');`);
|
|
392
445
|
}
|
|
393
446
|
else {
|
|
394
447
|
lines.push(`__out.push(${callExpr} || '');`);
|
|
@@ -446,8 +499,12 @@ export function generateFunctionBody(comp, importedNames) {
|
|
|
446
499
|
__currentCompName = '';
|
|
447
500
|
return lines.join('\n');
|
|
448
501
|
}
|
|
449
|
-
|
|
450
|
-
|
|
502
|
+
/**
|
|
503
|
+
* Builds the compiled body source for every component in an IR root. Used by
|
|
504
|
+
* both `buildComponentMap` (live functions) and `precompile.ts` (AOT SSR plans
|
|
505
|
+
* that reconstruct the same functions at function-bundle load time).
|
|
506
|
+
*/
|
|
507
|
+
export function buildComponentEntries(irRoot) {
|
|
451
508
|
const runtimeNames = extractRuntimeNames(irRoot.imports);
|
|
452
509
|
const localValueNames = localValueImportNames(irRoot.imports);
|
|
453
510
|
const topNames = extractTopLevelNames(irRoot.topLevelCode);
|
|
@@ -463,6 +520,7 @@ export function buildComponentMap(irRoot, useSharedScope) {
|
|
|
463
520
|
const allNames = [...new Set([...runtimeNames, ...topNames, ...extraNames, ...localValueNames])];
|
|
464
521
|
const scopeDecl = allNames.length > 0 ? `const { ${allNames.join(', ')} } = __vesk;\n` : '';
|
|
465
522
|
setVskImportedNames(importedNames);
|
|
523
|
+
const entries = [];
|
|
466
524
|
for (const comp of irRoot.components) {
|
|
467
525
|
const bodyCode = generateFunctionBody(comp, importedNames);
|
|
468
526
|
const paramInit = buildParamInit(comp.paramNames);
|
|
@@ -470,15 +528,22 @@ export function buildComponentMap(irRoot, useSharedScope) {
|
|
|
470
528
|
? `console.error('[SSR-CALL]', ${JSON.stringify(comp.name)}, props ? JSON.stringify(props) : String(props));\n`
|
|
471
529
|
: '';
|
|
472
530
|
const code = `${scopeDecl}${paramInit}${diag}\n${bodyCode}`;
|
|
531
|
+
entries.push({ name: comp.name, source: code, isAsync: comp.isAsync || comp.ssrAwait });
|
|
532
|
+
}
|
|
533
|
+
setVskImportedNames(null);
|
|
534
|
+
return entries;
|
|
535
|
+
}
|
|
536
|
+
export function buildComponentMap(irRoot, useSharedScope) {
|
|
537
|
+
const map = new Map();
|
|
538
|
+
for (const entry of buildComponentEntries(irRoot)) {
|
|
473
539
|
let fn;
|
|
474
|
-
if (
|
|
475
|
-
fn = new Function('props', '__registry', '__vesk', `return (async () => {\n${
|
|
540
|
+
if (entry.isAsync) {
|
|
541
|
+
fn = new Function('props', '__registry', '__vesk', `return (async () => {\n${entry.source}\n})()`);
|
|
476
542
|
}
|
|
477
543
|
else {
|
|
478
|
-
fn = new Function('props', '__registry', '__vesk',
|
|
544
|
+
fn = new Function('props', '__registry', '__vesk', entry.source);
|
|
479
545
|
}
|
|
480
|
-
map.set(
|
|
546
|
+
map.set(entry.name, fn);
|
|
481
547
|
}
|
|
482
|
-
setVskImportedNames(null);
|
|
483
548
|
return map;
|
|
484
549
|
}
|
|
@@ -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;AA6BvJ,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,iBAAiB,CAEhG;AA6CD,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,
|
|
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
|
@@ -83,7 +83,8 @@ export function render(source, componentName, props = {}, registry = new Map(),
|
|
|
83
83
|
const targetComp = compiled.ir.components.find((c) => c.name === componentName);
|
|
84
84
|
const isAsyncComp = !!(targetComp && (targetComp.isAsync || targetComp.ssrAwait));
|
|
85
85
|
globalThis.__vsk_ssr = true;
|
|
86
|
-
globalThis.__vsk_ssr_token
|
|
86
|
+
if (!globalThis.__vsk_ssr_token)
|
|
87
|
+
globalThis.__vsk_ssr_token = Math.random().toString(36).slice(2);
|
|
87
88
|
const renderToken = globalThis.__vsk_ssr_token;
|
|
88
89
|
if (isAsyncComp) {
|
|
89
90
|
return withSsrStore(() => (async () => {
|
|
@@ -93,6 +94,7 @@ export function render(source, componentName, props = {}, registry = new Map(),
|
|
|
93
94
|
}
|
|
94
95
|
finally {
|
|
95
96
|
delete globalThis.__vsk_ssr;
|
|
97
|
+
await settleSsrPromises(renderToken);
|
|
96
98
|
clearSsrCells(renderToken);
|
|
97
99
|
}
|
|
98
100
|
return bodyHtml;
|
|
@@ -130,8 +132,14 @@ export function renderPage(source, componentName, props = {}, registry = new Map
|
|
|
130
132
|
const targetComp = ir.components.find((c) => c.name === componentName);
|
|
131
133
|
const doRender = (ssrProps) => {
|
|
132
134
|
globalThis.__vsk_ssr = true;
|
|
133
|
-
|
|
135
|
+
// Reuse an in-flight render token: the generated adapter handler renders
|
|
136
|
+
// page -> layouts -> document sequentially within one request, and all of
|
|
137
|
+
// them must share a single data slot so the final renderFullPage merge can
|
|
138
|
+
// serialize the whole handoff. Only create when no render began yet.
|
|
139
|
+
if (!globalThis.__vsk_ssr_token)
|
|
140
|
+
globalThis.__vsk_ssr_token = Math.random().toString(36).slice(2);
|
|
134
141
|
const renderToken = globalThis.__vsk_ssr_token;
|
|
142
|
+
pruneSsrDataSlots();
|
|
135
143
|
if (targetComp && (targetComp.isAsync || targetComp.ssrAwait)) {
|
|
136
144
|
return withSsrStore(() => (async () => {
|
|
137
145
|
let bodyHtml;
|
|
@@ -140,6 +148,11 @@ export function renderPage(source, componentName, props = {}, registry = new Map
|
|
|
140
148
|
}
|
|
141
149
|
finally {
|
|
142
150
|
delete globalThis.__vsk_ssr;
|
|
151
|
+
// Settle before cleanup: the fetch resolution callbacks write the
|
|
152
|
+
// SSR data (flat + per-token slot), and without this the promises
|
|
153
|
+
// were deleted by clearSsrCells before they ever resolved — the
|
|
154
|
+
// handoff came back empty and hydration claimed stale SSR nodes.
|
|
155
|
+
await settleSsrPromises(renderToken);
|
|
143
156
|
clearSsrCells(renderToken);
|
|
144
157
|
}
|
|
145
158
|
return {
|
|
@@ -155,9 +168,19 @@ export function renderPage(source, componentName, props = {}, registry = new Map
|
|
|
155
168
|
}
|
|
156
169
|
finally {
|
|
157
170
|
delete globalThis.__vsk_ssr;
|
|
158
|
-
clearSsrCells(renderToken);
|
|
159
171
|
}
|
|
160
172
|
const headHtml = targetComp ? renderHeadHtml(targetComp, ssrProps) : '';
|
|
173
|
+
const pending = globalThis[`__vsk_ssr_promises_${renderToken}`];
|
|
174
|
+
if (pending && pending.length > 0) {
|
|
175
|
+
// A sync component still started server resources (useFetch): settle them
|
|
176
|
+
// before returning so the handoff serializes real data, not loading state.
|
|
177
|
+
return withSsrStore(() => (async () => {
|
|
178
|
+
await settleSsrPromises(renderToken);
|
|
179
|
+
clearSsrCells(renderToken);
|
|
180
|
+
return { body: bodyHtml, head: headHtml, props: ssrProps };
|
|
181
|
+
})());
|
|
182
|
+
}
|
|
183
|
+
clearSsrCells(renderToken);
|
|
161
184
|
return { body: bodyHtml, head: headHtml, props: ssrProps };
|
|
162
185
|
};
|
|
163
186
|
if (ir.loadFn) {
|
|
@@ -182,7 +205,6 @@ function clearSsrCells(token) {
|
|
|
182
205
|
return;
|
|
183
206
|
delete globalThis[`__vsk_ssr_promises_${token}`];
|
|
184
207
|
delete globalThis[`__vsk_ssr_failures_${token}`];
|
|
185
|
-
delete globalThis.__vsk_ssr_token;
|
|
186
208
|
const cells = globalThis.__vsk_ssr_cells;
|
|
187
209
|
if (!cells || !(cells instanceof Map))
|
|
188
210
|
return;
|
|
@@ -191,6 +213,35 @@ function clearSsrCells(token) {
|
|
|
191
213
|
cells.delete(k);
|
|
192
214
|
}
|
|
193
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Await every server resource promise registered under a render token. The
|
|
218
|
+
* fetch callbacks call setSsrData (flat + per-token slot) once they resolve,
|
|
219
|
+
* so a render MUST settle before cleanup or its handoff serializes nothing.
|
|
220
|
+
*/
|
|
221
|
+
async function settleSsrPromises(token) {
|
|
222
|
+
if (!token)
|
|
223
|
+
return;
|
|
224
|
+
const pending = globalThis[`__vsk_ssr_promises_${token}`];
|
|
225
|
+
if (!pending || pending.length === 0)
|
|
226
|
+
return;
|
|
227
|
+
await Promise.allSettled(pending);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Bound the per-token SSR data slots against abandonment (dev-partial
|
|
231
|
+
* renderPage / render() that never runs renderFullPage). Slots are keyed by
|
|
232
|
+
* Math.random tokens, so deleted-slot accounting by token can't be cleaned by
|
|
233
|
+
* an owning render; cap the map instead. RenderFullPage/RenderPageStream delete
|
|
234
|
+
* their own slot + token in every path, so this only guards the untracked ones.
|
|
235
|
+
*/
|
|
236
|
+
function pruneSsrDataSlots() {
|
|
237
|
+
const g = globalThis;
|
|
238
|
+
const keys = Object.keys(g).filter((k) => typeof k === 'string' && k.startsWith('__vsk_ssr_data_'));
|
|
239
|
+
if (keys.length <= 40)
|
|
240
|
+
return;
|
|
241
|
+
keys.sort().slice(0, keys.length - 40).forEach((k) => {
|
|
242
|
+
delete g[k];
|
|
243
|
+
});
|
|
244
|
+
}
|
|
194
245
|
/**
|
|
195
246
|
* Run the active plugins' `onHead` hooks (config order) against an assembled
|
|
196
247
|
* `<head>` string. A hook returns the new head, or `null` to keep the input.
|
|
@@ -279,11 +330,17 @@ export async function ssg(source, componentName, customProps, options = {}) {
|
|
|
279
330
|
const cssUrls = options.cssUrls || (options.cssUrl ? [options.cssUrl] : []);
|
|
280
331
|
const cssLink = cssUrls.map(u => `\t<link rel="stylesheet" href="${u}" />\n`).join('');
|
|
281
332
|
const renderCtx = { sourcePath: options.sourcePath };
|
|
282
|
-
|
|
333
|
+
const baseHeadParts = [];
|
|
334
|
+
const lowerHead = (headHtml || '').toLowerCase();
|
|
335
|
+
if (!lowerHead.includes('charset'))
|
|
336
|
+
baseHeadParts.push('\t<meta charset="utf-8" />');
|
|
337
|
+
if (!lowerHead.includes('viewport'))
|
|
338
|
+
baseHeadParts.push('\t<meta name="viewport" content="width=device-width, initial-scale=1" />');
|
|
339
|
+
let finalHead = baseHeadParts.join('\n');
|
|
283
340
|
if (cssLink)
|
|
284
|
-
finalHead += '\n' + cssLink.trimEnd();
|
|
341
|
+
finalHead += (finalHead ? '\n' : '') + cssLink.trimEnd();
|
|
285
342
|
if (headHtml)
|
|
286
|
-
finalHead += '\n' + headHtml.split('\n').map((l) => '\t' + l).join('\n');
|
|
343
|
+
finalHead += (finalHead ? '\n' : '') + headHtml.split('\n').map((l) => '\t' + l).join('\n');
|
|
287
344
|
finalHead = await applyHeadInjects(finalHead, { plugins: options.plugins, headExtra: options.headExtra }, renderCtx);
|
|
288
345
|
const html = `<!DOCTYPE html>
|
|
289
346
|
<html>
|
|
@@ -336,6 +393,19 @@ export function buildDataScripts(ssrProps, ssrData, external) {
|
|
|
336
393
|
export async function renderFullPage(source, componentName, props = {}, registry = new Map(), options = {}) {
|
|
337
394
|
return withSsrStore(async () => {
|
|
338
395
|
globalThis.__vsk_ssr = true;
|
|
396
|
+
// Reuse the request token when a page/layout render already started one (the
|
|
397
|
+
// generated adapter handler renders page -> layouts -> document within one
|
|
398
|
+
// process), otherwise start fresh. One request = one token = one data slot.
|
|
399
|
+
if (!globalThis.__vsk_ssr_token)
|
|
400
|
+
globalThis.__vsk_ssr_token = Math.random().toString(36).slice(2);
|
|
401
|
+
const renderToken = globalThis.__vsk_ssr_token;
|
|
402
|
+
pruneSsrDataSlots();
|
|
403
|
+
// Start this render's handoff store fresh: setSsrData mirrors into
|
|
404
|
+
// globalThis.__vsk_ssr_data (see resource.ts) because the AsyncLocalStorage
|
|
405
|
+
// sink snapshot can be empty when the write runs in a forked async context.
|
|
406
|
+
// Resetting here also gives the /about-after-/posts no-leak behavior the
|
|
407
|
+
// ALS isolation was introduced to fix.
|
|
408
|
+
globalThis.__vsk_ssr_data = {};
|
|
339
409
|
try {
|
|
340
410
|
let ssrProps = { ...props };
|
|
341
411
|
let serializedProps = null;
|
|
@@ -354,7 +424,22 @@ export async function renderFullPage(source, componentName, props = {}, registry
|
|
|
354
424
|
serializedProps = JSON.stringify(ssrProps);
|
|
355
425
|
}
|
|
356
426
|
const rendered = await renderPage(source, componentName, ssrProps, registry, { ...options, __vesk: options.__vesk });
|
|
357
|
-
|
|
427
|
+
// Defensive settle: page/layout renders already awaited their own SSR
|
|
428
|
+
// promises before cleanup; this covers any resource started here (loadFn).
|
|
429
|
+
await settleSsrPromises(renderToken);
|
|
430
|
+
// Merge the ALS sink snapshot with the request's per-token data slot. The
|
|
431
|
+
// slot is the deterministic channel for writes that happened in a forked
|
|
432
|
+
// async context (native fetch) — the sink alone can miss them. The slot is
|
|
433
|
+
// request-scoped (deleted below), so a late write can never leak into a
|
|
434
|
+
// following request the way the flat global would.
|
|
435
|
+
const slot = globalThis[`__vsk_ssr_data_${renderToken}`] || {};
|
|
436
|
+
const ssrData = {
|
|
437
|
+
...ssrSink.snapshot(),
|
|
438
|
+
...slot,
|
|
439
|
+
};
|
|
440
|
+
if (process.env.VESK_SSR_TRACE) {
|
|
441
|
+
console.error(`[render-trace] renderFullPage comp=${componentName} keys=${Object.keys(ssrData).join(',') || '∅'} slot=${Object.keys(slot).join(',') || '∅'} sink=${Object.keys(ssrSink.snapshot()).join(',') || '∅'}`);
|
|
442
|
+
}
|
|
358
443
|
let headHtml = rendered.head;
|
|
359
444
|
if (options.pageHead) {
|
|
360
445
|
const merged = mergeHeadHtml(options.pageHead, rendered.head);
|
|
@@ -373,7 +458,12 @@ export async function renderFullPage(source, componentName, props = {}, registry
|
|
|
373
458
|
: '';
|
|
374
459
|
const dataScripts = buildDataScripts(ssrProps, ssrData, options.externalDataScript);
|
|
375
460
|
const dataScriptBlock = dataScripts.length > 0 ? '\n' + dataScripts.join('\n') + '\n' : '';
|
|
376
|
-
const
|
|
461
|
+
const lowerHeadFull = (headHtml || '').toLowerCase();
|
|
462
|
+
const headLines = [];
|
|
463
|
+
if (!lowerHeadFull.includes('charset'))
|
|
464
|
+
headLines.push('\t<meta charset="utf-8" />');
|
|
465
|
+
if (!lowerHeadFull.includes('viewport'))
|
|
466
|
+
headLines.push('\t<meta name="viewport" content="width=device-width, initial-scale=1" />');
|
|
377
467
|
if (cssLink)
|
|
378
468
|
headLines.push(cssLink.trimEnd());
|
|
379
469
|
if (headHtml)
|
|
@@ -402,6 +492,9 @@ ${dataScriptBlock}${clientScript}</body>
|
|
|
402
492
|
}
|
|
403
493
|
finally {
|
|
404
494
|
delete globalThis.__vsk_ssr;
|
|
495
|
+
delete globalThis[`__vsk_ssr_data_${renderToken}`];
|
|
496
|
+
delete globalThis.__vsk_ssr_token;
|
|
497
|
+
pruneSsrDataSlots();
|
|
405
498
|
}
|
|
406
499
|
});
|
|
407
500
|
}
|
|
@@ -436,7 +529,7 @@ export function renderPageStream(source, componentName, props = {}, registry = n
|
|
|
436
529
|
const targetComp = ir.components.find((c) => c.name === componentName);
|
|
437
530
|
const cssUrls = options.cssUrls || (options.cssUrl ? [options.cssUrl] : []);
|
|
438
531
|
const cssLink = cssUrls.map(u => `\t<link rel="stylesheet" href="${u}" />\n`).join('');
|
|
439
|
-
const headParts = [
|
|
532
|
+
const headParts = [];
|
|
440
533
|
if (cssLink)
|
|
441
534
|
headParts.push(cssLink.trimEnd());
|
|
442
535
|
if (options.security) {
|
|
@@ -448,6 +541,7 @@ export function renderPageStream(source, componentName, props = {}, registry = n
|
|
|
448
541
|
if (sec.autoEscape !== false)
|
|
449
542
|
headParts.push('\t<!-- vesk: auto-escape enabled -->');
|
|
450
543
|
}
|
|
544
|
+
let streamHeadHtml = null;
|
|
451
545
|
if (targetComp) {
|
|
452
546
|
let headHtml = renderHeadHtml(targetComp, ssrProps);
|
|
453
547
|
if (options.pageHead) {
|
|
@@ -459,26 +553,50 @@ export function renderPageStream(source, componentName, props = {}, registry = n
|
|
|
459
553
|
}
|
|
460
554
|
}
|
|
461
555
|
}
|
|
462
|
-
if (headHtml)
|
|
556
|
+
if (headHtml) {
|
|
557
|
+
streamHeadHtml = headHtml;
|
|
463
558
|
headParts.push('\t' + headHtml.split('\n').join('\n\t'));
|
|
559
|
+
}
|
|
464
560
|
}
|
|
561
|
+
const lowerStreamHead = (streamHeadHtml || '').toLowerCase();
|
|
562
|
+
const baseStream = [];
|
|
563
|
+
if (!lowerStreamHead.includes('charset'))
|
|
564
|
+
baseStream.push('\t<meta charset="utf-8" />');
|
|
565
|
+
if (!lowerStreamHead.includes('viewport'))
|
|
566
|
+
baseStream.push('\t<meta name="viewport" content="width=device-width, initial-scale=1" />');
|
|
567
|
+
headParts.unshift(...baseStream);
|
|
465
568
|
yield '<!DOCTYPE html>\n<html>\n<head>\n';
|
|
466
569
|
const finalHead = await applyHeadInjects(headParts.join('\n'), { plugins: options.plugins, headExtra: options.headExtra }, { sourcePath: options.sourcePath, url: options.__vesk?.url || undefined });
|
|
467
570
|
yield finalHead + '\n';
|
|
468
571
|
yield '</head>\n<body>\n<div id="root">\n';
|
|
469
572
|
globalThis.__vsk_ssr = true;
|
|
470
|
-
|
|
573
|
+
// Fresh handoff store per render; see renderFullPage note.
|
|
574
|
+
globalThis.__vsk_ssr_data = {};
|
|
575
|
+
if (!globalThis.__vsk_ssr_token)
|
|
576
|
+
globalThis.__vsk_ssr_token = Math.random().toString(36).slice(2);
|
|
471
577
|
const renderToken = globalThis.__vsk_ssr_token;
|
|
578
|
+
pruneSsrDataSlots();
|
|
472
579
|
let bodyHtml;
|
|
473
580
|
try {
|
|
474
581
|
bodyHtml = await Promise.resolve(renderFn(ssrProps, fullRegistry, scopedVesk(renderFn, __vesk)));
|
|
475
582
|
}
|
|
476
583
|
finally {
|
|
477
584
|
delete globalThis.__vsk_ssr;
|
|
585
|
+
await settleSsrPromises(renderToken);
|
|
478
586
|
clearSsrCells(renderToken);
|
|
479
587
|
}
|
|
480
588
|
yield bodyHtml;
|
|
481
|
-
const
|
|
589
|
+
const slot = globalThis[`__vsk_ssr_data_${renderToken}`] || {};
|
|
590
|
+
const ssrData = {
|
|
591
|
+
...ssrSink.snapshot(),
|
|
592
|
+
...slot,
|
|
593
|
+
};
|
|
594
|
+
if (process.env.VESK_SSR_TRACE) {
|
|
595
|
+
console.error(`[render-trace] renderPageStream comp=${componentName} keys=${Object.keys(ssrData).join(',') || '∅'} slot=${Object.keys(slot).join(',') || '∅'} sink=${Object.keys(ssrSink.snapshot()).join(',') || '∅'}`);
|
|
596
|
+
}
|
|
597
|
+
delete globalThis[`__vsk_ssr_data_${renderToken}`];
|
|
598
|
+
delete globalThis.__vsk_ssr_token;
|
|
599
|
+
pruneSsrDataSlots();
|
|
482
600
|
const dataScripts = buildDataScripts(ssrProps, ssrData, options.externalDataScript);
|
|
483
601
|
const dataScriptBlock = dataScripts.length > 0 ? '\n' + dataScripts.join('\n') : '';
|
|
484
602
|
// Mirror renderFullPage: a full document must carry the client bundle or
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vesk/compiler",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.23",
|
|
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.23",
|
|
24
24
|
"acorn": "^8.15.0",
|
|
25
25
|
"esrap": "^2.3.0",
|
|
26
26
|
"is-reference": "^3.0.0",
|