@svelte-vitals/core 0.31.0 → 0.32.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 +27 -6
- package/dist/index.js +98 -15
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -544,6 +544,20 @@ interface KitModuleFacts {
|
|
|
544
544
|
name: string;
|
|
545
545
|
line: number;
|
|
546
546
|
}[];
|
|
547
|
+
/**
|
|
548
|
+
* `.set()`/`.update()` in a handler on an import resolving under the `$lib` server root.
|
|
549
|
+
* The call shape alone cannot tell a persistence client (`db.set(…)`) from a hand-rolled
|
|
550
|
+
* in-memory store, so the decision needs the target module — which this pure parse cannot
|
|
551
|
+
* read. `collectKitModuleFacts` resolves each one and promotes the in-memory ones into
|
|
552
|
+
* `importedStateWrites`; a consumer that ignores this field sees the pre-arbitration
|
|
553
|
+
* behaviour, i.e. every one of these exempt.
|
|
554
|
+
*/
|
|
555
|
+
pendingServerStoreWrites: {
|
|
556
|
+
name: string;
|
|
557
|
+
imported: string;
|
|
558
|
+
resolved: string;
|
|
559
|
+
line: number;
|
|
560
|
+
}[];
|
|
547
561
|
/** Value imports whose specifier resolves to a repo-local `.svelte.ts`/`.svelte.js` runes module (security/shared-state-import). */
|
|
548
562
|
runesModuleImports: {
|
|
549
563
|
source: string;
|
|
@@ -946,9 +960,9 @@ interface Rule {
|
|
|
946
960
|
severity: Severity;
|
|
947
961
|
/** 'route' = evaluated per route, 'project' = site-wide (design §10, §12). */
|
|
948
962
|
scope: Scope;
|
|
949
|
-
/** Why this rule matters — one or two sentences, surfaced by
|
|
963
|
+
/** Why this rule matters — one or two sentences, surfaced by `svelte-vitals explain` (issue #24). */
|
|
950
964
|
rationale: string;
|
|
951
|
-
/** Canonical remediation template, shared by findings and
|
|
965
|
+
/** Canonical remediation template, shared by findings and `svelte-vitals explain` (issue #24). */
|
|
952
966
|
fix?: Fix;
|
|
953
967
|
/** Configurable options for this rule; absent means the rule takes none. */
|
|
954
968
|
options?: RuleOptionsSpec;
|
|
@@ -1290,7 +1304,7 @@ declare const performanceStateRaw: Rule;
|
|
|
1290
1304
|
|
|
1291
1305
|
declare const allRules: Rule[];
|
|
1292
1306
|
|
|
1293
|
-
/** One configurable option of a rule, flattened for
|
|
1307
|
+
/** One configurable option of a rule, flattened for `svelte-vitals explain`'s output. */
|
|
1294
1308
|
interface RuleOptionInfo {
|
|
1295
1309
|
name: string;
|
|
1296
1310
|
/** `integer` replaces the default; `string-list`/`string-map` are ADDED to it. */
|
|
@@ -1314,7 +1328,7 @@ interface RuleInfo {
|
|
|
1314
1328
|
*/
|
|
1315
1329
|
options?: RuleOptionInfo[];
|
|
1316
1330
|
}
|
|
1317
|
-
/** Look up a rule's static metadata
|
|
1331
|
+
/** Look up a rule's static metadata, as `svelte-vitals explain` renders it (issue #24). Rule ids are matched exactly (case-sensitive, e.g. "seo/ssr-disabled"). */
|
|
1318
1332
|
declare function explainRule(id: string): RuleInfo | undefined;
|
|
1319
1333
|
|
|
1320
1334
|
interface HeadTagRuleOptions {
|
|
@@ -1326,7 +1340,7 @@ interface HeadTagRuleOptions {
|
|
|
1326
1340
|
/** Short human label, e.g. 'description'. */
|
|
1327
1341
|
label: string;
|
|
1328
1342
|
recommendation: string;
|
|
1329
|
-
/** Why this rule matters — surfaced by
|
|
1343
|
+
/** Why this rule matters — surfaced by `svelte-vitals explain` (issue #24). */
|
|
1330
1344
|
rationale: string;
|
|
1331
1345
|
/** Agent-actionable remediation attached to every finding (issue #18). */
|
|
1332
1346
|
fix?: Fix;
|
|
@@ -1432,7 +1446,14 @@ interface ScoreModel {
|
|
|
1432
1446
|
criticalCap: number | null;
|
|
1433
1447
|
}
|
|
1434
1448
|
interface ScoreResult {
|
|
1449
|
+
/** The score as displayed: `Math.floor(rawScore)`, so 100 means the deduction was exactly zero. */
|
|
1435
1450
|
score: number;
|
|
1451
|
+
/**
|
|
1452
|
+
* The same score before flooring, after `sitePenalty` and the cap, clamped to `[0, 100]`. Exposed so
|
|
1453
|
+
* `computeHealth` can average unrounded values and floor once — averaging the displayed scores would
|
|
1454
|
+
* compose two roundings and move Health by up to two points.
|
|
1455
|
+
*/
|
|
1456
|
+
rawScore: number;
|
|
1436
1457
|
scoreModel: ScoreModel;
|
|
1437
1458
|
}
|
|
1438
1459
|
interface ScoreOptions {
|
|
@@ -1482,7 +1503,7 @@ interface JsonReport {
|
|
|
1482
1503
|
}>;
|
|
1483
1504
|
siteIssues: JsonIssue[];
|
|
1484
1505
|
}
|
|
1485
|
-
/** Build the structured JSON report object (design §7).
|
|
1506
|
+
/** Build the structured JSON report object (design §7). The shape the `json` reporter emits (issue #24). */
|
|
1486
1507
|
declare function buildJsonReport(results: Result[], config: Config, meta: {
|
|
1487
1508
|
version: string;
|
|
1488
1509
|
}): JsonReport;
|
package/dist/index.js
CHANGED
|
@@ -1842,6 +1842,13 @@ function libServerRoot(aliases) {
|
|
|
1842
1842
|
if (lib && lib.replacement === null) return void 0;
|
|
1843
1843
|
return `${lib?.replacement ?? "src/lib"}/server`;
|
|
1844
1844
|
}
|
|
1845
|
+
function serverRootRelativePath(spec, importerFile, aliases) {
|
|
1846
|
+
const serverRoot = libServerRoot(aliases);
|
|
1847
|
+
if (serverRoot === void 0) return void 0;
|
|
1848
|
+
const path = resolveRepoLocalPath(spec, importerFile, aliases);
|
|
1849
|
+
if (path === void 0) return void 0;
|
|
1850
|
+
return path === serverRoot || path.startsWith(`${serverRoot}/`) ? path : void 0;
|
|
1851
|
+
}
|
|
1845
1852
|
function isLocalStateSpecifier(spec, importerFile, aliases) {
|
|
1846
1853
|
const serverRoot = libServerRoot(aliases);
|
|
1847
1854
|
if (serverRoot === void 0) return false;
|
|
@@ -1849,12 +1856,32 @@ function isLocalStateSpecifier(spec, importerFile, aliases) {
|
|
|
1849
1856
|
if (path === void 0) return false;
|
|
1850
1857
|
return path !== serverRoot && !path.startsWith(`${serverRoot}/`);
|
|
1851
1858
|
}
|
|
1859
|
+
var IN_MEMORY_CTORS = /* @__PURE__ */ new Set(["Map", "Set", "WeakMap", "WeakSet"]);
|
|
1860
|
+
function isInMemoryInit(init) {
|
|
1861
|
+
if (!init) return false;
|
|
1862
|
+
if (init.type === "ObjectExpression" || init.type === "ArrayExpression") return true;
|
|
1863
|
+
return init.type === "NewExpression" && init.callee?.type === "Identifier" && IN_MEMORY_CTORS.has(init.callee.name);
|
|
1864
|
+
}
|
|
1865
|
+
function parseInMemoryExports(source, filename) {
|
|
1866
|
+
const names = /* @__PURE__ */ new Set();
|
|
1867
|
+
const { program } = parseModuleProgram(source, filename);
|
|
1868
|
+
for (const stmt of program?.body ?? []) {
|
|
1869
|
+
if (stmt?.type !== "ExportNamedDeclaration" || !stmt.declaration) continue;
|
|
1870
|
+
const decl = stmt.declaration;
|
|
1871
|
+
if (decl.type !== "VariableDeclaration") continue;
|
|
1872
|
+
for (const d of decl.declarations ?? []) {
|
|
1873
|
+
if (d?.id?.type === "Identifier" && isInMemoryInit(d.init)) names.add(d.id.name);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
return names;
|
|
1877
|
+
}
|
|
1852
1878
|
function parseKitModuleFacts(source, filename, aliases) {
|
|
1853
1879
|
const suppressions = collectSuppressions(source);
|
|
1854
1880
|
const { program, wrapped } = parseModuleProgram(source, filename);
|
|
1855
1881
|
const moduleStateReassignments = [];
|
|
1856
1882
|
const importedStateWrites = [];
|
|
1857
1883
|
const importedStateWritesOutsideHandlers = [];
|
|
1884
|
+
const pendingServerStoreWrites = [];
|
|
1858
1885
|
const runesModuleImports = [];
|
|
1859
1886
|
const lifecycleCalls = [];
|
|
1860
1887
|
const browserGlobalRefs = [];
|
|
@@ -1863,6 +1890,7 @@ function parseKitModuleFacts(source, filename, aliases) {
|
|
|
1863
1890
|
moduleStateReassignments,
|
|
1864
1891
|
importedStateWrites,
|
|
1865
1892
|
importedStateWritesOutsideHandlers,
|
|
1893
|
+
pendingServerStoreWrites,
|
|
1866
1894
|
runesModuleImports,
|
|
1867
1895
|
lifecycleCalls,
|
|
1868
1896
|
browserGlobalRefs,
|
|
@@ -1872,6 +1900,7 @@ function parseKitModuleFacts(source, filename, aliases) {
|
|
|
1872
1900
|
}
|
|
1873
1901
|
const line = (start) => Math.max(0, lineOf(wrapped, start) - 1);
|
|
1874
1902
|
const importedSpecifiers = /* @__PURE__ */ new Map();
|
|
1903
|
+
const importedNames = /* @__PURE__ */ new Map();
|
|
1875
1904
|
for (const stmt of program.body ?? []) {
|
|
1876
1905
|
if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type") continue;
|
|
1877
1906
|
const spec = typeof stmt.source?.value === "string" ? stmt.source.value : "";
|
|
@@ -1880,6 +1909,10 @@ function parseKitModuleFacts(source, filename, aliases) {
|
|
|
1880
1909
|
if (s?.importKind === "type" || s?.local?.type !== "Identifier") continue;
|
|
1881
1910
|
names.push(s.local.name);
|
|
1882
1911
|
importedSpecifiers.set(s.local.name, spec);
|
|
1912
|
+
importedNames.set(
|
|
1913
|
+
s.local.name,
|
|
1914
|
+
s.type === "ImportSpecifier" && s.imported?.type === "Identifier" ? s.imported.name : s.local.name
|
|
1915
|
+
);
|
|
1883
1916
|
}
|
|
1884
1917
|
if (names.length === 0) continue;
|
|
1885
1918
|
const resolved = resolveRunesModuleSpecifier(spec, filename, aliases);
|
|
@@ -1956,8 +1989,21 @@ function parseKitModuleFacts(source, filename, aliases) {
|
|
|
1956
1989
|
const method = n.callee.property?.type === "Identifier" ? n.callee.property.name : void 0;
|
|
1957
1990
|
if (method === "set" || method === "update") {
|
|
1958
1991
|
const r = importedRoot(n.callee.object);
|
|
1959
|
-
|
|
1960
|
-
|
|
1992
|
+
const spec = r ? importedSpecifiers.get(r) : void 0;
|
|
1993
|
+
if (r && spec !== void 0) {
|
|
1994
|
+
if (isLocalStateSpecifier(spec, filename, aliases)) write = { name: r, via: "set-call" };
|
|
1995
|
+
else {
|
|
1996
|
+
const resolved = serverRootRelativePath(spec, filename, aliases);
|
|
1997
|
+
if (resolved !== void 0 && inHandler) {
|
|
1998
|
+
pendingServerStoreWrites.push({
|
|
1999
|
+
name: r,
|
|
2000
|
+
imported: importedNames.get(r) ?? r,
|
|
2001
|
+
resolved,
|
|
2002
|
+
line: line(n.start)
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
1961
2007
|
}
|
|
1962
2008
|
} else if (n.type === "AssignmentExpression" && (n.left?.type === "ObjectPattern" || n.left?.type === "ArrayPattern")) {
|
|
1963
2009
|
const scanPatternTargets = (pat) => {
|
|
@@ -2005,6 +2051,7 @@ function parseKitModuleFacts(source, filename, aliases) {
|
|
|
2005
2051
|
moduleStateReassignments: byLine(moduleStateReassignments),
|
|
2006
2052
|
importedStateWrites: byLine(importedStateWrites),
|
|
2007
2053
|
importedStateWritesOutsideHandlers: byLine(importedStateWritesOutsideHandlers),
|
|
2054
|
+
pendingServerStoreWrites: byLine(pendingServerStoreWrites),
|
|
2008
2055
|
runesModuleImports: byLine(runesModuleImports),
|
|
2009
2056
|
lifecycleCalls: byLine(lifecycleCalls),
|
|
2010
2057
|
browserGlobalRefs: byLine(browserGlobalRefs),
|
|
@@ -2024,6 +2071,7 @@ function emptyKitModuleFacts(file, kind) {
|
|
|
2024
2071
|
moduleStateReassignments: [],
|
|
2025
2072
|
importedStateWrites: [],
|
|
2026
2073
|
importedStateWritesOutsideHandlers: [],
|
|
2074
|
+
pendingServerStoreWrites: [],
|
|
2027
2075
|
runesModuleImports: [],
|
|
2028
2076
|
lifecycleCalls: [],
|
|
2029
2077
|
browserGlobalRefs: [],
|
|
@@ -2044,7 +2092,7 @@ async function collectKitModuleFacts(rt, cwd, aliases) {
|
|
|
2044
2092
|
];
|
|
2045
2093
|
const lists = await Promise.all(patterns.map((p) => rt.glob(p, cwd)));
|
|
2046
2094
|
const files = [...new Set(lists.flat())];
|
|
2047
|
-
|
|
2095
|
+
const facts = await Promise.all(
|
|
2048
2096
|
files.sort().map(async (rel) => {
|
|
2049
2097
|
const kind = kindOf(rel);
|
|
2050
2098
|
try {
|
|
@@ -2055,6 +2103,38 @@ async function collectKitModuleFacts(rt, cwd, aliases) {
|
|
|
2055
2103
|
}
|
|
2056
2104
|
})
|
|
2057
2105
|
);
|
|
2106
|
+
return arbitrateServerStoreWrites(rt, cwd, facts);
|
|
2107
|
+
}
|
|
2108
|
+
function moduleCandidates(repoPath) {
|
|
2109
|
+
if (repoPath.endsWith(".js")) return [repoPath, `${repoPath.slice(0, -3)}.ts`];
|
|
2110
|
+
if (repoPath.endsWith(".ts")) return [repoPath];
|
|
2111
|
+
return [`${repoPath}.ts`, `${repoPath}.js`, `${repoPath}/index.ts`, `${repoPath}/index.js`];
|
|
2112
|
+
}
|
|
2113
|
+
async function inMemoryExportsOf(rt, cwd, repoPath) {
|
|
2114
|
+
for (const rel of moduleCandidates(repoPath)) {
|
|
2115
|
+
try {
|
|
2116
|
+
if (!await rt.exists(rt.join(cwd, rel))) continue;
|
|
2117
|
+
return parseInMemoryExports(await rt.readFile(rt.join(cwd, rel)), rel);
|
|
2118
|
+
} catch {
|
|
2119
|
+
return /* @__PURE__ */ new Set();
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
return /* @__PURE__ */ new Set();
|
|
2123
|
+
}
|
|
2124
|
+
async function arbitrateServerStoreWrites(rt, cwd, facts) {
|
|
2125
|
+
const targets = [...new Set(facts.flatMap((f) => f.pendingServerStoreWrites.map((w) => w.resolved)))];
|
|
2126
|
+
if (targets.length === 0) return facts;
|
|
2127
|
+
const byPath = new Map(
|
|
2128
|
+
await Promise.all(targets.map(async (t) => [t, await inMemoryExportsOf(rt, cwd, t)]))
|
|
2129
|
+
);
|
|
2130
|
+
return facts.map((f) => {
|
|
2131
|
+
const promoted = f.pendingServerStoreWrites.filter((w) => byPath.get(w.resolved)?.has(w.imported)).map((w) => ({ name: w.name, line: w.line, via: "set-call" }));
|
|
2132
|
+
if (promoted.length === 0) return f;
|
|
2133
|
+
return {
|
|
2134
|
+
...f,
|
|
2135
|
+
importedStateWrites: [...f.importedStateWrites, ...promoted].sort((a, b) => a.line - b.line)
|
|
2136
|
+
};
|
|
2137
|
+
});
|
|
2058
2138
|
}
|
|
2059
2139
|
|
|
2060
2140
|
// src/config-object.ts
|
|
@@ -4938,7 +5018,7 @@ var architectureUnitEntryFile = {
|
|
|
4938
5018
|
category: "architecture",
|
|
4939
5019
|
severity: "info",
|
|
4940
5020
|
detection: { presence: "own", value: "static" },
|
|
4941
|
-
|
|
5021
|
+
location: expected,
|
|
4942
5022
|
message: "Unit entry file",
|
|
4943
5023
|
recommendation: recommendation8,
|
|
4944
5024
|
docsUrl: docsUrl8
|
|
@@ -5641,7 +5721,8 @@ function computeScore(results, config, options = {}) {
|
|
|
5641
5721
|
routeScores.set(route, routeScores.get(route) - deduction);
|
|
5642
5722
|
}
|
|
5643
5723
|
const scores = [...routeScores.values()].map(clamp);
|
|
5644
|
-
const
|
|
5724
|
+
const rawRouteAverage = scores.length ? scores.reduce((a, b) => a + b, 0) / scores.length : 100;
|
|
5725
|
+
const routeAverage = Math.floor(rawRouteAverage);
|
|
5645
5726
|
const projectRuleMax = /* @__PURE__ */ new Map();
|
|
5646
5727
|
for (const r of projectResults) {
|
|
5647
5728
|
if (!isPenalized(r.detection, config.treatDynamicAs)) continue;
|
|
@@ -5653,11 +5734,11 @@ function computeScore(results, config, options = {}) {
|
|
|
5653
5734
|
let sitePenalty = 0;
|
|
5654
5735
|
for (const deduction of projectRuleMax.values()) sitePenalty += deduction;
|
|
5655
5736
|
const applyCap = options.applyCriticalCap ?? true;
|
|
5656
|
-
const
|
|
5657
|
-
const capBinds = applyCap && anyCritical &&
|
|
5737
|
+
const rawUncapped = rawRouteAverage - sitePenalty;
|
|
5738
|
+
const capBinds = applyCap && anyCritical && rawUncapped > CRITICAL_CAP;
|
|
5658
5739
|
const criticalCap = capBinds ? CRITICAL_CAP : null;
|
|
5659
|
-
const
|
|
5660
|
-
return { score:
|
|
5740
|
+
const rawScore = clamp(capBinds ? CRITICAL_CAP : rawUncapped);
|
|
5741
|
+
return { score: Math.floor(rawScore), rawScore, scoreModel: { routeAverage, sitePenalty, criticalCap } };
|
|
5661
5742
|
}
|
|
5662
5743
|
function scoresByCategory(results, config) {
|
|
5663
5744
|
const byCat = /* @__PURE__ */ new Map();
|
|
@@ -5674,7 +5755,7 @@ function scoresByCategory(results, config) {
|
|
|
5674
5755
|
function computeHealth(results, config) {
|
|
5675
5756
|
const categories = scoresByCategory(results, config);
|
|
5676
5757
|
const weights = {};
|
|
5677
|
-
let
|
|
5758
|
+
let weightedDeficit = 0;
|
|
5678
5759
|
let total = 0;
|
|
5679
5760
|
for (const cat of Object.keys(categories)) {
|
|
5680
5761
|
const w = config.weights?.[cat] ?? 1;
|
|
@@ -5682,14 +5763,15 @@ function computeHealth(results, config) {
|
|
|
5682
5763
|
throw new RangeError(`invalid weight for '${cat}'; expected a finite number >= 0.`);
|
|
5683
5764
|
}
|
|
5684
5765
|
weights[cat] = w;
|
|
5685
|
-
|
|
5766
|
+
weightedDeficit += (100 - categories[cat].rawScore) * w;
|
|
5686
5767
|
total += w;
|
|
5687
5768
|
}
|
|
5688
5769
|
if (Object.keys(weights).length === 0) return { health: 100, categories, weights };
|
|
5689
5770
|
if (total === 0) {
|
|
5690
5771
|
throw new RangeError("Health weights sum to 0; at least one present category must have a positive weight.");
|
|
5691
5772
|
}
|
|
5692
|
-
const
|
|
5773
|
+
const averageDeficit = weightedDeficit / total;
|
|
5774
|
+
const health = averageDeficit === 0 ? 100 : Math.min(99, Math.floor(100 - averageDeficit));
|
|
5693
5775
|
return { health, categories, weights };
|
|
5694
5776
|
}
|
|
5695
5777
|
|
|
@@ -5763,7 +5845,7 @@ function byRouteTree(p, results, config, verbose) {
|
|
|
5763
5845
|
}
|
|
5764
5846
|
if (!verbose && scored.length > MAX_ROUTES_BY_ROUTE) {
|
|
5765
5847
|
const remaining = scored.slice(MAX_ROUTES_BY_ROUTE);
|
|
5766
|
-
const avgScore = Math.
|
|
5848
|
+
const avgScore = Math.floor(remaining.reduce((sum, r) => sum + r.score, 0) / remaining.length);
|
|
5767
5849
|
lines.push(
|
|
5768
5850
|
p.dim(
|
|
5769
5851
|
`\u2026and ${remaining.length} more route${remaining.length > 1 ? "s" : ""} (avg score ${avgScore}) \u2014 run with --verbose to see all`
|
|
@@ -5833,8 +5915,9 @@ function formatConsoleReport(results, config, options = {}) {
|
|
|
5833
5915
|
if (options.verbose) {
|
|
5834
5916
|
for (const r of passed) {
|
|
5835
5917
|
const marker = classify(r, config) === "dynamic" ? p.cyan(" \u21AF dynamic") : "";
|
|
5836
|
-
const
|
|
5837
|
-
|
|
5918
|
+
const where = r.location ?? r.route;
|
|
5919
|
+
const suffix = where ? ` ${where}` : "";
|
|
5920
|
+
lines.push(`${p.green("\u2713")} ${r.id} ${r.message}${marker}${suffix}`);
|
|
5838
5921
|
}
|
|
5839
5922
|
}
|
|
5840
5923
|
lines.push("");
|