basalt-ui 1.22.0 → 1.23.1

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.
Files changed (85) hide show
  1. package/AGENTS.md +29 -12
  2. package/CHANGELOG.md +28 -0
  3. package/MIGRATING.md +207 -12
  4. package/README.md +74 -21
  5. package/agent/rules/basalt-app.md +33 -3
  6. package/agent/rules/basalt-charts.md +78 -7
  7. package/agent/rules/basalt-content.md +3 -3
  8. package/agent/rules/basalt-mantine.md +4 -4
  9. package/agent/rules/basalt-tokens.md +35 -6
  10. package/agent/skills/basalt-app/SKILL.md +25 -12
  11. package/agent/skills/basalt-charts/SKILL.md +27 -10
  12. package/agent/templates/CLAUDE-block.md.tpl +3 -2
  13. package/agent/templates/DESIGN.md.tpl +3 -1
  14. package/configs/oxlint-plugin.js +49 -8
  15. package/configs/oxlint-plugin.test.ts +88 -31
  16. package/dist/charts/hooks/useBandPlot.d.ts +138 -0
  17. package/dist/charts/hooks/useBandPlot.d.ts.map +1 -0
  18. package/dist/charts/hooks/useBandPlot.js +127 -0
  19. package/dist/charts/index.d.ts +4 -0
  20. package/dist/charts/index.d.ts.map +1 -1
  21. package/dist/charts/index.js +14 -0
  22. package/dist/charts/kinds/BandStrip.d.ts +127 -0
  23. package/dist/charts/kinds/BandStrip.d.ts.map +1 -0
  24. package/dist/charts/kinds/BandStrip.js +271 -0
  25. package/dist/charts/kinds/BandStrip.test.d.ts +2 -0
  26. package/dist/charts/kinds/BandStrip.test.d.ts.map +1 -0
  27. package/dist/charts/kinds/Bars.d.ts +5 -1
  28. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  29. package/dist/charts/kinds/Bars.js +2 -0
  30. package/dist/charts/kinds/MirroredBars.d.ts +95 -0
  31. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -0
  32. package/dist/charts/kinds/MirroredBars.js +328 -0
  33. package/dist/charts/kinds/MirroredBars.test.d.ts +2 -0
  34. package/dist/charts/kinds/MirroredBars.test.d.ts.map +1 -0
  35. package/dist/charts/kinds/MultiLine.d.ts +5 -1
  36. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  37. package/dist/charts/kinds/MultiLine.js +2 -0
  38. package/dist/charts/kinds/StackedArea.d.ts +5 -1
  39. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  40. package/dist/charts/kinds/StackedArea.js +2 -0
  41. package/dist/charts/kinds/ZonedLine.d.ts +5 -1
  42. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  43. package/dist/charts/kinds/ZonedLine.js +2 -0
  44. package/dist/charts/primitives/CartesianChart.d.ts +18 -2
  45. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  46. package/dist/charts/primitives/CartesianChart.js +5 -2
  47. package/dist/charts/primitives/HatchPattern.d.ts +31 -0
  48. package/dist/charts/primitives/HatchPattern.d.ts.map +1 -0
  49. package/dist/charts/primitives/HatchPattern.js +31 -0
  50. package/dist/cli/index.d.ts +2 -0
  51. package/dist/cli/index.d.ts.map +1 -1
  52. package/dist/cli/index.js +228 -31
  53. package/dist/cli/shipped-versions.test.d.ts +2 -0
  54. package/dist/cli/shipped-versions.test.d.ts.map +1 -0
  55. package/dist/guard/index.d.ts.map +1 -1
  56. package/dist/guard/index.js +60 -13
  57. package/dist/vite.d.ts +42 -3
  58. package/dist/vite.d.ts.map +1 -1
  59. package/dist/vite.js +67 -30
  60. package/llms.txt +2 -2
  61. package/package.json +1 -1
  62. package/src/charts/hooks/useBandPlot.ts +302 -0
  63. package/src/charts/index.ts +14 -0
  64. package/src/charts/kinds/BandStrip.test.tsx +282 -0
  65. package/src/charts/kinds/BandStrip.tsx +457 -0
  66. package/src/charts/kinds/Bars.tsx +7 -1
  67. package/src/charts/kinds/DualPanel.test.tsx +10 -4
  68. package/src/charts/kinds/MirroredBars.test.tsx +232 -0
  69. package/src/charts/kinds/MirroredBars.tsx +491 -0
  70. package/src/charts/kinds/MultiLine.tsx +7 -1
  71. package/src/charts/kinds/StackedArea.tsx +7 -1
  72. package/src/charts/kinds/ZonedLine.tsx +7 -1
  73. package/src/charts/primitives/CartesianChart.test.tsx +86 -4
  74. package/src/charts/primitives/CartesianChart.tsx +28 -3
  75. package/src/charts/primitives/HatchPattern.tsx +55 -0
  76. package/src/cli/help.test.ts +74 -1
  77. package/src/cli/index.ts +436 -52
  78. package/src/cli/shipped-versions.test.ts +83 -0
  79. package/src/cli/tokens-css.test.ts +62 -0
  80. package/src/cli/toolchain-wiring.test.ts +171 -4
  81. package/src/cli/waiver-audit.test.ts +7 -3
  82. package/src/guard/check-source.test.ts +211 -42
  83. package/src/guard/index.ts +149 -23
  84. package/src/vite.test.ts +79 -0
  85. package/src/vite.ts +141 -39
package/dist/cli/index.js CHANGED
@@ -160,6 +160,16 @@ function hasBasaltProject(dir) {
160
160
  return false;
161
161
  }
162
162
  }
