@svelte-vitals/core 0.16.0 → 0.19.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/dist/index.d.ts CHANGED
@@ -55,7 +55,7 @@ interface Result {
55
55
  line?: number;
56
56
  }
57
57
  type Scope = 'route' | 'project' | 'component';
58
- type Category = 'seo' | 'performance' | 'correctness' | 'security';
58
+ type Category = 'seo' | 'performance' | 'correctness' | 'security' | 'architecture';
59
59
  /** How dynamic (`{data.title}`) values are treated by scoring (design §4, §12). */
60
60
  type TreatDynamicAs = 'pass' | 'warn' | 'fail';
61
61
  /** Per-rule override: disable, or change severity. */
@@ -216,13 +216,22 @@ interface EffectFact {
216
216
  line: number;
217
217
  /** True when the effect body only assigns to `$state` variables (the "use $derived" smell). */
218
218
  assignsOnlyState: boolean;
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
+ mountOnly: boolean;
219
221
  }
220
222
  /** A flagged source position in a component (e.g. an `{@html}` tag or a `javascript:` URL). */
221
223
  interface SourceSpan {
222
224
  /** 1-based source line, or 0 if unknown. */
223
225
  line: number;
224
226
  }
225
- /** Reactivity/correctness + security facts parsed from one `.svelte` component. */
227
+ /** An inline `svelte-vitals-disable-next-line` directive found in the component's source (issue #92). */
228
+ interface SuppressionDirective {
229
+ /** 1-based line the directive suppresses (the line immediately after the comment). */
230
+ line: number;
231
+ /** Rule ids suppressed on that line; undefined = suppress every rule on that line. */
232
+ ruleIds?: string[];
233
+ }
234
+ /** Reactivity/correctness + security + architecture facts parsed from one `.svelte` component. */
226
235
  interface ComponentFacts {
227
236
  /** Source file the component came from. */
228
237
  file: string;
@@ -232,8 +241,74 @@ interface ComponentFacts {
232
241
  htmlTags: SourceSpan[];
233
242
  /** Element attributes with a literal `javascript:` URL (Security SEC002). */
234
243
  javascriptUrls: SourceSpan[];
244
+ /** Source line count of the component file (Architecture ARCH001). */
245
+ loc: number;
246
+ /** Named props destructured from `$props()`; 0 when unknowable (rest / non-destructured) (Architecture ARCH002). */
247
+ propCount: number;
248
+ /** Module specifiers of every `import` in the instance + module scripts (Bundle PERF009). */
249
+ imports: string[];
250
+ /** Value `import * as X from '<bare pkg>'` namespace imports (type-only excluded) — Bundle PERF010. */
251
+ namespaceImports: {
252
+ source: string;
253
+ line: number;
254
+ }[];
255
+ /** `$state` declarations never written or escaped anywhere in the component — candidates for const (CORRECT004). */
256
+ constableStates: {
257
+ name: string;
258
+ line: number;
259
+ }[];
260
+ /** 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. */
261
+ suppressions?: SuppressionDirective[];
235
262
  }
236
263
 
264
+ /** Parse a component's reactivity/correctness + security + architecture facts (CLI/static + vite build mode). */
265
+ declare function parseComponentFacts(source: string, filename: string): {
266
+ eachBlocks: EachBlockFact[];
267
+ effects: EffectFact[];
268
+ htmlTags: SourceSpan[];
269
+ javascriptUrls: SourceSpan[];
270
+ loc: number;
271
+ propCount: number;
272
+ imports: string[];
273
+ namespaceImports: {
274
+ source: string;
275
+ line: number;
276
+ }[];
277
+ constableStates: {
278
+ name: string;
279
+ line: number;
280
+ }[];
281
+ suppressions: SuppressionDirective[];
282
+ };
283
+
284
+ type Node = any;
285
+ /**
286
+ * All keys that can bear child nodes in a Svelte AST node.
287
+ * Covers if/each/await blocks (pending/then/catch/fallback) as well as
288
+ * the standard fragment, nodes, consequent, alternate, and body keys.
289
+ */
290
+ declare const CHILD_NODE_KEYS: string[];
291
+ /**
292
+ * Determine a value's kind from a list of child/text nodes (design §4, §11):
293
+ * - any ExpressionTag present → 'dynamic' (e.g. {data.title}); we do NOT
294
+ * follow the expression — that would turn this into runtime analysis.
295
+ * - non-whitespace Text only → 'static'
296
+ * - empty / whitespace only → 'absent'
297
+ */
298
+ declare function valueFromNodes(nodes: Node[]): Value;
299
+ /** The literal text of a node list when fully static (no ExpressionTag), else undefined. */
300
+ declare function textFromNodes(nodes: Node[]): string | undefined;
301
+ /** Static string of an attribute (e.g. name="description"), or undefined if dynamic/absent. */
302
+ declare function attrText(attributes: Node[], name: string): string | undefined;
303
+ /** Value kind of an attribute's content (e.g. the `content` of a <meta>). */
304
+ declare function attrValue(attributes: Node[], name: string): Value;
305
+ declare function lineOf(source: string, offset: unknown): number;
306
+ declare function findAttr(attributes: Node[], name: string): Node | undefined;
307
+ /** Value kind of a single attribute (e.g. a component prop). */
308
+ declare function attrValueOf(attr: Node): Value;
309
+ /** Literal static text of a single attribute node (e.g. a component prop), or undefined if dynamic/absent. */
310
+ declare function attrTextOf(attr: Node): string | undefined;
311
+
237
312
  /**
238
313
  * Source-file locations that satisfy the project-scope rules, shared by every
239
314
  * mode so the static (CLI) and rendered (plugin) collectors never drift. This
@@ -404,10 +479,20 @@ declare const seo030HeadingOrder: Rule;
404
479
 
405
480
  declare const correct001EachKey: Rule;
406
481
  declare const correct002EffectDerived: Rule;
482
+ declare const correct003EffectAsOnMount: Rule;
483
+
484
+ declare const correct004UnmutatedState: Rule;
407
485
 
408
486
  declare const sec001Html: Rule;
409
487
  declare const sec002JavascriptUrl: Rule;
410
488
 
489
+ declare const arch001ComponentSize: Rule;
490
+ declare const arch002PropCount: Rule;
491
+
492
+ declare const perf009HeavyImport: Rule;
493
+
494
+ declare const perf010NamespaceImport: Rule;
495
+
411
496
  declare const allRules: Rule[];
412
497
 
413
498
  interface RuleInfo {
@@ -498,10 +583,26 @@ declare function summarize(results: Result[], config: Config): Summary;
498
583
  /** Whether the run should fail the build/CI per the minimum failing severity. */
499
584
  declare function hasFailureAtOrAbove(summary: Summary, min: Severity): boolean;
500
585
 
586
+ /** String decorators for the console reporter. Injected so core stays pure/dep-free. */
587
+ interface Palette {
588
+ bold: (s: string) => string;
589
+ dim: (s: string) => string;
590
+ red: (s: string) => string;
591
+ yellow: (s: string) => string;
592
+ green: (s: string) => string;
593
+ cyan: (s: string) => string;
594
+ }
595
+ /** Default: no decoration (identity) — output is byte-identical to plain text. */
596
+ declare const noColorPalette: Palette;
597
+ /** Green ≥ 90, yellow ≥ 70, red otherwise — for a 0–100 score. */
598
+ declare function scoreColor(p: Palette, score: number): (s: string) => string;
599
+
501
600
  interface ConsoleReportOptions {
502
601
  byRoute?: boolean;
503
602
  /** Mode label shown in the header (default 'static mode'). */
504
603
  mode?: string;
604
+ /** Color decorators; defaults to no color. */
605
+ palette?: Palette;
505
606
  }
506
607
  /**
507
608
  * Render results as a console report string (design §7). Pure: returns a string,
@@ -616,4 +717,4 @@ declare function selectRules(rules: Rule[], config: Config): Rule[];
616
717
  /** Apply per-rule severity overrides to results (design §6). */
617
718
  declare function applyRuleSeverities(results: Result[], config: Config): Result[];
618
719
 
619
- export { BAND_COLOR, 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 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 TreatDynamicAs, type Value, allRules, applyRuleSeverities, buildHtmlDocument, buildJsonReport, classify, computeHealth, computeScore, correct001EachKey, correct002EffectDerived, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, escapeHtml, explainRule, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, linkRule, perf001ImageDimensions, perf002ImageLoading, perf003PreloadAs, perf004FontPreloadCrossorigin, perf005LcpImage, perf006ResponsiveImage, perf007RenderBlockingScript, perf008Preconnect, runRules, safeHref, scoreBand, 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 };
720
+ 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, computeHealth, computeScore, correct001EachKey, correct002EffectDerived, correct003EffectAsOnMount, correct004UnmutatedState, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, escapeHtml, explainRule, findAttr, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, 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 };
package/dist/index.js CHANGED
@@ -14,6 +14,408 @@ function defineConfig(config = {}) {
14
14
  return { ...defaultConfig, ...config };
15
15
  }
