onsystem 0.10.3 → 0.11.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.
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-7wZoJsMm.js";
3
- import { a as notReadNotices, i as missingDependencies, o as explainProject, s as librariesLeftOut, t as serveStdio } from "./stdio-B31hp2UZ.js";
4
- import { r as loadTarget } from "./workspace-C5pMS6Xv.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-D7ppJYbd.js";
3
+ import { a as notReadNotices, i as missingDependencies, o as explainProject, s as librariesLeftOut, t as serveStdio } from "./stdio-c3Y34to3.js";
4
+ import { r as loadTarget } from "./workspace-DUgzMLGs.js";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import { escapePath, glob } from "tinyglobby";
@@ -678,7 +678,8 @@ const APP_STYLESHEETS = [
678
678
  * from a registry is read through its `exports` alone, when they point at the
679
679
  * component sources it publishes, not from `dist/` back to `src/`.
680
680
  * 4. A flat folder of components that wrap a primitives library, the way React
681
- * Aria's Tailwind starter ships them (`src/Button.tsx`, `src/Checkbox.tsx`).
681
+ * Aria's Tailwind starter ships them (`src/Button.tsx`, `src/Checkbox.tsx`), or the
682
+ * same files copied into an app's `src/components/`, `components/` or `app/components/`.
682
683
  *
683
684
  * With more than one design system, the one the root's code imports most comes first
684
685
  * (`designSystems`). A candidate whose globs match no file is skipped. Returns undefined
@@ -818,7 +819,7 @@ function detectProject(root, tsconfig, trace, declared, packages = new PackageFi
818
819
  };
819
820
  }
820
821
  let flatWhy = "no package.json";
