svelte-vitals 0.24.0 → 0.25.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/bin.js +202 -12
- package/dist/{chunk-TCZ6OF6J.js → chunk-OCLDCX4Y.js} +130 -14
- package/dist/index.d.ts +80 -5
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
readCoreVersion,
|
|
8
8
|
readPackageVersion,
|
|
9
9
|
run
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-OCLDCX4Y.js";
|
|
11
11
|
|
|
12
12
|
// src/bin.ts
|
|
13
13
|
import mri3 from "mri";
|
|
@@ -135,6 +135,11 @@ function resolveArgs(argv) {
|
|
|
135
135
|
const verbose = Boolean(argv["verbose"]);
|
|
136
136
|
const noColor = argv.color === false;
|
|
137
137
|
const noAnimation = argv.animation === false;
|
|
138
|
+
const noSuppressions = argv.suppressions === false;
|
|
139
|
+
const updateSuppressions = Boolean(argv["update-suppressions"]);
|
|
140
|
+
if (updateSuppressions && noSuppressions) {
|
|
141
|
+
errors.push("svelte-vitals: --update-suppressions and --no-suppressions cannot be used together.");
|
|
142
|
+
}
|
|
138
143
|
const rulesConfig = buildRulesConfig(allow, ignore);
|
|
139
144
|
const rules = Object.keys(rulesConfig).length > 0 ? rulesConfig : void 0;
|
|
140
145
|
if (errors.length > 0) return { options: null, warnings, errors };
|
|
@@ -160,7 +165,9 @@ function resolveArgs(argv) {
|
|
|
160
165
|
...noAnimation ? { noAnimation } : {},
|
|
161
166
|
...diffBase !== void 0 ? { diffBase } : {},
|
|
162
167
|
...staged ? { staged } : {},
|
|
163
|
-
...baselineRef !== void 0 ? { baseline: baselineRef } : {}
|
|
168
|
+
...baselineRef !== void 0 ? { baseline: baselineRef } : {},
|
|
169
|
+
...noSuppressions ? { noSuppressions } : {},
|
|
170
|
+
...updateSuppressions ? { updateSuppressions } : {}
|
|
164
171
|
},
|
|
165
172
|
warnings,
|
|
166
173
|
errors
|
|
@@ -303,6 +310,22 @@ function isAgentTargetId(id) {
|
|
|
303
310
|
return AGENT_TARGETS.some((t) => t.id === id);
|
|
304
311
|
}
|
|
305
312
|
|
|
313
|
+
// src/install/config-targets.ts
|
|
314
|
+
var CONFIG_TARGETS = [
|
|
315
|
+
{
|
|
316
|
+
id: "config-file",
|
|
317
|
+
label: "Config file",
|
|
318
|
+
hint: "Scaffolds svelte-vitals.config.mjs with every option commented out",
|
|
319
|
+
relPath: "svelte-vitals.config.mjs"
|
|
320
|
+
}
|
|
321
|
+
];
|
|
322
|
+
function configTargetById(id) {
|
|
323
|
+
return CONFIG_TARGETS.find((t) => t.id === id);
|
|
324
|
+
}
|
|
325
|
+
function isConfigTargetId(id) {
|
|
326
|
+
return CONFIG_TARGETS.some((t) => t.id === id);
|
|
327
|
+
}
|
|
328
|
+
|
|
306
329
|
// src/install/skill-content.ts
|
|
307
330
|
import { allRules, docsUrlFor } from "@svelte-vitals/core";
|
|
308
331
|
var CATEGORY_ORDER = ["seo", "performance", "correctness", "security", "architecture"];
|
|
@@ -368,6 +391,19 @@ alwaysApply: false
|
|
|
368
391
|
${sharedBody(version)}`;
|
|
369
392
|
}
|
|
370
393
|
|
|
394
|
+
// src/install/config-content.ts
|
|
395
|
+
function buildConfigFileTemplate() {
|
|
396
|
+
return `// svelte-vitals config file \u2014 https://oekazuma.github.io/svelte-vitals/guides/configuration/
|
|
397
|
+
export default {
|
|
398
|
+
// treatDynamicAs: 'pass', // 'pass' | 'warn' | 'fail' \u2014 how {data.title}-style dynamic values are scored
|
|
399
|
+
// metaComponents: ['Seo'], // component names that resolve SEO tags into <head>
|
|
400
|
+
// rules: {}, // e.g. { SEO001: 'off' } to disable a rule
|
|
401
|
+
// failOn: 'critical', // 'critical' | 'warning' | 'info'
|
|
402
|
+
// weights: {} // e.g. { seo: 2 } \u2014 per-category weight for the combined Health score
|
|
403
|
+
};
|
|
404
|
+
`;
|
|
405
|
+
}
|
|
406
|
+
|
|
371
407
|
// src/install/codemod-vite-config.ts
|
|
372
408
|
import { parseModule, generateCode, builders, MagicastError } from "magicast";
|
|
373
409
|
var MANUAL_SNIPPET = `import { svelteVitals } from '@svelte-vitals/vite';
|
|
@@ -543,6 +579,13 @@ function planForAgentTarget(target, io, force, version) {
|
|
|
543
579
|
const status = existing === void 0 ? "created" : force ? "updated" : "exists";
|
|
544
580
|
return { id: target.id, label: target.label, path, status, content };
|
|
545
581
|
}
|
|
582
|
+
function planForConfigTarget(target, io, force) {
|
|
583
|
+
const path = join3(io.cwd, target.relPath);
|
|
584
|
+
const existing = io.readFile(path);
|
|
585
|
+
const content = buildConfigFileTemplate();
|
|
586
|
+
const status = existing === void 0 ? "created" : force ? "updated" : "exists";
|
|
587
|
+
return { id: target.id, label: target.label, path, status, content };
|
|
588
|
+
}
|
|
546
589
|
function indent(text) {
|
|
547
590
|
return text.split("\n").map((l) => ` ${l}`).join("\n");
|
|
548
591
|
}
|
|
@@ -551,7 +594,57 @@ function rowLine(r) {
|
|
|
551
594
|
return r.status === "manual" && r.snippet ? `${head}
|
|
552
595
|
${indent(r.snippet)}` : head;
|
|
553
596
|
}
|
|
597
|
+
async function runRefresh(io, flags, version) {
|
|
598
|
+
let hadFailure = false;
|
|
599
|
+
const rows = [];
|
|
600
|
+
for (const target of AGENT_TARGETS) {
|
|
601
|
+
const path = join3(io.cwd, target.relPath);
|
|
602
|
+
try {
|
|
603
|
+
if (io.readFile(path) === void 0) continue;
|
|
604
|
+
rows.push(planForAgentTarget(
|
|
605
|
+
target,
|
|
606
|
+
io,
|
|
607
|
+
/* force */
|
|
608
|
+
true,
|
|
609
|
+
version
|
|
610
|
+
));
|
|
611
|
+
} catch (err) {
|
|
612
|
+
hadFailure = true;
|
|
613
|
+
io.errorLog(`svelte-vitals: failed to read ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (rows.length === 0) {
|
|
617
|
+
if (hadFailure) return 2;
|
|
618
|
+
io.errorLog(
|
|
619
|
+
"svelte-vitals: no generated agent files found \u2014 run `svelte-vitals install --client claude-skill,cursor-rules` first."
|
|
620
|
+
);
|
|
621
|
+
return 0;
|
|
622
|
+
}
|
|
623
|
+
const planText = rows.map(rowLine).join("\n");
|
|
624
|
+
io.log("Plan:");
|
|
625
|
+
io.log(planText);
|
|
626
|
+
if (flags.dryRun) {
|
|
627
|
+
io.log("Dry run \u2014 no files written.");
|
|
628
|
+
return hadFailure ? 2 : 0;
|
|
629
|
+
}
|
|
630
|
+
for (const r of rows) {
|
|
631
|
+
try {
|
|
632
|
+
io.writeFile(r.path, r.content ?? "");
|
|
633
|
+
io.log(`\u2713 ${r.label}: ${r.status} ${r.path}`);
|
|
634
|
+
} catch (err) {
|
|
635
|
+
hadFailure = true;
|
|
636
|
+
io.errorLog(`svelte-vitals: failed to write ${r.path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (hadFailure) return 2;
|
|
640
|
+
io.log("");
|
|
641
|
+
io.log(`\u2713 refreshed ${rows.length} file(s).`);
|
|
642
|
+
return 0;
|
|
643
|
+
}
|
|
554
644
|
async function runInstall(flags, io, prompts, version = "0.0.0") {
|
|
645
|
+
if (flags.refresh) {
|
|
646
|
+
return runRefresh(io, flags, version);
|
|
647
|
+
}
|
|
555
648
|
let ids;
|
|
556
649
|
if (flags.client && flags.client.length > 0) {
|
|
557
650
|
ids = flags.client;
|
|
@@ -583,7 +676,8 @@ async function runInstall(flags, io, prompts, version = "0.0.0") {
|
|
|
583
676
|
const options = [
|
|
584
677
|
...CLIENTS.map((c) => ({ id: c.id, label: c.label })),
|
|
585
678
|
...VITE_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint })),
|
|
586
|
-
...AGENT_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint }))
|
|
679
|
+
...AGENT_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint })),
|
|
680
|
+
...CONFIG_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint }))
|
|
587
681
|
];
|
|
588
682
|
const picked = await prompts.selectClients(options, detected);
|
|
589
683
|
if (picked === null) {
|
|
@@ -593,14 +687,15 @@ async function runInstall(flags, io, prompts, version = "0.0.0") {
|
|
|
593
687
|
ids = picked;
|
|
594
688
|
} else {
|
|
595
689
|
io.errorLog(
|
|
596
|
-
"svelte-vitals: no TTY; pass --client <claude-code,cursor,codex,vite-plugin,vite-hooks,claude-skill,cursor-rules> to install non-interactively."
|
|
690
|
+
"svelte-vitals: no TTY; pass --client <claude-code,cursor,codex,vite-plugin,vite-hooks,claude-skill,cursor-rules,config-file> to install non-interactively."
|
|
597
691
|
);
|
|
598
692
|
return 2;
|
|
599
693
|
}
|
|
600
694
|
const clients = ids.map(clientById).filter((c) => c !== void 0);
|
|
601
695
|
const viteIds = ids.filter(isViteTargetId);
|
|
602
696
|
const agentIds = ids.filter(isAgentTargetId);
|
|
603
|
-
|
|
697
|
+
const configIds = ids.filter(isConfigTargetId);
|
|
698
|
+
if (clients.length === 0 && viteIds.length === 0 && agentIds.length === 0 && configIds.length === 0) {
|
|
604
699
|
io.errorLog("svelte-vitals: no valid clients or targets selected.");
|
|
605
700
|
return 2;
|
|
606
701
|
}
|
|
@@ -638,6 +733,10 @@ async function runInstall(flags, io, prompts, version = "0.0.0") {
|
|
|
638
733
|
const target = agentTargetById(agentId);
|
|
639
734
|
rows.push(planForAgentTarget(target, io, flags.force ?? false, version));
|
|
640
735
|
}
|
|
736
|
+
for (const configId of configIds) {
|
|
737
|
+
const target = configTargetById(configId);
|
|
738
|
+
rows.push(planForConfigTarget(target, io, flags.force ?? false));
|
|
739
|
+
}
|
|
641
740
|
const planText = rows.map(rowLine).join("\n");
|
|
642
741
|
io.log("Plan:");
|
|
643
742
|
io.log(planText);
|
|
@@ -726,14 +825,22 @@ function resolveInstallArgs(argv) {
|
|
|
726
825
|
if (rawScope === "project" || rawScope === "global") scope = rawScope;
|
|
727
826
|
else errors.push(`svelte-vitals: unknown --scope '${rawScope}'; expected project|global.`);
|
|
728
827
|
}
|
|
828
|
+
const refresh = Boolean(argv.refresh);
|
|
829
|
+
if (refresh && rawClients.length > 0) {
|
|
830
|
+
errors.push("svelte-vitals: --refresh regenerates existing files and cannot be combined with --client.");
|
|
831
|
+
}
|
|
729
832
|
if (errors.length > 0) return { flags: null, warnings, errors };
|
|
833
|
+
if (refresh && (scope !== void 0 || Boolean(argv.yes) || Boolean(argv.force))) {
|
|
834
|
+
warnings.push("svelte-vitals: --scope, --yes, and --force are ignored with --refresh.");
|
|
835
|
+
}
|
|
730
836
|
return {
|
|
731
837
|
flags: {
|
|
732
838
|
...client.length > 0 ? { client } : {},
|
|
733
839
|
...scope ? { scope } : {},
|
|
734
840
|
yes: Boolean(argv.yes),
|
|
735
841
|
dryRun: Boolean(argv["dry-run"]),
|
|
736
|
-
force: Boolean(argv.force)
|
|
842
|
+
force: Boolean(argv.force),
|
|
843
|
+
...refresh ? { refresh: true } : {}
|
|
737
844
|
},
|
|
738
845
|
warnings,
|
|
739
846
|
errors
|
|
@@ -747,7 +854,7 @@ Usage:
|
|
|
747
854
|
svelte-vitals install [options]
|
|
748
855
|
|
|
749
856
|
Options:
|
|
750
|
-
--client <ids> Comma-separated: claude-code,cursor,codex,vite-plugin,vite-hooks,claude-skill,cursor-rules
|
|
857
|
+
--client <ids> Comma-separated: claude-code,cursor,codex,vite-plugin,vite-hooks,claude-skill,cursor-rules,config-file
|
|
751
858
|
(skips the interactive picker)
|
|
752
859
|
vite-plugin registers the build-mode plugin in vite.config.{ts,js,mjs}; vite-hooks
|
|
753
860
|
wires up the svelteVitalsHandle hook in src/hooks.server.{ts,js}, which improves the
|
|
@@ -756,10 +863,15 @@ Options:
|
|
|
756
863
|
claude-skill writes a Claude Code skill (.claude/skills/svelte-vitals/SKILL.md); cursor-rules
|
|
757
864
|
writes a Cursor rules file (.cursor/rules/svelte-vitals.mdc). Both are generated from the
|
|
758
865
|
current rule set and support --force to regenerate.
|
|
866
|
+
config-file scaffolds svelte-vitals.config.mjs with every option commented out;
|
|
867
|
+
supports --force to regenerate.
|
|
759
868
|
--scope <scope> project | global (applies to all selected clients; codex is always global)
|
|
760
869
|
--yes, -y Skip the confirmation prompt
|
|
761
870
|
--dry-run Print the planned changes and exit without writing
|
|
762
871
|
--force Overwrite an existing svelte-vitals entry
|
|
872
|
+
--refresh Regenerate existing agent skill/rules files with the current rule set
|
|
873
|
+
(claude-skill / cursor-rules). Only regenerates files already present on
|
|
874
|
+
disk \u2014 it never creates one. Cannot be combined with --client.
|
|
763
875
|
-h, --help Show this help`;
|
|
764
876
|
function realIO() {
|
|
765
877
|
return {
|
|
@@ -827,7 +939,7 @@ ${planText}` });
|
|
|
827
939
|
}
|
|
828
940
|
async function runInstallCli(args) {
|
|
829
941
|
const argv = mri(args, {
|
|
830
|
-
boolean: ["yes", "dry-run", "force", "help"],
|
|
942
|
+
boolean: ["yes", "dry-run", "force", "refresh", "help"],
|
|
831
943
|
string: ["client", "scope"],
|
|
832
944
|
alias: { y: "yes", h: "help" }
|
|
833
945
|
});
|
|
@@ -884,22 +996,56 @@ function buildWorkflowYaml(opts) {
|
|
|
884
996
|
].join("\n");
|
|
885
997
|
}
|
|
886
998
|
|
|
999
|
+
// src/ci/upgrade.ts
|
|
1000
|
+
var ACTION_USES_LINE = /^(?<indent>\s*-\s*uses:\s*(?:&\S+\s+)?oekazuma\/svelte-vitals\/packages\/action@)(?<ref>[^\s#]+)(?<comment>\s*#.*)?$/;
|
|
1001
|
+
function upgradeActionPin(content, sha, version) {
|
|
1002
|
+
const lines = content.split("\n");
|
|
1003
|
+
let replaced = 0;
|
|
1004
|
+
let from;
|
|
1005
|
+
const next = lines.map((line) => {
|
|
1006
|
+
const eol = line.endsWith("\r") ? "\r" : "";
|
|
1007
|
+
const bare = eol ? line.slice(0, -1) : line;
|
|
1008
|
+
const match = ACTION_USES_LINE.exec(bare);
|
|
1009
|
+
if (!match || !match.groups) return line;
|
|
1010
|
+
const { indent: indent2, ref } = match.groups;
|
|
1011
|
+
if (indent2 === void 0 || ref === void 0) return line;
|
|
1012
|
+
if (ref === sha) return line;
|
|
1013
|
+
if (from === void 0) {
|
|
1014
|
+
const commentMatch = /#\s*@svelte-vitals\/action@(\S+)/.exec(match.groups.comment ?? "");
|
|
1015
|
+
from = commentMatch ? commentMatch[1] : ref.slice(0, 7);
|
|
1016
|
+
}
|
|
1017
|
+
replaced += 1;
|
|
1018
|
+
return `${indent2}${sha} # @svelte-vitals/action@${version}${eol}`;
|
|
1019
|
+
});
|
|
1020
|
+
if (replaced === 0) {
|
|
1021
|
+
const hasAnyReference = lines.some((line) => ACTION_USES_LINE.test(line.endsWith("\r") ? line.slice(0, -1) : line));
|
|
1022
|
+
return { status: hasAnyReference ? "up-to-date" : "no-reference" };
|
|
1023
|
+
}
|
|
1024
|
+
return { status: "upgraded", content: next.join("\n"), replaced, from };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
887
1027
|
// src/ci/action-pin.generated.ts
|
|
888
|
-
var ACTION_SHA = "
|
|
889
|
-
var ACTION_VERSION = "0.
|
|
1028
|
+
var ACTION_SHA = "2c21acbad36c5360228bbb0790dbb4a06b7c773a";
|
|
1029
|
+
var ACTION_VERSION = "0.3.0";
|
|
890
1030
|
|
|
891
1031
|
// src/ci/cli.ts
|
|
892
1032
|
var CI_HELP = `svelte-vitals ci \u2014 scaffold CI integration
|
|
893
1033
|
|
|
894
1034
|
Usage:
|
|
895
1035
|
svelte-vitals ci install [options]
|
|
1036
|
+
svelte-vitals ci upgrade [--dry-run]
|
|
896
1037
|
|
|
897
1038
|
Adds a GitHub Actions workflow (${WORKFLOW_PATH}) that calls the \`@svelte-vitals/action\`
|
|
898
1039
|
GitHub Action on pull requests: inline annotations, a job summary, and a sticky PR
|
|
899
1040
|
comment with the findings.
|
|
900
1041
|
|
|
1042
|
+
\`ci upgrade\` rewrites only the pinned \`@svelte-vitals/action\` reference in an existing
|
|
1043
|
+
workflow to the pin bundled with this CLI, leaving the rest of the file (and any other
|
|
1044
|
+
pins, like actions/checkout) untouched. To pick up the latest pin, run
|
|
1045
|
+
\`npx svelte-vitals@latest ci upgrade\`.
|
|
1046
|
+
|
|
901
1047
|
Options:
|
|
902
|
-
--force Overwrite an existing workflow file
|
|
1048
|
+
--force Overwrite an existing workflow file (install only)
|
|
903
1049
|
--dry-run Print the plan and exit without writing
|
|
904
1050
|
-h, --help Show this help`;
|
|
905
1051
|
async function runCiCli(args, io = realIO()) {
|
|
@@ -908,6 +1054,9 @@ async function runCiCli(args, io = realIO()) {
|
|
|
908
1054
|
io.log(CI_HELP);
|
|
909
1055
|
return 0;
|
|
910
1056
|
}
|
|
1057
|
+
if (sub === "upgrade") {
|
|
1058
|
+
return runCiUpgrade(args.slice(1), io);
|
|
1059
|
+
}
|
|
911
1060
|
if (sub !== "install") {
|
|
912
1061
|
io.log(CI_HELP);
|
|
913
1062
|
return 2;
|
|
@@ -945,6 +1094,44 @@ async function runCiCli(args, io = realIO()) {
|
|
|
945
1094
|
io.log("Done. Commit the workflow file and open a PR to see it in action.");
|
|
946
1095
|
return 0;
|
|
947
1096
|
}
|
|
1097
|
+
async function runCiUpgrade(args, io) {
|
|
1098
|
+
const argv = mri2(args, {
|
|
1099
|
+
boolean: ["dry-run", "help"],
|
|
1100
|
+
alias: { h: "help" }
|
|
1101
|
+
});
|
|
1102
|
+
if (argv.help) {
|
|
1103
|
+
io.log(CI_HELP);
|
|
1104
|
+
return 0;
|
|
1105
|
+
}
|
|
1106
|
+
const path = join4(io.cwd, WORKFLOW_PATH);
|
|
1107
|
+
const existing = io.readFile(path);
|
|
1108
|
+
if (existing === void 0) {
|
|
1109
|
+
io.errorLog(`svelte-vitals: no ${WORKFLOW_PATH} found \u2014 run \`svelte-vitals ci install\` first.`);
|
|
1110
|
+
return 2;
|
|
1111
|
+
}
|
|
1112
|
+
const outcome = upgradeActionPin(existing, ACTION_SHA, ACTION_VERSION);
|
|
1113
|
+
if (outcome.status === "no-reference") {
|
|
1114
|
+
io.errorLog(`svelte-vitals: no @svelte-vitals/action reference found in ${WORKFLOW_PATH}.`);
|
|
1115
|
+
return 2;
|
|
1116
|
+
}
|
|
1117
|
+
if (outcome.status === "up-to-date") {
|
|
1118
|
+
io.log(`= already up to date (@svelte-vitals/action@${ACTION_VERSION}).`);
|
|
1119
|
+
return 0;
|
|
1120
|
+
}
|
|
1121
|
+
if (argv["dry-run"]) {
|
|
1122
|
+
io.log(`Would upgrade @svelte-vitals/action: ${outcome.from} \u2192 ${ACTION_VERSION} (${outcome.replaced} line(s)).`);
|
|
1123
|
+
io.log("Dry run \u2014 no files written.");
|
|
1124
|
+
return 0;
|
|
1125
|
+
}
|
|
1126
|
+
try {
|
|
1127
|
+
io.writeFile(path, outcome.content ?? existing);
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
io.errorLog(`svelte-vitals: failed to write ${WORKFLOW_PATH}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1130
|
+
return 2;
|
|
1131
|
+
}
|
|
1132
|
+
io.log(`\u2713 upgraded @svelte-vitals/action: ${outcome.from} \u2192 ${ACTION_VERSION} (${outcome.replaced} line(s)).`);
|
|
1133
|
+
return 0;
|
|
1134
|
+
}
|
|
948
1135
|
|
|
949
1136
|
// src/bin.ts
|
|
950
1137
|
var HELP = `svelte-vitals \u2014 a deterministic SvelteKit code-health scanner (SEO \xB7 performance \xB7 correctness \xB7 security \xB7 architecture)
|
|
@@ -953,6 +1140,7 @@ Usage:
|
|
|
953
1140
|
svelte-vitals [path] [options]
|
|
954
1141
|
svelte-vitals install Set up the MCP server, Vite integration, or agent skills/rules
|
|
955
1142
|
svelte-vitals ci install Add a GitHub Actions PR gate (annotations + summary comment)
|
|
1143
|
+
svelte-vitals ci upgrade Refresh the pinned @svelte-vitals/action in an existing workflow
|
|
956
1144
|
|
|
957
1145
|
Options:
|
|
958
1146
|
--meta-components <names> Comma-separated component names that emit head metadata
|
|
@@ -961,6 +1149,8 @@ Options:
|
|
|
961
1149
|
--diff [ref] Report only findings in files changed vs ref (default HEAD; e.g. --diff main)
|
|
962
1150
|
--staged Report only findings in files staged for commit (pre-commit gate)
|
|
963
1151
|
--baseline <ref> Report only findings not present at ref (compare against e.g. origin/main)
|
|
1152
|
+
--update-suppressions Write svelte-vitals-suppressions.json accepting all current findings (introduce gates on legacy projects)
|
|
1153
|
+
--no-suppressions Ignore svelte-vitals-suppressions.json for this run
|
|
964
1154
|
--by-route Show per-route score breakdown in console output
|
|
965
1155
|
--reporter <fmt> console | json | agent | sarif | github | html | md (auto: agent under AI-agent envs, github under GitHub Actions)
|
|
966
1156
|
--out-file <path> Output path for --reporter html (default: svelte-vitals-report.html; '-' for stdout)
|
|
@@ -1004,7 +1194,7 @@ async function main() {
|
|
|
1004
1194
|
}
|
|
1005
1195
|
const argv = mri3(process.argv.slice(2), {
|
|
1006
1196
|
alias: { h: "help", v: "version" },
|
|
1007
|
-
boolean: ["by-route", "staged", "score", "verbose"],
|
|
1197
|
+
boolean: ["by-route", "staged", "score", "verbose", "update-suppressions"],
|
|
1008
1198
|
string: [
|
|
1009
1199
|
"meta-components",
|
|
1010
1200
|
"treat-dynamic-as",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { mkdirSync, writeFileSync } from "fs";
|
|
3
|
-
import { dirname as dirname2, join as
|
|
2
|
+
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
3
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
4
4
|
import {
|
|
5
5
|
allRules as allRules2,
|
|
6
6
|
runRules,
|
|
@@ -637,9 +637,8 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache) {
|
|
|
637
637
|
headings: { route, headings }
|
|
638
638
|
};
|
|
639
639
|
}
|
|
640
|
-
async function collectRoutes(rt, cwd, config = defaultConfig) {
|
|
640
|
+
async function collectRoutes(rt, cwd, config = defaultConfig, cache = /* @__PURE__ */ new Map()) {
|
|
641
641
|
const [pages, layouts] = await Promise.all([enumerateRoutePages(rt, cwd), collectLayouts(rt, cwd)]);
|
|
642
|
-
const cache = /* @__PURE__ */ new Map();
|
|
643
642
|
const facts = await Promise.all(pages.map((page) => resolveRoute(rt, cwd, page, config, layouts, cache)));
|
|
644
643
|
return {
|
|
645
644
|
heads: facts.map((f) => f.head),
|
|
@@ -793,6 +792,104 @@ function filterToNewFindings(results, baselineResults) {
|
|
|
793
792
|
return results.filter((r) => !baselineKeys.has(findingKey(r)));
|
|
794
793
|
}
|
|
795
794
|
|
|
795
|
+
// src/suppressions.ts
|
|
796
|
+
import { readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
797
|
+
import { join as join4 } from "path";
|
|
798
|
+
import { isPenalized } from "@svelte-vitals/core";
|
|
799
|
+
var SUPPRESSIONS_FILE = "svelte-vitals-suppressions.json";
|
|
800
|
+
function isPlainObject(value) {
|
|
801
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
802
|
+
}
|
|
803
|
+
function loadSuppressions(cwd) {
|
|
804
|
+
const path = join4(cwd, SUPPRESSIONS_FILE);
|
|
805
|
+
let raw;
|
|
806
|
+
try {
|
|
807
|
+
raw = readFileSync2(path, "utf8");
|
|
808
|
+
} catch {
|
|
809
|
+
return void 0;
|
|
810
|
+
}
|
|
811
|
+
let parsed;
|
|
812
|
+
try {
|
|
813
|
+
parsed = JSON.parse(raw);
|
|
814
|
+
} catch (err) {
|
|
815
|
+
throw new Error(
|
|
816
|
+
`invalid ${SUPPRESSIONS_FILE}: not valid JSON (${err instanceof Error ? err.message : String(err)}).`,
|
|
817
|
+
{ cause: err }
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
if (!isPlainObject(parsed)) {
|
|
821
|
+
throw new Error(`invalid ${SUPPRESSIONS_FILE}: expected a top-level JSON object.`);
|
|
822
|
+
}
|
|
823
|
+
if (parsed.version !== 1) {
|
|
824
|
+
throw new Error(`invalid ${SUPPRESSIONS_FILE}: expected "version": 1, got ${JSON.stringify(parsed.version)}.`);
|
|
825
|
+
}
|
|
826
|
+
if (!Array.isArray(parsed.suppressions)) {
|
|
827
|
+
throw new Error(`invalid ${SUPPRESSIONS_FILE}: "suppressions" must be an array.`);
|
|
828
|
+
}
|
|
829
|
+
const entries = [];
|
|
830
|
+
parsed.suppressions.forEach((entry, i) => {
|
|
831
|
+
if (!isPlainObject(entry) || typeof entry.id !== "string") {
|
|
832
|
+
throw new Error(`invalid ${SUPPRESSIONS_FILE}: suppressions[${i}] must be an object with a string "id".`);
|
|
833
|
+
}
|
|
834
|
+
entries.push({
|
|
835
|
+
id: entry.id,
|
|
836
|
+
...typeof entry.route === "string" ? { route: entry.route } : {},
|
|
837
|
+
...typeof entry.location === "string" ? { location: entry.location } : {}
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
return entries;
|
|
841
|
+
}
|
|
842
|
+
function toEntry(r) {
|
|
843
|
+
return {
|
|
844
|
+
id: r.id,
|
|
845
|
+
...r.route !== void 0 ? { route: r.route } : {},
|
|
846
|
+
...r.location !== void 0 ? { location: r.location } : {}
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
function compareEntries(a, b) {
|
|
850
|
+
if (a.id !== b.id) return a.id < b.id ? -1 : 1;
|
|
851
|
+
const ar = a.route ?? "";
|
|
852
|
+
const br = b.route ?? "";
|
|
853
|
+
if (ar !== br) return ar < br ? -1 : 1;
|
|
854
|
+
const al = a.location ?? "";
|
|
855
|
+
const bl = b.location ?? "";
|
|
856
|
+
if (al !== bl) return al < bl ? -1 : 1;
|
|
857
|
+
return 0;
|
|
858
|
+
}
|
|
859
|
+
function writeSuppressions(cwd, results, config) {
|
|
860
|
+
const seen = /* @__PURE__ */ new Set();
|
|
861
|
+
const entries = [];
|
|
862
|
+
for (const r of results) {
|
|
863
|
+
if (!isPenalized(r.detection, config.treatDynamicAs)) continue;
|
|
864
|
+
const entry = toEntry(r);
|
|
865
|
+
const key = findingKey(entry);
|
|
866
|
+
if (seen.has(key)) continue;
|
|
867
|
+
seen.add(key);
|
|
868
|
+
entries.push(entry);
|
|
869
|
+
}
|
|
870
|
+
entries.sort(compareEntries);
|
|
871
|
+
const path = join4(cwd, SUPPRESSIONS_FILE);
|
|
872
|
+
writeFileSync(path, JSON.stringify({ version: 1, suppressions: entries }, null, 2) + "\n");
|
|
873
|
+
return entries.length;
|
|
874
|
+
}
|
|
875
|
+
function applySuppressions(results, entries, config) {
|
|
876
|
+
const keys = new Set(entries.map((e) => findingKey(e)));
|
|
877
|
+
const usedKeys = /* @__PURE__ */ new Set();
|
|
878
|
+
const kept = [];
|
|
879
|
+
let suppressed = 0;
|
|
880
|
+
for (const r of results) {
|
|
881
|
+
const key = findingKey(r);
|
|
882
|
+
if (keys.has(key) && isPenalized(r.detection, config.treatDynamicAs)) {
|
|
883
|
+
suppressed++;
|
|
884
|
+
usedKeys.add(key);
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
kept.push(r);
|
|
888
|
+
}
|
|
889
|
+
const stale = [...keys].filter((k) => !usedKeys.has(k)).length;
|
|
890
|
+
return { results: kept, suppressed, stale };
|
|
891
|
+
}
|
|
892
|
+
|
|
796
893
|
// src/color.ts
|
|
797
894
|
import { noColorPalette } from "@svelte-vitals/core";
|
|
798
895
|
var wrap = (open, close = 0) => (s) => `\x1B[${open}m${s}\x1B[${close}m`;
|
|
@@ -857,7 +954,7 @@ var FACE_WINK_BOTH = ["\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2
|
|
|
857
954
|
var FACE_WINK_ONE = ["\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", "\u2502 \u25CF < \u2502", "\u2502 \u2500\u2500 \u2502", "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"];
|
|
858
955
|
var FACE_CONTENT = ["\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", "\u2502 \u25CF \u25CF \u2502", "\u2502 \u25E1\u25E1 \u2502", "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"];
|
|
859
956
|
var FACE_HAPPY = ["\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", "\u2502 \u25CF \u25CF \u2502", "\u2502 \u2570\u2500\u2500\u256F \u2502", "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"];
|
|
860
|
-
var FACE_ECSTATIC = ["\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", "\u2502 ^ ^ \u2502", "\u2502
|
|
957
|
+
var FACE_ECSTATIC = ["\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", "\u2502 ^ ^ \u2502", "\u2502 \u2570\u2500\u2500\u256F \u2502", "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"];
|
|
861
958
|
var REACTION_FACES = {
|
|
862
959
|
content: FACE_CONTENT,
|
|
863
960
|
happy: FACE_HAPPY,
|
|
@@ -988,7 +1085,7 @@ async function playMascotGreeting(opts) {
|
|
|
988
1085
|
|
|
989
1086
|
// src/config-file.ts
|
|
990
1087
|
import { existsSync as existsSync2 } from "fs";
|
|
991
|
-
import { join as
|
|
1088
|
+
import { join as join5 } from "path";
|
|
992
1089
|
import { pathToFileURL } from "url";
|
|
993
1090
|
|
|
994
1091
|
// src/rules-config.ts
|
|
@@ -1015,7 +1112,7 @@ var CATEGORIES = ["seo", "performance", "correctness", "security", "architecture
|
|
|
1015
1112
|
var TREAT_DYNAMIC_AS_VALUES = ["pass", "warn", "fail"];
|
|
1016
1113
|
var FAIL_ON_VALUES = ["critical", "warning", "info"];
|
|
1017
1114
|
var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["treatDynamicAs", "metaComponents", "rules", "failOn", "weights"]);
|
|
1018
|
-
function
|
|
1115
|
+
function isPlainObject2(value) {
|
|
1019
1116
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1020
1117
|
}
|
|
1021
1118
|
function isMissingExtensionLoaderError(err) {
|
|
@@ -1053,7 +1150,7 @@ function validateConfigFile(raw, path) {
|
|
|
1053
1150
|
}
|
|
1054
1151
|
}
|
|
1055
1152
|
if (raw.rules !== void 0) {
|
|
1056
|
-
if (!
|
|
1153
|
+
if (!isPlainObject2(raw.rules)) {
|
|
1057
1154
|
throw new Error(`${path}: rules must be an object of rule-id \u2192 setting.`);
|
|
1058
1155
|
}
|
|
1059
1156
|
const rules = raw.rules;
|
|
@@ -1066,7 +1163,7 @@ function validateConfigFile(raw, path) {
|
|
|
1066
1163
|
config.rules = rules;
|
|
1067
1164
|
}
|
|
1068
1165
|
if (raw.weights !== void 0) {
|
|
1069
|
-
if (!
|
|
1166
|
+
if (!isPlainObject2(raw.weights)) {
|
|
1070
1167
|
throw new Error(`${path}: weights must be an object of category \u2192 number.`);
|
|
1071
1168
|
}
|
|
1072
1169
|
const weights = {};
|
|
@@ -1085,7 +1182,7 @@ function validateConfigFile(raw, path) {
|
|
|
1085
1182
|
return { config, warnings };
|
|
1086
1183
|
}
|
|
1087
1184
|
async function loadConfigFile(cwd) {
|
|
1088
|
-
const found = CONFIG_FILENAMES.map((name) =>
|
|
1185
|
+
const found = CONFIG_FILENAMES.map((name) => join5(cwd, name)).find((path) => existsSync2(path));
|
|
1089
1186
|
if (!found) return void 0;
|
|
1090
1187
|
let mod;
|
|
1091
1188
|
try {
|
|
@@ -1099,7 +1196,7 @@ async function loadConfigFile(cwd) {
|
|
|
1099
1196
|
}
|
|
1100
1197
|
throw err;
|
|
1101
1198
|
}
|
|
1102
|
-
if (!
|
|
1199
|
+
if (!isPlainObject2(mod.default)) {
|
|
1103
1200
|
throw new Error(
|
|
1104
1201
|
`${found} must have a default export that is a plain object (e.g. \`export default defineConfig({...})\` or a plain object literal).`
|
|
1105
1202
|
);
|
|
@@ -1189,7 +1286,7 @@ async function analyzeProject(opts = {}) {
|
|
|
1189
1286
|
});
|
|
1190
1287
|
await detectProject(rt, cwd);
|
|
1191
1288
|
const matches = routeMatcher(opts.route);
|
|
1192
|
-
const collected = await collectRoutes(rt, cwd, config);
|
|
1289
|
+
const collected = await collectRoutes(rt, cwd, config, opts.parseCache);
|
|
1193
1290
|
const heads = collected.heads.filter((h) => matches(h.route));
|
|
1194
1291
|
const images = collected.images.filter((i) => matches(i.route));
|
|
1195
1292
|
const headings = collected.headings.filter((h) => matches(h.route));
|
|
@@ -1233,6 +1330,18 @@ async function applyScope(results, opts) {
|
|
|
1233
1330
|
}
|
|
1234
1331
|
}
|
|
1235
1332
|
}
|
|
1333
|
+
if (!opts.noSuppressions && opts.config) {
|
|
1334
|
+
const entries = loadSuppressions(opts.cwd);
|
|
1335
|
+
if (entries !== void 0) {
|
|
1336
|
+
const { results: afterSuppressions, suppressed, stale } = applySuppressions(scoped, entries, opts.config);
|
|
1337
|
+
scoped = afterSuppressions;
|
|
1338
|
+
if (suppressed > 0 || stale > 0) {
|
|
1339
|
+
errorLog(
|
|
1340
|
+
`svelte-vitals: ${suppressed} finding(s) suppressed by ${SUPPRESSIONS_FILE}` + (stale > 0 ? ` (${stale} stale entr${stale === 1 ? "y" : "ies"} \u2014 re-run --update-suppressions to prune)` : "") + "."
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1236
1345
|
return scoped;
|
|
1237
1346
|
}
|
|
1238
1347
|
async function run(opts = {}) {
|
|
@@ -1302,7 +1411,7 @@ async function run(opts = {}) {
|
|
|
1302
1411
|
errorLog(`svelte-vitals: pass one as a path, e.g. \`npx svelte-vitals ${apps[0]}\`.`);
|
|
1303
1412
|
return 2;
|
|
1304
1413
|
}
|
|
1305
|
-
cwd =
|
|
1414
|
+
cwd = join6(cwd, chosen);
|
|
1306
1415
|
try {
|
|
1307
1416
|
analysis = await analyzeProject({
|
|
1308
1417
|
cwd,
|
|
@@ -1331,11 +1440,18 @@ async function run(opts = {}) {
|
|
|
1331
1440
|
for (const w of analysis.warnings) errorLog(`svelte-vitals: ${w}`);
|
|
1332
1441
|
try {
|
|
1333
1442
|
const { config, version } = analysis;
|
|
1443
|
+
if (opts.updateSuppressions) {
|
|
1444
|
+
const count = writeSuppressions(cwd, analysis.results, config);
|
|
1445
|
+
errorLog(`svelte-vitals: wrote ${count} suppression(s) to ${SUPPRESSIONS_FILE}.`);
|
|
1446
|
+
return 0;
|
|
1447
|
+
}
|
|
1334
1448
|
const results = await applyScope(analysis.results, {
|
|
1335
1449
|
cwd,
|
|
1450
|
+
config,
|
|
1336
1451
|
staged: opts.staged,
|
|
1337
1452
|
diffBase: opts.diffBase,
|
|
1338
1453
|
baseline: opts.baseline,
|
|
1454
|
+
noSuppressions: opts.noSuppressions,
|
|
1339
1455
|
errorLog,
|
|
1340
1456
|
analyzeOpts: {
|
|
1341
1457
|
metaComponents: opts.metaComponents,
|
|
@@ -1378,7 +1494,7 @@ async function run(opts = {}) {
|
|
|
1378
1494
|
const path = opts.outFile || "svelte-vitals-report.html";
|
|
1379
1495
|
const write = opts.writeFile ?? ((p, c) => {
|
|
1380
1496
|
mkdirSync(dirname2(p), { recursive: true });
|
|
1381
|
-
|
|
1497
|
+
writeFileSync2(p, c);
|
|
1382
1498
|
});
|
|
1383
1499
|
write(path, html);
|
|
1384
1500
|
errorLog(`svelte-vitals: wrote report to ${path}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
|
-
import { RuleSetting, Config, Severity, Category, Result } from '@svelte-vitals/core';
|
|
1
|
+
import { HeadTag, RuleSetting, Config, Severity, Category, Result } from '@svelte-vitals/core';
|
|
2
2
|
export { defineConfig } from '@svelte-vitals/core';
|
|
3
3
|
|
|
4
|
+
/** A resolved import binding: which module, and which export ('default' for default imports). */
|
|
5
|
+
interface ImportInfo {
|
|
6
|
+
source: string;
|
|
7
|
+
imported: string;
|
|
8
|
+
}
|
|
9
|
+
/** local identifier -> import binding. */
|
|
10
|
+
type ImportMap = Map<string, ImportInfo>;
|
|
11
|
+
|
|
12
|
+
/** A head tag parsed from one file, before layout-chain presence is assigned. */
|
|
13
|
+
type ParsedTag = Omit<HeadTag, 'presence' | 'file'>;
|
|
14
|
+
type Node = any;
|
|
15
|
+
interface ComponentUse {
|
|
16
|
+
name: string;
|
|
17
|
+
attributes: Node[];
|
|
18
|
+
hasSpread: boolean;
|
|
19
|
+
}
|
|
20
|
+
interface ParsedImage {
|
|
21
|
+
hasWidth: boolean;
|
|
22
|
+
hasHeight: boolean;
|
|
23
|
+
hasLoading: boolean;
|
|
24
|
+
hasAlt: boolean;
|
|
25
|
+
lazy: boolean;
|
|
26
|
+
hasSrcset: boolean;
|
|
27
|
+
/** 1-based source line, or 0 if unknown. */
|
|
28
|
+
line: number;
|
|
29
|
+
}
|
|
30
|
+
/** A page-body heading (<h1>–<h6>) parsed from one file (SEO027). */
|
|
31
|
+
interface ParsedHeading {
|
|
32
|
+
/** Heading level 1–6. */
|
|
33
|
+
level: number;
|
|
34
|
+
/** 1-based source line, or 0 if unknown. */
|
|
35
|
+
line: number;
|
|
36
|
+
}
|
|
37
|
+
interface ParsedFile {
|
|
38
|
+
headTags: ParsedTag[];
|
|
39
|
+
components: ComponentUse[];
|
|
40
|
+
imports: ImportMap;
|
|
41
|
+
images: ParsedImage[];
|
|
42
|
+
headings: ParsedHeading[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Per-run read+parse memo, keyed by project-root-relative path (as normalized by
|
|
47
|
+
* chainFiles / resolveComponentPath). Shared across routes so a file imported by
|
|
48
|
+
* many pages (a root layout, a common $lib component) is only parsed once per run.
|
|
49
|
+
*/
|
|
50
|
+
type ParseCache = Map<string, Promise<ParsedFile>>;
|
|
51
|
+
|
|
4
52
|
type ReporterName = 'console' | 'json' | 'agent' | 'sarif' | 'github' | 'html' | 'md';
|
|
5
53
|
|
|
6
54
|
/** Thrown when the target directory is not a SvelteKit project (CLI maps to exit 2). */
|
|
@@ -77,6 +125,10 @@ interface RunOptions {
|
|
|
77
125
|
staged?: boolean;
|
|
78
126
|
/** Report only findings not present when analyzing this git ref (e.g. the PR base). */
|
|
79
127
|
baseline?: string;
|
|
128
|
+
/** Disable applying svelte-vitals-suppressions.json for this run. */
|
|
129
|
+
noSuppressions?: boolean;
|
|
130
|
+
/** Analyze, then (re)write svelte-vitals-suppressions.json with all currently penalized findings and exit 0. */
|
|
131
|
+
updateSuppressions?: boolean;
|
|
80
132
|
/** Disable ANSI color in console output. */
|
|
81
133
|
noColor?: boolean;
|
|
82
134
|
/** Override stdout TTY detection (tests). */
|
|
@@ -127,6 +179,18 @@ interface AnalyzeOptions {
|
|
|
127
179
|
weights?: Partial<Record<Category, number>>;
|
|
128
180
|
/** Restrict analysis to rules in these categories (applied after rules/ignore selection). */
|
|
129
181
|
categories?: Category[];
|
|
182
|
+
/**
|
|
183
|
+
* Reuse this parse cache across multiple `analyzeProject` calls instead of
|
|
184
|
+
* starting fresh each time — the vite dev dashboard passes a long-lived cache
|
|
185
|
+
* and invalidates only the changed file's entry between re-analyses, so
|
|
186
|
+
* unchanged routes/layouts are never re-read or re-parsed. This only covers
|
|
187
|
+
* the route/layout (head-resolution) parse path via `collectRoutes` —
|
|
188
|
+
* `collectComponentFacts` (Correctness facts) is unaffected and still scans
|
|
189
|
+
* every component on each call. Callers that don't need cross-call reuse
|
|
190
|
+
* (the CLI's `run()`, MCP, the Action — each analyzes once per process) can
|
|
191
|
+
* omit this; a fresh cache is created automatically.
|
|
192
|
+
*/
|
|
193
|
+
parseCache?: ParseCache;
|
|
130
194
|
}
|
|
131
195
|
interface AnalyzeResult {
|
|
132
196
|
results: Result[];
|
|
@@ -152,14 +216,25 @@ interface ApplyScopeOptions {
|
|
|
152
216
|
staged?: boolean;
|
|
153
217
|
diffBase?: string;
|
|
154
218
|
baseline?: string;
|
|
219
|
+
/**
|
|
220
|
+
* Resolved config, needed to decide which findings count as "penalized" when
|
|
221
|
+
* applying svelte-vitals-suppressions.json (`isPenalized`). Suppression
|
|
222
|
+
* application is skipped entirely when omitted, keeping such callers'
|
|
223
|
+
* behavior unchanged (the CLI and @svelte-vitals/action both pass it).
|
|
224
|
+
*/
|
|
225
|
+
config?: Config;
|
|
226
|
+
/** Disable applying svelte-vitals-suppressions.json for this run. */
|
|
227
|
+
noSuppressions?: boolean;
|
|
155
228
|
errorLog?: (line: string) => void;
|
|
156
229
|
analyzeOpts?: AnalyzeOptions;
|
|
157
230
|
}
|
|
158
231
|
/**
|
|
159
232
|
* Narrow `results` to what a PR gate cares about: `--staged`/`--diff` restrict to
|
|
160
|
-
* changed files, `--baseline` drops findings that already existed at that ref
|
|
161
|
-
*
|
|
162
|
-
*
|
|
233
|
+
* changed files, `--baseline` drops findings that already existed at that ref,
|
|
234
|
+
* and (last) svelte-vitals-suppressions.json drops findings that were explicitly
|
|
235
|
+
* accepted via `--update-suppressions`. Shared by `run()` and
|
|
236
|
+
* `@svelte-vitals/action` (issue #154) so the git-diff/baseline orchestration
|
|
237
|
+
* lives in exactly one place.
|
|
163
238
|
*/
|
|
164
239
|
declare function applyScope(results: Result[], opts: ApplyScopeOptions): Promise<Result[]>;
|
|
165
240
|
/**
|
|
@@ -168,4 +243,4 @@ declare function applyScope(results: Result[], opts: ApplyScopeOptions): Promise
|
|
|
168
243
|
*/
|
|
169
244
|
declare function run(opts?: RunOptions): Promise<number>;
|
|
170
245
|
|
|
171
|
-
export { type AnalyzeOptions, type AnalyzeResult, type ApplyScopeOptions, type LoadedConfigFile, ProjectError, type RunOptions, analyzeProject, applyScope, buildRulesConfig, findUnknownRuleIds, knownRuleIds, loadConfigFile, routeMatcher, run, spinnerEnabled };
|
|
246
|
+
export { type AnalyzeOptions, type AnalyzeResult, type ApplyScopeOptions, type LoadedConfigFile, type ParseCache, ProjectError, type RunOptions, analyzeProject, applyScope, buildRulesConfig, findUnknownRuleIds, knownRuleIds, loadConfigFile, routeMatcher, run, spinnerEnabled };
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-vitals",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "A SvelteKit SEO checker — not a runtime Web Vitals reporter. Static analysis of your routes' head metadata.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"smol-toml": "^1.7.0",
|
|
47
47
|
"svelte": "^5.56.4",
|
|
48
48
|
"tinyglobby": "^0.2.17",
|
|
49
|
-
"@svelte-vitals/core": "0.
|
|
49
|
+
"@svelte-vitals/core": "0.24.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^24.13.3"
|