svelte-vitals 0.44.2 → 0.44.4
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/bin.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
readPackageVersion,
|
|
9
9
|
readPkg,
|
|
10
10
|
run
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-YY567AYV.js";
|
|
12
12
|
import {
|
|
13
13
|
consoleIO,
|
|
14
14
|
parseCliArgs,
|
|
@@ -1429,7 +1429,9 @@ function realIO() {
|
|
|
1429
1429
|
writeFileSync(path, content);
|
|
1430
1430
|
},
|
|
1431
1431
|
cwd: process.cwd(),
|
|
1432
|
-
|
|
1432
|
+
// clack reads from stdin and renders to stdout, so both must be interactive —
|
|
1433
|
+
// a piped/redirected stdin would leave the prompt hanging for input that never comes.
|
|
1434
|
+
isTTY: Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY),
|
|
1433
1435
|
nodeVersion: process.version,
|
|
1434
1436
|
log: (line) => console.log(line),
|
|
1435
1437
|
errorLog: (line) => console.error(line),
|
|
@@ -1484,15 +1486,15 @@ ${planText}` });
|
|
|
1484
1486
|
}
|
|
1485
1487
|
};
|
|
1486
1488
|
}
|
|
1487
|
-
async function runInstallCli(args) {
|
|
1489
|
+
async function runInstallCli(args, io = consoleIO) {
|
|
1488
1490
|
const argv = parseInstallArgs(args);
|
|
1489
1491
|
if (argv.help) {
|
|
1490
|
-
|
|
1492
|
+
io.log(INSTALL_HELP);
|
|
1491
1493
|
return 0;
|
|
1492
1494
|
}
|
|
1493
1495
|
const { flags, warnings, errors } = resolveInstallArgs(argv);
|
|
1494
|
-
for (const w of warnings)
|
|
1495
|
-
for (const e of errors)
|
|
1496
|
+
for (const w of warnings) io.errorLog(w);
|
|
1497
|
+
for (const e of errors) io.errorLog(e);
|
|
1496
1498
|
if (!flags) return 2;
|
|
1497
1499
|
return runInstall(flags, realIO(), clackPrompts(), readPackageVersion());
|
|
1498
1500
|
}
|
|
@@ -1720,7 +1722,7 @@ function runExplainCli(args, io = consoleIO) {
|
|
|
1720
1722
|
return 0;
|
|
1721
1723
|
}
|
|
1722
1724
|
|
|
1723
|
-
// src/
|
|
1725
|
+
// src/cli.ts
|
|
1724
1726
|
var HELP = `svelte-vitals \u2014 a deterministic SvelteKit code-health scanner (SEO \xB7 performance \xB7 correctness \xB7 security \xB7 architecture)
|
|
1725
1727
|
|
|
1726
1728
|
Usage:
|
|
@@ -1784,45 +1786,53 @@ var VERSION = readPackageVersion();
|
|
|
1784
1786
|
function selectApp(apps) {
|
|
1785
1787
|
return selectAppPrompt(apps, "Multiple SvelteKit apps found \u2014 which one should svelte-vitals analyze?");
|
|
1786
1788
|
}
|
|
1787
|
-
async function
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
return;
|
|
1797
|
-
}
|
|
1798
|
-
if (
|
|
1799
|
-
const code2 = await
|
|
1800
|
-
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
if (
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
}
|
|
1816
|
-
const { options, warnings, errors, minHealth } = resolveArgs(argv);
|
|
1817
|
-
for (const w of warnings) console.error(w);
|
|
1818
|
-
for (const e of errors) console.error(e);
|
|
1819
|
-
if (!options) process.exit(2);
|
|
1789
|
+
async function runCli(argv, io = consoleIO) {
|
|
1790
|
+
if (argv[0] === "docs") {
|
|
1791
|
+
const { runDocsCli } = await import("./cli-KH6FL5V7.js");
|
|
1792
|
+
return { code: runDocsCli(argv.slice(1), io), exit: "natural" };
|
|
1793
|
+
}
|
|
1794
|
+
if (argv[0] === "explain") {
|
|
1795
|
+
return { code: runExplainCli(argv.slice(1), io), exit: "natural" };
|
|
1796
|
+
}
|
|
1797
|
+
if (argv[0] === "install") {
|
|
1798
|
+
return { code: await runInstallCli(argv.slice(1), io), exit: "immediate" };
|
|
1799
|
+
}
|
|
1800
|
+
if (argv[0] === "ci") {
|
|
1801
|
+
const code2 = await runCiCli(argv.slice(1), { ...realIO(), log: io.log, errorLog: io.errorLog });
|
|
1802
|
+
return { code: code2, exit: "immediate" };
|
|
1803
|
+
}
|
|
1804
|
+
const parsed = parseRunArgs(argv);
|
|
1805
|
+
if (parsed.help) {
|
|
1806
|
+
io.log(HELP);
|
|
1807
|
+
return { code: 0, exit: "natural" };
|
|
1808
|
+
}
|
|
1809
|
+
if (parsed.version) {
|
|
1810
|
+
io.log(`${VERSION} (core ${readCoreVersion()})`);
|
|
1811
|
+
io.errorLog("svelte-vitals: run `svelte-vitals docs list` for the bundled guides.");
|
|
1812
|
+
return { code: 0, exit: "natural" };
|
|
1813
|
+
}
|
|
1814
|
+
const { options, warnings, errors, minHealth } = resolveArgs(parsed);
|
|
1815
|
+
for (const w of warnings) io.errorLog(w);
|
|
1816
|
+
for (const e of errors) io.errorLog(e);
|
|
1817
|
+
if (!options) return { code: 2, exit: "immediate" };
|
|
1820
1818
|
const code = await run({
|
|
1821
1819
|
...options,
|
|
1822
1820
|
minHealth,
|
|
1823
|
-
selectApp
|
|
1821
|
+
selectApp,
|
|
1822
|
+
log: io.log,
|
|
1823
|
+
errorLog: io.errorLog
|
|
1824
1824
|
});
|
|
1825
1825
|
await new Promise((resolve) => process.stdout.write("", resolve));
|
|
1826
|
-
|
|
1826
|
+
return { code, exit: "immediate" };
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// src/bin.ts
|
|
1830
|
+
async function main() {
|
|
1831
|
+
const { code, exit } = await runCli(process.argv.slice(2));
|
|
1832
|
+
if (exit === "immediate") {
|
|
1833
|
+
process.exit(code);
|
|
1834
|
+
} else {
|
|
1835
|
+
process.exitCode = code;
|
|
1836
|
+
}
|
|
1827
1837
|
}
|
|
1828
1838
|
void main();
|
|
@@ -723,6 +723,7 @@ function chainFiles(pageRel, layouts) {
|
|
|
723
723
|
async function resolveRoute(rt, cwd, pageRel, config, layouts, cache) {
|
|
724
724
|
const files = chainFiles(pageRel, layouts);
|
|
725
725
|
const composed = /* @__PURE__ */ new Map();
|
|
726
|
+
const jsonldTags = [];
|
|
726
727
|
let broadOwn = false;
|
|
727
728
|
let broadInherited = false;
|
|
728
729
|
const images = [];
|
|
@@ -738,7 +739,9 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache) {
|
|
|
738
739
|
}
|
|
739
740
|
const resolved = await resolveFileTags(rt, cwd, rel, parsed, config, MAX_DEPTH, /* @__PURE__ */ new Set([rel]), cache);
|
|
740
741
|
for (const tag of resolved.tags) {
|
|
741
|
-
|
|
742
|
+
const stamped = { ...tag, presence: isPage ? "own" : "inherited", file: rel };
|
|
743
|
+
if (tag.kind === "jsonld") jsonldTags.push(stamped);
|
|
744
|
+
else composed.set(tagKey(tag), stamped);
|
|
742
745
|
}
|
|
743
746
|
if (resolved.broad) {
|
|
744
747
|
if (isPage) broadOwn = true;
|
|
@@ -755,7 +758,7 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache) {
|
|
|
755
758
|
}
|
|
756
759
|
const route = deriveRoute(pageRel);
|
|
757
760
|
return {
|
|
758
|
-
head: { route, source: "static", tags: [...composed.values()], file: pageRel },
|
|
761
|
+
head: { route, source: "static", tags: [...composed.values(), ...jsonldTags], file: pageRel },
|
|
759
762
|
images: { route, images },
|
|
760
763
|
headings: { route, headings, componentHeadings }
|
|
761
764
|
};
|
|
@@ -773,7 +776,7 @@ async function collectRoutes(rt, cwd, config = defaultConfig, cache = /* @__PURE
|
|
|
773
776
|
// src/route-matcher.ts
|
|
774
777
|
function routeMatcher(glob2) {
|
|
775
778
|
if (!glob2) return () => true;
|
|
776
|
-
const body = glob2.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "
|
|
779
|
+
const body = glob2.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\/\0$/g, "(?:/.*)?").replace(/^\0\//g, "(?:.*/)?").replace(/\0\//g, "(?:.*/)?").replace(/\/\0/g, "(?:/.*)?").replace(/\0/g, ".*");
|
|
777
780
|
const re = new RegExp(`^${body}$`);
|
|
778
781
|
return (route) => re.test(route.replace(/^\//, ""));
|
|
779
782
|
}
|
|
@@ -781,14 +784,23 @@ function routeMatcher(glob2) {
|
|
|
781
784
|
// src/collect-all.ts
|
|
782
785
|
async function collectAll(rt, cwd, config, opts = {}) {
|
|
783
786
|
const matches = routeMatcher(opts.route);
|
|
784
|
-
const
|
|
787
|
+
const project = await collectProjectFacts(rt, cwd);
|
|
788
|
+
const [collected, components, kitModules, sourceFiles] = await Promise.all([
|
|
789
|
+
collectRoutes(rt, cwd, config, opts.parseCache),
|
|
790
|
+
// Component (Correctness) facts are file-scoped with no route attribution yet, so a
|
|
791
|
+
// route-filtered run skips them rather than reporting unrelated components (#68 review);
|
|
792
|
+
// kitModules is skipped for the same reason.
|
|
793
|
+
opts.route ? [] : collectComponentFacts(rt, cwd),
|
|
794
|
+
opts.route ? [] : collectKitModuleFacts(rt, cwd, project.kitAliases),
|
|
795
|
+
// Unlike its two neighbours above, the --route branch gets `undefined` here, not `[]`: an empty
|
|
796
|
+
// inventory would tell architecture/unit-entry-file that the declared unit directories truly do
|
|
797
|
+
// not exist, so it would report every declaration as inert, whereas `undefined` means the mode
|
|
798
|
+
// never collected the fact at all, and the rule stays silent instead of raising a false alarm.
|
|
799
|
+
opts.route ? void 0 : collectSourceFiles(rt, cwd)
|
|
800
|
+
]);
|
|
785
801
|
const heads = collected.heads.filter((h) => matches(h.route));
|
|
786
802
|
const images = collected.images.filter((i) => matches(i.route));
|
|
787
803
|
const headings = collected.headings.filter((h) => matches(h.route));
|
|
788
|
-
const project = await collectProjectFacts(rt, cwd);
|
|
789
|
-
const components = opts.route ? [] : await collectComponentFacts(rt, cwd);
|
|
790
|
-
const kitModules = opts.route ? [] : await collectKitModuleFacts(rt, cwd, project.kitAliases);
|
|
791
|
-
const sourceFiles = opts.route ? void 0 : await collectSourceFiles(rt, cwd);
|
|
792
804
|
return { heads, images, headings, project, components, kitModules, sourceFiles };
|
|
793
805
|
}
|
|
794
806
|
|
|
@@ -978,7 +990,7 @@ function filterToNewFindings(results, baselineResults, config = defaultConfig3)
|
|
|
978
990
|
}
|
|
979
991
|
|
|
980
992
|
// src/suppressions.ts
|
|
981
|
-
import { readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
993
|
+
import { readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
982
994
|
import { join as join6 } from "path";
|
|
983
995
|
import { isPenalized as isPenalized3 } from "@svelte-vitals/core";
|
|
984
996
|
|
|
@@ -1221,7 +1233,17 @@ function writeSuppressions(cwd, results, config) {
|
|
|
1221
1233
|
}
|
|
1222
1234
|
entries.sort(compareEntries);
|
|
1223
1235
|
const path = join6(cwd, SUPPRESSIONS_FILE);
|
|
1224
|
-
|
|
1236
|
+
const tmpPath = `${path}.tmp`;
|
|
1237
|
+
try {
|
|
1238
|
+
writeFileSync(tmpPath, JSON.stringify({ version: 1, suppressions: entries }, null, 2) + "\n");
|
|
1239
|
+
renameSync(tmpPath, path);
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
try {
|
|
1242
|
+
unlinkSync(tmpPath);
|
|
1243
|
+
} catch {
|
|
1244
|
+
}
|
|
1245
|
+
throw err;
|
|
1246
|
+
}
|
|
1225
1247
|
return entries.length;
|
|
1226
1248
|
}
|
|
1227
1249
|
function applySuppressions(results, entries, config, allResults) {
|
|
@@ -1540,6 +1562,16 @@ function overridesOffWarnings(allowRules, overrides) {
|
|
|
1540
1562
|
}
|
|
1541
1563
|
return warnings;
|
|
1542
1564
|
}
|
|
1565
|
+
function skippedFileWarnings(facts) {
|
|
1566
|
+
const files = [...new Set(facts.filter((f) => f.parseFailed).map((f) => f.file))].sort();
|
|
1567
|
+
if (files.length === 0) return [];
|
|
1568
|
+
const shown = files.slice(0, 10);
|
|
1569
|
+
const list = files.length > shown.length ? `${shown.join(", ")}, \u2026 and ${files.length - shown.length} more` : shown.join(", ");
|
|
1570
|
+
return [
|
|
1571
|
+
`skipped ${files.length} file(s) that could not be parsed: ${list}`,
|
|
1572
|
+
"findings for these files are unavailable until they parse."
|
|
1573
|
+
];
|
|
1574
|
+
}
|
|
1543
1575
|
async function analyzeProject(opts = {}) {
|
|
1544
1576
|
const cwd = opts.cwd ?? process.cwd();
|
|
1545
1577
|
const rt = createNodeRuntime();
|
|
@@ -1588,7 +1620,7 @@ async function analyzeProject(opts = {}) {
|
|
|
1588
1620
|
version: readPackageVersion(),
|
|
1589
1621
|
ruleIds: rules.map((r) => r.id),
|
|
1590
1622
|
examined,
|
|
1591
|
-
warnings,
|
|
1623
|
+
warnings: [...warnings, ...skippedFileWarnings([...components, ...kitModules])],
|
|
1592
1624
|
loadedConfig: loaded
|
|
1593
1625
|
};
|
|
1594
1626
|
}
|
|
@@ -39,7 +39,7 @@ var EMBEDDED_DOCS = [
|
|
|
39
39
|
name: "scoping",
|
|
40
40
|
title: "Scoping findings to a change",
|
|
41
41
|
description: "Use --diff, --staged, --baseline and the suppressions file so only what a change introduced is reported, instead of a legacy backlog.",
|
|
42
|
-
body: "# Scoping findings to a change\n\nAn existing project usually has a backlog nobody is about to fix. Scope the report rather than\ndisabling rules.\n\n## Scope by file\n\n- **`--diff [ref]`** \u2014 only findings in files changed versus `ref` (default `HEAD`).\n- **`--staged`** \u2014 only findings in staged files. The pre-commit gate.\n\n```bash\nsvelte-vitals . --diff --reporter agent # after editing: what did I just break?\nsvelte-vitals . --staged # before committing\n```\n\nBoth work when the project is not at the git repo root.\n\n## Scope by finding (`--baseline <ref>`)\n\nReports only findings **not already present** at `ref`. Scopes by finding identity rather than by\nfile, so a pre-existing problem in a file you touched does not fail the gate. No default ref.\n\n```bash\nsvelte-vitals --diff origin/main --baseline origin/main --fail-on warning # PR gate\n```\n\nIt checks `ref` out into a temporary worktree and subtracts those findings. On failure (no git,\nbad ref) it warns and reports everything rather than failing the run.\n\n## Accept a backlog once (`svelte-vitals-suppressions.json`)\n\n`--baseline` handles the transient case. For a persistent ramp, record today's findings once:\n\n```bash\nsvelte-vitals --update-suppressions\ngit add svelte-vitals-suppressions.json\n```\n\nThis analyzes the whole project (`--diff`/`--staged`/`--baseline` are ignored), writes every\npenalized finding, and exits `0` without a report.\n\nThe file then applies automatically on every run, after `--diff`/`--staged` and `--baseline`.\nFixing an accepted finding leaves a **stale** entry, reported on stderr but never failing the run.\n`--no-suppressions` ignores the file for one run.\n\nA malformed suppressions file is a hard error (exit `2`), not a silent skip.\n\n## Which one\n\n| Situation | Use |\n| -------------------------------------- | ---------------------------------------- |\n| Checking an edit you just made | `--diff` |\n| Pre-commit hook | `--staged` |\n| PR gate against a base branch | `--diff <base> --baseline <base>` |\n| Adopting on a legacy project, for good | `--update-suppressions`, commit the file |\n\nMatching ignores line numbers in both `--baseline` and the suppressions file, so a second\nviolation of the same rule lower in the same file does not surface as new.\n\n## Related\n\n- `svelte-vitals docs show ci` \u2014 the generated PR gate already does the `--diff`/`--baseline` pairing\n- `svelte-vitals docs show config` \u2014 turning a rule off for good, when that is genuinely right"
|
|
42
|
+
body: "# Scoping findings to a change\n\nAn existing project usually has a backlog nobody is about to fix. Scope the report rather than\ndisabling rules.\n\n## Scope by file\n\n- **`--diff [ref]`** \u2014 only findings in files changed versus `ref` (default `HEAD`).\n- **`--staged`** \u2014 only findings in staged files. The pre-commit gate.\n\n```bash\nsvelte-vitals . --diff --reporter agent # after editing: what did I just break?\nsvelte-vitals . --staged # before committing\n```\n\nBoth work when the project is not at the git repo root.\n\n## Scope by finding (`--baseline <ref>`)\n\nReports only findings **not already present** at `ref`. Scopes by finding identity rather than by\nfile, so a pre-existing problem in a file you touched does not fail the gate. No default ref.\n\n```bash\nsvelte-vitals --diff origin/main --baseline origin/main --fail-on warning # PR gate\n```\n\nIt checks `ref` out into a temporary worktree and subtracts those findings. On failure (no git,\nbad ref) it warns and reports everything rather than failing the run.\n\n## Accept a backlog once (`svelte-vitals-suppressions.json`)\n\n`--baseline` handles the transient case. For a persistent ramp, record today's findings once:\n\n```bash\nsvelte-vitals --update-suppressions\ngit add svelte-vitals-suppressions.json\n```\n\nThis analyzes the whole project (`--diff`/`--staged`/`--baseline` are ignored), writes every\npenalized finding, and exits `0` without a report.\n\nThe file then applies automatically on every run, after `--diff`/`--staged` and `--baseline`.\nFixing an accepted finding leaves a **stale** entry, reported on stderr but never failing the run.\n`--no-suppressions` ignores the file for one run.\n\nAn entry covers whatever its rule reports at that route and location, not just the message\nrecorded when written \u2014 a different finding from the same rule at the same spot still matches\nand stays suppressed (and not stale).\n\nA malformed suppressions file is a hard error (exit `2`), not a silent skip.\n\n## Which one\n\n| Situation | Use |\n| -------------------------------------- | ---------------------------------------- |\n| Checking an edit you just made | `--diff` |\n| Pre-commit hook | `--staged` |\n| PR gate against a base branch | `--diff <base> --baseline <base>` |\n| Adopting on a legacy project, for good | `--update-suppressions`, commit the file |\n\nMatching ignores line numbers in both `--baseline` and the suppressions file, so a second\nviolation of the same rule lower in the same file does not surface as new.\n\n## Related\n\n- `svelte-vitals docs show ci` \u2014 the generated PR gate already does the `--diff`/`--baseline` pairing\n- `svelte-vitals docs show config` \u2014 turning a rule off for good, when that is genuinely right"
|
|
43
43
|
}
|
|
44
44
|
];
|
|
45
45
|
|
package/dist/index.d.ts
CHANGED
|
@@ -231,7 +231,7 @@ interface AnalyzeResult {
|
|
|
231
231
|
ruleIds: string[];
|
|
232
232
|
/** Per-rule, per-declaration counts of places examined, unfiltered by `--diff`/`--baseline`/suppressions. */
|
|
233
233
|
examined: Record<string, Record<string, number>>;
|
|
234
|
-
/** Non-fatal config-file
|
|
234
|
+
/** Non-fatal issues surfaced during analysis: config-file problems (unknown top-level keys, invalid enum values), version-floor notices, `--rules`/overrides conflicts, and skipped-file notices. Empty when none apply. */
|
|
235
235
|
warnings: string[];
|
|
236
236
|
/**
|
|
237
237
|
* This analysis's config-file load result (`undefined` when no config file exists at its
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-vitals",
|
|
3
|
-
"version": "0.44.
|
|
3
|
+
"version": "0.44.4",
|
|
4
4
|
"description": "A deterministic SvelteKit code-health scanner (SEO, performance, correctness, security, architecture) — not a runtime Web Vitals reporter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"magicast": "^0.5.4",
|
|
49
49
|
"svelte": "^5.56.8",
|
|
50
50
|
"tinyglobby": "^0.2.17",
|
|
51
|
-
"@svelte-vitals/core": "0.
|
|
51
|
+
"@svelte-vitals/core": "0.41.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/estree": "^1.0.9",
|