821
- const flat = own ? fromFlatFolder(root, own, (r) => {
822
+ const flat = own ? fromFlatFolder(root, own, "src", (r) => {
822
823
  flatWhy = r;
823
824
  }) : void 0;
824
825
  if (flat && matchesAny(root, flat)) {
@@ -826,6 +827,19 @@ function detectProject(root, tsconfig, trace, declared, packages = new PackageFi
826
827
  return detection(root, [flat], false, flat.source);
827
828
  }
828
829
  step("a flat src/ of components", false, flatWhy);
830
+ if (!own || !primitivesDependencies(own).length) return void 0;
831
+ for (const folder of COMPONENT_FOLDERS) {
832
+ if (!fs.existsSync(path.join(root, folder))) continue;
833
+ let why = "";
834
+ const inFolder = fromFlatFolder(root, own, folder, (r) => {
835
+ why = r;
836
+ });
837
+ if (inFolder && matchesAny(root, inFolder)) {
838
+ step(`a flat ${folder}/ of components`, true, inFolder.source);
839
+ return detection(root, [inFolder], true, inFolder.source);
840
+ }
841
+ step(`a flat ${folder}/ of components`, false, why || "its components match no file");
842
+ }
829
843
  }
830
844
  /** `12 component files`, for the trace. */
831
845
  function countFiles(root, found) {
@@ -850,8 +864,11 @@ function componentGlob(root, patterns) {
850
864
  function plural(n, one, many = `${one}s`) {
851
865
  return `${n.toLocaleString("en-US")} ${n === 1 ? one : many}`;
852
866
  }
853
- /** Libraries of unstyled primitives that design systems wrap. */
854
- const PRIMITIVES = /^(?:react-aria-components|react-aria|radix-ui|@radix-ui\/react-[\w-]+|@base-ui-components\/react|@base-ui\/react|@headlessui\/react|@ark-ui\/react)(?:\/|$)/;
867
+ /**
868
+ * Libraries of unstyled primitives that design systems wrap. Not Radix's icons
869
+ * (`@radix-ui/react-icons`), which an app's own components import as much as a design system's.
870
+ */
871
+ const PRIMITIVES = /^(?:react-aria-components|react-aria|radix-ui|@radix-ui\/react-(?!icons(?:\/|$))[\w-]+|@base-ui-components\/react|@base-ui\/react|@headlessui\/react|@ark-ui\/react)(?:\/|$)/;
855
872
  /** Files that make `src/` an app rather than a library: entries and route folders. */
856
873
  const APP_ENTRIES = /^(?:main|index|App|app|root|entry-client|entry-server)\.[jt]sx?$/;
857
874
  const APP_FOLDERS = /* @__PURE__ */ new Set([
@@ -859,41 +876,62 @@ const APP_FOLDERS = /* @__PURE__ */ new Set([
859
876
  "pages",
860
877
  "routes"
861
878
  ]);
862
- /** Fewer PascalCase files than this in `src/` are an app's, not a component library. */
879
+ /** Fewer PascalCase files than this in the folder are an app's, not a component library. */
863
880
  const MIN_FLAT_COMPONENTS = 5;
864
881
  /**
865
- * A flat `src/` of components, as React Aria's Tailwind starter has it. It needs all of:
866
- * a dependency on a primitives library, no app entry (`main.tsx`, `App.tsx`) or route
867
- * folder in `src/`, and at least five PascalCase `.tsx`/`.jsx` files at its top level, four
868
- * in five of which import the primitives library. An app whose `src/` holds pages or
869
- * features imports its own components, not primitives, so it does not qualify.
882
+ * An app's folders of components, read like a flat `src/` when they are one: React Aria's
883
+ * starter copied into an app (`src/components/Button.tsx` next to `src/App.tsx`), or into a
884
+ * Next.js app's `components/` or `app/components/`.
870
885
  */
871
- function fromFlatFolder(root, pkg, note = ignore) {
872
- const dependencies = Object.keys({
886
+ const COMPONENT_FOLDERS = [
887
+ "src/components",
888
+ "components",
889
+ "app/components"
890
+ ];
891
+ /** The primitives libraries among a package's dependencies. */
892
+ function primitivesDependencies(pkg) {
893
+ return Object.keys({
873
894
  ...asRecord(pkg.dependencies),
874
895
  ...asRecord(pkg.devDependencies),
875
896
  ...asRecord(pkg.peerDependencies)
876
897
  }).filter((name) => PRIMITIVES.test(name));
898
+ }
899
+ /** `react-aria-components`, or `a primitives library (@radix-ui/react-dialog, @radix-ui/react-slot)`. */
900
+ function primitivesNamed(dependencies) {
901
+ if (dependencies.length <= 1) return dependencies[0] ?? "a primitives library";
902
+ const shown = dependencies.slice(0, 3).join(", ");
903
+ const more = dependencies.length - 3;
904
+ return `a primitives library (${more > 0 ? `${shown} and ${String(more)} more` : shown})`;
905
+ }
906
+ /**
907
+ * A flat folder of components, as React Aria's Tailwind starter has `src/`. It needs all of:
908
+ * a dependency on a primitives library, at least five PascalCase `.tsx`/`.jsx` files at the
909
+ * folder's top level, four in five of which import the primitives library, and, for `src/`,
910
+ * no app entry (`main.tsx`, `App.tsx`) or route folder in it. An app whose folder holds pages
911
+ * or features imports its own components, not primitives, so it does not qualify.
912
+ */
913
+ function fromFlatFolder(root, pkg, folder, note = ignore) {
914
+ const dependencies = primitivesDependencies(pkg);
877
915
  if (!dependencies.length) {
878
916
  note("no dependency on a primitives library (React Aria, Radix, Base UI, Headless UI, Ark UI)");
879
917
  return;
880
918
  }
881
- const dir = path.join(root, "src");
919
+ const dir = path.join(root, folder);
882
920
  let entries;
883
921
  try {
884
922
  entries = fs.readdirSync(dir, { withFileTypes: true });
885
923
  } catch {
886
- note("no src/ folder");
924
+ note(`no ${folder}/ folder`);
887
925
  return;
888
926
  }
889
- const app = entries.find((e) => e.isFile() && APP_ENTRIES.test(e.name) || e.isDirectory() && APP_FOLDERS.has(e.name));
927
+ const app = folder === "src" ? entries.find((e) => e.isFile() && APP_ENTRIES.test(e.name) || e.isDirectory() && APP_FOLDERS.has(e.name)) : void 0;
890
928
  if (app) {
891
929
  note(`src/${app.name} makes src/ an app`);
892
930
  return;
893
931
  }
894
932
  const files = entries.filter((e) => e.isFile() && /^[A-Z][A-Za-z0-9]*\.[jt]sx$/.test(e.name)).map((e) => e.name);
895
933
  if (files.length < MIN_FLAT_COMPONENTS) {
896
- note(`${plural(files.length, "PascalCase .tsx file")} in src/, fewer than ${MIN_FLAT_COMPONENTS}`);
934
+ note(`${plural(files.length, "PascalCase .tsx file")} in ${folder}/, fewer than ${MIN_FLAT_COMPONENTS}`);
897
935
  return;
898
936
  }
899
937
  const libraries = /* @__PURE__ */ new Map();
@@ -914,17 +952,17 @@ function fromFlatFolder(root, pkg, note = ignore) {
914
952
  for (const library of used) libraries.set(library, (libraries.get(library) ?? 0) + 1);
915
953
  }
916
954
  if (wrapping * 5 < files.length * 4) {
917
- note(`${wrapping} of ${files.length} files in src/ import ${dependencies[0] ?? "a primitives library"}, fewer than four in five`);
955
+ note(`${wrapping} of ${files.length} files in ${folder}/ import ${primitivesNamed(dependencies)}, fewer than four in five`);
918
956
  return;
919
957
  }
920
958
  const library = [...libraries].sort((a, b) => b[1] - a[1])[0]?.[0] ?? dependencies[0] ?? "";
921
- const stylesheet = STYLESHEETS.map((f) => path.join(root, f)).find((f) => fs.existsSync(f));
959
+ const stylesheet = folder === "src" ? STYLESHEETS.map((f) => path.join(root, f)).find((f) => fs.existsSync(f)) : void 0;
922
960
  return {
923
- components: ["src/*.{tsx,jsx}"],
961
+ components: [`${folder}/*.{tsx,jsx}`],
924
962
  tokens: stylesheet ? [escapePath(relativePath(root, stylesheet))] : [],
925
963
  imports: [],
926
964
  dirs: [dir],
927
- source: `src/ (${String(files.length)} components wrapping ${library})`
965
+ source: `${folder}/ (${String(files.length)} components wrapping ${library})`
928
966
  };
929
967
  }
930
968
  function detection(root, found, withDefaults, source) {
@@ -8146,21 +8184,29 @@ function toDesignMd(tokens, options) {
8146
8184
  const list = tokens.filter((t) => t.category === section.category);
8147
8185
  if (!list.length) continue;
8148
8186
  if (section.key) {
8149
- const values = {};
8187
+ const values = /* @__PURE__ */ new Map();
8188
+ let steps;
8150
8189
  for (const token of list) {
8190
+ if (isSpacingUnit(token)) {
8191
+ steps = spacingSteps(token, lookup);
8192
+ continue;
8193
+ }
8151
8194
  const key = keys.get(token);
8152
8195
  if (key === void 0) continue;
8153
8196
  const target = token.aliasOf ? byName.get(token.aliasOf) : void 0;
8154
- const targetKey = target?.category === token.category && frontMatterValue(target, section.key, lookup) !== void 0 ? keys.get(target) : void 0;
8197
+ const targetKey = section.key !== "typography" && target?.category === token.category && !isSpacingUnit(target) && frontMatterValue(target, section.key, lookup) !== void 0 ? keys.get(target) : void 0;
8155
8198
  const value = targetKey === void 0 ? frontMatterValue(token, section.key, lookup) : `{${section.key}.${targetKey}}`;
8156
8199
  if (value === void 0) continue;
8157
- if (key in values) {
8200
+ if (values.has(key)) {
8158
8201
  warnings.push(`${token.name} is left out of the front matter: another token has its key`);
8159
8202
  continue;
8160
8203
  }
8161
- values[key] = value;
8204
+ values.set(key, value);
8162
8205
  }
8163
- if (Object.keys(values).length) sections[section.key] = values;
8206
+ for (const [key, value] of steps ?? []) if (!values.has(key)) values.set(key, value);
8207
+ const step = (key) => Number.isFinite(Number(key)) ? Number(key) : Infinity;
8208
+ const ordered = steps ? new Map([...values].sort(([a], [b]) => step(a) - step(b) || 0)) : values;
8209
+ if (ordered.size) sections[section.key] = ordered;
8164
8210
  }
8165
8211
  body.push("", `## ${section.heading}`, "", section.intro, "", ...table(list, keys));
8166
8212
  }
@@ -8180,6 +8226,59 @@ function designKeys(tokens) {
8180
8226
  }
8181
8227
  return out;
8182
8228
  }
8229
+ /** Tailwind's spacing unit, `--spacing`, which `p-4` takes 4 times. */
8230
+ function isSpacingUnit(token) {
8231
+ return token.category === "spacing" && token.tailwind === "";
8232
+ }
8233
+ /** The steps of Tailwind's default spacing scale, `p-0.5` to `p-96`, in spacing units. */
8234
+ const SPACING_STEPS = [
8235
+ .5,
8236
+ 1,
8237
+ 1.5,
8238
+ 2,
8239
+ 2.5,
8240
+ 3,
8241
+ 3.5,
8242
+ 4,
8243
+ 5,
8244
+ 6,
8245
+ 7,
8246
+ 8,
8247
+ 9,
8248
+ 10,
8249
+ 11,
8250
+ 12,
8251
+ 14,
8252
+ 16,
8253
+ 20,
8254
+ 24,
8255
+ 28,
8256
+ 32,
8257
+ 36,
8258
+ 40,
8259
+ 44,
8260
+ 48,
8261
+ 52,
8262
+ 56,
8263
+ 60,
8264
+ 64,
8265
+ 72,
8266
+ 80,
8267
+ 96
8268
+ ];
8269
+ /**
8270
+ * Tailwind's spacing unit as the steps it makes: `"4": 1rem` for `p-4`. A DESIGN.md has no
8271
+ * multiplier: a reader takes each spacing entry as a step, so the unit alone (`base: 0.25rem`)
8272
+ * would make 4px the only one.
8273
+ */
8274
+ function spacingSteps(token, lookup) {
8275
+ const plain = PLAIN_LENGTH.exec(token.value.trim());
8276
+ const [amount, unit] = plain ? [Number(plain[1]), plain[2] ?? "px"] : [evaluateLength(token.value, lookup), "px"];
8277
+ const steps = /* @__PURE__ */ new Map();
8278
+ if (amount === void 0 || !Number.isFinite(amount) || amount <= 0) return steps;
8279
+ for (const n of SPACING_STEPS) steps.set(String(n), `${round(n * amount)}${unit}`);
8280
+ return steps;
8281
+ }
8183
8282
  function frontMatterValue(token, key, lookup) {
8184
8283
  const value = token.value.trim();
8185
8284
  switch (key) {
@@ -8343,21 +8442,25 @@ function beforeV4(range) {
8343
8442
  /**
8344
8443
  * DTCG files and the CSS generated from them often describe the same tokens.
8345
8444
  * Merge entries that share a custom property: keep the DTCG name and
8346
- * description, and add the Tailwind mapping from CSS.
8445
+ * description, and add the Tailwind mapping from CSS. A token without one (a
8446
+ * `@theme inline` value, `rounded-sm`) merges with the token of its name and
8447
+ * Tailwind key, such as the one a DTCG file exported from the stylesheet has.
8347
8448
  */
8348
8449
  function mergeTokens(tokens) {
8349
8450
  const overridden = new Set(tokens.filter((t) => t.origin !== "tailwind-default").map((t) => themeKey(t)));
8350
- const byVar = /* @__PURE__ */ new Map();
8451
+ const byKey = /* @__PURE__ */ new Map();
8351
8452
  const out = [];
8352
8453
  for (const token of tokens) {
8353
8454
  if (token.origin === "tailwind-default" && overridden.has(themeKey(token))) continue;
8354
- const existing = token.cssVar ? byVar.get(token.cssVar) : void 0;
8455
+ const key = token.cssVar ?? (token.tailwind === void 0 ? void 0 : `${token.name}:${token.tailwind}`);
8456
+ const existing = key === void 0 ? void 0 : byKey.get(key);
8355
8457
  if (!existing) {
8356
- if (token.cssVar) byVar.set(token.cssVar, token);
8458
+ if (key !== void 0) byKey.set(key, token);
8357
8459
  out.push(token);
8358
8460
  continue;
8359
8461
  }
8360
8462
  if (existing.tailwind === void 0 && token.tailwind !== void 0) existing.tailwind = token.tailwind;
8463
+ if (existing.category === "other" && token.category !== "other") existing.category = token.category;
8361
8464
  existing.usage = [.../* @__PURE__ */ new Set([...token.usage, ...existing.usage])];
8362
8465
  existing.description ??= token.description;
8363
8466
  if (token.modes) existing.modes = {
@@ -12243,7 +12346,7 @@ function isObject(value) {
12243
12346
  // package.json
12244
12347
  var name = "onsystem";
12245
12348
  // src/version.ts
12246
- const VERSION = "0.10.3";
12349
+ const VERSION = "0.11.1";
12247
12350
  const NAME = name;
12248
12351
  // src/lint/sarif.ts
12249
12352
  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-7wZoJsMm.js";
2
- import { r as loadTarget } from "./workspace-C5pMS6Xv.js";
1
+ import { At as relativePath, a as fingerprint, i as componentFiles, ot as excludedBy, r as buildModel } from "./design-system-D7ppJYbd.js";
2
+ import { r as loadTarget } from "./workspace-DUgzMLGs.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-7wZoJsMm.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-D7ppJYbd.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 {
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-7wZoJsMm.js";
2
- import { n as INSTRUCTIONS, r as createServer, t as serveStdio } from "./stdio-B31hp2UZ.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-D7ppJYbd.js";
2
+ import { n as INSTRUCTIONS, r as createServer, t as serveStdio } from "./stdio-c3Y34to3.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-7wZoJsMm.js";
2
- import { i as projectDirOf, n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-C5pMS6Xv.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-D7ppJYbd.js";
2
+ import { i as projectDirOf, n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-DUgzMLGs.js";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -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-7wZoJsMm.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-D7ppJYbd.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.3",
3
+ "version": "0.11.1",
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": [