@svelte-vitals/core 0.36.1 → 0.38.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 CHANGED
@@ -956,6 +956,14 @@ interface RuleContext {
956
956
  sourceFiles?: string[];
957
957
  project: Project;
958
958
  config: Config;
959
+ /**
960
+ * Report per-declaration counts of places this rule examined. The engine supplies it and keys the
961
+ * result by rule id; a rule that does not call it gets no entry, which is distinct from an entry of
962
+ * zeros. Absent in contexts a caller builds directly. Silent last-write-wins: calling it more than
963
+ * once keeps only the most recent map, with no merge and no error — call it once, with the complete
964
+ * counts, at the end of `check()`.
965
+ */
966
+ recordExamined?: (counts: Record<string, number>) => void;
959
967
  }
960
968
  interface Rule {
961
969
  id: string;
@@ -995,7 +1003,10 @@ declare function isPenalized(detection: Detection, treatDynamicAs: TreatDynamicA
995
1003
  * Rules are independent, so they run concurrently; results are flattened in
996
1004
  * rule order for stable output.
997
1005
  */
998
- declare function runRules(rules: Rule[], ctx: RuleContext): Promise<Result[]>;
1006
+ declare function runRules(rules: Rule[], ctx: RuleContext): Promise<{
1007
+ results: Result[];
1008
+ examined: Record<string, Record<string, number>>;
1009
+ }>;
999
1010
 
1000
1011
  /**
1001
1012
  * seo/title-presence — every route should resolve a non-empty <title> (design §11).
@@ -1554,15 +1565,22 @@ interface JsonReport {
1554
1565
  * and floors that sum once, so adding this map's already-floored entries and re-dividing can disagree.
1555
1566
  */
1556
1567
  inventories: Record<string, number>;
1568
+ /**
1569
+ * Per-rule, per-declaration counts of places examined. Unlike `rules`, this describes the analysis
1570
+ * rather than the report: `--diff`, `--baseline` and suppressions do not narrow it. Three states: a
1571
+ * rule that reports no counts has no entry; a rule that counts but whose configuration declares
1572
+ * nothing has an empty entry; a declaration that judged nothing has an entry of `0`.
1573
+ */
1574
+ examined?: Record<string, Record<string, number>>;
1557
1575
  }
1558
1576
  /** Build the structured JSON report object (design §7). The shape the `json` reporter emits (issue #24). */
1559
1577
  declare function buildJsonReport(results: Result[], config: Config, meta: {
1560
1578
  version: string;
1561
- }, ruleIds?: readonly string[]): JsonReport;
1579
+ }, ruleIds?: readonly string[], examined?: Record<string, Record<string, number>>): JsonReport;
1562
1580
  /** Render results as the documented JSON report string (design §7). */
1563
1581
  declare function formatJsonReport(results: Result[], config: Config, meta: {
1564
1582
  version: string;
1565
- }, ruleIds?: readonly string[]): string;
1583
+ }, ruleIds?: readonly string[], examined?: Record<string, Record<string, number>>): string;
1566
1584
 
1567
1585
  /** Render failing findings as an agent-actionable Markdown remediation document (issue #18). */
1568
1586
  declare function formatAgentReport(results: Result[], config: Config): string;
@@ -1625,13 +1643,11 @@ declare const APP_SCRIPT: string;
1625
1643
  declare function renderAppShell(snapshot: AppSnapshot): string;
1626
1644
  /**
1627
1645
  * Static (non-live) document over a prebuilt JsonReport — kept as the public name the
1628
- * html reporter has always exported. `routeBadges` preserves the old opts shape.
1646
+ * html reporter has always exported.
1629
1647
  */
1630
1648
  declare function buildHtmlDocument(report: JsonReport, meta: {
1631
1649
  version: string;
1632
1650
  coreVersion?: string;
1633
- }, opts?: {
1634
- routeBadges?: Record<string, RouteBadge>;
1635
1651
  }): string;
1636
1652
  /** Render results as the self-contained HTML report (the CLI's `--reporter html`). */
