@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.
Files changed (105) hide show
  1. package/README.md +4 -1
  2. package/index.d.ts +461 -102
  3. package/index.js +9 -4
  4. package/package.json +3 -3
  5. package/src/action-flash.js +185 -0
  6. package/src/action-input.js +208 -0
  7. package/src/action-params.js +156 -0
  8. package/src/action-route.js +8 -9
  9. package/src/action-seed.js +44 -5
  10. package/src/actions.js +112 -15
  11. package/src/api.js +67 -10
  12. package/src/app-layout.js +133 -0
  13. package/src/asset-hash.js +82 -49
  14. package/src/auth.js +612 -108
  15. package/src/browser-entries.js +2 -17
  16. package/src/build-epoch.js +1 -1
  17. package/src/cache-tags.js +5 -1
  18. package/src/check/helpers.js +85 -0
  19. package/src/check/rules-actions.js +37 -1
  20. package/src/check/rules-components.js +133 -28
  21. package/src/check/rules-imports.js +175 -4
  22. package/src/check/rules-route-modules.js +200 -0
  23. package/src/check/rules-routes.js +55 -0
  24. package/src/check/rules-routing.js +143 -95
  25. package/src/check/rules-typescript.js +24 -2
  26. package/src/check/rules.js +109 -9
  27. package/src/check/runner-support.js +53 -171
  28. package/src/check/runner.js +24 -6
  29. package/src/client-signals.js +171 -0
  30. package/src/component-scanner.js +22 -16
  31. package/src/context.js +152 -16
  32. package/src/cookie-store.js +262 -0
  33. package/src/core-exports.js +190 -0
  34. package/src/dev/bun-app-source.js +95 -5
  35. package/src/dev/config.js +7 -51
  36. package/src/dev/dev-service-worker.js +48 -0
  37. package/src/dev/dev-status.js +122 -0
  38. package/src/dev/esm-hot-hooks.js +373 -0
  39. package/src/dev/handler.js +632 -155
  40. package/src/dev/helpers.js +127 -96
  41. package/src/dev/hot-host.js +33 -6
  42. package/src/dev/poke-state.js +110 -0
  43. package/src/dev/serve.js +191 -133
  44. package/src/dev/server.js +109 -37
  45. package/src/dev/specifier-versions.js +125 -0
  46. package/src/dev/watch-ignore.js +49 -4
  47. package/src/dev/watch-poll.js +78 -0
  48. package/src/dev/watch-tree.js +59 -22
  49. package/src/dev-classify.js +59 -26
  50. package/src/dev-error.js +104 -3
  51. package/src/dev-hot-classify.js +291 -0
  52. package/src/dev-hot-client.js +245 -0
  53. package/src/dev-import.js +22 -5
  54. package/src/dev-overlay.js +26 -0
  55. package/src/dev-ready-probe.js +69 -0
  56. package/src/dev-regenerate.js +322 -78
  57. package/src/dev-reload-hold.js +49 -20
  58. package/src/dev-reload-reason.js +61 -0
  59. package/src/dev-reload-worker.js +62 -13
  60. package/src/dev-secret.js +73 -0
  61. package/src/dev-source-locations-hook.js +4 -1
  62. package/src/dev-source-locations.js +33 -2
  63. package/src/dev-styles.js +64 -6
  64. package/src/dev.js +0 -1
  65. package/src/env-files.js +124 -0
  66. package/src/file-storage.js +2 -2
  67. package/src/foreign-imports.js +228 -0
  68. package/src/form-action-identity.js +52 -25
  69. package/src/form-dispatch.js +150 -69
  70. package/src/html-cache.js +35 -4
  71. package/src/importmap.js +2 -2
  72. package/src/instrumentation.js +59 -3
  73. package/src/js-scan.js +5 -4
  74. package/src/jwe.js +155 -0
  75. package/src/lazy-components.js +166 -0
  76. package/src/listener-core.js +4 -2
  77. package/src/lit-hints.js +172 -0
  78. package/src/middleware-load.js +248 -0
  79. package/src/module-graph.js +73 -47
  80. package/src/module-side-effect.js +284 -0
  81. package/src/public-files.js +73 -0
  82. package/src/redirect-location.js +19 -0
  83. package/src/redirects.js +9 -1
  84. package/src/route-frontier.js +151 -0
  85. package/src/route-report.js +130 -0
  86. package/src/router.js +120 -13
  87. package/src/session.js +32 -10
  88. package/src/ssr/client-router-flag.js +1 -1
  89. package/src/ssr/document.js +49 -17
  90. package/src/ssr/head.js +228 -25
  91. package/src/ssr/preloads.js +31 -45
  92. package/src/ssr/render.js +243 -55
  93. package/src/ssr/responses.js +51 -4
  94. package/src/ssr/shipped-modules.js +52 -16
  95. package/src/testing.d.ts +31 -2
  96. package/src/testing.js +85 -17
  97. package/src/thenable-params.js +26 -0
  98. package/src/vendor/jspm.js +12 -1
  99. package/src/vendor/pins.js +9 -8
  100. package/src/vendor/scanner.js +14 -9
  101. package/src/vendor.js +5 -3
  102. package/webjs-config.schema.json +3 -8
  103. package/src/component-elision.js +0 -1852
  104. package/src/elision-differential.js +0 -101
  105. 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
- if (m.themeColor) metaTags.push(`<meta name="theme-color" content="${escapeAttr(m.themeColor)}">`);
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: { index, follow, googleBot, etc. }
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
- if (typeof m.robots === 'string') {
500
- metaTags.push(`<meta name="robots" content="${escapeAttr(m.robots)}">`);
501
- } else if (typeof m.robots === 'object') {
502
- const parts = [];
503
- if (m.robots.index === false) parts.push('noindex');
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 fully elided page ships no
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
- ${reload}
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
+ }
@@ -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, elidable, lazyFiles) {
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, elidableComponents, precomputedDeps) {
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 elidable components and any subtree reachable only through
137
- // them: their imports are stripped from served source, so the
138
- // browser never fetches these modules.
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` (which already drops
157
- * inert page/layout modules and substitutes an import-only page with its
158
- * components), plus `componentUrls` = the rendered components. From those roots
159
- * it walks the transitive app-graph closure (elidable components and the subtree
160
- * reachable only through them excluded), and collects each reached file's bare
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 (which elision keeps
170
- * off the client) is never preloaded (no over-fetch).
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>} [elidableComponents]
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, elidableComponents, serverFiles, precomputedDeps) {
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 SHIPPED page/layout modules (already inert-dropped + import-only
189
- // expanded by the caller) + the rendered components, keyed by the graph's own
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, elidableComponents)) files.add(dep);
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: the elidable components plus, when the
215
- * app has `static lazy` components (#1524), every lazy file. Built once per
216
- * lazy map (a per-build object), since it was rebuilt per render (#1644).
217
- * @type {WeakMap<Map<string, string[]>, WeakMap<Set<string>, Set<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(elidable, lazyFiles) {
228
- if (!lazyFiles || !lazyFiles.size) return elidable;
229
- const key = elidable || NO_ELIDABLE;
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([...(elidable || []), ...lazyFiles.keys()]);
235
- byElidable.set(key, skip);
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
- * inert modules already dropped and an import-only page already substituted)
249
- * plus the rendered eager components, NOT the raw route entries: rooting at
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, elidableComponents, lazyFiles, serverFiles } = input;
267
- const skip = preloadSkipSet(elidableComponents, lazyFiles);
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;