@svelte-vitals/core 0.24.0 → 0.26.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.
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  [![npm](https://img.shields.io/npm/v/@svelte-vitals/core)](https://www.npmjs.com/package/@svelte-vitals/core)
4
4
  [![MIT](https://img.shields.io/npm/l/@svelte-vitals/core)](https://opensource.org/licenses/MIT)
5
5
 
6
- Runtime-agnostic core for [svelte-vitals](https://github.com/oekazuma/svelte-vitals): shared types, the rule engine, scorer, reporter, and the SEO rule set.
6
+ Runtime-agnostic core for [svelte-vitals](https://github.com/oekazuma/svelte-vitals): shared types, the rule engine, scorer, and reporters, plus the full rule set across five categories — SEO, Performance, Correctness, Security, Architecture.
7
7
 
8
8
  This package is **mode-independent** and contains no I/O — it operates on a normalized `ResolvedHead[]` intermediate representation, so the same rules run unchanged whether heads come from static source analysis (the `svelte-vitals` CLI) or from prerendered HTML (the `@svelte-vitals/vite` plugin). It has zero runtime dependencies and no `node:` imports.
9
9
 
10
10
  > Most users don't depend on this directly — install [`svelte-vitals`](https://www.npmjs.com/package/svelte-vitals) instead. This package is for building tools on top of the shared engine.
11
11
 
12
- > **ESM-only** (Node 18+). Ships ES modules only; `require()` is unsupported by design.
12
+ > **ESM-only** (Node 22.13+). Ships ES modules only; `require()` is unsupported by design.
13
13
 
14
14
  ## License
15
15
 
package/dist/index.d.ts CHANGED
@@ -219,6 +219,35 @@ interface EffectFact {
219
219
  /** 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
220
  mountOnly: boolean;
221
221
  }
222
+ /** A `$effect` guaranteed to run outside component initialisation — it throws `effect_orphan` at runtime (CORRECT006). */
223
+ interface OrphanEffectFact {
224
+ /** 1-based source line, or 0 if unknown. For 'constructor-instantiated', the module-scope `new` site. */
225
+ line: number;
226
+ /** 'top-level' = runs at module evaluation; 'constructor-instantiated' = module-scope `new` of a same-file class whose constructor creates a bare effect. */
227
+ kind: 'top-level' | 'constructor-instantiated';
228
+ /** Class name when kind is 'constructor-instantiated' (used in the finding message). */
229
+ className?: string;
230
+ }
231
+ /** A svelte lifecycle/context call guaranteed to run outside component initialisation — it throws `lifecycle_outside_component` at runtime (CORRECT007). */
232
+ interface OrphanLifecycleCallFact {
233
+ /** Canonical svelte export name (alias-resolved), e.g. 'onMount'. */
234
+ name: string;
235
+ /** 1-based source line, or 0 if unknown. For 'constructor-instantiated', the module-scope `new` site. */
236
+ line: number;
237
+ /** 'top-level' = runs at module evaluation; 'constructor-instantiated' = module-scope `new` of a same-file class whose constructor calls a tracked function. */
238
+ kind: 'top-level' | 'constructor-instantiated';
239
+ /** Class name when kind is 'constructor-instantiated' (used in the finding message). */
240
+ className?: string;
241
+ }
242
+ /** A browser-only global read in code that runs on the server — SSR crashes with "<name> is not defined" (CORRECT008/009). */
243
+ interface BrowserGlobalRefFact {
244
+ /** The global's name, e.g. 'window'. */
245
+ name: string;
246
+ /** 1-based source line, or 0 if unknown. */
247
+ line: number;
248
+ /** 'module' = module evaluation (script module / runes module — CORRECT008); 'instance' = component-init top level (runs on the server during SSR — CORRECT009). */
249
+ context: 'module' | 'instance';
250
+ }
222
251
  /** A flagged source position in a component (e.g. an `{@html}` tag or a `javascript:` URL). */
223
252
  interface SourceSpan {
224
253
  /** 1-based source line, or 0 if unknown. */
@@ -267,52 +296,123 @@ interface ComponentFacts {
267
296
  name: string;
268
297
  line: number;
269
298
  }[];
299
+ /** `$effect` calls guaranteed to run outside component initialisation — module scope in `.svelte.ts`/`.svelte.js` or `<script module>` (CORRECT006). */
300
+ orphanEffects: OrphanEffectFact[];
301
+ /** Svelte lifecycle/context calls guaranteed to run outside component initialisation — module scope in `.svelte.ts`/`.svelte.js` or `<script module>` (CORRECT007). */
302
+ orphanLifecycleCalls: OrphanLifecycleCallFact[];
303
+ /** Browser-global reads in server-executed positions of this file (CORRECT008/009). */
304
+ browserGlobalRefs: BrowserGlobalRefFact[];
305
+ /** 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. */
306
+ moduleStateDecls: {
307
+ name: string;
308
+ line: number;
309
+ }[];
270
310
  /** 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
311
  suppressions?: SuppressionDirective[];
272
312
  }
273
313
 
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;
314
+ /** What the per-file parsers produce `ComponentFacts` minus `file`, with `suppressions` always present. */
315
+ type ParsedFacts = Omit<ComponentFacts, 'file' | 'suppressions'> & {
316
+ suppressions: SuppressionDirective[];
317
+ };
318
+ /**
319
+ * Parse one source file's facts (CLI/static + vite build mode): a `.svelte` component's
320
+ * reactivity/correctness + security + architecture facts, or a `.svelte.ts`/`.svelte.js`
321
+ * runes module's orphan-$effect facts (CORRECT006).
322
+ */
323
+ declare function parseComponentFacts(source: string, filename: string): ParsedFacts;
324
+
325
+ /**
326
+ * Fallback facts for a file that fails to read or parse (dev tooling must never
327
+ * throw). This is the single source of truth for the empty-facts shape — add new
328
+ * `ComponentFacts` fields HERE so TypeScript catches every call site that still
329
+ * needs updating.
330
+ */
331
+ declare function emptyComponentFacts(file: string): ComponentFacts;
332
+ /**
333
+ * Scan every `.svelte` component and `.svelte.ts`/`.svelte.js` runes module under `src/`
334
+ * for Correctness/Security/Architecture/Bundle-Performance facts. Independent of route
335
+ * resolution — covers `$lib` and non-route components too. A file that fails to read or
336
+ * parse contributes empty facts instead of aborting the whole scan (dev tooling must
337
+ * never throw).
338
+ */
339
+ declare function collectComponentFacts(rt: Runtime, cwd: string): Promise<ComponentFacts[]>;
340
+
341
+ /**
342
+ * Facts parsed from one SvelteKit route/hooks file for the SSR shared-state rules
343
+ * (SEC003–005). Collected by `collectKitModuleFacts` (static/CLI + vite build mode).
344
+ */
345
+ interface KitModuleFacts {
346
+ /** Repo-relative source file. */
347
+ file: string;
348
+ /** 'server' = runs only on the server (+*.server, +server, hooks.server); 'universal' = +page.ts/+layout.ts (still runs on the server during SSR). */
349
+ kind: 'server' | 'universal';
350
+ /** Module-scope let/var reassigned from inside a function (SEC004). */
351
+ moduleStateReassignments: {
352
+ name: string;
285
353
  line: number;
354
+ inHandler: boolean;
286
355
  }[];
287
- namespaceImports: {
356
+ /** Writes to an imported binding from inside an exported handler (SEC003). */
357
+ importedStateWrites: {
358
+ name: string;
359
+ line: number;
360
+ via: 'assignment' | 'set-call';
361
+ }[];
362
+ /** Writes to an imported binding outside handlers — top level or helper functions (SEC005's write flavour). */
363
+ importedStateWritesOutsideHandlers: {
364
+ name: string;
365
+ line: number;
366
+ }[];
367
+ /** Value imports whose specifier resolves to a repo-local `.svelte.ts`/`.svelte.js` runes module (SEC005). */
368
+ runesModuleImports: {
288
369
  source: string;
370
+ resolved: string;
371
+ names: string[];
289
372
  line: number;
290
373
  }[];
291
- constableStates: {
374
+ /** Svelte lifecycle/context calls that run outside component initialisation — top level, handler bodies, or the `init` hook (CORRECT007). */
375
+ lifecycleCalls: {
292
376
  name: string;
293
377
  line: number;
378
+ inHandler: boolean;
294
379
  }[];
295
- mutatedProps: {
380
+ /** Browser-global reads in server-executed positions — top level, handler bodies, the `init` hook (CORRECT008). Empty when the file itself exports `ssr = false`. */
381
+ browserGlobalRefs: {
296
382
  name: string;
297
383
  line: number;
384
+ inHandler: boolean;
298
385
  }[];
386
+ /** Inline `svelte-vitals-disable-next-line` directives in this file. */
299
387
  suppressions: SuppressionDirective[];
300
- };
388
+ }
301
389
 
302
390
  /**
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.
391
+ * Resolve an import specifier to a repo-relative `.svelte.ts`/`.svelte.js` path, or
392
+ * undefined when it cannot be a runes module: `$lib/` maps to `src/lib/`, `./`/`../`
393
+ * resolve against the importing file's directory; bare packages, other aliases, and
394
+ * a relative specifier whose `..` segments escape the repo root are skipped. An
395
+ * extensionless `…/x.svelte` specifier canonicalises to `….svelte.ts` (SEC005 also
396
+ * tries the `.js` sibling when matching).
307
397
  */
308
- declare function emptyComponentFacts(file: string): ComponentFacts;
398
+ declare function resolveRunesModuleSpecifier(spec: string, importerFile: string): string | undefined;
309
399
  /**
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).
400
+ * Parse one SvelteKit route/hooks file's SSR shared-state facts (SEC003–005). Uses
401
+ * the shared wrap parser (`parseModuleProgram`), so reported lines subtract the
402
+ * 1-line wrap prefix; suppressions are scanned on the unwrapped source.
314
403
  */
315
- declare function collectComponentFacts(rt: Runtime, cwd: string): Promise<ComponentFacts[]>;
404
+ declare function parseKitModuleFacts(source: string, filename: string): Omit<KitModuleFacts, 'file' | 'kind'>;
405
+
406
+ /** Fallback facts for a Kit file that fails to read or parse (dev tooling must never throw). */
407
+ declare function emptyKitModuleFacts(file: string, kind: KitModuleFacts['kind']): KitModuleFacts;
408
+ /**
409
+ * Scan SvelteKit route/hooks files for SSR shared-state facts (SEC003–005): route
410
+ * `+page`/`+layout` server and universal modules, `+server` endpoints, and
411
+ * `src/hooks.server`. `src/lib/server/**` is deliberately NOT scanned — legitimate
412
+ * module singletons (DB connections, clients) live there (design). A file that
413
+ * fails to read or parse contributes empty facts instead of aborting the scan.
414
+ */
415
+ declare function collectKitModuleFacts(rt: Runtime, cwd: string): Promise<KitModuleFacts[]>;
316
416
 
317
417
  type Node = any;
318
418
  /**
@@ -361,6 +461,8 @@ interface RuleContext {
361
461
  headings?: ResolvedHeadings[];
362
462
  /** Per-file component-body facts for Correctness rules (static/CLI mode only). */
363
463
  components?: ComponentFacts[];
464
+ /** Per-file SvelteKit route/hooks facts for the SSR shared-state rules (static/CLI + vite build mode only). */
465
+ kitModules?: KitModuleFacts[];
364
466
  project: Project;
365
467
  config: Config;
366
468
  }
@@ -518,9 +620,36 @@ declare const correct004UnmutatedState: Rule;
518
620
 
519
621
  declare const correct005PropMutation: Rule;
520
622
 
623
+ declare const correct006OrphanEffect: Rule;
624
+
625
+ /**
626
+ * CORRECT007 — svelte lifecycle/context calls guaranteed to run outside component
627
+ * initialisation: module scope in runes modules / `<script module>`, the constructor of
628
+ * a module-scope-instantiated class, and Kit load/handler/`init` bodies. A custom check
629
+ * because the facts live on BOTH the component channel and the Kit-module channel.
630
+ */
631
+ declare const correct007OrphanLifecycle: Rule;
632
+
633
+ /**
634
+ * CORRECT008 — browser globals read in server-executed MODULE code: module scope of
635
+ * runes modules / `<script module>`, and Kit route/hooks files (top level, handler
636
+ * bodies, the `init` hook). All of it runs on the server, where these globals do not
637
+ * exist — SSR crashes with a ReferenceError. Instance-script reads are CORRECT009's
638
+ * (warning) territory. A custom check because the facts live on both channels.
639
+ */
640
+ declare const correct008BrowserGlobals: Rule;
641
+
642
+ declare const correct009InstanceBrowserGlobals: Rule;
643
+
521
644
  declare const sec001Html: Rule;
522
645
  declare const sec002JavascriptUrl: Rule;
523
646
 
647
+ declare const sec003LoadStateWrite: Rule;
648
+
649
+ declare const sec004ServerModuleState: Rule;
650
+
651
+ declare const sec005SharedStateImport: Rule;
652
+
524
653
  declare const arch001ComponentSize: Rule;
525
654
  declare const arch002PropCount: Rule;
526
655
 
@@ -740,6 +869,57 @@ declare function formatMarkdownReport(results: Result[], config: Config, meta: {
740
869
  version: string;
741
870
  }): string;
742
871
 
872
+ /** Provenance of a route's findings: real rendered page vs. source-only analysis. */
873
+ type RouteBadge = 'measured' | 'static';
874
+ interface AppSnapshot {
875
+ report: JsonReport;
876
+ badges: Record<string, RouteBadge>;
877
+ analyzing: boolean;
878
+ /** Monotonically increasing; lets the client discard an out-of-order /data.json response. */
879
+ sequence: number;
880
+ /** Whether a dev server is behind this page (SSE updates, /data.json refetch, connection dot). */
881
+ live: boolean;
882
+ meta: {
883
+ version: string;
884
+ coreVersion?: string;
885
+ };
886
+ }
887
+ /**
888
+ * Hand-authored CSS for the master/detail shell. Reuses the same design-token
889
+ * names/values as the rest of the project, and adds a dark theme via
890
+ * `:root[data-theme="dark"]` plus a `prefers-color-scheme` fallback for a
891
+ * first-ever visit with no stored preference.
892
+ */
893
+ declare const APP_STYLE: string;
894
+ /**
895
+ * Hand-authored client script for the shell — no bundler, no framework. Parses the
896
+ * AppSnapshot embedded by renderAppShell, then owns all rendering: sidebar
897
+ * (search/sort/route list) and detail pane (Overview or a selected route). When the
898
+ * snapshot says `live`, it additionally re-fetches /data.json on every SSE `update`
899
+ * and on the EventSource's `open` event (covers the initial connection and every
900
+ * auto-reconnect, since EventSource replays no missed events) — discarding any
901
+ * response whose `sequence` isn't newer than what's already rendered.
902
+ */
903
+ declare const APP_SCRIPT: string;
904
+ /** The shell HTML: empty sidebar/detail/topbar containers, the stylesheet, the
905
+ * client script, and the snapshot embedded as JSON for the client's first paint. */
906
+ declare function renderAppShell(snapshot: AppSnapshot): string;
907
+ /**
908
+ * Static (non-live) document over a prebuilt JsonReport — kept as the public name the
909
+ * html reporter has always exported. `routeBadges` preserves the old opts shape.
910
+ */
911
+ declare function buildHtmlDocument(report: JsonReport, meta: {
912
+ version: string;
913
+ coreVersion?: string;
914
+ }, opts?: {
915
+ routeBadges?: Record<string, RouteBadge>;
916
+ }): string;
917
+ /** Render results as the self-contained HTML report (the CLI's `--reporter html`). */
918
+ declare function formatHtmlReport(results: Result[], config: Config, meta: {
919
+ version: string;
920
+ coreVersion?: string;
921
+ }): string;
922
+
743
923
  type Band = 'good' | 'warn' | 'poor';
744
924
  declare const BAND_COLOR: Record<Band, string>;
745
925
  declare function scoreBand(score: number): Band;
@@ -754,20 +934,10 @@ declare function escapeHtml(s: string): string;
754
934
  * keeping core runtime-agnostic and lib-minimal.
755
935
  */
756
936
  declare function safeHref(url: string): string | null;
757
- declare function buildHtmlDocument(report: JsonReport, meta: {
758
- version: string;
759
- coreVersion?: string;
760
- }, opts?: {
761
- routeBadges?: Record<string, 'measured' | 'static'>;
762
- }): string;
763
- declare function formatHtmlReport(results: Result[], config: Config, meta: {
764
- version: string;
765
- coreVersion?: string;
766
- }): string;
767
937
 
768
938
  /** Drop rules disabled via config (design §6). */
769
939
  declare function selectRules(rules: Rule[], config: Config): Rule[];
770
940
  /** Apply per-rule severity overrides to results (design §6). */
771
941
  declare function applyRuleSeverities(results: Result[], config: Config): Result[];
772
942
 
773
- export { 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 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, 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 };
943
+ 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 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, 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 };