onsystem 0.10.2 → 0.11.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/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { $ as DEFAULT_TOKENS, A as unrecordedRules, At as relativePath, B as statusKinds, C as countBaseline, D as pruneBaseline, Dt as isInside, E as onLines, Et as SCRIPT_FILE, It as unique, J as ConfigError, L as toDesignMd, M as writeBaseline, O as readBaseline, Ot as matchesGlob, Pt as plural, R as toDtcg, Rt as readProjectConfig, S as applyBaseline, St as isDesignSystemName, T as goneFiles, Y as DEFAULT_DOCS, _ as formatDiagnostics, _t as PACKAGE_FIELD, b as BASELINE_FILE, bt as detectProject, c as silentLogger, et as FALLBACK_TOKENS, g as escapeData, gt as notReadYet, h as uncheckedNotice, ht as libraryTitle, i as componentFiles, j as updateBaseline, jt as toPosix, k as rulesRun, l as stderrLogger, o as loadDesignSystem, ot as excludedBy, q as CONFIG_FILES, rt as RULE_IDS, s as tokenFiles, tt as LEGACY_CONFIG_FILES, ut as rulesFor, v as NAME, w as findMoves, x as LEGACY_BASELINE_FILE, y as VERSION, yt as declaredConfig } from "./design-system-rl__N6tw.js";
3
- import { a as notReadNotices, i as missingDependencies, o as explainProject, s as librariesLeftOut, t as serveStdio } from "./stdio-CkczPt_1.js";
4
- import { r as loadTarget } from "./workspace-INef0Yqt.js";
2
+ import { $ as DEFAULT_TOKENS, A as unrecordedRules, At as relativePath, B as statusKinds, C as countBaseline, D as pruneBaseline, Dt as isInside, E as onLines, Et as SCRIPT_FILE, It as unique, J as ConfigError, L as toDesignMd, M as writeBaseline, O as readBaseline, Ot as matchesGlob, Pt as plural, R as toDtcg, Rt as readProjectConfig, S as applyBaseline, St as isDesignSystemName, T as goneFiles, Y as DEFAULT_DOCS, _ as formatDiagnostics, _t as PACKAGE_FIELD, b as BASELINE_FILE, bt as detectProject, c as silentLogger, et as FALLBACK_TOKENS, g as escapeData, gt as notReadYet, h as uncheckedNotice, ht as libraryTitle, i as componentFiles, j as updateBaseline, jt as toPosix, k as rulesRun, l as stderrLogger, o as loadDesignSystem, ot as excludedBy, q as CONFIG_FILES, rt as RULE_IDS, s as tokenFiles, tt as LEGACY_CONFIG_FILES, ut as rulesFor, v as NAME, w as findMoves, x as LEGACY_BASELINE_FILE, y as VERSION, yt as declaredConfig } from "./design-system-Csqts7Eq.js";
3
+ import { a as notReadNotices, i as missingDependencies, o as explainProject, s as librariesLeftOut, t as serveStdio } from "./stdio-0TpyqZaD.js";
4
+ import { r as loadTarget } from "./workspace-Dbxubo9n.js";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import { escapePath, glob } from "tinyglobby";
@@ -324,7 +324,9 @@ function readManifest(root) {
324
324
  /**
325
325
  * Declares the config in the package's package.json, `"onsystem": "./onsystem.config.json"`,
326
326
  * and adds it to `files` when the package lists the files it publishes, so apps that install it
327
- * get it too. Writes ONSYSTEM.md for the apps' teams unless one exists. Returns what it wrote.
327
+ * get it too. Writes ONSYSTEM.md for the apps' teams unless one exists, and adds it to `files`
328
+ * as well, for the teams of apps that install the package, unless `files` leaves it out
329
+ * (`!ONSYSTEM.md`). Returns what it wrote.
328
330
  */
329
331
  function declarePackage(root, manifest, name, force, notes) {
330
332
  const written = [];
@@ -336,15 +338,18 @@ function declarePackage(root, manifest, name, force, notes) {
336
338
  changed = true;
337
339
  }
338
340
  const files = Array.isArray(json.files) ? json.files : void 0;
339
- if (files && !ships(files, CONFIG_FILE)) {
340
- files.push(CONFIG_FILE);
341
+ const added = [];
342
+ for (const file of [CONFIG_FILE, GUIDE_FILE]) {
343
+ if (!files || ships(files, file) || file === "ONSYSTEM.md" && excludes(files, file)) continue;
344
+ files.push(file);
345
+ added.push(file);
341
346
  changed = true;
342
347
  }
343
348
  if (changed) {
344
349
  fs.writeFileSync(manifest.file, `${JSON.stringify(json, null, manifest.indent)}${manifest.newline ? "\n" : ""}`);
345
350
  written.push("package.json");
346
351
  }
347
- notes.push(`This is a design-system package: package.json declares its config ("${PACKAGE_FIELD}": "${value}"${files ? ", also added to \"files\"" : ""}), which every app that depends on ${name} reads, under its own config when it has one.`);
352
+ notes.push(`This is a design-system package: package.json declares its config ("${PACKAGE_FIELD}": "${value}"${added.length ? `, with ${added.join(" and ")} added to "files"` : ""}), which every app that depends on ${name} reads, under its own config when it has one.`);
348
353
  const guide = path.join(root, GUIDE_FILE);
349
354
  if (force || !fs.existsSync(guide)) {
350
355
  fs.writeFileSync(guide, consumerGuide(name));
@@ -625,6 +630,14 @@ function aliasFolder(alias, project) {
625
630
  }
626
631
  }
627
632
  }
633
+ /** Whether npm's `files` list leaves `file` out with a negated entry: `!ONSYSTEM.md`, `!*.md`. */
634
+ function excludes(files, file) {
635
+ return files.some((entry) => {
636
+ if (typeof entry !== "string" || !entry.startsWith("!")) return false;
637
+ const pattern = entry.slice(1).replace(/^\.?\//, "");
638
+ return pattern === file || matchesGlob(file, pattern);
639
+ });
640
+ }
628
641
  /** Whether npm's `files` list publishes `file`, a path relative to the package. */
629
642
  function ships(files, file) {
630
643
  return files.some((entry) => {
@@ -387,7 +387,7 @@ function moduleFile(base, extensions = MODULE_EXTENSIONS, within) {
387
387
  base,
388
388
  ...extensions.map((ext) => `${base}${ext}`),
389
389
  ...extensions.map((ext) => path.join(base, `index${ext}`))
390
- ].find((file) => isFile$1(file) && !/(?:^|[\\/])node_modules[\\/]/.test(within ? path.relative(within, file) : file));
390
+ ].find((file) => isFile$2(file) && !/(?:^|[\\/])node_modules[\\/]/.test(within ? path.relative(within, file) : file));
391
391
  }
392
392
  const BUILD_CONFIGS = [
393
393
  "tsup",
@@ -580,7 +580,7 @@ function reexports(source) {
580
580
  barrel
581
581
  };
582
582
  }
583
- function isFile$1(file) {
583
+ function isFile$2(file) {
584
584
  try {
585
585
  return fs.statSync(file).isFile();
586
586
  } catch {
@@ -985,7 +985,7 @@ function fromComponentsJson(root, dir, json, project, packages, note = ignore) {
985
985
  */
986
986
  function withAppStylesheets(root, named) {
987
987
  if (named.every((file) => isInside(root, file))) return named;
988
- const own = APP_STYLESHEETS.map((f) => path.join(root, f)).filter(isFile);
988
+ const own = APP_STYLESHEETS.map((f) => path.join(root, f)).filter(isFile$1);
989
989
  if (!own.length) return named;
990
990
  const imported = new Set(importedStylesheets(own, stylesheetResolver(root)));
991
991
  return [...own, ...named.filter((file) => !imported.has(file))];
@@ -1145,7 +1145,7 @@ function fromPackage(root, dir, pkg, note = ignore, installed = false) {
1145
1145
  for (const mapping of mappings) {
1146
1146
  const file = path.resolve(root, mapping.target);
1147
1147
  if (installed && isCompiledExport(dir, file)) continue;
1148
- if (mapping.target.includes("*") || !SCRIPT_FILE$1.test(file) || !isFile(file)) {
1148
+ if (mapping.target.includes("*") || !SCRIPT_FILE$1.test(file) || !isFile$1(file)) {
1149
1149
  add(mapping, COMPONENT_FILE.test(mapping.target));
1150
1150
  continue;
1151
1151
  }
@@ -1460,7 +1460,7 @@ function stylesheetResolver(root, { importPath } = {}) {
1460
1460
  }
1461
1461
  return designSystems.get(name);
1462
1462
  };
1463
- const stylesheet = (file, within) => [file, `${file}.css`].find((f) => f.endsWith(".css") && (within === void 0 || isInside(within, f) && !isBuildOutput(within, f)) && !/(?:^|[\\/])node_modules[\\/]/.test(within === void 0 ? f : path.relative(within, f)) && isFile(f));
1463
+ const stylesheet = (file, within) => [file, `${file}.css`].find((f) => f.endsWith(".css") && (within === void 0 || isInside(within, f) && !isBuildOutput(within, f)) && !/(?:^|[\\/])node_modules[\\/]/.test(within === void 0 ? f : path.relative(within, f)) && isFile$1(f));
1464
1464
  return (specifier, fromFile) => {
1465
1465
  if (/^(?:[a-z][\w+.-]*:|\/)/i.test(specifier)) return void 0;
1466
1466
  const relative = stylesheet(path.resolve(path.dirname(fromFile), specifier), installedPackageOf(fromFile));
@@ -1491,7 +1491,7 @@ function installedStylesheet(dir, subpath) {
1491
1491
  if (!match) return void 0;
1492
1492
  const target = exportTargets(match.value, APP_CONDITIONS).map((t) => t.replaceAll("*", match.middle)).find((t) => t.endsWith(".css"));
1493
1493
  const file = target === void 0 ? void 0 : path.resolve(dir, target);
1494
- return file && isInside(dir, file) && !isBuildOutput(dir, file) && isFile(file) ? file : null;
1494
+ return file && isInside(dir, file) && !isBuildOutput(dir, file) && isFile$1(file) ? file : null;
1495
1495
  }
1496
1496
  /**
1497
1497
  * The folder of `name` installed from a registry, when zero config reads it as a design system:
@@ -1521,12 +1521,15 @@ function installedPackageOf(file) {
1521
1521
  function exportedFile(dir, subpath, accept) {
1522
1522
  for (const { middle, value } of exportMatches(dir, subpath)) for (const target of exportTargets(value)) {
1523
1523
  const file = path.resolve(dir, target.replaceAll("*", middle));
1524
- if (isInside(dir, file) && accept(file) && isFile(file)) return file;
1524
+ if (isInside(dir, file) && accept(file) && isFile$1(file)) return file;
1525
1525
  }
1526
1526
  }
1527
1527
  /**
1528
1528
  * The entries of package `dir`'s `exports` that map `subpath`, with what their `*` stands for:
1529
- * an exact subpath first, then the most specific pattern, as Node.js picks them.
1529
+ * an exact subpath first, then the patterns, as Node.js orders them (`PATTERN_KEY_COMPARE`): the
1530
+ * longest prefix before the `*` first, then the longest key, so `./components/*.js` comes before
1531
+ * `./components/*` for `./components/tip.js`. A pattern has one `*`, which stands for at least
1532
+ * one character.
1530
1533
  */
1531
1534
  function exportMatches(dir, subpath) {
1532
1535
  const exports = readJson$1(path.join(dir, "package.json"))?.exports;
@@ -1538,22 +1541,35 @@ function exportMatches(dir, subpath) {
1538
1541
  if (key === subpath) matches.push({
1539
1542
  middle: "",
1540
1543
  value,
1541
- rank: Infinity
1544
+ rank: Infinity,
1545
+ length: key.length
1542
1546
  });
1543
1547
  continue;
1544
1548
  }
1549
+ if (key.lastIndexOf("*") !== star) continue;
1545
1550
  const prefix = key.slice(0, star);
1546
1551
  const suffix = key.slice(star + 1);
1547
- if (subpath.startsWith(prefix) && subpath.endsWith(suffix) && subpath.length >= prefix.length + suffix.length) {
1552
+ if (subpath.startsWith(prefix) && subpath.endsWith(suffix) && subpath.length > prefix.length + suffix.length) {
1548
1553
  const middle = subpath.slice(prefix.length, subpath.length - suffix.length);
1549
1554
  matches.push({
1550
1555
  middle,
1551
1556
  value,
1552
- rank: prefix.length
1557
+ rank: prefix.length,
1558
+ length: key.length
1553
1559
  });
1554
1560
  }
1555
1561
  }
1556
- return matches.sort((a, b) => b.rank - a.rank);
1562
+ return matches.sort((a, b) => b.rank - a.rank || b.length - a.length);
1563
+ }
1564
+ /**
1565
+ * The targets package `dir`'s `exports` map `subpath` (`./components/tooltip`) to, as written:
1566
+ * through the entry Node.js picks (its exact subpath, else the most specific pattern) and every
1567
+ * condition but `types`, with `*` filled in. Undefined when no entry maps the subpath; none when
1568
+ * the entry blocks it (`null`).
1569
+ */
1570
+ function exportedTargets(dir, subpath) {
1571
+ const [match] = exportMatches(dir, subpath);
1572
+ return match && exportTargets(match.value).map((t) => t.replaceAll("*", match.middle));
1557
1573
  }
1558
1574
  /** Every target of an export condition map, in order, but its types, or those of `conditions` alone. */
1559
1575
  function exportTargets(value, conditions) {
@@ -1603,7 +1619,7 @@ function declaredConfig(dir, pkg = readJson$1(path.join(dir, "package.json"))) {
1603
1619
  if (typeof value !== "string" || !value.endsWith(".json")) return { problem: `${field} is ${JSON.stringify(value)}: it names a JSON config file in the package, such as "./onsystem.config.json".` };
1604
1620
  const file = path.resolve(dir, value);
1605
1621
  if (!isInside(dir, file) || file === path.resolve(dir)) return { problem: `${field} names ${JSON.stringify(value)}, which is outside the package.` };
1606
- if (!isFile(file)) return { problem: `${field} names ${JSON.stringify(value)}, which does not exist.` };
1622
+ if (!isFile$1(file)) return { problem: `${field} names ${JSON.stringify(value)}, which does not exist.` };
1607
1623
  return { file };
1608
1624
  }
1609
1625
  /**
@@ -1722,7 +1738,7 @@ function scopeOf$1(name) {
1722
1738
  function declaresTypes(dir, pkg) {
1723
1739
  if (typeof pkg.types === "string" || typeof pkg.typings === "string") return true;
1724
1740
  const hasTypes = (value) => Array.isArray(value) ? value.some(hasTypes) : value !== null && typeof value === "object" && Object.entries(value).some(([key, nested]) => key === "types" || hasTypes(nested));
1725
- return hasTypes(pkg.exports) || isFile(path.join(dir, "index.d.ts"));
1741
+ return hasTypes(pkg.exports) || isFile$1(path.join(dir, "index.d.ts"));
1726
1742
  }
1727
1743
  /** Files that hold no component to read: declarations, tests and stories, as `exclude` has them. */
1728
1744
  const NOT_COMPONENT_SOURCES = [
@@ -2061,7 +2077,7 @@ function readJson$1(file) {
2061
2077
  function asRecord(value) {
2062
2078
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2063
2079
  }
2064
- function isFile(file) {
2080
+ function isFile$1(file) {
2065
2081
  try {
2066
2082
  return fs.statSync(file).isFile();
2067
2083
  } catch {
@@ -8130,21 +8146,29 @@ function toDesignMd(tokens, options) {
8130
8146
  const list = tokens.filter((t) => t.category === section.category);
8131
8147
  if (!list.length) continue;
8132
8148
  if (section.key) {
8133
- const values = {};
8149
+ const values = /* @__PURE__ */ new Map();
8150
+ let steps;
8134
8151
  for (const token of list) {
8152
+ if (isSpacingUnit(token)) {
8153
+ steps = spacingSteps(token, lookup);
8154
+ continue;
8155
+ }
8135
8156
  const key = keys.get(token);
8136
8157
  if (key === void 0) continue;
8137
8158
  const target = token.aliasOf ? byName.get(token.aliasOf) : void 0;
8138
- const targetKey = target?.category === token.category && frontMatterValue(target, section.key, lookup) !== void 0 ? keys.get(target) : void 0;
8159
+ const targetKey = section.key !== "typography" && target?.category === token.category && !isSpacingUnit(target) && frontMatterValue(target, section.key, lookup) !== void 0 ? keys.get(target) : void 0;
8139
8160
  const value = targetKey === void 0 ? frontMatterValue(token, section.key, lookup) : `{${section.key}.${targetKey}}`;
8140
8161
  if (value === void 0) continue;
8141
- if (key in values) {
8162
+ if (values.has(key)) {
8142
8163
  warnings.push(`${token.name} is left out of the front matter: another token has its key`);
8143
8164
  continue;
8144
8165
  }
8145
- values[key] = value;
8166
+ values.set(key, value);
8146
8167
  }
8147
- if (Object.keys(values).length) sections[section.key] = values;
8168
+ for (const [key, value] of steps ?? []) if (!values.has(key)) values.set(key, value);
8169
+ const step = (key) => Number.isFinite(Number(key)) ? Number(key) : Infinity;
8170
+ const ordered = steps ? new Map([...values].sort(([a], [b]) => step(a) - step(b) || 0)) : values;
8171
+ if (ordered.size) sections[section.key] = ordered;
8148
8172
  }
8149
8173
  body.push("", `## ${section.heading}`, "", section.intro, "", ...table(list, keys));
8150
8174
  }
@@ -8164,6 +8188,59 @@ function designKeys(tokens) {
8164
8188
  }
8165
8189
  return out;
8166
8190
  }
8191
+ /** Tailwind's spacing unit, `--spacing`, which `p-4` takes 4 times. */
8192
+ function isSpacingUnit(token) {
8193
+ return token.category === "spacing" && token.tailwind === "";
8194
+ }
8195
+ /** The steps of Tailwind's default spacing scale, `p-0.5` to `p-96`, in spacing units. */
8196
+ const SPACING_STEPS = [
8197
+ .5,
8198
+ 1,
8199
+ 1.5,
8200
+ 2,
8201
+ 2.5,
8202
+ 3,
8203
+ 3.5,
8204
+ 4,
8205
+ 5,
8206
+ 6,
8207
+ 7,
8208
+ 8,
8209
+ 9,
8210
+ 10,
8211
+ 11,
8212
+ 12,
8213
+ 14,
8214
+ 16,
8215
+ 20,
8216
+ 24,
8217
+ 28,
8218
+ 32,
8219
+ 36,
8220
+ 40,
8221
+ 44,
8222
+ 48,
8223
+ 52,
8224
+ 56,
8225
+ 60,
8226
+ 64,
8227
+ 72,
8228
+ 80,
8229
+ 96
8230
+ ];
8231
+ /**
8232
+ * Tailwind's spacing unit as the steps it makes: `"4": 1rem` for `p-4`. A DESIGN.md has no
8233
+ * multiplier: a reader takes each spacing entry as a step, so the unit alone (`base: 0.25rem`)
8234
+ * would make 4px the only one.
8235
+ */
8236
+ function spacingSteps(token, lookup) {
8237
+ const plain = PLAIN_LENGTH.exec(token.value.trim());
8238
+ const [amount, unit] = plain ? [Number(plain[1]), plain[2] ?? "px"] : [evaluateLength(token.value, lookup), "px"];
8239
+ const steps = /* @__PURE__ */ new Map();
8240
+ if (amount === void 0 || !Number.isFinite(amount) || amount <= 0) return steps;
8241
+ for (const n of SPACING_STEPS) steps.set(String(n), `${round(n * amount)}${unit}`);
8242
+ return steps;
8243
+ }
8167
8244
  function frontMatterValue(token, key, lookup) {
8168
8245
  const value = token.value.trim();
8169
8246
  switch (key) {
@@ -8243,6 +8320,7 @@ async function loadTokens(root, sources) {
8243
8320
  const warnings = [];
8244
8321
  const all = [];
8245
8322
  const sheets = [];
8323
+ const sheetFiles = [];
8246
8324
  let sheetsAt;
8247
8325
  for (const source of sources) {
8248
8326
  const rel = relativePath(root, source.file);
@@ -8266,6 +8344,7 @@ async function loadTokens(root, sources) {
8266
8344
  warnings.push(...result.warnings);
8267
8345
  } else {
8268
8346
  sheets.push(readCssSheet(text, rel));
8347
+ sheetFiles.push(rel);
8269
8348
  sheetsAt ??= all.length;
8270
8349
  }
8271
8350
  } catch (error) {
@@ -8273,6 +8352,9 @@ async function loadTokens(root, sources) {
8273
8352
  }
8274
8353
  }
8275
8354
  if (sheets.length) {
8355
+ const partial = sheets.findIndex((s) => s.theme.size > 0 || s.resets.size > 0);
8356
+ const sheet = sheets[partial];
8357
+ if (sheet && !sheets.some((s) => s.tailwind) && await runsTailwindV4(root)) sheet.tailwind = sheetFiles[partial];
8276
8358
  const result = cssSheetTokens(sheets);
8277
8359
  all.splice(sheetsAt ?? all.length, 0, ...result.tokens);
8278
8360
  warnings.push(...result.warnings);
@@ -8282,24 +8364,65 @@ async function loadTokens(root, sources) {
8282
8364
  warnings
8283
8365
  };
8284
8366
  }
8367
+ /** The packages that run Tailwind CSS v4 in a build: PostCSS's, Vite's and its CLI. */
8368
+ const TAILWIND_V4_PACKAGES = [
8369
+ "@tailwindcss/postcss",
8370
+ "@tailwindcss/vite",
8371
+ "@tailwindcss/cli"
8372
+ ];
8373
+ /**
8374
+ * Whether the project at `root` runs Tailwind CSS v4, whose default theme has a spacing unit and
8375
+ * the radius scale up to `rounded-4xl`: its package.json depends on a package that runs v4, or on
8376
+ * `tailwindcss` in a range that is not v3's (`^3.4.1`, `~3.4`, `3.x`), which has neither. Its
8377
+ * plugins (`@tailwindcss/typography`) say nothing of the version.
8378
+ */
8379
+ async function runsTailwindV4(root) {
8380
+ let pkg;
8381
+ try {
8382
+ pkg = JSON.parse(await fsp.readFile(path.join(root, "package.json"), "utf8"));
8383
+ } catch {
8384
+ return false;
8385
+ }
8386
+ if (typeof pkg !== "object" || pkg === null) return false;
8387
+ const lists = [];
8388
+ for (const field of [
8389
+ "dependencies",
8390
+ "devDependencies",
8391
+ "peerDependencies"
8392
+ ]) {
8393
+ const dependencies = pkg[field];
8394
+ if (typeof dependencies === "object" && dependencies !== null) lists.push(dependencies);
8395
+ }
8396
+ if (lists.some((list) => TAILWIND_V4_PACKAGES.some((name) => Object.hasOwn(list, name)))) return true;
8397
+ const declared = lists.find((list) => Object.hasOwn(list, "tailwindcss"));
8398
+ return declared !== void 0 && !beforeV4(declared.tailwindcss);
8399
+ }
8400
+ /** A range of Tailwind CSS v3 or older in package.json: `^3.4.1`, `~3`, `3.x`, `^2 || ^3`. */
8401
+ function beforeV4(range) {
8402
+ return typeof range === "string" && range.split("||").every((alternative) => /^(?:npm:tailwindcss@)?[\^~=v]*\s*[0-3](?:\.|$)/.test(alternative.trim()));
8403
+ }
8285
8404
  /**
8286
8405
  * DTCG files and the CSS generated from them often describe the same tokens.
8287
8406
  * Merge entries that share a custom property: keep the DTCG name and
8288
- * description, and add the Tailwind mapping from CSS.
8407
+ * description, and add the Tailwind mapping from CSS. A token without one (a
8408
+ * `@theme inline` value, `rounded-sm`) merges with the token of its name and
8409
+ * Tailwind key, such as the one a DTCG file exported from the stylesheet has.
8289
8410
  */
8290
8411
  function mergeTokens(tokens) {
8291
8412
  const overridden = new Set(tokens.filter((t) => t.origin !== "tailwind-default").map((t) => themeKey(t)));
8292
- const byVar = /* @__PURE__ */ new Map();
8413
+ const byKey = /* @__PURE__ */ new Map();
8293
8414
  const out = [];
8294
8415
  for (const token of tokens) {
8295
8416
  if (token.origin === "tailwind-default" && overridden.has(themeKey(token))) continue;
8296
- const existing = token.cssVar ? byVar.get(token.cssVar) : void 0;
8417
+ const key = token.cssVar ?? (token.tailwind === void 0 ? void 0 : `${token.name}:${token.tailwind}`);
8418
+ const existing = key === void 0 ? void 0 : byKey.get(key);
8297
8419
  if (!existing) {
8298
- if (token.cssVar) byVar.set(token.cssVar, token);
8420
+ if (key !== void 0) byKey.set(key, token);
8299
8421
  out.push(token);
8300
8422
  continue;
8301
8423
  }
8302
8424
  if (existing.tailwind === void 0 && token.tailwind !== void 0) existing.tailwind = token.tailwind;
8425
+ if (existing.category === "other" && token.category !== "other") existing.category = token.category;
8303
8426
  existing.usage = [.../* @__PURE__ */ new Set([...token.usage, ...existing.usage])];
8304
8427
  existing.description ??= token.description;
8305
8428
  if (token.modes) existing.modes = {
@@ -8976,6 +9099,8 @@ var LintTarget = class {
8976
9099
  sources;
8977
9100
  /** Published libraries the model read from type declarations: `@mui/material`. */
8978
9101
  libraries;
9102
+ /** The name in each package.json read, by its path: undefined where there is none. */
9103
+ manifests = /* @__PURE__ */ new Map();
8979
9104
  /**
8980
9105
  * `resolver` finds the module an import points at and what it exports
8981
9106
  * (`@calcom/ui/components/icon` → `components/icon/index.ts`), so a name from
@@ -9052,14 +9177,49 @@ var LintTarget = class {
9052
9177
  * model left out rather than an invented one: its module exists outside the
9053
9178
  * folders extraction read and exports `name`, or re-exports from such a
9054
9179
  * module (an excluded `icons/` folder, a barrel's `export * from "./icons"`).
9055
- * A module declared in a package's `exports` but not on disk counts too,
9056
- * except the design system's own import path.
9180
+ * A module declared in a package's `exports` but not on disk counts too (built to `dist/`,
9181
+ * not built yet), except the design system's own import path, and one whose export, as the
9182
+ * package.json writes it, leads to a source file only: `@acme/ui/components/tooltip` is
9183
+ * `src/components/tooltip.tsx` through `"./components/*": "./src/components/*.tsx"`, and with
9184
+ * no such file it does not exist.
9057
9185
  */
9058
9186
  outsideModel(specifier, name, fromFile = "snippet.tsx") {
9059
9187
  const location = this.resolver?.locate(specifier, fromFile);
9060
9188
  if (!location) return false;
9061
- if (!location.file) return !this.importPaths.has(specifier) && specifier !== this.config.importPath;
9062
- return this.mayExport(location.file, name, 0);
9189
+ if (location.file) return this.mayExport(location.file, name, 0);
9190
+ if (this.importPaths.has(specifier) || specifier === this.config.importPath) return false;
9191
+ const source = this.exportedSource(specifier);
9192
+ return source === void 0 || this.mayExport(source, name, 0);
9193
+ }
9194
+ /**
9195
+ * The module a design-system package's `exports` lead `specifier` to, relative to the root, as
9196
+ * its package.json writes them: the first of the entry's targets that is on disk, else, when
9197
+ * each of them is a source file, the first (which does not exist). Undefined when a target
9198
+ * leads to a build folder (`"./*": "./dist/components/*.js"`, maybe not built yet, whose
9199
+ * sources may be anywhere), the entry blocks the subpath (`null`), or no entry maps it.
9200
+ */
9201
+ exportedSource(specifier) {
9202
+ const name = packageOf(specifier);
9203
+ const led = this.config.imports?.find((m) => packageOf(m.specifier) === name);
9204
+ const dir = led && this.packageDir(path.resolve(this.model.root, led.target), name);
9205
+ const targets = dir ? exportedTargets(dir, `.${specifier.slice(name.length)}`) : void 0;
9206
+ if (!dir || !targets?.length) return void 0;
9207
+ const files = targets.map((target) => path.resolve(dir, target));
9208
+ const file = files.flatMap((f) => f.endsWith(".jsx") ? [f, `${f.slice(0, -4)}.tsx`] : [f]).find((f) => isInside(dir, f) && isFile(f)) ?? (targets.every(isSourceFile) ? files[0] : void 0);
9209
+ return file && relativePath(this.model.root, file);
9210
+ }
9211
+ /**
9212
+ * The folder of package `name` that `file` is in: the nearest package.json above it with a
9213
+ * name, when that name is `name`. One without a name (`{"type": "module"}` in a subfolder) is
9214
+ * no package's. Each package.json is read once.
9215
+ */
9216
+ packageDir(file, name) {
9217
+ for (let dir = path.dirname(file); dir !== path.dirname(dir); dir = path.dirname(dir)) {
9218
+ const manifest = path.join(dir, "package.json");
9219
+ if (!this.manifests.has(manifest)) this.manifests.set(manifest, manifestName(manifest));
9220
+ const found = this.manifests.get(manifest);
9221
+ if (found !== void 0) return found === name ? dir : void 0;
9222
+ }
9063
9223
  }
9064
9224
  /**
9065
9225
  * The component a JSX tag means when `name` is imported from `specifier`, among the
@@ -9236,6 +9396,30 @@ function fit(component, element) {
9236
9396
  function isPackageName(specifier) {
9237
9397
  return specifier.startsWith("@") ? /^@[^/]+\/[^/]+$/.test(specifier) : /^[a-z0-9][^/]*$/i.test(specifier);
9238
9398
  }
9399
+ /** The name a package.json gives its package; undefined when it has none, or there is none. */
9400
+ function manifestName(manifest) {
9401
+ try {
9402
+ const { name } = JSON.parse(fs.readFileSync(manifest, "utf8")) ?? {};
9403
+ return typeof name === "string" && name ? name : void 0;
9404
+ } catch {
9405
+ return;
9406
+ }
9407
+ }
9408
+ function isFile(file) {
9409
+ return fs.statSync(file, { throwIfNoEntry: false })?.isFile() ?? false;
9410
+ }
9411
+ /** The package an import specifier names: `@acme/ui` for `@acme/ui/components/button`. */
9412
+ function packageOf(specifier) {
9413
+ return specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
9414
+ }
9415
+ /**
9416
+ * An export target that is a TypeScript or JSX source file (`./src/components/tooltip.tsx`), not
9417
+ * a declaration file or a file in a build folder (`./dist/tooltip.tsx`, not built yet).
9418
+ */
9419
+ function isSourceFile(target) {
9420
+ if (/\.d\.[cm]?ts$/.test(target) || /^(?:\.\/)?(?:dist|build|lib|out|esm|cjs)\//.test(target)) return false;
9421
+ return /\.(?:[cm]?tsx?|jsx)$/.test(target);
9422
+ }
9239
9423
  function stripExtension(file) {
9240
9424
  return file.replace(/\.(?:[cm]?[jt]sx?)$/, "");
9241
9425
  }
@@ -12124,7 +12308,7 @@ function isObject(value) {
12124
12308
  // package.json
12125
12309
  var name = "onsystem";
12126
12310
  // src/version.ts
12127
- const VERSION = "0.10.2";
12311
+ const VERSION = "0.11.0";
12128
12312
  const NAME = name;
12129
12313
  // src/lint/sarif.ts
12130
12314
  const SCHEMA = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json";
@@ -31,4 +31,6 @@ export interface Call {
31
31
  signal: Int32Array;
32
32
  port: MessagePort;
33
33
  }
34
+ /** Resolves when the looks under way have ended. Nothing waits on them but tests. */
35
+ export declare function looking(): Promise<unknown>;
34
36
  export declare function load(request: LoadRequest): Promise<LoadResponse>;
@@ -1,5 +1,6 @@
1
- import { At as relativePath, i as componentFiles, ot as excludedBy, r as buildModel } from "./design-system-rl__N6tw.js";
2
- import { r as loadTarget } from "./workspace-INef0Yqt.js";
1
+ import { At as relativePath, a as fingerprint, i as componentFiles, ot as excludedBy, r as buildModel } from "./design-system-Csqts7Eq.js";
2
+ import { r as loadTarget } from "./workspace-Dbxubo9n.js";
3
+ import { createHash } from "node:crypto";
3
4
  import fs from "node:fs";
4
5
  import path from "node:path";
5
6
  import { parentPort } from "node:worker_threads";
@@ -8,13 +9,25 @@ import { parentPort } from "node:worker_threads";
8
9
  * The ESLint plugin's loader, in a worker thread. ESLint rules run synchronously, and finding a
9
10
  * project's design system does not (config files written in code, globs, the extraction
10
11
  * cache), so the plugin asks this worker and waits for the answer (see `bridge.ts`).
12
+ *
13
+ * The worker keeps what it loaded and answers from it at once. Once that is two seconds old, the
14
+ * next lint starts a look at the project again (its config, then its design system's files) in
15
+ * the background, and the lints after the look get what it found. Waiting for the look instead
16
+ * held up every lint after a pause in an editor for as long as the look took, which grows with
17
+ * the design system.
11
18
  */
12
19
  /** What the configs said (a design-system package's skipped, or its paths ignored), each said once. */
13
20
  const warned = /* @__PURE__ */ new Set();
14
- /** How long a loaded design system is trusted before its files are looked at again. */
21
+ /** How long what a look found is used before a lint starts the next look. */
15
22
  const FRESH_MS = 2e3;
23
+ /** By where ESLint runs and its settings. */
16
24
  const targets = /* @__PURE__ */ new Map();
25
+ /**
26
+ * By target and project root: two targets can reach the same project with configs of their own
27
+ * (runs from two folders, two ESLint configs), and neither may be answered with the other's.
28
+ */
17
29
  const projects = /* @__PURE__ */ new Map();
30
+ const looks = /* @__PURE__ */ new Set();
18
31
  function realPath(file) {
19
32
  try {
20
33
  return fs.realpathSync.native(file);
@@ -22,46 +35,122 @@ function realPath(file) {
22
35
  return file;
23
36
  }
24
37
  }
25
- async function targetFor({ cwd, root, config }) {
26
- const key = JSON.stringify([
27
- cwd,
28
- root,
29
- config
30
- ]);
31
- const cached = targets.get(key);
32
- if (cached && Date.now() - cached.at < FRESH_MS) return cached.target;
33
- const target = loadTarget({
38
+ function loadTargetFor({ cwd, root, config }) {
39
+ return loadTarget({
34
40
  cwd,
35
41
  root: root === void 0 ? void 0 : path.resolve(cwd, root),
36
42
  config: config === void 0 ? void 0 : path.resolve(cwd, config)
37
43
  });
38
- targets.set(key, {
44
+ }
45
+ function keep(target) {
46
+ target.catch(() => void 0);
47
+ return {
39
48
  at: Date.now(),
40
49
  target
50
+ };
51
+ }
52
+ /** The config a file is checked with: its own project's, from a monorepo root. */
53
+ async function configFor(target, file) {
54
+ return target.workspace ? (await target.workspace.projectFor(file)).config : target.config;
55
+ }
56
+ function versionOf(key, config) {
57
+ return createHash("sha256").update(key).update(JSON.stringify(config)).digest("hex");
58
+ }
59
+ async function build(config) {
60
+ const { model, key = String(Date.now()), dependencies = [] } = await buildModel(config, { cache: true });
61
+ const own = new Set((await componentFiles(config)).map(realPath));
62
+ return {
63
+ at: Date.now(),
64
+ key,
65
+ dependencies,
66
+ version: versionOf(key, config),
67
+ config,
68
+ model,
69
+ own
70
+ };
71
+ }
72
+ /**
73
+ * `project` for `config` as the files are now: the same model when nothing it is built from has
74
+ * changed (a config edit that only changes rules, or no change at all), else a new build.
75
+ */
76
+ async function rebuild(project, config) {
77
+ if (await fingerprint(config, project.dependencies) !== project.key) return build(config);
78
+ const { key, dependencies, model, own } = project;
79
+ return {
80
+ at: Date.now(),
81
+ key,
82
+ dependencies,
83
+ version: versionOf(key, config),
84
+ config,
85
+ model,
86
+ own
87
+ };
88
+ }
89
+ /**
90
+ * Runs a look in the background, held in `looks` until it ends. It starts on a later turn of the
91
+ * event loop, after the lint that started it has its answer: the start of a look (finding the
92
+ * project, reading its config, listing the design system's files) runs synchronously, and run at
93
+ * once it held up that answer for as long.
94
+ */
95
+ function background(look) {
96
+ const running = new Promise((resolve) => setImmediate(resolve)).then(look).catch(() => void 0).finally(() => looks.delete(running));
97
+ looks.add(running);
98
+ return running;
99
+ }
100
+ function projectKey(key, config) {
101
+ return JSON.stringify([key, config.root]);
102
+ }
103
+ /** A build that fails drops the project, so that the next lint builds it again and says why. */
104
+ function lookAtProject(slot, project, config) {
105
+ project.looking = background(async () => {
106
+ try {
107
+ projects.set(slot, await rebuild(project, config));
108
+ } catch (error) {
109
+ if (projects.get(slot) === project) projects.delete(slot);
110
+ throw error;
111
+ }
112
+ });
113
+ return project.looking;
114
+ }
115
+ function lookAtTarget(key, kept, request) {
116
+ kept.looking = background(async () => {
117
+ const loading = loadTargetFor(request);
118
+ await loading.catch(() => void 0);
119
+ targets.set(key, keep(loading));
120
+ const config = await configFor(await loading, request.file);
121
+ const slot = projectKey(key, config);
122
+ const project = projects.get(slot);
123
+ if (project && !project.looking) await lookAtProject(slot, project, config);
41
124
  });
42
- return target;
125
+ return kept.looking;
126
+ }
127
+ /** Resolves when the looks under way have ended. Nothing waits on them but tests. */
128
+ function looking() {
129
+ return Promise.all(looks);
43
130
  }
44
131
  async function load(request) {
45
- const target = await targetFor(request);
46
- const config = target.workspace ? (await target.workspace.projectFor(request.file)).config : target.config;
132
+ const key = JSON.stringify([
133
+ request.cwd,
134
+ request.root,
135
+ request.config
136
+ ]);
137
+ let kept = targets.get(key);
138
+ if (!kept) {
139
+ kept = keep(loadTargetFor(request));
140
+ targets.set(key, kept);
141
+ } else if (!kept.looking && Date.now() - kept.at >= FRESH_MS) lookAtTarget(key, kept, request);
142
+ const config = await configFor(await kept.target, request.file);
47
143
  for (const text of config.warnings ?? []) {
48
144
  if (warned.has(text)) continue;
49
145
  warned.add(text);
50
146
  process.stderr.write(`onsystem: ${text}\n`);
51
147
  }
52
- let project = projects.get(config.root);
53
- if (!project || Date.now() - project.at >= FRESH_MS) {
54
- const { model, key } = await buildModel(config, { cache: true });
55
- const own = new Set((await componentFiles(config)).map(realPath));
56
- project = {
57
- at: Date.now(),
58
- version: key ?? String(Date.now()),
59
- config,
60
- model,
61
- own
62
- };
63
- projects.set(config.root, project);
64
- }
148
+ const slot = projectKey(key, config);
149
+ let project = projects.get(slot);
150
+ if (!project) {
151
+ project = await build(config);
152
+ projects.set(slot, project);
153
+ } else if (!kept.looking && !project.looking && (project.config !== config || Date.now() - project.at >= FRESH_MS)) lookAtProject(slot, project, config);
65
154
  const relative = relativePath(config.root, request.file);
66
155
  const excluded = excludedBy(config, relative, { includeTests: true });
67
156
  if (excluded) return { skip: `excluded by "${excluded}"` };
@@ -89,4 +178,4 @@ parentPort?.on("message", ({ request, signal, port }) => {
89
178
  reply({ error: error.message });
90
179
  });
91
180
  });
92
- export { load };
181
+ export { load, looking };
package/dist/eslint.js CHANGED
@@ -1,4 +1,4 @@
1
- import { At as relativePath, N as RULES, Q as DEFAULT_TEST_FOLDERS, X as DEFAULT_SEVERITY, Z as DEFAULT_TEST_EXCLUDE, t as DesignSystem, v as NAME, y as VERSION } from "./design-system-rl__N6tw.js";
1
+ import { At as relativePath, N as RULES, Q as DEFAULT_TEST_FOLDERS, X as DEFAULT_SEVERITY, Z as DEFAULT_TEST_EXCLUDE, m as checkSource, rt as RULE_IDS, t as DesignSystem, ut as rulesFor, v as NAME, y as VERSION } from "./design-system-Csqts7Eq.js";
2
2
  import path from "node:path";
3
3
  import { MessageChannel, Worker, receiveMessageOnPort } from "node:worker_threads";
4
4
  // src/eslint/bridge.ts
@@ -51,7 +51,8 @@ function loadSync(request) {
51
51
  * working directory), with the project's config: its `rules` and `overrides` turn rules off
52
52
  * per file, `// onsystem-disable-*` comments suppress findings, and the files `check .` leaves
53
53
  * out (the config's `exclude`, the design system's own components) are not checked. ESLint's
54
- * config sets each rule's severity.
54
+ * config sets each rule's severity, and can set a rule's options (`allow`) over the project's.
55
+ * `onsystem/suppression` reports a misspelled rule in a suppression comment, as check does.
55
56
  */
56
57
  const RULES_PAGE = "https://onsystem.vercel.app/rules";
57
58
  /** Design systems by project root, at the version the loader last reported. */
@@ -101,7 +102,25 @@ function run(cwd, file, text, settings) {
101
102
  if (result.skipped) return { skipped: true };
102
103
  return {
103
104
  result,
104
- fromSystem: (specifier) => ds.lint.isDesignSystemImport(specifier, relative)
105
+ fromSystem: (specifier) => ds.lint.isDesignSystemImport(specifier, relative),
106
+ checkWith(id, options) {
107
+ const own = rulesFor(ds.config, relative)[id];
108
+ const off = {
109
+ severity: "off",
110
+ options: {}
111
+ };
112
+ const rules = Object.fromEntries(RULE_IDS.map((rule) => [rule, rule === id ? {
113
+ ...own,
114
+ options: {
115
+ ...own.options,
116
+ ...options
117
+ }
118
+ } : off]));
119
+ return checkSource(text, ds.lint, {
120
+ filename: relative,
121
+ rules
122
+ });
123
+ }
105
124
  };
106
125
  }
107
126
  /**
@@ -153,27 +172,45 @@ function typeOnly(def) {
153
172
  const kind = (node) => node?.importKind;
154
173
  return def.type === "ImportBinding" && (kind(def.node) === "type" || kind(def.parent) === "type");
155
174
  }
175
+ /** A rule's options in ESLint's config, as in onsystem.config.json: `['error', { allow: ['#fff'] }]`. */
176
+ const OPTIONS_SCHEMA = [{
177
+ type: "object",
178
+ properties: { allow: {
179
+ type: "array",
180
+ items: { type: "string" }
181
+ } },
182
+ additionalProperties: false
183
+ }];
184
+ /**
185
+ * Check's misspelled rule names in `// onsystem-disable-*` comments, which suppress nothing. A rule
186
+ * of its own, so that it is a warning, as check reports it, and can be turned off like the others.
187
+ */
188
+ const SUPPRESSION = "suppression";
156
189
  function createRule(id, description) {
190
+ const suppression = id === SUPPRESSION;
157
191
  return {
158
192
  meta: {
159
193
  type: "problem",
160
194
  docs: {
161
195
  description,
162
- url: `${RULES_PAGE}#${id}`
196
+ url: `${RULES_PAGE}#${suppression ? "suppressing-findings" : id}`
197
+ },
198
+ ...suppression ? {} : {
199
+ fixable: "code",
200
+ hasSuggestions: true
163
201
  },
164
- fixable: "code",
165
- hasSuggestions: true,
166
- schema: []
202
+ schema: suppression ? [] : OPTIONS_SCHEMA
167
203
  },
168
204
  create(context) {
169
205
  const file = path.resolve(context.cwd, context.filename);
170
206
  const settings = context.settings.onsystem ?? {};
207
+ const options = context.options?.[0];
171
208
  return { "Program:exit"(node) {
172
209
  const pass = passFor(context, file, settings);
173
210
  const { outcome } = pass;
174
211
  if ("skipped" in outcome) return;
175
212
  if ("error" in outcome) {
176
- if (pass.reported) return;
213
+ if (suppression || pass.reported) return;
177
214
  pass.reported = true;
178
215
  context.report({
179
216
  node,
@@ -181,7 +218,8 @@ function createRule(id, description) {
181
218
  });
182
219
  return;
183
220
  }
184
- for (const d of outcome.result.diagnostics) {
221
+ const { diagnostics } = !suppression && options && Object.keys(options).length ? outcome.checkWith(id, options) : outcome.result;
222
+ for (const d of diagnostics) {
185
223
  if (d.ruleId !== id) continue;
186
224
  const edits = d.fix ?? [];
187
225
  const fix = (fixer) => edits.map((e) => fixer.replaceTextRange(e.range, e.text));
@@ -209,6 +247,7 @@ function createRule(id, description) {
209
247
  };
210
248
  }
211
249
  const rules = Object.fromEntries(RULES.map((rule) => [rule.id, createRule(rule.id, rule.description)]));
250
+ rules[SUPPRESSION] = createRule(SUPPRESSION, "Suppression comments must name rules that exist.");
212
251
  const plugin = {
213
252
  meta: {
214
253
  name: NAME,
@@ -218,14 +257,17 @@ const plugin = {
218
257
  configs: {}
219
258
  };
220
259
  /**
221
- * Every rule at its default severity, for `.tsx` and `.jsx` files other than tests and stories,
222
- * which `check` also leaves out unless asked (`--include-tests`).
260
+ * Every rule at its default severity, and `suppression` as a warning, for `.tsx` and `.jsx` files
261
+ * other than tests and stories, which `check` also leaves out unless asked (`--include-tests`).
223
262
  */
224
263
  plugin.configs.recommended = {
225
264
  name: "onsystem/recommended",
226
265
  files: ["**/*.{tsx,jsx}"],
227
266
  ignores: [DEFAULT_TEST_EXCLUDE, DEFAULT_TEST_FOLDERS],
228
267
  plugins: { onsystem: plugin },
229
- rules: Object.fromEntries(RULES.map((rule) => [`onsystem/${rule.id}`, DEFAULT_SEVERITY[rule.id]]))
268
+ rules: {
269
+ ...Object.fromEntries(RULES.map((rule) => [`onsystem/${rule.id}`, DEFAULT_SEVERITY[rule.id]])),
270
+ [`onsystem/${SUPPRESSION}`]: "warn"
271
+ }
230
272
  };
231
273
  export { plugin as default };
package/dist/index.d.ts CHANGED
@@ -157,7 +157,9 @@ export declare function loadTokens(root: string, sources: TokenSource[]): Promis
157
157
  /**
158
158
  * DTCG files and the CSS generated from them often describe the same tokens.
159
159
  * Merge entries that share a custom property: keep the DTCG name and
160
- * description, and add the Tailwind mapping from CSS.
160
+ * description, and add the Tailwind mapping from CSS. A token without one (a
161
+ * `@theme inline` value, `rounded-sm`) merges with the token of its name and
162
+ * Tailwind key, such as the one a DTCG file exported from the stylesheet has.
161
163
  */
162
164
  export declare function mergeTokens(tokens: Token[]): Token[];
163
165
  interface ColorCandidate {
@@ -269,7 +271,7 @@ export declare class TokenIndex {
269
271
  /** What the lint rules need to know about the design system. */
270
272
  declare class LintTarget {
271
273
  readonly model: DesignSystemModel;
272
- readonly config: Pick<ResolvedConfig, 'elements' | 'importPath'> & Partial<Pick<ResolvedConfig, 'designSystems'>>;
274
+ readonly config: Pick<ResolvedConfig, 'elements' | 'importPath'> & Partial<Pick<ResolvedConfig, 'designSystems' | 'imports'>>;
273
275
  private readonly resolver?;
274
276
  readonly components: Map<string, ComponentInfo>;
275
277
  /** Every component under each name and alias, when two design systems share one (`Button`). */
@@ -291,13 +293,15 @@ declare class LintTarget {
291
293
  private readonly sources?;
292
294
  /** Published libraries the model read from type declarations: `@mui/material`. */
293
295
  readonly libraries: Set<string>;
296
+ /** The name in each package.json read, by its path: undefined where there is none. */
297
+ private readonly manifests;
294
298
  /**
295
299
  * `resolver` finds the module an import points at and what it exports
296
300
  * (`@calcom/ui/components/icon` → `components/icon/index.ts`), so a name from
297
301
  * a module the model left out is not taken for an invented one. Without it,
298
302
  * every missing name is invented.
299
303
  */
300
- constructor(model: DesignSystemModel, config: Pick<ResolvedConfig, 'elements' | 'importPath'> & Partial<Pick<ResolvedConfig, 'designSystems'>>, resolver?: Pick<ModuleResolver, "locate" | "exportsOf"> | undefined);
304
+ constructor(model: DesignSystemModel, config: Pick<ResolvedConfig, 'elements' | 'importPath'> & Partial<Pick<ResolvedConfig, 'designSystems' | 'imports'>>, resolver?: Pick<ModuleResolver, "locate" | "exportsOf"> | undefined);
301
305
  /**
302
306
  * Whether an import specifier points at the design system. Relative
303
307
  * specifiers are resolved against `fromFile` (relative to the root) and
@@ -318,10 +322,27 @@ declare class LintTarget {
318
322
  * model left out rather than an invented one: its module exists outside the
319
323
  * folders extraction read and exports `name`, or re-exports from such a
320
324
  * module (an excluded `icons/` folder, a barrel's `export * from "./icons"`).
321
- * A module declared in a package's `exports` but not on disk counts too,
322
- * except the design system's own import path.
325
+ * A module declared in a package's `exports` but not on disk counts too (built to `dist/`,
326
+ * not built yet), except the design system's own import path, and one whose export, as the
327
+ * package.json writes it, leads to a source file only: `@acme/ui/components/tooltip` is
328
+ * `src/components/tooltip.tsx` through `"./components/*": "./src/components/*.tsx"`, and with
329
+ * no such file it does not exist.
323
330
  */
324
331
  outsideModel(specifier: string, name: string, fromFile?: string): boolean;
332
+ /**
333
+ * The module a design-system package's `exports` lead `specifier` to, relative to the root, as
334
+ * its package.json writes them: the first of the entry's targets that is on disk, else, when
335
+ * each of them is a source file, the first (which does not exist). Undefined when a target
336
+ * leads to a build folder (`"./*": "./dist/components/*.js"`, maybe not built yet, whose
337
+ * sources may be anywhere), the entry blocks the subpath (`null`), or no entry maps it.
338
+ */
339
+ private exportedSource;
340
+ /**
341
+ * The folder of package `name` that `file` is in: the nearest package.json above it with a
342
+ * name, when that name is `name`. One without a name (`{"type": "module"}` in a subfolder) is
343
+ * no package's. Each package.json is read once.
344
+ */
345
+ private packageDir;
325
346
  /**
326
347
  * The component a JSX tag means when `name` is imported from `specifier`, among the
327
348
  * model's components of that name: the one declared where the import leads, through
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { F as loadTokens, G as parseDoc, H as parseDesignMd, I as mergeTokens, K as extractComponents, L as toDesignMd, N as RULES, P as TokenIndex, R as toDtcg, U as parseCssTokens, V as parseDtcgTokens, W as attachDocs, _ as formatDiagnostics, a as fingerprint, at as defineConfig, ct as loadConfig, f as searchComponents, ft as findVariantDefinitions, it as configJsonSchema, m as checkSource, n as DesignSystemHost, o as loadDesignSystem, p as applyFixes, r as buildModel, rt as RULE_IDS, t as DesignSystem, y as VERSION } from "./design-system-rl__N6tw.js";
2
- import { n as INSTRUCTIONS, r as createServer, t as serveStdio } from "./stdio-CkczPt_1.js";
1
+ import { F as loadTokens, G as parseDoc, H as parseDesignMd, I as mergeTokens, K as extractComponents, L as toDesignMd, N as RULES, P as TokenIndex, R as toDtcg, U as parseCssTokens, V as parseDtcgTokens, W as attachDocs, _ as formatDiagnostics, a as fingerprint, at as defineConfig, ct as loadConfig, f as searchComponents, ft as findVariantDefinitions, it as configJsonSchema, m as checkSource, n as DesignSystemHost, o as loadDesignSystem, p as applyFixes, r as buildModel, rt as RULE_IDS, t as DesignSystem, y as VERSION } from "./design-system-Csqts7Eq.js";
2
+ import { n as INSTRUCTIONS, r as createServer, t as serveStdio } from "./stdio-0TpyqZaD.js";
3
3
  export { DesignSystem, DesignSystemHost, INSTRUCTIONS, RULES, RULE_IDS, TokenIndex, VERSION, applyFixes, attachDocs, buildModel, checkSource, configJsonSchema, createServer, defineConfig, extractComponents, findVariantDefinitions, fingerprint, formatDiagnostics, loadConfig, loadDesignSystem, loadTokens, mergeTokens, parseCssTokens, parseDesignMd, parseDoc, parseDtcgTokens, searchComponents, serveStdio, toDesignMd, toDtcg };
@@ -1,5 +1,5 @@
1
- import { At as relativePath, Ct as isWorkspaceRoot, Dt as isInside, Et as SCRIPT_FILE, Ft as truncate, It as unique, J as ConfigError, Lt as defaultProjectConfig, Mt as closest, Nt as json, Pt as plural, X as DEFAULT_SEVERITY, bt as detectProject, ct as loadConfig, d as descriptionOf, dt as formatPx, f as searchComponents, gt as notReadYet, h as uncheckedNotice, ht as libraryTitle, kt as pathOf, mt as LIBRARY_IDS, n as DesignSystemHost, ot as excludedBy, pt as LIBRARIES, q as CONFIG_FILES, r as buildModel, rt as RULE_IDS, tt as LEGACY_CONFIG_FILES, u as buildSearchIndex, v as NAME, xt as findDesignSystemPackage, y as VERSION, z as TOKEN_CATEGORIES } from "./design-system-rl__N6tw.js";
2
- import { i as projectDirOf, n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-INef0Yqt.js";
1
+ import { At as relativePath, Ct as isWorkspaceRoot, Dt as isInside, Et as SCRIPT_FILE, Ft as truncate, It as unique, J as ConfigError, Lt as defaultProjectConfig, Mt as closest, Nt as json, Pt as plural, X as DEFAULT_SEVERITY, bt as detectProject, ct as loadConfig, d as descriptionOf, dt as formatPx, f as searchComponents, gt as notReadYet, h as uncheckedNotice, ht as libraryTitle, kt as pathOf, mt as LIBRARY_IDS, n as DesignSystemHost, ot as excludedBy, pt as LIBRARIES, q as CONFIG_FILES, r as buildModel, rt as RULE_IDS, tt as LEGACY_CONFIG_FILES, u as buildSearchIndex, v as NAME, xt as findDesignSystemPackage, y as VERSION, z as TOKEN_CATEGORIES } from "./design-system-Csqts7Eq.js";
2
+ import { i as projectDirOf, n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-Dbxubo9n.js";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -19,8 +19,11 @@ function explainProject(ds, cwd, indent = "") {
19
19
  const out = (line = "") => lines.push(line ? `${indent}${line}` : "");
20
20
  out("How the design system was found");
21
21
  for (const step of packageSteps(config, cwd)) out(` ${(step.accepted ? "found" : "no").padEnd(6)} ${step.candidate}: ${step.reason}`);
22
- if (config.configured) out(` components come from ${config.inherits && config.designSystemPackage ? display(cwd, config.designSystemPackage.config) : config.configFile ? `${display(cwd, config.configFile)}${over(config, cwd)}` : "--components"}, so zero config did not look`);
23
- else {
22
+ if (config.configured) {
23
+ out(` components come from ${config.inherits && config.designSystemPackage ? display(cwd, config.designSystemPackage.config) : config.configFile ? `${display(cwd, config.configFile)}${over(config, cwd)}` : "--components"}, so zero config did not look`);
24
+ const unpublished = unpublishedComponents(ds);
25
+ if (unpublished) out(` ${unpublished}`);
26
+ } else {
24
27
  const steps = [];
25
28
  detectProject(config.root, config.tsconfig, steps, config.designSystemPackage);
26
29
  for (const step of steps) out(` ${(step.accepted ? "found" : "no").padEnd(6)} ${step.candidate}: ${step.reason}`);
@@ -85,6 +88,22 @@ function explainProject(ds, cwd, indent = "") {
85
88
  }
86
89
  return lines;
87
90
  }
91
+ /**
92
+ * Why a project that takes on the config of a design-system package installed in node_modules
93
+ * knows none of its components: the package publishes neither their sources nor their type
94
+ * declarations, or `"libraries": false` turns off the declarations it publishes. The one message
95
+ * for it, where a project without components would otherwise be told to check the "components"
96
+ * globs. Undefined otherwise.
97
+ */
98
+ function unpublishedComponents(ds) {
99
+ const declared = ds.config.designSystemPackage;
100
+ if (!declared?.installed || !ds.config.inherits || ds.model.components.length) return void 0;
101
+ const tokens = ds.model.tokens.length ? " The token rules still check against the tokens it publishes." : "";
102
+ const name = pathOf(declared.name);
103
+ const see = "See https://onsystem.vercel.app/docs/design-system-packages#what-apps-get";
104
+ if (ds.config.librariesOff?.some((library) => library.package === declared.name)) return `${name} publishes no component sources, and "libraries": false in the config turns off the type declarations it publishes, so its components are unknown here and the component rules did not run: set "libraries" to [], which reads its declarations and no library, or remove it.${tokens} ${see}`;
105
+ return `${name} publishes neither its component sources nor their type declarations, so its components are unknown here and the component rules did not run: ask its team to add its sources to "files" in its package.json, or to publish type declarations.${tokens} ${see}`;
106
+ }
88
107
  /** Why a library the project depends on is not read: `left out by "libraries" in onsystem.config.json`. */
89
108
  function librariesLeftOut(config, cwd) {
90
109
  return `left out by "libraries" in ${config.configFile ? display(cwd, config.configFile) : "the config"}`;
@@ -562,8 +581,12 @@ function listTail(listed) {
562
581
  if (listed.omitted) lines.push("", `${listed.omitted.toLocaleString("en-US")} more not shown: pass a higher \`limit\`, or a \`query\`.`);
563
582
  return lines;
564
583
  }
565
- /** The shared explanation when a project has no design-system components to show. */
566
- function noComponentsMessage(root, project, monorepo = false) {
584
+ /**
585
+ * The shared explanation when a project has no design-system components to show. `reason`, when
586
+ * known, says why instead ({@link unpublishedComponents}).
587
+ */
588
+ function noComponentsMessage(root, project, monorepo = false, reason) {
589
+ if (reason) return answer([reason]);
567
590
  if (monorepo) return answer([`No design-system components found in any project of this monorepo (root: ${root}). Check the "components" globs in the config. Run \`onsystem inspect\` there to see what was looked at.`]);
568
591
  return answer([`No design-system components found${project ? ` in ${pathOf(project)}` : ""}. Check the "components" globs in the config (root: ${root}).`]);
569
592
  }
@@ -571,7 +594,7 @@ function noComponentsMessage(root, project, monorepo = false) {
571
594
  function renderComponentList(ds, project, filter = {}) {
572
595
  const roots = ds.roots();
573
596
  const where = project ? ` in ${pathOf(project)}` : "";
574
- if (!roots.length) return noComponentsMessage(ds.root, project);
597
+ if (!roots.length) return noComponentsMessage(ds.root, project, false, unpublishedComponents(ds));
575
598
  const listed = listComponents(roots, (c) => c, filter);
576
599
  if (!listed.matched) return answer([`No components match ${filterText(filter)}${where} (${roots.length} in all). Call list_components without filters to see them.`]);
577
600
  const marks = new Marks();
@@ -686,7 +709,7 @@ function renderAmbiguous(name, entries) {
686
709
  }
687
710
  /** get_component's answer for a name no component has, with the nearest and all of them. */
688
711
  function renderNotFound(name, guess, names, where = "") {
689
- return answer([`No component named ${json(name)}${where}.` + (guess ? ` Did you mean ${json(guess)}?` : "") + ` Available: ${names.join(", ")}.`]);
712
+ return answer([`No component named ${json(name)}${where}.` + (guess ? ` Did you mean ${json(guess)}?` : "") + (names.length ? ` Available: ${names.join(", ")}.` : "")]);
690
713
  }
691
714
  /**
692
715
  * The build-with-design-system prompt: `task`, the steps, and what the design system has, its
@@ -1552,7 +1575,7 @@ function register(serve, { getDesignSystem, getProjects }) {
1552
1575
  const { ds } = found;
1553
1576
  const component = ds.getComponent(name);
1554
1577
  if (!component) {
1555
- if (!ds.roots().length) return refuse(noComponentsMessage(ds.root, found.project?.dir));
1578
+ if (!ds.roots().length) return refuse(noComponentsMessage(ds.root, found.project?.dir, false, unpublishedComponents(ds)));
1556
1579
  const names = ds.roots().map((c) => c.name);
1557
1580
  return refuse(renderNotFound(name, ds.suggestComponent(name), names));
1558
1581
  }
@@ -1,4 +1,4 @@
1
- import { At as relativePath, Ct as isWorkspaceRoot, Dt as isInside, Tt as workspacePatterns, ct as loadConfig, i as componentFiles, lt as loadConfigWith, nt as PROJECT_MARKERS, st as hasOwnConfig, vt as PackageFinder, wt as workspacePackageDirs, xt as findDesignSystemPackage } from "./design-system-rl__N6tw.js";
1
+ import { At as relativePath, Ct as isWorkspaceRoot, Dt as isInside, Tt as workspacePatterns, ct as loadConfig, i as componentFiles, lt as loadConfigWith, nt as PROJECT_MARKERS, st as hasOwnConfig, vt as PackageFinder, wt as workspacePackageDirs, xt as findDesignSystemPackage } from "./design-system-Csqts7Eq.js";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onsystem",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "mcpName": "io.github.dgesteves/onsystem",
5
5
  "description": "Keeps coding agents on your design system: catches palette colors, native elements and APIs your system lacks, at every edit and pull request, with the fix. Set it up once in your design-system package; every app in your monorepo gets it.",
6
6
  "keywords": [