@vesk/compiler 0.2.22 → 0.2.24
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 +247 -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 +84 -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)
|
|
@@ -185,18 +209,27 @@ function opaqueRegionToJS(node, isAsync = false, tracked) {
|
|
|
185
209
|
const lines = [];
|
|
186
210
|
const cond = exprJSX(node.condition, tracked);
|
|
187
211
|
lines.push(`if (${cond}) {`);
|
|
212
|
+
// Static branch content still needs server claim markers: the client
|
|
213
|
+
// re-branches an if/else-if region on every tracked change (OpaqueDynamicRegion
|
|
214
|
+
// claimStatic codegen), so opposed to plain component bodies it must be able
|
|
215
|
+
// to adopt the SSR nodes in place — not leave them orphaned next to the
|
|
216
|
+
// region's anchors. Mirrors mapRegionToJS' alternate handling below.
|
|
217
|
+
setVskForceClaim(true);
|
|
188
218
|
for (const n of node.consequentNodes) {
|
|
189
219
|
const code = irNodeToJS(n, null, isAsync, tracked);
|
|
190
220
|
if (code)
|
|
191
221
|
lines.push(indent(code));
|
|
192
222
|
}
|
|
223
|
+
setVskForceClaim(false);
|
|
193
224
|
if (node.alternateNodes.length > 0) {
|
|
194
225
|
lines.push(`} else {`);
|
|
226
|
+
setVskForceClaim(true);
|
|
195
227
|
for (const n of node.alternateNodes) {
|
|
196
228
|
const code = irNodeToJS(n, null, isAsync, tracked);
|
|
197
229
|
if (code)
|
|
198
230
|
lines.push(indent(code));
|
|
199
231
|
}
|
|
232
|
+
setVskForceClaim(false);
|
|
200
233
|
}
|
|
201
234
|
lines.push(`}`);
|
|
202
235
|
return lines.join('\n');
|
|
@@ -205,6 +238,12 @@ function mapRegionToJS(node, isAsync = false, tracked) {
|
|
|
205
238
|
const lines = [];
|
|
206
239
|
const arr = exprJSX(node.expression, tracked);
|
|
207
240
|
const item = node.itemVariable;
|
|
241
|
+
// In hydration SSR, stamp each keyed item root with `data-vsk-key` so the
|
|
242
|
+
// client can claim items by key instead of rebuilding them. The binding is
|
|
243
|
+
// injected into a shadow copy of the item root at codegen time — it flows
|
|
244
|
+
// through the existing dynamic-attribute machinery (which also forces a
|
|
245
|
+
// claim marker on otherwise-static roots).
|
|
246
|
+
const bodyForItem = __vskHydrate && node.keyExpr ? keyedItemTemplate(node) : node.bodyTemplate;
|
|
208
247
|
const hasAlternate = node.alternateNodes.length > 0;
|
|
209
248
|
const arrVar = hasAlternate ? `__a${nextVskId()}` : null;
|
|
210
249
|
if (hasAlternate) {
|
|
@@ -224,7 +263,7 @@ function mapRegionToJS(node, isAsync = false, tracked) {
|
|
|
224
263
|
lines.push('let __i = 0;');
|
|
225
264
|
lines.push(`for (const ${item} of ${loopArr}) {`);
|
|
226
265
|
lines.push(indent(`const ${node.indexVariable} = __i;`));
|
|
227
|
-
for (const n of
|
|
266
|
+
for (const n of bodyForItem) {
|
|
228
267
|
const code = irNodeToJS(n, null, isAsync, tracked);
|
|
229
268
|
if (code)
|
|
230
269
|
lines.push(indent(code));
|
|
@@ -234,7 +273,7 @@ function mapRegionToJS(node, isAsync = false, tracked) {
|
|
|
234
273
|
}
|
|
235
274
|
else {
|
|
236
275
|
lines.push(`for (const ${item} of ${loopArr}) {`);
|
|
237
|
-
for (const n of
|
|
276
|
+
for (const n of bodyForItem) {
|
|
238
277
|
const code = irNodeToJS(n, null, isAsync, tracked);
|
|
239
278
|
if (code)
|
|
240
279
|
lines.push(indent(code));
|
|
@@ -246,6 +285,27 @@ function mapRegionToJS(node, isAsync = false, tracked) {
|
|
|
246
285
|
}
|
|
247
286
|
return lines.join('\n');
|
|
248
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Returns a per-item body where the item root element (the first top-level
|
|
290
|
+
* StaticNode) carries an injected `data-vsk-key` dynamic attribute matching
|
|
291
|
+
* the map's `keyExpr`. Only the common case — an element as the item root —
|
|
292
|
+
* is supported; other root shapes (conditionals/components) render without a
|
|
293
|
+
* key and degrade to fresh rendering, exactly as before.
|
|
294
|
+
*/
|
|
295
|
+
function keyedItemTemplate(node) {
|
|
296
|
+
if (!node.keyExpr)
|
|
297
|
+
return node.bodyTemplate;
|
|
298
|
+
const template = node.bodyTemplate;
|
|
299
|
+
const idx = template.findIndex((n) => n instanceof StaticNode);
|
|
300
|
+
if (idx === -1)
|
|
301
|
+
return template;
|
|
302
|
+
const root = template[idx];
|
|
303
|
+
const copy = new StaticNode(root.tag, root.attributes, [...root.children, new DynamicBinding(node.keyExpr, 'attribute', 'data-vsk-key')], root.keyExpr);
|
|
304
|
+
copy.selfClosing = root.selfClosing;
|
|
305
|
+
const out = template.slice();
|
|
306
|
+
out[idx] = copy;
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
249
309
|
function whileLoopToJS(node, isAsync = false, tracked) {
|
|
250
310
|
const lines = [];
|
|
251
311
|
if (node.isDoWhile) {
|
|
@@ -388,7 +448,9 @@ function componentCallToJS(node, importedNames, isAsync = false, tracked) {
|
|
|
388
448
|
lines.push(`const ${calleeVar} = ${callee};`);
|
|
389
449
|
const callExpr = `${awaitKw}${calleeVar}(${propsObj}, __registry, (${calleeVar}.__veskScope || __vesk))`;
|
|
390
450
|
if (__vskHydrate) {
|
|
391
|
-
|
|
451
|
+
// Wrapper purpose documented at HYDRATE_COMPONENT_WRAPPER; it must never
|
|
452
|
+
// become a layout box (see the constant's comment for why).
|
|
453
|
+
lines.push(`__out.push(${JSON.stringify(HYDRATE_COMPONENT_WRAPPER)} + (${callExpr} || '') + '</span>');`);
|
|
392
454
|
}
|
|
393
455
|
else {
|
|
394
456
|
lines.push(`__out.push(${callExpr} || '');`);
|
|
@@ -446,8 +508,12 @@ export function generateFunctionBody(comp, importedNames) {
|
|
|
446
508
|
__currentCompName = '';
|
|
447
509
|
return lines.join('\n');
|
|
448
510
|
}
|
|
449
|
-
|
|
450
|
-
|
|
511
|
+
/**
|
|
512
|
+
* Builds the compiled body source for every component in an IR root. Used by
|
|
513
|
+
* both `buildComponentMap` (live functions) and `precompile.ts` (AOT SSR plans
|
|
514
|
+
* that reconstruct the same functions at function-bundle load time).
|
|
515
|
+
*/
|
|
516
|
+
export function buildComponentEntries(irRoot) {
|
|
451
517
|
const runtimeNames = extractRuntimeNames(irRoot.imports);
|
|
452
518
|
const localValueNames = localValueImportNames(irRoot.imports);
|
|
453
519
|
const topNames = extractTopLevelNames(irRoot.topLevelCode);
|
|
@@ -463,6 +529,7 @@ export function buildComponentMap(irRoot, useSharedScope) {
|
|
|
463
529
|
const allNames = [...new Set([...runtimeNames, ...topNames, ...extraNames, ...localValueNames])];
|
|
464
530
|
const scopeDecl = allNames.length > 0 ? `const { ${allNames.join(', ')} } = __vesk;\n` : '';
|
|
465
531
|
setVskImportedNames(importedNames);
|
|
532
|
+
const entries = [];
|
|
466
533
|
for (const comp of irRoot.components) {
|
|
467
534
|
const bodyCode = generateFunctionBody(comp, importedNames);
|
|
468
535
|
const paramInit = buildParamInit(comp.paramNames);
|
|
@@ -470,15 +537,22 @@ export function buildComponentMap(irRoot, useSharedScope) {
|
|
|
470
537
|
? `console.error('[SSR-CALL]', ${JSON.stringify(comp.name)}, props ? JSON.stringify(props) : String(props));\n`
|
|
471
538
|
: '';
|
|
472
539
|
const code = `${scopeDecl}${paramInit}${diag}\n${bodyCode}`;
|
|
540
|
+
entries.push({ name: comp.name, source: code, isAsync: comp.isAsync || comp.ssrAwait });
|
|
541
|
+
}
|
|
542
|
+
setVskImportedNames(null);
|
|
543
|
+
return entries;
|
|
544
|
+
}
|
|
545
|
+
export function buildComponentMap(irRoot, useSharedScope) {
|
|
546
|
+
const map = new Map();
|
|
547
|
+
for (const entry of buildComponentEntries(irRoot)) {
|
|
473
548
|
let fn;
|
|
474
|
-
if (
|
|
475
|
-
fn = new Function('props', '__registry', '__vesk', `return (async () => {\n${
|
|
549
|
+
if (entry.isAsync) {
|
|
550
|
+
fn = new Function('props', '__registry', '__vesk', `return (async () => {\n${entry.source}\n})()`);
|
|
476
551
|
}
|
|
477
552
|
else {
|
|
478
|
-
fn = new Function('props', '__registry', '__vesk',
|
|
553
|
+
fn = new Function('props', '__registry', '__vesk', entry.source);
|
|
479
554
|
}
|
|
480
|
-
map.set(
|
|
555
|
+
map.set(entry.name, fn);
|
|
481
556
|
}
|
|
482
|
-
setVskImportedNames(null);
|
|
483
557
|
return map;
|
|
484
558
|
}
|
|
@@ -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.24",
|
|
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.24",
|
|
24
24
|
"acorn": "^8.15.0",
|
|
25
25
|
"esrap": "^2.3.0",
|
|
26
26
|
"is-reference": "^3.0.0",
|