{ "version": 3, "sources": ["site/lib/template-resolver.mjs"], "sourcesContent": ["/**\n * template-resolver.mjs - gh#3600: the pure decision points site.js's\n * templateResolver makes, extracted for testability.\n *\n * Consumer: site/site.js's router.templateResolver.\n *\n * Browser-reachable module: NO `node:*` imports (same house rule\n * site/lib/routes.mjs already states - see AGENTS.md \"No top-level\n * `import 'node:*'`\"). Pure functions, plus holdRouteUntilAssets(), which\n * touches only the element it is handed, so a test can pass a stand-in.\n */\n\n/**\n * gh#3565: the real meta description content for a route. Falls\n * back to the site-wide default for the majority of routes with no\n * authored sitemap.json meta.description, so a client-side nav away from\n * a described route never leaves its description stale on an undescribed\n * one.\n *\n * @param {{ meta?: { description?: string } | null }} route\n * @param {string} defaultDescription\n * @returns {string}\n */\nexport function resolveMetaDescription(route, defaultDescription) {\n return route?.meta?.description || defaultDescription;\n}\n\n/**\n * Which of templateResolver's three branches a route's `template` name\n * resolves to:\n * - 'blank': no template name, or the literal 'blank' - render the\n * fetched content unchanged, no chrome.\n * - 'standalone': the content file is a full HTML document, rendered\n * via an isolated iframe rather than injected into doc-template chrome.\n * - 'doc': everything else - the legacy doc-template path (fetch\n * `site/templates/.html`, inject content, run DOM surgery).\n *\n * @param {string | null | undefined} name - route.template\n * @returns {'blank' | 'standalone' | 'doc'}\n */\nexport function classifyTemplateBranch(name) {\n if (!name || name === 'blank') return 'blank';\n if (name === 'standalone') return 'standalone';\n return 'doc';\n}\n\n/**\n * Ticket 10164: does this route's markup need the trait registry?\n *\n * A declared `traits=\"...\"` attribute resolves each name through the\n * registry at connectedCallback, warns once on a miss and skips the trait, so\n * the barrel that fills the registry (`traits/index.js`, 60 modules no\n * component imports) has to be registered before such markup is written.\n * Components import the traits they apply themselves (`static traits`), so a\n * route with no declared trait never needs the barrel. Hosts that render a\n * generated tree (``, ``, ``) take the\n * registry as a precaution: a payload can hand any attribute to an element it\n * stamps, and this markup scan cannot see a payload. A source search of the\n * a2ui package and the web-modules runtime found nothing that sets a `traits`\n * attribute, so the clause is conservative, not required.\n *\n * @param {string | null | undefined} html - the fetched route content\n * @returns {boolean}\n */\nexport function needsTraitRegistry(html) {\n return typeof html === 'string' && /\\btraits=|<(?:traits-host|a2ui-root|gen-root|canvas-ui)\\b/.test(html);\n}\n\n/**\n * Ticket 11233: the attribute that keeps `` hidden while the route\n * it just wrote is still loading its component assets (site.css hides\n * `router-ui[data-route-assets-pending]` with `visibility: hidden`).\n */\nexport const ROUTE_ASSETS_PENDING = 'data-route-assets-pending';\n\n/**\n * Ticket 11233: hold the router hidden until `assets` settles, then show it.\n *\n * templateResolver writes the route HTML at once and calls this with hook 1's\n * component load. Shown before that load lands, the route's tags upgrade in\n * view and shift the page (admin-dashboard CLS 0.305); written only after it\n * lands (PR 379), the router's innerHTML upgrades the whole route in one long\n * task. Hidden by `visibility`, the route keeps its layout, so components\n * that measure themselves while upgrading (chart-ui) still read real sizes.\n *\n * `token` names this resolution. A release clears the attribute only while it\n * still holds its own token: a superseded resolution landing late cannot show\n * a newer route early, and a newer navigation that never reached its own hold\n * (its resolver threw) cannot leave the route hidden. A failed load shows the\n * route too, unstyled, never blank. `assets` null: nothing to wait for, so any\n * hold an older resolution left is cleared now.\n *\n * Ticket 11231: a load that lands in a later task shows the route two frames\n * after it settles. A component that reflects its measured size from a\n * ResizeObserver (page-ui's `data-pad-step`, rAF-batched) still holds the\n * value it classified at connect, before the route's sheets applied, in the\n * frame the load lands; it corrects one frame later. The first frame lays the\n * hidden route out with its sheets and runs the observers, the second runs\n * their rAF reflects, so the route is shown settled (admin-dashboard over\n * HTTP/2: CLS 0.0101 to 0.0021). A load that lands in this task (already\n * loaded) still shows the route before the browser can paint: its sheets\n * were in place when the route's tags connected, so nothing is left to\n * settle, and a cached navigation never blanks.\n *\n * @param {{ getAttribute(name: string): string | null, setAttribute(name: string, value: string): void, removeAttribute(name: string): void }} router\n * @param {Promise | null} assets\n * @param {string} token\n * @param {(fn: () => void) => void} [frame] - schedules `fn` for the next\n * frame; requestAnimationFrame where there is one, else at once\n */\nexport function holdRouteUntilAssets(router, assets, token, frame = nextFrame) {\n if (!assets) {\n router.removeAttribute(ROUTE_ASSETS_PENDING);\n return;\n }\n router.setAttribute(ROUTE_ASSETS_PENDING, token);\n const release = () => {\n if (router.getAttribute(ROUTE_ASSETS_PENDING) === token) router.removeAttribute(ROUTE_ASSETS_PENDING);\n };\n let sameTask = true;\n setTimeout(() => { sameTask = false; }, 0);\n const settle = () => (sameTask ? release() : frame(() => frame(release)));\n assets.then(settle, settle);\n}\n\nfunction nextFrame(fn) {\n if (typeof requestAnimationFrame === 'function') requestAnimationFrame(fn);\n else fn();\n}\n\n/**\n * Ticket 11234: the promise the route hold waits on. site.js stamps a\n * \"Composes\" aside above a component route once the sidecar fetch answers; if\n * that lands after the route is shown, the demo shifts down (button route CLS\n * 0.0169 once the route's modules arrive early). So the hold waits for the\n * sidecar fetch too, started at the same time as the assets, and gives up on\n * it after `capMs` so a slow or hung sidecar can never keep a route hidden. A\n * failed fetch counts as settled. `assets` null (nothing to wait for): null.\n *\n * @param {Promise | null} assets\n * @param {Promise | null} composesReady\n * @param {number} [capMs]\n * @returns {Promise | null}\n */\nexport function holdWithComposes(assets, composesReady, capMs = 1500) {\n if (!assets) return null;\n const cap = new Promise((resolve) => setTimeout(resolve, capMs));\n const sidecar = Promise.race([Promise.resolve(composesReady).catch(() => null), cap]);\n return Promise.all([assets, sidecar]);\n}\n\n/**\n * Ticket 11266: the upper bound on the route hold. Measured on the dev server\n * (finish() to the hold coming off): admin-dashboard 0.38 s unthrottled, 1.86 s\n * at 100 ms per request and a 4x CPU throttle, 3.1 s at 300 ms and 6x; the\n * table route 1.35 s at 300 ms and 6x. 8 s is 2.6 times the slowest of those,\n * so an ordinary slow load never meets the cap (meeting it shows the route\n * before its late tags upgrade, a layout shift, and runs its setup then).\n */\nexport const ROUTE_ASSETS_CAP_MS = 8000;\n\n/**\n * Ticket 11266: `held` (the promise the route hold waits on), bounded. One\n * component stylesheet or module request that never answers used to keep the\n * route hidden for as long as the browser waited on it; now the hold resolves\n * after `capMs` anyway and `onTimeout` reports what had not settled. A failed\n * `held` resolves too. `held` null (nothing to wait for): null.\n *\n * @param {Promise | null} held\n * @param {() => void} onTimeout\n * @param {number} [capMs]\n * @returns {Promise | null}\n */\nexport function capRouteHold(held, onTimeout, capMs = ROUTE_ASSETS_CAP_MS) {\n if (!held) return null;\n let timer;\n const cap = new Promise((resolve) => {\n timer = setTimeout(() => { onTimeout(); resolve(); }, capMs);\n });\n const done = () => { clearTimeout(timer); };\n return Promise.race([Promise.resolve(held).then(done, done), cap]);\n}\n\n/**\n * Ticket 11262: settles once every tag in `tags` that has a module in\n * `tagMap` is defined. A route's setup module assigns `.data` or `.columns`\n * to the route's elements; assigned before the element upgrades, the value\n * lands as an own property that shadows the class accessor for good. A\n * structural tag (`js: null`) is never defined, and a tag outside the map has\n * no module to wait for, so neither is waited on.\n *\n * @param {string[]} tags\n * @param {Record} tagMap\n * @param {{ whenDefined(tag: string): Promise }} [registry]\n * @returns {Promise}\n */\nexport function whenRouteDefined(tags, tagMap, registry = customElements) {\n return Promise.all(tags.filter((t) => tagMap[t]?.js != null).map((t) => registry.whenDefined(t)));\n}\n\n/**\n * Ticket 11266: what the route was still waiting on when the cap fired: the\n * module of each route tag not yet defined, and every stylesheet the asset\n * loader has in flight (a composed child's sheet counts, which is why the\n * sheets are not limited to `tags`). `loader` is the loader's shared registry\n * (`globalThis.__nonounAssetLoader`: `cssPending` tag -> promise, `cssSettled`\n * the promises that have settled); without one, only the modules are named.\n *\n * @param {string[]} tags\n * @param {Record} tagMap\n * @param {{ cssPending?: Map>, cssSettled?: WeakSet } | undefined} loader\n * @param {(tag: string) => boolean} isDefined\n * @returns {string[]}\n */\nexport function unsettledRouteAssets(tags, tagMap, loader, isDefined) {\n const modules = tags.filter((t) => tagMap[t]?.js != null && !isDefined(t)).map((t) => tagMap[t].js);\n const sheets = loader?.cssPending\n ? [...loader.cssPending].filter(([, p]) => !loader.cssSettled?.has(p)).map(([t]) => tagMap[t]?.css ?? t)\n : [];\n return [...new Set([...modules, ...sheets])];\n}\n\n/**\n * Ticket 11362: settles once the asset loader's stylesheet for each of `tags`\n * has settled (adopted, or failed and reported). The loader adopts a tag's\n * sheet once the tag is defined by any source, so after the bundle fallback\n * defines the route's tags this waits only on their sheets, never on a route\n * module that did not answer. A tag with no sheet in flight adds nothing.\n *\n * @param {string[]} tags\n * @param {{ cssPending?: Map> } | undefined} loader\n * @returns {Promise}\n */\nexport function whenRouteSheets(tags, loader) {\n const sheets = tags.map((t) => loader?.cssPending?.get(t)).filter(Boolean);\n return Promise.allSettled(sheets).then(() => undefined);\n}\n"], "mappings": "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", "names": ["route", "defaultDescription", "name", "html", "router", "assets", "token", "frame", "nextFrame", "release", "sameTask", "settle", "fn", "composesReady", "capMs", "cap", "resolve", "sidecar", "held", "onTimeout", "timer", "done", "tags", "tagMap", "registry", "t", "loader", "isDefined", "modules", "sheets", "p"] }