@webjsdev/server 0.8.59 → 0.8.60
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/index.d.ts +99 -10
- package/index.js +1 -0
- package/package.json +2 -2
- package/src/action-seed.js +75 -7
- package/src/component-elision.js +75 -16
- package/src/component-scanner.js +82 -26
- package/src/dev.js +62 -9
- package/src/elision-differential.js +101 -0
- package/src/elision-report.js +133 -30
- package/src/form-dispatch.js +169 -3
- package/src/js-scan.js +41 -13
- package/src/ssr.js +50 -14
package/src/js-scan.js
CHANGED
|
@@ -632,28 +632,46 @@ export function matchClosingParenthesis(s, start) {
|
|
|
632
632
|
* `}` inside `'…'`, `"…"`, or backtick templates don't decrement depth.
|
|
633
633
|
* Returns -1 if no balanced brace is found.
|
|
634
634
|
*
|
|
635
|
+
* A template hole is a CODE context nested inside a template, not a brace in
|
|
636
|
+
* the enclosing block, so it gets its own frame on the stack: `${` pushes,
|
|
637
|
+
* and the `}` that returns that frame to depth zero pops back into the
|
|
638
|
+
* template rather than counting toward the block being matched. An earlier
|
|
639
|
+
* version incremented the outer depth at `${` and then never decremented it
|
|
640
|
+
* (the closing `}` arrived while still in template state), so depth could
|
|
641
|
+
* never return to zero and a class body holding `` html`…${x}…` `` was
|
|
642
|
+
* unmatchable. Every caller passes a masked source in which holes are already
|
|
643
|
+
* blanked, so the bug is invisible until one passes raw source.
|
|
644
|
+
*
|
|
635
645
|
* @param {string} s
|
|
636
646
|
* @param {number} start
|
|
637
647
|
*/
|
|
638
648
|
export function matchClosingBrace(s, start) {
|
|
639
|
-
|
|
649
|
+
// Innermost first. `tpl` frames are template literals (no brace counting);
|
|
650
|
+
// `!tpl` frames are code, each with its own depth.
|
|
651
|
+
/** @type {Array<{ tpl: boolean, depth: number }>} */
|
|
652
|
+
const stack = [{ tpl: false, depth: 1 }];
|
|
640
653
|
let i = start;
|
|
641
|
-
let str = ''; // '', "'", '"', or backtick
|
|
642
654
|
while (i < s.length) {
|
|
655
|
+
const top = stack[stack.length - 1];
|
|
643
656
|
const c = s[i];
|
|
644
|
-
if (
|
|
657
|
+
if (top.tpl) {
|
|
645
658
|
if (c === '\\') { i += 2; continue; }
|
|
646
|
-
if (c ===
|
|
647
|
-
|
|
648
|
-
// template hole, count its closing `}` toward our brace depth.
|
|
649
|
-
depth++;
|
|
650
|
-
i += 2;
|
|
651
|
-
continue;
|
|
652
|
-
}
|
|
659
|
+
if (c === '`') { stack.pop(); i++; continue; }
|
|
660
|
+
if (c === '$' && s[i + 1] === '{') { stack.push({ tpl: false, depth: 1 }); i += 2; continue; }
|
|
653
661
|
i++;
|
|
654
662
|
continue;
|
|
655
663
|
}
|
|
656
|
-
if (c === "'" || c === '"'
|
|
664
|
+
if (c === "'" || c === '"') {
|
|
665
|
+
i++;
|
|
666
|
+
while (i < s.length) {
|
|
667
|
+
if (s[i] === '\\') { i += 2; continue; }
|
|
668
|
+
const d = s[i];
|
|
669
|
+
i++;
|
|
670
|
+
if (d === c || d === '\n') break; // closed, or unterminated at EOL
|
|
671
|
+
}
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (c === '`') { stack.push({ tpl: true, depth: 0 }); i++; continue; }
|
|
657
675
|
if (c === '/' && s[i + 1] === '/') { // line comment
|
|
658
676
|
while (i < s.length && s[i] !== '\n') i++;
|
|
659
677
|
continue;
|
|
@@ -664,8 +682,18 @@ export function matchClosingBrace(s, start) {
|
|
|
664
682
|
i += 2;
|
|
665
683
|
continue;
|
|
666
684
|
}
|
|
667
|
-
if (c === '{') depth++;
|
|
668
|
-
|
|
685
|
+
if (c === '{') { top.depth++; i++; continue; }
|
|
686
|
+
if (c === '}') {
|
|
687
|
+
top.depth--;
|
|
688
|
+
// Depth zero closes this frame: the outermost one is the answer, an inner
|
|
689
|
+
// one is a template hole ending and hands control back to its template.
|
|
690
|
+
if (top.depth === 0) {
|
|
691
|
+
if (stack.length === 1) return i;
|
|
692
|
+
stack.pop();
|
|
693
|
+
}
|
|
694
|
+
i++;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
669
697
|
i++;
|
|
670
698
|
}
|
|
671
699
|
return -1;
|
package/src/ssr.js
CHANGED
|
@@ -6,7 +6,7 @@ import { withBasePath } from './base-path.js';
|
|
|
6
6
|
import { withAssetHash } from './asset-hash.js';
|
|
7
7
|
import { jsonForScriptTag } from './script-tag-json.js';
|
|
8
8
|
import { transitiveDeps, bareImports } from './module-graph.js';
|
|
9
|
-
import { seedingEnabled, collectSeeds, buildSeedScript } from './action-seed.js';
|
|
9
|
+
import { seedingEnabled, collectSeeds, buildSeedScript, SEED_DROP_BLOCK } from './action-seed.js';
|
|
10
10
|
import { BUFFERED_MARKER, STREAM_MARKER } from './conditional-get.js';
|
|
11
11
|
import {
|
|
12
12
|
readRevalidate,
|
|
@@ -57,7 +57,15 @@ export async function ssrPage(route, params, url, opts) {
|
|
|
57
57
|
revalidateSeconds = readRevalidate(pageMod);
|
|
58
58
|
if (revalidateSeconds !== null) {
|
|
59
59
|
const hit = await readHtmlCache(url);
|
|
60
|
-
if (hit)
|
|
60
|
+
if (hit) {
|
|
61
|
+
const cached = cachedHtmlResponse(hit, opts.req, url);
|
|
62
|
+
// A cache hit returns before any seed work runs, so it would otherwise
|
|
63
|
+
// report nothing at all, which reads as "seeding is broken" when it is
|
|
64
|
+
// really the cache answering (#1309). The seed block rides INSIDE the
|
|
65
|
+
// cached bytes, so the seeds are exactly as fresh as the HTML.
|
|
66
|
+
if (opts.dev) cached.headers.set('X-Webjs-Seed', 'html-cache');
|
|
67
|
+
return cached;
|
|
68
|
+
}
|
|
61
69
|
}
|
|
62
70
|
} catch {
|
|
63
71
|
// A load / store failure falls through to a normal fresh render: the
|
|
@@ -288,9 +296,33 @@ export async function ssrPage(route, params, url, opts) {
|
|
|
288
296
|
// block (those slow regions keep the stale-while-revalidate refetch). An
|
|
289
297
|
// empty collector yields '' so the output stays byte-identical.
|
|
290
298
|
let outBody = streamBody;
|
|
291
|
-
|
|
292
|
-
|
|
299
|
+
// Dev-only seeding diagnostics (#1309). `off` (seeding disabled) is kept
|
|
300
|
+
// DISTINCT from `collected=0` on purpose: the counting lives with the
|
|
301
|
+
// collector rather than behind the seed gate, so a seeding-DISABLED app
|
|
302
|
+
// never looks like a seeding-BROKEN one.
|
|
303
|
+
let seedHeader = 'off';
|
|
304
|
+
const streamed = suspenseCtx.pending.length > 0;
|
|
305
|
+
if (seedCollector && streamed) {
|
|
306
|
+
// A streamed render's deferred boundaries resolve AFTER the first flush,
|
|
307
|
+
// so their results cannot ride this block and none is emitted in prod. In
|
|
308
|
+
// DEV emit the marker alone, so the client reports the CAUSE instead of
|
|
309
|
+
// leaving the developer to guess why every action call went to the network.
|
|
310
|
+
seedHeader = `collected=${seedCollector.size}, emitted=0, streamed`;
|
|
311
|
+
if (opts.dev) {
|
|
312
|
+
const marker = await buildSeedScript(null, { dev: true, reason: 'streamed' });
|
|
313
|
+
if (marker) outBody = streamBody + marker;
|
|
314
|
+
}
|
|
315
|
+
} else if (seedCollector) {
|
|
316
|
+
const seedScript = await buildSeedScript(seedCollector, { dev: opts.dev });
|
|
293
317
|
if (seedScript) outBody = streamBody + seedScript;
|
|
318
|
+
// `emitted` differs from `collected` exactly when the serializer threw and
|
|
319
|
+
// dropped the whole block, which is otherwise a completely invisible
|
|
320
|
+
// failure. In DEV that throw still emits a marker-only block, so the
|
|
321
|
+
// browser can name the cause; it carries no seeds, so it must NOT count as
|
|
322
|
+
// emitted or the header would report a total success on the very failure
|
|
323
|
+
// it exists to expose.
|
|
324
|
+
const dropped = seedScript === SEED_DROP_BLOCK;
|
|
325
|
+
seedHeader = `collected=${seedCollector.size}, emitted=${seedScript && !dropped ? seedCollector.size : 0}`;
|
|
294
326
|
}
|
|
295
327
|
const res = streamingHtmlResponse(
|
|
296
328
|
prefix,
|
|
@@ -307,6 +339,11 @@ export async function ssrPage(route, params, url, opts) {
|
|
|
307
339
|
nonce,
|
|
308
340
|
opts.dev,
|
|
309
341
|
);
|
|
342
|
+
// Dev-only seeding diagnostics (#1309). A miss is indistinguishable from a
|
|
343
|
+
// hit from the outside, so an app whose seeding silently broke looks exactly
|
|
344
|
+
// like one where it works. Dev only: a production header would publish how
|
|
345
|
+
// many server calls a page made, for no benefit.
|
|
346
|
+
if (opts.dev) res.headers.set('X-Webjs-Seed', seedHeader);
|
|
310
347
|
// REDUCED response (#1009): the X-Webjs-Have short-circuit omitted the
|
|
311
348
|
// outer-layout chrome, so these bytes are only valid for a request that
|
|
312
349
|
// sent a matching `have`. Left shared-cacheable, a CDN edge could store the
|
|
@@ -2072,7 +2109,7 @@ function reachedVendorSpecifiers(graph, entryFiles, componentUrls, appDir, elida
|
|
|
2072
2109
|
* @param {string} prefix
|
|
2073
2110
|
* @param {string} bodyHtml
|
|
2074
2111
|
* @param {string} closer
|
|
2075
|
-
* @param {{ pending: {id: string, promise: Promise<unknown
|
|
2112
|
+
* @param {{ pending: {id: string, promise: Promise<unknown>}[], nextId: number }} ctx
|
|
2076
2113
|
* @param {number} status
|
|
2077
2114
|
* @param {Request | undefined} req
|
|
2078
2115
|
* @param {URL | undefined} url
|
|
@@ -2128,15 +2165,14 @@ function streamingHtmlResponse(prefix, bodyHtml, closer, ctx, status, req, url,
|
|
|
2128
2165
|
try {
|
|
2129
2166
|
const resolved = await p.promise;
|
|
2130
2167
|
const sub = { pending: [], nextId: ctx.nextId, dev: ctx.dev };
|
|
2131
|
-
//
|
|
2132
|
-
//
|
|
2133
|
-
// a `<button formaction=${fn}>` inside a bound
|
|
2134
|
-
//
|
|
2135
|
-
//
|
|
2136
|
-
//
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
});
|
|
2168
|
+
// A fresh scan that cannot see the shell the boundary sits in.
|
|
2169
|
+
// That used to require carrying the boundary's form scope
|
|
2170
|
+
// (#1207), or a `<button formaction=${fn}>` inside a bound
|
|
2171
|
+
// form's boundary read as form-less, was refused, and the catch
|
|
2172
|
+
// below turned it into an empty boundary on a 200 in
|
|
2173
|
+
// production. #1307 made a bound submitter self-sufficient, so
|
|
2174
|
+
// there is nothing left to carry.
|
|
2175
|
+
const html = await renderToString(resolved, { ssr: true, suspenseCtx: sub });
|
|
2140
2176
|
ctx.nextId = sub.nextId;
|
|
2141
2177
|
for (const n of sub.pending) ctx.pending.push(n);
|
|
2142
2178
|
return { id: p.id, html };
|