1637
1653
  declare function formatHtmlReport(results: Result[], config: Config, meta: {
package/dist/index.js CHANGED
@@ -167,17 +167,9 @@ function collectEachBlocks(node, source, acc) {
167
167
  }
168
168
  }
169
169
  var WALK_IGNORED_KEYS = /* @__PURE__ */ new Set(["type", "start", "end", "loc", "range"]);
170
+ var NO_BOUNDARIES = /* @__PURE__ */ new Set();
170
171
  function walkEstree(node, visit) {
171
- if (Array.isArray(node)) {
172
- for (const child of node) walkEstree(child, visit);
173
- return;
174
- }
175
- if (!node || typeof node !== "object" || typeof node.type !== "string") return;
176
- visit(node);
177
- for (const key of Object.keys(node)) {
178
- if (WALK_IGNORED_KEYS.has(key)) continue;
179
- walkEstree(node[key], visit);
180
- }
172
+ walkEvalScope(node, (n) => void visit(n), /* @__PURE__ */ new Set(), NO_BOUNDARIES);
181
173
  }
182
174
  function isEffectCall(node) {
183
175
  const c = node?.callee;
@@ -280,18 +272,7 @@ function scopeIntroducedNames(node) {
280
272
  return introduced;
281
273
  }
282
274
  function walkScoped(node, visit, shadowed = /* @__PURE__ */ new Set()) {
283
- if (Array.isArray(node)) {
284
- for (const child of node) walkScoped(child, visit, shadowed);
285
- return;
286
- }
287
- if (!node || typeof node !== "object" || typeof node.type !== "string") return;
288
- const introduced = scopeIntroducedNames(node);
289
- const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
290
- visit(node, scope);
291
- for (const key of Object.keys(node)) {
292
- if (WALK_IGNORED_KEYS.has(key)) continue;
293
- walkScoped(node[key], visit, scope);
294
- }
275
+ walkEvalScope(node, (n, scope) => void visit(n, scope), shadowed, NO_BOUNDARIES);
295
276
  }
296
277
  function collectStateWrites(root, stateNames, acc, kinds) {
297
278
  const record = (name, kind) => {
@@ -769,7 +750,7 @@ function collectPropNames(program, includeBindable) {
769
750
  let seen = 0;
770
751
  let ambiguous = false;
771
752
  walkEstree(program, (n) => {
772
- if (n.type !== "VariableDeclarator" || !n.init || !isPropsCall(n.init)) return;
753
+ if (n.type !== "VariableDeclarator" || !n.init || !isPropsCall(unwrapTs(n.init))) return;
773
754
  seen++;
774
755
  if (n.id?.type === "Identifier") {
775
756
  names.add(n.id.name);
@@ -842,7 +823,7 @@ function countProps(program) {
842
823
  let seen = 0;
843
824
  let uncountable = false;
844
825
  walkEstree(program, (n) => {
845
- if (n.type !== "VariableDeclarator" || !n.init || !isPropsCall(n.init)) return;
826
+ if (n.type !== "VariableDeclarator" || !n.init || !isPropsCall(unwrapTs(n.init))) return;
846
827
  seen++;
847
828
  const props = n.id?.type === "ObjectPattern" ? n.id.properties : void 0;
848
829
  if (!Array.isArray(props) || props.some((p) => p?.type === "RestElement")) {
@@ -958,19 +939,19 @@ var EVAL_SCOPE_BOUNDARIES = /* @__PURE__ */ new Set([
958
939
  "ClassDeclaration",
959
940
  "ClassExpression"
960
941
  ]);
961
- function walkEvalScope(node, visit, shadowed = /* @__PURE__ */ new Set()) {
942
+ function walkEvalScope(node, visit, shadowed = /* @__PURE__ */ new Set(), boundaries = EVAL_SCOPE_BOUNDARIES) {
962
943
  if (Array.isArray(node)) {
963
- for (const child of node) walkEvalScope(child, visit, shadowed);
944
+ for (const child of node) walkEvalScope(child, visit, shadowed, boundaries);
964
945
  return;
965
946
  }
966
947
  if (!node || typeof node !== "object" || typeof node.type !== "string") return;
967
948
  const introduced = scopeIntroducedNames(node);
968
949
  const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
969
950
  if (visit(node, scope)) return;
970
- if (EVAL_SCOPE_BOUNDARIES.has(node.type)) return;
951
+ if (boundaries.has(node.type)) return;
971
952
  for (const key of Object.keys(node)) {
972
953
  if (WALK_IGNORED_KEYS.has(key)) continue;
973
- walkEvalScope(node[key], visit, scope);
954
+ walkEvalScope(node[key], visit, scope, boundaries);
974
955
  }
975
956
  }
976
957
  function collectEvalScopeCalls(root, source, matcher, skipSubtree, initialShadowed) {
@@ -1261,13 +1242,13 @@ function collectModuleStateDecls(program, source) {
1261
1242
  const decl = unwrapExport(stmt);
1262
1243
  if (decl?.type === "VariableDeclaration") {
1263
1244
  for (const d of decl.declarations ?? []) {
1264
- if (d?.id?.type === "Identifier" && d.init && isStateDeclaration(d.init)) {
1245
+ if (d?.id?.type === "Identifier" && d.init && isStateDeclaration(unwrapTs(d.init))) {
1265
1246
  out.push({ name: d.id.name, line: lineOf(source, d.start) });
1266
1247
  }
1267
1248
  }
1268
1249
  } else if (decl?.type === "ClassDeclaration" && decl.id?.type === "Identifier") {
1269
1250
  const hasStateField = (decl.body?.body ?? []).some(
1270
- (m) => m?.type === "PropertyDefinition" && m.value && isStateDeclaration(m.value)
1251
+ (m) => m?.type === "PropertyDefinition" && m.value && isStateDeclaration(unwrapTs(m.value))
1271
1252
  );
1272
1253
  if (hasStateField) statefulClasses.add(decl.id.name);
1273
1254
  }
@@ -1303,6 +1284,17 @@ function parseModuleFacts(source, filename) {
1303
1284
  for (const l of raw) basePathLinks.push({ ...l, line: shift(l.line) });
1304
1285
  basePathLinks.sort((a, b) => a.line - b.line);
1305
1286
  }
1287
+ const importSpans = [];
1288
+ const namespaceImports = [];
1289
+ if (program) {
1290
+ const rawImportSpans = [];
1291
+ collectImportSources(program, wrapped, rawImportSpans);
1292
+ for (const s of rawImportSpans) importSpans.push({ ...s, line: shift(s.line) });
1293
+ const rawNamespaceImports = [];
1294
+ collectNamespaceImports(program, wrapped, rawNamespaceImports);
1295
+ for (const n of rawNamespaceImports) namespaceImports.push({ ...n, line: shift(n.line) });
1296
+ }
1297
+ const imports = importSpans.map((s) => s.source);
1306
1298
  return {
1307
1299
  eachBlocks: [],
1308
1300
  effects: [],
@@ -1310,9 +1302,9 @@ function parseModuleFacts(source, filename) {
1310
1302
  javascriptUrls: [],
1311
1303
  loc: 0,
1312
1304
  propCount: 0,
1313
- imports: [],
1314
- importSpans: [],
1315
- namespaceImports: [],
1305
+ imports,
1306
+ importSpans,
1307
+ namespaceImports,
1316
1308
  constableStates: [],
1317
1309
  mutatedProps: [],
1318
1310
  stalePropDerivations: [],
@@ -1409,11 +1401,12 @@ function parseComponentFacts(source, filename) {
1409
1401
  const stateDecls = [];
1410
1402
  walkEstree(program, (n) => {
1411
1403
  if (n.type !== "VariableDeclarator" || !n.init) return;
1412
- if (isStateDeclaration(n.init) && n.id?.type === "Identifier") {
1404
+ const init = unwrapTs(n.init);
1405
+ if (isStateDeclaration(init) && n.id?.type === "Identifier") {
1413
1406
  stateNames.add(n.id.name);
1414
1407
  stateDecls.push({ name: n.id.name, line: lineOf(source, n.start) });
1415
1408
  }
1416
- if (isStateDeclaration(n.init) || isDerivedDeclaration(n.init) || isPropsCall(n.init))
1409
+ if (isStateDeclaration(init) || isDerivedDeclaration(init) || isPropsCall(init))
1417
1410
  addBoundNames(n.id, reactiveNames);
1418
1411
  });
1419
1412
  walkEstree(program, (n) => {
@@ -1440,8 +1433,10 @@ function parseComponentFacts(source, filename) {
1440
1433
  for (const stmt of program.body ?? []) {
1441
1434
  if (stmt?.type !== "VariableDeclaration") continue;
1442
1435
  for (const d of stmt.declarations ?? []) {
1443
- if (d?.id?.type !== "Identifier" || !d.init || !isPlainStateCall(d.init)) continue;
1444
- const arg = unwrapTs(d.init.arguments?.[0]);
1436
+ if (d?.id?.type !== "Identifier" || !d.init) continue;
1437
+ const init = unwrapTs(d.init);
1438
+ if (!isPlainStateCall(init)) continue;
1439
+ const arg = unwrapTs(init.arguments?.[0]);
1445
1440
  if (arg?.type === "ObjectExpression" || arg?.type === "ArrayExpression") {
1446
1441
  rawableCandidates.push({ name: d.id.name, line: lineOf(source, d.start) });
1447
1442
  }
@@ -1475,8 +1470,10 @@ function parseComponentFacts(source, filename) {
1475
1470
  for (const stmt of program.body ?? []) {
1476
1471
  if (stmt?.type !== "VariableDeclaration") continue;
1477
1472
  for (const d of stmt.declarations ?? []) {
1478
- if (d?.id?.type !== "Identifier" || !d.init || !isPlainStateCall(d.init)) continue;
1479
- const arg = unwrapTs(d.init.arguments?.[0]);
1473
+ if (d?.id?.type !== "Identifier" || !d.init) continue;
1474
+ const init = unwrapTs(d.init);
1475
+ if (!isPlainStateCall(init)) continue;
1476
+ const arg = unwrapTs(init.arguments?.[0]);
1480
1477
  if (arg?.type === "NewExpression" && arg.callee?.type === "Identifier" && BUILTIN_STATE_TYPES.has(arg.callee.name)) {
1481
1478
  builtinCandidates.set(d.id.name, { type: arg.callee.name, line: lineOf(source, d.start) });
1482
1479
  }
@@ -2444,8 +2441,11 @@ function isPenalized(detection, treatDynamicAs) {
2444
2441
 
2445
2442
  // src/engine.ts
2446
2443
  async function runRules(rules, ctx) {
2447
- const perRule = await Promise.all(rules.map((rule) => rule.check(ctx)));
2448
- return perRule.flat();
2444
+ const examined = {};
2445
+ const perRule = await Promise.all(
2446
+ rules.map((rule) => rule.check({ ...ctx, recordExamined: (counts) => void (examined[rule.id] = counts) }))
2447
+ );
2448
+ return { results: perRule.flat(), examined };
2449
2449
  }
2450
2450
 
2451
2451
  // src/rules/seo/title-presence.ts
@@ -3511,7 +3511,6 @@ var PLACEHOLDER_RES = [
3511
3511
  /yourcompany/i,
3512
3512
  /your name here/i
3513
3513
  ];
3514
- var PLACEHOLDERS = PLACEHOLDER_RES.map((r) => r.source);
3515
3514
  function hasPlaceholder(s) {
3516
3515
  return PLACEHOLDER_RES.some((re) => re.test(s));
3517
3516
  }
@@ -3743,14 +3742,12 @@ var seoJsonLdRequiredProps = jsonldRule({
3743
3742
  });
3744
3743
 
3745
3744
  // src/rules/seo/text-metrics.ts
3746
- var segmenter = typeof Intl !== "undefined" && typeof Intl.Segmenter === "function" ? new Intl.Segmenter() : void 0;
3745
+ var segmenter = new Intl.Segmenter();
3747
3746
  function collapseWhitespace(s) {
3748
3747
  return s.trim().replace(/\s+/g, " ");
3749
3748
  }
3750
3749
  function visibleLength(s) {
3751
- const collapsed = collapseWhitespace(s);
3752
- if (!segmenter) return [...collapsed].length;
3753
- return [...segmenter.segment(collapsed)].length;
3750
+ return [...segmenter.segment(collapseWhitespace(s))].length;
3754
3751
  }
3755
3752
 
3756
3753
  // src/rules/seo/length-rule.ts
@@ -5479,6 +5476,8 @@ var architectureReservedNamePlacement = {
5479
5476
  }
5480
5477
  }
5481
5478
  const usedAlternatives = /* @__PURE__ */ new Set();
5479
+ const examinedCounts = {};
5480
+ for (const key of globalAlternatives.keys()) examinedCounts[key] = 0;
5482
5481
  const allDirs = [...dirs].sort();
5483
5482
  for (const dir of allDirs) {
5484
5483
  const o = resolveRuleOptions(ID7, OPTIONS6, ctx.config, { route: dir, file: dir }, compiledOverrides);
@@ -5503,6 +5502,17 @@ var architectureReservedNamePlacement = {
5503
5502
  if (isExcluded(dir, ancestorDirs2(dir), excluded)) {
5504
5503
  continue;
5505
5504
  }
5505
+ const judged = /* @__PURE__ */ new Set();
5506
+ const resolvedValues = [
5507
+ ["placements", placements[name]],
5508
+ ["capitalisedUnitPlacements", capUnits[name]],
5509
+ ["anyCaseUnitPlacements", anyUnits[name]]
5510
+ ];
5511
+ for (const [map, value] of resolvedValues) {
5512
+ if (value === void 0) continue;
5513
+ for (const glob of globsOf(value)) judged.add(label(map, name, glob));
5514
+ }
5515
+ for (const key of judged) if (globalAlternatives.has(key)) examinedCounts[key] = (examinedCounts[key] ?? 0) + 1;
5506
5516
  const record = (map, value, qualifies) => {
5507
5517
  if (value === void 0) return false;
5508
5518
  const { matched } = matchKeys(parent, compile(globsOf(value), true));
@@ -5571,6 +5581,7 @@ var architectureReservedNamePlacement = {
5571
5581
  docsUrl: docsUrl11
5572
5582
  });
5573
5583
  }
5584
+ ctx.recordExamined?.(examinedCounts);
5574
5585
  return out;
5575
5586
  }
5576
5587
  };
@@ -6258,7 +6269,7 @@ function ruleEvidence(results, config, ruleIds) {
6258
6269
  }
6259
6270
  return out;
6260
6271
  }
6261
- function buildJsonReport(results, config, meta, ruleIds) {
6272
+ function buildJsonReport(results, config, meta, ruleIds, examined) {
6262
6273
  const { health, categories: byCat, weights } = computeHealth(results, config);
6263
6274
  const summary = summarize(results, config);
6264
6275
  const rules = ruleEvidence(results, config, ruleIds);
@@ -6288,10 +6299,21 @@ function buildJsonReport(results, config, meta, ruleIds) {
6288
6299
  const inventories = Object.fromEntries(
6289
6300
  [...buildInventory(config)].map(([pair, weight]) => [pair, Math.max(weight, INVENTORY_FLOOR)])
6290
6301
  );
6291
- return { version: meta.version, score: health, weights, categories, summary, rules, routes, siteIssues, inventories };
6302
+ return {
6303
+ version: meta.version,
6304
+ score: health,
6305
+ weights,
6306
+ categories,
6307
+ summary,
6308
+ rules,
6309
+ routes,
6310
+ siteIssues,
6311
+ inventories,
6312
+ ...examined && Object.keys(examined).length > 0 ? { examined } : {}
6313
+ };
6292
6314
  }
6293
- function formatJsonReport(results, config, meta, ruleIds) {
6294
- return JSON.stringify(buildJsonReport(results, config, meta, ruleIds), null, 2);
6315
+ function formatJsonReport(results, config, meta, ruleIds, examined) {
6316
+ return JSON.stringify(buildJsonReport(results, config, meta, ruleIds, examined), null, 2);
6295
6317
  }
6296
6318
 
6297
6319
  // src/reporter/agent.ts
@@ -7174,13 +7196,10 @@ function renderAppShell(snapshot) {
7174
7196
  const title = snapshot.live ? "svelte-vitals dashboard" : "svelte-vitals report";
7175
7197
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title><style>${APP_STYLE}</style></head><body><div class="dv-app" id="dv-app"><header class="dv-topbar" id="dv-topbar"></header><nav class="dv-sidebar" id="dv-sidebar"></nav><main class="dv-detail" id="dv-detail"></main></div><script type="application/json" id="svelte-vitals-data">${embedJson(safe)}</script><script>${APP_SCRIPT}</script></body></html>`;
7176
7198
  }
7177
- function buildHtmlDocument(report, meta, opts) {
7178
- const badges = Object.fromEntries(
7179
- Object.entries(opts?.routeBadges ?? {}).filter(([, b]) => b === "measured" || b === "static")
7180
- );
7199
+ function buildHtmlDocument(report, meta) {
7181
7200
  return renderAppShell({
7182
7201
  report,
7183
- badges,
7202
+ badges: {},
7184
7203
  analyzing: false,
7185
7204
  sequence: 0,
7186
7205
  live: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.36.1",
3
+ "version": "0.38.0",
4
4
  "description": "Shared, runtime-agnostic core for svelte-vitals (types, rule engine, scorer, reporter).",
5
5
  "type": "module",
6
6
  "license": "MIT",