@webjsdev/server 0.8.91 → 0.10.0
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/README.md +4 -1
- package/index.d.ts +461 -102
- package/index.js +9 -4
- package/package.json +3 -3
- package/src/action-flash.js +185 -0
- package/src/action-input.js +208 -0
- package/src/action-params.js +156 -0
- package/src/action-route.js +8 -9
- package/src/action-seed.js +44 -5
- package/src/actions.js +112 -15
- package/src/api.js +67 -10
- package/src/app-layout.js +133 -0
- package/src/asset-hash.js +82 -49
- package/src/auth.js +612 -108
- package/src/browser-entries.js +2 -17
- package/src/build-epoch.js +1 -1
- package/src/cache-tags.js +5 -1
- package/src/check/helpers.js +85 -0
- package/src/check/rules-actions.js +37 -1
- package/src/check/rules-components.js +133 -28
- package/src/check/rules-imports.js +175 -4
- package/src/check/rules-route-modules.js +200 -0
- package/src/check/rules-routes.js +55 -0
- package/src/check/rules-routing.js +143 -95
- package/src/check/rules-typescript.js +24 -2
- package/src/check/rules.js +109 -9
- package/src/check/runner-support.js +53 -171
- package/src/check/runner.js +24 -6
- package/src/client-signals.js +171 -0
- package/src/component-scanner.js +22 -16
- package/src/context.js +152 -16
- package/src/cookie-store.js +262 -0
- package/src/core-exports.js +190 -0
- package/src/dev/bun-app-source.js +95 -5
- package/src/dev/config.js +7 -51
- package/src/dev/dev-service-worker.js +48 -0
- package/src/dev/dev-status.js +122 -0
- package/src/dev/esm-hot-hooks.js +373 -0
- package/src/dev/handler.js +632 -155
- package/src/dev/helpers.js +127 -96
- package/src/dev/hot-host.js +33 -6
- package/src/dev/poke-state.js +110 -0
- package/src/dev/serve.js +191 -133
- package/src/dev/server.js +109 -37
- package/src/dev/specifier-versions.js +125 -0
- package/src/dev/watch-ignore.js +49 -4
- package/src/dev/watch-poll.js +78 -0
- package/src/dev/watch-tree.js +59 -22
- package/src/dev-classify.js +59 -26
- package/src/dev-error.js +104 -3
- package/src/dev-hot-classify.js +291 -0
- package/src/dev-hot-client.js +245 -0
- package/src/dev-import.js +22 -5
- package/src/dev-overlay.js +26 -0
- package/src/dev-ready-probe.js +69 -0
- package/src/dev-regenerate.js +322 -78
- package/src/dev-reload-hold.js +49 -20
- package/src/dev-reload-reason.js +61 -0
- package/src/dev-reload-worker.js +62 -13
- package/src/dev-secret.js +73 -0
- package/src/dev-source-locations-hook.js +4 -1
- package/src/dev-source-locations.js +33 -2
- package/src/dev-styles.js +64 -6
- package/src/dev.js +0 -1
- package/src/env-files.js +124 -0
- package/src/file-storage.js +2 -2
- package/src/foreign-imports.js +228 -0
- package/src/form-action-identity.js +52 -25
- package/src/form-dispatch.js +150 -69
- package/src/html-cache.js +35 -4
- package/src/importmap.js +2 -2
- package/src/instrumentation.js +59 -3
- package/src/js-scan.js +5 -4
- package/src/jwe.js +155 -0
- package/src/lazy-components.js +166 -0
- package/src/listener-core.js +4 -2
- package/src/lit-hints.js +172 -0
- package/src/middleware-load.js +248 -0
- package/src/module-graph.js +73 -47
- package/src/module-side-effect.js +284 -0
- package/src/public-files.js +73 -0
- package/src/redirect-location.js +19 -0
- package/src/redirects.js +9 -1
- package/src/route-frontier.js +151 -0
- package/src/route-report.js +130 -0
- package/src/router.js +120 -13
- package/src/session.js +32 -10
- package/src/ssr/client-router-flag.js +1 -1
- package/src/ssr/document.js +49 -17
- package/src/ssr/head.js +228 -25
- package/src/ssr/preloads.js +31 -45
- package/src/ssr/render.js +243 -55
- package/src/ssr/responses.js +51 -4
- package/src/ssr/shipped-modules.js +52 -16
- package/src/testing.d.ts +31 -2
- package/src/testing.js +85 -17
- package/src/thenable-params.js +26 -0
- package/src/vendor/jspm.js +12 -1
- package/src/vendor/pins.js +9 -8
- package/src/vendor/scanner.js +14 -9
- package/src/vendor.js +5 -3
- package/webjs-config.schema.json +3 -8
- package/src/component-elision.js +0 -1852
- package/src/elision-differential.js +0 -101
- package/src/elision-report.js +0 -193
package/src/ssr/head.js
CHANGED
|
@@ -305,7 +305,7 @@ export function collectHoistedHeadTags(bodyHtml) {
|
|
|
305
305
|
* (breaks the ES-module waterfall without a bundler) and any user-declared
|
|
306
306
|
* `metadata.preload` entries.
|
|
307
307
|
*
|
|
308
|
-
* @param {{ metadata: Record<string,any>, moduleUrls: string[], dev: boolean, streaming: boolean, preloads?: string[], lazyComponents?: Record<string, string>, nonce?: string }} opts
|
|
308
|
+
* @param {{ metadata: Record<string,any>, moduleUrls: string[], dev: boolean, streaming: boolean, preloads?: string[], lazyComponents?: Record<string, string>, nonce?: string, pageUrl?: string }} opts
|
|
309
309
|
*/
|
|
310
310
|
/**
|
|
311
311
|
* Extract leading `<script>`, `<style>`, and `<link>` tags from the body
|
|
@@ -323,13 +323,161 @@ export function hoistHeadTags(headHtml, bodyHtml) {
|
|
|
323
323
|
// streaming path so both hoist identically. See collectHoistedHeadTags.
|
|
324
324
|
const { tags: hoisted, body: remaining } = collectHoistedHeadTags(bodyHtml);
|
|
325
325
|
if (!hoisted.length) return { head: headHtml, body: bodyHtml };
|
|
326
|
-
const newHead = headHtml.replace('</head>', hoisted.join('\n') + '\n</head>');
|
|
326
|
+
const newHead = headHtml.replace('</head>', () => hoisted.join('\n') + '\n</head>');
|
|
327
327
|
return { head: newHead, body: remaining };
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
/** Internal helper re-exported for unit testing. */
|
|
331
331
|
export const _hoistHeadTags = hoistHeadTags;
|
|
332
332
|
|
|
333
|
+
/**
|
|
334
|
+
* The attribute naming the boundary a hoisted stylesheet came from (#1669).
|
|
335
|
+
* Its value is the SEGMENT of the `wj:children` range the link was rendered
|
|
336
|
+
* in: a nested layout's own markup sits inside its parent's range, and a
|
|
337
|
+
* page's inside its own boundary or its innermost layout's. The client router
|
|
338
|
+
* switches the sheet off when it swaps that range and the incoming page no
|
|
339
|
+
* longer links the same file, so a section's stylesheet leaves with the
|
|
340
|
+
* section, and switches it back on when a later page links it again.
|
|
341
|
+
*/
|
|
342
|
+
export const STYLE_OWNER_ATTR = 'data-wj-style-owner';
|
|
343
|
+
|
|
344
|
+
const BOUNDARY_OPEN = '<!--wj:children:';
|
|
345
|
+
// One token of a leading run after a boundary marker: a comment that is not a
|
|
346
|
+
// client-router marker, or a `<link>` tag (quote-aware, like the root hoist).
|
|
347
|
+
// Sticky, so it matches at `lastIndex` without copying the rest of the body.
|
|
348
|
+
const OWNED_RUN_TOKEN_RE =
|
|
349
|
+
/\s*(<!--(?!\/?wj:)[\s\S]*?-->|<link\b(?:[^>"']|"[^"]*"|'[^']*')*>)/iy;
|
|
350
|
+
const LINK_TAG_RE = /<link\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi;
|
|
351
|
+
|
|
352
|
+
// One attribute after the tag name, read in order so a quoted value is
|
|
353
|
+
// consumed whole and never mistaken for another attribute.
|
|
354
|
+
const ATTR_RE = /\s*([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/y;
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* A tag's attributes, lowercased names to raw values (the first of a repeated
|
|
358
|
+
* name wins, as in HTML).
|
|
359
|
+
* @param {string} tag
|
|
360
|
+
* @returns {Map<string, string>}
|
|
361
|
+
*/
|
|
362
|
+
function tagAttrs(tag) {
|
|
363
|
+
/** @type {Map<string, string>} */
|
|
364
|
+
const out = new Map();
|
|
365
|
+
const start = /^<[a-zA-Z][^\s/>]*/.exec(tag);
|
|
366
|
+
let pos = start ? start[0].length : 0;
|
|
367
|
+
for (;;) {
|
|
368
|
+
ATTR_RE.lastIndex = pos;
|
|
369
|
+
const m = ATTR_RE.exec(tag);
|
|
370
|
+
if (!m || m[0].length === 0) break;
|
|
371
|
+
pos += m[0].length;
|
|
372
|
+
const name = m[1].toLowerCase();
|
|
373
|
+
if (!out.has(name)) out.set(name, m[2] ?? m[3] ?? m[4] ?? '');
|
|
374
|
+
}
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The identity of a stylesheet `<link>`, or null when the tag is not an
|
|
380
|
+
* active one (`rel` naming `stylesheet` and not `alternate`, with an href).
|
|
381
|
+
* The URL the href resolves to against the page's own URL (`base`), plus the
|
|
382
|
+
* `media` it applies under, so `./a.css` and `/a.css` on one page are one
|
|
383
|
+
* sheet and a print sheet and a screen sheet of one file are two. The router
|
|
384
|
+
* keys its sheets the same way (owned-styles.js). With no base the href is
|
|
385
|
+
* used as written.
|
|
386
|
+
* @param {string} tag
|
|
387
|
+
* @param {string} [base]
|
|
388
|
+
* @returns {string | null}
|
|
389
|
+
*/
|
|
390
|
+
function stylesheetTagKey(tag, base) {
|
|
391
|
+
const attrs = tagAttrs(tag);
|
|
392
|
+
const rel = (attrs.get('rel') || '').toLowerCase().split(/\s+/);
|
|
393
|
+
let href = attrs.get('href');
|
|
394
|
+
if (!rel.includes('stylesheet') || rel.includes('alternate') || !href) return null;
|
|
395
|
+
if (base) { try { href = new URL(href, base).href; } catch { /* keep as written */ } }
|
|
396
|
+
const media = (attrs.get('media') || '').trim();
|
|
397
|
+
return media && media !== 'all' ? `${href} ${media}` : href;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* The keys (`stylesheetTagKey`) of every stylesheet `<link>` in an HTML string.
|
|
402
|
+
* @param {string} html
|
|
403
|
+
* @param {string} [base] The page's URL, which hrefs resolve against.
|
|
404
|
+
* @returns {Set<string>}
|
|
405
|
+
*/
|
|
406
|
+
export function stylesheetKeys(html, base) {
|
|
407
|
+
const out = new Set();
|
|
408
|
+
for (const m of html.matchAll(LINK_TAG_RE)) {
|
|
409
|
+
const key = stylesheetTagKey(m[0], base);
|
|
410
|
+
if (key) out.add(key);
|
|
411
|
+
}
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Hoist the stylesheet links at the start of each nested layout's and page's
|
|
417
|
+
* output into `<head>` (#1669). The root hoist (`collectHoistedHeadTags`) only
|
|
418
|
+
* reads the leading run of the WHOLE body and stops at the first boundary
|
|
419
|
+
* marker, so a section layout's `<link rel="stylesheet">` stayed in `<body>`,
|
|
420
|
+
* where it is not reliably render-blocking (#406). This reads the leading run
|
|
421
|
+
* after EVERY `<!--wj:children:...-->` open marker instead.
|
|
422
|
+
*
|
|
423
|
+
* Only `rel="stylesheet"` links move, each tagged with `STYLE_OWNER_ATTR` set
|
|
424
|
+
* to the marker's segment. Comments and other `<link>` tags in the run stay
|
|
425
|
+
* where they are and do not end it; anything else does. A stylesheet whose
|
|
426
|
+
* href and media are already in `seen` (the head so far, or an earlier
|
|
427
|
+
* hoist) is dropped rather than hoisted twice, since one URL linked twice is
|
|
428
|
+
* two active sheets the browser matches every rule against. Every framework
|
|
429
|
+
* the WebJs router takes cues from puts a route's sheet in the head and
|
|
430
|
+
* dedupes by href. Attributes are read in order (`tagAttrs`), so a name
|
|
431
|
+
* inside another attribute's quoted value is never mistaken for one.
|
|
432
|
+
*
|
|
433
|
+
* @param {string} bodyHtml
|
|
434
|
+
* @param {Set<string>} [seen] Keys (`stylesheetKeys`) already linked in the head (mutated).
|
|
435
|
+
* @param {string} [base] The page's URL, which hrefs resolve against for the key.
|
|
436
|
+
* @returns {{ tags: string[], body: string }}
|
|
437
|
+
*/
|
|
438
|
+
export function hoistOwnedStylesheets(bodyHtml, seen = new Set(), base) {
|
|
439
|
+
if (!bodyHtml.includes(BOUNDARY_OPEN)) return { tags: [], body: bodyHtml };
|
|
440
|
+
/** @type {string[]} */
|
|
441
|
+
const tags = [];
|
|
442
|
+
let out = '';
|
|
443
|
+
let pos = 0;
|
|
444
|
+
for (;;) {
|
|
445
|
+
const open = bodyHtml.indexOf(BOUNDARY_OPEN, pos);
|
|
446
|
+
if (open < 0) break;
|
|
447
|
+
const close = bodyHtml.indexOf('-->', open);
|
|
448
|
+
if (close < 0) break;
|
|
449
|
+
const markerEnd = close + 3;
|
|
450
|
+
out += bodyHtml.slice(pos, markerEnd);
|
|
451
|
+
// The route-key is everything after the LAST ':' (the router's own split,
|
|
452
|
+
// boundaries.js), so the segment is everything before it.
|
|
453
|
+
const rest = bodyHtml.slice(open + BOUNDARY_OPEN.length, close);
|
|
454
|
+
const cut = rest.lastIndexOf(':');
|
|
455
|
+
const segment = cut > 0 ? rest.slice(0, cut) : null;
|
|
456
|
+
pos = markerEnd;
|
|
457
|
+
if (!segment) continue;
|
|
458
|
+
let m;
|
|
459
|
+
for (;;) {
|
|
460
|
+
OWNED_RUN_TOKEN_RE.lastIndex = pos;
|
|
461
|
+
m = OWNED_RUN_TOKEN_RE.exec(bodyHtml);
|
|
462
|
+
if (!m) break;
|
|
463
|
+
const token = m[1];
|
|
464
|
+
const lead = m[0].slice(0, m[0].length - token.length);
|
|
465
|
+
pos += m[0].length;
|
|
466
|
+
const key = token.startsWith('<!--') ? null : stylesheetTagKey(token, base);
|
|
467
|
+
if (!key) {
|
|
468
|
+
out += lead + token;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
out += lead;
|
|
472
|
+
if (seen.has(key)) continue;
|
|
473
|
+
seen.add(key);
|
|
474
|
+
const owner = ` ${STYLE_OWNER_ATTR}="${escapeAttr(segment)}">`;
|
|
475
|
+
tags.push(token.replace(/\s*\/?>$/, () => owner));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return { tags, body: out + bodyHtml.slice(pos) };
|
|
479
|
+
}
|
|
480
|
+
|
|
333
481
|
/**
|
|
334
482
|
* The initial-load Suspense resolver: swaps each streamed
|
|
335
483
|
* `<template data-webjs-resolve>` into its placeholder.
|
|
@@ -491,28 +639,22 @@ export function wrapHead(opts) {
|
|
|
491
639
|
}
|
|
492
640
|
metaTags.push(`<meta name="viewport" content="${escapeAttr(viewportStr || 'width=device-width,initial-scale=1')}">`);
|
|
493
641
|
|
|
494
|
-
|
|
642
|
+
// themeColor: a string, a `{ color, media? }` descriptor, or an array of
|
|
643
|
+
// them (Next's viewport shape): one <meta> per descriptor.
|
|
644
|
+
for (const tc of themeColorList(m.themeColor)) {
|
|
645
|
+
const media = tc.media ? ` media="${escapeAttr(tc.media)}"` : '';
|
|
646
|
+
metaTags.push(`<meta name="theme-color" content="${escapeAttr(tc.color)}"${media}>`);
|
|
647
|
+
}
|
|
495
648
|
if (m.colorScheme) metaTags.push(`<meta name="color-scheme" content="${escapeAttr(m.colorScheme)}">`);
|
|
496
649
|
|
|
497
|
-
// robots:
|
|
650
|
+
// robots: a string or Next's object (index, follow, the directive keys, and
|
|
651
|
+
// a nested `googleBot` resolved the same way into <meta name="googlebot">).
|
|
498
652
|
if (m.robots) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
if (
|
|
504
|
-
else if (m.robots.index === true) parts.push('index');
|
|
505
|
-
if (m.robots.follow === false) parts.push('nofollow');
|
|
506
|
-
else if (m.robots.follow === true) parts.push('follow');
|
|
507
|
-
if (m.robots.noarchive) parts.push('noarchive');
|
|
508
|
-
if (m.robots.nosnippet) parts.push('nosnippet');
|
|
509
|
-
if (m.robots.noimageindex) parts.push('noimageindex');
|
|
510
|
-
if (parts.length) {
|
|
511
|
-
metaTags.push(`<meta name="robots" content="${escapeAttr(parts.join(', '))}">`);
|
|
512
|
-
}
|
|
513
|
-
if (typeof m.robots.googleBot === 'string') {
|
|
514
|
-
metaTags.push(`<meta name="googlebot" content="${escapeAttr(m.robots.googleBot)}">`);
|
|
515
|
-
}
|
|
653
|
+
const basic = resolveRobotsValue(m.robots);
|
|
654
|
+
if (basic) metaTags.push(`<meta name="robots" content="${escapeAttr(basic)}">`);
|
|
655
|
+
if (typeof m.robots === 'object') {
|
|
656
|
+
const gb = resolveRobotsValue(m.robots.googleBot);
|
|
657
|
+
if (gb) metaTags.push(`<meta name="googlebot" content="${escapeAttr(gb)}">`);
|
|
516
658
|
}
|
|
517
659
|
}
|
|
518
660
|
|
|
@@ -722,8 +864,8 @@ export function wrapHead(opts) {
|
|
|
722
864
|
// above. `vendorPreloadTargets` still excludes core deliberately; this hint
|
|
723
865
|
// is emitted from the head builder, not the vendor path.
|
|
724
866
|
//
|
|
725
|
-
// Gated on the boot actually shipping something. A
|
|
726
|
-
// boot module and must not be handed a preload for a runtime it never loads
|
|
867
|
+
// Gated on the boot actually shipping something. A page with no component
|
|
868
|
+
// ships no boot module and must not be handed a preload for a runtime it never loads
|
|
727
869
|
// (#780), which also keeps it off `global-error.{js,ts}`, whose document is
|
|
728
870
|
// returned verbatim with no importmap and no boot script.
|
|
729
871
|
if (opts.moduleUrls.length || lazyEntries) {
|
|
@@ -961,6 +1103,13 @@ export function wrapHead(opts) {
|
|
|
961
1103
|
// conditional line makes the CSP-off document one newline shorter than the
|
|
962
1104
|
// CSP-on one, and the framework guarantees the two render identically
|
|
963
1105
|
// apart from the nonce itself.
|
|
1106
|
+
//
|
|
1107
|
+
// The dev reload client goes BEFORE the boot script (#1689): module scripts
|
|
1108
|
+
// run in document order, and it installs the hot client (the template
|
|
1109
|
+
// interner core's `html()` reads) that has to exist before the first
|
|
1110
|
+
// component module evaluates. It sits on the boot's own line and the blank
|
|
1111
|
+
// line after the boot is where it used to be, so a production document,
|
|
1112
|
+
// which has no reload client, is unchanged.
|
|
964
1113
|
const title = m.title || 'webjs app';
|
|
965
1114
|
|
|
966
1115
|
return `<!doctype html>
|
|
@@ -974,10 +1123,64 @@ ${metaTags.join('\n')}
|
|
|
974
1123
|
${publicEnvShim({ dev: opts.dev, nonce: opts.nonce })}${embedScriptTag({ dev: opts.dev, nonce: opts.nonce })}${clientRouterEnabled() ? '' : `\n<script${n}>window.__WEBJS_CLIENT_ROUTER__=false;</script>`}
|
|
975
1124
|
${importMapTag({ nonce: opts.nonce })}
|
|
976
1125
|
${linkTags.join('\n')}
|
|
977
|
-
${scriptTags.length ? scriptTags.join('\n') + '\n' : ''}${boot}
|
|
978
|
-
|
|
1126
|
+
${reload}${scriptTags.length ? scriptTags.join('\n') + '\n' : ''}${boot}
|
|
1127
|
+
|
|
979
1128
|
${suspenseBoot}
|
|
980
1129
|
</head>
|
|
981
1130
|
<body>
|
|
982
1131
|
`;
|
|
983
1132
|
}
|
|
1133
|
+
|
|
1134
|
+
/** Next's robot directive keys, in its emit order. */
|
|
1135
|
+
const ROBOTS_KEYS = [
|
|
1136
|
+
'noarchive', 'nosnippet', 'noimageindex', 'nocache', 'notranslate', 'indexifembedded',
|
|
1137
|
+
'nositelinkssearchbox', 'unavailable_after', 'max-video-preview', 'max-image-preview', 'max-snippet',
|
|
1138
|
+
];
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* A port of Next.js's `resolveRobotsValue` (resolve-basics.ts): a string
|
|
1142
|
+
* passes through; an object becomes `index` / `noindex`, `follow` /
|
|
1143
|
+
* `nofollow`, then each set directive key (`key` for `true`,
|
|
1144
|
+
* `key:value` otherwise).
|
|
1145
|
+
* @param {unknown} robots
|
|
1146
|
+
* @returns {string | null}
|
|
1147
|
+
*/
|
|
1148
|
+
export function resolveRobotsValue(robots) {
|
|
1149
|
+
if (!robots) return null;
|
|
1150
|
+
if (typeof robots === 'string') return robots;
|
|
1151
|
+
if (typeof robots !== 'object') return null;
|
|
1152
|
+
const r = /** @type {Record<string, unknown>} */ (robots);
|
|
1153
|
+
const values = [];
|
|
1154
|
+
if (r.index) values.push('index');
|
|
1155
|
+
else if (typeof r.index === 'boolean') values.push('noindex');
|
|
1156
|
+
if (r.follow) values.push('follow');
|
|
1157
|
+
else if (typeof r.follow === 'boolean') values.push('nofollow');
|
|
1158
|
+
for (const key of ROBOTS_KEYS) {
|
|
1159
|
+
const value = r[key];
|
|
1160
|
+
if (typeof value !== 'undefined' && value !== false) {
|
|
1161
|
+
values.push(typeof value === 'boolean' ? key : `${key}:${value}`);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return values.length ? values.join(', ') : null;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Normalize Next's `themeColor` (a string, a descriptor, or an array of
|
|
1169
|
+
* descriptors) to a list of `{ color, media? }`.
|
|
1170
|
+
* @param {unknown} v
|
|
1171
|
+
* @returns {{ color: string, media?: string }[]}
|
|
1172
|
+
*/
|
|
1173
|
+
export function themeColorList(v) {
|
|
1174
|
+
if (!v) return [];
|
|
1175
|
+
if (typeof v === 'string') return [{ color: v }];
|
|
1176
|
+
const list = Array.isArray(v) ? v : [v];
|
|
1177
|
+
/** @type {{ color: string, media?: string }[]} */
|
|
1178
|
+
const out = [];
|
|
1179
|
+
for (const d of list) {
|
|
1180
|
+
if (typeof d === 'string') out.push({ color: d });
|
|
1181
|
+
else if (d && typeof d === 'object' && typeof d.color === 'string') {
|
|
1182
|
+
out.push(typeof d.media === 'string' ? { color: d.color, media: d.media } : { color: d.color });
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return out;
|
|
1186
|
+
}
|
package/src/ssr/preloads.js
CHANGED
|
@@ -31,21 +31,16 @@ export function toUrlPath(file, appDir) {
|
|
|
31
31
|
* are NOT preloaded: they're loaded by the IntersectionObserver-based
|
|
32
32
|
* lazy-loader when the element enters the viewport.
|
|
33
33
|
*
|
|
34
|
-
* Elidable (display-only) components are skipped entirely: their imports
|
|
35
|
-
* are stripped from the served source, so preloading their module would
|
|
36
|
-
* fetch JS the browser never executes.
|
|
37
|
-
*
|
|
38
34
|
* A tag counts as lazy when its class says so at runtime (`isLazy`) OR its
|
|
39
35
|
* file declares `static lazy = true` in source (`lazyFiles`, #1524), the same
|
|
40
36
|
* per-file verdict the served-source rewrite uses, so the two never disagree.
|
|
41
37
|
*
|
|
42
38
|
* @param {Set<string>} usedTags
|
|
43
39
|
* @param {string} appDir
|
|
44
|
-
* @param {Set<string>} [elidable] absolute paths of elidable component files
|
|
45
40
|
* @param {Map<string, string[]>} [lazyFiles] lazy component file -> its tags
|
|
46
41
|
* @returns {{ eager: string[], lazy: Record<string, string> }}
|
|
47
42
|
*/
|
|
48
|
-
export function componentPreloads(usedTags, appDir,
|
|
43
|
+
export function componentPreloads(usedTags, appDir, lazyFiles) {
|
|
49
44
|
const eager = [];
|
|
50
45
|
/** @type {Record<string, string>} */
|
|
51
46
|
const lazy = {};
|
|
@@ -55,7 +50,6 @@ export function componentPreloads(usedTags, appDir, elidable, lazyFiles) {
|
|
|
55
50
|
try {
|
|
56
51
|
const abs = fileURLToPath(fileUrl);
|
|
57
52
|
if (!abs.startsWith(appDir)) continue;
|
|
58
|
-
if (elidable && elidable.has(abs)) continue;
|
|
59
53
|
const url = toUrlPath(abs, appDir);
|
|
60
54
|
if (isLazy(tag) || (lazyFiles && lazyFiles.has(abs))) {
|
|
61
55
|
lazy[tag] = url;
|
|
@@ -90,13 +84,13 @@ export function componentPreloads(usedTags, appDir, elidable, lazyFiles) {
|
|
|
90
84
|
* @param {import('../module-graph.js').ModuleGraph | undefined} graph
|
|
91
85
|
* @param {string[]} entryFiles absolute paths of the SHIPPED page/layout modules (from `moduleUrls`)
|
|
92
86
|
* @param {string} appDir
|
|
93
|
-
* @param {Set<string>} [elidableComponents] absolute paths to skip in the walk
|
|
94
87
|
* @param {Set<string>} [serverFiles] the action / server-file index, never preloaded
|
|
88
|
+
* @param {Set<string>} [skip] absolute paths to skip in the walk (the lazy files)
|
|
95
89
|
* @param {string[]} [precomputedDeps] the transitive closure `pagePreloadHints` already walked for
|
|
96
90
|
* THESE roots and THIS skip set. Omitted, the walk runs here (a direct caller)
|
|
97
91
|
* @returns {string[]}
|
|
98
92
|
*/
|
|
99
|
-
export function deduplicatedPreloads(componentUrls, moduleUrls, graph, entryFiles, appDir, serverFiles,
|
|
93
|
+
export function deduplicatedPreloads(componentUrls, moduleUrls, graph, entryFiles, appDir, serverFiles, skip, precomputedDeps) {
|
|
100
94
|
const seen = new Set(moduleUrls);
|
|
101
95
|
const result = [];
|
|
102
96
|
|
|
@@ -133,10 +127,9 @@ export function deduplicatedPreloads(componentUrls, moduleUrls, graph, entryFile
|
|
|
133
127
|
const abs = resolve(appDir, url.startsWith('/') ? url.slice(1) : url);
|
|
134
128
|
allEntries.push(abs);
|
|
135
129
|
}
|
|
136
|
-
// Skip
|
|
137
|
-
// them:
|
|
138
|
-
|
|
139
|
-
deps = transitiveDeps(graph, allEntries, appDir, elidableComponents);
|
|
130
|
+
// Skip the lazy components and any subtree reachable only through
|
|
131
|
+
// them: the lazy loader fetches those on first sight, not up front.
|
|
132
|
+
deps = transitiveDeps(graph, allEntries, appDir, skip);
|
|
140
133
|
}
|
|
141
134
|
for (const dep of deps) {
|
|
142
135
|
if (byIndex(dep)) continue;
|
|
@@ -153,11 +146,11 @@ export function deduplicatedPreloads(componentUrls, moduleUrls, graph, entryFile
|
|
|
153
146
|
/**
|
|
154
147
|
* Collect the bare npm vendor specifiers the page's SHIPPED modules import
|
|
155
148
|
* (#754). The walk ROOTS are the boot's actually-shipped module set: the caller
|
|
156
|
-
* passes `entryFiles` = the absolute paths of `moduleUrls` (
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
149
|
+
* passes `entryFiles` = the absolute paths of `moduleUrls` (the components each
|
|
150
|
+
* route module renders, never a route module itself, #1686), plus
|
|
151
|
+
* `componentUrls` = the rendered components. From those roots it walks the
|
|
152
|
+
* transitive app-graph closure (the lazy components and the subtree reachable
|
|
153
|
+
* only through them excluded), and collects each reached file's bare
|
|
161
154
|
* imports, excluding server files. The specifiers are resolved to `modulepreload`
|
|
162
155
|
* targets via the vendor importmap; a specifier not in the map (unpinned /
|
|
163
156
|
* unreached) drops out there.
|
|
@@ -166,35 +159,34 @@ export function deduplicatedPreloads(componentUrls, moduleUrls, graph, entryFile
|
|
|
166
159
|
* dropped from the boot, whether a dropped page's SSR-only DIRECT vendor import
|
|
167
160
|
* or its SSR-only RELATIVE HELPER's vendor, is never collected: the dropped
|
|
168
161
|
* module is not a root, and nothing that ships imports it (pages/layouts are not
|
|
169
|
-
* importable). So the canonical SSR-only-dependency pattern
|
|
170
|
-
*
|
|
162
|
+
* importable). So the canonical SSR-only-dependency pattern is never preloaded
|
|
163
|
+
* (no over-fetch).
|
|
171
164
|
*
|
|
172
165
|
* @param {import('../module-graph.js').ModuleGraph | undefined} graph
|
|
173
166
|
* @param {string[]} entryFiles absolute paths of the SHIPPED page/layout modules (from `moduleUrls`)
|
|
174
167
|
* @param {string[]} componentUrls rendered eager component URL paths
|
|
175
168
|
* @param {string} appDir
|
|
176
|
-
* @param {Set<string>} [
|
|
169
|
+
* @param {Set<string>} [skip] absolute paths to skip in the walk (the lazy files)
|
|
177
170
|
* @param {Set<string>} [serverFiles] the action / server-file index (`'use server'`, incl. no-`.server.` files)
|
|
178
171
|
* @param {string[]} [precomputedDeps] the transitive closure `pagePreloadHints` already walked for
|
|
179
172
|
* THESE roots and THIS skip set. Omitted, the walk runs here (a direct caller)
|
|
180
173
|
* @returns {Set<string>}
|
|
181
174
|
*/
|
|
182
|
-
export function reachedVendorSpecifiers(graph, entryFiles, componentUrls, appDir,
|
|
175
|
+
export function reachedVendorSpecifiers(graph, entryFiles, componentUrls, appDir, skip, serverFiles, precomputedDeps) {
|
|
183
176
|
/** @type {Set<string>} */
|
|
184
177
|
const specs = new Set();
|
|
185
178
|
if (!graph) return specs;
|
|
186
179
|
const bare = bareImports(graph);
|
|
187
180
|
if (!bare.size) return specs;
|
|
188
|
-
// Roots = the
|
|
189
|
-
//
|
|
190
|
-
// absolute paths. Walk their non-elided transitive closure, unless
|
|
181
|
+
// Roots = the boot's shipped modules + the rendered components, keyed by the
|
|
182
|
+
// graph's own absolute paths. Walk their transitive closure, unless
|
|
191
183
|
// `pagePreloadHints` already walked it for this render (#1644).
|
|
192
184
|
const allEntries = [...entryFiles];
|
|
193
185
|
for (const url of componentUrls) {
|
|
194
186
|
allEntries.push(resolve(appDir, url.startsWith('/') ? url.slice(1) : url));
|
|
195
187
|
}
|
|
196
188
|
const files = new Set(allEntries);
|
|
197
|
-
for (const dep of precomputedDeps || transitiveDeps(graph, allEntries, appDir,
|
|
189
|
+
for (const dep of precomputedDeps || transitiveDeps(graph, allEntries, appDir, skip)) files.add(dep);
|
|
198
190
|
for (const file of files) {
|
|
199
191
|
// A server file is never served to the browser (its source is an RPC /
|
|
200
192
|
// throw-at-load stub), so a vendor it imports never ships and must NOT be
|
|
@@ -211,28 +203,23 @@ export function reachedVendorSpecifiers(graph, entryFiles, componentUrls, appDir
|
|
|
211
203
|
}
|
|
212
204
|
|
|
213
205
|
/**
|
|
214
|
-
* The skip set for the preload walks:
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* @type {WeakMap<Map<string, string[]>,
|
|
206
|
+
* The skip set for the preload walks: every `static lazy` component file
|
|
207
|
+
* (#1524). Built once per lazy map (a per-build object), since it was rebuilt
|
|
208
|
+
* per render (#1644).
|
|
209
|
+
* @type {WeakMap<Map<string, string[]>, Set<string>>}
|
|
218
210
|
*/
|
|
219
211
|
const SKIP_MEMO = new WeakMap();
|
|
220
|
-
const NO_ELIDABLE = new Set();
|
|
221
212
|
|
|
222
213
|
/**
|
|
223
|
-
* @param {Set<string> | undefined} elidable
|
|
224
214
|
* @param {Map<string, string[]> | undefined} lazyFiles
|
|
225
215
|
* @returns {Set<string> | undefined}
|
|
226
216
|
*/
|
|
227
|
-
function preloadSkipSet(
|
|
228
|
-
if (!lazyFiles || !lazyFiles.size) return
|
|
229
|
-
|
|
230
|
-
let byElidable = SKIP_MEMO.get(lazyFiles);
|
|
231
|
-
if (!byElidable) { byElidable = new WeakMap(); SKIP_MEMO.set(lazyFiles, byElidable); }
|
|
232
|
-
let skip = byElidable.get(key);
|
|
217
|
+
function preloadSkipSet(lazyFiles) {
|
|
218
|
+
if (!lazyFiles || !lazyFiles.size) return undefined;
|
|
219
|
+
let skip = SKIP_MEMO.get(lazyFiles);
|
|
233
220
|
if (!skip) {
|
|
234
|
-
skip = new Set(
|
|
235
|
-
|
|
221
|
+
skip = new Set(lazyFiles.keys());
|
|
222
|
+
SKIP_MEMO.set(lazyFiles, skip);
|
|
236
223
|
}
|
|
237
224
|
return skip;
|
|
238
225
|
}
|
|
@@ -245,8 +232,8 @@ function preloadSkipSet(elidable, lazyFiles) {
|
|
|
245
232
|
* walks it once and hands the closure to both.
|
|
246
233
|
*
|
|
247
234
|
* The walk roots are the BOOT's actually-shipped module set (`moduleUrls`,
|
|
248
|
-
*
|
|
249
|
-
*
|
|
235
|
+
* the component frontier of each route module, #1686) plus the rendered eager
|
|
236
|
+
* components, NOT the raw route entries: rooting at
|
|
250
237
|
* the raw entries would walk a dropped page's SSR-only subtree and hint a
|
|
251
238
|
* `modulepreload` for a module nothing that ships imports (#780, the
|
|
252
239
|
* app-module analog of the #754 vendor over-fetch).
|
|
@@ -256,15 +243,14 @@ function preloadSkipSet(elidable, lazyFiles) {
|
|
|
256
243
|
* appDir: string,
|
|
257
244
|
* moduleUrls: string[],
|
|
258
245
|
* eagerComponents: string[],
|
|
259
|
-
* elidableComponents: Set<string> | undefined,
|
|
260
246
|
* lazyFiles: Map<string, string[]> | undefined,
|
|
261
247
|
* serverFiles: Set<string> | undefined,
|
|
262
248
|
* }} input
|
|
263
249
|
* @returns {{ preloads: string[], vendorSpecifiers: Set<string>, skip: Set<string> | undefined }}
|
|
264
250
|
*/
|
|
265
251
|
export function pagePreloadHints(input) {
|
|
266
|
-
const { graph, appDir, moduleUrls, eagerComponents,
|
|
267
|
-
const skip = preloadSkipSet(
|
|
252
|
+
const { graph, appDir, moduleUrls, eagerComponents, lazyFiles, serverFiles } = input;
|
|
253
|
+
const skip = preloadSkipSet(lazyFiles);
|
|
268
254
|
const shippedRoots = moduleUrls.map((u) => resolve(appDir, u.startsWith('/') ? u.slice(1) : u));
|
|
269
255
|
/** @type {string[] | undefined} */
|
|
270
256
|
let deps;
|