@svelte-vitals/core 0.16.0 → 0.18.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 +47 -3
- package/dist/index.js +163 -20
- package/package.json +2 -2
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,15 @@ 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
|
+
/** Reactivity/correctness + security + architecture facts parsed from one `.svelte` component. */
|
|
226
228
|
interface ComponentFacts {
|
|
227
229
|
/** Source file the component came from. */
|
|
228
230
|
file: string;
|
|
@@ -232,6 +234,22 @@ interface ComponentFacts {
|
|
|
232
234
|
htmlTags: SourceSpan[];
|
|
233
235
|
/** Element attributes with a literal `javascript:` URL (Security SEC002). */
|
|
234
236
|
javascriptUrls: SourceSpan[];
|
|
237
|
+
/** Source line count of the component file (Architecture ARCH001). */
|
|
238
|
+
loc: number;
|
|
239
|
+
/** Named props destructured from `$props()`; 0 when unknowable (rest / non-destructured) (Architecture ARCH002). */
|
|
240
|
+
propCount: number;
|
|
241
|
+
/** Module specifiers of every `import` in the instance + module scripts (Bundle PERF009). */
|
|
242
|
+
imports: string[];
|
|
243
|
+
/** Value `import * as X from '<bare pkg>'` namespace imports (type-only excluded) — Bundle PERF010. */
|
|
244
|
+
namespaceImports: {
|
|
245
|
+
source: string;
|
|
246
|
+
line: number;
|
|
247
|
+
}[];
|
|
248
|
+
/** `$state` declarations never written or escaped anywhere in the component — candidates for const (CORRECT004). */
|
|
249
|
+
constableStates: {
|
|
250
|
+
name: string;
|
|
251
|
+
line: number;
|
|
252
|
+
}[];
|
|
235
253
|
}
|
|
236
254
|
|
|
237
255
|
/**
|
|
@@ -404,10 +422,20 @@ declare const seo030HeadingOrder: Rule;
|
|
|
404
422
|
|
|
405
423
|
declare const correct001EachKey: Rule;
|
|
406
424
|
declare const correct002EffectDerived: Rule;
|
|
425
|
+
declare const correct003EffectAsOnMount: Rule;
|
|
426
|
+
|
|
427
|
+
declare const correct004UnmutatedState: Rule;
|
|
407
428
|
|
|
408
429
|
declare const sec001Html: Rule;
|
|
409
430
|
declare const sec002JavascriptUrl: Rule;
|
|
410
431
|
|
|
432
|
+
declare const arch001ComponentSize: Rule;
|
|
433
|
+
declare const arch002PropCount: Rule;
|
|
434
|
+
|
|
435
|
+
declare const perf009HeavyImport: Rule;
|
|
436
|
+
|
|
437
|
+
declare const perf010NamespaceImport: Rule;
|
|
438
|
+
|
|
411
439
|
declare const allRules: Rule[];
|
|
412
440
|
|
|
413
441
|
interface RuleInfo {
|
|
@@ -498,10 +526,26 @@ declare function summarize(results: Result[], config: Config): Summary;
|
|
|
498
526
|
/** Whether the run should fail the build/CI per the minimum failing severity. */
|
|
499
527
|
declare function hasFailureAtOrAbove(summary: Summary, min: Severity): boolean;
|
|
500
528
|
|
|
529
|
+
/** String decorators for the console reporter. Injected so core stays pure/dep-free. */
|
|
530
|
+
interface Palette {
|
|
531
|
+
bold: (s: string) => string;
|
|
532
|
+
dim: (s: string) => string;
|
|
533
|
+
red: (s: string) => string;
|
|
534
|
+
yellow: (s: string) => string;
|
|
535
|
+
green: (s: string) => string;
|
|
536
|
+
cyan: (s: string) => string;
|
|
537
|
+
}
|
|
538
|
+
/** Default: no decoration (identity) — output is byte-identical to plain text. */
|
|
539
|
+
declare const noColorPalette: Palette;
|
|
540
|
+
/** Green ≥ 90, yellow ≥ 70, red otherwise — for a 0–100 score. */
|
|
541
|
+
declare function scoreColor(p: Palette, score: number): (s: string) => string;
|
|
542
|
+
|
|
501
543
|
interface ConsoleReportOptions {
|
|
502
544
|
byRoute?: boolean;
|
|
503
545
|
/** Mode label shown in the header (default 'static mode'). */
|
|
504
546
|
mode?: string;
|
|
547
|
+
/** Color decorators; defaults to no color. */
|
|
548
|
+
palette?: Palette;
|
|
505
549
|
}
|
|
506
550
|
/**
|
|
507
551
|
* Render results as a console report string (design §7). Pure: returns a string,
|
|
@@ -616,4 +660,4 @@ declare function selectRules(rules: Rule[], config: Config): Rule[];
|
|
|
616
660
|
/** Apply per-rule severity overrides to results (design §6). */
|
|
617
661
|
declare function applyRuleSeverities(results: Result[], config: Config): Result[];
|
|
618
662
|
|
|
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 };
|
|
663
|
+
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 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 TreatDynamicAs, type Value, allRules, applyRuleSeverities, arch001ComponentSize, arch002PropCount, buildHtmlDocument, buildJsonReport, classify, computeHealth, computeScore, correct001EachKey, correct002EffectDerived, correct003EffectAsOnMount, correct004UnmutatedState, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, escapeHtml, explainRule, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, linkRule, noColorPalette, 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 };
|
package/dist/index.js
CHANGED
|
@@ -1507,6 +1507,32 @@ var correct002EffectDerived = componentRule({
|
|
|
1507
1507
|
applies: (c) => c.effects.length > 0,
|
|
1508
1508
|
bad: (c) => c.effects.filter((e) => e.assignsOnlyState).map((e) => ({ line: e.line, message: "$effect only assigns state \u2014 use $derived instead" }))
|
|
1509
1509
|
});
|
|
1510
|
+
var correct003EffectAsOnMount = componentRule({
|
|
1511
|
+
id: "CORRECT003",
|
|
1512
|
+
title: "Effect used as onMount",
|
|
1513
|
+
category: "correctness",
|
|
1514
|
+
label: "$effect usage",
|
|
1515
|
+
recommendation: "Move mount-time side effects to onMount (import { onMount } from 'svelte'); reserve $effect for logic that reacts to $state/$derived/$props.",
|
|
1516
|
+
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.",
|
|
1517
|
+
applies: (c) => c.effects.length > 0,
|
|
1518
|
+
bad: (c) => c.effects.filter((e) => e.mountOnly).map((e) => ({ line: e.line, message: "$effect reads no reactive value \u2014 use onMount instead" }))
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
// src/rules/correctness/correct004-unmutated-state.ts
|
|
1522
|
+
var correct004UnmutatedState = componentRule({
|
|
1523
|
+
id: "CORRECT004",
|
|
1524
|
+
title: "Unmutated $state",
|
|
1525
|
+
category: "correctness",
|
|
1526
|
+
severity: "info",
|
|
1527
|
+
label: "$state usage",
|
|
1528
|
+
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.",
|
|
1529
|
+
rationale: "A $state that is never mutated pays for reactivity (deep proxying, tracking) it never uses; const (or $state.raw) is clearer and cheaper.",
|
|
1530
|
+
applies: (c) => c.constableStates.length > 0,
|
|
1531
|
+
bad: (c) => c.constableStates.map((s) => ({
|
|
1532
|
+
line: s.line,
|
|
1533
|
+
message: `$state "${s.name}" is never mutated \u2014 use const (or $state.raw if you only reassign it)`
|
|
1534
|
+
}))
|
|
1535
|
+
});
|
|
1510
1536
|
|
|
1511
1537
|
// src/rules/security/sec001-002.ts
|
|
1512
1538
|
var sec001Html = componentRule({
|
|
@@ -1530,6 +1556,83 @@ var sec002JavascriptUrl = componentRule({
|
|
|
1530
1556
|
bad: (c) => c.javascriptUrls.map((u) => ({ line: u.line, message: "javascript: URL in an attribute" }))
|
|
1531
1557
|
});
|
|
1532
1558
|
|
|
1559
|
+
// src/rules/architecture/arch001-002.ts
|
|
1560
|
+
var MAX_LOC = 400;
|
|
1561
|
+
var MAX_PROPS = 10;
|
|
1562
|
+
var arch001ComponentSize = componentRule({
|
|
1563
|
+
id: "ARCH001",
|
|
1564
|
+
title: "Component size",
|
|
1565
|
+
category: "architecture",
|
|
1566
|
+
severity: "info",
|
|
1567
|
+
label: "Component size",
|
|
1568
|
+
recommendation: `Split components over ${MAX_LOC} lines into smaller, focused pieces.`,
|
|
1569
|
+
rationale: "A very large component is hard to read, test, and reuse, and is a common sign that several responsibilities should be split out.",
|
|
1570
|
+
applies: (c) => c.loc > 0,
|
|
1571
|
+
// skip unanalyzable files (loc 0 = read/parse failure), don't PASS them
|
|
1572
|
+
bad: (c) => c.loc > MAX_LOC ? [{ line: 1, message: `Component is ${c.loc} lines (over ${MAX_LOC})` }] : []
|
|
1573
|
+
});
|
|
1574
|
+
var arch002PropCount = componentRule({
|
|
1575
|
+
id: "ARCH002",
|
|
1576
|
+
title: "Prop count",
|
|
1577
|
+
category: "architecture",
|
|
1578
|
+
severity: "info",
|
|
1579
|
+
label: "Prop count",
|
|
1580
|
+
recommendation: `Group related props into an object, or split the component, when it takes more than ${MAX_PROPS} props.`,
|
|
1581
|
+
rationale: "A component taking many props is usually doing too much; grouping or splitting keeps its API understandable.",
|
|
1582
|
+
applies: (c) => c.propCount > 0,
|
|
1583
|
+
// only components whose props we could count
|
|
1584
|
+
bad: (c) => c.propCount > MAX_PROPS ? [{ line: 1, message: `Component takes ${c.propCount} props (over ${MAX_PROPS})` }] : []
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
// src/rules/performance/perf009-heavy-import.ts
|
|
1588
|
+
var HEAVY_PACKAGES = {
|
|
1589
|
+
lodash: "import a submodule (lodash/debounce) or use lodash-es for tree-shaking",
|
|
1590
|
+
moment: "use a lighter date library (date-fns or dayjs) \u2014 moment is large and not tree-shakeable"
|
|
1591
|
+
};
|
|
1592
|
+
var perf009HeavyImport = componentRule({
|
|
1593
|
+
id: "PERF009",
|
|
1594
|
+
title: "Heavy dependency import",
|
|
1595
|
+
category: "performance",
|
|
1596
|
+
severity: "info",
|
|
1597
|
+
label: "No heavy imports",
|
|
1598
|
+
recommendation: "Import a submodule or switch to a lighter, tree-shakeable alternative.",
|
|
1599
|
+
rationale: "Importing a large, non-tree-shakeable package pulls its whole weight into the bundle even when only a fraction is used, slowing load.",
|
|
1600
|
+
applies: (c) => c.imports.length > 0,
|
|
1601
|
+
bad: (c) => {
|
|
1602
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1603
|
+
const out = [];
|
|
1604
|
+
for (const src of c.imports) {
|
|
1605
|
+
if (!Object.hasOwn(HEAVY_PACKAGES, src) || seen.has(src)) continue;
|
|
1606
|
+
seen.add(src);
|
|
1607
|
+
out.push({ line: 0, message: `Heavy import "${src}" \u2014 ${HEAVY_PACKAGES[src]}` });
|
|
1608
|
+
}
|
|
1609
|
+
return out;
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
// src/rules/performance/perf010-namespace-import.ts
|
|
1614
|
+
var perf010NamespaceImport = componentRule({
|
|
1615
|
+
id: "PERF010",
|
|
1616
|
+
title: "Namespace import",
|
|
1617
|
+
category: "performance",
|
|
1618
|
+
severity: "info",
|
|
1619
|
+
label: "No namespace imports",
|
|
1620
|
+
recommendation: "Use named imports (import { x } from 'pkg') instead of import * as \u2014 a namespace import keeps the whole module in the bundle.",
|
|
1621
|
+
rationale: "A namespace import (import * as X) forces the bundler to retain the entire module, so unused exports cannot be tree-shaken out.",
|
|
1622
|
+
applies: (c) => c.namespaceImports.length > 0,
|
|
1623
|
+
bad: (c) => {
|
|
1624
|
+
const minLine = /* @__PURE__ */ new Map();
|
|
1625
|
+
for (const ns of c.namespaceImports) {
|
|
1626
|
+
const prev = minLine.get(ns.source);
|
|
1627
|
+
if (prev === void 0 || ns.line < prev) minLine.set(ns.source, ns.line);
|
|
1628
|
+
}
|
|
1629
|
+
return [...minLine.entries()].sort((a, b) => a[1] - b[1]).map(([source, line]) => ({
|
|
1630
|
+
line,
|
|
1631
|
+
message: `Namespace import "* as \u2026 from '${source}'" \u2014 prefer named imports so the bundler can tree-shake`
|
|
1632
|
+
}));
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1533
1636
|
// src/rules/index.ts
|
|
1534
1637
|
var allRules = [
|
|
1535
1638
|
seo001Title,
|
|
@@ -1572,8 +1675,14 @@ var allRules = [
|
|
|
1572
1675
|
seo030HeadingOrder,
|
|
1573
1676
|
correct001EachKey,
|
|
1574
1677
|
correct002EffectDerived,
|
|
1678
|
+
correct003EffectAsOnMount,
|
|
1679
|
+
correct004UnmutatedState,
|
|
1575
1680
|
sec001Html,
|
|
1576
|
-
sec002JavascriptUrl
|
|
1681
|
+
sec002JavascriptUrl,
|
|
1682
|
+
arch001ComponentSize,
|
|
1683
|
+
arch002PropCount,
|
|
1684
|
+
perf009HeavyImport,
|
|
1685
|
+
perf010NamespaceImport
|
|
1577
1686
|
];
|
|
1578
1687
|
function explainRule(id) {
|
|
1579
1688
|
const target = id.toUpperCase();
|
|
@@ -1700,6 +1809,21 @@ function computeHealth(results, config) {
|
|
|
1700
1809
|
return { health, categories, weights };
|
|
1701
1810
|
}
|
|
1702
1811
|
|
|
1812
|
+
// src/reporter/palette.ts
|
|
1813
|
+
var noColorPalette = {
|
|
1814
|
+
bold: (s) => s,
|
|
1815
|
+
dim: (s) => s,
|
|
1816
|
+
red: (s) => s,
|
|
1817
|
+
yellow: (s) => s,
|
|
1818
|
+
green: (s) => s,
|
|
1819
|
+
cyan: (s) => s
|
|
1820
|
+
};
|
|
1821
|
+
function scoreColor(p, score) {
|
|
1822
|
+
if (score >= 90) return p.green;
|
|
1823
|
+
if (score >= 70) return p.yellow;
|
|
1824
|
+
return p.red;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1703
1827
|
// src/reporter/console.ts
|
|
1704
1828
|
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
1829
|
var SEVERITY_TITLE = {
|
|
@@ -1711,66 +1835,77 @@ var CATEGORY_LABEL = {
|
|
|
1711
1835
|
seo: "SEO",
|
|
1712
1836
|
performance: "Performance",
|
|
1713
1837
|
correctness: "Correctness",
|
|
1714
|
-
security: "Security"
|
|
1838
|
+
security: "Security",
|
|
1839
|
+
architecture: "Architecture"
|
|
1715
1840
|
};
|
|
1716
|
-
var CATEGORY_ORDER = ["seo", "performance", "correctness", "security"];
|
|
1717
|
-
function scoreLine(label, { score, scoreModel }) {
|
|
1841
|
+
var CATEGORY_ORDER = ["seo", "performance", "correctness", "security", "architecture"];
|
|
1842
|
+
function scoreLine(p, label, { score, scoreModel }) {
|
|
1718
1843
|
const parts = [`route avg ${scoreModel.routeAverage}`];
|
|
1719
1844
|
if (scoreModel.sitePenalty > 0) parts.push(`site \u2212${scoreModel.sitePenalty}`);
|
|
1720
1845
|
if (scoreModel.criticalCap !== null) parts.push(`capped at ${scoreModel.criticalCap}: critical present`);
|
|
1721
|
-
return `${label} Score: ${score}/100 (${parts.join(" \xB7 ")})`;
|
|
1846
|
+
return `${label} Score: ${scoreColor(p, score)(`${score}/100`)} ${p.dim(`(${parts.join(" \xB7 ")})`)}`;
|
|
1722
1847
|
}
|
|
1723
|
-
function byRouteTree(results, config) {
|
|
1848
|
+
function byRouteTree(p, results, config) {
|
|
1724
1849
|
const routes = /* @__PURE__ */ new Map();
|
|
1725
1850
|
for (const r of results) {
|
|
1726
1851
|
if (r.route === void 0) continue;
|
|
1727
1852
|
if (!routes.has(r.route)) routes.set(r.route, []);
|
|
1728
1853
|
routes.get(r.route).push(r);
|
|
1729
1854
|
}
|
|
1730
|
-
const lines = ["By route", RULE];
|
|
1855
|
+
const lines = [p.bold("By route"), p.dim(RULE)];
|
|
1731
1856
|
for (const [route, rs] of [...routes.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
1732
1857
|
const { score } = computeScore(rs, config, { applyCriticalCap: false });
|
|
1733
|
-
lines.push(`${route.padEnd(28)} ${score}`);
|
|
1858
|
+
lines.push(`${route.padEnd(28)} ${scoreColor(p, score)(`${score}`)}`);
|
|
1734
1859
|
for (const r of rs.filter((x) => classify(x, config) === "fail")) {
|
|
1735
|
-
lines.push(` \u2717 ${r.id} ${r.message}`);
|
|
1860
|
+
lines.push(` ${p.red("\u2717")} ${r.id} ${r.message}`);
|
|
1736
1861
|
}
|
|
1737
1862
|
}
|
|
1738
1863
|
lines.push("");
|
|
1739
1864
|
return lines;
|
|
1740
1865
|
}
|
|
1741
1866
|
function formatConsoleReport(results, config, options = {}) {
|
|
1867
|
+
const p = options.palette ?? noColorPalette;
|
|
1742
1868
|
const summary = summarize(results, config);
|
|
1743
1869
|
const { health, categories: byCat } = computeHealth(results, config);
|
|
1744
1870
|
const present2 = CATEGORY_ORDER.filter((c) => byCat[c] !== void 0);
|
|
1745
|
-
const header = [
|
|
1871
|
+
const header = [
|
|
1872
|
+
p.bold(`Svelte Vitals \xB7 ${options.mode ?? "static mode"}`),
|
|
1873
|
+
"",
|
|
1874
|
+
`${p.bold("Health:")} ${scoreColor(p, health)(`${health}/100`)}`
|
|
1875
|
+
];
|
|
1746
1876
|
for (const c of present2) {
|
|
1747
|
-
header.push(scoreLine(CATEGORY_LABEL[c] ?? c, byCat[c]));
|
|
1877
|
+
header.push(scoreLine(p, CATEGORY_LABEL[c] ?? c, byCat[c]));
|
|
1748
1878
|
}
|
|
1749
1879
|
const lines = [...header, ""];
|
|
1880
|
+
const SEVERITY_COLOR = {
|
|
1881
|
+
critical: (s) => p.red(p.bold(s)),
|
|
1882
|
+
warning: (s) => p.yellow(p.bold(s)),
|
|
1883
|
+
info: (s) => p.dim(s)
|
|
1884
|
+
};
|
|
1750
1885
|
const failures = results.filter((r) => classify(r, config) === "fail");
|
|
1751
1886
|
for (const severity of ["critical", "warning", "info"]) {
|
|
1752
1887
|
const bucket = failures.filter((r) => effectiveSeverity(r, config) === severity);
|
|
1753
1888
|
if (bucket.length === 0) continue;
|
|
1754
|
-
lines.push(`${SEVERITY_TITLE[severity]} (${bucket.length})
|
|
1889
|
+
lines.push(SEVERITY_COLOR[severity](`${SEVERITY_TITLE[severity]} (${bucket.length})`), p.dim(RULE));
|
|
1755
1890
|
for (const r of bucket) {
|
|
1756
|
-
lines.push(
|
|
1757
|
-
if (r.route) lines.push(` ${r.route}`);
|
|
1758
|
-
if (r.location) lines.push(` ${r.location}${r.line ? `:${r.line}` : ""}`);
|
|
1891
|
+
lines.push(`${p.red("\u2717")} ${r.id} ${r.message}`);
|
|
1892
|
+
if (r.route) lines.push(p.dim(` ${r.route}`));
|
|
1893
|
+
if (r.location) lines.push(p.dim(` ${r.location}${r.line ? `:${r.line}` : ""}`));
|
|
1759
1894
|
}
|
|
1760
1895
|
lines.push("");
|
|
1761
1896
|
}
|
|
1762
1897
|
const passed = results.filter((r) => classify(r, config) !== "fail");
|
|
1763
1898
|
if (passed.length > 0) {
|
|
1764
|
-
lines.push(`Passed (${passed.length})
|
|
1899
|
+
lines.push(p.bold(`Passed (${passed.length})`), p.dim(RULE));
|
|
1765
1900
|
for (const r of passed) {
|
|
1766
|
-
const marker = classify(r, config) === "dynamic" ? " \u21AF dynamic" : "";
|
|
1901
|
+
const marker = classify(r, config) === "dynamic" ? p.cyan(" \u21AF dynamic") : "";
|
|
1767
1902
|
const route = r.route ? ` ${r.route}` : "";
|
|
1768
|
-
lines.push(
|
|
1903
|
+
lines.push(`${p.green("\u2713")} ${r.id} ${r.message}${marker}${route}`);
|
|
1769
1904
|
}
|
|
1770
1905
|
lines.push("");
|
|
1771
1906
|
}
|
|
1772
|
-
if (options.byRoute) lines.push(...byRouteTree(results, config));
|
|
1773
|
-
if (summary.dynamic > 0) lines.push("\u21AF = set dynamically (verified at runtime).");
|
|
1907
|
+
if (options.byRoute) lines.push(...byRouteTree(p, results, config));
|
|
1908
|
+
if (summary.dynamic > 0) lines.push(p.dim("\u21AF = set dynamically (verified at runtime)."));
|
|
1774
1909
|
return lines.join("\n").replace(/\n+$/, "\n");
|
|
1775
1910
|
}
|
|
1776
1911
|
|
|
@@ -2161,6 +2296,8 @@ export {
|
|
|
2161
2296
|
SITEMAP_SOURCE_PATHS,
|
|
2162
2297
|
allRules,
|
|
2163
2298
|
applyRuleSeverities,
|
|
2299
|
+
arch001ComponentSize,
|
|
2300
|
+
arch002PropCount,
|
|
2164
2301
|
buildHtmlDocument,
|
|
2165
2302
|
buildJsonReport,
|
|
2166
2303
|
classify,
|
|
@@ -2168,6 +2305,8 @@ export {
|
|
|
2168
2305
|
computeScore,
|
|
2169
2306
|
correct001EachKey,
|
|
2170
2307
|
correct002EffectDerived,
|
|
2308
|
+
correct003EffectAsOnMount,
|
|
2309
|
+
correct004UnmutatedState,
|
|
2171
2310
|
defaultConfig,
|
|
2172
2311
|
defaultProject,
|
|
2173
2312
|
defineConfig,
|
|
@@ -2186,6 +2325,7 @@ export {
|
|
|
2186
2325
|
imageRule,
|
|
2187
2326
|
isPenalized,
|
|
2188
2327
|
linkRule,
|
|
2328
|
+
noColorPalette,
|
|
2189
2329
|
perf001ImageDimensions,
|
|
2190
2330
|
perf002ImageLoading,
|
|
2191
2331
|
perf003PreloadAs,
|
|
@@ -2194,9 +2334,12 @@ export {
|
|
|
2194
2334
|
perf006ResponsiveImage,
|
|
2195
2335
|
perf007RenderBlockingScript,
|
|
2196
2336
|
perf008Preconnect,
|
|
2337
|
+
perf009HeavyImport,
|
|
2338
|
+
perf010NamespaceImport,
|
|
2197
2339
|
runRules,
|
|
2198
2340
|
safeHref,
|
|
2199
2341
|
scoreBand,
|
|
2342
|
+
scoreColor,
|
|
2200
2343
|
scoresByCategory,
|
|
2201
2344
|
sec001Html,
|
|
2202
2345
|
sec002JavascriptUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svelte-vitals/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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,7 +21,7 @@
|
|
|
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
27
|
"exports": {
|