@svelte-vitals/core 0.55.1 → 0.55.2

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.
@@ -1,5 +1,5 @@
1
- import { parse } from "svelte/compiler";
2
1
  import { aria, roles } from "aria-query";
2
+ import { parse } from "svelte/compiler";
3
3
  //#region src/types.ts
4
4
  const defaultProject = {
5
5
  hasRobotsTxt: false,
@@ -153,6 +153,19 @@ const seoTitlePresence = {
153
153
  };
154
154
  //#endregion
155
155
  //#region src/rules/seo/head-tag-rule.ts
156
+ /** Open Graph keys are read from `property=`; `name="og:*"` is the common slip. */
157
+ function ogMisspelled(key) {
158
+ return {
159
+ match: (t) => t.kind === "meta" && t.name === key && t.property === void 0,
160
+ label: `<meta name="${key}"> should be <meta property="${key}">`,
161
+ recommendation: `Open Graph reads ${key} from the property attribute: change name="${key}" to property="${key}".`,
162
+ fix: {
163
+ description: `Change name="${key}" to property="${key}" on the existing tag.`,
164
+ snippet: `<meta property="${key}" content="…" />`,
165
+ lang: "svelte"
166
+ }
167
+ };
168
+ }
156
169
  function detect(head, match) {
157
170
  const matches = head.tags.filter(match);
158
171
  const rank = (t) => (t.value !== "absent" ? 2 : 0) + (t.presence === "own" ? 1 : 0);
@@ -180,7 +193,8 @@ function headTagRule(opts) {
180
193
  async check(ctx) {
181
194
  return (opts.appliesTo ? ctx.heads.filter(opts.appliesTo) : ctx.heads).map((head) => {
182
195
  const detection = detect(head, opts.match);
183
- const message = detection.presence === "none" ? `Missing ${opts.label}` : detection.value === "absent" ? `Empty ${opts.label}` : opts.label;
196
+ const misspelled = detection.presence === "none" && opts.misspelled && head.tags.some(opts.misspelled.match) ? opts.misspelled : void 0;
197
+ const message = misspelled ? misspelled.label : detection.presence === "none" ? `Missing ${opts.label}` : detection.value === "absent" ? `Empty ${opts.label}` : opts.label;
184
198
  return {
185
199
  id: opts.id,
186
200
  category: "seo",
@@ -189,9 +203,9 @@ function headTagRule(opts) {
189
203
  route: head.route,
190
204
  location: head.file,
191
205
  message,
192
- recommendation: opts.recommendation,
206
+ recommendation: misspelled?.recommendation ?? opts.recommendation,
193
207
  docsUrl,
194
- ...opts.fix ? { fix: { ...opts.fix } } : {}
208
+ ...misspelled ? { fix: { ...misspelled.fix } } : opts.fix ? { fix: { ...opts.fix } } : {}
195
209
  };
196
210
  });
197
211
  }
@@ -236,6 +250,7 @@ const seoOgImage = headTagRule({
236
250
  title: "Open Graph image",
237
251
  severity: "warning",
238
252
  match: (t) => t.kind === "meta" && t.property === "og:image",
253
+ misspelled: ogMisspelled("og:image"),
239
254
  label: "<meta property=\"og:image\">",
240
255
  recommendation: "Add <meta property=\"og:image\">, or set openGraph.images on your meta component.",
241
256
  rationale: "og:image is the preview thumbnail shown when the page is shared on social platforms; without it links render bare and get fewer clicks.",
@@ -252,6 +267,7 @@ const seoOgTitle = headTagRule({
252
267
  title: "Open Graph title",
253
268
  severity: "warning",
254
269
  match: (t) => t.kind === "meta" && t.property === "og:title",
270
+ misspelled: ogMisspelled("og:title"),
255
271
  label: "<meta property=\"og:title\">",
256
272
  recommendation: "Add <meta property=\"og:title\">, or set openGraph.title on your meta component.",
257
273
  rationale: "og:title controls the headline shown when the page is shared on social platforms, independent of the document <title>.",
@@ -510,7 +526,7 @@ const performanceResponsiveImage = imageRule({
510
526
  snippet: "<img src=\"/hero.jpg\" srcset=\"/hero-800.jpg 800w, /hero-1600.jpg 1600w\" sizes=\"100vw\" width=\"1600\" height=\"900\" alt=\"…\" />",
511
527
  lang: "svelte"
512
528
  },
513
- ok: (img) => img.hasSrcset
529
+ ok: (img) => img.hasSrcset || img.svg === true
514
530
  });
515
531
  //#endregion
516
532
  //#region src/rules/perf/link-rule.ts
@@ -1252,7 +1268,7 @@ const seoTwitterCard = headTagRule({
1252
1268
  id: "seo/twitter-card",
1253
1269
  title: "Twitter Card",
1254
1270
  severity: "info",
1255
- match: (t) => t.kind === "meta" && t.name === "twitter:card",
1271
+ match: (t) => t.kind === "meta" && (t.name === "twitter:card" || t.property === "twitter:card"),
1256
1272
  label: "<meta name=\"twitter:card\">",
1257
1273
  recommendation: "Add <meta name=\"twitter:card\" content=\"summary_large_image\"> so X/Twitter renders a rich card.",
1258
1274
  rationale: "twitter:card selects how the page renders when shared on X/Twitter; without it the platform falls back to a basic link (Open Graph tags are used as fallbacks for the rest).",
@@ -1269,6 +1285,7 @@ const seoOgDescription = headTagRule({
1269
1285
  title: "Open Graph description",
1270
1286
  severity: "info",
1271
1287
  match: (t) => t.kind === "meta" && t.property === "og:description",
1288
+ misspelled: ogMisspelled("og:description"),
1272
1289
  label: "<meta property=\"og:description\">",
1273
1290
  recommendation: "Add <meta property=\"og:description\">, or set openGraph.description on your meta component.",
1274
1291
  rationale: "og:description is the summary shown under the title in social previews; without it platforms guess or show nothing, lowering click-through. The Open Graph protocol lists it as an optional property.",
@@ -1285,6 +1302,7 @@ const seoOgUrl = headTagRule({
1285
1302
  title: "Open Graph URL",
1286
1303
  severity: "warning",
1287
1304
  match: (t) => t.kind === "meta" && t.property === "og:url",
1305
+ misspelled: ogMisspelled("og:url"),
1288
1306
  label: "<meta property=\"og:url\">",
1289
1307
  recommendation: "Add <meta property=\"og:url\"> with the canonical URL, or set openGraph.url on your meta component.",
1290
1308
  rationale: "og:url tells social platforms the canonical address to attribute shares and likes to, consolidating engagement on one URL. The Open Graph protocol lists it as a required property.",
@@ -2084,6 +2102,223 @@ const seoHreflang = {
2084
2102
  }
2085
2103
  };
2086
2104
  //#endregion
2105
+ //#region src/rules/a11y/aria-data.ts
2106
+ /**
2107
+ * ARIA names `aria-query@5.3.2` does not carry. It is not the clean ARIA 1.2 snapshot it looks
2108
+ * like — it holds 1.2's 48 attributes plus three 1.3 additions — so the gap is patched by name
2109
+ * rather than by version. Every entry here is defined in the ARIA 1.3 editor's draft, which is
2110
+ * what makes reporting it as an unknown name wrong — how far implementations have got is a
2111
+ * separate question this rule does not ask. Re-check this list whenever the aria-query dependency
2112
+ * moves.
2113
+ */
2114
+ const ARIA_1_3_ROLES = /* @__PURE__ */ new Set([
2115
+ "comment",
2116
+ "image",
2117
+ "sectionheader",
2118
+ "sectionfooter",
2119
+ "suggestion"
2120
+ ]);
2121
+ const ARIA_1_3_ATTRIBUTES = /* @__PURE__ */ new Set(["aria-colindextext", "aria-rowindextext"]);
2122
+ /**
2123
+ * Roles whose `requiredProps` row in aria-query is stale. Neither ARIA 1.2 nor the 1.3 editor's
2124
+ * draft lists required states or properties for these — the 1.3 draft re-lists inherited
2125
+ * requirements where they apply (`menuitemradio` → `aria-checked`), so the absence is deliberate
2126
+ * rather than an inheritance the table left implicit. Without this, idiomatic APG listbox and
2127
+ * tree markup is flagged.
2128
+ */
2129
+ const NO_REQUIRED_PROPS = /* @__PURE__ */ new Set(["option", "treeitem"]);
2130
+ function isKnownRole(role) {
2131
+ return ARIA_1_3_ROLES.has(role) || roles.has(role);
2132
+ }
2133
+ function isAbstractRole(role) {
2134
+ return roles.get(role)?.abstract === true;
2135
+ }
2136
+ /** A concrete (non-abstract) role is the one a user agent resolves a role token list to. */
2137
+ function isConcreteRole(role) {
2138
+ return isKnownRole(role) && !isAbstractRole(role);
2139
+ }
2140
+ /**
2141
+ * The role a user agent applies: the first token naming a concrete role, or undefined when none
2142
+ * does. Shared so the role rules cannot disagree about what a fallback list means.
2143
+ */
2144
+ function resolveRole(tokens) {
2145
+ return tokens.find(isConcreteRole);
2146
+ }
2147
+ function isKnownAriaAttribute(name) {
2148
+ return ARIA_1_3_ATTRIBUTES.has(name) || aria.has(name);
2149
+ }
2150
+ function requiredAriaProps(role) {
2151
+ if (NO_REQUIRED_PROPS.has(role)) return [];
2152
+ const def = roles.get(role);
2153
+ return def ? Object.keys(def.requiredProps) : [];
2154
+ }
2155
+ function ariaValueKind(name) {
2156
+ const def = aria.get(name);
2157
+ if (!def) return void 0;
2158
+ return {
2159
+ type: def.type,
2160
+ ...def.values ? { values: def.values.map(String) } : {}
2161
+ };
2162
+ }
2163
+ //#endregion
2164
+ //#region src/a11y.ts
2165
+ /**
2166
+ * Branch-aware occurrence fold (design "Control-flow semantics"): within a branch
2167
+ * occurrences sum, across the arms of one exclusive block the arm with the most
2168
+ * occurrences wins (tie → lowest branch index) and ITS occurrences are the group's
2169
+ * representatives — so a caller's count is always `list.length`, with a location per
2170
+ * representative. `{#each}`/`{#snippet}` occurrences render 0..N times and drop out.
2171
+ * The max is per key: there is no scalar total to maximize.
2172
+ */
2173
+ function foldOccurrences(nodes) {
2174
+ const byKey = /* @__PURE__ */ new Map();
2175
+ for (const node of nodes) {
2176
+ if (node.repeatable) continue;
2177
+ const list = byKey.get(node.key);
2178
+ if (list) list.push(node);
2179
+ else byKey.set(node.key, [node]);
2180
+ }
2181
+ const folded = /* @__PURE__ */ new Map();
2182
+ for (const [key, list] of byKey) folded.set(key, foldAt(list, 0));
2183
+ return folded;
2184
+ }
2185
+ function foldAt(nodes, depth) {
2186
+ const unconditional = [];
2187
+ const groups = /* @__PURE__ */ new Map();
2188
+ for (const node of nodes) {
2189
+ const step = node.path[depth];
2190
+ if (!step) {
2191
+ unconditional.push(node);
2192
+ continue;
2193
+ }
2194
+ let branches = groups.get(step.group);
2195
+ if (!branches) groups.set(step.group, branches = /* @__PURE__ */ new Map());
2196
+ const list = branches.get(step.branch);
2197
+ if (list) list.push(node);
2198
+ else branches.set(step.branch, [node]);
2199
+ }
2200
+ const representatives = [...unconditional];
2201
+ for (const branches of groups.values()) {
2202
+ let best = [];
2203
+ let bestBranch = Number.POSITIVE_INFINITY;
2204
+ for (const [branch, list] of branches) {
2205
+ const arm = foldAt(list, depth + 1);
2206
+ if (arm.length > best.length || arm.length === best.length && branch < bestBranch) {
2207
+ best = arm;
2208
+ bestBranch = branch;
2209
+ }
2210
+ }
2211
+ representatives.push(...best);
2212
+ }
2213
+ return representatives;
2214
+ }
2215
+ /**
2216
+ * Decode a fragment identifier the way navigation does before matching an element id
2217
+ * (`href="#caf%C3%A9"` targets `id="café"`). Malformed escapes are kept verbatim —
2218
+ * the browser would also fail to decode them, so the raw text is the comparable form.
2219
+ */
2220
+ function decodeFragmentId(fragment) {
2221
+ try {
2222
+ return decodeURIComponent(fragment);
2223
+ } catch {
2224
+ return fragment;
2225
+ }
2226
+ }
2227
+ /** Whitespace-split tokens of a (possibly undefined) literal attribute value. */
2228
+ function splitTokens(value) {
2229
+ return value ? value.trim().split(/\s+/).filter(Boolean) : [];
2230
+ }
2231
+ /** Explicit `role` values that map to the landmark kinds the route rules inspect. */
2232
+ const LANDMARK_ROLES = /* @__PURE__ */ new Set([
2233
+ "main",
2234
+ "banner",
2235
+ "contentinfo",
2236
+ "complementary"
2237
+ ]);
2238
+ /** Sectioning content (HTML-AAM): a `<header>`/`<footer>` below one of these is not a landmark. */
2239
+ const SECTIONING_TAGS = /* @__PURE__ */ new Set([
2240
+ "article",
2241
+ "aside",
2242
+ "main",
2243
+ "nav",
2244
+ "section"
2245
+ ]);
2246
+ /**
2247
+ * Tags whose landmark-ness depends on sectioning ancestry. A provider that cannot see ancestry
2248
+ * (the per-file AST walk) uses this same set to mark the deferral (topLevel) — reading it from
2249
+ * here keeps the deferral set from drifting when the policy widens.
2250
+ */
2251
+ const ANCESTRY_DEPENDENT_TAGS = /* @__PURE__ */ new Set(["header", "footer"]);
2252
+ /** Attributes that give an element the accessible name the `<aside>` landmark decision reads. */
2253
+ const NAMING_ATTRS$1 = ["aria-label", "aria-labelledby"];
2254
+ /**
2255
+ * HTML-AAM: an `<aside>` scoped to `body` or `main` is a `complementary` landmark; scoped to
2256
+ * sectioning content it is one only when it has an accessible name. `main` is deliberately absent
2257
+ * — it is a scope in which an `aside` *is* a landmark, unlike the sectioning set that decides
2258
+ * `<header>`/`<footer>`.
2259
+ */
2260
+ const ASIDE_DEMOTING_TAGS = /* @__PURE__ */ new Set([
2261
+ "article",
2262
+ "aside",
2263
+ "nav",
2264
+ "section"
2265
+ ]);
2266
+ /**
2267
+ * The landmark kind an element contributes ('main'|'banner'|'contentinfo'|'complementary'), or
2268
+ * undefined. The one home for the decision both providers apply — the source AST walk and the
2269
+ * rendered HTML walk each feed it their own tree's context. ARIA fallback role lists resolve to
2270
+ * the first token naming a concrete role (resolveRole).
2271
+ */
2272
+ function resolveLandmark(input) {
2273
+ const { tag, roleTokens, named, insideSectioning, insideAsideDemoting } = input;
2274
+ if (roleTokens !== void 0) {
2275
+ const role = resolveRole(roleTokens);
2276
+ return role && LANDMARK_ROLES.has(role) ? role : void 0;
2277
+ }
2278
+ if (tag === "main") return "main";
2279
+ if (tag !== void 0 && ANCESTRY_DEPENDENT_TAGS.has(tag)) return insideSectioning ? void 0 : tag === "header" ? "banner" : "contentinfo";
2280
+ if (tag === "aside") return !insideAsideDemoting || named ? "complementary" : void 0;
2281
+ }
2282
+ /**
2283
+ * Attributes whose (whitespace-tokenized) values reference element ids: the ARIA id-reference and
2284
+ * id-reference-list properties, and HTML's own (`for`, `list`, `headers`, `form`, the popover and
2285
+ * command targets). `href="#…"` is handled separately — its value is a URL, not a token list.
2286
+ */
2287
+ const IDREF_ATTRS = [
2288
+ "for",
2289
+ "list",
2290
+ "headers",
2291
+ "form",
2292
+ "popovertarget",
2293
+ "commandfor",
2294
+ "aria-labelledby",
2295
+ "aria-describedby",
2296
+ "aria-controls",
2297
+ "aria-activedescendant",
2298
+ "aria-owns",
2299
+ "aria-details",
2300
+ "aria-errormessage",
2301
+ "aria-flowto"
2302
+ ];
2303
+ /**
2304
+ * Whether a decoded URL fragment is HTML's "top of the document" indicator: `#top` (ASCII
2305
+ * case-insensitive) scrolls to the top when no element has that id, so it is never a missing
2306
+ * reference. Compare AFTER percent-decoding — `#%74op` navigates identically to `#top`.
2307
+ */
2308
+ function isTopFragment(id) {
2309
+ return id.toLowerCase() === "top";
2310
+ }
2311
+ /**
2312
+ * A fragment with its text directive removed. Everything from the first `:~:` on is user-agent
2313
+ * instructions for finding text and names no element, while anything before it is still an
2314
+ * ordinary element fragment — `#section:~:text=hi` targets `id="section"`, `#:~:text=hi` targets
2315
+ * nothing. Returns an empty string when the fragment is a directive and nothing else.
2316
+ */
2317
+ function stripTextDirective(fragment) {
2318
+ const i = fragment.indexOf(":~:");
2319
+ return i === -1 ? fragment : fragment.slice(0, i);
2320
+ }
2321
+ //#endregion
2087
2322
  //#region src/rules/seo/single-h1.ts
2088
2323
  const docsUrl$6 = docsUrlFor("seo/single-h1");
2089
2324
  const passRecommendation = "Use exactly one <h1> per page for its main topic; demote extra top-level headings to <h2>+.";
@@ -2098,10 +2333,24 @@ const multipleRecommendation = "A single, clear <h1> is the conventional signal
2098
2333
  * signal, but no official source documents a ranking penalty for several (2026-08-09 v1
2099
2334
  * rule-validity review, P2 #11) — so it's flagged as a style nit, not a defect. Exactly
2100
2335
  * one passes. A route whose headings were not collected (channel unset) emits nothing, as does
2101
- * one whose `dynamicHeading` flag says an undeterminable `<svelte:element>` may be its <h1>. A
2336
+ * one whose `dynamicHeading` flag says a heading of undeterminable level may be its <h1>. A
2102
2337
  * global `rules: { 'seo/single-h1': <severity> }` override flattens both arms to one
2103
2338
  * severity (design, `applyRuleSeverities`).
2104
2339
  */
2340
+ /**
2341
+ * The <h1>s that can render together, in input order: the arms of one `{#if}`/`{#await}`/
2342
+ * `<svelte:boundary>` contribute only their largest arm. Separate blocks still sum, since both may be true.
2343
+ */
2344
+ function renderableH1s(headings) {
2345
+ const nodes = headings.filter((h) => h.level === 1).map((heading) => ({
2346
+ key: "h1",
2347
+ path: heading.path ?? [],
2348
+ repeatable: false,
2349
+ heading
2350
+ }));
2351
+ const kept = new Set([...foldOccurrences(nodes).values()].flat().map((n) => n.heading));
2352
+ return headings.filter((h) => kept.has(h));
2353
+ }
2105
2354
  const seoSingleH1 = {
2106
2355
  id: "seo/single-h1",
2107
2356
  title: "Heading hierarchy",
@@ -2112,7 +2361,7 @@ const seoSingleH1 = {
2112
2361
  async check(ctx) {
2113
2362
  const out = [];
2114
2363
  for (const route of ctx.headings ?? []) {
2115
- const h1 = [...route.headings, ...route.componentHeadings ?? []].filter((h) => h.level === 1);
2364
+ const h1 = renderableH1s([...route.headings, ...route.componentHeadings ?? []]);
2116
2365
  let problem;
2117
2366
  let where = {};
2118
2367
  if (h1.length === 0) {
@@ -2246,6 +2495,13 @@ const seoDuplicateDescription = uniquenessRule({
2246
2495
  rationale: "Duplicate meta descriptions give search engines no per-page summary, so they are often ignored or rewritten."
2247
2496
  });
2248
2497
  //#endregion
2498
+ //#region src/headings.ts
2499
+ /** Two headings can never render together: different arms of one block. */
2500
+ function exclusiveHeadings(a, b) {
2501
+ if (!a.path || !b.path) return false;
2502
+ return a.path.some((s) => b.path.some((t) => s.group === t.group && s.branch !== t.branch));
2503
+ }
2504
+ //#endregion
2249
2505
  //#region src/rules/seo/heading-level-skip.ts
2250
2506
  const docsUrl$5 = docsUrlFor("seo/heading-level-skip");
2251
2507
  const recommendation$5 = "Increase heading levels one step at a time (do not jump, e.g. from <h2> straight to <h4>).";
@@ -2266,20 +2522,16 @@ const seoHeadingLevelSkip = {
2266
2522
  const out = [];
2267
2523
  for (const route of ctx.headings ?? []) {
2268
2524
  if (route.headings.length === 0) continue;
2269
- let prev = route.headings[0].level;
2270
2525
  let skip;
2271
- for (let i = 1; i < route.headings.length; i++) {
2526
+ for (let i = 1; i < route.headings.length && !skip; i++) {
2272
2527
  const h = route.headings[i];
2273
- if (h.level > prev + 1) {
2274
- skip = {
2275
- level: h.level,
2276
- prev,
2277
- line: h.line,
2278
- file: h.file
2279
- };
2280
- break;
2281
- }
2282
- prev = h.level;
2528
+ const prev = route.headings.slice(0, i).reverse().find((p) => !exclusiveHeadings(p, h));
2529
+ if (prev && h.level > prev.level + 1) skip = {
2530
+ level: h.level,
2531
+ prev: prev.level,
2532
+ line: h.line,
2533
+ file: h.file
2534
+ };
2283
2535
  }
2284
2536
  out.push(skip ? {
2285
2537
  id: "seo/heading-level-skip",
@@ -2409,11 +2661,94 @@ function kitModuleRule(opts) {
2409
2661
  });
2410
2662
  }
2411
2663
  //#endregion
2412
- //#region src/rules/seo/ssr-disabled.ts
2413
- /** The root layout — disabling SSR there turns the whole app into an SPA. */
2664
+ //#region src/kit-module.ts
2665
+ /** The root layout — its page options cover the whole app. */
2414
2666
  const ROOT_LAYOUT_RE = /^src\/routes\/\+layout(\.server)?\.(ts|js)$/;
2415
- /** `ssr` is a page option — it has no effect in `+server` endpoints or hooks files. */
2667
+ /** Files whose `ssr` export is a page option — it has no effect in `+server` endpoints or hooks files. */
2416
2668
  const PAGE_OPTION_FILE_RE = /\+(page|layout)(\.server)?\.(ts|js)$/;
2669
+ const appSsrCache = /* @__PURE__ */ new WeakMap();
2670
+ /**
2671
+ * Whether no page is ever server-rendered: the root layout exports `ssr = false` and no page-option
2672
+ * file might turn it back on. Universal loads and components then run only in the browser; server
2673
+ * files (`+page.server`, `+server`, hooks) still run on the server.
2674
+ */
2675
+ function appSsrDisabled(kitModules) {
2676
+ if (!kitModules) return false;
2677
+ let hit = appSsrCache.get(kitModules);
2678
+ if (hit === void 0) {
2679
+ hit = kitModules.some((m) => m.ssrDisabled && ROOT_LAYOUT_RE.test(m.file)) && !kitModules.some((m) => PAGE_OPTION_FILE_RE.test(m.file) && (m.ssrEnabled || m.parseFailed));
2680
+ appSsrCache.set(kitModules, hit);
2681
+ }
2682
+ return hit;
2683
+ }
2684
+ const ROUTES_DIR$1 = "src/routes";
2685
+ /** A `+page`/`+layout` file: its directory, kind, and the `@segment` layout reset (on `.svelte` names). */
2686
+ const ROUTE_FILE_RE = /^(src\/routes(?:\/.+)?)\/\+(page|layout)(?:@([^/]*))?(?:\.server)?\.(?:svelte|ts|js)$/;
2687
+ const parentDir = (dir) => dir === ROUTES_DIR$1 ? void 0 : dir.slice(0, dir.lastIndexOf("/"));
2688
+ /** The nearest ancestor-or-self directory a `@segment` reset names (`''` is the root). */
2689
+ function resetTarget(dir, segment) {
2690
+ for (let d = dir; d !== void 0; d = parentDir(d)) if (segment === "" ? d === ROUTES_DIR$1 : d.slice(d.lastIndexOf("/") + 1) === segment) return d;
2691
+ }
2692
+ const routeSsrCache = /* @__PURE__ */ new WeakMap();
2693
+ /**
2694
+ * Per route directory, whether its page (`page:<dir>`) or layout (`layout:<dir>`) is never
2695
+ * server-rendered. A page takes the nearest `ssr` export along its own files and then its layout
2696
+ * chain, following `+page@x`/`+layout@x` resets as SvelteKit does; an export other than a literal
2697
+ * `false`, or an unparsed option file, counts as SSR on. A layout is off only when every page using
2698
+ * it is — and the root layout also needs its own `ssr = false`, since it wraps the 404 page.
2699
+ */
2700
+ function routeSsrOff(scope) {
2701
+ const kitModules = scope.kitModules ?? [];
2702
+ let hit = routeSsrCache.get(scope);
2703
+ if (hit) return hit;
2704
+ const pages = /* @__PURE__ */ new Map();
2705
+ const layouts = /* @__PURE__ */ new Map();
2706
+ const files = [
2707
+ ...scope.sourceFiles ?? [],
2708
+ ...(scope.components ?? []).map((c) => c.file),
2709
+ ...kitModules.map((m) => m.file)
2710
+ ];
2711
+ for (const file of files) {
2712
+ const match = ROUTE_FILE_RE.exec(file);
2713
+ if (!match) continue;
2714
+ const map = match[2] === "page" ? pages : layouts;
2715
+ if (match[3] !== void 0 || !map.has(match[1])) map.set(match[1], match[3]);
2716
+ }
2717
+ const option = /* @__PURE__ */ new Map();
2718
+ for (const m of kitModules) {
2719
+ const match = ROUTE_FILE_RE.exec(m.file);
2720
+ if (!match) continue;
2721
+ const key = `${match[2]}:${match[1]}`;
2722
+ if (m.ssrEnabled || m.parseFailed) option.set(key, "on");
2723
+ else if (m.ssrDisabled && !option.has(key)) option.set(key, "off");
2724
+ }
2725
+ hit = /* @__PURE__ */ new Map();
2726
+ for (const [dir, reset] of pages) {
2727
+ const chain = [];
2728
+ for (let d = reset === void 0 ? dir : resetTarget(dir, reset); d !== void 0;) {
2729
+ const layoutReset = layouts.get(d);
2730
+ if (layouts.has(d)) chain.push(d);
2731
+ d = layoutReset === void 0 ? parentDir(d) : resetTarget(parentDir(d), layoutReset);
2732
+ }
2733
+ const off = [`page:${dir}`, ...chain.map((l) => `layout:${l}`)].map((k) => option.get(k)).find(Boolean) === "off";
2734
+ hit.set(`page:${dir}`, off);
2735
+ for (const l of chain) hit.set(`layout:${l}`, (hit.get(`layout:${l}`) ?? true) && off);
2736
+ }
2737
+ if (option.get(`layout:${ROUTES_DIR$1}`) !== "off") hit.delete(`layout:${ROUTES_DIR$1}`);
2738
+ routeSsrCache.set(scope, hit);
2739
+ return hit;
2740
+ }
2741
+ /** Whether a `+page`/`+layout` file (module or component) belongs to a route that is never server-rendered. */
2742
+ function routeNeverSsr(file, scope) {
2743
+ const match = ROUTE_FILE_RE.exec(file);
2744
+ return !!match && routeSsrOff(scope).get(`${match[2]}:${match[1]}`) === true;
2745
+ }
2746
+ /** Whether a universal `+page`/`+layout` module never runs on the server — its own `ssr = false`, an app-wide one, or one its route inherits. */
2747
+ function universalNeverSsr(m, scope) {
2748
+ return m.kind === "universal" && (m.ssrDisabled !== void 0 || appSsrDisabled(scope.kitModules) || routeNeverSsr(m.file, scope));
2749
+ }
2750
+ //#endregion
2751
+ //#region src/rules/seo/ssr-disabled.ts
2417
2752
  const seoSsrDisabled = kitModuleRule({
2418
2753
  id: "seo/ssr-disabled",
2419
2754
  title: "SSR disabled",
@@ -2755,9 +3090,19 @@ function messageFor(link) {
2755
3090
  if (link.kind === "goto") return `goto('${link.path}') is root-relative — it navigates outside this project's kit.paths.base and 404s in production. Use goto(resolve('${link.path}')) with resolve from '$app/paths'.`;
2756
3091
  return `redirect(…, '${link.path}') is root-relative — the Location header points outside this project's kit.paths.base and 404s in production. Use resolve('${link.path}') from '$app/paths'.`;
2757
3092
  }
3093
+ /**
3094
+ * A literal that already carries a statically known base is correct as written; only a dynamic
3095
+ * base (no `value`) leaves every root-relative literal suspect.
3096
+ */
3097
+ function startsWithBase(path, base) {
3098
+ if (base === void 0 || !path.startsWith(base)) return false;
3099
+ if (/(^|\/)(\.|%2e){2}(\/|$)/i.test(path.split(/[?#]/)[0])) return false;
3100
+ const next = path.charAt(base.length);
3101
+ return next === "" || next === "/" || next === "?" || next === "#";
3102
+ }
2758
3103
  /** Emit one file's PASS/PENALIZED results — same shapes as componentRule/kitModuleRule. */
2759
- function emitFile$1(out, file, links, suppressions) {
2760
- const bad = links.filter((l) => !(l.line > 0 && isSuppressed(suppressions, ID$8, l.line)));
3104
+ function emitFile$1(out, file, links, suppressions, base) {
3105
+ const bad = links.filter((l) => !startsWithBase(l.path, base) && !(l.line > 0 && isSuppressed(suppressions, ID$8, l.line)));
2761
3106
  if (bad.length === 0) {
2762
3107
  out.push({
2763
3108
  id: ID$8,
@@ -2802,16 +3147,17 @@ const correctnessBasePathNavigation = {
2802
3147
  fix: { ...FIX$2 },
2803
3148
  async check(ctx) {
2804
3149
  if (!ctx.project.kitPathsBase) return [];
3150
+ const base = ctx.project.kitPathsBase.value;
2805
3151
  const out = [];
2806
3152
  for (const c of ctx.components ?? []) {
2807
3153
  const links = c.basePathLinks ?? [];
2808
3154
  if (links.length === 0) continue;
2809
- emitFile$1(out, c.file, links, c.suppressions);
3155
+ emitFile$1(out, c.file, links, c.suppressions, base);
2810
3156
  }
2811
3157
  for (const m of ctx.kitModules ?? []) {
2812
3158
  const links = m.basePathLinks ?? [];
2813
3159
  if (links.length === 0) continue;
2814
- emitFile$1(out, m.file, links, m.suppressions);
3160
+ emitFile$1(out, m.file, links, m.suppressions, base);
2815
3161
  }
2816
3162
  return out;
2817
3163
  }
@@ -2878,7 +3224,8 @@ const correctnessServerBrowserGlobal = {
2878
3224
  })), c.suppressions);
2879
3225
  }
2880
3226
  for (const m of ctx.kitModules ?? []) {
2881
- const refs = m.browserGlobalRefs ?? [];
3227
+ const neverSsr = universalNeverSsr(m, ctx);
3228
+ const refs = (m.browserGlobalRefs ?? []).filter((r) => !(neverSsr && r.inHandler));
2882
3229
  if (refs.length === 0) continue;
2883
3230
  emitFile(out, m.file, refs.map((r) => ({
2884
3231
  line: r.line,
@@ -2897,7 +3244,7 @@ const correctnessInstanceBrowserGlobal = componentRule({
2897
3244
  label: "Server-safe component init",
2898
3245
  recommendation: "Move browser-only code into onMount or $effect (they never run on the server), or guard it with browser from $app/environment (or a typeof check).",
2899
3246
  rationale: "A component instance script runs on the server on every SSR render, where window/document/localStorage do not exist. Warning, not critical: a component rendered only behind a parent {#if browser} (or a client-only dynamic import) is a legitimate pattern that static analysis cannot prove cross-file.",
2900
- applies: (c) => (c.browserGlobalRefs ?? []).some((r) => r.context === "instance"),
3247
+ applies: (c, _o, ctx) => (c.browserGlobalRefs ?? []).some((r) => r.context === "instance") && !appSsrDisabled(ctx.kitModules) && !routeNeverSsr(c.file, ctx),
2901
3248
  bad: (c) => (c.browserGlobalRefs ?? []).filter((r) => r.context === "instance").map((r) => ({
2902
3249
  line: r.line,
2903
3250
  message: `${r.name} is accessed during component initialisation — during SSR this runs on the server, where ${r.name} is not defined`
@@ -2943,7 +3290,7 @@ const securityHandlerStateWrite = kitModuleRule({
2943
3290
  label: "Load/handler purity",
2944
3291
  recommendation: "Return the data from load (or the action) and pass it via page data instead of writing it to module state; per-user data belongs in cookies/locals plus a database. Caches and rate limiters keyed by non-personal data are the benign shape — if that describes this write, add an inline suppression.",
2945
3292
  rationale: "SvelteKit's docs mark this NEVER-DO-THIS: the server is one long-lived process shared by every user, so module state written during a request is visible to ALL later requests.",
2946
- applies: (m) => m.importedStateWrites.length > 0 && !(m.kind === "universal" && m.ssrDisabled),
3293
+ applies: (m, ctx) => m.importedStateWrites.length > 0 && !universalNeverSsr(m, ctx),
2947
3294
  bad: (m) => m.importedStateWrites.map((w) => ({
2948
3295
  line: w.line,
2949
3296
  message: `a server-executed handler writes imported module state "${w.name}" — module state is shared across all requests on the server; if this holds per-request or per-user data, one user's data can leak to another. Caches and rate limiters keyed by non-personal data are the benign shape — verify which this is`
@@ -2958,7 +3305,7 @@ const securityServerModuleState = kitModuleRule({
2958
3305
  label: "Server module state",
2959
3306
  recommendation: "Do not keep request data in module scope on the server — authenticate with cookies/locals and persist per-user data in a database. For a deliberate process-wide cache, prefer a const container (e.g. a Map) or add an inline suppression.",
2960
3307
  rationale: "Module scope on the server is one shared, long-lived instance (SvelteKit docs: \"Avoid shared state on the server\"): a value reassigned during one user's request is served to every other user, and it silently resets on every deploy or restart.",
2961
- applies: (m) => m.moduleStateReassignments.length > 0,
3308
+ applies: (m, ctx) => m.moduleStateReassignments.length > 0 && !universalNeverSsr(m, ctx),
2962
3309
  bad: (m) => m.moduleStateReassignments.map((r) => ({
2963
3310
  line: r.line,
2964
3311
  message: r.inHandler ? `module-scope variable "${r.name}" is reassigned from a request handler — its value is shared across all requests on the server` : `module-scope variable "${r.name}" is reassigned from a function — if it runs during a request, the value is shared across all requests on the server`
@@ -2977,7 +3324,7 @@ const securitySharedStateImport = kitModuleRule({
2977
3324
  label: "Server state imports",
2978
3325
  recommendation: "Keep module-scope $state out of server-executed code: return data from load and share it via page data or the context API. If the module is genuinely client-only, restructure so server files do not import it, or add an inline suppression.",
2979
3326
  rationale: "A .svelte.ts module with module-scope $state is one shared instance on the server: mutated, it leaks data between users; read-only, every request sees the same boot-time value instead of per-user data.",
2980
- applies: (m) => m.runesModuleImports.length > 0 && !(m.kind === "universal" && m.ssrDisabled),
3327
+ applies: (m, ctx) => m.runesModuleImports.length > 0 && !universalNeverSsr(m, ctx),
2981
3328
  bad: (m, ctx) => {
2982
3329
  const stateFiles = new Set((ctx.components ?? []).filter((c) => c.moduleStateDecls.length > 0).map((c) => c.file));
2983
3330
  const writtenOutside = new Set(m.importedStateWritesOutsideHandlers.map((w) => w.name));
@@ -3258,7 +3605,8 @@ function rootObjectName(node) {
3258
3605
  * `then`/`catch` value/error bindings, and a fragment's own `{@const ...}` /
3259
3606
  * `{let ...}` / `{const ...}` declaration tags (attributed to the enclosing Fragment,
3260
3607
  * shadowing the whole fragment like a block's `let` — a write to one of these
3261
- * template-locals, possible for `{let}`, is still not a write to the outer binding).
3608
+ * template-locals, possible for `{let}`, is still not a write to the outer binding), and an
3609
+ * element's or component's `let:` directive bindings.
3262
3610
  * Used by `walkScoped` so a write/mutation detector
3263
3611
  * doesn't misattribute a write to one of these locals as a write to an outer `$state`/prop
3264
3612
  * of the same name (issue #140 — originally a deliberately partial mitigation that left
@@ -3284,7 +3632,20 @@ function scopeIntroducedNames(node) {
3284
3632
  else if (node.type === "AwaitBlock") {
3285
3633
  if (node.value) addBoundNames(node.value, introduced);
3286
3634
  if (node.error) addBoundNames(node.error, introduced);
3287
- } else if (node.type === "Fragment") {
3635
+ } else if (Array.isArray(node.attributes) && node.fragment) for (const attr of node.attributes) {
3636
+ if (attr?.type !== "LetDirective") continue;
3637
+ const e = attr.expression;
3638
+ const asPattern = {
3639
+ ObjectExpression: "ObjectPattern",
3640
+ ArrayExpression: "ArrayPattern"
3641
+ }[e?.type];
3642
+ if (e) addBoundNames(asPattern ? {
3643
+ ...e,
3644
+ type: asPattern
3645
+ } : e, introduced);
3646
+ else introduced.add(attr.name);
3647
+ }
3648
+ else if (node.type === "Fragment") {
3288
3649
  for (const child of node.nodes ?? []) if (child?.type === "ConstTag" || child?.type === "DeclarationTag") for (const d of child.declaration?.declarations ?? []) addBoundNames(d.id, introduced);
3289
3650
  }
3290
3651
  return introduced;
@@ -3340,11 +3701,21 @@ function walkEvalScope(node, visit, shadowed = /* @__PURE__ */ new Set(), bounda
3340
3701
  const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
3341
3702
  if (visit(node, scope)) return;
3342
3703
  if (boundaries.has(node.type)) return;
3704
+ const outer = introduced.size > 0 ? outerScopeKeys(node) : void 0;
3343
3705
  for (const key of Object.keys(node)) {
3344
3706
  if (WALK_IGNORED_KEYS.has(key)) continue;
3345
- walkEvalScope(node[key], visit, scope, boundaries);
3707
+ if (!outer?.has(key)) walkEvalScope(node[key], visit, scope, boundaries);
3708
+ else if (!Array.isArray(node[key])) walkEvalScope(node[key], visit, shadowed, boundaries);
3709
+ else for (const child of node[key]) walkEvalScope(child, visit, child?.type === "LetDirective" ? scope : shadowed, boundaries);
3346
3710
  }
3347
3711
  }
3712
+ /** Children of a binding construct that are evaluated before its bindings exist. */
3713
+ function outerScopeKeys(node) {
3714
+ if (node.type === "EachBlock" || node.type === "AwaitBlock") return OUTER_SCOPE_KEYS;
3715
+ if (Array.isArray(node.attributes) && node.fragment) return ATTRIBUTE_KEYS;
3716
+ }
3717
+ const OUTER_SCOPE_KEYS = /* @__PURE__ */ new Set(["expression"]);
3718
+ const ATTRIBUTE_KEYS = /* @__PURE__ */ new Set(["attributes"]);
3348
3719
  /**
3349
3720
  * Unwrap a top-level statement's `export`/`export default` wrapper to the declaration
3350
3721
  * (or expression) it wraps; a non-export statement is returned as-is. Used so pattern 2
@@ -3449,12 +3820,13 @@ function collectNamedImportAliases(program, moduleSource, names) {
3449
3820
  }
3450
3821
  const BROWSER_GUARD_NAMES = /* @__PURE__ */ new Set(["browser"]);
3451
3822
  /**
3452
- * Local names of `browser` value-imported from '$app/environment' (alias-resolved) —
3823
+ * Local names of `browser` value-imported from '$app/environment' or its SvelteKit 3 name
3824
+ * '$app/env' (alias-resolved) —
3453
3825
  * the guard binding recognised by the browser-global scanner (correctness/server-browser-global, correctness/instance-browser-global).
3454
3826
  * Shared with the Kit-module parser.
3455
3827
  */
3456
3828
  function collectBrowserGuardImports(program) {
3457
- return collectNamedImportAliases(program, "$app/environment", BROWSER_GUARD_NAMES);
3829
+ return /* @__PURE__ */ new Set([...collectNamedImportAliases(program, "$app/environment", BROWSER_GUARD_NAMES), ...collectNamedImportAliases(program, "$app/env", BROWSER_GUARD_NAMES)]);
3458
3830
  }
3459
3831
  /**
3460
3832
  * Names bound at the program's top level: every import's local name plus every
@@ -3490,10 +3862,12 @@ function guardTerminates(consequent) {
3490
3862
  /**
3491
3863
  * Whether a guard-clause test establishes a browser environment: it references the
3492
3864
  * `$app/environment` `browser` binding, or contains a
3493
- * `typeof <tracked-global> === | !== 'undefined'` comparison (correctness/server-browser-global, correctness/instance-browser-global).
3865
+ * `typeof <tracked-global>` comparison against any type string (`'undefined'`, `'function'`, …)
3866
+ * (correctness/server-browser-global, correctness/instance-browser-global).
3494
3867
  * Over-matching here only widens the skip — a conservative miss, never a false positive.
3495
3868
  */
3496
3869
  function isBrowserGuardTest(test, guardBindings) {
3870
+ if (test?.type === "LogicalExpression" && test.operator !== "&&") return isBrowserGuardTest(test.left, guardBindings) && isBrowserGuardTest(test.right, guardBindings);
3497
3871
  let guarded = false;
3498
3872
  walkEstree(test, (n) => {
3499
3873
  if (n.type === "Identifier" && guardBindings.has(n.name)) guarded = true;
@@ -3505,8 +3879,9 @@ function isBrowserGuardTest(test, guardBindings) {
3505
3879
  ].includes(n.operator)) {
3506
3880
  const sides = [n.left, n.right];
3507
3881
  const hasTypeofGlobal = sides.some((s) => s?.type === "UnaryExpression" && s.operator === "typeof" && s.argument?.type === "Identifier" && BROWSER_GLOBALS.has(s.argument.name));
3508
- const hasUndefinedString = sides.some((s) => s?.type === "Literal" && s.value === "undefined");
3509
- if (hasTypeofGlobal && hasUndefinedString) guarded = true;
3882
+ const type = sides.find((s) => s?.type === "Literal" && typeof s.value === "string")?.value;
3883
+ const positive = n.operator === "===" || n.operator === "==";
3884
+ if (hasTypeofGlobal && type !== void 0 && (type === "undefined" || positive)) guarded = true;
3510
3885
  }
3511
3886
  });
3512
3887
  return guarded;
@@ -3564,7 +3939,7 @@ function collectBrowserGlobalRefs(program, source, extra) {
3564
3939
  if (typeof n !== "object" || typeof n.type !== "string") return;
3565
3940
  if (EVAL_SCOPE_BOUNDARIES.has(n.type)) return;
3566
3941
  if ((n.type === "IfStatement" || n.type === "ConditionalExpression") && isBrowserGuardTest(n.test, guards)) return;
3567
- if (n.type === "LogicalExpression" && isBrowserGuardTest(n.left, guards)) return;
3942
+ if (n.type === "LogicalExpression" && (n.operator === "&&" ? isBrowserGuardTest(n.left, guards) : n.left?.type === "UnaryExpression" && n.left.operator === "!" && isBrowserGuardTest(n.left.argument, guards))) return;
3568
3943
  const introduced = scopeIntroducedNames(n);
3569
3944
  const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
3570
3945
  switch (n.type) {
@@ -3776,6 +4151,16 @@ function findFalseOptOut(program, source, name) {
3776
4151
  });
3777
4152
  return hit;
3778
4153
  }
4154
+ /** Whether the module exports a binding named `name` in any form, re-exports and unresolved aliases included. */
4155
+ function exportsName(program, name) {
4156
+ return (program.body ?? []).some((stmt) => {
4157
+ if (stmt?.type !== "ExportNamedDeclaration" || stmt.exportKind === "type") return false;
4158
+ const decl = stmt.declaration;
4159
+ if (decl?.type === "VariableDeclaration") return decl.declarations?.some((d) => d?.id?.name === name);
4160
+ if (decl) return decl.id?.name === name;
4161
+ return (stmt.specifiers ?? []).some((s) => s?.exportKind !== "type" && (s?.exported?.name ?? s?.exported?.value) === name);
4162
+ });
4163
+ }
3779
4164
  /**
3780
4165
  * The exported `load` function node (inline `export function load` / `export const
3781
4166
  * load = …`, `satisfies`/`as` unwrapped) or a same-file alias export. Cross-file
@@ -3805,7 +4190,15 @@ function collectAwaits(node, out = []) {
3805
4190
  }
3806
4191
  return out;
3807
4192
  }
4193
+ const MUTATING_METHODS = /* @__PURE__ */ new Set([
4194
+ "push",
4195
+ "unshift",
4196
+ "splice",
4197
+ "add",
4198
+ "set"
4199
+ ]);
3808
4200
  const REDIRECT_NAMES = /* @__PURE__ */ new Set(["redirect"]);
4201
+ const EXIT_NAMES = /* @__PURE__ */ new Set(["redirect", "error"]);
3809
4202
  /**
3810
4203
  * Root-relative `redirect(status, '/…')` targets (correctness/base-path-navigation). Argument 1
3811
4204
  * is the location (argument 0 is the status). Only a plain string literal counts, which
@@ -3825,6 +4218,74 @@ function collectRedirectCalls(node, locals, out = []) {
3825
4218
  }
3826
4219
  return out;
3827
4220
  }
4221
+ /** Whether `node` holds a `return` outside nested functions. */
4222
+ function containsReturn(node) {
4223
+ if (Array.isArray(node)) return node.some(containsReturn);
4224
+ if (!node || typeof node !== "object" || typeof node.type !== "string" || isFunctionNode(node)) return false;
4225
+ if (node.type === "ReturnStatement") return true;
4226
+ return Object.keys(node).some((key) => !WALK_IGNORED_KEYS.has(key) && containsReturn(node[key]));
4227
+ }
4228
+ /**
4229
+ * Whether the exported `load` redirects or errors on every call: every path through its body reaches a
4230
+ * `redirect(…)` / `error(…)` (thrown or not) before any `return` — through both arms of an `if`/`else`,
4231
+ * through a `try` whose `catch` redirects or rethrows, and through a call to a top-level function
4232
+ * of the same file that itself always redirects. A redirect under an `if` without an `else`, in a
4233
+ * loop, or in a function declared inside `load` does not count.
4234
+ */
4235
+ function loadNeverRenders(program, locals) {
4236
+ const load = locals.size > 0 ? findLoadFunction(program) : void 0;
4237
+ if (!load?.body) return false;
4238
+ const topLevel = collectTopLevelBindings(program);
4239
+ const fnRedirects = (fn, seen, observed) => {
4240
+ const shadow = /* @__PURE__ */ new Set();
4241
+ for (const p of fn.params ?? []) addBoundNames(p, shadow);
4242
+ const frame = {
4243
+ seen: /* @__PURE__ */ new Set([...seen, fn]),
4244
+ observed
4245
+ };
4246
+ return fn.body?.type === "BlockStatement" ? always(fn.body.body, shadow, void 0, frame) : isRedirect(fn.body, shadow, frame, observed);
4247
+ };
4248
+ const isRedirect = (expr, shadow, frame, returned = false) => {
4249
+ let e = unwrapTs(expr);
4250
+ const awaited = e?.type === "AwaitExpression";
4251
+ if (awaited) e = unwrapTs(e.argument);
4252
+ if (e?.type !== "CallExpression" || e.callee?.type !== "Identifier" || shadow.has(e.callee.name)) return false;
4253
+ if (locals.has(e.callee.name)) return true;
4254
+ const fn = topLevel.get(e.callee.name);
4255
+ if (!isFunctionNode(fn) || fn.generator || frame.seen.has(fn) || fn.async && !awaited && !returned) return false;
4256
+ return fnRedirects(fn, frame.seen, awaited || returned);
4257
+ };
4258
+ const within = (body, outer, param) => {
4259
+ const own = /* @__PURE__ */ new Set();
4260
+ addBoundNames(param, own);
4261
+ for (const stmt of body ?? []) if (stmt?.type === "VariableDeclaration") for (const d of stmt.declarations ?? []) addBoundNames(d?.id, own);
4262
+ else if ((stmt?.type === "FunctionDeclaration" || stmt?.type === "ClassDeclaration") && stmt.id) addBoundNames(stmt.id, own);
4263
+ return own.size === 0 ? outer : /* @__PURE__ */ new Set([...outer, ...own]);
4264
+ };
4265
+ const redirects = (stmt, shadow, rethrow, frame) => {
4266
+ if (stmt?.type === "ExpressionStatement") return isRedirect(stmt.expression, shadow, frame);
4267
+ if (stmt?.type === "ThrowStatement" || stmt?.type === "ReturnStatement") {
4268
+ if (isRedirect(stmt.argument, shadow, frame, stmt.type === "ReturnStatement" && frame.observed)) return true;
4269
+ return stmt.type === "ThrowStatement" && stmt.argument?.type === "Identifier" && stmt.argument.name === rethrow;
4270
+ }
4271
+ if (stmt?.type === "BlockStatement") return always(stmt.body, shadow, rethrow, frame);
4272
+ if (stmt?.type === "IfStatement") return !!stmt.alternate && redirects(stmt.consequent, shadow, rethrow, frame) && redirects(stmt.alternate, shadow, rethrow, frame);
4273
+ if (stmt?.type === "TryStatement") {
4274
+ const param = stmt.handler?.param?.type === "Identifier" ? stmt.handler.param.name : void 0;
4275
+ return always(stmt.block?.body, shadow, void 0, frame) && (!stmt.handler || always(stmt.handler.body?.body, within([], shadow, stmt.handler.param), param, frame)) && !containsReturn(stmt.finalizer);
4276
+ }
4277
+ return false;
4278
+ };
4279
+ const always = (body, outer, rethrow, frame) => {
4280
+ const shadow = within(body, outer);
4281
+ for (const stmt of body ?? []) {
4282
+ if (redirects(stmt, shadow, rethrow, frame)) return true;
4283
+ if (containsReturn(stmt)) return false;
4284
+ }
4285
+ return false;
4286
+ };
4287
+ return fnRedirects(load, /* @__PURE__ */ new Set(), true);
4288
+ }
3828
4289
  /**
3829
4290
  * Whether `await`'s argument is a `parent()` / `<x>.parent()` call (Kit's parent-load
3830
4291
  * step, exempt from performance/load-waterfall and performance/sequential-awaits). Any `<expr>.parent()` member call matches — over-broad
@@ -3857,6 +4318,26 @@ function isBodyParseCall(arg) {
3857
4318
  const callee = e.callee;
3858
4319
  return callee?.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && BODY_METHODS.has(callee.property.name);
3859
4320
  }
4321
+ const WORK_STARTING = /* @__PURE__ */ new Set([
4322
+ "CallExpression",
4323
+ "NewExpression",
4324
+ "ImportExpression",
4325
+ "TaggedTemplateExpression"
4326
+ ]);
4327
+ /** Whether an await argument starts work (a call, tagged template, `new`, or `import()`), as opposed to awaiting an existing promise. */
4328
+ function startsWork(arg) {
4329
+ let e = unwrapTs(arg);
4330
+ if (e?.type === "ChainExpression") e = e.expression;
4331
+ return WORK_STARTING.has(e?.type);
4332
+ }
4333
+ /** Whether evaluating `node` may start work: a work-starting expression anywhere outside a nested function. */
4334
+ function containsWork(node) {
4335
+ if (Array.isArray(node)) return node.some(containsWork);
4336
+ if (!node || typeof node !== "object" || typeof node.type !== "string") return false;
4337
+ if (isFunctionNode(node)) return false;
4338
+ if (WORK_STARTING.has(node.type)) return true;
4339
+ return Object.keys(node).some((key) => !WALK_IGNORED_KEYS.has(key) && containsWork(node[key]));
4340
+ }
3860
4341
  /**
3861
4342
  * Whether the expression references any tainted name. Threads nested-function
3862
4343
  * shadowing (`scopeIntroducedNames`) so a callback parameter that shadows a
@@ -3889,15 +4370,36 @@ function refsTainted(node, tainted) {
3889
4370
  return hit;
3890
4371
  }
3891
4372
  /**
4373
+ * Awaits that only run when a tainted value says so — `a ?? await x`, `t ? await x : y` — and so
4374
+ * cannot start before the earlier await that decides them.
4375
+ */
4376
+ function guardedAwaits(node, tainted, out = /* @__PURE__ */ new Set()) {
4377
+ if (Array.isArray(node)) {
4378
+ for (const child of node) guardedAwaits(child, tainted, out);
4379
+ return out;
4380
+ }
4381
+ if (!node || typeof node !== "object" || typeof node.type !== "string" || isFunctionNode(node)) return out;
4382
+ if (node.type === "LogicalExpression" && refsTainted(node.left, tainted)) for (const a of collectAwaits(node.right)) out.add(a);
4383
+ else if (node.type === "ConditionalExpression" && refsTainted(node.test, tainted)) for (const a of collectAwaits([node.consequent, node.alternate])) out.add(a);
4384
+ for (const key of Object.keys(node)) {
4385
+ if (WALK_IGNORED_KEYS.has(key)) continue;
4386
+ guardedAwaits(node[key], tainted, out);
4387
+ }
4388
+ return out;
4389
+ }
4390
+ /**
3892
4391
  * performance/load-waterfall, performance/sequential-awaits — forward-taint analysis of the exported `load`'s straight-line
3893
4392
  * statements (direct `try` blocks inlined; `if`/loops/`switch` are not classified
3894
4393
  * but still propagate taint from their assignments; nested functions are never
3895
4394
  * entered). One await site per statement; a site whose awaits' argument subtrees
3896
4395
  * reference an earlier site's bindings (transitively, through intermediate consts
3897
- * and assignments — member-expression targets taint their root object) is
3898
- * dependent, anchored at the first dependent await; otherwise independent when a
3899
- * prior site exists, unless every await merely resumes an already-created promise
3900
- * (a bare identifier argument starts no request). `await parent()` and
4396
+ * and assignments — member-expression targets taint their root object), or that a
4397
+ * tainted `??`/`&&`/`||`/`?:` guard decides (`guardedAwaits`), is
4398
+ * dependent, anchored at the first dependent await — unless it starts no work and its
4399
+ * promise came straight from an await result (`await deferred.state` after
4400
+ * `await parent()`); otherwise independent when a prior site exists, unless every
4401
+ * await merely resumes an already-created promise (a bare identifier argument starts
4402
+ * no request). `await parent()` and
3901
4403
  * response-body reads are never sites, but their bindings taint. Lines are
3902
4404
  * returned in ORIGINAL-source coordinates (the −1 wrap shift is applied here).
3903
4405
  */
@@ -3911,12 +4413,27 @@ function collectLoadWaterfalls(program, wrapped) {
3911
4413
  };
3912
4414
  const line = (start) => Math.max(0, lineOf(wrapped, start) - 1);
3913
4415
  const tainted = /* @__PURE__ */ new Set();
4416
+ const syncTainted = /* @__PURE__ */ new Set();
3914
4417
  let sawAwaitSite = false;
3915
- const taintAssignTarget = (left) => {
3916
- if (left?.type === "MemberExpression") {
3917
- const root = rootObjectName(left);
3918
- if (root) tainted.add(root);
3919
- } else addBoundNames(left, tainted);
4418
+ const taintTarget = (target, rhs) => {
4419
+ const awaited = collectAwaits(rhs).length > 0;
4420
+ if (!awaited && !refsTainted(rhs, tainted)) return;
4421
+ const names = /* @__PURE__ */ new Set();
4422
+ if (target?.type === "MemberExpression") {
4423
+ const root = rootObjectName(target);
4424
+ if (root) names.add(root);
4425
+ } else addBoundNames(target, names);
4426
+ for (const name of names) {
4427
+ tainted.add(name);
4428
+ if (!awaited && (containsWork(rhs) || refsTainted(rhs, syncTainted))) syncTainted.add(name);
4429
+ }
4430
+ };
4431
+ const taintMutation = (call) => {
4432
+ const callee = unwrapTs(call?.callee);
4433
+ if (call?.type !== "CallExpression" || callee?.type !== "MemberExpression" || callee.computed) return;
4434
+ if (!MUTATING_METHODS.has(callee.property?.name) || !(call.arguments ?? []).some((a) => refsTainted(a, tainted))) return;
4435
+ const root = rootObjectName(callee);
4436
+ if (root) tainted.add(root);
3920
4437
  };
3921
4438
  const taintOnly = (node) => {
3922
4439
  if (Array.isArray(node)) {
@@ -3925,10 +4442,14 @@ function collectLoadWaterfalls(program, wrapped) {
3925
4442
  }
3926
4443
  if (!node || typeof node !== "object" || typeof node.type !== "string") return;
3927
4444
  if (isFunctionNode(node)) return;
3928
- if (node.type === "AssignmentExpression") {
3929
- if (collectAwaits(node.right).length > 0 || refsTainted(node.right, tainted)) taintAssignTarget(node.left);
4445
+ if (node.type === "AssignmentExpression") taintTarget(node.left, node.right);
4446
+ else if (node.type === "CallExpression") taintMutation(node);
4447
+ else if ((node.type === "ForOfStatement" || node.type === "ForInStatement") && refsTainted(node.right, tainted)) {
4448
+ const names = /* @__PURE__ */ new Set();
4449
+ addBoundNames(node.left?.type === "VariableDeclaration" ? node.left.declarations?.[0]?.id : node.left, names);
4450
+ for (const name of names) tainted.add(name);
3930
4451
  } else if (node.type === "VariableDeclaration") {
3931
- for (const d of node.declarations ?? []) if (d?.id && d.init && (collectAwaits(d.init).length > 0 || refsTainted(d.init, tainted))) addBoundNames(d.id, tainted);
4452
+ for (const d of node.declarations ?? []) if (d?.id && d.init) taintTarget(d.id, d.init);
3932
4453
  }
3933
4454
  for (const key of Object.keys(node)) {
3934
4455
  if (WALK_IGNORED_KEYS.has(key)) continue;
@@ -3947,12 +4468,13 @@ function collectLoadWaterfalls(program, wrapped) {
3947
4468
  if (stmt.type === "VariableDeclaration" || stmt.type === "ExpressionStatement" || stmt.type === "ReturnStatement") {
3948
4469
  const sites = collectAwaits(stmt).filter((a) => !isParentCall(a.argument) && !isBodyParseCall(a.argument));
3949
4470
  if (sites.length > 0) {
3950
- const dependent = sites.filter((a) => refsTainted(a.argument, tainted));
4471
+ const guarded = guardedAwaits(stmt, tainted);
4472
+ const dependent = sites.filter((a) => (guarded.has(a) || refsTainted(a.argument, tainted)) && (startsWork(a.argument) || refsTainted(a.argument, syncTainted)));
3951
4473
  if (dependent.length > 0) {
3952
4474
  const anchor = dependent.reduce((m, a) => a.start < m.start ? a : m);
3953
4475
  dependentLines.push(line(anchor.start));
3954
4476
  } else if (sawAwaitSite) {
3955
- const workSites = sites.filter((a) => unwrapTs(a.argument)?.type !== "Identifier");
4477
+ const workSites = sites.filter((a) => startsWork(a.argument));
3956
4478
  if (workSites.length > 0) {
3957
4479
  const anchor = workSites.reduce((m, a) => a.start < m.start ? a : m);
3958
4480
  independentLines.push(line(anchor.start));
@@ -3960,15 +4482,12 @@ function collectLoadWaterfalls(program, wrapped) {
3960
4482
  }
3961
4483
  sawAwaitSite = true;
3962
4484
  }
3963
- if (stmt.type === "VariableDeclaration") for (const d of stmt.declarations ?? []) {
3964
- if (!d?.id || !d.init) continue;
3965
- if (collectAwaits(d.init).length > 0 || refsTainted(d.init, tainted)) addBoundNames(d.id, tainted);
3966
- }
3967
- else if (stmt.type === "ExpressionStatement") {
4485
+ if (stmt.type === "VariableDeclaration") {
4486
+ for (const d of stmt.declarations ?? []) if (d?.id && d.init) taintTarget(d.id, d.init);
4487
+ } else if (stmt.type === "ExpressionStatement") {
3968
4488
  const expr = unwrapTs(stmt.expression);
3969
- if (expr?.type === "AssignmentExpression") {
3970
- if (collectAwaits(expr.right).length > 0 || refsTainted(expr.right, tainted)) taintAssignTarget(expr.left);
3971
- }
4489
+ if (expr?.type === "AssignmentExpression") taintTarget(expr.left, expr.right);
4490
+ else taintMutation(expr);
3972
4491
  }
3973
4492
  } else taintOnly(stmt);
3974
4493
  }
@@ -4005,23 +4524,19 @@ function walkKit(node, handlerFns, startupFns, visit, shadowed = /* @__PURE__ */
4005
4524
  }
4006
4525
  }
4007
4526
  /**
4008
- * Normalize a posix path, resolving `.` and `..` segments — string-only, no I/O.
4009
- * Returns undefined when a `..` segment pops an empty stack, i.e. the path escapes
4010
- * its root (e.g. `../../../../src/lib/server/db` from a shallow route file): the
4011
- * real target lies outside the project tree we can see, so there is no
4012
- * project-relative path to return at all.
4527
+ * Normalize a posix path, resolving `.` and `..` segments — string-only, no I/O. `..` segments
4528
+ * that climb above the root are kept as a leading `../` run (`../../plugins/x`).
4013
4529
  */
4014
4530
  function normalizePosix(path) {
4015
4531
  const out = [];
4016
4532
  for (const seg of path.split("/")) {
4017
4533
  if (seg === "" || seg === ".") continue;
4018
- if (seg === "..") {
4019
- if (out.length === 0) return void 0;
4020
- out.pop();
4021
- } else out.push(seg);
4534
+ if (seg === ".." && out.length > 0 && out[out.length - 1] !== "..") out.pop();
4535
+ else out.push(seg);
4022
4536
  }
4023
4537
  return out.join("/");
4024
4538
  }
4539
+ const escapesRoot = (path) => path === ".." || path.startsWith("../");
4025
4540
  /**
4026
4541
  * What `resolveRepoLocalPath` assumes when no config was read: SvelteKit's own `$lib`, at the
4027
4542
  * default `src/lib`. This single entry is the whole of this analyzer's pre-alias behaviour.
@@ -4048,8 +4563,12 @@ function aliasMatches(entry, spec) {
4048
4563
  * be a project-local module: the caller's `aliases` list decides which non-relative
4049
4564
  * specifiers resolve, defaulting to `$lib` → `src/lib`; `./`/`../` resolve against the
4050
4565
  * importing file's directory; bare packages and other aliases are skipped (they can't
4051
- * be resolved to a project-local path at all). Also undefined when a relative
4052
- * specifier's `..` segments escape the project root (see `normalizePosix`), or when a
4566
+ * be resolved to a project-local path at all). A path above the project root (a leading `../`
4567
+ * run) resolves only inside a directory a `kit.alias` value itself names above the root
4568
+ * (`$plugins: '../../plugins'` in a monorepo app): the user's config says the app builds from
4569
+ * there, so it is read like `src/` — likewise inside a workspace package's directory an entry
4570
+ * names as its `root` — and anywhere else above the root is undefined, as nothing says the app
4571
+ * reaches it. Also undefined when a
4053
4572
  * matched alias's value is itself absolute (e.g. `/opt/shared/src`, or a posixified Windows
4054
4573
  * drive-letter path like `C:/shared/src`): an absolute target is outside the analyzed project by
4055
4574
  * definition, and without this check `normalizePosix` would quietly drop the leading empty
@@ -4058,7 +4577,7 @@ function aliasMatches(entry, spec) {
4058
4577
  * Exported from the package's public barrel because `architecture/private-scope-import`
4059
4578
  * and `architecture/route-component-import` (inside `packages/core`) both need resolution
4060
4579
  * that is not restricted to runes modules, unlike `resolveRunesModuleSpecifier` — and
4061
- * because `resolveComponentPath` (`packages/cli/src/providers/source/resolve.ts`), which
4580
+ * because component resolution (`packages/cli/src/providers/source/resolve.ts`), which
4062
4581
  * drives transitive `<head>`/heading resolution, delegates its alias/`$lib`/relative
4063
4582
  * mapping here too, rather than duplicating it. This is the single site for every
4064
4583
  * repo-local specifier resolution in the repo.
@@ -4070,9 +4589,15 @@ function resolveRepoLocalPath(spec, importerFile, aliases = DEFAULT_KIT_ALIASES)
4070
4589
  const entry = aliases.find((a) => aliasMatches(a, spec));
4071
4590
  if (entry?.replacement == null) return void 0;
4072
4591
  if (entry.replacement.startsWith("/") || /^[A-Za-z]:\//.test(entry.replacement)) return void 0;
4073
- path = entry.replacement + spec.slice(entry.find.length);
4592
+ path = entry.replacement + spec.slice(entry.find.length) + (entry.suffix ?? "");
4074
4593
  }
4075
- return normalizePosix(path);
4594
+ const normalized = normalizePosix(path);
4595
+ if (!escapesRoot(normalized)) return normalized;
4596
+ return aliases.some((a) => {
4597
+ const named = a.root ?? a.replacement;
4598
+ const dir = named === null ? "" : normalizePosix(named);
4599
+ return escapesRoot(dir) && (normalized === dir || normalized.startsWith(`${dir}/`));
4600
+ }) ? normalized : void 0;
4076
4601
  }
4077
4602
  /**
4078
4603
  * Resolve an import specifier to a repo-relative `.svelte.ts`/`.svelte.js` path, or
@@ -4080,7 +4605,7 @@ function resolveRepoLocalPath(spec, importerFile, aliases = DEFAULT_KIT_ALIASES)
4080
4605
  * `aliases` (defaulting to `$lib` → `src/lib` when omitted), so a project's declared
4081
4606
  * `kit.alias`/`kit.files.lib` resolve here exactly as they do at rule time; `./`/`../`
4082
4607
  * resolve against the importing file's directory; bare packages, unmatched aliases, and
4083
- * a relative specifier whose `..` segments escape the repo root are skipped. An
4608
+ * a path above the project root outside every `kit.alias` directory are skipped. An
4084
4609
  * extensionless `…/x.svelte` specifier canonicalises to `….svelte.ts` (security/shared-state-import also
4085
4610
  * tries the `.js` sibling when matching).
4086
4611
  */
@@ -4115,8 +4640,8 @@ function libServerRoot(aliases) {
4115
4640
  * an equivalent relative path, resolving to exactly that directory — importing the
4116
4641
  * index file rather than a named submodule) is exempt too. A specifier that
4117
4642
  * resolves to undefined — a bare package, an alias that can't map to a repo path, or
4118
- * a relative specifier whose `..` segments escape the repo root (see
4119
- * `normalizePosix`) — is conservatively NOT local state: we can't see that file, so
4643
+ * a relative specifier whose `..` segments escape the project root (outside every
4644
+ * `kit.alias` directory, see `resolveRepoLocalPath`) — is conservatively NOT local state: we can't see that file, so
4120
4645
  * we don't flag writes to it. Installed packages (drizzle, redis, @vercel/kv, …) are
4121
4646
  * excluded: `.set()`/`.update()` on those is persistence, not shared-module-state
4122
4647
  * mutation. When the `$lib` entry is opaque (`libServerRoot` returns undefined — an unreadable
@@ -4147,15 +4672,25 @@ const IN_MEMORY_CTORS = /* @__PURE__ */ new Set([
4147
4672
  "WeakMap",
4148
4673
  "WeakSet"
4149
4674
  ]);
4150
- /** Whether an initializer positively identifies an in-memory container. */
4675
+ /**
4676
+ * Whether an initializer positively identifies an in-memory container. An object literal with a
4677
+ * spread is a composite of values this parse cannot see — typically a facade over clients and
4678
+ * imported modules (`{ ...prismaModels, ...handlers }`) — so it counts only when one of its own
4679
+ * properties is itself a container.
4680
+ */
4151
4681
  function isInMemoryInit(init) {
4152
4682
  if (!init) return false;
4153
- if (init.type === "ObjectExpression" || init.type === "ArrayExpression") return true;
4683
+ if (init.type === "ArrayExpression") return true;
4684
+ if (init.type === "ObjectExpression") {
4685
+ const props = init.properties ?? [];
4686
+ if (!props.some((p) => p?.type === "SpreadElement")) return true;
4687
+ return props.some((p) => p?.type === "Property" && isInMemoryInit(p.value));
4688
+ }
4154
4689
  return init.type === "NewExpression" && init.callee?.type === "Identifier" && IN_MEMORY_CTORS.has(init.callee.name);
4155
4690
  }
4156
4691
  /**
4157
4692
  * The exported bindings of a module that are initialized to an in-memory container — an object
4158
- * or array literal, or `new Map`/`Set`/`WeakMap`/`WeakSet`. Used to arbitrate a `.set()` call on
4693
+ * or array literal (a spread literal only as `isInMemoryInit` allows), or `new Map`/`Set`/`WeakMap`/`WeakSet`. Used to arbitrate a `.set()` call on
4159
4694
  * an import from under the `$lib` server root, where the call shape alone cannot separate a
4160
4695
  * hand-rolled store from a database client.
4161
4696
  *
@@ -4204,6 +4739,7 @@ function parseKitModuleFacts(source, filename, aliases) {
4204
4739
  const line = (start) => Math.max(0, lineOf(wrapped, start) - 1);
4205
4740
  const importedSpecifiers = /* @__PURE__ */ new Map();
4206
4741
  const importedNames = /* @__PURE__ */ new Map();
4742
+ const namespaceImports = /* @__PURE__ */ new Set();
4207
4743
  for (const stmt of program.body ?? []) {
4208
4744
  if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type") continue;
4209
4745
  const spec = typeof stmt.source?.value === "string" ? stmt.source.value : "";
@@ -4212,6 +4748,7 @@ function parseKitModuleFacts(source, filename, aliases) {
4212
4748
  if (s?.importKind === "type" || s?.local?.type !== "Identifier") continue;
4213
4749
  names.push(s.local.name);
4214
4750
  importedSpecifiers.set(s.local.name, spec);
4751
+ if (s.type === "ImportNamespaceSpecifier") namespaceImports.add(s.local.name);
4215
4752
  importedNames.set(s.local.name, s.type === "ImportSpecifier" && s.imported?.type === "Identifier" ? s.imported.name : s.local.name);
4216
4753
  }
4217
4754
  if (names.length === 0) continue;
@@ -4309,7 +4846,8 @@ function parseKitModuleFacts(source, filename, aliases) {
4309
4846
  };
4310
4847
  } else if (n.type === "CallExpression" && n.callee?.type === "MemberExpression") {
4311
4848
  const method = n.callee.property?.type === "Identifier" ? n.callee.property.name : void 0;
4312
- if (method === "set" || method === "update") {
4849
+ const nsFunction = n.callee.object?.type === "Identifier" && namespaceImports.has(n.callee.object.name);
4850
+ if ((method === "set" || method === "update") && !nsFunction) {
4313
4851
  const r = importedRoot(n.callee.object);
4314
4852
  const spec = r ? importedSpecifiers.get(r) : void 0;
4315
4853
  if (r && spec !== void 0) {
@@ -4387,7 +4925,9 @@ function parseKitModuleFacts(source, filename, aliases) {
4387
4925
  browserGlobalRefs: byLine(browserGlobalRefs),
4388
4926
  basePathLinks: byLine(basePathLinks),
4389
4927
  ...ssrOptOut ? { ssrDisabled: { line: Math.max(0, ssrOptOut.line - 1) } } : {},
4928
+ ...!ssrOptOut && exportsName(program, "ssr") ? { ssrEnabled: true } : {},
4390
4929
  ...csrOptOut ? { csrDisabled: { line: Math.max(0, csrOptOut.line - 1) } } : {},
4930
+ ...loadNeverRenders(program, collectNamedImportAliases(program, "@sveltejs/kit", EXIT_NAMES)) ? { loadNeverRenders: true } : {},
4391
4931
  ...waterfalls.dependentLines.length > 0 || waterfalls.independentLines.length > 0 ? { loadWaterfalls: waterfalls } : {},
4392
4932
  suppressions
4393
4933
  };
@@ -5609,8 +6149,8 @@ function isRouteEntry(path) {
5609
6149
  /** The route entries this component imports, with the line each import sits on. */
5610
6150
  function routeEntryImports(c, ctx) {
5611
6151
  const out = [];
5612
- for (const { source, line, type } of c.importSpans ?? []) {
5613
- if (type) continue;
6152
+ for (const { source, line, type, named } of c.importSpans ?? []) {
6153
+ if (type || named) continue;
5614
6154
  const target = resolveRepoLocalPath(source, c.file, ctx.project.kitAliases);
5615
6155
  if (target !== void 0 && isRouteEntry(target)) out.push({
5616
6156
  line,
@@ -5956,223 +6496,6 @@ const performanceIframeLoading = componentRule({
5956
6496
  }))
5957
6497
  });
5958
6498
  //#endregion
5959
- //#region src/rules/a11y/aria-data.ts
5960
- /**
5961
- * ARIA names `aria-query@5.3.2` does not carry. It is not the clean ARIA 1.2 snapshot it looks
5962
- * like — it holds 1.2's 48 attributes plus three 1.3 additions — so the gap is patched by name
5963
- * rather than by version. Every entry here is defined in the ARIA 1.3 editor's draft, which is
5964
- * what makes reporting it as an unknown name wrong — how far implementations have got is a
5965
- * separate question this rule does not ask. Re-check this list whenever the aria-query dependency
5966
- * moves.
5967
- */
5968
- const ARIA_1_3_ROLES = /* @__PURE__ */ new Set([
5969
- "comment",
5970
- "image",
5971
- "sectionheader",
5972
- "sectionfooter",
5973
- "suggestion"
5974
- ]);
5975
- const ARIA_1_3_ATTRIBUTES = /* @__PURE__ */ new Set(["aria-colindextext", "aria-rowindextext"]);
5976
- /**
5977
- * Roles whose `requiredProps` row in aria-query is stale. Neither ARIA 1.2 nor the 1.3 editor's
5978
- * draft lists required states or properties for these — the 1.3 draft re-lists inherited
5979
- * requirements where they apply (`menuitemradio` → `aria-checked`), so the absence is deliberate
5980
- * rather than an inheritance the table left implicit. Without this, idiomatic APG listbox and
5981
- * tree markup is flagged.
5982
- */
5983
- const NO_REQUIRED_PROPS = /* @__PURE__ */ new Set(["option", "treeitem"]);
5984
- function isKnownRole(role) {
5985
- return ARIA_1_3_ROLES.has(role) || roles.has(role);
5986
- }
5987
- function isAbstractRole(role) {
5988
- return roles.get(role)?.abstract === true;
5989
- }
5990
- /** A concrete (non-abstract) role is the one a user agent resolves a role token list to. */
5991
- function isConcreteRole(role) {
5992
- return isKnownRole(role) && !isAbstractRole(role);
5993
- }
5994
- /**
5995
- * The role a user agent applies: the first token naming a concrete role, or undefined when none
5996
- * does. Shared so the role rules cannot disagree about what a fallback list means.
5997
- */
5998
- function resolveRole(tokens) {
5999
- return tokens.find(isConcreteRole);
6000
- }
6001
- function isKnownAriaAttribute(name) {
6002
- return ARIA_1_3_ATTRIBUTES.has(name) || aria.has(name);
6003
- }
6004
- function requiredAriaProps(role) {
6005
- if (NO_REQUIRED_PROPS.has(role)) return [];
6006
- const def = roles.get(role);
6007
- return def ? Object.keys(def.requiredProps) : [];
6008
- }
6009
- function ariaValueKind(name) {
6010
- const def = aria.get(name);
6011
- if (!def) return void 0;
6012
- return {
6013
- type: def.type,
6014
- ...def.values ? { values: def.values.map(String) } : {}
6015
- };
6016
- }
6017
- //#endregion
6018
- //#region src/a11y.ts
6019
- /**
6020
- * Branch-aware occurrence fold (design "Control-flow semantics"): within a branch
6021
- * occurrences sum, across the arms of one exclusive block the arm with the most
6022
- * occurrences wins (tie → lowest branch index) and ITS occurrences are the group's
6023
- * representatives — so a caller's count is always `list.length`, with a location per
6024
- * representative. `{#each}`/`{#snippet}` occurrences render 0..N times and drop out.
6025
- * The max is per key: there is no scalar total to maximize.
6026
- */
6027
- function foldOccurrences(nodes) {
6028
- const byKey = /* @__PURE__ */ new Map();
6029
- for (const node of nodes) {
6030
- if (node.repeatable) continue;
6031
- const list = byKey.get(node.key);
6032
- if (list) list.push(node);
6033
- else byKey.set(node.key, [node]);
6034
- }
6035
- const folded = /* @__PURE__ */ new Map();
6036
- for (const [key, list] of byKey) folded.set(key, foldAt(list, 0));
6037
- return folded;
6038
- }
6039
- function foldAt(nodes, depth) {
6040
- const unconditional = [];
6041
- const groups = /* @__PURE__ */ new Map();
6042
- for (const node of nodes) {
6043
- const step = node.path[depth];
6044
- if (!step) {
6045
- unconditional.push(node);
6046
- continue;
6047
- }
6048
- let branches = groups.get(step.group);
6049
- if (!branches) groups.set(step.group, branches = /* @__PURE__ */ new Map());
6050
- const list = branches.get(step.branch);
6051
- if (list) list.push(node);
6052
- else branches.set(step.branch, [node]);
6053
- }
6054
- const representatives = [...unconditional];
6055
- for (const branches of groups.values()) {
6056
- let best = [];
6057
- let bestBranch = Number.POSITIVE_INFINITY;
6058
- for (const [branch, list] of branches) {
6059
- const arm = foldAt(list, depth + 1);
6060
- if (arm.length > best.length || arm.length === best.length && branch < bestBranch) {
6061
- best = arm;
6062
- bestBranch = branch;
6063
- }
6064
- }
6065
- representatives.push(...best);
6066
- }
6067
- return representatives;
6068
- }
6069
- /**
6070
- * Decode a fragment identifier the way navigation does before matching an element id
6071
- * (`href="#caf%C3%A9"` targets `id="café"`). Malformed escapes are kept verbatim —
6072
- * the browser would also fail to decode them, so the raw text is the comparable form.
6073
- */
6074
- function decodeFragmentId(fragment) {
6075
- try {
6076
- return decodeURIComponent(fragment);
6077
- } catch {
6078
- return fragment;
6079
- }
6080
- }
6081
- /** Whitespace-split tokens of a (possibly undefined) literal attribute value. */
6082
- function splitTokens(value) {
6083
- return value ? value.trim().split(/\s+/).filter(Boolean) : [];
6084
- }
6085
- /** Explicit `role` values that map to the landmark kinds the route rules inspect. */
6086
- const LANDMARK_ROLES = /* @__PURE__ */ new Set([
6087
- "main",
6088
- "banner",
6089
- "contentinfo",
6090
- "complementary"
6091
- ]);
6092
- /** Sectioning content (HTML-AAM): a `<header>`/`<footer>` below one of these is not a landmark. */
6093
- const SECTIONING_TAGS = /* @__PURE__ */ new Set([
6094
- "article",
6095
- "aside",
6096
- "main",
6097
- "nav",
6098
- "section"
6099
- ]);
6100
- /**
6101
- * Tags whose landmark-ness depends on sectioning ancestry. A provider that cannot see ancestry
6102
- * (the per-file AST walk) uses this same set to mark the deferral (topLevel) — reading it from
6103
- * here keeps the deferral set from drifting when the policy widens.
6104
- */
6105
- const ANCESTRY_DEPENDENT_TAGS = /* @__PURE__ */ new Set(["header", "footer"]);
6106
- /** Attributes that give an element the accessible name the `<aside>` landmark decision reads. */
6107
- const NAMING_ATTRS$1 = ["aria-label", "aria-labelledby"];
6108
- /**
6109
- * HTML-AAM: an `<aside>` scoped to `body` or `main` is a `complementary` landmark; scoped to
6110
- * sectioning content it is one only when it has an accessible name. `main` is deliberately absent
6111
- * — it is a scope in which an `aside` *is* a landmark, unlike the sectioning set that decides
6112
- * `<header>`/`<footer>`.
6113
- */
6114
- const ASIDE_DEMOTING_TAGS = /* @__PURE__ */ new Set([
6115
- "article",
6116
- "aside",
6117
- "nav",
6118
- "section"
6119
- ]);
6120
- /**
6121
- * The landmark kind an element contributes ('main'|'banner'|'contentinfo'|'complementary'), or
6122
- * undefined. The one home for the decision both providers apply — the source AST walk and the
6123
- * rendered HTML walk each feed it their own tree's context. ARIA fallback role lists resolve to
6124
- * the first token naming a concrete role (resolveRole).
6125
- */
6126
- function resolveLandmark(input) {
6127
- const { tag, roleTokens, named, insideSectioning, insideAsideDemoting } = input;
6128
- if (roleTokens !== void 0) {
6129
- const role = resolveRole(roleTokens);
6130
- return role && LANDMARK_ROLES.has(role) ? role : void 0;
6131
- }
6132
- if (tag === "main") return "main";
6133
- if (tag !== void 0 && ANCESTRY_DEPENDENT_TAGS.has(tag)) return insideSectioning ? void 0 : tag === "header" ? "banner" : "contentinfo";
6134
- if (tag === "aside") return !insideAsideDemoting || named ? "complementary" : void 0;
6135
- }
6136
- /**
6137
- * Attributes whose (whitespace-tokenized) values reference element ids: the ARIA id-reference and
6138
- * id-reference-list properties, and HTML's own (`for`, `list`, `headers`, `form`, the popover and
6139
- * command targets). `href="#…"` is handled separately — its value is a URL, not a token list.
6140
- */
6141
- const IDREF_ATTRS = [
6142
- "for",
6143
- "list",
6144
- "headers",
6145
- "form",
6146
- "popovertarget",
6147
- "commandfor",
6148
- "aria-labelledby",
6149
- "aria-describedby",
6150
- "aria-controls",
6151
- "aria-activedescendant",
6152
- "aria-owns",
6153
- "aria-details",
6154
- "aria-errormessage",
6155
- "aria-flowto"
6156
- ];
6157
- /**
6158
- * Whether a decoded URL fragment is HTML's "top of the document" indicator: `#top` (ASCII
6159
- * case-insensitive) scrolls to the top when no element has that id, so it is never a missing
6160
- * reference. Compare AFTER percent-decoding — `#%74op` navigates identically to `#top`.
6161
- */
6162
- function isTopFragment(id) {
6163
- return id.toLowerCase() === "top";
6164
- }
6165
- /**
6166
- * A fragment with its text directive removed. Everything from the first `:~:` on is user-agent
6167
- * instructions for finding text and names no element, while anything before it is still an
6168
- * ordinary element fragment — `#section:~:text=hi` targets `id="section"`, `#:~:text=hi` targets
6169
- * nothing. Returns an empty string when the fragment is a directive and nothing else.
6170
- */
6171
- function stripTextDirective(fragment) {
6172
- const i = fragment.indexOf(":~:");
6173
- return i === -1 ? fragment : fragment.slice(0, i);
6174
- }
6175
- //#endregion
6176
6499
  //#region src/rules/a11y/invalid-role.ts
6177
6500
  const a11yInvalidRole = componentRule({
6178
6501
  id: "a11y/invalid-role",
@@ -6872,7 +7195,7 @@ const ELEMENT_FACT_OVERRIDES = {
6872
7195
  };
6873
7196
  /**
6874
7197
  * The role(s) an `AriaElementFact` may have. Explicit first: a literal role's first concrete token
6875
- * (`resolveRole`); an expression role, or a spread with no literal role, makes the role unknowable
7198
+ * (`resolveRole`); an expression role, or a spread or `use:` action with no literal role, makes the role unknowable
6876
7199
  * and yields `undefined`. Otherwise the element's implicit role — never one role but the set of
6877
7200
  * candidates the dataset's conditional outcomes leave open, since the selectors that decide between
6878
7201
  * them are not evaluated (design 2026-08-19-aria-role-table-rules, "Which role an element has").
@@ -6887,7 +7210,7 @@ function roleCandidates(e) {
6887
7210
  namingProhibited: false
6888
7211
  } : void 0;
6889
7212
  }
6890
- if (e.hasSpread) return void 0;
7213
+ if (e.hasSpread || e.hasAction) return void 0;
6891
7214
  const el = Object.hasOwn(HTML_SPEC.elements, e.tag) ? HTML_SPEC.elements[e.tag] : void 0;
6892
7215
  if (!el) return void 0;
6893
7216
  const aria = (Object.hasOwn(ELEMENT_FACT_OVERRIDES, e.tag) ? ELEMENT_FACT_OVERRIDES[e.tag] : void 0) ?? el.aria;
@@ -7454,7 +7777,12 @@ function surplusRule(spec) {
7454
7777
  first ??= reps[0];
7455
7778
  for (let i = 1; i < reps.length; i++) {
7456
7779
  surplus = true;
7457
- out.push(result(route.route, PENALIZED$1, reps[i], spec.message(key, i, reps.length, reps[0])));
7780
+ const finding = result(route.route, PENALIZED$1, reps[i], spec.message(key, i, reps.length, reps[0]));
7781
+ const recommendation = spec.recommendationFor?.(reps[i], reps[0]);
7782
+ out.push(recommendation ? {
7783
+ ...finding,
7784
+ recommendation
7785
+ } : finding);
7458
7786
  }
7459
7787
  }
7460
7788
  if (first && !surplus) out.push(result(route.route, PASS, {
@@ -7632,6 +7960,7 @@ const a11yIdDuplication = surplusRule({
7632
7960
  recommendation: "Every id in a route should be unique.",
7633
7961
  map: (route) => Object.entries(route.ids).sort(([, a], [, b]) => a[0].file.localeCompare(b[0].file) || a[0].line - b[0].line),
7634
7962
  message: (id, _i, _n, first) => first.file === "src/app.html" ? `Duplicate id "${id}" — also defined by the src/app.html shell (line ${first.line})` : `Duplicate id "${id}"`,
7963
+ recommendationFor: (rep, first) => first.file === "src/app.html" || first.line === 0 ? void 0 : rep.file === first.file && rep.line === first.line ? "This id is hardcoded in a component rendered more than once on this route. Generate it per instance with $props.id() (Svelte 5.20+): `const uid = $props.id();`, then `id=\"{uid}-name\"`." : "Rename one of them. Separate {#if} blocks are treated as able to render together; if these never render at the same time, make them branches of one {#if}…{:else}.",
7635
7964
  passMessage: "No duplicate ids"
7636
7965
  });
7637
7966
  //#endregion
@@ -8299,4 +8628,4 @@ function formatMarkdownReport(results, config, meta) {
8299
8628
  return lines.join("\n");
8300
8629
  }
8301
8630
  //#endregion
8302
- export { SECTIONING_TAGS as $, formatFailedRuleWarning as $n, valueFromNodes as $t, a11yAbbrTitle as A, seoDescriptionLength as An, collectDerivedGuardBindings as At, a11yUseList as B, seoOgUrl as Bn, unwrapTs as Bt, a11yDisallowedElement as C, seoHeadingLevelSkip as Cn, summarize as Cr, parseKitModuleFacts as Ct, a11yDeprecatedElement as D, seoHreflang as Dn, defaultConfig as Dr, addBoundNames as Dt, a11yDeprecatedAttr as E, seoSingleH1 as En, CATEGORIES as Er, WALK_IGNORED_KEYS as Et, isInteractiveContainer as F, seoJsonLdRelativeUrl as Fn, matchLifecycleCall as Ft, a11yInvalidAriaValue as G, isPlainObject as Gn, attrText as Gt, a11yAccessibleName as H, seoTwitterCard as Hn, walkEvalScope as Ht, isInteractiveElement as I, seoJsonLdDeprecatedType as In, parseModuleProgram as It, a11yInvalidRole as J, validateRuleSetting as Jn, attrValueOf as Jt, a11yRequiredAriaProps as K, resolveRuleOptions as Kn, attrTextOf as Kt, literalTabindexValue as L, seoJsonLdValidity as Ln, rootObjectName as Lt, a11yNoAutofocus as M, seoJsonLdRequiredProps as Mn, collectProgramBindings as Mt, a11yNoAccesskey as N, seoJsonLdPlaceholder as Nn, collectSuppressions as Nt, HTML_SPEC as O, seoImageAlt as On, defaultProject as Or, collectBrowserGlobalRefs as Ot, a11yPositiveTabindex as P, seoJsonLdDateFormat as Pn, collectSvelteLifecycleImports as Pt, NAMING_ATTRS$1 as Q, compileOverrides as Qn, textFromNodes as Qt, a11yRequireDatetime as R, seoSitemapInRobots as Rn, scopeIntroducedNames as Rt, a11yPermittedContents as S, seoSsrDisabled as Sn, hasFailureAtOrAbove as Sr, parseInMemoryExports as St, a11yDisallowedAriaProps as T, seoDuplicateTitle as Tn, isPenalized as Tr, isRootRelativePath as Tt, a11yAriaHiddenFocus as U, seoIndexability as Un, walkScoped as Ut, a11yLabelHasControl as V, seoOgDescription as Vn, walkEstree as Vt, a11yInteractiveNesting as W, performancePreconnect as Wn, CHILD_NODE_KEYS as Wt, ASIDE_DEMOTING_TAGS as X, applyOverrides as Xn, lineOf as Xt, ANCESTRY_DEPENDENT_TAGS as Y, SEARCH_RESULT_RULES as Yn, findAttr as Yt, IDREF_ATTRS as Z, applyRuleSeverities as Zn, parseSvelte as Zt, a11yIdDuplication as _, correctnessUnmutatedState as _n, seoCanonicalUrl as _r, architectureReservedDirectoryNames as _t, formatJsonReport as a, securityJavascriptUrl as an, performanceLcpImage as ar, stripTextDirective as at, a11yDoctype as b, correctnessEachIndexKey as bn, classify as br, architecturePrivateScopeImport as bt, formatGithubReport as c, correctnessServerBrowserGlobal as cn, performanceResponsiveImage as cr, performanceStateRaw as ct, ruleMetaById as d, correctnessOrphanEffect as dn, seoHtmlLang as dr, performanceMinifyDisabled as dt, architecturePropCount as en, selectRules as er, decodeFragmentId as et, severityToSarifLevel as f, correctnessAutoplayMuted as fn, seoSitemapXml as fr, performanceNamespaceImport as ft, a11yNoMissingIdRef as g, correctnessPropMutation as gn, seoOgImage as gr, architectureReservedNamePlacement as gt, a11yUnverifiedIdRef as h, correctnessStalePropDerivation as hn, seoOgTitle as hr, architectureRouteComponentImport as ht, buildJsonReport as i, securityHandlerStateWrite as in, performanceRenderBlockingScript as ir, splitTokens as it, a11yNoDuplicateDt as j, seoTitleLength as jn, collectNamedImportAliases as jt, a11yPatternTitle as k, seoCharset as kn, defineConfig as kr, collectBrowserGuardImports as kt, SEVERITY_RANK as l, correctnessBasePathNavigation as ln, performanceImageLoadingHint as lr, performanceSequentialAwaits as lt, explainRule as m, correctnessNonreactiveBuiltinState as mn, seoJsonLd as mr, architectureDocLinkTarget as mt, mdEscape as n, securitySharedStateImport as nn, withFailedRulesOff as nr, isTopFragment as nt, computeHealth as o, securityRawHtml as on, performanceFontPreloadCrossorigin as or, resolveRole as ot, allRules as p, correctnessCheckableBindValue as pn, seoRobotsTxt as pr, performanceHeavyImport as pt, a11yUnknownAriaAttribute as q, shouldSkipRangeCheck as qn, attrValue as qt, terminalSafe as r, securityServerModuleState as rn, withIndexableOff as rr, resolveLandmark as rt, computeScore as s, correctnessInstanceBrowserGlobal as sn, performancePreloadMissingAs as sr, performanceIframeLoading as st, formatMarkdownReport as t, architectureComponentSize as tn, settingSeverity as tr, foldOccurrences as tt, messageText as u, correctnessOrphanLifecycle as un, performanceImageDimensions as ur, performanceLoadWaterfall as ut, a11yTopLevelLandmark as v, correctnessEffectAsOnMount as vn, seoDescriptionPresence as vr, architectureDirectoryNaming as vt, a11yDeprecatedAria as w, seoDuplicateDescription as wn, docsUrlFor as wr, resolveRepoLocalPath as wt, a11yRequiredElement as x, correctnessEachKey as xn, effectiveSeverity as xr, collectTopLevelBindings as xt, a11yDuplicateLandmark as y, correctnessEffectAsDerived as yn, seoTitlePresence as yr, architectureUnitEntryFile as yt, a11yPlaceholderLabelOption as z, seoViewport as zn, unwrapExport as zt };
8631
+ export { performanceMinifyDisabled as $, applyRuleSeverities as $n, correctnessOrphanLifecycle as $t, a11yAbbrTitle as A, seoImageAlt as An, defaultProject as Ar, walkEstree as At, a11yUseList as B, seoSitemapInRobots as Bn, parseSvelte as Bt, a11yDisallowedElement as C, foldOccurrences as Cn, effectiveSeverity as Cr, collectSvelteLifecycleImports as Ct, a11yDeprecatedElement as D, stripTextDirective as Dn, isPenalized as Dr, scopeIntroducedNames as Dt, a11yDeprecatedAttr as E, splitTokens as En, docsUrlFor as Er, rootObjectName as Et, isInteractiveContainer as F, seoJsonLdPlaceholder as Fn, attrTextOf as Ft, a11yInvalidAriaValue as G, seoIndexability as Gn, securitySharedStateImport as Gt, a11yAccessibleName as H, seoOgUrl as Hn, valueFromNodes as Ht, isInteractiveElement as I, seoJsonLdDateFormat as In, attrValue as It, a11yInvalidRole as J, resolveRuleOptions as Jn, securityJavascriptUrl as Jt, a11yRequiredAriaProps as K, performancePreconnect as Kn, securityServerModuleState as Kt, literalTabindexValue as L, seoJsonLdRelativeUrl as Ln, attrValueOf as Lt, a11yNoAutofocus as M, seoDescriptionLength as Mn, walkScoped as Mt, a11yNoAccesskey as N, seoTitleLength as Nn, CHILD_NODE_KEYS as Nt, HTML_SPEC as O, resolveRole as On, CATEGORIES as Or, unwrapExport as Ot, a11yPositiveTabindex as P, seoJsonLdRequiredProps as Pn, attrText as Pt, performanceLoadWaterfall as Q, applyOverrides as Qn, correctnessBasePathNavigation as Qt, a11yRequireDatetime as R, seoJsonLdDeprecatedType as Rn, findAttr as Rt, a11yPermittedContents as S, decodeFragmentId as Sn, classify as Sr, collectSuppressions as St, a11yDisallowedAriaProps as T, resolveLandmark as Tn, summarize as Tr, parseModuleProgram as Tt, a11yAriaHiddenFocus as U, seoOgDescription as Un, architecturePropCount as Ut, a11yLabelHasControl as V, seoViewport as Vn, textFromNodes as Vt, a11yInteractiveNesting as W, seoTwitterCard as Wn, architectureComponentSize as Wt, performanceStateRaw as X, validateRuleSetting as Xn, correctnessInstanceBrowserGlobal as Xt, performanceIframeLoading as Y, shouldSkipRangeCheck as Yn, securityRawHtml as Yt, performanceSequentialAwaits as Z, SEARCH_RESULT_RULES as Zn, correctnessServerBrowserGlobal as Zt, a11yIdDuplication as _, ANCESTRY_DEPENDENT_TAGS as _n, seoOgTitle as _r, collectBrowserGlobalRefs as _t, formatJsonReport as a, correctnessPropMutation as an, withIndexableOff as ar, architectureReservedDirectoryNames as at, a11yDoctype as b, NAMING_ATTRS$1 as bn, seoDescriptionPresence as br, collectNamedImportAliases as bt, formatGithubReport as c, correctnessEffectAsDerived as cn, performanceFontPreloadCrossorigin as cr, architecturePrivateScopeImport as ct, ruleMetaById as d, seoSsrDisabled as dn, performanceImageLoadingHint as dr, parseInMemoryExports as dt, correctnessOrphanEffect as en, compileOverrides as er, performanceNamespaceImport as et, severityToSarifLevel as f, routeNeverSsr as fn, performanceImageDimensions as fr, parseKitModuleFacts as ft, a11yNoMissingIdRef as g, seoSingleH1 as gn, seoJsonLd as gr, addBoundNames as gt, a11yUnverifiedIdRef as h, seoDuplicateTitle as hn, seoRobotsTxt as hr, WALK_IGNORED_KEYS as ht, buildJsonReport as i, correctnessStalePropDerivation as in, withFailedRulesOff as ir, architectureReservedNamePlacement as it, a11yNoDuplicateDt as j, seoCharset as jn, defineConfig as jr, walkEvalScope as jt, a11yPatternTitle as k, seoHreflang as kn, defaultConfig as kr, unwrapTs as kt, SEVERITY_RANK as l, correctnessEachIndexKey as ln, performancePreloadMissingAs as lr, DEFAULT_KIT_ALIASES as lt, explainRule as m, seoDuplicateDescription as mn, seoSitemapXml as mr, isRootRelativePath as mt, mdEscape as n, correctnessCheckableBindValue as nn, selectRules as nr, architectureDocLinkTarget as nt, computeHealth as o, correctnessUnmutatedState as on, performanceRenderBlockingScript as or, architectureDirectoryNaming as ot, allRules as p, seoHeadingLevelSkip as pn, seoHtmlLang as pr, resolveRepoLocalPath as pt, a11yUnknownAriaAttribute as q, isPlainObject as qn, securityHandlerStateWrite as qt, terminalSafe as r, correctnessNonreactiveBuiltinState as rn, settingSeverity as rr, architectureRouteComponentImport as rt, computeScore as s, correctnessEffectAsOnMount as sn, performanceLcpImage as sr, architectureUnitEntryFile as st, formatMarkdownReport as t, correctnessAutoplayMuted as tn, formatFailedRuleWarning as tr, performanceHeavyImport as tt, messageText as u, correctnessEachKey as un, performanceResponsiveImage as ur, collectTopLevelBindings as ut, a11yTopLevelLandmark as v, ASIDE_DEMOTING_TAGS as vn, seoOgImage as vr, collectBrowserGuardImports as vt, a11yDeprecatedAria as w, isTopFragment as wn, hasFailureAtOrAbove as wr, matchLifecycleCall as wt, a11yRequiredElement as x, SECTIONING_TAGS as xn, seoTitlePresence as xr, collectProgramBindings as xt, a11yDuplicateLandmark as y, IDREF_ATTRS as yn, seoCanonicalUrl as yr, collectDerivedGuardBindings as yt, a11yPlaceholderLabelOption as z, seoJsonLdValidity as zn, lineOf as zt };