@svelte-vitals/core 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +187 -26
  2. package/dist/index.js +1141 -8
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -60,6 +60,26 @@ type Category = 'seo' | 'performance' | 'correctness' | 'security' | 'architectu
60
60
  type TreatDynamicAs = 'pass' | 'warn' | 'fail';
61
61
  /** Per-rule override: disable, or change severity. */
62
62
  type RuleSetting = 'off' | Severity;
63
+ /**
64
+ * Scoped rule override (design 2026-07-18), applied to results after analysis.
65
+ * An entry matches a finding when any `route` glob matches its route id or any
66
+ * `files` glob matches its source location; at least one of the two must be
67
+ * set. Glob syntax: `*` matches within a segment, `**` across segments, a
68
+ * trailing `/**` also matches the bare prefix, and all other characters
69
+ * (including SvelteKit's `(`, `)`, `[`, `]`) are literal.
70
+ */
71
+ interface RuleOverride {
72
+ /**
73
+ * Route-id glob(s), e.g. '/admin/**'. Note route ids drop `(group)` segments
74
+ * (`src/routes/(app)/dashboard` reports as '/dashboard') — target a group
75
+ * via `files` instead.
76
+ */
77
+ route?: string | string[];
78
+ /** Source-path glob(s) matched against a finding's location, e.g. 'src/routes/(app)/**'. */
79
+ files?: string | string[];
80
+ /** Keys are rule ids ('SEO001') or category names ('seo'). Rule id beats category within an entry. */
81
+ rules: Record<string, RuleSetting>;
82
+ }
63
83
  interface Config {
64
84
  treatDynamicAs: TreatDynamicAs;
65
85
  /** Component names treated as meta sources of unknown content (design §11 layer 4). */
@@ -70,6 +90,8 @@ interface Config {
70
90
  failOn: Severity;
71
91
  /** Per-category weights for the combined Health score (default: equal, 1 each) (#10). */
72
92
  weights?: Partial<Record<Category, number>>;
93
+ /** Route-/file-scoped rule overrides, applied to results after analysis (later entries win). */
94
+ overrides?: RuleOverride[];
73
95
  }
74
96
  declare const defaultConfig: Config;
75
97
  /** Merge user config over defaults. Identity helper for config files (design §6). */
@@ -219,6 +241,35 @@ interface EffectFact {
219
241
  /** True when this $effect has a NON-EMPTY body that reads no reactive value and makes no bare call — it never re-runs, so it should be onMount (CORRECT003). */
220
242
  mountOnly: boolean;
221
243
  }
244
+ /** A `$effect` guaranteed to run outside component initialisation — it throws `effect_orphan` at runtime (CORRECT006). */
245
+ interface OrphanEffectFact {
246
+ /** 1-based source line, or 0 if unknown. For 'constructor-instantiated', the module-scope `new` site. */
247
+ line: number;
248
+ /** 'top-level' = runs at module evaluation; 'constructor-instantiated' = module-scope `new` of a same-file class whose constructor creates a bare effect. */
249
+ kind: 'top-level' | 'constructor-instantiated';
250
+ /** Class name when kind is 'constructor-instantiated' (used in the finding message). */
251
+ className?: string;
252
+ }
253
+ /** A svelte lifecycle/context call guaranteed to run outside component initialisation — it throws `lifecycle_outside_component` at runtime (CORRECT007). */
254
+ interface OrphanLifecycleCallFact {
255
+ /** Canonical svelte export name (alias-resolved), e.g. 'onMount'. */
256
+ name: string;
257
+ /** 1-based source line, or 0 if unknown. For 'constructor-instantiated', the module-scope `new` site. */
258
+ line: number;
259
+ /** 'top-level' = runs at module evaluation; 'constructor-instantiated' = module-scope `new` of a same-file class whose constructor calls a tracked function. */
260
+ kind: 'top-level' | 'constructor-instantiated';
261
+ /** Class name when kind is 'constructor-instantiated' (used in the finding message). */
262
+ className?: string;
263
+ }
264
+ /** A browser-only global read in code that runs on the server — SSR crashes with "<name> is not defined" (CORRECT008/009). */
265
+ interface BrowserGlobalRefFact {
266
+ /** The global's name, e.g. 'window'. */
267
+ name: string;
268
+ /** 1-based source line, or 0 if unknown. */
269
+ line: number;
270
+ /** 'module' = module evaluation (script module / runes module — CORRECT008); 'instance' = component-init top level (runs on the server during SSR — CORRECT009). */
271
+ context: 'module' | 'instance';
272
+ }
222
273
  /** A flagged source position in a component (e.g. an `{@html}` tag or a `javascript:` URL). */
223
274
  interface SourceSpan {
224
275
  /** 1-based source line, or 0 if unknown. */
@@ -267,52 +318,123 @@ interface ComponentFacts {
267
318
  name: string;
268
319
  line: number;
269
320
  }[];
321
+ /** `$effect` calls guaranteed to run outside component initialisation — module scope in `.svelte.ts`/`.svelte.js` or `<script module>` (CORRECT006). */
322
+ orphanEffects: OrphanEffectFact[];
323
+ /** Svelte lifecycle/context calls guaranteed to run outside component initialisation — module scope in `.svelte.ts`/`.svelte.js` or `<script module>` (CORRECT007). */
324
+ orphanLifecycleCalls: OrphanLifecycleCallFact[];
325
+ /** Browser-global reads in server-executed positions of this file (CORRECT008/009). */
326
+ browserGlobalRefs: BrowserGlobalRefFact[];
327
+ /** Module-scope `$state` declarations in a `.svelte.ts`/`.svelte.js` runes module — on a server, one instance shared by every request (SEC005). Always empty for `.svelte` files. */
328
+ moduleStateDecls: {
329
+ name: string;
330
+ line: number;
331
+ }[];
270
332
  /** Inline `svelte-vitals-disable-next-line` directives found in this file's source — component-rule escape hatch (issue #92). Optional: absent is equivalent to no directives, so existing external constructors of `ComponentFacts` are unaffected. */
271
333
  suppressions?: SuppressionDirective[];
272
334
  }
273
335
 
274
- /** Parse a component's reactivity/correctness + security + architecture facts (CLI/static + vite build mode). */
275
- declare function parseComponentFacts(source: string, filename: string): {
276
- eachBlocks: EachBlockFact[];
277
- effects: EffectFact[];
278
- htmlTags: SourceSpan[];
279
- javascriptUrls: SourceSpan[];
280
- loc: number;
281
- propCount: number;
282
- imports: string[];
283
- importSpans: {
284
- source: string;
336
+ /** What the per-file parsers produce `ComponentFacts` minus `file`, with `suppressions` always present. */
337
+ type ParsedFacts = Omit<ComponentFacts, 'file' | 'suppressions'> & {
338
+ suppressions: SuppressionDirective[];
339
+ };
340
+ /**
341
+ * Parse one source file's facts (CLI/static + vite build mode): a `.svelte` component's
342
+ * reactivity/correctness + security + architecture facts, or a `.svelte.ts`/`.svelte.js`
343
+ * runes module's orphan-$effect facts (CORRECT006).
344
+ */
345
+ declare function parseComponentFacts(source: string, filename: string): ParsedFacts;
346
+
347
+ /**
348
+ * Fallback facts for a file that fails to read or parse (dev tooling must never
349
+ * throw). This is the single source of truth for the empty-facts shape — add new
350
+ * `ComponentFacts` fields HERE so TypeScript catches every call site that still
351
+ * needs updating.
352
+ */
353
+ declare function emptyComponentFacts(file: string): ComponentFacts;
354
+ /**
355
+ * Scan every `.svelte` component and `.svelte.ts`/`.svelte.js` runes module under `src/`
356
+ * for Correctness/Security/Architecture/Bundle-Performance facts. Independent of route
357
+ * resolution — covers `$lib` and non-route components too. A file that fails to read or
358
+ * parse contributes empty facts instead of aborting the whole scan (dev tooling must
359
+ * never throw).
360
+ */
361
+ declare function collectComponentFacts(rt: Runtime, cwd: string): Promise<ComponentFacts[]>;
362
+
363
+ /**
364
+ * Facts parsed from one SvelteKit route/hooks file for the SSR shared-state rules
365
+ * (SEC003–005). Collected by `collectKitModuleFacts` (static/CLI + vite build mode).
366
+ */
367
+ interface KitModuleFacts {
368
+ /** Repo-relative source file. */
369
+ file: string;
370
+ /** 'server' = runs only on the server (+*.server, +server, hooks.server); 'universal' = +page.ts/+layout.ts (still runs on the server during SSR). */
371
+ kind: 'server' | 'universal';
372
+ /** Module-scope let/var reassigned from inside a function (SEC004). */
373
+ moduleStateReassignments: {
374
+ name: string;
285
375
  line: number;
376
+ inHandler: boolean;
286
377
  }[];
287
- namespaceImports: {
378
+ /** Writes to an imported binding from inside an exported handler (SEC003). */
379
+ importedStateWrites: {
380
+ name: string;
381
+ line: number;
382
+ via: 'assignment' | 'set-call';
383
+ }[];
384
+ /** Writes to an imported binding outside handlers — top level or helper functions (SEC005's write flavour). */
385
+ importedStateWritesOutsideHandlers: {
386
+ name: string;
387
+ line: number;
388
+ }[];
389
+ /** Value imports whose specifier resolves to a repo-local `.svelte.ts`/`.svelte.js` runes module (SEC005). */
390
+ runesModuleImports: {
288
391
  source: string;
392
+ resolved: string;
393
+ names: string[];
289
394
  line: number;
290
395
  }[];
291
- constableStates: {
396
+ /** Svelte lifecycle/context calls that run outside component initialisation — top level, handler bodies, or the `init` hook (CORRECT007). */
397
+ lifecycleCalls: {
292
398
  name: string;
293
399
  line: number;
400
+ inHandler: boolean;
294
401
  }[];
295
- mutatedProps: {
402
+ /** Browser-global reads in server-executed positions — top level, handler bodies, the `init` hook (CORRECT008). Empty when the file itself exports `ssr = false`. */
403
+ browserGlobalRefs: {
296
404
  name: string;
297
405
  line: number;
406
+ inHandler: boolean;
298
407
  }[];
408
+ /** Inline `svelte-vitals-disable-next-line` directives in this file. */
299
409
  suppressions: SuppressionDirective[];
300
- };
410
+ }
301
411
 
302
412
  /**
303
- * Fallback facts for a file that fails to read or parse (dev tooling must never
304
- * throw). This is the single source of truth for the empty-facts shape — add new
305
- * `ComponentFacts` fields HERE so TypeScript catches every call site that still
306
- * needs updating.
413
+ * Resolve an import specifier to a repo-relative `.svelte.ts`/`.svelte.js` path, or
414
+ * undefined when it cannot be a runes module: `$lib/` maps to `src/lib/`, `./`/`../`
415
+ * resolve against the importing file's directory; bare packages, other aliases, and
416
+ * a relative specifier whose `..` segments escape the repo root are skipped. An
417
+ * extensionless `…/x.svelte` specifier canonicalises to `….svelte.ts` (SEC005 also
418
+ * tries the `.js` sibling when matching).
307
419
  */
308
- declare function emptyComponentFacts(file: string): ComponentFacts;
420
+ declare function resolveRunesModuleSpecifier(spec: string, importerFile: string): string | undefined;
309
421
  /**
310
- * Scan every `.svelte` component under `src/` for Correctness/Security/Architecture/
311
- * Bundle-Performance facts. Independent of route resolution covers `$lib` and
312
- * non-route components too. A file that fails to read or parse contributes empty
313
- * facts instead of aborting the whole scan (dev tooling must never throw).
422
+ * Parse one SvelteKit route/hooks file's SSR shared-state facts (SEC003–005). Uses
423
+ * the shared wrap parser (`parseModuleProgram`), so reported lines subtract the
424
+ * 1-line wrap prefix; suppressions are scanned on the unwrapped source.
314
425
  */
315
- declare function collectComponentFacts(rt: Runtime, cwd: string): Promise<ComponentFacts[]>;
426
+ declare function parseKitModuleFacts(source: string, filename: string): Omit<KitModuleFacts, 'file' | 'kind'>;
427
+
428
+ /** Fallback facts for a Kit file that fails to read or parse (dev tooling must never throw). */
429
+ declare function emptyKitModuleFacts(file: string, kind: KitModuleFacts['kind']): KitModuleFacts;
430
+ /**
431
+ * Scan SvelteKit route/hooks files for SSR shared-state facts (SEC003–005): route
432
+ * `+page`/`+layout` server and universal modules, `+server` endpoints, and
433
+ * `src/hooks.server`. `src/lib/server/**` is deliberately NOT scanned — legitimate
434
+ * module singletons (DB connections, clients) live there (design). A file that
435
+ * fails to read or parse contributes empty facts instead of aborting the scan.
436
+ */
437
+ declare function collectKitModuleFacts(rt: Runtime, cwd: string): Promise<KitModuleFacts[]>;
316
438
 
317
439
  type Node = any;
318
440
  /**
@@ -361,6 +483,8 @@ interface RuleContext {
361
483
  headings?: ResolvedHeadings[];
362
484
  /** Per-file component-body facts for Correctness rules (static/CLI mode only). */
363
485
  components?: ComponentFacts[];
486
+ /** Per-file SvelteKit route/hooks facts for the SSR shared-state rules (static/CLI + vite build mode only). */
487
+ kitModules?: KitModuleFacts[];
364
488
  project: Project;
365
489
  config: Config;
366
490
  }
@@ -518,9 +642,36 @@ declare const correct004UnmutatedState: Rule;
518
642
 
519
643
  declare const correct005PropMutation: Rule;
520
644
 
645
+ declare const correct006OrphanEffect: Rule;
646
+
647
+ /**
648
+ * CORRECT007 — svelte lifecycle/context calls guaranteed to run outside component
649
+ * initialisation: module scope in runes modules / `<script module>`, the constructor of
650
+ * a module-scope-instantiated class, and Kit load/handler/`init` bodies. A custom check
651
+ * because the facts live on BOTH the component channel and the Kit-module channel.
652
+ */
653
+ declare const correct007OrphanLifecycle: Rule;
654
+
655
+ /**
656
+ * CORRECT008 — browser globals read in server-executed MODULE code: module scope of
657
+ * runes modules / `<script module>`, and Kit route/hooks files (top level, handler
658
+ * bodies, the `init` hook). All of it runs on the server, where these globals do not
659
+ * exist — SSR crashes with a ReferenceError. Instance-script reads are CORRECT009's
660
+ * (warning) territory. A custom check because the facts live on both channels.
661
+ */
662
+ declare const correct008BrowserGlobals: Rule;
663
+
664
+ declare const correct009InstanceBrowserGlobals: Rule;
665
+
521
666
  declare const sec001Html: Rule;
522
667
  declare const sec002JavascriptUrl: Rule;
523
668
 
669
+ declare const sec003LoadStateWrite: Rule;
670
+
671
+ declare const sec004ServerModuleState: Rule;
672
+
673
+ declare const sec005SharedStateImport: Rule;
674
+
524
675
  declare const arch001ComponentSize: Rule;
525
676
  declare const arch002PropCount: Rule;
526
677
 
@@ -810,5 +961,15 @@ declare function safeHref(url: string): string | null;
810
961
  declare function selectRules(rules: Rule[], config: Config): Rule[];
811
962
  /** Apply per-rule severity overrides to results (design §6). */
812
963
  declare function applyRuleSeverities(results: Result[], config: Config): Result[];
964
+ /**
965
+ * Apply route-/file-scoped overrides to results (design 2026-07-18). An entry
966
+ * matches when any `route` glob matches the finding's route id or any `files`
967
+ * glob matches its location (OR). `'off'` removes a matched result entirely —
968
+ * passing seeds included, so scoring and "checks passed" counts behave as if
969
+ * the rule never ran there. A severity value rewrites the result's severity.
970
+ * Entries are evaluated in order (later entries win); within one entry a
971
+ * rule-id key beats a category key.
972
+ */
973
+ declare function applyOverrides(results: Result[], config: Config): Result[];
813
974
 
814
- export { APP_SCRIPT, APP_STYLE, type AppSnapshot, BAND_COLOR, CHILD_NODE_KEYS, type Category, type Classification, type ComponentFacts, type Config, type ConsoleReportOptions, type Detection, type EachBlockFact, type EffectFact, type Fix, type HeadProvider, type HeadTag, type HeadingInfo, type HealthResult, type ImageInfo, type JsonReport, type Palette, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type ResolvedHeadings, type ResolvedImages, type Result, type RouteBadge, type Rule, type RuleContext, type RuleInfo, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type SourceSpan, type Summary, type SuppressionDirective, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, arch001ComponentSize, arch002PropCount, attrText, attrTextOf, attrValue, attrValueOf, buildHtmlDocument, buildJsonReport, classify, collectComponentFacts, computeHealth, computeScore, correct001EachKey, correct002EffectDerived, correct003EffectAsOnMount, correct004UnmutatedState, correct005PropMutation, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, emptyComponentFacts, escapeHtml, explainRule, findAttr, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatMarkdownReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, lineOf, linkRule, noColorPalette, parseComponentFacts, perf001ImageDimensions, perf002ImageLoading, perf003PreloadAs, perf004FontPreloadCrossorigin, perf005LcpImage, perf006ResponsiveImage, perf007RenderBlockingScript, perf008Preconnect, perf009HeavyImport, perf010NamespaceImport, renderAppShell, runRules, safeHref, scoreBand, scoreColor, scoresByCategory, sec001Html, sec002JavascriptUrl, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, seo010Indexability, seo011TwitterCard, seo012OgDescription, seo013OgUrl, seo014Viewport, seo015SitemapInRobots, seo016JsonLdValidity, seo017DeprecatedType, seo018RelativeUrl, seo019DateFormat, seo020Placeholder, seo021RequiredProps, seo022TitleLength, seo023DescriptionLength, seo024Charset, seo025ImageAlt, seo026Hreflang, seo027Heading, seo028TitleUnique, seo029DescriptionUnique, seo030HeadingOrder, summarize, textFromNodes, valueFromNodes };
975
+ export { APP_SCRIPT, APP_STYLE, type AppSnapshot, BAND_COLOR, CHILD_NODE_KEYS, type Category, type Classification, type ComponentFacts, type Config, type ConsoleReportOptions, type Detection, type EachBlockFact, type EffectFact, type Fix, type HeadProvider, type HeadTag, type HeadingInfo, type HealthResult, type ImageInfo, type JsonReport, type KitModuleFacts, type OrphanEffectFact, type Palette, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type ResolvedHeadings, type ResolvedImages, type Result, type RouteBadge, type Rule, type RuleContext, type RuleInfo, type RuleOverride, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type SourceSpan, type Summary, type SuppressionDirective, type TreatDynamicAs, type Value, allRules, applyOverrides, applyRuleSeverities, arch001ComponentSize, arch002PropCount, attrText, attrTextOf, attrValue, attrValueOf, buildHtmlDocument, buildJsonReport, classify, collectComponentFacts, collectKitModuleFacts, computeHealth, computeScore, correct001EachKey, correct002EffectDerived, correct003EffectAsOnMount, correct004UnmutatedState, correct005PropMutation, correct006OrphanEffect, correct007OrphanLifecycle, correct008BrowserGlobals, correct009InstanceBrowserGlobals, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, emptyComponentFacts, emptyKitModuleFacts, escapeHtml, explainRule, findAttr, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatMarkdownReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, lineOf, linkRule, noColorPalette, parseComponentFacts, parseKitModuleFacts, perf001ImageDimensions, perf002ImageLoading, perf003PreloadAs, perf004FontPreloadCrossorigin, perf005LcpImage, perf006ResponsiveImage, perf007RenderBlockingScript, perf008Preconnect, perf009HeavyImport, perf010NamespaceImport, renderAppShell, resolveRunesModuleSpecifier, runRules, safeHref, scoreBand, scoreColor, scoresByCategory, sec001Html, sec002JavascriptUrl, sec003LoadStateWrite, sec004ServerModuleState, sec005SharedStateImport, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, seo010Indexability, seo011TwitterCard, seo012OgDescription, seo013OgUrl, seo014Viewport, seo015SitemapInRobots, seo016JsonLdValidity, seo017DeprecatedType, seo018RelativeUrl, seo019DateFormat, seo020Placeholder, seo021RequiredProps, seo022TitleLength, seo023DescriptionLength, seo024Charset, seo025ImageAlt, seo026Hreflang, seo027Heading, seo028TitleUnique, seo029DescriptionUnique, seo030HeadingOrder, summarize, textFromNodes, valueFromNodes };