@svelte-vitals/core 0.22.1 → 0.24.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 +13 -0
- package/dist/index.js +90 -30
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -247,6 +247,11 @@ interface ComponentFacts {
|
|
|
247
247
|
propCount: number;
|
|
248
248
|
/** Module specifiers of every `import` in the instance + module scripts (Bundle PERF009). */
|
|
249
249
|
imports: string[];
|
|
250
|
+
/** Module specifiers of every `import`, each with its source line (Bundle PERF009). */
|
|
251
|
+
importSpans: {
|
|
252
|
+
source: string;
|
|
253
|
+
line: number;
|
|
254
|
+
}[];
|
|
250
255
|
/** Value `import * as X from '<bare pkg>'` namespace imports (type-only excluded) — Bundle PERF010. */
|
|
251
256
|
namespaceImports: {
|
|
252
257
|
source: string;
|
|
@@ -275,6 +280,10 @@ declare function parseComponentFacts(source: string, filename: string): {
|
|
|
275
280
|
loc: number;
|
|
276
281
|
propCount: number;
|
|
277
282
|
imports: string[];
|
|
283
|
+
importSpans: {
|
|
284
|
+
source: string;
|
|
285
|
+
line: number;
|
|
286
|
+
}[];
|
|
278
287
|
namespaceImports: {
|
|
279
288
|
source: string;
|
|
280
289
|
line: number;
|
|
@@ -629,6 +638,10 @@ interface ConsoleReportOptions {
|
|
|
629
638
|
mode?: string;
|
|
630
639
|
/** Color decorators; defaults to no color. */
|
|
631
640
|
palette?: Palette;
|
|
641
|
+
/** Show every failing/passed/route entry uncapped and ungrouped, exactly as before this option existed. Default false (capped, grouped by rule). */
|
|
642
|
+
verbose?: boolean;
|
|
643
|
+
/** Internal: set by the CLI when it has already animated the Health header itself — skips the brand/Health lines (category score lines still print). Default false. */
|
|
644
|
+
omitHeader?: boolean;
|
|
632
645
|
}
|
|
633
646
|
/**
|
|
634
647
|
* Render results as a console report string (design §7). Pure: returns a string,
|
package/dist/index.js
CHANGED
|
@@ -419,9 +419,11 @@ function countLines(source) {
|
|
|
419
419
|
if (source.length === 0) return 0;
|
|
420
420
|
return source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
|
|
421
421
|
}
|
|
422
|
-
function collectImportSources(program, acc) {
|
|
422
|
+
function collectImportSources(program, source, acc) {
|
|
423
423
|
walkEstree(program, (n) => {
|
|
424
|
-
if (n.type === "ImportDeclaration" && typeof n.source?.value === "string")
|
|
424
|
+
if (n.type === "ImportDeclaration" && typeof n.source?.value === "string") {
|
|
425
|
+
acc.push({ source: n.source.value, line: lineOf(source, n.start) });
|
|
426
|
+
}
|
|
425
427
|
});
|
|
426
428
|
}
|
|
427
429
|
function isBareSpecifier(s) {
|
|
@@ -459,10 +461,10 @@ function parseComponentFacts(source, filename) {
|
|
|
459
461
|
collectSecurityFacts(ast.fragment ?? ast, source, htmlTags, javascriptUrls);
|
|
460
462
|
const loc = countLines(source);
|
|
461
463
|
const suppressions = collectSuppressions(source);
|
|
462
|
-
const
|
|
464
|
+
const importSpans = [];
|
|
463
465
|
const namespaceImports = [];
|
|
464
466
|
if (ast.module?.content) {
|
|
465
|
-
collectImportSources(ast.module.content,
|
|
467
|
+
collectImportSources(ast.module.content, source, importSpans);
|
|
466
468
|
collectNamespaceImports(ast.module.content, source, namespaceImports);
|
|
467
469
|
}
|
|
468
470
|
const effects = [];
|
|
@@ -471,7 +473,7 @@ function parseComponentFacts(source, filename) {
|
|
|
471
473
|
let propCount = 0;
|
|
472
474
|
const program = ast.instance?.content;
|
|
473
475
|
if (program) {
|
|
474
|
-
collectImportSources(program,
|
|
476
|
+
collectImportSources(program, source, importSpans);
|
|
475
477
|
collectNamespaceImports(program, source, namespaceImports);
|
|
476
478
|
propCount = countProps(program);
|
|
477
479
|
const nonBindableProps = collectNonBindableProps(program);
|
|
@@ -509,6 +511,7 @@ function parseComponentFacts(source, filename) {
|
|
|
509
511
|
if (!writtenOrEscaped.has(d.name)) constableStates.push(d);
|
|
510
512
|
}
|
|
511
513
|
}
|
|
514
|
+
const imports = importSpans.map((s) => s.source);
|
|
512
515
|
return {
|
|
513
516
|
eachBlocks,
|
|
514
517
|
effects,
|
|
@@ -517,6 +520,7 @@ function parseComponentFacts(source, filename) {
|
|
|
517
520
|
loc,
|
|
518
521
|
propCount,
|
|
519
522
|
imports,
|
|
523
|
+
importSpans,
|
|
520
524
|
namespaceImports,
|
|
521
525
|
constableStates,
|
|
522
526
|
mutatedProps,
|
|
@@ -535,6 +539,7 @@ function emptyComponentFacts(file) {
|
|
|
535
539
|
loc: 0,
|
|
536
540
|
propCount: 0,
|
|
537
541
|
imports: [],
|
|
542
|
+
importSpans: [],
|
|
538
543
|
namespaceImports: [],
|
|
539
544
|
constableStates: [],
|
|
540
545
|
mutatedProps: [],
|
|
@@ -2143,7 +2148,7 @@ var arch002PropCount = componentRule({
|
|
|
2143
2148
|
bad: (c) => c.propCount > MAX_PROPS ? [{ line: 1, message: `Component takes ${c.propCount} props (over ${MAX_PROPS})` }] : []
|
|
2144
2149
|
});
|
|
2145
2150
|
|
|
2146
|
-
// src/rules/
|
|
2151
|
+
// src/rules/perf/perf009-heavy-import.ts
|
|
2147
2152
|
var HEAVY_PACKAGES = {
|
|
2148
2153
|
lodash: "import a submodule (lodash/debounce) or use lodash-es for tree-shaking",
|
|
2149
2154
|
moment: "use a lighter date library (date-fns or dayjs) \u2014 moment is large and not tree-shakeable"
|
|
@@ -2156,20 +2161,24 @@ var perf009HeavyImport = componentRule({
|
|
|
2156
2161
|
label: "No heavy imports",
|
|
2157
2162
|
recommendation: "Import a submodule or switch to a lighter, tree-shakeable alternative.",
|
|
2158
2163
|
rationale: "Importing a large, non-tree-shakeable package pulls its whole weight into the bundle even when only a fraction is used, slowing load.",
|
|
2159
|
-
|
|
2164
|
+
// ComponentFacts is a public @svelte-vitals/core export — an external caller compiled
|
|
2165
|
+
// against an older version may still construct one without importSpans. Fall back to the
|
|
2166
|
+
// line-less `imports` (line: 0, the pre-fix behavior) instead of crashing on `undefined`.
|
|
2167
|
+
applies: (c) => (c.importSpans ?? c.imports).length > 0,
|
|
2160
2168
|
bad: (c) => {
|
|
2161
2169
|
const seen = /* @__PURE__ */ new Set();
|
|
2162
2170
|
const out = [];
|
|
2163
|
-
|
|
2171
|
+
const spans = c.importSpans ?? c.imports.map((source) => ({ source, line: 0 }));
|
|
2172
|
+
for (const { source: src, line } of spans) {
|
|
2164
2173
|
if (!Object.hasOwn(HEAVY_PACKAGES, src) || seen.has(src)) continue;
|
|
2165
2174
|
seen.add(src);
|
|
2166
|
-
out.push({ line
|
|
2175
|
+
out.push({ line, message: `Heavy import "${src}" \u2014 ${HEAVY_PACKAGES[src]}` });
|
|
2167
2176
|
}
|
|
2168
2177
|
return out;
|
|
2169
2178
|
}
|
|
2170
2179
|
});
|
|
2171
2180
|
|
|
2172
|
-
// src/rules/
|
|
2181
|
+
// src/rules/perf/perf010-namespace-import.ts
|
|
2173
2182
|
var perf010NamespaceImport = componentRule({
|
|
2174
2183
|
id: "PERF010",
|
|
2175
2184
|
title: "Namespace import",
|
|
@@ -2399,27 +2408,53 @@ var CATEGORY_LABEL = {
|
|
|
2399
2408
|
architecture: "Architecture"
|
|
2400
2409
|
};
|
|
2401
2410
|
var CATEGORY_ORDER = ["seo", "performance", "correctness", "security", "architecture"];
|
|
2411
|
+
var MAX_RULE_GROUPS_PER_BUCKET = 5;
|
|
2412
|
+
function groupByRule(results) {
|
|
2413
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2414
|
+
for (const r of results) {
|
|
2415
|
+
const bucket = groups.get(r.id);
|
|
2416
|
+
if (bucket) bucket.push(r);
|
|
2417
|
+
else groups.set(r.id, [r]);
|
|
2418
|
+
}
|
|
2419
|
+
return [...groups.entries()].map(([id, rs]) => ({ id, results: rs })).sort((a, b) => b.results.length - a.results.length || a.id.localeCompare(b.id));
|
|
2420
|
+
}
|
|
2402
2421
|
function scoreLine(p, label, { score, scoreModel }) {
|
|
2403
2422
|
const parts = [`route avg ${scoreModel.routeAverage}`];
|
|
2404
2423
|
if (scoreModel.sitePenalty > 0) parts.push(`site \u2212${scoreModel.sitePenalty}`);
|
|
2405
2424
|
if (scoreModel.criticalCap !== null) parts.push(`capped at ${scoreModel.criticalCap}: critical present`);
|
|
2406
2425
|
return `${label} Score: ${scoreColor(p, score)(`${score}/100`)} ${p.dim(`(${parts.join(" \xB7 ")})`)}`;
|
|
2407
2426
|
}
|
|
2408
|
-
|
|
2427
|
+
var MAX_ROUTES_BY_ROUTE = 10;
|
|
2428
|
+
function byRouteTree(p, results, config, verbose) {
|
|
2409
2429
|
const routes = /* @__PURE__ */ new Map();
|
|
2410
2430
|
for (const r of results) {
|
|
2411
2431
|
if (r.route === void 0) continue;
|
|
2412
2432
|
if (!routes.has(r.route)) routes.set(r.route, []);
|
|
2413
2433
|
routes.get(r.route).push(r);
|
|
2414
2434
|
}
|
|
2435
|
+
const scored = [...routes.entries()].map(([route, rs]) => ({
|
|
2436
|
+
route,
|
|
2437
|
+
rs,
|
|
2438
|
+
score: computeScore(rs, config, { applyCriticalCap: false }).score
|
|
2439
|
+
}));
|
|
2440
|
+
scored.sort((a, b) => a.score - b.score || a.route.localeCompare(b.route));
|
|
2441
|
+
const shown = verbose ? scored : scored.slice(0, MAX_ROUTES_BY_ROUTE);
|
|
2415
2442
|
const lines = [p.bold("By route"), p.dim(RULE)];
|
|
2416
|
-
for (const
|
|
2417
|
-
const { score } = computeScore(rs, config, { applyCriticalCap: false });
|
|
2443
|
+
for (const { route, rs, score } of shown) {
|
|
2418
2444
|
lines.push(`${route.padEnd(28)} ${scoreColor(p, score)(`${score}`)}`);
|
|
2419
2445
|
for (const r of rs.filter((x) => classify(x, config) === "fail")) {
|
|
2420
2446
|
lines.push(` ${p.red("\u2717")} ${r.id} ${r.message}`);
|
|
2421
2447
|
}
|
|
2422
2448
|
}
|
|
2449
|
+
if (!verbose && scored.length > MAX_ROUTES_BY_ROUTE) {
|
|
2450
|
+
const remaining = scored.slice(MAX_ROUTES_BY_ROUTE);
|
|
2451
|
+
const avgScore = Math.round(remaining.reduce((sum, r) => sum + r.score, 0) / remaining.length);
|
|
2452
|
+
lines.push(
|
|
2453
|
+
p.dim(
|
|
2454
|
+
`\u2026and ${remaining.length} more route${remaining.length > 1 ? "s" : ""} (avg score ${avgScore}) \u2014 run with --verbose to see all`
|
|
2455
|
+
)
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2423
2458
|
lines.push("");
|
|
2424
2459
|
return lines;
|
|
2425
2460
|
}
|
|
@@ -2428,15 +2463,18 @@ function formatConsoleReport(results, config, options = {}) {
|
|
|
2428
2463
|
const summary = summarize(results, config);
|
|
2429
2464
|
const { health, categories: byCat } = computeHealth(results, config);
|
|
2430
2465
|
const present2 = CATEGORY_ORDER.filter((c) => byCat[c] !== void 0);
|
|
2431
|
-
const
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2466
|
+
const lines = [];
|
|
2467
|
+
if (!options.omitHeader) {
|
|
2468
|
+
lines.push(
|
|
2469
|
+
p.bold(`Svelte Vitals \xB7 ${options.mode ?? "static mode"}`),
|
|
2470
|
+
"",
|
|
2471
|
+
`${p.bold("Health:")} ${scoreColor(p, health)(`${health}/100`)}`
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2436
2474
|
for (const c of present2) {
|
|
2437
|
-
|
|
2475
|
+
lines.push(scoreLine(p, CATEGORY_LABEL[c] ?? c, byCat[c]));
|
|
2438
2476
|
}
|
|
2439
|
-
|
|
2477
|
+
lines.push("");
|
|
2440
2478
|
const SEVERITY_COLOR = {
|
|
2441
2479
|
critical: (s) => p.red(p.bold(s)),
|
|
2442
2480
|
warning: (s) => p.yellow(p.bold(s)),
|
|
@@ -2447,25 +2485,47 @@ function formatConsoleReport(results, config, options = {}) {
|
|
|
2447
2485
|
const bucket = failures.filter((r) => effectiveSeverity(r, config) === severity);
|
|
2448
2486
|
if (bucket.length === 0) continue;
|
|
2449
2487
|
lines.push(SEVERITY_COLOR[severity](`${SEVERITY_TITLE[severity]} (${bucket.length})`), p.dim(RULE));
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2488
|
+
if (options.verbose) {
|
|
2489
|
+
for (const r of bucket) {
|
|
2490
|
+
lines.push(`${p.red("\u2717")} ${r.id} ${r.message}`);
|
|
2491
|
+
if (r.route) lines.push(p.dim(` ${r.route}`));
|
|
2492
|
+
if (r.location) lines.push(p.dim(` ${r.location}${r.line ? `:${r.line}` : ""}`));
|
|
2493
|
+
}
|
|
2494
|
+
} else {
|
|
2495
|
+
const groups = groupByRule(bucket);
|
|
2496
|
+
const shownGroups = groups.slice(0, MAX_RULE_GROUPS_PER_BUCKET);
|
|
2497
|
+
for (const group of shownGroups) {
|
|
2498
|
+
const r = group.results[0];
|
|
2499
|
+
lines.push(`${p.red("\u2717")} ${r.id} ${r.message}`);
|
|
2500
|
+
if (r.route) lines.push(p.dim(` ${r.route}`));
|
|
2501
|
+
if (r.location) lines.push(p.dim(` ${r.location}${r.line ? `:${r.line}` : ""}`));
|
|
2502
|
+
if (group.results.length > 1) {
|
|
2503
|
+
lines.push(p.dim(` \u2026and ${group.results.length - 1} more`));
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
if (groups.length > MAX_RULE_GROUPS_PER_BUCKET) {
|
|
2507
|
+
const remaining = groups.length - MAX_RULE_GROUPS_PER_BUCKET;
|
|
2508
|
+
lines.push(
|
|
2509
|
+
p.dim(`\u2026and ${remaining} more rule${remaining > 1 ? "s" : ""} affected \u2014 run with --verbose to see all`)
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2454
2512
|
}
|
|
2455
2513
|
lines.push("");
|
|
2456
2514
|
}
|
|
2457
2515
|
const passed = results.filter((r) => classify(r, config) !== "fail");
|
|
2458
2516
|
if (passed.length > 0) {
|
|
2459
2517
|
lines.push(p.bold(`Passed (${passed.length})`), p.dim(RULE));
|
|
2460
|
-
|
|
2461
|
-
const
|
|
2462
|
-
|
|
2463
|
-
|
|
2518
|
+
if (options.verbose) {
|
|
2519
|
+
for (const r of passed) {
|
|
2520
|
+
const marker = classify(r, config) === "dynamic" ? p.cyan(" \u21AF dynamic") : "";
|
|
2521
|
+
const route = r.route ? ` ${r.route}` : "";
|
|
2522
|
+
lines.push(`${p.green("\u2713")} ${r.id} ${r.message}${marker}${route}`);
|
|
2523
|
+
}
|
|
2464
2524
|
}
|
|
2465
2525
|
lines.push("");
|
|
2466
2526
|
}
|
|
2467
|
-
if (options.byRoute) lines.push(...byRouteTree(p, results, config));
|
|
2468
|
-
if (summary.dynamic > 0) lines.push(p.dim("\u21AF = set dynamically (verified at runtime)."));
|
|
2527
|
+
if (options.byRoute) lines.push(...byRouteTree(p, results, config, options.verbose ?? false));
|
|
2528
|
+
if (options.verbose && summary.dynamic > 0) lines.push(p.dim("\u21AF = set dynamically (verified at runtime)."));
|
|
2469
2529
|
return lines.join("\n").replace(/\n+$/, "\n");
|
|
2470
2530
|
}
|
|
2471
2531
|
|