🍓 Sync 2026-08-11 20:54:16

This commit is contained in:
github-actions[bot]
2026-08-11 20:54:16 +08:00
parent b10bf57011
commit 030d9d82a4
23 changed files with 228 additions and 213 deletions
@@ -381,33 +381,6 @@ function seed() {
adoptEntry();
}
/* Does this document already carry the stylesheet a menu.d node names?
*
* The value is a path under /luci-static/resources (`view/foo/foo.css`), and head.ut prints it with
* a `?v=` cache key, so compare the PATH only and by suffix — the theme does not get to assume the
* resource base, and a re-hosted sheet keeps its element and its href (fs-sheets.js disables the
* <link>, it never removes it), which is exactly the "already here" this asks about. */
function documentHasSheet(css) {
const want = '/' + css.replace(/^\/+/, '');
for (const link of document.querySelectorAll('link[rel~="stylesheet"][href]')) {
/* A <link> INSIDE the view tree dies with the swap — it is the one shape fs-sheets.js needs
* no handling for, and every scan there skips it for the same reason (VIEW_SHEETS, and the
* `closest('#view')` guards in documentPoisoned/scopeToCurrentPage/dedupeViewSheets).
* Counting it here would answer the wrong question: `luci-app-nlbwmon` returns
* `E('link', { rel: 'stylesheet', href: L.resource('view/nlbw.css') })` from render(), so
* standing on that page its sheet IS in the document — and it is exactly the sheet the next
* dom.content() throws away. An app in that shape moving to a menu.d `css` is the migration
* #8920 was written for, so the guard has to survive it: leaving from that page must be a
* full load, not a swap into an unstyled document. */
if (link.closest('#view'))
continue;
const href = (link.getAttribute('href') || '').split('?')[0];
if (href.endsWith(want))
return true;
}
return false;
}
/* Attempt an in-place navigation to `pathname`. Returns true if handled as a
* SPA nav (caller should preventDefault), false to let the browser do a normal
* full navigation. `push` adds a history entry (false when replaying popstate).
@@ -432,25 +405,29 @@ function navigate(pathname, push, kbd) {
/* A PAGE WHOSE STYLESHEET ONLY THE SERVER CAN EMIT IS NOT OURS TO SWAP INTO.
*
* A menu.d node may name its own sheet (`"css": "view/foo/foo.css"`), and the server links it
* from <head> on a full load (partials/head.ut). Nothing here can: a swap replaces #view's
* children, it does not re-render a document, so reaching such a page by CLICK would show it
* with the app's CSS missing — while reaching the same page by URL or F5 showed it styled. One
* page, two appearances, decided by how the user got there.
* A menu.d node may name its own sheet (`"css": "view/foo/foo.css"`), and the server links it from
* <head> on a full load (partials/head.ut). Nothing here can: a swap replaces #view's children, it
* does not re-render a document, so reaching such a page by CLICK would show it with the app's CSS
* missing — while reaching the same page by URL or F5 showed it styled. One page, two appearances,
* decided by how the user got there.
*
* So decline, exactly as the poisoned-document bail above does: speed is traded for
* correctness, never the other way. It costs ONE full load per such page — after it the <link>
* is in the document, this test passes, and every later visit is a swap again (fs-sheets.js
* owns the sheet from then on and re-lights it per page).
* So decline, exactly as the poisoned-document bail above does: speed is traded for correctness,
* never the other way. It costs ONE full load per such page — after it the <link> is in the
* document, this test passes, and every later visit is a swap again (fs-sheets.js owns the sheet
* from then on and re-lights it per page).
*
* Injecting the <link> here instead would work and is deliberately not done: it would put the
* theme in charge of fetching and ordering a foreign stylesheet, which is the job fs-sheets.js
* exists to keep out of the theme. The server already does it correctly.
* Injecting the <link> here instead would work and is deliberately not done: it would put the theme
* in charge of fetching and ordering a foreign stylesheet, which is the job fs-sheets.js exists to
* keep out of the theme. The server already does it correctly.
*
* `node.css` reaches the client because /admin/menu serves the dispatcher's own tree and
* ui.js's scrubMenu() only rewrites `satisfied`. On a luci-base that predates the `css` schema
* entry the property is dropped server-side, so this is simply never true there. */
if (typeof node.css === 'string' && node.css !== '' && !documentHasSheet(node.css))
* The QUESTION is fs-sheets.js's, not this module's: which sheets a document carries, and which of
* them a swap is about to delete, is the one thing that file knows — see documentCarries(). What is
* decided here is only what to do about the answer.
*
* `node.css` reaches the client because /admin/menu serves the dispatcher's own tree and ui.js's
* scrubMenu() only rewrites `satisfied`. On a luci-base that predates the `css` schema entry the
* property is dropped server-side, so this is simply never true there. */
if (typeof node.css === 'string' && node.css !== '' && !sheets.documentCarries(node.css))
return false;
const rsegs = res.segs;
@@ -307,6 +307,37 @@ function judgeSheet(el, universe) {
* <link> INSIDE the view tree (`luci-app-nlbwmon`) needs no handling: it dies with the swap. */
const VIEW_SHEETS = 'style:not([data-fs-shell]), link[rel~="stylesheet"]:not([data-fs-shell])';
/* DOES THIS SHEET OUTLIVE THE PAGE IT ARRIVED WITH? Everything this module decides hangs off that
* one question, and the answer is where the element sits: a <style>/<link> inside the view tree
* dies with the swap (dom.content() replaces #view's children), so it can neither poison the next
* page, nor need scoping to this one, nor be a duplicate worth removing. Only sheets outside it
* are this module's business. Named because it is asked in four places and read wrong in none of
* them only by luck: `!el.closest('#view')` states where an element is, not what follows from it. */
function outlivesPage(el) {
return !el.closest('#view');
}
/* Is `path` — a menu.d node's `css`, i.e. a path under /luci-static/resources — already carried by
* this document in a form that SURVIVES a swap?
*
* The router asks before committing a client navigation: only a server render emits that <link>, so
* a page whose stylesheet is missing must arrive by full load (see fs-router.js). The question is
* this module's because the answer is: a link inside #view is about to be deleted with the rest of
* the view, and counting it would hand the router a sheet the next dom.content() throws away —
* `luci-app-nlbwmon` returns E('link', …, L.resource('view/nlbw.css')) from render(), so that shape
* is real. Compared by PATH and by suffix: head.ut prints `{{ resource }}/{{ dispatched.css }}?v=…`,
* so neither the resource base nor the cache key is anyone's to assume. */
function documentCarries(path) {
const want = '/' + String(path).replace(/^\/+/, '');
for (const link of document.querySelectorAll('link[rel~="stylesheet"][href]')) {
if (!outlivesPage(link))
continue;
if ((link.getAttribute('href') || '').split('?')[0].endsWith(want))
return true;
}
return false;
}
/* An invasive sheet we OWN is contained — scopeToCurrentPage() darkens it the moment the router
* stamps the new page, so it cannot reach the next page and the document is not spent. One we could
* not attribute (not re-hostable, so never owned: an @import at the top, a sheet built with
@@ -331,7 +362,7 @@ function documentPoisoned() {
const names = themeNames();
return Array.prototype.some.call(
document.querySelectorAll(VIEW_SHEETS),
(el) => !el.closest('#view')
(el) => outlivesPage(el)
&& (!names || (invasiveSheet(el, names) && !_owner.has(el) && !_silenced.has(el))));
}
@@ -623,7 +654,7 @@ function scopeToCurrentPage(segs) {
if (segs) _curKey = appKey(segs);
const key = currentKey();
document.querySelectorAll(VIEW_SHEETS).forEach((el) => {
if (el.closest('#view') || !_owner.has(el)) return;
if (!outlivesPage(el) || !_owner.has(el)) return;
setEnabled(el, _owner.get(el) === key);
});
}
@@ -731,7 +762,7 @@ function sheetKey(el) {
function dedupeViewSheets() {
const seen = new Set();
document.querySelectorAll(VIEW_SHEETS).forEach((el) => {
if (el.closest('#view')) return;
if (!outlivesPage(el)) return;
const key = sheetKey(el);
if (key === null) return;
if (seen.has(key)) el.remove();
@@ -841,6 +872,7 @@ function watchViewSheets() {
}
return baseclass.extend({
documentCarries,
documentPoisoned,
scopeToCurrentPage,
watchViewSheets