@svelte-vitals/core 0.55.0 → 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
@@ -730,9 +746,9 @@ function withFailedRulesOff(config, failedRuleIds) {
730
746
  }
731
747
  /**
732
748
  * The rules that only pay off for a page that appears in a search result, turned off by
733
- * `seo: { indexable: false }`. `seo/single-h1` and `seo/robots-txt` are deliberately absent:
734
- * a primary heading is document structure either way, and a `noindex` meta is only read after
735
- * the page is crawled, so robots.txt still does real work. Design:
749
+ * `seo: { indexable: false }` — plus `seo/indexability`, whose "is this noindex intentional?"
750
+ * the switch answers. Every other SEO rule is deliberately absent, and a test fails on an SEO
751
+ * rule classified neither way. Design:
736
752
  * docs/superpowers/specs/2026-09-21-noindex-seo-rules-design.md.
737
753
  */
738
754
  const SEARCH_RESULT_RULES = [
@@ -746,7 +762,13 @@ const SEARCH_RESULT_RULES = [
746
762
  "seo/sitemap-xml",
747
763
  "seo/sitemap-in-robots",
748
764
  "seo/title-length",
749
- "seo/description-length"
765
+ "seo/description-length",
766
+ "seo/description-presence",
767
+ "seo/duplicate-title",
768
+ "seo/duplicate-description",
769
+ "seo/hreflang",
770
+ "seo/ssr-disabled",
771
+ "seo/indexability"
750
772
  ];
751
773
  /**
752
774
  * Expand `seo: { indexable: false }` into the `'off'` entries a user writes by hand today, so
@@ -1246,7 +1268,7 @@ const seoTwitterCard = headTagRule({
1246
1268
  id: "seo/twitter-card",
1247
1269
  title: "Twitter Card",
1248
1270
  severity: "info",
1249
- match: (t) => t.kind === "meta" && t.name === "twitter:card",
1271
+ match: (t) => t.kind === "meta" && (t.name === "twitter:card" || t.property === "twitter:card"),
1250
1272
  label: "<meta name=\"twitter:card\">",
1251
1273
  recommendation: "Add <meta name=\"twitter:card\" content=\"summary_large_image\"> so X/Twitter renders a rich card.",
1252
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).",
@@ -1263,6 +1285,7 @@ const seoOgDescription = headTagRule({
1263
1285
  title: "Open Graph description",
1264
1286
  severity: "info",
1265
1287
  match: (t) => t.kind === "meta" && t.property === "og:description",
1288
+ misspelled: ogMisspelled("og:description"),
1266
1289
  label: "<meta property=\"og:description\">",
1267
1290
  recommendation: "Add <meta property=\"og:description\">, or set openGraph.description on your meta component.",
1268
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.",
@@ -1279,6 +1302,7 @@ const seoOgUrl = headTagRule({
1279
1302
  title: "Open Graph URL",
1280
1303
  severity: "warning",
1281
1304
  match: (t) => t.kind === "meta" && t.property === "og:url",
1305
+ misspelled: ogMisspelled("og:url"),
1282
1306
  label: "<meta property=\"og:url\">",
1283
1307
  recommendation: "Add <meta property=\"og:url\"> with the canonical URL, or set openGraph.url on your meta component.",
1284
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.",
@@ -2078,6 +2102,223 @@ const seoHreflang = {
2078
2102
  }
2079
2103
  };
2080
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
2081
2322
  //#region src/rules/seo/single-h1.ts
2082
2323
  const docsUrl$6 = docsUrlFor("seo/single-h1");
2083
2324
  const passRecommendation = "Use exactly one <h1> per page for its main topic; demote extra top-level headings to <h2>+.";
@@ -2092,10 +2333,24 @@ const multipleRecommendation = "A single, clear <h1> is the conventional signal
2092
2333
  * signal, but no official source documents a ranking penalty for several (2026-08-09 v1
2093
2334
  * rule-validity review, P2 #11) — so it's flagged as a style nit, not a defect. Exactly
2094
2335
  * one passes. A route whose headings were not collected (channel unset) emits nothing, as does
2095
- * 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
2096
2337
  * global `rules: { 'seo/single-h1': <severity> }` override flattens both arms to one
2097
2338
  * severity (design, `applyRuleSeverities`).
2098
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
+ }
2099
2354
  const seoSingleH1 = {
2100
2355
  id: "seo/single-h1",
2101
2356
  title: "Heading hierarchy",
@@ -2106,7 +2361,7 @@ const seoSingleH1 = {
2106
2361
  async check(ctx) {
2107
2362
  const out = [];
2108
2363
  for (const route of ctx.headings ?? []) {
2109
- const h1 = [...route.headings, ...route.componentHeadings ?? []].filter((h) => h.level === 1);
2364
+ const h1 = renderableH1s([...route.headings, ...route.componentHeadings ?? []]);
2110
2365
  let problem;
2111
2366
  let where = {};
2112
2367
  if (h1.length === 0) {
@@ -2240,6 +2495,13 @@ const seoDuplicateDescription = uniquenessRule({
2240
2495
  rationale: "Duplicate meta descriptions give search engines no per-page summary, so they are often ignored or rewritten."
2241
2496
  });
2242
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
2243
2505
  //#region src/rules/seo/heading-level-skip.ts
2244
2506
  const docsUrl$5 = docsUrlFor("seo/heading-level-skip");
2245
2507
  const recommendation$5 = "Increase heading levels one step at a time (do not jump, e.g. from <h2> straight to <h4>).";
@@ -2260,20 +2522,16 @@ const seoHeadingLevelSkip = {
2260
2522
  const out = [];
2261
2523
  for (const route of ctx.headings ?? []) {
2262
2524
  if (route.headings.length === 0) continue;
2263
- let prev = route.headings[0].level;
2264
2525
  let skip;
2265
- for (let i = 1; i < route.headings.length; i++) {
2526
+ for (let i = 1; i < route.headings.length && !skip; i++) {
2266
2527
  const h = route.headings[i];
2267
- if (h.level > prev + 1) {
2268
- skip = {
2269
- level: h.level,
2270
- prev,
2271
- line: h.line,
2272
- file: h.file
2273
- };
2274
- break;
2275
- }
2276
- 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
+ };
2277
2535
  }
2278
2536
  out.push(skip ? {
2279
2537
  id: "seo/heading-level-skip",
@@ -2403,11 +2661,94 @@ function kitModuleRule(opts) {
2403
2661
  });
2404
2662
  }
2405
2663
  //#endregion
2406
- //#region src/rules/seo/ssr-disabled.ts
2407
- /** 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. */
2408
2666
  const ROOT_LAYOUT_RE = /^src\/routes\/\+layout(\.server)?\.(ts|js)$/;
2409
- /** `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. */
2410
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
2411
2752
  const seoSsrDisabled = kitModuleRule({
2412
2753
  id: "seo/ssr-disabled",
2413
2754
  title: "SSR disabled",
@@ -2749,9 +3090,19 @@ function messageFor(link) {
2749
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'.`;
2750
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'.`;
2751
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
+ }
2752
3103
  /** Emit one file's PASS/PENALIZED results — same shapes as componentRule/kitModuleRule. */
2753
- function emitFile$1(out, file, links, suppressions) {
2754
- 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)));
2755
3106
  if (bad.length === 0) {
2756
3107
  out.push({
2757
3108
  id: ID$8,
@@ -2796,16 +3147,17 @@ const correctnessBasePathNavigation = {
2796
3147
  fix: { ...FIX$2 },
2797
3148
  async check(ctx) {
2798
3149
  if (!ctx.project.kitPathsBase) return [];
3150
+ const base = ctx.project.kitPathsBase.value;
2799
3151
  const out = [];
2800
3152
  for (const c of ctx.components ?? []) {
2801
3153
  const links = c.basePathLinks ?? [];
2802
3154
  if (links.length === 0) continue;
2803
- emitFile$1(out, c.file, links, c.suppressions);
3155
+ emitFile$1(out, c.file, links, c.suppressions, base);
2804
3156
  }
2805
3157
  for (const m of ctx.kitModules ?? []) {
2806
3158
  const links = m.basePathLinks ?? [];
2807
3159
  if (links.length === 0) continue;
2808
- emitFile$1(out, m.file, links, m.suppressions);
3160
+ emitFile$1(out, m.file, links, m.suppressions, base);
2809
3161
  }
2810
3162
  return out;
2811
3163
  }
@@ -2872,7 +3224,8 @@ const correctnessServerBrowserGlobal = {
2872
3224
  })), c.suppressions);
2873
3225
  }
2874
3226
  for (const m of ctx.kitModules ?? []) {
2875
- const refs = m.browserGlobalRefs ?? [];
3227
+ const neverSsr = universalNeverSsr(m, ctx);
3228
+ const refs = (m.browserGlobalRefs ?? []).filter((r) => !(neverSsr && r.inHandler));
2876
3229
  if (refs.length === 0) continue;
2877
3230
  emitFile(out, m.file, refs.map((r) => ({
2878
3231
  line: r.line,
@@ -2891,7 +3244,7 @@ const correctnessInstanceBrowserGlobal = componentRule({
2891
3244
  label: "Server-safe component init",
2892
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).",
2893
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.",
2894
- 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),
2895
3248
  bad: (c) => (c.browserGlobalRefs ?? []).filter((r) => r.context === "instance").map((r) => ({
2896
3249
  line: r.line,
2897
3250
  message: `${r.name} is accessed during component initialisation — during SSR this runs on the server, where ${r.name} is not defined`
@@ -2937,7 +3290,7 @@ const securityHandlerStateWrite = kitModuleRule({
2937
3290
  label: "Load/handler purity",
2938
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.",
2939
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.",
2940
- applies: (m) => m.importedStateWrites.length > 0 && !(m.kind === "universal" && m.ssrDisabled),
3293
+ applies: (m, ctx) => m.importedStateWrites.length > 0 && !universalNeverSsr(m, ctx),
2941
3294
  bad: (m) => m.importedStateWrites.map((w) => ({
2942
3295
  line: w.line,
2943
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`
@@ -2952,7 +3305,7 @@ const securityServerModuleState = kitModuleRule({
2952
3305
  label: "Server module state",
2953
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.",
2954
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.",
2955
- applies: (m) => m.moduleStateReassignments.length > 0,
3308
+ applies: (m, ctx) => m.moduleStateReassignments.length > 0 && !universalNeverSsr(m, ctx),
2956
3309
  bad: (m) => m.moduleStateReassignments.map((r) => ({
2957
3310
  line: r.line,
2958
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`
@@ -2971,7 +3324,7 @@ const securitySharedStateImport = kitModuleRule({
2971
3324
  label: "Server state imports",
2972
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.",
2973
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.",
2974
- applies: (m) => m.runesModuleImports.length > 0 && !(m.kind === "universal" && m.ssrDisabled),
3327
+ applies: (m, ctx) => m.runesModuleImports.length > 0 && !universalNeverSsr(m, ctx),
2975
3328
  bad: (m, ctx) => {
2976
3329
  const stateFiles = new Set((ctx.components ?? []).filter((c) => c.moduleStateDecls.length > 0).map((c) => c.file));
2977
3330
  const writtenOutside = new Set(m.importedStateWritesOutsideHandlers.map((w) => w.name));
@@ -3252,7 +3605,8 @@ function rootObjectName(node) {
3252
3605
  * `then`/`catch` value/error bindings, and a fragment's own `{@const ...}` /
3253
3606
  * `{let ...}` / `{const ...}` declaration tags (attributed to the enclosing Fragment,
3254
3607
  * shadowing the whole fragment like a block's `let` — a write to one of these
3255
- * 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.
3256
3610
  * Used by `walkScoped` so a write/mutation detector
3257
3611
  * doesn't misattribute a write to one of these locals as a write to an outer `$state`/prop
3258
3612
  * of the same name (issue #140 — originally a deliberately partial mitigation that left
@@ -3278,7 +3632,20 @@ function scopeIntroducedNames(node) {
3278
3632
  else if (node.type === "AwaitBlock") {
3279
3633
  if (node.value) addBoundNames(node.value, introduced);
3280
3634
  if (node.error) addBoundNames(node.error, introduced);
3281
- } 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") {
3282
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);
3283
3650
  }
3284
3651
  return introduced;
@@ -3334,11 +3701,21 @@ function walkEvalScope(node, visit, shadowed = /* @__PURE__ */ new Set(), bounda
3334
3701
  const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
3335
3702
  if (visit(node, scope)) return;
3336
3703
  if (boundaries.has(node.type)) return;
3704
+ const outer = introduced.size > 0 ? outerScopeKeys(node) : void 0;
3337
3705
  for (const key of Object.keys(node)) {
3338
3706
  if (WALK_IGNORED_KEYS.has(key)) continue;
3339
- 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);
3340
3710
  }
3341
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"]);
3342
3719
  /**
3343
3720
  * Unwrap a top-level statement's `export`/`export default` wrapper to the declaration
3344
3721
  * (or expression) it wraps; a non-export statement is returned as-is. Used so pattern 2
@@ -3443,12 +3820,13 @@ function collectNamedImportAliases(program, moduleSource, names) {
3443
3820
  }
3444
3821
  const BROWSER_GUARD_NAMES = /* @__PURE__ */ new Set(["browser"]);
3445
3822
  /**
3446
- * 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) —
3447
3825
  * the guard binding recognised by the browser-global scanner (correctness/server-browser-global, correctness/instance-browser-global).
3448
3826
  * Shared with the Kit-module parser.
3449
3827
  */
3450
3828
  function collectBrowserGuardImports(program) {
3451
- 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)]);
3452
3830
  }
3453
3831
  /**
3454
3832
  * Names bound at the program's top level: every import's local name plus every
@@ -3484,10 +3862,12 @@ function guardTerminates(consequent) {
3484
3862
  /**
3485
3863
  * Whether a guard-clause test establishes a browser environment: it references the
3486
3864
  * `$app/environment` `browser` binding, or contains a
3487
- * `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).
3488
3867
  * Over-matching here only widens the skip — a conservative miss, never a false positive.
3489
3868
  */
3490
3869
  function isBrowserGuardTest(test, guardBindings) {
3870
+ if (test?.type === "LogicalExpression" && test.operator !== "&&") return isBrowserGuardTest(test.left, guardBindings) && isBrowserGuardTest(test.right, guardBindings);
3491
3871
  let guarded = false;
3492
3872
  walkEstree(test, (n) => {
3493
3873
  if (n.type === "Identifier" && guardBindings.has(n.name)) guarded = true;
@@ -3499,8 +3879,9 @@ function isBrowserGuardTest(test, guardBindings) {
3499
3879
  ].includes(n.operator)) {
3500
3880
  const sides = [n.left, n.right];
3501
3881
  const hasTypeofGlobal = sides.some((s) => s?.type === "UnaryExpression" && s.operator === "typeof" && s.argument?.type === "Identifier" && BROWSER_GLOBALS.has(s.argument.name));
3502
- const hasUndefinedString = sides.some((s) => s?.type === "Literal" && s.value === "undefined");
3503
- 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;
3504
3885
  }
3505
3886
  });
3506
3887
  return guarded;
@@ -3558,7 +3939,7 @@ function collectBrowserGlobalRefs(program, source, extra) {
3558
3939
  if (typeof n !== "object" || typeof n.type !== "string") return;
3559
3940
  if (EVAL_SCOPE_BOUNDARIES.has(n.type)) return;
3560
3941
  if ((n.type === "IfStatement" || n.type === "ConditionalExpression") && isBrowserGuardTest(n.test, guards)) return;
3561
- 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;
3562
3943
  const introduced = scopeIntroducedNames(n);
3563
3944
  const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
3564
3945
  switch (n.type) {
@@ -3770,6 +4151,16 @@ function findFalseOptOut(program, source, name) {
3770
4151
  });
3771
4152
  return hit;
3772
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
+ }
3773
4164
  /**
3774
4165
  * The exported `load` function node (inline `export function load` / `export const
3775
4166
  * load = …`, `satisfies`/`as` unwrapped) or a same-file alias export. Cross-file
@@ -3799,7 +4190,15 @@ function collectAwaits(node, out = []) {
3799
4190
  }
3800
4191
  return out;
3801
4192
  }
4193
+ const MUTATING_METHODS = /* @__PURE__ */ new Set([
4194
+ "push",
4195
+ "unshift",
4196
+ "splice",
4197
+ "add",
4198
+ "set"
4199
+ ]);
3802
4200
  const REDIRECT_NAMES = /* @__PURE__ */ new Set(["redirect"]);
4201
+ const EXIT_NAMES = /* @__PURE__ */ new Set(["redirect", "error"]);
3803
4202
  /**
3804
4203
  * Root-relative `redirect(status, '/…')` targets (correctness/base-path-navigation). Argument 1
3805
4204
  * is the location (argument 0 is the status). Only a plain string literal counts, which
@@ -3819,6 +4218,74 @@ function collectRedirectCalls(node, locals, out = []) {
3819
4218
  }
3820
4219
  return out;
3821
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
+ }
3822
4289
  /**
3823
4290
  * Whether `await`'s argument is a `parent()` / `<x>.parent()` call (Kit's parent-load
3824
4291
  * step, exempt from performance/load-waterfall and performance/sequential-awaits). Any `<expr>.parent()` member call matches — over-broad
@@ -3851,6 +4318,26 @@ function isBodyParseCall(arg) {
3851
4318
  const callee = e.callee;
3852
4319
  return callee?.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && BODY_METHODS.has(callee.property.name);
3853
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
+ }
3854
4341
  /**
3855
4342
  * Whether the expression references any tainted name. Threads nested-function
3856
4343
  * shadowing (`scopeIntroducedNames`) so a callback parameter that shadows a
@@ -3883,15 +4370,36 @@ function refsTainted(node, tainted) {
3883
4370
  return hit;
3884
4371
  }
3885
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
+ /**
3886
4391
  * performance/load-waterfall, performance/sequential-awaits — forward-taint analysis of the exported `load`'s straight-line
3887
4392
  * statements (direct `try` blocks inlined; `if`/loops/`switch` are not classified
3888
4393
  * but still propagate taint from their assignments; nested functions are never
3889
4394
  * entered). One await site per statement; a site whose awaits' argument subtrees
3890
4395
  * reference an earlier site's bindings (transitively, through intermediate consts
3891
- * and assignments — member-expression targets taint their root object) is
3892
- * dependent, anchored at the first dependent await; otherwise independent when a
3893
- * prior site exists, unless every await merely resumes an already-created promise
3894
- * (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
3895
4403
  * response-body reads are never sites, but their bindings taint. Lines are
3896
4404
  * returned in ORIGINAL-source coordinates (the −1 wrap shift is applied here).
3897
4405
  */
@@ -3905,12 +4413,27 @@ function collectLoadWaterfalls(program, wrapped) {
3905
4413
  };
3906
4414
  const line = (start) => Math.max(0, lineOf(wrapped, start) - 1);
3907
4415
  const tainted = /* @__PURE__ */ new Set();
4416
+ const syncTainted = /* @__PURE__ */ new Set();
3908
4417
  let sawAwaitSite = false;
3909
- const taintAssignTarget = (left) => {
3910
- if (left?.type === "MemberExpression") {
3911
- const root = rootObjectName(left);
3912
- if (root) tainted.add(root);
3913
- } 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);
3914
4437
  };
3915
4438
  const taintOnly = (node) => {
3916
4439
  if (Array.isArray(node)) {
@@ -3919,10 +4442,14 @@ function collectLoadWaterfalls(program, wrapped) {
3919
4442
  }
3920
4443
  if (!node || typeof node !== "object" || typeof node.type !== "string") return;
3921
4444
  if (isFunctionNode(node)) return;
3922
- if (node.type === "AssignmentExpression") {
3923
- 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);
3924
4451
  } else if (node.type === "VariableDeclaration") {
3925
- 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);
3926
4453
  }
3927
4454
  for (const key of Object.keys(node)) {
3928
4455
  if (WALK_IGNORED_KEYS.has(key)) continue;
@@ -3941,12 +4468,13 @@ function collectLoadWaterfalls(program, wrapped) {
3941
4468
  if (stmt.type === "VariableDeclaration" || stmt.type === "ExpressionStatement" || stmt.type === "ReturnStatement") {
3942
4469
  const sites = collectAwaits(stmt).filter((a) => !isParentCall(a.argument) && !isBodyParseCall(a.argument));
3943
4470
  if (sites.length > 0) {
3944
- 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)));
3945
4473
  if (dependent.length > 0) {
3946
4474
  const anchor = dependent.reduce((m, a) => a.start < m.start ? a : m);
3947
4475
  dependentLines.push(line(anchor.start));
3948
4476
  } else if (sawAwaitSite) {
3949
- const workSites = sites.filter((a) => unwrapTs(a.argument)?.type !== "Identifier");
4477
+ const workSites = sites.filter((a) => startsWork(a.argument));
3950
4478
  if (workSites.length > 0) {
3951
4479
  const anchor = workSites.reduce((m, a) => a.start < m.start ? a : m);
3952
4480
  independentLines.push(line(anchor.start));
@@ -3954,15 +4482,12 @@ function collectLoadWaterfalls(program, wrapped) {
3954
4482
  }
3955
4483
  sawAwaitSite = true;
3956
4484
  }
3957
- if (stmt.type === "VariableDeclaration") for (const d of stmt.declarations ?? []) {
3958
- if (!d?.id || !d.init) continue;
3959
- if (collectAwaits(d.init).length > 0 || refsTainted(d.init, tainted)) addBoundNames(d.id, tainted);
3960
- }
3961
- 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") {
3962
4488
  const expr = unwrapTs(stmt.expression);
3963
- if (expr?.type === "AssignmentExpression") {
3964
- if (collectAwaits(expr.right).length > 0 || refsTainted(expr.right, tainted)) taintAssignTarget(expr.left);
3965
- }
4489
+ if (expr?.type === "AssignmentExpression") taintTarget(expr.left, expr.right);
4490
+ else taintMutation(expr);
3966
4491
  }
3967
4492
  } else taintOnly(stmt);
3968
4493
  }
@@ -3999,23 +4524,19 @@ function walkKit(node, handlerFns, startupFns, visit, shadowed = /* @__PURE__ */
3999
4524
  }
4000
4525
  }
4001
4526
  /**
4002
- * Normalize a posix path, resolving `.` and `..` segments — string-only, no I/O.
4003
- * Returns undefined when a `..` segment pops an empty stack, i.e. the path escapes
4004
- * its root (e.g. `../../../../src/lib/server/db` from a shallow route file): the
4005
- * real target lies outside the project tree we can see, so there is no
4006
- * 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`).
4007
4529
  */
4008
4530
  function normalizePosix(path) {
4009
4531
  const out = [];
4010
4532
  for (const seg of path.split("/")) {
4011
4533
  if (seg === "" || seg === ".") continue;
4012
- if (seg === "..") {
4013
- if (out.length === 0) return void 0;
4014
- out.pop();
4015
- } else out.push(seg);
4534
+ if (seg === ".." && out.length > 0 && out[out.length - 1] !== "..") out.pop();
4535
+ else out.push(seg);
4016
4536
  }
4017
4537
  return out.join("/");
4018
4538
  }
4539
+ const escapesRoot = (path) => path === ".." || path.startsWith("../");
4019
4540
  /**
4020
4541
  * What `resolveRepoLocalPath` assumes when no config was read: SvelteKit's own `$lib`, at the
4021
4542
  * default `src/lib`. This single entry is the whole of this analyzer's pre-alias behaviour.
@@ -4042,8 +4563,12 @@ function aliasMatches(entry, spec) {
4042
4563
  * be a project-local module: the caller's `aliases` list decides which non-relative
4043
4564
  * specifiers resolve, defaulting to `$lib` → `src/lib`; `./`/`../` resolve against the
4044
4565
  * importing file's directory; bare packages and other aliases are skipped (they can't
4045
- * be resolved to a project-local path at all). Also undefined when a relative
4046
- * 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
4047
4572
  * matched alias's value is itself absolute (e.g. `/opt/shared/src`, or a posixified Windows
4048
4573
  * drive-letter path like `C:/shared/src`): an absolute target is outside the analyzed project by
4049
4574
  * definition, and without this check `normalizePosix` would quietly drop the leading empty
@@ -4052,7 +4577,7 @@ function aliasMatches(entry, spec) {
4052
4577
  * Exported from the package's public barrel because `architecture/private-scope-import`
4053
4578
  * and `architecture/route-component-import` (inside `packages/core`) both need resolution
4054
4579
  * that is not restricted to runes modules, unlike `resolveRunesModuleSpecifier` — and
4055
- * because `resolveComponentPath` (`packages/cli/src/providers/source/resolve.ts`), which
4580
+ * because component resolution (`packages/cli/src/providers/source/resolve.ts`), which
4056
4581
  * drives transitive `<head>`/heading resolution, delegates its alias/`$lib`/relative
4057
4582
  * mapping here too, rather than duplicating it. This is the single site for every
4058
4583
  * repo-local specifier resolution in the repo.
@@ -4064,9 +4589,15 @@ function resolveRepoLocalPath(spec, importerFile, aliases = DEFAULT_KIT_ALIASES)
4064
4589
  const entry = aliases.find((a) => aliasMatches(a, spec));
4065
4590
  if (entry?.replacement == null) return void 0;
4066
4591
  if (entry.replacement.startsWith("/") || /^[A-Za-z]:\//.test(entry.replacement)) return void 0;
4067
- path = entry.replacement + spec.slice(entry.find.length);
4592
+ path = entry.replacement + spec.slice(entry.find.length) + (entry.suffix ?? "");
4068
4593
  }
4069
- 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;
4070
4601
  }
4071
4602
  /**
4072
4603
  * Resolve an import specifier to a repo-relative `.svelte.ts`/`.svelte.js` path, or
@@ -4074,7 +4605,7 @@ function resolveRepoLocalPath(spec, importerFile, aliases = DEFAULT_KIT_ALIASES)
4074
4605
  * `aliases` (defaulting to `$lib` → `src/lib` when omitted), so a project's declared
4075
4606
  * `kit.alias`/`kit.files.lib` resolve here exactly as they do at rule time; `./`/`../`
4076
4607
  * resolve against the importing file's directory; bare packages, unmatched aliases, and
4077
- * 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
4078
4609
  * extensionless `…/x.svelte` specifier canonicalises to `….svelte.ts` (security/shared-state-import also
4079
4610
  * tries the `.js` sibling when matching).
4080
4611
  */
@@ -4109,8 +4640,8 @@ function libServerRoot(aliases) {
4109
4640
  * an equivalent relative path, resolving to exactly that directory — importing the
4110
4641
  * index file rather than a named submodule) is exempt too. A specifier that
4111
4642
  * resolves to undefined — a bare package, an alias that can't map to a repo path, or
4112
- * a relative specifier whose `..` segments escape the repo root (see
4113
- * `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
4114
4645
  * we don't flag writes to it. Installed packages (drizzle, redis, @vercel/kv, …) are
4115
4646
  * excluded: `.set()`/`.update()` on those is persistence, not shared-module-state
4116
4647
  * mutation. When the `$lib` entry is opaque (`libServerRoot` returns undefined — an unreadable
@@ -4141,15 +4672,25 @@ const IN_MEMORY_CTORS = /* @__PURE__ */ new Set([
4141
4672
  "WeakMap",
4142
4673
  "WeakSet"
4143
4674
  ]);
4144
- /** 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
+ */
4145
4681
  function isInMemoryInit(init) {
4146
4682
  if (!init) return false;
4147
- 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
+ }
4148
4689
  return init.type === "NewExpression" && init.callee?.type === "Identifier" && IN_MEMORY_CTORS.has(init.callee.name);
4149
4690
  }
4150
4691
  /**
4151
4692
  * The exported bindings of a module that are initialized to an in-memory container — an object
4152
- * 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
4153
4694
  * an import from under the `$lib` server root, where the call shape alone cannot separate a
4154
4695
  * hand-rolled store from a database client.
4155
4696
  *
@@ -4198,6 +4739,7 @@ function parseKitModuleFacts(source, filename, aliases) {
4198
4739
  const line = (start) => Math.max(0, lineOf(wrapped, start) - 1);
4199
4740
  const importedSpecifiers = /* @__PURE__ */ new Map();
4200
4741
  const importedNames = /* @__PURE__ */ new Map();
4742
+ const namespaceImports = /* @__PURE__ */ new Set();
4201
4743
  for (const stmt of program.body ?? []) {
4202
4744
  if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type") continue;
4203
4745
  const spec = typeof stmt.source?.value === "string" ? stmt.source.value : "";
@@ -4206,6 +4748,7 @@ function parseKitModuleFacts(source, filename, aliases) {
4206
4748
  if (s?.importKind === "type" || s?.local?.type !== "Identifier") continue;
4207
4749
  names.push(s.local.name);
4208
4750
  importedSpecifiers.set(s.local.name, spec);
4751
+ if (s.type === "ImportNamespaceSpecifier") namespaceImports.add(s.local.name);
4209
4752
  importedNames.set(s.local.name, s.type === "ImportSpecifier" && s.imported?.type === "Identifier" ? s.imported.name : s.local.name);
4210
4753
  }
4211
4754
  if (names.length === 0) continue;
@@ -4303,7 +4846,8 @@ function parseKitModuleFacts(source, filename, aliases) {
4303
4846
  };
4304
4847
  } else if (n.type === "CallExpression" && n.callee?.type === "MemberExpression") {
4305
4848
  const method = n.callee.property?.type === "Identifier" ? n.callee.property.name : void 0;
4306
- 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) {
4307
4851
  const r = importedRoot(n.callee.object);
4308
4852
  const spec = r ? importedSpecifiers.get(r) : void 0;
4309
4853
  if (r && spec !== void 0) {
@@ -4381,7 +4925,9 @@ function parseKitModuleFacts(source, filename, aliases) {
4381
4925
  browserGlobalRefs: byLine(browserGlobalRefs),
4382
4926
  basePathLinks: byLine(basePathLinks),
4383
4927
  ...ssrOptOut ? { ssrDisabled: { line: Math.max(0, ssrOptOut.line - 1) } } : {},
4928
+ ...!ssrOptOut && exportsName(program, "ssr") ? { ssrEnabled: true } : {},
4384
4929
  ...csrOptOut ? { csrDisabled: { line: Math.max(0, csrOptOut.line - 1) } } : {},
4930
+ ...loadNeverRenders(program, collectNamedImportAliases(program, "@sveltejs/kit", EXIT_NAMES)) ? { loadNeverRenders: true } : {},
4385
4931
  ...waterfalls.dependentLines.length > 0 || waterfalls.independentLines.length > 0 ? { loadWaterfalls: waterfalls } : {},
4386
4932
  suppressions
4387
4933
  };
@@ -5603,8 +6149,8 @@ function isRouteEntry(path) {
5603
6149
  /** The route entries this component imports, with the line each import sits on. */
5604
6150
  function routeEntryImports(c, ctx) {
5605
6151
  const out = [];
5606
- for (const { source, line, type } of c.importSpans ?? []) {
5607
- if (type) continue;
6152
+ for (const { source, line, type, named } of c.importSpans ?? []) {
6153
+ if (type || named) continue;
5608
6154
  const target = resolveRepoLocalPath(source, c.file, ctx.project.kitAliases);
5609
6155
  if (target !== void 0 && isRouteEntry(target)) out.push({
5610
6156
  line,
@@ -5950,223 +6496,6 @@ const performanceIframeLoading = componentRule({
5950
6496
  }))
5951
6497
  });
5952
6498
  //#endregion
5953
- //#region src/rules/a11y/aria-data.ts
5954
- /**
5955
- * ARIA names `aria-query@5.3.2` does not carry. It is not the clean ARIA 1.2 snapshot it looks
5956
- * like — it holds 1.2's 48 attributes plus three 1.3 additions — so the gap is patched by name
5957
- * rather than by version. Every entry here is defined in the ARIA 1.3 editor's draft, which is
5958
- * what makes reporting it as an unknown name wrong — how far implementations have got is a
5959
- * separate question this rule does not ask. Re-check this list whenever the aria-query dependency
5960
- * moves.
5961
- */
5962
- const ARIA_1_3_ROLES = /* @__PURE__ */ new Set([
5963
- "comment",
5964
- "image",
5965
- "sectionheader",
5966
- "sectionfooter",
5967
- "suggestion"
5968
- ]);
5969
- const ARIA_1_3_ATTRIBUTES = /* @__PURE__ */ new Set(["aria-colindextext", "aria-rowindextext"]);
5970
- /**
5971
- * Roles whose `requiredProps` row in aria-query is stale. Neither ARIA 1.2 nor the 1.3 editor's
5972
- * draft lists required states or properties for these — the 1.3 draft re-lists inherited
5973
- * requirements where they apply (`menuitemradio` → `aria-checked`), so the absence is deliberate
5974
- * rather than an inheritance the table left implicit. Without this, idiomatic APG listbox and
5975
- * tree markup is flagged.
5976
- */
5977
- const NO_REQUIRED_PROPS = /* @__PURE__ */ new Set(["option", "treeitem"]);
5978
- function isKnownRole(role) {
5979
- return ARIA_1_3_ROLES.has(role) || roles.has(role);
5980
- }
5981
- function isAbstractRole(role) {
5982
- return roles.get(role)?.abstract === true;
5983
- }
5984
- /** A concrete (non-abstract) role is the one a user agent resolves a role token list to. */
5985
- function isConcreteRole(role) {
5986
- return isKnownRole(role) && !isAbstractRole(role);
5987
- }
5988
- /**
5989
- * The role a user agent applies: the first token naming a concrete role, or undefined when none
5990
- * does. Shared so the role rules cannot disagree about what a fallback list means.
5991
- */
5992
- function resolveRole(tokens) {
5993
- return tokens.find(isConcreteRole);
5994
- }
5995
- function isKnownAriaAttribute(name) {
5996
- return ARIA_1_3_ATTRIBUTES.has(name) || aria.has(name);
5997
- }
5998
- function requiredAriaProps(role) {
5999
- if (NO_REQUIRED_PROPS.has(role)) return [];
6000
- const def = roles.get(role);
6001
- return def ? Object.keys(def.requiredProps) : [];
6002
- }
6003
- function ariaValueKind(name) {
6004
- const def = aria.get(name);
6005
- if (!def) return void 0;
6006
- return {
6007
- type: def.type,
6008
- ...def.values ? { values: def.values.map(String) } : {}
6009
- };
6010
- }
6011
- //#endregion
6012
- //#region src/a11y.ts
6013
- /**
6014
- * Branch-aware occurrence fold (design "Control-flow semantics"): within a branch
6015
- * occurrences sum, across the arms of one exclusive block the arm with the most
6016
- * occurrences wins (tie → lowest branch index) and ITS occurrences are the group's
6017
- * representatives — so a caller's count is always `list.length`, with a location per
6018
- * representative. `{#each}`/`{#snippet}` occurrences render 0..N times and drop out.
6019
- * The max is per key: there is no scalar total to maximize.
6020
- */
6021
- function foldOccurrences(nodes) {
6022
- const byKey = /* @__PURE__ */ new Map();
6023
- for (const node of nodes) {
6024
- if (node.repeatable) continue;
6025
- const list = byKey.get(node.key);
6026
- if (list) list.push(node);
6027
- else byKey.set(node.key, [node]);
6028
- }
6029
- const folded = /* @__PURE__ */ new Map();
6030
- for (const [key, list] of byKey) folded.set(key, foldAt(list, 0));
6031
- return folded;
6032
- }
6033
- function foldAt(nodes, depth) {
6034
- const unconditional = [];
6035
- const groups = /* @__PURE__ */ new Map();
6036
- for (const node of nodes) {
6037
- const step = node.path[depth];
6038
- if (!step) {
6039
- unconditional.push(node);
6040
- continue;
6041
- }
6042
- let branches = groups.get(step.group);
6043
- if (!branches) groups.set(step.group, branches = /* @__PURE__ */ new Map());
6044
- const list = branches.get(step.branch);
6045
- if (list) list.push(node);
6046
- else branches.set(step.branch, [node]);
6047
- }
6048
- const representatives = [...unconditional];
6049
- for (const branches of groups.values()) {
6050
- let best = [];
6051
- let bestBranch = Number.POSITIVE_INFINITY;
6052
- for (const [branch, list] of branches) {
6053
- const arm = foldAt(list, depth + 1);
6054
- if (arm.length > best.length || arm.length === best.length && branch < bestBranch) {
6055
- best = arm;
6056
- bestBranch = branch;
6057
- }
6058
- }
6059
- representatives.push(...best);
6060
- }
6061
- return representatives;
6062
- }
6063
- /**
6064
- * Decode a fragment identifier the way navigation does before matching an element id
6065
- * (`href="#caf%C3%A9"` targets `id="café"`). Malformed escapes are kept verbatim —
6066
- * the browser would also fail to decode them, so the raw text is the comparable form.
6067
- */
6068
- function decodeFragmentId(fragment) {
6069
- try {
6070
- return decodeURIComponent(fragment);
6071
- } catch {
6072
- return fragment;
6073
- }
6074
- }
6075
- /** Whitespace-split tokens of a (possibly undefined) literal attribute value. */
6076
- function splitTokens(value) {
6077
- return value ? value.trim().split(/\s+/).filter(Boolean) : [];
6078
- }
6079
- /** Explicit `role` values that map to the landmark kinds the route rules inspect. */
6080
- const LANDMARK_ROLES = /* @__PURE__ */ new Set([
6081
- "main",
6082
- "banner",
6083
- "contentinfo",
6084
- "complementary"
6085
- ]);
6086
- /** Sectioning content (HTML-AAM): a `<header>`/`<footer>` below one of these is not a landmark. */
6087
- const SECTIONING_TAGS = /* @__PURE__ */ new Set([
6088
- "article",
6089
- "aside",
6090
- "main",
6091
- "nav",
6092
- "section"
6093
- ]);
6094
- /**
6095
- * Tags whose landmark-ness depends on sectioning ancestry. A provider that cannot see ancestry
6096
- * (the per-file AST walk) uses this same set to mark the deferral (topLevel) — reading it from
6097
- * here keeps the deferral set from drifting when the policy widens.
6098
- */
6099
- const ANCESTRY_DEPENDENT_TAGS = /* @__PURE__ */ new Set(["header", "footer"]);
6100
- /** Attributes that give an element the accessible name the `<aside>` landmark decision reads. */
6101
- const NAMING_ATTRS$1 = ["aria-label", "aria-labelledby"];
6102
- /**
6103
- * HTML-AAM: an `<aside>` scoped to `body` or `main` is a `complementary` landmark; scoped to
6104
- * sectioning content it is one only when it has an accessible name. `main` is deliberately absent
6105
- * — it is a scope in which an `aside` *is* a landmark, unlike the sectioning set that decides
6106
- * `<header>`/`<footer>`.
6107
- */
6108
- const ASIDE_DEMOTING_TAGS = /* @__PURE__ */ new Set([
6109
- "article",
6110
- "aside",
6111
- "nav",
6112
- "section"
6113
- ]);
6114
- /**
6115
- * The landmark kind an element contributes ('main'|'banner'|'contentinfo'|'complementary'), or
6116
- * undefined. The one home for the decision both providers apply — the source AST walk and the
6117
- * rendered HTML walk each feed it their own tree's context. ARIA fallback role lists resolve to
6118
- * the first token naming a concrete role (resolveRole).
6119
- */
6120
- function resolveLandmark(input) {
6121
- const { tag, roleTokens, named, insideSectioning, insideAsideDemoting } = input;
6122
- if (roleTokens !== void 0) {
6123
- const role = resolveRole(roleTokens);
6124
- return role && LANDMARK_ROLES.has(role) ? role : void 0;
6125
- }
6126
- if (tag === "main") return "main";
6127
- if (tag !== void 0 && ANCESTRY_DEPENDENT_TAGS.has(tag)) return insideSectioning ? void 0 : tag === "header" ? "banner" : "contentinfo";
6128
- if (tag === "aside") return !insideAsideDemoting || named ? "complementary" : void 0;
6129
- }
6130
- /**
6131
- * Attributes whose (whitespace-tokenized) values reference element ids: the ARIA id-reference and
6132
- * id-reference-list properties, and HTML's own (`for`, `list`, `headers`, `form`, the popover and
6133
- * command targets). `href="#…"` is handled separately — its value is a URL, not a token list.
6134
- */
6135
- const IDREF_ATTRS = [
6136
- "for",
6137
- "list",
6138
- "headers",
6139
- "form",
6140
- "popovertarget",
6141
- "commandfor",
6142
- "aria-labelledby",
6143
- "aria-describedby",
6144
- "aria-controls",
6145
- "aria-activedescendant",
6146
- "aria-owns",
6147
- "aria-details",
6148
- "aria-errormessage",
6149
- "aria-flowto"
6150
- ];
6151
- /**
6152
- * Whether a decoded URL fragment is HTML's "top of the document" indicator: `#top` (ASCII
6153
- * case-insensitive) scrolls to the top when no element has that id, so it is never a missing
6154
- * reference. Compare AFTER percent-decoding — `#%74op` navigates identically to `#top`.
6155
- */
6156
- function isTopFragment(id) {
6157
- return id.toLowerCase() === "top";
6158
- }
6159
- /**
6160
- * A fragment with its text directive removed. Everything from the first `:~:` on is user-agent
6161
- * instructions for finding text and names no element, while anything before it is still an
6162
- * ordinary element fragment — `#section:~:text=hi` targets `id="section"`, `#:~:text=hi` targets
6163
- * nothing. Returns an empty string when the fragment is a directive and nothing else.
6164
- */
6165
- function stripTextDirective(fragment) {
6166
- const i = fragment.indexOf(":~:");
6167
- return i === -1 ? fragment : fragment.slice(0, i);
6168
- }
6169
- //#endregion
6170
6499
  //#region src/rules/a11y/invalid-role.ts
6171
6500
  const a11yInvalidRole = componentRule({
6172
6501
  id: "a11y/invalid-role",
@@ -6866,7 +7195,7 @@ const ELEMENT_FACT_OVERRIDES = {
6866
7195
  };
6867
7196
  /**
6868
7197
  * The role(s) an `AriaElementFact` may have. Explicit first: a literal role's first concrete token
6869
- * (`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
6870
7199
  * and yields `undefined`. Otherwise the element's implicit role — never one role but the set of
6871
7200
  * candidates the dataset's conditional outcomes leave open, since the selectors that decide between
6872
7201
  * them are not evaluated (design 2026-08-19-aria-role-table-rules, "Which role an element has").
@@ -6881,7 +7210,7 @@ function roleCandidates(e) {
6881
7210
  namingProhibited: false
6882
7211
  } : void 0;
6883
7212
  }
6884
- if (e.hasSpread) return void 0;
7213
+ if (e.hasSpread || e.hasAction) return void 0;
6885
7214
  const el = Object.hasOwn(HTML_SPEC.elements, e.tag) ? HTML_SPEC.elements[e.tag] : void 0;
6886
7215
  if (!el) return void 0;
6887
7216
  const aria = (Object.hasOwn(ELEMENT_FACT_OVERRIDES, e.tag) ? ELEMENT_FACT_OVERRIDES[e.tag] : void 0) ?? el.aria;
@@ -7448,7 +7777,12 @@ function surplusRule(spec) {
7448
7777
  first ??= reps[0];
7449
7778
  for (let i = 1; i < reps.length; i++) {
7450
7779
  surplus = true;
7451
- 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);
7452
7786
  }
7453
7787
  }
7454
7788
  if (first && !surplus) out.push(result(route.route, PASS, {
@@ -7626,6 +7960,7 @@ const a11yIdDuplication = surplusRule({
7626
7960
  recommendation: "Every id in a route should be unique.",
7627
7961
  map: (route) => Object.entries(route.ids).sort(([, a], [, b]) => a[0].file.localeCompare(b[0].file) || a[0].line - b[0].line),
7628
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}.",
7629
7964
  passMessage: "No duplicate ids"
7630
7965
  });
7631
7966
  //#endregion
@@ -8293,4 +8628,4 @@ function formatMarkdownReport(results, config, meta) {
8293
8628
  return lines.join("\n");
8294
8629
  }
8295
8630
  //#endregion
8296
- 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 };