163
+ function descendantProjects(cwd, depth = 2) {
164
+ if (depth === 0) return [];
165
+ const out = [];
166
+ for (const name of subdirNames(cwd, { skipDotDirs: true })) {
167
+ const dir = resolve(cwd, name);
168
+ if (hasBasaltProject(dir)) out.push(dir);
169
+ else out.push(...descendantProjects(dir, depth - 1));
170
+ }
171
+ return out;
172
+ }
163
173
  function resolveProjectDir(cwd) {
164
174
  const override = process.env["BASALT_CWD"];
165
175
  if (override !== void 0 && override.length > 0) {
@@ -167,7 +177,8 @@ function resolveProjectDir(cwd) {
167
177
  return { dir, relocatedFrom: dir === cwd ? null : cwd, ambiguous: null };
168
178
  }
169
179
  if (hasBasaltProject(cwd)) return { dir: cwd, relocatedFrom: null, ambiguous: null };
170
- const candidates = collectWorkspacePackages(cwd).packages.filter((pkg) => pkg.dir !== cwd && hasBasaltProject(pkg.dir)).map((pkg) => pkg.dir);
180
+ const declared = collectWorkspacePackages(cwd).packages.filter((pkg) => pkg.dir !== cwd && hasBasaltProject(pkg.dir)).map((pkg) => pkg.dir);
181
+ const candidates = declared.length > 0 ? declared : descendantProjects(cwd);
171
182
  if (candidates.length === 1) {
172
183
  return { dir: candidates[0], relocatedFrom: cwd, ambiguous: null };
173
184
  }
@@ -239,7 +250,7 @@ function checkTheme(invocationCwd = process.cwd(), flags = []) {
239
250
  const project = resolveProjectDir(invocationCwd);
240
251
  if (project.ambiguous !== null) {
241
252
  console.error(
242
- `\u2716 basalt-ui check-theme: no basalt config at ${invocationCwd}, and ${project.ambiguous.length} workspace packages carry one (${project.ambiguous.map((d) => relativePosix(invocationCwd, d)).join(", ")}) \u2014 run it from one of them, or set BASALT_CWD to pick.`
253
+ `\u2716 basalt-ui check-theme: no basalt config at ${invocationCwd}, and ${project.ambiguous.length} packages below it carry one (${project.ambiguous.map((d) => relativePosix(invocationCwd, d)).join(", ")}) \u2014 run it from one of them, or set BASALT_CWD to pick.`
243
254
  );
244
255
  return 1;
245
256
  }
@@ -565,8 +576,9 @@ basalt.exemptRules entries (${exemptCount}):`);
565
576
  `
566
577
  ${live} live, ${dead} dead, ${outOfReach} unjudgeable, ${unaccountable} unaccountable (reported as theme-allow-unscoped by a normal run).`
567
578
  );
579
+ const includes = cfg.include ?? [];
568
580
  lines.push(
569
- `Scope: the ${scanned.length} file(s) check-theme scans under basalt.roots (${resolveRoots(cfg).join(", ")}). A waiver outside them is not audited \u2014 widen roots to cover it, or accept that nothing polices that file.`
581
+ `Scope: the ${scanned.length} file(s) check-theme scans \u2014 everything under basalt.roots (${resolveRoots(cfg).join(", ")}), plus each root's sibling index.html and public/ tree, ` + (includes.length === 0 ? "plus anything named in basalt.include (none). " : `plus basalt.include (${includes.join(", ")}). `) + "A waiver in a file none of those reach is not audited \u2014 widen roots, or name it in basalt.include, or accept that nothing polices that file."
570
582
  );
571
583
  if (dead > 0) {
572
584
  lines.push(
@@ -605,9 +617,13 @@ function readFrameworkVersion(pkgRoot) {
605
617
  return "0.0.0";
606
618
  }
607
619
  }
620
+ function basaltBinCommand(install, cwd) {
621
+ if (install.dir === null) return "bunx basalt-ui";
622
+ const binDir = resolve(install.dir, "..", ".bin");
623
+ return `${relativePosix(cwd, binDir).replace(/^\.\//, "")}/basalt-ui`;
624
+ }
608
625
  function buildTemplateVars(pkgRoot, cwd, cfg) {
609
626
  const install = findBasaltInstall(cwd);
610
- const binDir = install.dir === null ? null : resolve(install.dir, "..", ".bin");
611
627
  return {
612
628
  APP_NAME: readPackageName(cwd),
613
629
  BASALT_VERSION: readFrameworkVersion(pkgRoot),
@@ -616,7 +632,7 @@ function buildTemplateVars(pkgRoot, cwd, cfg) {
616
632
  ROOTS_GLOBS: resolveRoots(cfg).map(toRootGlob).join(" "),
617
633
  OXLINT_PRESET_PATH: shippedAssetPath(install, cwd, "configs/oxlint.json"),
618
634
  LEFTHOOK_PRESET_PATH: shippedAssetPath(install, cwd, "configs/lefthook.yml"),
619
- BASALT_BIN: binDir === null ? "bunx basalt-ui" : `${relativePosix(cwd, binDir).replace(/^\.\//, "")}/basalt-ui`
635
+ BASALT_BIN: basaltBinCommand(install, cwd)
620
636
  };
621
637
  }
622
638
  function fillTemplate(tpl, vars) {
@@ -860,6 +876,16 @@ function migrateLegacyOxfmt(cwd, pkgRoot, manifest) {
860
876
  const shipped = readSource(pkgRoot, "configs/oxfmt.json");
861
877
  if (shipped !== null && onDisk === shipped) unlinkSync(legacyAbs);
862
878
  }
879
+ const DESIGN_VERSION_LINE = /^(> Managed by basalt-ui) \(\d+\.\d+\.\d+\)\./m;
880
+ function migrateDesignVersionLine(cwd) {
881
+ const dest = resolve(cwd, "DESIGN.md");
882
+ const onDisk = readIfExists(dest);
883
+ if (onDisk === null || !DESIGN_VERSION_LINE.test(onDisk)) return;
884
+ writeFileEnsuringDir(
885
+ dest,
886
+ onDisk.replace(DESIGN_VERSION_LINE, "$1 \u2014 run `basalt-ui doctor` for the version.")
887
+ );
888
+ }
863
889
  function writeFileEnsuringDir(abs, content) {
864
890
  mkdirSync(dirname(abs), { recursive: true });
865
891
  writeFileSync(abs, content);
@@ -1246,7 +1272,7 @@ Theme guard hook: add to .claude/settings.json \u2192 hooks.PreToolUse to catch
1246
1272
  "PreToolUse": [
1247
1273
  {
1248
1274
  "matcher": "Write|Edit|MultiEdit",
1249
- "hooks": [{ "type": "command", "command": "bunx basalt-ui guard-hook" }]
1275
+ "hooks": [{ "type": "command", "command": "${ctx.vars.BASALT_BIN} guard-hook" }]
1250
1276
  }
1251
1277
  ]
1252
1278
  }`
@@ -1277,6 +1303,9 @@ function readOxlintPresetPlugins(pkgRoot) {
1277
1303
  return [];
1278
1304
  }
1279
1305
  }
1306
+ function parentInstallAdvice(cwd, parent, verb) {
1307
+ return `This repo's install is at ${relativePosix(cwd, parent)} (${parent}) \u2014 run ${verb} there, or set BASALT_CWD to it.`;
1308
+ }
1280
1309
  function findManifestAbove(dir) {
1281
1310
  let current = resolve(dir, "..");
1282
1311
  for (; ; ) {
@@ -1287,10 +1316,15 @@ function findManifestAbove(dir) {
1287
1316
  }
1288
1317
  }
1289
1318
  function sync(opts = {}, invocationCwd = process.cwd()) {
1319
+ const syncFlags = opts.flags ?? [];
1320
+ if (conflictingProfileFlags(syncFlags)) {
1321
+ console.error("basalt-ui sync: --tokens-only and --framework are alternatives \u2014 pass one.");
1322
+ return 1;
1323
+ }
1290
1324
  const project = resolveProjectDir(invocationCwd);
1291
1325
  if (project.ambiguous !== null) {
1292
1326
  console.error(
1293
- `\u2716 basalt-ui sync: no basalt config at ${invocationCwd}, and ${project.ambiguous.length} workspace packages carry one (${project.ambiguous.map((d) => relativePosix(invocationCwd, d)).join(", ")}) \u2014 run it from one of them, or set BASALT_CWD to pick.`
1327
+ `\u2716 basalt-ui sync: no basalt config at ${invocationCwd}, and ${project.ambiguous.length} packages below it carry one (${project.ambiguous.map((d) => relativePosix(invocationCwd, d)).join(", ")}) \u2014 run it from one of them, or set BASALT_CWD to pick.`
1294
1328
  );
1295
1329
  return 1;
1296
1330
  }
@@ -1300,18 +1334,27 @@ function sync(opts = {}, invocationCwd = process.cwd()) {
1300
1334
  `basalt-ui sync: no basalt config at ${project.relocatedFrom} \u2014 running in ${relativePosix(project.relocatedFrom, cwd)}, where it lives.`
1301
1335
  );
1302
1336
  }
1337
+ if (declaredProfile(readBasaltConfig(cwd), syncFlags) === "tokens-only") {
1338
+ console.log(
1339
+ `basalt-ui sync: ${MANIFEST_PATH}: n/a \u2014 a tokens-only consumer has no scaffold to reconcile. The token layer is refreshed by re-running \`basalt-ui tokens:css\`, and \`tokens:css --check\` is its drift gate. Pass --framework to force the full profile.`
1340
+ );
1341
+ return 0;
1342
+ }
1303
1343
  if (!existsSync(resolve(cwd, MANIFEST_PATH))) {
1304
1344
  const elsewhere = findManifestAbove(cwd);
1305
1345
  const wouldWrite = managedFiles(resolvePeerFlags(cwd, {}), resolvePlacement(cwd)).length;
1306
1346
  console.error(
1307
- `\u2716 basalt-ui sync: no ${MANIFEST_PATH} at ${cwd} \u2014 refusing to scaffold. \`sync\` refreshes an EXISTING basalt install; creating one is \`basalt-ui init\`'s decision, and continuing here would write ${wouldWrite} managed file(s) into this directory.` + (elsewhere === null ? " No manifest exists at any ancestor either \u2014 run `basalt-ui init` here if this package really is meant to be a consumer." : ` This repo's install is at ${relativePosix(cwd, elsewhere)} (${elsewhere}) \u2014 run sync there, or set BASALT_CWD to it.`)
1347
+ `\u2716 basalt-ui sync: no ${MANIFEST_PATH} at ${cwd} \u2014 refusing to scaffold. \`sync\` refreshes an EXISTING basalt install; creating one is \`basalt-ui init\`'s decision, and continuing here would write ${wouldWrite} managed file(s) into this directory.` + (elsewhere === null ? " No manifest exists at any ancestor either \u2014 run `basalt-ui init` here if this package really is meant to be a consumer." : ` ${parentInstallAdvice(cwd, elsewhere, "sync")}`)
1308
1348
  );
1309
1349
  return 1;
1310
1350
  }
1311
1351
  const rootsState = reconcileRoots(cwd, { write: opts.check !== true });
1312
1352
  const ctx = renderContext(cwd);
1313
1353
  const manifest = readManifest(cwd);
1314
- if (!opts.check) migrateLegacyOxfmt(cwd, ctx.pkgRoot, manifest);
1354
+ if (!opts.check) {
1355
+ migrateLegacyOxfmt(cwd, ctx.pkgRoot, manifest);
1356
+ migrateDesignVersionLine(cwd);
1357
+ }
1315
1358
  const peers = resolvePeerFlags(cwd, {});
1316
1359
  const placement = resolvePlacement(cwd);
1317
1360
  const files = managedFiles(peers, placement);
@@ -1634,7 +1677,7 @@ function doctor(invocationCwd = process.cwd(), flags = []) {
1634
1677
  `
1635
1678
  basalt-ui doctor \u2014 ${invocationCwd}
1636
1679
 
1637
- \u2716 no basalt config at ${invocationCwd}, and ${project.ambiguous.length} workspace packages carry one (${project.ambiguous.map((d) => relativePosix(invocationCwd, d)).join(", ")}) \u2014 run doctor from one of them, or set BASALT_CWD to pick.
1680
+ \u2716 no basalt config at ${invocationCwd}, and ${project.ambiguous.length} packages below it carry one (${project.ambiguous.map((d) => relativePosix(invocationCwd, d)).join(", ")}) \u2014 run doctor from one of them, or set BASALT_CWD to pick.
1638
1681
 
1639
1682
  1 hard failure(s), 0 warning(s).
1640
1683
  `
@@ -1670,6 +1713,7 @@ basalt-ui doctor \u2014 ${cwd}
1670
1713
  }
1671
1714
  const manifestAbs = resolve(cwd, MANIFEST_PATH);
1672
1715
  const manifestExists = existsSync(manifestAbs);
1716
+ const parentInstall = manifestExists ? null : findManifestAbove(cwd);
1673
1717
  const profile = inferredProfile(cwd, cfg, flags);
1674
1718
  if (profile === "tokens-only") {
1675
1719
  lines.push(
@@ -1685,6 +1729,10 @@ basalt-ui doctor \u2014 ${cwd}
1685
1729
  pass(`${MANIFEST_PATH}: n/a \u2014 a tokens-only consumer has no scaffold to reconcile`);
1686
1730
  } else if (manifestExists) {
1687
1731
  pass(`${MANIFEST_PATH} exists`);
1732
+ } else if (parentInstall !== null) {
1733
+ fail(
1734
+ `${MANIFEST_PATH} missing here, but this is not an unscaffolded consumer. ${parentInstallAdvice(cwd, parentInstall, "doctor")} \`basalt-ui init\` is NOT the fix: it would scaffold a SECOND consumer beside the real one.`
1735
+ );
1688
1736
  } else {
1689
1737
  fail(`${MANIFEST_PATH} missing \u2014 run \`basalt-ui init\` to scaffold the consumer repo`);
1690
1738
  }
@@ -1780,7 +1828,7 @@ basalt-ui doctor \u2014 ${cwd}
1780
1828
  const oxlintrcRaw = readIfExists(resolve(cwd, ".oxlintrc.json"));
1781
1829
  if (oxlintrcRaw === null) {
1782
1830
  fail(
1783
- ".oxlintrc.json missing \u2014 the shipped oxlint preset (the basalt/* design rules) is not active. Run `basalt-ui init` to seed it."
1831
+ ".oxlintrc.json missing \u2014 the shipped oxlint preset (the basalt/* design rules) is not active. " + (parentInstall === null ? "Run `basalt-ui init` to seed it." : `${parentInstallAdvice(cwd, parentInstall, "doctor")} The preset is seeded once, at the install \u2014 not per package.`)
1784
1832
  );
1785
1833
  } else {
1786
1834
  const parsed = parseJsonc(oxlintrcRaw);
@@ -1828,30 +1876,43 @@ basalt-ui doctor \u2014 ${cwd}
1828
1876
  );
1829
1877
  } else if (lefthookGate.kind === "absent") {
1830
1878
  warn(
1831
- `${lefthookGate.file}: \`lefthook dump\` resolves the merged config and NO pre-commit command runs check-theme \u2014 the theme guard is not gating commits. Either add "extends: [${lefthookCorrect}]", or add your own command running \`bunx basalt-ui check-theme\` (with \`root:\` if it must run in a subdirectory).`
1879
+ `${lefthookGate.file}: \`lefthook dump\` resolves the merged config and NO pre-commit command runs check-theme \u2014 the theme guard is not gating commits. Either add "extends: [${lefthookCorrect}]", or add your own command running \`${basaltBinCommand(install, cwd)} check-theme\` (with \`root:\` if it must run in a subdirectory).`
1832
1880
  );
1833
1881
  } else {
1834
1882
  warn(
1835
- `${lefthookGate.file} names no basalt gate and \`lefthook dump\` could not be run here, so this check cannot tell a missing gate from one wired through an \`include:\` or a remote config it never read \u2014 treat it as advisory. If the guard really is unwired, add "extends: [${lefthookCorrect}]" or your own command running \`bunx basalt-ui check-theme\`.`
1883
+ `${lefthookGate.file} names no basalt gate and \`lefthook dump\` could not be run here, so this check cannot tell a missing gate from one wired through an \`include:\` or a remote config it never read \u2014 treat it as advisory. If the guard really is unwired, add "extends: [${lefthookCorrect}]" or your own command running \`${basaltBinCommand(install, cwd)} check-theme\`.`
1836
1884
  );
1837
1885
  }
1838
1886
  const publicDir = resolve(cwd, "public");
1839
1887
  if (profile === "framework" && existsSync(publicDir)) {
1840
- const iconFiles = [
1841
- "favicon.ico",
1842
- "favicon.svg",
1843
- "favicon-96x96.png",
1844
- "apple-touch-icon.png",
1845
- "web-app-manifest-192x192.png",
1846
- "web-app-manifest-512x512.png"
1847
- ];
1848
- const missingIcons = iconFiles.filter((f) => !existsSync(resolve(publicDir, f)));
1849
- if (missingIcons.length > 0) {
1850
- warn(
1851
- `public/ is missing basaltAppPlugin icon file(s): ${missingIcons.join(", ")} \u2014 generate them (e.g. via realfavicongenerator.net) and place them at the public/ root.`
1888
+ const icons = readAppIconsOption(cwd);
1889
+ if (icons.kind === "none") {
1890
+ pass("basaltAppPlugin is configured with no icons \u2014 nothing to check");
1891
+ } else if (icons.kind === "explicit") {
1892
+ const missing = icons.sources.filter(
1893
+ (src) => !existsSync(resolve(publicDir, src.replace(/^\//, "")))
1852
1894
  );
1895
+ if (missing.length > 0) {
1896
+ warn(
1897
+ `basaltAppPlugin declares icon(s) that are not in public/: ${missing.join(", ")} \u2014 the manifest and the head links will point at a 404. Place them, or drop them from the \`icons\` array.`
1898
+ );
1899
+ } else {
1900
+ pass(
1901
+ `public/ has all ${icons.sources.length} icon file(s) basaltAppPlugin's \`icons\` option names`
1902
+ );
1903
+ }
1853
1904
  } else {
1854
- pass("public/ has all of basaltAppPlugin's default icon files");
1905
+ const dir = icons.kind === "default" ? icons.dir : null;
1906
+ const iconRoot = dir === null ? publicDir : resolve(publicDir, dir.replace(/^\//, ""));
1907
+ const shown = dir === null ? "public/" : `public/${dir.replace(/^\//, "")}`;
1908
+ const missingIcons = DEFAULT_APP_ICON_FILES.filter((f) => !existsSync(resolve(iconRoot, f)));
1909
+ if (missingIcons.length > 0) {
1910
+ warn(
1911
+ `${shown} is missing basaltAppPlugin icon file(s): ${missingIcons.join(", ")} \u2014 generate them (e.g. via realfavicongenerator.net) and place them at the ${shown} root, or name the icons this app really has via the plugin's \`icons\` array.`
1912
+ );
1913
+ } else {
1914
+ pass(`${shown} has all of basaltAppPlugin's default icon files`);
1915
+ }
1855
1916
  }
1856
1917
  }
1857
1918
  const workspaceWalk = collectWorkspacePackages(cwd);
@@ -1972,6 +2033,68 @@ basalt-ui v${pkgVersion} \u2014 published surface
1972
2033
  console.log("");
1973
2034
  return 0;
1974
2035
  }
2036
+ const DEFAULT_APP_ICON_FILES = [
2037
+ "favicon.ico",
2038
+ "favicon.svg",
2039
+ "favicon-96x96.png",
2040
+ "apple-touch-icon.png",
2041
+ "web-app-manifest-192x192.png",
2042
+ "web-app-manifest-512x512.png"
2043
+ ];
2044
+ const VITE_CONFIG_FILES = [
2045
+ "vite.config.ts",
2046
+ "vite.config.mts",
2047
+ "vite.config.cts",
2048
+ "vite.config.js",
2049
+ "vite.config.mjs",
2050
+ "vite.config.cjs"
2051
+ ];
2052
+ function balancedSpan(text, open) {
2053
+ const pairs = { "(": ")", "{": "}", "[": "]" };
2054
+ const closer = pairs[text[open]];
2055
+ if (closer === void 0) return null;
2056
+ let depth = 0;
2057
+ for (let i = open; i < text.length; i++) {
2058
+ const ch = text[i];
2059
+ if (ch === text[open]) depth++;
2060
+ else if (ch === closer) {
2061
+ depth--;
2062
+ if (depth === 0) return text.slice(open, i + 1);
2063
+ }
2064
+ }
2065
+ return null;
2066
+ }
2067
+ function readAppIconsOption(cwd) {
2068
+ const configPath = VITE_CONFIG_FILES.map((f) => resolve(cwd, f)).find((f) => existsSync(f));
2069
+ if (configPath === void 0) return { kind: "unknown" };
2070
+ const text = readIfExists(configPath);
2071
+ if (text === null) return { kind: "unknown" };
2072
+ const call = text.indexOf("basaltAppPlugin(");
2073
+ if (call === -1) return { kind: "unknown" };
2074
+ const args = balancedSpan(text, call + "basaltAppPlugin".length);
2075
+ if (args === null) return { kind: "unknown" };
2076
+ const key = args.search(/\bicons\s*:/);
2077
+ if (key === -1) return { kind: "default", dir: null };
2078
+ const valueStart = args.indexOf(":", key) + 1;
2079
+ const rest = args.slice(valueStart).trimStart();
2080
+ if (rest.startsWith("false")) return { kind: "none" };
2081
+ const offset = args.length - rest.length;
2082
+ if (rest.startsWith("[")) {
2083
+ const list = balancedSpan(args, offset);
2084
+ if (list === null) return { kind: "unknown" };
2085
+ const sources = [...list.matchAll(/\bsrc\s*:\s*['"`]([^'"`]+)['"`]/g)].map(
2086
+ (m) => m[1]
2087
+ );
2088
+ return sources.length === 0 ? { kind: "none" } : { kind: "explicit", sources };
2089
+ }
2090
+ if (rest.startsWith("{")) {
2091
+ const obj = balancedSpan(args, offset);
2092
+ if (obj === null) return { kind: "unknown" };
2093
+ const dir = /\bdir\s*:\s*['"`]([^'"`]+)['"`]/.exec(obj);
2094
+ return { kind: "default", dir: dir === null ? null : dir[1] };
2095
+ }
2096
+ return { kind: "unknown" };
2097
+ }
1975
2098
  function flagValue(flags, name) {
1976
2099
  const i = flags.indexOf(name);
1977
2100
  if (i === -1) return void 0;
@@ -1985,6 +2108,12 @@ function generatedHeader(version, command, flags) {
1985
2108
  /* basalt-ui ${version} \u2014 \`basalt-ui ${invocation}\` */
1986
2109
  `;
1987
2110
  }
2111
+ function withoutProvenanceLine(css) {
2112
+ const lines = css.split("\n");
2113
+ if (lines.length < 2) return css;
2114
+ lines[1] = "";
2115
+ return lines.join("\n");
2116
+ }
1988
2117
  function normalizeColorFunctions(css) {
1989
2118
  return css.replace(/\b(rgba?|hsla?)\(([^()]*)\)/g, (_match, fn, args) => {
1990
2119
  const parts = args.split(",").map((part) => normalizeCssNumber(part.trim()));
@@ -2019,12 +2148,15 @@ function emitGeneratedCss(content, flags, cwd, command) {
2019
2148
  );
2020
2149
  return 1;
2021
2150
  }
2022
- if (onDisk === content) {
2023
- console.log(`\u2713 ${command} --check: ${shown} is up to date.`);
2151
+ if (withoutProvenanceLine(onDisk) === withoutProvenanceLine(content)) {
2152
+ const staleProvenance = onDisk.split("\n")[1] !== content.split("\n")[1];
2153
+ console.log(
2154
+ `\u2713 ${command} --check: ${shown} is up to date.` + (staleProvenance ? " (Its provenance line still names an older basalt-ui; that line is deliberately not gated \u2014 re-run without --check whenever you want it refreshed.)" : "")
2155
+ );
2024
2156
  return 0;
2025
2157
  }
2026
2158
  console.error(
2027
- `\u2716 ${command} --check: ${shown} differs from what \`basalt-ui ${command}\` emits today (on disk ${onDisk.split("\n").length} lines, emitted ${content.split("\n").length}) \u2014 re-run the same command without --check and commit the result.`
2159
+ `\u2716 ${command} --check: ${shown} differs from what \`basalt-ui ${command}\` emits today (on disk ${onDisk.split("\n").length} lines, emitted ${content.split("\n").length}) \u2014 re-run the same command without --check and commit the result. The \`@generated\` header's provenance line is excluded from this comparison, so a version bump alone never trips it.`
2028
2160
  );
2029
2161
  return 1;
2030
2162
  }
@@ -2205,13 +2337,71 @@ async function guardHook(cwd = process.cwd()) {
2205
2337
  }
2206
2338
  return 0;
2207
2339
  }
2208
- const USAGE = "Usage: basalt-ui <init [--with-router] [--with-query] [--merge-lint] | sync [--force] [--check] |\n check-theme [--audit-allows] | check-coverage | info [--json] |\n doctor [--tokens-only|--framework] | guard-hook | tokens:css | fonts:css | help>\n\ncheck-theme [--tokens-only|--framework] [--audit-allows]\n --audit-allows reports instead of scanning: every `theme-allow` annotation and every\n `basalt.exemptRules` entry, with what each one still suppresses \u2014 proved by re-running the\n guard with that one waiver neutralized, not by reading its text. Exits 1 on a waiver that\n suppresses nothing (dead), which is what makes it usable as a CI gate.\n\ntokens:css [--out <path>] [--check] [--selector-attribute <attr> | --selector-class <class>]\n [--light-class <class>] [--dark-value <v>] [--light-value <v>]\n [--default-scheme <dark|light|none>] [--media-fallback] [--only <core|all>]\n [--no-legacy-aliases]\n Emit the --vx-* stylesheet (stdout unless --out). Defaults reproduce basalt-ui/tokens.css.\n --selector-class emits `:root.dark` instead of an attribute selector (the Tailwind convention).\n --check writes nothing and exits 1 when --out differs from what would be emitted (a CI gate).\n --no-legacy-aliases drops the deprecated camelCase spellings (--vx-accentFill and friends),\n which are emitted by default as aliases of the canonical kebab-case names.\n\nfonts:css [--out <path>] [--check]\n Emit the shipped --basalt-font-* stacks as plain CSS \u2014 the typeface half of the token layer,\n otherwise reachable only by importing styles.css.\n\ndoctor [--tokens-only|--framework]\n Auto-detects a tokens-only consumer (no manifest + no @mantine/core) and checks only what\n applies. A check that cannot RUN is reported as SKIPPED and exits non-zero.\n\ncheck-theme / doctor honour BASALT_CWD, and relocate to the single workspace package carrying a\nbasalt config when invoked from a repo root that has none \u2014 several carrying one is ambiguous\nand exits 1 in BOTH. --tokens-only and --framework are alternatives; passing both is an error.\n\nEvery subcommand accepts --help / -h to print this message and exit without running.";
2340
+ const COMMAND_FLAGS = {
2341
+ init: ["--with-router", "--with-query", "--merge-lint"],
2342
+ sync: ["--force", "--check", "--tokens-only", "--framework"],
2343
+ "check-theme": ["--audit-allows", "--tokens-only", "--framework"],
2344
+ "check-coverage": [],
2345
+ info: ["--json"],
2346
+ doctor: ["--tokens-only", "--framework"],
2347
+ "guard-hook": [],
2348
+ "tokens:css": [
2349
+ "--out",
2350
+ "--check",
2351
+ "--selector-attribute",
2352
+ "--selector-class",
2353
+ "--light-class",
2354
+ "--dark-value",
2355
+ "--light-value",
2356
+ "--default-scheme",
2357
+ "--media-fallback",
2358
+ "--only",
2359
+ "--no-legacy-aliases"
2360
+ ],
2361
+ "fonts:css": ["--out", "--check"]
2362
+ };
2363
+ const VALUE_FLAGS = /* @__PURE__ */ new Set([
2364
+ "--out",
2365
+ "--selector-attribute",
2366
+ "--selector-class",
2367
+ "--light-class",
2368
+ "--dark-value",
2369
+ "--light-value",
2370
+ "--default-scheme",
2371
+ "--only"
2372
+ ]);
2373
+ function unknownFlag(cmd, flags) {
2374
+ const allowed = COMMAND_FLAGS[cmd];
2375
+ if (allowed === void 0) return null;
2376
+ for (let i = 0; i < flags.length; i++) {
2377
+ const flag = flags[i];
2378
+ if (!flag.startsWith("-")) continue;
2379
+ if (!allowed.includes(flag)) return flag;
2380
+ if (VALUE_FLAGS.has(flag)) i++;
2381
+ }
2382
+ return null;
2383
+ }
2384
+ const USAGE = "Usage: basalt-ui <--version | init [--with-router] [--with-query] [--merge-lint] |\n sync [--force] [--check] [--tokens-only|--framework] |\n check-theme [--audit-allows] | check-coverage | info [--json] |\n doctor [--tokens-only|--framework] | guard-hook | tokens:css | fonts:css | help>\n\ncheck-theme [--tokens-only|--framework] [--audit-allows]\n --audit-allows reports instead of scanning: every `theme-allow` annotation and every\n `basalt.exemptRules` entry, with what each one still suppresses \u2014 proved by re-running the\n guard with that one waiver neutralized, not by reading its text. Exits 1 on a waiver that\n suppresses nothing (dead), which is what makes it usable as a CI gate.\n\ntokens:css [--out <path>] [--check] [--selector-attribute <attr> | --selector-class <class>]\n [--light-class <class>] [--dark-value <v>] [--light-value <v>]\n [--default-scheme <dark|light|none>] [--media-fallback] [--only <core|all>]\n [--no-legacy-aliases]\n Emit the --vx-* stylesheet (stdout unless --out). Defaults reproduce basalt-ui/tokens.css.\n --selector-class emits `:root.dark` instead of an attribute selector (the Tailwind convention).\n --check writes nothing and exits 1 when --out differs from what would be emitted (a CI gate).\n --no-legacy-aliases drops the deprecated camelCase spellings (--vx-accentFill and friends),\n which are emitted by default as aliases of the canonical kebab-case names.\n\nfonts:css [--out <path>] [--check]\n Emit the shipped --basalt-font-* stacks as plain CSS \u2014 the typeface half of the token layer,\n otherwise reachable only by importing styles.css.\n\ndoctor [--tokens-only|--framework]\n Auto-detects a tokens-only consumer (no manifest + no @mantine/core) and checks only what\n applies. A check that cannot RUN is reported as SKIPPED and exits non-zero.\n\nsync [--force] [--check] [--tokens-only|--framework]\n Refreshes an EXISTING install; it never creates one (that is init). A tokens-only consumer\n has no scaffold to reconcile, so sync reports n/a and exits 0 there \u2014 as doctor does.\n\ncheck-theme / doctor / sync all resolve their project the same way: they honour BASALT_CWD, and\nrelocate to the single package below the cwd carrying a basalt config when invoked from a repo\nroot that has none \u2014 several carrying one is ambiguous and exits 1 in ALL THREE.\n--tokens-only and --framework are alternatives; passing both is an error.\n\nEvery subcommand accepts --help / -h to print this message and exit without running.\n--version / -v prints the resolved package version and exits 0 \u2014 the one command that proves\nWHICH basalt-ui ran, which a `bunx` cache can otherwise make a lie. An unknown command or an\nunrecognized flag exits 1 and names what it did not understand; neither is ever ignored.";
2209
2385
  function run(argv, cwd = process.cwd()) {
2210
2386
  const [cmd, ...flags] = argv;
2211
2387
  if (cmd === "help" || cmd === "--help" || cmd === "-h" || flags.includes("--help") || flags.includes("-h")) {
2212
2388
  console.log(USAGE);
2213
2389
  return 0;
2214
2390
  }
2391
+ if (cmd === "--version" || cmd === "-v" || cmd === "version") {
2392
+ console.log(readFrameworkVersion(packageRoot()));
2393
+ return 0;
2394
+ }
2395
+ if (cmd !== void 0) {
2396
+ const bad = unknownFlag(cmd, flags);
2397
+ if (bad !== null) {
2398
+ console.error(
2399
+ `basalt-ui ${cmd}: unrecognized flag '${bad}'. Accepted: ${(COMMAND_FLAGS[cmd] ?? []).join(" ") || "(none)"} --help
2400
+ `
2401
+ );
2402
+ return 1;
2403
+ }
2404
+ }
2215
2405
  switch (cmd) {
2216
2406
  case "init":
2217
2407
  return init(cwd, {
@@ -2220,7 +2410,10 @@ function run(argv, cwd = process.cwd()) {
2220
2410
  mergeLint: flags.includes("--merge-lint")
2221
2411
  });
2222
2412
  case "sync":
2223
- return sync({ force: flags.includes("--force"), check: flags.includes("--check") }, cwd);
2413
+ return sync(
2414
+ { force: flags.includes("--force"), check: flags.includes("--check"), flags },
2415
+ cwd
2416
+ );
2224
2417
  case "check-theme":
2225
2418
  return checkTheme(cwd, flags);
2226
2419
  case "check-coverage":
@@ -2236,6 +2429,10 @@ function run(argv, cwd = process.cwd()) {
2236
2429
  case "fonts:css":
2237
2430
  return fontsCss(flags, cwd);
2238
2431
  default:
2432
+ console.error(
2433
+ cmd === void 0 ? "basalt-ui: no command given.\n" : `basalt-ui: unknown command '${cmd}'.
2434
+ `
2435
+ );
2239
2436
  console.error(USAGE);
2240
2437
  return 1;
2241
2438
  }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=shipped-versions.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shipped-versions.test.d.ts","sourceRoot":"","sources":["../../src/cli/shipped-versions.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/guard/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/guard/index.ts"],"names":[],"mappings":"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"}
@@ -108,8 +108,28 @@ const RADIUS_PROP_RE = /\bradius=(?:\{[0-9]+\}|"[0-9]+")/g;
108
108
  const MOTION_TRANSITION_NUMERIC = /\btransition\s*=\s*\{\{[^}]*\b(?:duration|stiffness|damping|mass)\s*:\s*-?\d/g;
109
109
  const MOTION_TRANSITION_EASE_ARRAY = /\btransition\s*=\s*\{\{[^}]*\bease\s*:\s*\[/g;
110
110
  const RAW_CHART_LEGEND_ARRAY = /<ChartLegend\b[^>]*?\bitems\s*=\s*\{\s*\[/g;
111
- const CHART_ENTRY_POINT_TAG = /<(?:MultiLine|Bars|Donut|DualPanel|Heatmap|ZonedLine|StackedArea|LineSparkline|BarSparkline)\b(?:<[^<>]*>)?(?:=>|[^>])*?>/g;
111
+ const CHART_ENTRY_POINT_TAG = /<(MultiLine|Bars|BandStrip|Donut|DualPanel|Heatmap|MirroredBars|ZonedLine|StackedArea|LineSparkline|BarSparkline)\b(?:<[^<>]*>)?(?:=>|[^>])*?>/g;
112
112
  const HAS_ARIA_LABEL_PROP = /\bariaLabel\s*=/;
113
+ const LOCAL_COMPONENT_DEF = /(?:^|[\n;}])\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var)\s+([A-Z]\w*)/g;
114
+ const BASALT_NAMED_IMPORT = /import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+['"]basalt-ui(?:\/[^'"]*)?['"]/g;
115
+ function localComponentNames(codeText) {
116
+ const out = /* @__PURE__ */ new Set();
117
+ for (const m of codeText.matchAll(LOCAL_COMPONENT_DEF)) out.add(m[1]);
118
+ return out;
119
+ }
120
+ function basaltImportedNames(codeText) {
121
+ const out = /* @__PURE__ */ new Set();
122
+ for (const m of codeText.matchAll(BASALT_NAMED_IMPORT)) {
123
+ for (const raw of m[1].split(",")) {
124
+ const spec = raw.trim().replace(/^type\s+/, "");
125
+ if (spec.length === 0) continue;
126
+ const parts = spec.split(/\s+as\s+/);
127
+ const bound = parts[parts.length - 1].trim();
128
+ if (bound.length > 0) out.add(bound);
129
+ }
130
+ }
131
+ return out;
132
+ }
113
133
  const RAW_FORM_CONTROL = /<(?:input|select|textarea)\b/g;
114
134
  const RAW_FORM_CONTROL_TAG = /<(?:input|select|textarea)\b(?:=>|[^>])*?>/g;
115
135
  const STYLES_INPUT_PART = /\bstyles\s*=\s*\{\{\s*input\s*:\s*\{([^}]*)\}/g;
@@ -250,10 +270,23 @@ function parseAllowAnnotation(rest, scope) {
250
270
  return { rules, unknownRules, hasReason: reason.length >= MIN_ALLOW_REASON_LENGTH, scope };
251
271
  }
252
272
  const MAX_CSS_CONTINUATION_LINES = 8;
253
- const MAX_COMMENT_BLOCK_LINES = 8;
273
+ function commentBlockEnd(lines, index, tokenAt, syntax) {
274
+ const line = lines[index] ?? "";
275
+ if (syntax === "ts" && /\/\/\s*$/.test(line.slice(0, tokenAt))) return index;
276
+ const closer = syntax === "markup" ? "-->" : "*/";
277
+ if (line.slice(tokenAt).includes(closer)) return index;
278
+ for (let ahead = index + 1; ahead < lines.length; ahead++) {
279
+ if ((lines[ahead] ?? "").includes(closer)) return ahead;
280
+ }
281
+ return index;
282
+ }
254
283
  const COMMENT_OPENER_BEFORE = /(?:\/\/|\/\*|<!--)\s*$/;
255
284
  const COMMENT_ONLY_LINE = /^\s*(?:\{\s*\/\*|\/\/|\/\*|\*|<!--)/;
256
285
  const JSX_COMMENT_CLOSE = /\*\/\s*\}\s*$/;
286
+ function isCommentOnly(raw, stripped) {
287
+ const code = stripped.trim();
288
+ return code === "" || COMMENT_ONLY_LINE.test(raw) || code === "}" && JSX_COMMENT_CLOSE.test(raw);
289
+ }
257
290
  const ANNOTATION_PREFIX = /(?:^|\/\/|\/\*\*?|<!--|^\s*\*)\s*$/;
258
291
  function escapeRegExp(value) {
259
292
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -273,7 +306,7 @@ function collectAllowAnnotations(lines, codeLines, allowComment, syntax) {
273
306
  list.push(annotation);
274
307
  byLine.set(index, list);
275
308
  };
276
- const record = (index, annotation, trailing) => {
309
+ const record = (index, annotation, trailing, tokenAt) => {
277
310
  declared.push({ line: index + 1, annotation });
278
311
  if (annotation.scope === "file") {
279
312
  fileScoped.push(annotation);
@@ -281,11 +314,14 @@ function collectAllowAnnotations(lines, codeLines, allowComment, syntax) {
281
314
  }
282
315
  add(index, annotation);
283
316
  if (!trailing) {
284
- for (let ahead = 1; ahead <= MAX_COMMENT_BLOCK_LINES; ahead++) {
285
- const raw = lines[index + ahead] ?? "";
286
- add(index + ahead, annotation);
287
- if (raw.trim() !== "" && (codeLines[index + ahead] ?? "").trim() !== "") break;
317
+ const closesAt = commentBlockEnd(lines, index, tokenAt, syntax);
318
+ for (let ahead = 1; index + ahead < lines.length; ahead++) {
319
+ const at = index + ahead;
320
+ add(at, annotation);
321
+ if (at <= closesAt) continue;
322
+ const raw = lines[at] ?? "";
288
323
  if (raw.trim() === "") break;
324
+ if (!isCommentOnly(raw, codeLines[at] ?? "")) break;
289
325
  }
290
326
  return;
291
327
  }
@@ -301,11 +337,15 @@ function collectAllowAnnotations(lines, codeLines, allowComment, syntax) {
301
337
  for (let i = 0; i < lines.length; i++) {
302
338
  const line = lines[i] ?? "";
303
339
  const stripped = codeLines[i] ?? "";
304
- const code = stripped.trim();
305
- const isCommentOnlyLine = code === "" || COMMENT_ONLY_LINE.test(line) || code === "}" && JSX_COMMENT_CLOSE.test(line);
340
+ const isCommentOnlyLine = isCommentOnly(line, stripped);
306
341
  if (syntax === "markup") {
307
342
  for (const m of line.matchAll(jsonKey)) {
308
- record(i, parseAllowAnnotation(m[2] ?? "", m[1] === void 0 ? "line" : "file"), false);
343
+ record(
344
+ i,
345
+ parseAllowAnnotation(m[2] ?? "", m[1] === void 0 ? "line" : "file"),
346
+ false,
347
+ m.index
348
+ );
309
349
  }
310
350
  }
311
351
  for (const m of line.matchAll(token)) {
@@ -317,7 +357,8 @@ function collectAllowAnnotations(lines, codeLines, allowComment, syntax) {
317
357
  record(
318
358
  i,
319
359
  parseAllowAnnotation(rest, m[1] === void 0 ? "line" : "file"),
320
- !isCommentOnlyLine
360
+ !isCommentOnlyLine,
361
+ at
321
362
  );
322
363
  }
323
364
  }
@@ -943,15 +984,21 @@ function checkSource(text, relPath, cfg) {
943
984
  });
944
985
  }
945
986
  }
946
- if (GUARD_RULES["unframed-chart"].enabled(cfg) && ruleApplies("unframed-chart", relPath)) {
987
+ const unframedChartRuns = GUARD_RULES["unframed-chart"].enabled(cfg) && ruleApplies("unframed-chart", relPath);
988
+ const ariaLabelRuns = GUARD_RULES["chart-missing-aria-label"].enabled(cfg) && ruleApplies("chart-missing-aria-label", relPath);
989
+ const localNames = unframedChartRuns || ariaLabelRuns ? localComponentNames(codeText) : /* @__PURE__ */ new Set();
990
+ const basaltNames = unframedChartRuns || ariaLabelRuns ? basaltImportedNames(codeText) : /* @__PURE__ */ new Set();
991
+ const isShippedTag = (tag) => basaltNames.has(tag) || !localNames.has(tag);
992
+ if (unframedChartRuns && isShippedTag("ChartLegend")) {
947
993
  for (const m of codeText.matchAll(RAW_CHART_LEGEND_ARRAY)) {
948
994
  const lineNo = codeText.slice(0, (m.index ?? 0) + m[0].length).split("\n").length;
949
995
  push("unframed-chart", lineNo, "items={[");
950
996
  }
951
997
  }
952
- if (GUARD_RULES["chart-missing-aria-label"].enabled(cfg) && ruleApplies("chart-missing-aria-label", relPath)) {
998
+ if (ariaLabelRuns) {
953
999
  for (const m of codeText.matchAll(CHART_ENTRY_POINT_TAG)) {
954
1000
  const tagText = m[0];
1001
+ if (!isShippedTag(m[1])) continue;
955
1002
  if (HAS_ARIA_LABEL_PROP.test(tagText)) continue;
956
1003
  const startLine = codeText.slice(0, m.index ?? 0).split("\n").length;
957
1004
  const endLine = startLine + (tagText.split("\n").length - 1);