16
16
 
17
+ // src/component-parse.ts
18
+ import { parse } from "svelte/compiler";
19
+
20
+ // src/svelte-ast.ts
21
+ var CHILD_NODE_KEYS = [
22
+ "fragment",
23
+ "nodes",
24
+ "consequent",
25
+ "alternate",
26
+ "body",
27
+ "pending",
28
+ "then",
29
+ "catch",
30
+ "fallback"
31
+ ];
32
+ function valueFromNodes(nodes) {
33
+ if (!Array.isArray(nodes)) return "absent";
34
+ if (nodes.some((n) => n?.type === "ExpressionTag")) return "dynamic";
35
+ const text = nodes.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
36
+ return text.trim().length > 0 ? "static" : "absent";
37
+ }
38
+ function textFromNodes(nodes) {
39
+ if (!Array.isArray(nodes) || nodes.some((n) => n?.type === "ExpressionTag")) return void 0;
40
+ const text = nodes.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
41
+ return text.trim().length > 0 ? text : void 0;
42
+ }
43
+ function attrText(attributes, name) {
44
+ const attr = findAttr(attributes, name);
45
+ if (!attr) return void 0;
46
+ const v = attr.value;
47
+ if (v === true) return "";
48
+ if (Array.isArray(v)) {
49
+ if (v.some((n) => n?.type === "ExpressionTag")) return void 0;
50
+ return v.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
51
+ }
52
+ return void 0;
53
+ }
54
+ function attrValue(attributes, name) {
55
+ const attr = findAttr(attributes, name);
56
+ if (!attr) return "absent";
57
+ const v = attr.value;
58
+ if (v === true) return "absent";
59
+ if (Array.isArray(v)) return valueFromNodes(v);
60
+ if (v && v.type === "ExpressionTag") return "dynamic";
61
+ return "absent";
62
+ }
63
+ function lineOf(source, offset) {
64
+ if (typeof offset !== "number" || offset < 0) return 0;
65
+ let line = 1;
66
+ const end = Math.min(offset, source.length);
67
+ for (let i = 0; i < end; i++) if (source[i] === "\n") line++;
68
+ return line;
69
+ }
70
+ function findAttr(attributes, name) {
71
+ if (!Array.isArray(attributes)) return void 0;
72
+ return attributes.find((a) => a?.type === "Attribute" && a.name === name);
73
+ }
74
+ function attrValueOf(attr) {
75
+ const v = attr?.value;
76
+ if (v === true) return "absent";
77
+ if (Array.isArray(v)) return valueFromNodes(v);
78
+ if (v && v.type === "ExpressionTag") return "dynamic";
79
+ return "absent";
80
+ }
81
+ function attrTextOf(attr) {
82
+ const v = attr?.value;
83
+ if (!Array.isArray(v) || v.some((n) => n?.type === "ExpressionTag")) return void 0;
84
+ const text = v.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
85
+ return text.trim().length > 0 ? text : void 0;
86
+ }
87
+
88
+ // src/component-parse.ts
89
+ function isConstantListEach(node) {
90
+ const expr = node?.expression;
91
+ return expr?.type === "ArrayExpression" && Array.isArray(expr.elements) && !expr.elements.some((el) => el?.type === "SpreadElement");
92
+ }
93
+ function collectEachBlocks(node, source, acc) {
94
+ if (Array.isArray(node)) {
95
+ for (const child of node) collectEachBlocks(child, source, acc);
96
+ return;
97
+ }
98
+ if (!node || typeof node !== "object") return;
99
+ if (node.type === "EachBlock" && !isConstantListEach(node)) {
100
+ acc.push({ hasKey: node.key != null, line: lineOf(source, node.start) });
101
+ }
102
+ for (const key of CHILD_NODE_KEYS) {
103
+ if (key in node) collectEachBlocks(node[key], source, acc);
104
+ }
105
+ }
106
+ function walkEstree(node, visit) {
107
+ if (Array.isArray(node)) {
108
+ for (const child of node) walkEstree(child, visit);
109
+ return;
110
+ }
111
+ if (!node || typeof node !== "object" || typeof node.type !== "string") return;
112
+ visit(node);
113
+ for (const key of Object.keys(node)) {
114
+ if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range") continue;
115
+ walkEstree(node[key], visit);
116
+ }
117
+ }
118
+ function isEffectCall(node) {
119
+ const c = node?.callee;
120
+ if (c?.type === "Identifier") return c.name === "$effect";
121
+ if (c?.type === "MemberExpression" && c.object?.type === "Identifier" && c.object.name === "$effect") {
122
+ return c.property?.type === "Identifier" && c.property.name === "pre";
123
+ }
124
+ return false;
125
+ }
126
+ function isStateDeclaration(node) {
127
+ const c = node?.callee;
128
+ if (c?.type === "Identifier") return c.name === "$state";
129
+ if (c?.type === "MemberExpression" && c.object?.type === "Identifier" && c.object.name === "$state") {
130
+ return c.property?.type === "Identifier" && (c.property.name === "raw" || c.property.name === "frozen");
131
+ }
132
+ return false;
133
+ }
134
+ function bodyOnlyAssignsState(fn, stateNames) {
135
+ const isStateAssign = (expr) => expr?.type === "AssignmentExpression" && expr.operator === "=" && expr.left?.type === "Identifier" && stateNames.has(expr.left.name);
136
+ const body = fn?.body;
137
+ if (!body) return false;
138
+ if (body.type !== "BlockStatement") return isStateAssign(body);
139
+ if (body.body.length === 0) return false;
140
+ return body.body.every((s) => s?.type === "ExpressionStatement" && isStateAssign(s.expression));
141
+ }
142
+ function isDerivedDeclaration(node) {
143
+ const c = node?.callee;
144
+ if (c?.type === "Identifier") return c.name === "$derived";
145
+ if (c?.type === "MemberExpression" && c.object?.type === "Identifier" && c.object.name === "$derived") {
146
+ return c.property?.type === "Identifier" && c.property.name === "by";
147
+ }
148
+ return false;
149
+ }
150
+ function addBoundNames(id, acc) {
151
+ if (!id) return;
152
+ switch (id.type) {
153
+ case "Identifier":
154
+ acc.add(id.name);
155
+ break;
156
+ case "ObjectPattern":
157
+ for (const p of id.properties ?? []) {
158
+ if (p?.type === "Property") addBoundNames(p.value, acc);
159
+ else if (p?.type === "RestElement") addBoundNames(p.argument, acc);
160
+ }
161
+ break;
162
+ case "ArrayPattern":
163
+ for (const el of id.elements ?? []) addBoundNames(el, acc);
164
+ break;
165
+ case "AssignmentPattern":
166
+ addBoundNames(id.left, acc);
167
+ break;
168
+ case "RestElement":
169
+ addBoundNames(id.argument, acc);
170
+ break;
171
+ }
172
+ }
173
+ function rootObjectName(node) {
174
+ let cur = node;
175
+ while (cur?.type === "MemberExpression") cur = cur.object;
176
+ return cur?.type === "Identifier" ? cur.name : void 0;
177
+ }
178
+ function collectStateWrites(root, stateNames, acc) {
179
+ walkEstree(root, (n) => {
180
+ if (n?.type === "AssignmentExpression") {
181
+ if (n.left?.type === "Identifier" && stateNames.has(n.left.name)) acc.add(n.left.name);
182
+ else if (n.left?.type === "MemberExpression") {
183
+ const r = rootObjectName(n.left);
184
+ if (r && stateNames.has(r)) acc.add(r);
185
+ } else if (n.left?.type === "ObjectPattern" || n.left?.type === "ArrayPattern") {
186
+ const bound = /* @__PURE__ */ new Set();
187
+ addBoundNames(n.left, bound);
188
+ for (const name of bound) if (stateNames.has(name)) acc.add(name);
189
+ }
190
+ } else if (n?.type === "UpdateExpression") {
191
+ const r = rootObjectName(n.argument);
192
+ if (r && stateNames.has(r)) acc.add(r);
193
+ } else if (n?.type === "UnaryExpression" && n.operator === "delete") {
194
+ const r = rootObjectName(n.argument);
195
+ if (r && stateNames.has(r)) acc.add(r);
196
+ } else if (n?.type === "CallExpression") {
197
+ if (n.callee?.type === "MemberExpression") {
198
+ const r = rootObjectName(n.callee);
199
+ if (r && stateNames.has(r)) acc.add(r);
200
+ }
201
+ for (const a of n.arguments ?? []) {
202
+ const arg = a?.type === "SpreadElement" ? a.argument : a;
203
+ const r = rootObjectName(arg);
204
+ if (r && stateNames.has(r)) acc.add(r);
205
+ }
206
+ }
207
+ });
208
+ }
209
+ var COMPONENT_LIKE_TYPES = /* @__PURE__ */ new Set(["Component", "SvelteComponent", "SvelteSelf"]);
210
+ function collectTemplateEscapes(node, stateNames, acc) {
211
+ if (Array.isArray(node)) {
212
+ for (const c of node) collectTemplateEscapes(c, stateNames, acc);
213
+ return;
214
+ }
215
+ if (!node || typeof node !== "object" || typeof node.type !== "string") return;
216
+ if (Array.isArray(node.attributes)) {
217
+ for (const attr of node.attributes) {
218
+ if (attr?.type === "BindDirective") {
219
+ const r = rootObjectName(attr.expression);
220
+ if (r && stateNames.has(r)) acc.add(r);
221
+ } else if (COMPONENT_LIKE_TYPES.has(node.type)) {
222
+ walkEstree(attr, (m) => {
223
+ if (m?.type === "Identifier" && stateNames.has(m.name)) acc.add(m.name);
224
+ });
225
+ }
226
+ }
227
+ }
228
+ for (const key of CHILD_NODE_KEYS) {
229
+ if (key in node) collectTemplateEscapes(node[key], stateNames, acc);
230
+ }
231
+ }
232
+ var RUNE_NAMES = /* @__PURE__ */ new Set(["$state", "$derived", "$effect", "$props", "$bindable", "$inspect", "$host"]);
233
+ function bodyReadsReactive(fn, reactiveNames) {
234
+ let reads = false;
235
+ const IGNORED_KEYS = /* @__PURE__ */ new Set(["type", "start", "end", "loc", "range"]);
236
+ const visit = (n) => {
237
+ if (reads || !n) return;
238
+ if (Array.isArray(n)) {
239
+ for (const c of n) visit(c);
240
+ return;
241
+ }
242
+ if (typeof n !== "object" || typeof n.type !== "string") return;
243
+ if (n.type === "Identifier") {
244
+ if (reactiveNames.has(n.name) || n.name.startsWith("$") && !RUNE_NAMES.has(n.name)) reads = true;
245
+ return;
246
+ }
247
+ if (n.type === "CallExpression" && n.callee?.type === "Identifier") {
248
+ reads = true;
249
+ return;
250
+ }
251
+ if (n.type === "MemberExpression") {
252
+ visit(n.object);
253
+ if (n.computed) visit(n.property);
254
+ return;
255
+ }
256
+ if (n.type === "Property") {
257
+ if (n.computed) visit(n.key);
258
+ visit(n.value);
259
+ return;
260
+ }
261
+ for (const key of Object.keys(n)) {
262
+ if (!IGNORED_KEYS.has(key)) visit(n[key]);
263
+ }
264
+ };
265
+ visit(fn.body);
266
+ return reads;
267
+ }
268
+ function bodyIsEmpty(fn) {
269
+ const body = fn?.body;
270
+ if (!body) return true;
271
+ if (body.type === "BlockStatement") return (body.body ?? []).length === 0;
272
+ return false;
273
+ }
274
+ var URL_ATTRS = ["href", "src", "action", "formaction"];
275
+ function collectSecurityFacts(node, source, htmlTags, jsUrls) {
276
+ if (Array.isArray(node)) {
277
+ for (const child of node) collectSecurityFacts(child, source, htmlTags, jsUrls);
278
+ return;
279
+ }
280
+ if (!node || typeof node !== "object") return;
281
+ if (node.type === "HtmlTag") htmlTags.push({ line: lineOf(source, node.start) });
282
+ if ((node.type === "RegularElement" || node.type === "SvelteElement") && Array.isArray(node.attributes)) {
283
+ for (const name of URL_ATTRS) {
284
+ const attr = findAttr(node.attributes, name);
285
+ if (!attr) continue;
286
+ const value = attrTextOf(attr);
287
+ if (value !== void 0 && /^\s*javascript:/i.test(value)) {
288
+ jsUrls.push({ line: lineOf(source, attr.start ?? node.start) });
289
+ }
290
+ }
291
+ }
292
+ for (const key of CHILD_NODE_KEYS) {
293
+ if (key in node) collectSecurityFacts(node[key], source, htmlTags, jsUrls);
294
+ }
295
+ }
296
+ function isPropsCall(node) {
297
+ return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "$props";
298
+ }
299
+ function countProps(program) {
300
+ let count = 0;
301
+ let seen = 0;
302
+ let uncountable = false;
303
+ walkEstree(program, (n) => {
304
+ if (n.type !== "VariableDeclarator" || !n.init || !isPropsCall(n.init)) return;
305
+ seen++;
306
+ const props = n.id?.type === "ObjectPattern" ? n.id.properties : void 0;
307
+ if (!Array.isArray(props) || props.some((p) => p?.type === "RestElement")) {
308
+ uncountable = true;
309
+ return;
310
+ }
311
+ count = props.filter((p) => p?.type === "Property").length;
312
+ });
313
+ return uncountable || seen > 1 ? 0 : count;
314
+ }
315
+ function countLines(source) {
316
+ if (source.length === 0) return 0;
317
+ return source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
318
+ }
319
+ function collectImportSources(program, acc) {
320
+ walkEstree(program, (n) => {
321
+ if (n.type === "ImportDeclaration" && typeof n.source?.value === "string") acc.push(n.source.value);
322
+ });
323
+ }
324
+ function isBareSpecifier(s) {
325
+ return !/^[./$#]/.test(s);
326
+ }
327
+ function collectNamespaceImports(program, source, acc) {
328
+ walkEstree(program, (n) => {
329
+ if (n.type !== "ImportDeclaration" || n.importKind === "type") return;
330
+ const spec = n.source?.value;
331
+ if (typeof spec !== "string" || !isBareSpecifier(spec)) return;
332
+ if (Array.isArray(n.specifiers) && n.specifiers.some((s) => s?.type === "ImportNamespaceSpecifier")) {
333
+ acc.push({ source: spec, line: lineOf(source, n.start) });
334
+ }
335
+ });
336
+ }
337
+ var JS_DIRECTIVE = /^\s*\/\/\s*svelte-vitals-disable-next-line(?:\s+([A-Za-z]+\d+(?:\s*,\s*[A-Za-z]+\d+)*))?\s*$/;
338
+ var HTML_DIRECTIVE = /^\s*<!--\s*svelte-vitals-disable-next-line(?:\s+([A-Za-z]+\d+(?:\s*,\s*[A-Za-z]+\d+)*))?\s*-->\s*$/;
339
+ function collectSuppressions(source) {
340
+ const out = [];
341
+ const lines = source.split("\n");
342
+ lines.forEach((line, i) => {
343
+ const m = JS_DIRECTIVE.exec(line) ?? HTML_DIRECTIVE.exec(line);
344
+ if (!m) return;
345
+ const ruleIds = m[1]?.split(",").map((s) => s.trim().toUpperCase());
346
+ out.push({ line: i + 2, ruleIds });
347
+ });
348
+ return out;
349
+ }
350
+ function parseComponentFacts(source, filename) {
351
+ const ast = parse(source, { modern: true, filename });
352
+ const eachBlocks = [];
353
+ collectEachBlocks(ast.fragment ?? ast, source, eachBlocks);
354
+ const htmlTags = [];
355
+ const javascriptUrls = [];
356
+ collectSecurityFacts(ast.fragment ?? ast, source, htmlTags, javascriptUrls);
357
+ const loc = countLines(source);
358
+ const suppressions = collectSuppressions(source);
359
+ const imports = [];
360
+ const namespaceImports = [];
361
+ if (ast.module?.content) {
362
+ collectImportSources(ast.module.content, imports);
363
+ collectNamespaceImports(ast.module.content, source, namespaceImports);
364
+ }
365
+ const effects = [];
366
+ const constableStates = [];
367
+ let propCount = 0;
368
+ const program = ast.instance?.content;
369
+ if (program) {
370
+ collectImportSources(program, imports);
371
+ collectNamespaceImports(program, source, namespaceImports);
372
+ propCount = countProps(program);
373
+ const stateNames = /* @__PURE__ */ new Set();
374
+ const reactiveNames = /* @__PURE__ */ new Set();
375
+ const stateDecls = [];
376
+ walkEstree(program, (n) => {
377
+ if (n.type !== "VariableDeclarator" || !n.init) return;
378
+ if (isStateDeclaration(n.init) && n.id?.type === "Identifier") {
379
+ stateNames.add(n.id.name);
380
+ stateDecls.push({ name: n.id.name, line: lineOf(source, n.start) });
381
+ }
382
+ if (isStateDeclaration(n.init) || isDerivedDeclaration(n.init) || isPropsCall(n.init))
383
+ addBoundNames(n.id, reactiveNames);
384
+ });
385
+ walkEstree(program, (n) => {
386
+ if (n.type !== "CallExpression" || !isEffectCall(n)) return;
387
+ const fn = n.arguments?.[0];
388
+ const isFn = fn?.type === "ArrowFunctionExpression" || fn?.type === "FunctionExpression";
389
+ effects.push({
390
+ line: lineOf(source, n.start),
391
+ assignsOnlyState: isFn ? bodyOnlyAssignsState(fn, stateNames) : false,
392
+ mountOnly: isFn ? !bodyIsEmpty(fn) && !bodyReadsReactive(fn, reactiveNames) : false
393
+ });
394
+ });
395
+ const writtenOrEscaped = /* @__PURE__ */ new Set();
396
+ collectStateWrites(program, stateNames, writtenOrEscaped);
397
+ if (ast.fragment) {
398
+ collectStateWrites(ast.fragment, stateNames, writtenOrEscaped);
399
+ collectTemplateEscapes(ast.fragment, stateNames, writtenOrEscaped);
400
+ }
401
+ for (const d of stateDecls) {
402
+ if (!writtenOrEscaped.has(d.name)) constableStates.push(d);
403
+ }
404
+ }
405
+ return {
406
+ eachBlocks,
407
+ effects,
408
+ htmlTags,
409
+ javascriptUrls,
410
+ loc,
411
+ propCount,
412
+ imports,
413
+ namespaceImports,
414
+ constableStates,
415
+ suppressions
416
+ };
417
+ }
418
+
17
419
  // src/project-paths.ts
18
420
  var ROBOTS_SOURCE_PATHS = [
19
421
  "static/robots.txt",
@@ -1438,6 +1840,9 @@ var seo030HeadingOrder = {
1438
1840
  // src/rules/component-rule.ts
1439
1841
  var PENALIZED2 = { presence: "none", value: "absent" };
1440
1842
  var PASS2 = { presence: "own", value: "static" };
1843
+ function isSuppressed(c, ruleId, line) {
1844
+ return (c.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
1845
+ }
1441
1846
  function componentRule(opts) {
1442
1847
  const docsUrl7 = docsUrlFor(opts.id);
1443
1848
  const severity = opts.severity ?? "warning";
@@ -1452,7 +1857,7 @@ function componentRule(opts) {
1452
1857
  const out = [];
1453
1858
  for (const c of ctx.components ?? []) {
1454
1859
  if (!opts.applies(c)) continue;
1455
- const bad = opts.bad(c);
1860
+ const bad = opts.bad(c).filter((b) => !(b.line > 0 && isSuppressed(c, opts.id, b.line)));
1456
1861
  if (bad.length === 0) {
1457
1862
  out.push({
1458
1863
  id: opts.id,
@@ -1507,6 +1912,32 @@ var correct002EffectDerived = componentRule({
1507
1912
  applies: (c) => c.effects.length > 0,
1508
1913
  bad: (c) => c.effects.filter((e) => e.assignsOnlyState).map((e) => ({ line: e.line, message: "$effect only assigns state \u2014 use $derived instead" }))
1509
1914
  });
1915
+ var correct003EffectAsOnMount = componentRule({
1916
+ id: "CORRECT003",
1917
+ title: "Effect used as onMount",
1918
+ category: "correctness",
1919
+ label: "$effect usage",
1920
+ recommendation: "Move mount-time side effects to onMount (import { onMount } from 'svelte'); reserve $effect for logic that reacts to $state/$derived/$props.",
1921
+ rationale: "An $effect that reads no reactive value runs once after mount and never re-runs \u2014 it is an onMount in disguise, which obscures intent and misuses the reactivity system.",
1922
+ applies: (c) => c.effects.length > 0,
1923
+ bad: (c) => c.effects.filter((e) => e.mountOnly).map((e) => ({ line: e.line, message: "$effect reads no reactive value \u2014 use onMount instead" }))
1924
+ });
1925
+
1926
+ // src/rules/correctness/correct004-unmutated-state.ts
1927
+ var correct004UnmutatedState = componentRule({
1928
+ id: "CORRECT004",
1929
+ title: "Unmutated $state",
1930
+ category: "correctness",
1931
+ severity: "info",
1932
+ label: "$state usage",
1933
+ recommendation: "If a value never changes, use const; if you only ever reassign it wholesale (never mutate its properties), use $state.raw to skip deep proxying.",
1934
+ rationale: "A $state that is never mutated pays for reactivity (deep proxying, tracking) it never uses; const (or $state.raw) is clearer and cheaper.",
1935
+ applies: (c) => c.constableStates.length > 0,
1936
+ bad: (c) => c.constableStates.map((s) => ({
1937
+ line: s.line,
1938
+ message: `$state "${s.name}" is never mutated \u2014 use const (or $state.raw if you only reassign it)`
1939
+ }))
1940
+ });
1510
1941
 
1511
1942
  // src/rules/security/sec001-002.ts
1512
1943
  var sec001Html = componentRule({
@@ -1530,6 +1961,83 @@ var sec002JavascriptUrl = componentRule({
1530
1961
  bad: (c) => c.javascriptUrls.map((u) => ({ line: u.line, message: "javascript: URL in an attribute" }))
1531
1962
  });
1532
1963
 
1964
+ // src/rules/architecture/arch001-002.ts
1965
+ var MAX_LOC = 400;
1966
+ var MAX_PROPS = 10;
1967
+ var arch001ComponentSize = componentRule({
1968
+ id: "ARCH001",
1969
+ title: "Component size",
1970
+ category: "architecture",
1971
+ severity: "info",
1972
+ label: "Component size",
1973
+ recommendation: `Split components over ${MAX_LOC} lines into smaller, focused pieces.`,
1974
+ rationale: "A very large component is hard to read, test, and reuse, and is a common sign that several responsibilities should be split out.",
1975
+ applies: (c) => c.loc > 0,
1976
+ // skip unanalyzable files (loc 0 = read/parse failure), don't PASS them
1977
+ bad: (c) => c.loc > MAX_LOC ? [{ line: 1, message: `Component is ${c.loc} lines (over ${MAX_LOC})` }] : []
1978
+ });
1979
+ var arch002PropCount = componentRule({
1980
+ id: "ARCH002",
1981
+ title: "Prop count",
1982
+ category: "architecture",
1983
+ severity: "info",
1984
+ label: "Prop count",
1985
+ recommendation: `Group related props into an object, or split the component, when it takes more than ${MAX_PROPS} props.`,
1986
+ rationale: "A component taking many props is usually doing too much; grouping or splitting keeps its API understandable.",
1987
+ applies: (c) => c.propCount > 0,
1988
+ // only components whose props we could count
1989
+ bad: (c) => c.propCount > MAX_PROPS ? [{ line: 1, message: `Component takes ${c.propCount} props (over ${MAX_PROPS})` }] : []
1990
+ });
1991
+
1992
+ // src/rules/performance/perf009-heavy-import.ts
1993
+ var HEAVY_PACKAGES = {
1994
+ lodash: "import a submodule (lodash/debounce) or use lodash-es for tree-shaking",
1995
+ moment: "use a lighter date library (date-fns or dayjs) \u2014 moment is large and not tree-shakeable"
1996
+ };
1997
+ var perf009HeavyImport = componentRule({
1998
+ id: "PERF009",
1999
+ title: "Heavy dependency import",
2000
+ category: "performance",
2001
+ severity: "info",
2002
+ label: "No heavy imports",
2003
+ recommendation: "Import a submodule or switch to a lighter, tree-shakeable alternative.",
2004
+ rationale: "Importing a large, non-tree-shakeable package pulls its whole weight into the bundle even when only a fraction is used, slowing load.",
2005
+ applies: (c) => c.imports.length > 0,
2006
+ bad: (c) => {
2007
+ const seen = /* @__PURE__ */ new Set();
2008
+ const out = [];
2009
+ for (const src of c.imports) {
2010
+ if (!Object.hasOwn(HEAVY_PACKAGES, src) || seen.has(src)) continue;
2011
+ seen.add(src);
2012
+ out.push({ line: 0, message: `Heavy import "${src}" \u2014 ${HEAVY_PACKAGES[src]}` });
2013
+ }
2014
+ return out;
2015
+ }
2016
+ });
2017
+
2018
+ // src/rules/performance/perf010-namespace-import.ts
2019
+ var perf010NamespaceImport = componentRule({
2020
+ id: "PERF010",
2021
+ title: "Namespace import",
2022
+ category: "performance",
2023
+ severity: "info",
2024
+ label: "No namespace imports",
2025
+ recommendation: "Use named imports (import { x } from 'pkg') instead of import * as \u2014 a namespace import keeps the whole module in the bundle.",
2026
+ rationale: "A namespace import (import * as X) forces the bundler to retain the entire module, so unused exports cannot be tree-shaken out.",
2027
+ applies: (c) => c.namespaceImports.length > 0,
2028
+ bad: (c) => {
2029
+ const minLine = /* @__PURE__ */ new Map();
2030
+ for (const ns of c.namespaceImports) {
2031
+ const prev = minLine.get(ns.source);
2032
+ if (prev === void 0 || ns.line < prev) minLine.set(ns.source, ns.line);
2033
+ }
2034
+ return [...minLine.entries()].sort((a, b) => a[1] - b[1]).map(([source, line]) => ({
2035
+ line,
2036
+ message: `Namespace import "* as \u2026 from '${source}'" \u2014 prefer named imports so the bundler can tree-shake`
2037
+ }));
2038
+ }
2039
+ });
2040
+
1533
2041
  // src/rules/index.ts
1534
2042
  var allRules = [
1535
2043
  seo001Title,
@@ -1572,8 +2080,14 @@ var allRules = [
1572
2080
  seo030HeadingOrder,
1573
2081
  correct001EachKey,
1574
2082
  correct002EffectDerived,
2083
+ correct003EffectAsOnMount,
2084
+ correct004UnmutatedState,
1575
2085
  sec001Html,
1576
- sec002JavascriptUrl
2086
+ sec002JavascriptUrl,
2087
+ arch001ComponentSize,
2088
+ arch002PropCount,
2089
+ perf009HeavyImport,
2090
+ perf010NamespaceImport
1577
2091
  ];
1578
2092
  function explainRule(id) {
1579
2093
  const target = id.toUpperCase();
@@ -1700,6 +2214,21 @@ function computeHealth(results, config) {
1700
2214
  return { health, categories, weights };
1701
2215
  }
1702
2216
 
2217
+ // src/reporter/palette.ts
2218
+ var noColorPalette = {
2219
+ bold: (s) => s,
2220
+ dim: (s) => s,
2221
+ red: (s) => s,
2222
+ yellow: (s) => s,
2223
+ green: (s) => s,
2224
+ cyan: (s) => s
2225
+ };
2226
+ function scoreColor(p, score) {
2227
+ if (score >= 90) return p.green;
2228
+ if (score >= 70) return p.yellow;
2229
+ return p.red;
2230
+ }
2231
+
1703
2232
  // src/reporter/console.ts
1704
2233
  var RULE = "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
1705
2234
  var SEVERITY_TITLE = {
@@ -1711,66 +2240,77 @@ var CATEGORY_LABEL = {
1711
2240
  seo: "SEO",
1712
2241
  performance: "Performance",
1713
2242
  correctness: "Correctness",
1714
- security: "Security"
2243
+ security: "Security",
2244
+ architecture: "Architecture"
1715
2245
  };
1716
- var CATEGORY_ORDER = ["seo", "performance", "correctness", "security"];
1717
- function scoreLine(label, { score, scoreModel }) {
2246
+ var CATEGORY_ORDER = ["seo", "performance", "correctness", "security", "architecture"];
2247
+ function scoreLine(p, label, { score, scoreModel }) {
1718
2248
  const parts = [`route avg ${scoreModel.routeAverage}`];
1719
2249
  if (scoreModel.sitePenalty > 0) parts.push(`site \u2212${scoreModel.sitePenalty}`);
1720
2250
  if (scoreModel.criticalCap !== null) parts.push(`capped at ${scoreModel.criticalCap}: critical present`);
1721
- return `${label} Score: ${score}/100 (${parts.join(" \xB7 ")})`;
2251
+ return `${label} Score: ${scoreColor(p, score)(`${score}/100`)} ${p.dim(`(${parts.join(" \xB7 ")})`)}`;
1722
2252
  }
1723
- function byRouteTree(results, config) {
2253
+ function byRouteTree(p, results, config) {
1724
2254
  const routes = /* @__PURE__ */ new Map();
1725
2255
  for (const r of results) {
1726
2256
  if (r.route === void 0) continue;
1727
2257
  if (!routes.has(r.route)) routes.set(r.route, []);
1728
2258
  routes.get(r.route).push(r);
1729
2259
  }
1730
- const lines = ["By route", RULE];
2260
+ const lines = [p.bold("By route"), p.dim(RULE)];
1731
2261
  for (const [route, rs] of [...routes.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
1732
2262
  const { score } = computeScore(rs, config, { applyCriticalCap: false });
1733
- lines.push(`${route.padEnd(28)} ${score}`);
2263
+ lines.push(`${route.padEnd(28)} ${scoreColor(p, score)(`${score}`)}`);
1734
2264
  for (const r of rs.filter((x) => classify(x, config) === "fail")) {
1735
- lines.push(` \u2717 ${r.id} ${r.message}`);
2265
+ lines.push(` ${p.red("\u2717")} ${r.id} ${r.message}`);
1736
2266
  }
1737
2267
  }
1738
2268
  lines.push("");
1739
2269
  return lines;
1740
2270
  }
1741
2271
  function formatConsoleReport(results, config, options = {}) {
2272
+ const p = options.palette ?? noColorPalette;
1742
2273
  const summary = summarize(results, config);
1743
2274
  const { health, categories: byCat } = computeHealth(results, config);
1744
2275
  const present2 = CATEGORY_ORDER.filter((c) => byCat[c] !== void 0);
1745
- const header = [`Svelte Vitals \xB7 ${options.mode ?? "static mode"}`, "", `Health: ${health}/100`];
2276
+ const header = [
2277
+ p.bold(`Svelte Vitals \xB7 ${options.mode ?? "static mode"}`),
2278
+ "",
2279
+ `${p.bold("Health:")} ${scoreColor(p, health)(`${health}/100`)}`
2280
+ ];
1746
2281
  for (const c of present2) {
1747
- header.push(scoreLine(CATEGORY_LABEL[c] ?? c, byCat[c]));
2282
+ header.push(scoreLine(p, CATEGORY_LABEL[c] ?? c, byCat[c]));
1748
2283
  }
1749
2284
  const lines = [...header, ""];
2285
+ const SEVERITY_COLOR = {
2286
+ critical: (s) => p.red(p.bold(s)),
2287
+ warning: (s) => p.yellow(p.bold(s)),
2288
+ info: (s) => p.dim(s)
2289
+ };
1750
2290
  const failures = results.filter((r) => classify(r, config) === "fail");
1751
2291
  for (const severity of ["critical", "warning", "info"]) {
1752
2292
  const bucket = failures.filter((r) => effectiveSeverity(r, config) === severity);
1753
2293
  if (bucket.length === 0) continue;
1754
- lines.push(`${SEVERITY_TITLE[severity]} (${bucket.length})`, RULE);
2294
+ lines.push(SEVERITY_COLOR[severity](`${SEVERITY_TITLE[severity]} (${bucket.length})`), p.dim(RULE));
1755
2295
  for (const r of bucket) {
1756
- lines.push(`\u2717 ${r.id} ${r.message}`);
1757
- if (r.route) lines.push(` ${r.route}`);
1758
- if (r.location) lines.push(` ${r.location}${r.line ? `:${r.line}` : ""}`);
2296
+ lines.push(`${p.red("\u2717")} ${r.id} ${r.message}`);
2297
+ if (r.route) lines.push(p.dim(` ${r.route}`));
2298
+ if (r.location) lines.push(p.dim(` ${r.location}${r.line ? `:${r.line}` : ""}`));
1759
2299
  }
1760
2300
  lines.push("");
1761
2301
  }
1762
2302
  const passed = results.filter((r) => classify(r, config) !== "fail");
1763
2303
  if (passed.length > 0) {
1764
- lines.push(`Passed (${passed.length})`, RULE);
2304
+ lines.push(p.bold(`Passed (${passed.length})`), p.dim(RULE));
1765
2305
  for (const r of passed) {
1766
- const marker = classify(r, config) === "dynamic" ? " \u21AF dynamic" : "";
2306
+ const marker = classify(r, config) === "dynamic" ? p.cyan(" \u21AF dynamic") : "";
1767
2307
  const route = r.route ? ` ${r.route}` : "";
1768
- lines.push(`\u2713 ${r.id} ${r.message}${marker}${route}`);
2308
+ lines.push(`${p.green("\u2713")} ${r.id} ${r.message}${marker}${route}`);
1769
2309
  }
1770
2310
  lines.push("");
1771
2311
  }
1772
- if (options.byRoute) lines.push(...byRouteTree(results, config));
1773
- if (summary.dynamic > 0) lines.push("\u21AF = set dynamically (verified at runtime).");
2312
+ if (options.byRoute) lines.push(...byRouteTree(p, results, config));
2313
+ if (summary.dynamic > 0) lines.push(p.dim("\u21AF = set dynamically (verified at runtime)."));
1774
2314
  return lines.join("\n").replace(/\n+$/, "\n");
1775
2315
  }
1776
2316
 
@@ -2157,10 +2697,17 @@ function applyRuleSeverities(results, config) {
2157
2697
  }
2158
2698
  export {
2159
2699
  BAND_COLOR,
2700
+ CHILD_NODE_KEYS,
2160
2701
  ROBOTS_SOURCE_PATHS,
2161
2702
  SITEMAP_SOURCE_PATHS,
2162
2703
  allRules,
2163
2704
  applyRuleSeverities,
2705
+ arch001ComponentSize,
2706
+ arch002PropCount,
2707
+ attrText,
2708
+ attrTextOf,
2709
+ attrValue,
2710
+ attrValueOf,
2164
2711
  buildHtmlDocument,
2165
2712
  buildJsonReport,
2166
2713
  classify,
@@ -2168,6 +2715,8 @@ export {
2168
2715
  computeScore,
2169
2716
  correct001EachKey,
2170
2717
  correct002EffectDerived,
2718
+ correct003EffectAsOnMount,
2719
+ correct004UnmutatedState,
2171
2720
  defaultConfig,
2172
2721
  defaultProject,
2173
2722
  defineConfig,
@@ -2175,6 +2724,7 @@ export {
2175
2724
  effectiveSeverity,
2176
2725
  escapeHtml,
2177
2726
  explainRule,
2727
+ findAttr,
2178
2728
  formatAgentReport,
2179
2729
  formatConsoleReport,
2180
2730
  formatGithubReport,
@@ -2185,7 +2735,10 @@ export {
2185
2735
  headTagRule,
2186
2736
  imageRule,
2187
2737
  isPenalized,
2738
+ lineOf,
2188
2739
  linkRule,
2740
+ noColorPalette,
2741
+ parseComponentFacts,
2189
2742
  perf001ImageDimensions,
2190
2743
  perf002ImageLoading,
2191
2744
  perf003PreloadAs,
@@ -2194,9 +2747,12 @@ export {
2194
2747
  perf006ResponsiveImage,
2195
2748
  perf007RenderBlockingScript,
2196
2749
  perf008Preconnect,
2750
+ perf009HeavyImport,
2751
+ perf010NamespaceImport,
2197
2752
  runRules,
2198
2753
  safeHref,
2199
2754
  scoreBand,
2755
+ scoreColor,
2200
2756
  scoresByCategory,
2201
2757
  sec001Html,
2202
2758
  sec002JavascriptUrl,
@@ -2231,5 +2787,7 @@ export {
2231
2787
  seo028TitleUnique,
2232
2788
  seo029DescriptionUnique,
2233
2789
  seo030HeadingOrder,
2234
- summarize
2790
+ summarize,
2791
+ textFromNodes,
2792
+ valueFromNodes
2235
2793
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.16.0",
3
+ "version": "0.19.0",
4
4
  "description": "Shared, runtime-agnostic core for svelte-vitals (types, rule engine, scorer, reporter).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,9 +21,12 @@
21
21
  },
22
22
  "homepage": "https://github.com/oekazuma/svelte-vitals#readme",
23
23
  "engines": {
24
- "node": ">=18"
24
+ "node": ">=18.20.8"
25
25
  },
26
26
  "sideEffects": false,
27
+ "dependencies": {
28
+ "svelte": "^5.56.4"
29
+ },
27
30
  "exports": {
28
31
  ".": {
29
32
  "types": "./dist/index.d.ts",