@webjsdev/server 0.8.20 → 0.8.22
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 +111 -5
- package/index.js +5 -0
- package/package.json +1 -1
- package/src/auth.js +7 -0
- package/src/check.js +260 -68
- package/src/component-elision.js +61 -0
- package/src/module-graph.js +16 -7
- package/src/page-action.js +5 -3
- package/src/ssr.js +14 -2
- package/src/vendor.js +839 -96
package/src/component-elision.js
CHANGED
|
@@ -111,6 +111,9 @@ export const CLIENT_LIFECYCLE_HOOKS = [
|
|
|
111
111
|
'firstUpdated',
|
|
112
112
|
'getUpdateComplete',
|
|
113
113
|
'renderError',
|
|
114
|
+
// The async-render re-fetch loading UI (#469): defining it means the
|
|
115
|
+
// component suspends and re-renders on the client, so it must ship.
|
|
116
|
+
'renderFallback',
|
|
114
117
|
];
|
|
115
118
|
|
|
116
119
|
/**
|
|
@@ -122,6 +125,24 @@ export const CLIENT_LIFECYCLE_HOOKS = [
|
|
|
122
125
|
*/
|
|
123
126
|
export const CLIENT_METHOD_CALLS = ['addController', 'removeController', 'requestUpdate'];
|
|
124
127
|
|
|
128
|
+
/**
|
|
129
|
+
* A bare `async render()` is NOT a standalone ship signal (#474). Its SSR
|
|
130
|
+
* pass bakes the resolved data into the first paint, so a light-DOM async
|
|
131
|
+
* component with no OTHER client signal renders identical HTML with or
|
|
132
|
+
* without its JS and is elidable like any display-only component, saving a
|
|
133
|
+
* module download plus a redundant on-hydration re-fetch. It ships only when
|
|
134
|
+
* it ALSO carries an independent signal: an `@event`, a non-`state` reactive
|
|
135
|
+
* prop, a reactive import, a lifecycle hook (`renderFallback()` included, via
|
|
136
|
+
* CLIENT_LIFECYCLE_HOOKS), a `<slot>`, `static shadow = true`, `static refresh
|
|
137
|
+
* = true`, cross-module observation, or a transitively-reachable interactive
|
|
138
|
+
* dep / child (the fixpoint's import + render rules). The two genuinely new
|
|
139
|
+
* carve-outs, both handled per-class below, are `static shadow` (Declarative
|
|
140
|
+
* Shadow DOM only attaches during HTML parsing, so a streamed / soft-navigated
|
|
141
|
+
* shadow component needs its module to re-run `attachShadow`) and the explicit
|
|
142
|
+
* `static refresh = true` opt-in (keeps the stale-while-revalidate on-load
|
|
143
|
+
* re-fetch that eliding would drop).
|
|
144
|
+
*/
|
|
145
|
+
|
|
125
146
|
/** Match a `@event=${...}` binding inside a template (unquoted per invariant 4). */
|
|
126
147
|
const EVENT_BINDING_RE = /@[A-Za-z][\w-]*\s*=\s*\$\{/;
|
|
127
148
|
|
|
@@ -396,6 +417,23 @@ export function analyzeComponentSource(src) {
|
|
|
396
417
|
}
|
|
397
418
|
|
|
398
419
|
for (const body of bodies) {
|
|
420
|
+
// Shadow DOM always ships (#474). Declarative Shadow DOM attaches ONLY
|
|
421
|
+
// during HTML parsing; an elided component that arrives via a client DOM
|
|
422
|
+
// insertion (a soft-nav swap, a streamed <webjs-suspense> boundary's
|
|
423
|
+
// replaceWith) would never re-attach its shadow root, because eliding
|
|
424
|
+
// drops the module that runs `attachShadow`. The analyser is context-free
|
|
425
|
+
// (it cannot tell whether a given component will be streamed or
|
|
426
|
+
// navigated to), so any shadow component is shipped.
|
|
427
|
+
if (declaresStaticTrue(body, 'shadow')) {
|
|
428
|
+
return { interactive: true, reason: 'declares static shadow (DSD must re-attach on a client swap)' };
|
|
429
|
+
}
|
|
430
|
+
// `static refresh = true` is the explicit opt-in to ship a bare async
|
|
431
|
+
// component so its stale-while-revalidate refresh runs ~after SSR; eliding
|
|
432
|
+
// a bare async component drops that on-load re-fetch (moot for request-
|
|
433
|
+
// stable data, the default), so an author who wants fresh-on-load opts in.
|
|
434
|
+
if (declaresStaticTrue(body, 'refresh')) {
|
|
435
|
+
return { interactive: true, reason: 'declares static refresh = true (keeps the on-load re-fetch)' };
|
|
436
|
+
}
|
|
399
437
|
for (const hook of CLIENT_LIFECYCLE_HOOKS) {
|
|
400
438
|
// A client lifecycle hook as a method (`hook(`) OR as an arrow class
|
|
401
439
|
// field (`hook = () =>`), which shadows the prototype method and still
|
|
@@ -420,6 +458,29 @@ export function analyzeComponentSource(src) {
|
|
|
420
458
|
return { interactive: false, reason: null };
|
|
421
459
|
}
|
|
422
460
|
|
|
461
|
+
/**
|
|
462
|
+
* True if a class body declares a `static <name>` whose value is not the
|
|
463
|
+
* literal `false`. Backs the `static shadow` (DSD-on-client-swap) and
|
|
464
|
+
* `static refresh = true` (ship-the-async-refetch opt-in) ship signals (#474).
|
|
465
|
+
*
|
|
466
|
+
* Conservative on anything it cannot evaluate: a getter form
|
|
467
|
+
* (`static get <name>()`) ships, and a non-`false` value of any shape
|
|
468
|
+
* (`true`, a variable, an expression) ships. Only an absent declaration or a
|
|
469
|
+
* literal `= false` is cleared as inert, matching the light-DOM /
|
|
470
|
+
* no-refresh defaults.
|
|
471
|
+
*
|
|
472
|
+
* @param {string} classBody redacted class body
|
|
473
|
+
* @param {string} name the static field name (`shadow` / `refresh`)
|
|
474
|
+
* @returns {boolean}
|
|
475
|
+
*/
|
|
476
|
+
function declaresStaticTrue(classBody, name) {
|
|
477
|
+
// A getter cannot be evaluated statically; ship.
|
|
478
|
+
if (new RegExp(`\\bstatic\\s+get\\s+${name}\\b`).test(classBody)) return true;
|
|
479
|
+
const m = new RegExp(`\\bstatic\\s+${name}\\b\\s*=\\s*([^;\\n]*)`).exec(classBody);
|
|
480
|
+
if (!m) return false; // not declared: the default (light DOM / no refresh)
|
|
481
|
+
return !/^false\b/.test(m[1].trim()); // `= false` is inert; anything else ships
|
|
482
|
+
}
|
|
483
|
+
|
|
423
484
|
/**
|
|
424
485
|
* @param {string} src
|
|
425
486
|
* @returns {string | null} the offending imported name, or null
|
package/src/module-graph.js
CHANGED
|
@@ -262,13 +262,22 @@ async function parseFile(file, appDir, graph, seen) {
|
|
|
262
262
|
// Mask of `src` with all string / template-literal / comment / regex
|
|
263
263
|
// CONTENT blanked to spaces (positions preserved). Used to reject an
|
|
264
264
|
// `import '…'` / `export … from '…'` that appears as TEXT inside a
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
// specifier
|
|
269
|
-
// the mask
|
|
270
|
-
// redaction, i.e. sits in code
|
|
271
|
-
|
|
265
|
+
// literal (e.g. example code shown in a `<pre>` inside an `html\`\``
|
|
266
|
+
// template, as the docs site does, OR a code-example written as a plain
|
|
267
|
+
// quoted string) rather than as a real statement. We still read the
|
|
268
|
+
// specifier from the RAW `src` (the specifier is itself a string,
|
|
269
|
+
// blanked in the mask), and only consult the mask to confirm the
|
|
270
|
+
// `import` / `export` KEYWORD survived redaction, i.e. sits in code
|
|
271
|
+
// position and not inside a literal.
|
|
272
|
+
//
|
|
273
|
+
// `blankStrings: true` is load-bearing: the default mask keeps PLAIN
|
|
274
|
+
// string + verbatim-template bodies verbatim (so `register('tag')` stays
|
|
275
|
+
// readable for other scanners), which would leave an `import` written
|
|
276
|
+
// inside a plain string looking like a real keyword and create a phantom
|
|
277
|
+
// graph edge to whatever path that string names. Since this caller only
|
|
278
|
+
// checks keyword-in-code-position, blank every literal body so a
|
|
279
|
+
// code-example `import` string never becomes an edge.
|
|
280
|
+
const masked = redactStringsAndTemplates(src, true);
|
|
272
281
|
const deps = new Set();
|
|
273
282
|
for (const re of [IMPORT_RE, EXPORT_FROM_RE]) {
|
|
274
283
|
for (const m of src.matchAll(re)) {
|
package/src/page-action.js
CHANGED
|
@@ -196,9 +196,11 @@ export async function runPageAction(route, params, url, loaded, req, ssrOpts) {
|
|
|
196
196
|
} catch (err) {
|
|
197
197
|
if (isRedirect(err)) {
|
|
198
198
|
const e = /** @type any */ (err);
|
|
199
|
-
// A thrown redirect from an action
|
|
200
|
-
//
|
|
201
|
-
//
|
|
199
|
+
// A thrown redirect from an action (a POST) defaults to 307 Temporary
|
|
200
|
+
// Redirect, which is method-preserving so the action's intent survives
|
|
201
|
+
// the bounce; an explicit `redirect(url, status)` overrides it. This is
|
|
202
|
+
// deliberately NOT the GET gate's 302 default (see ssr.js). PRG (303) is
|
|
203
|
+
// the SUCCESS-result path below.
|
|
202
204
|
return new Response(null, { status: e.status || 307, headers: { location: e.url } });
|
|
203
205
|
}
|
|
204
206
|
if (isNotFound(err)) {
|
package/src/ssr.js
CHANGED
|
@@ -198,7 +198,12 @@ export async function ssrPage(route, params, url, opts) {
|
|
|
198
198
|
} catch (err) {
|
|
199
199
|
if (isRedirect(err)) {
|
|
200
200
|
const e = /** @type any */ (err);
|
|
201
|
-
|
|
201
|
+
// A redirect thrown during a GET page/layout render is a GET-to-GET
|
|
202
|
+
// navigation (an auth bounce, a gate). 302 Found is the conventional
|
|
203
|
+
// code there, so it is the default when the caller did not pick one. An
|
|
204
|
+
// explicit `redirect(url, status)` overrides it. (Action redirects, a
|
|
205
|
+
// POST, default to 307 in page-action.js so the method is preserved.)
|
|
206
|
+
return new Response(null, { status: e.status || 302, headers: { location: e.url } });
|
|
202
207
|
}
|
|
203
208
|
if (isNotFound(err)) {
|
|
204
209
|
const html = await ssrNotFoundHtml(null, opts);
|
|
@@ -1476,7 +1481,14 @@ function streamingHtmlResponse(prefix, bodyHtml, closer, ctx, status, req, url,
|
|
|
1476
1481
|
|
|
1477
1482
|
const stream = new ReadableStream({
|
|
1478
1483
|
async start(controller) {
|
|
1479
|
-
|
|
1484
|
+
// Flush the shell (prefix + body with fallbacks) immediately, followed by
|
|
1485
|
+
// a shell-ready sentinel comment IN THE SAME chunk. The resolved boundary
|
|
1486
|
+
// templates and the `</body></html>` closer are emitted LATER (after the
|
|
1487
|
+
// slow data settles), so without this sentinel a streaming soft-nav client
|
|
1488
|
+
// could not tell "shell complete, awaiting the slow boundary" from "shell
|
|
1489
|
+
// still arriving" and would block its progressive swap until the slow
|
|
1490
|
+
// boundary (#473). The comment is inert for the native initial-load parse.
|
|
1491
|
+
controller.enqueue(encoder.encode(prefix + bodyHtml + '<!--wj-stream-shell-->'));
|
|
1480
1492
|
try {
|
|
1481
1493
|
// Loop: resolve all currently-pending promises in parallel; nested
|
|
1482
1494
|
// Suspense inside resolved content adds more pending entries.
|