onsystem 0.9.1 → 0.10.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/README.md +16 -1
- package/dist/cli.js +152 -56
- package/dist/{config-B98w8bcg.d.ts → config-BbTW1jKU.d.ts} +12 -2
- package/dist/{design-system-DZm2aP1z.js → design-system-CPH4C7-1.js} +389 -170
- package/dist/eslint-worker.d.ts +1 -1
- package/dist/eslint-worker.js +2 -2
- package/dist/eslint.js +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +2 -2
- package/dist/{stdio-BJvjEnKW.js → stdio-BK2kMN1x.js} +681 -339
- package/dist/{workspace-Dqu3nkYM.js → workspace-BINqxi7p.js} +8 -7
- package/package.json +1 -1
- package/schema.json +5 -1
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ npx -y onsystem check . # the same check the agent and CI run
|
|
|
69
69
|
npx -y onsystem init # write the config, and offer a baseline
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
At a [monorepo root](https://onsystem.vercel.app/docs/configuration#monorepo-roots), `inspect` lists every app and design-system package, and `check` checks each file by its own project's design system. In a [design-system package](https://onsystem.vercel.app/docs/design-system-packages), `init` sets it up for every app that depends on it, with no config in the apps: as a workspace package, or installed from npm when it publishes
|
|
72
|
+
At a [monorepo root](https://onsystem.vercel.app/docs/configuration#monorepo-roots), `inspect` lists every app and design-system package, and `check` checks each file by its own project's design system. In a [design-system package](https://onsystem.vercel.app/docs/design-system-packages), `init` sets it up for every app that depends on it, with no config in the apps: as a workspace package, or installed from npm. Installed apps read the components from the sources the package publishes, or from its type declarations (`.d.ts`) when it publishes only compiled `dist/`, and get every rule either way (`init` checks its `files` list). From a package that publishes compiled code and no type declarations, they get the token rules but no component, prop, variant or native-element checks.
|
|
73
73
|
|
|
74
74
|
In a new create-next-app, the template page's hardcoded colors already make the first `check .` fail; `init` offers to record them as a baseline, so only new findings fail. Folders installed from a shadcn registry, such as `components/ai-elements`, are someone else's code: [Code from shadcn registries](https://onsystem.vercel.app/docs/configuration#code-from-shadcn-registries) says how to check them or leave them out.
|
|
75
75
|
|
|
@@ -128,6 +128,21 @@ steps:
|
|
|
128
128
|
|
|
129
129
|
That count is onsystem's own, and the MCP server and the hook show the agent the same errors to fix. In tasks that ask for a green status label in a design system with no success token, agents with onsystem often used `chart-*` tokens, which it counts as clean. The blind labels by an AI reviewer, 12 files per arm, are too few to confirm the count either way, and every violation they found with the hook or all three is a palette color in Dub, whose tokens 0.5.0 did not read. The [benchmark page](https://onsystem.vercel.app/benchmark) has every table, and [bench/v2](bench/v2) the method, the per-run data and every generated file.
|
|
130
130
|
|
|
131
|
+
## Compatibility
|
|
132
|
+
|
|
133
|
+
What `package.json` declares and CI runs:
|
|
134
|
+
|
|
135
|
+
| With | Supported |
|
|
136
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
137
|
+
| Node.js | Node.js 20.19 or later (`engines`). CI runs the tests on Node.js 22, 24 and 26, and the built package on 20.19 |
|
|
138
|
+
| Operating system | Linux, macOS and Windows: CI runs the tests and the pack test on each |
|
|
139
|
+
| ESLint | ESLint 8.57, 9 and 10, for the ESLint plugin only: `eslint` is an optional peer, and the CLI, the MCP server, the hook and the Action do not need it |
|
|
140
|
+
| TypeScript | Your project's TypeScript is not used, and a JavaScript project needs none: onsystem reads `.tsx` and `.jsx` with its own TypeScript 6.0, a dependency, not a peer |
|
|
141
|
+
| React | Components written for React 18 (`forwardRef`) or React 19 (`ref` as a prop), read from source or type declarations. onsystem does not depend on React |
|
|
142
|
+
| Tailwind CSS | v4 (`@theme`) and v3 (`tailwind.config`, read without running it). Without Tailwind, tokens come from CSS custom properties, DTCG files or a DESIGN.md |
|
|
143
|
+
| shadcn/ui | Both bases, Radix (`asChild`) and Base UI (`render`), on Tailwind v3 or v4, with no config |
|
|
144
|
+
| MCP clients | Any client that starts a stdio server; CI connects on the 2025-11-25 and 2026-07-28 protocols. [Set up your agent](https://onsystem.vercel.app/docs/clients) has each client's config and support tier |
|
|
145
|
+
|
|
131
146
|
## Docs
|
|
132
147
|
|
|
133
148
|
On the [website](https://onsystem.vercel.app/docs), and as Markdown in [docs/](docs):
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
3
|
-
import { a as notReadNotices, i as missingDependencies, o as explainProject, s as librariesLeftOut, t as serveStdio } from "./stdio-
|
|
4
|
-
import { r as loadTarget } from "./workspace-
|
|
2
|
+
import { $ as DEFAULT_TOKENS, A as unrecordedRules, At as toPosix, B as statusKinds, C as countBaseline, D as pruneBaseline, Dt as matchesGlob, E as onLines, Et as isInside, Ft as unique, J as ConfigError, L as toDesignMd, Lt as readProjectConfig, M as writeBaseline, Nt as plural, O as readBaseline, R as toDtcg, S as applyBaseline, T as goneFiles, Tt as SCRIPT_FILE, Y as DEFAULT_DOCS, _ as formatDiagnostics, at as excludedBy, b as BASELINE_FILE, c as silentLogger, et as FALLBACK_TOKENS, g as escapeData, gt as PACKAGE_FIELD, h as uncheckedNotice, ht as notReadYet, i as componentFiles, j as updateBaseline, k as rulesRun, kt as relativePath, l as stderrLogger, lt as rulesFor, mt as libraryTitle, nt as RULE_IDS, o as loadDesignSystem, q as CONFIG_FILES, s as tokenFiles, tt as LEGACY_CONFIG_FILES, v as NAME, vt as declaredConfig, w as findMoves, x as LEGACY_BASELINE_FILE, xt as isDesignSystemName, y as VERSION, yt as detectProject } from "./design-system-CPH4C7-1.js";
|
|
3
|
+
import { a as notReadNotices, i as missingDependencies, o as explainProject, s as librariesLeftOut, t as serveStdio } from "./stdio-BK2kMN1x.js";
|
|
4
|
+
import { r as loadTarget } from "./workspace-BINqxi7p.js";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { escapePath, glob } from "tinyglobby";
|
|
@@ -26,7 +26,8 @@ const GUIDE_FILE = "ONSYSTEM.md";
|
|
|
26
26
|
* (`"onsystem": "./onsystem.config.json"`, and in `files` when the package lists them), so
|
|
27
27
|
* every app that depends on the package reads it, and writes ONSYSTEM.md, which tells those
|
|
28
28
|
* apps' teams how to add the agent tools and the CI check. In an app that takes on such a
|
|
29
|
-
* package's config, it writes no config of its own unless --force says to
|
|
29
|
+
* package's config, it writes no config of its own unless --force says to, and then one that
|
|
30
|
+
* builds on the package's, for the app's own rules and overrides.
|
|
30
31
|
*
|
|
31
32
|
* In a terminal, it offers to add a short section on onsystem to the project's AGENTS.md or
|
|
32
33
|
* CLAUDE.md, for coding agents.
|
|
@@ -52,20 +53,28 @@ async function init(configOptions, options, io, runCheck) {
|
|
|
52
53
|
const notes = [];
|
|
53
54
|
const relative = (abs) => relativePath(root, abs);
|
|
54
55
|
const inherited = options.force ? void 0 : config.designSystemPackage;
|
|
55
|
-
const
|
|
56
|
+
const declaring = options.force && !configOptions.components?.length ? config.designSystemPackage : void 0;
|
|
57
|
+
const replaced = declaring && (config.inherits || !await namesOwnComponents(config, declaring)) ? declaring : void 0;
|
|
56
58
|
let found = false;
|
|
57
|
-
if (inherited) notes.push(`This project takes on the config ${inherited.name} declares in its package.json (${relativePath(io.cwd, inherited.config) || inherited.config}), so it needs none of its own; nothing was written. Pass --force to write one, which
|
|
59
|
+
if (inherited) notes.push(`This project takes on the config ${inherited.name} declares in its package.json (${relativePath(io.cwd, inherited.config) || inherited.config}), so it needs none of its own; nothing was written. Pass --force to write one for its own rules and overrides, which builds on ${inherited.name}'s.`);
|
|
58
60
|
else if (configOptions.components?.length) {
|
|
59
61
|
file.components = configOptions.components;
|
|
60
62
|
if (configOptions.tokens?.length) file.tokens = configOptions.tokens;
|
|
61
63
|
if (configOptions.docs?.length) file.docs = configOptions.docs;
|
|
62
|
-
} else if (
|
|
64
|
+
} else if (replaced) {
|
|
65
|
+
const tokens = existing ? ownTokens(existing, root, replaced) : void 0;
|
|
66
|
+
if (tokens) file.tokens = tokens;
|
|
67
|
+
} else if (workspace) notes.push(`This is a monorepo root: ${workspace.projects.length} of ${workspace.projects.length + workspace.others.length} workspace packages have a design system (${workspace.projects.map((p) => p.dir).join(", ") || "none"}). Each keeps zero config; rules and overrides you add here apply to every project without a config of its own when it is checked from this folder, over the config its design-system package declares.`);
|
|
63
68
|
else if (config.detected) {
|
|
64
69
|
notes.push(`Found the design system through ${config.detected}.`);
|
|
65
70
|
const specifiers = new Set((config.imports ?? []).map((m) => m.specifier));
|
|
66
71
|
const tokens = (await tokenFiles(config)).map(relative);
|
|
67
|
-
|
|
72
|
+
const fromInstalled = installedFields(config.components, tokens);
|
|
73
|
+
if (fromInstalled.length) notes.push(installedNote(fromInstalled, tokens));
|
|
74
|
+
const changed = fromInstalled.includes("tokens") && !fromInstalled.includes("components") ? await tokensWithComponents(config, tokens) : void 0;
|
|
75
|
+
if (fromInstalled.includes("components")) {} else if (specifiers.size > 1) notes.push(`"components" is left to zero config: apps import them by ${specifiers.size} specifiers from the package's exports (${[...specifiers].slice(0, 2).join(", ")}, …), which a config cannot spell.`);
|
|
68
76
|
else if (config.components.length > MAX_GLOBS) notes.push(`"components" is left to zero config: it found ${config.components.length} files through the package's exports, which it keeps following as the package changes.`);
|
|
77
|
+
else if (changed) notes.push(`"components" is left to zero config as well: a config that names them reads the tokens only from the usual stylesheets (${DEFAULT_TOKENS[0]} and the like)${changed.lost.length ? `, which leave out ${andList(changed.lost)}` : ", not the ones zero config reads"}.`);
|
|
69
78
|
else {
|
|
70
79
|
file.components = config.components;
|
|
71
80
|
found = true;
|
|
@@ -74,20 +83,20 @@ async function init(configOptions, options, io, runCheck) {
|
|
|
74
83
|
const docs = config.docs.filter((d) => !DEFAULT_DOCS.includes(d));
|
|
75
84
|
if (docs.length) file.docs = [...DEFAULT_DOCS, ...docs];
|
|
76
85
|
}
|
|
77
|
-
if (tokens.length) file.tokens = tokens;
|
|
86
|
+
if (tokens.length && !fromInstalled.includes("tokens")) file.tokens = tokens;
|
|
78
87
|
} else if ((await componentFiles(config)).length) {
|
|
79
|
-
const declared = replaced && config.configured;
|
|
80
88
|
const matching = [];
|
|
81
89
|
for (const glob of config.components) if ((await componentFiles({
|
|
82
90
|
...config,
|
|
83
91
|
components: [glob]
|
|
84
92
|
})).length) matching.push(glob);
|
|
85
|
-
notes.push(
|
|
93
|
+
notes.push(`Found the design system in the default folders: ${matching.join(", ")}.`);
|
|
86
94
|
file.components = matching;
|
|
87
95
|
found = true;
|
|
88
|
-
if (declared && config.importPath) file.importPath = config.importPath;
|
|
89
96
|
const tokens = (await tokenFiles(config)).map(relative);
|
|
90
|
-
|
|
97
|
+
const fromInstalled = installedFields([], tokens);
|
|
98
|
+
if (fromInstalled.length) notes.push(installedNote(fromInstalled, tokens));
|
|
99
|
+
else if (tokens.length) file.tokens = tokens;
|
|
91
100
|
} else {
|
|
92
101
|
if (!ask) {
|
|
93
102
|
io.stderr(`init: zero config found no design system in ${root}. Pass --components (and --tokens) to say where it is, or run init in a terminal to answer a few questions. \`${NAME} inspect --explain\` shows what it looked at.`);
|
|
@@ -124,13 +133,7 @@ async function init(configOptions, options, io, runCheck) {
|
|
|
124
133
|
fs.writeFileSync(out, `${JSON.stringify(file, null, 2)}\n`);
|
|
125
134
|
written.push(CONFIG_FILE);
|
|
126
135
|
}
|
|
127
|
-
if (replaced)
|
|
128
|
-
const own = (await loadTarget({
|
|
129
|
-
...configOptions,
|
|
130
|
-
config: void 0
|
|
131
|
-
})).config;
|
|
132
|
-
notes.push(await replacedNote(replaced, config, own, io.cwd));
|
|
133
|
-
}
|
|
136
|
+
if (replaced) notes.push(replacedNote(replaced, io.cwd, file.tokens !== void 0));
|
|
134
137
|
let registry;
|
|
135
138
|
if (designSystem && manifest && name) {
|
|
136
139
|
written.push(...declarePackage(root, manifest, name, options.force, notes));
|
|
@@ -341,7 +344,7 @@ function declarePackage(root, manifest, name, force, notes) {
|
|
|
341
344
|
fs.writeFileSync(manifest.file, `${JSON.stringify(json, null, manifest.indent)}${manifest.newline ? "\n" : ""}`);
|
|
342
345
|
written.push("package.json");
|
|
343
346
|
}
|
|
344
|
-
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
|
|
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.`);
|
|
345
348
|
const guide = path.join(root, GUIDE_FILE);
|
|
346
349
|
if (force || !fs.existsSync(guide)) {
|
|
347
350
|
fs.writeFileSync(guide, consumerGuide(name));
|
|
@@ -354,7 +357,8 @@ function declarePackage(root, manifest, name, force, notes) {
|
|
|
354
357
|
* files its `files` list leaves out of the published package, and those outside the package
|
|
355
358
|
* (`../tailwind-config/theme.css`), which an installed package's config cannot reach. Undefined
|
|
356
359
|
* for a private package, or when they find every file. Without a `files` list, npm publishes
|
|
357
|
-
* everything in the package.
|
|
360
|
+
* everything in the package. Without any component file, those apps read the components from
|
|
361
|
+
* the type declarations the package publishes, when it names some.
|
|
358
362
|
*/
|
|
359
363
|
async function unshipped(config, json) {
|
|
360
364
|
if (json.private === true) return void 0;
|
|
@@ -374,13 +378,14 @@ async function unshipped(config, json) {
|
|
|
374
378
|
tokens: tokens.filter(outside)
|
|
375
379
|
};
|
|
376
380
|
const noComponents = components.length > 0 && missing.components.length + away.components.length === components.length;
|
|
381
|
+
const declarations = noComponents ? publishedDeclarations(config.root, json, files) : [];
|
|
377
382
|
const lost = (lists, one) => {
|
|
378
383
|
const c = lists.components.length;
|
|
379
384
|
const t = lists.tokens.length;
|
|
380
385
|
const kinds = andList([...c ? [count$1(c, "component file")] : [], ...t ? [count$1(t, "token file")] : []]);
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
return `(${kinds}). Those apps do not know the ${
|
|
386
|
+
const them = one ? "it" : "them";
|
|
387
|
+
if (c && noComponents && declarations.length) return `(${kinds}). Those apps read the components from the type declarations it publishes instead (${andList(declarations)})${t ? `, and do not know the tokens in ${them}` : ""}.`;
|
|
388
|
+
return `(${kinds}). Those apps do not know the ${andList([...c ? ["components"] : [], ...t ? ["tokens"] : []], "or")} in ${them}${c && noComponents ? `: with no type declarations published either, they get no component, prop, variant or native-element checks, and \`${NAME} check --require-design-system\` fails in them` : ""}.`;
|
|
384
389
|
};
|
|
385
390
|
const notes = [];
|
|
386
391
|
const top = (f) => f.includes("/") ? `${f.slice(0, f.indexOf("/"))}/` : f;
|
|
@@ -397,36 +402,121 @@ async function unshipped(config, json) {
|
|
|
397
402
|
if (!notes.length) return void 0;
|
|
398
403
|
return {
|
|
399
404
|
notes,
|
|
400
|
-
next: noComponents && missing.components.length ? `gets no component checks until "files" publishes ${andList([...new Set(missing.components.map(top))])}` : `does not read ${andList([...folders, ...beyond])}`
|
|
405
|
+
next: noComponents && missing.components.length ? declarations.length ? `reads its components from the type declarations it publishes, not from ${andList([...new Set(missing.components.map(top))])}` : `gets no component checks until "files" publishes ${andList([...new Set(missing.components.map(top))])}` : `does not read ${andList([...folders, ...beyond])}`
|
|
401
406
|
};
|
|
402
407
|
}
|
|
403
408
|
/**
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
409
|
+
* The type declarations a package's package.json names, as apps that install it look for them:
|
|
410
|
+
* `types` or `typings`, a `types` condition in its `exports`, or an `index.d.ts` beside it, each
|
|
411
|
+
* that its `files` list publishes. Relative to the package.
|
|
407
412
|
*/
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
413
|
+
function publishedDeclarations(root, json, files) {
|
|
414
|
+
const named = [];
|
|
415
|
+
for (const field of ["types", "typings"]) {
|
|
416
|
+
const value = json[field];
|
|
417
|
+
if (typeof value === "string") named.push(value);
|
|
418
|
+
}
|
|
419
|
+
const conditions = (value, key) => {
|
|
420
|
+
if (typeof value === "string") {
|
|
421
|
+
if (key === "types") named.push(value);
|
|
422
|
+
} else if (Array.isArray(value)) for (const item of value) conditions(item);
|
|
423
|
+
else if (value && typeof value === "object") for (const [k, v] of Object.entries(value)) conditions(v, k);
|
|
412
424
|
};
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
425
|
+
conditions(json.exports);
|
|
426
|
+
if (fs.existsSync(path.join(root, "index.d.ts"))) named.push("index.d.ts");
|
|
427
|
+
return [...new Set(named.map((f) => f.replace(/^\.\//, "")))].filter((f) => !files || ships(files, f));
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Said when --force writes a config in an app on a design-system package that declares one: the
|
|
431
|
+
* new config builds on the package's, which still gives it what it leaves out.
|
|
432
|
+
*/
|
|
433
|
+
function replacedNote(pkg, cwd, keptTokens) {
|
|
416
434
|
const declared = relativePath(cwd, pkg.config) || pkg.config;
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
435
|
+
const fields = keptTokens ? "\"components\" and \"importPath\"" : "\"components\", \"importPath\" and \"tokens\"";
|
|
436
|
+
const kept = keptTokens ? " It keeps the \"tokens\" the old one listed, which name a stylesheet of this project's own." : "";
|
|
437
|
+
return `This project reads the config ${pkg.name} declares (${declared}). With --force it has one of its own, ${CONFIG_FILE}, read over that one: what it leaves out, such as ${fields}, still comes from ${pkg.name}'s config, and the rules and overrides you add to it apply over ${pkg.name}'s rules.${kept} "extends": false in it reads it alone.`;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* The `tokens` an existing JSON config lists, when one of them is outside the design-system
|
|
441
|
+
* package, links resolved: an app that takes the package's components and reads its own
|
|
442
|
+
* stylesheet too lists both. A list of the package's files only is left to the package's config.
|
|
443
|
+
*/
|
|
444
|
+
function ownTokens(file, root, pkg) {
|
|
445
|
+
let tokens;
|
|
446
|
+
try {
|
|
447
|
+
tokens = JSON.parse(fs.readFileSync(file, "utf8")).tokens;
|
|
448
|
+
} catch {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (!Array.isArray(tokens) || !tokens.every((t) => typeof t === "string")) return;
|
|
452
|
+
const real = (abs) => {
|
|
453
|
+
try {
|
|
454
|
+
return fs.realpathSync(abs);
|
|
455
|
+
} catch {
|
|
456
|
+
return abs;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
const dir = real(pkg.dir);
|
|
460
|
+
return tokens.some((t) => !isInside(dir, real(path.resolve(root, t)))) ? tokens : void 0;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Whether the files a config's `components` match include some outside the design-system
|
|
464
|
+
* package, links resolved: a config that names only the package's (copied from its config, as
|
|
465
|
+
* `node_modules/@acme/ui/src/*.tsx`), or none, has no components of its own.
|
|
466
|
+
*/
|
|
467
|
+
async function namesOwnComponents(config, pkg) {
|
|
468
|
+
const real = (file) => {
|
|
469
|
+
try {
|
|
470
|
+
return fs.realpathSync(file);
|
|
471
|
+
} catch {
|
|
472
|
+
return path.resolve(file);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
const dir = real(pkg.dir);
|
|
476
|
+
return (await componentFiles(config)).some((file) => !isInside(dir, real(file)));
|
|
477
|
+
}
|
|
478
|
+
/** Whether a path, relative to the root, is in a package installed in `node_modules`. */
|
|
479
|
+
const isInstalledPath = (file) => /(?:^|\/)node_modules\//.test(file);
|
|
480
|
+
/**
|
|
481
|
+
* The fields `init` leaves to zero config because what zero config found for them is in a
|
|
482
|
+
* package installed in `node_modules`: `components` (their globs) and `tokens` (files).
|
|
483
|
+
*/
|
|
484
|
+
function installedFields(components, tokens) {
|
|
485
|
+
return [...components.some(isInstalledPath) ? ["components"] : [], ...tokens.some(isInstalledPath) ? ["tokens"] : []];
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Why `fields` are left to zero config: a path into an installed package breaks when it is
|
|
489
|
+
* upgraded, since pnpm keeps each version in a folder of its own
|
|
490
|
+
* (`node_modules/.pnpm/@acme+ui@1.2.0_react@19.2.0/node_modules/@acme/ui`), and a list of its
|
|
491
|
+
* files misses the ones a new version adds. Zero config finds it again each time.
|
|
492
|
+
*/
|
|
493
|
+
function installedNote(fields, tokens) {
|
|
494
|
+
const one = fields.length === 1;
|
|
495
|
+
const what = fields.includes("tokens") && !tokens.every(isInstalledPath) ? `${fields.includes("components") ? "the components and " : ""}some of the token files come` : one ? "it comes" : "they come";
|
|
496
|
+
return `${andList(fields.map((f) => `"${f}"`))} ${one ? "is" : "are"} left to zero config: ${what} from a package installed in node_modules, which zero config finds again after each upgrade, while a path written here would break once the package's folder or files change (pnpm keeps each version in a folder of its own in node_modules/.pnpm).`;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* What changes when a config names components and leaves `tokens` (relative to the root, as
|
|
500
|
+
* zero config finds them) to zero config: such a config turns detection off, so the stylesheets
|
|
501
|
+
* it found (components.json's `app/styles/tailwind.css`, and what they import from an installed
|
|
502
|
+
* package) give way to the usual ones ({@link DEFAULT_TOKENS}), with what those import. Undefined
|
|
503
|
+
* when that reads the same token files; else `lost` holds the ones it leaves out, the app's own
|
|
504
|
+
* first.
|
|
505
|
+
*/
|
|
506
|
+
async function tokensWithComponents(config, tokens) {
|
|
507
|
+
const usual = await tokenFiles({
|
|
508
|
+
...config,
|
|
509
|
+
tokens: [...DEFAULT_TOKENS.map((p) => ({ path: p })), ...FALLBACK_TOKENS.map((p) => ({
|
|
510
|
+
path: p,
|
|
511
|
+
fallback: true
|
|
512
|
+
}))],
|
|
513
|
+
tokensDetected: true
|
|
514
|
+
});
|
|
515
|
+
const read = new Set(usual.map((f) => relativePath(config.root, f)));
|
|
516
|
+
if (read.size === tokens.length && tokens.every((t) => read.has(t))) return void 0;
|
|
517
|
+
const lost = tokens.filter((t) => !read.has(t));
|
|
518
|
+
const own = lost.filter((t) => !isInstalledPath(t));
|
|
519
|
+
return { lost: own.length ? own : lost };
|
|
430
520
|
}
|
|
431
521
|
const REGISTRY_DOCS = "https://onsystem.vercel.app/docs/configuration#code-from-shadcn-registries";
|
|
432
522
|
/**
|
|
@@ -555,7 +645,7 @@ function consumerGuide(name) {
|
|
|
555
645
|
`npx -y ${NAME} inspect`,
|
|
556
646
|
"```",
|
|
557
647
|
"",
|
|
558
|
-
`An app with an ${CONFIG_FILE} of its own reads
|
|
648
|
+
`An app with an ${CONFIG_FILE} of its own reads it over ${name}'s config: the rules and overrides it sets apply over ${name}'s rules, and, unless it sets components of its own, what it leaves out, such as the components, \`importPath\` and tokens, comes from ${name}'s. \`"extends": false\` in it reads it alone.`,
|
|
559
649
|
"",
|
|
560
650
|
"## Coding agents",
|
|
561
651
|
"",
|
|
@@ -854,7 +944,8 @@ async function main(argv, io = defaultIo) {
|
|
|
854
944
|
io.stderr(error.message);
|
|
855
945
|
return 2;
|
|
856
946
|
}
|
|
857
|
-
|
|
947
|
+
io.stderr(error.stack ?? String(error));
|
|
948
|
+
return 2;
|
|
858
949
|
}
|
|
859
950
|
}
|
|
860
951
|
async function check(patterns, configOptions, values, io) {
|
|
@@ -1427,7 +1518,7 @@ async function inspect(configOptions, values, io) {
|
|
|
1427
1518
|
}
|
|
1428
1519
|
const lines = [
|
|
1429
1520
|
`root ${model.root}`,
|
|
1430
|
-
`config ${config.configFile ? `${displayPath(io.cwd, config.configFile)}${declaredBy(config)}` : "(defaults)"}`,
|
|
1521
|
+
`config ${config.configFile ? `${displayPath(io.cwd, config.configFile)}${declaredBy(config, (f) => displayPath(io.cwd, f))}` : "(defaults)"}`,
|
|
1431
1522
|
...config.detected ? [`detected ${config.detected}`] : [],
|
|
1432
1523
|
...describeLibraries(config, model, io.cwd).map((line) => `library ${line}`),
|
|
1433
1524
|
`files ${plural(model.stats.files.components, "component file")}, ${plural(model.stats.files.tokens, "token file")}, ${plural(model.stats.files.docs, "doc")}`,
|
|
@@ -1561,7 +1652,7 @@ async function inspectWorkspace(workspace, values, io) {
|
|
|
1561
1652
|
const { model } = ds;
|
|
1562
1653
|
const roots = ds.roots();
|
|
1563
1654
|
const tokens = [...tokenCounts(model)].map(([category, n]) => `${category} ${n}`);
|
|
1564
|
-
const own = project.config.configFile && project.config.configFile !== workspace.configFile ? `${displayPath(workspace.root, project.config.configFile)}${declaredBy(project.config)}` : void 0;
|
|
1655
|
+
const own = project.config.configFile && (project.config.configFile !== workspace.configFile || project.config.designSystemPackage) ? `${displayPath(workspace.root, project.config.configFile)}${declaredBy(project.config, (f) => displayPath(workspace.root, f))}` : void 0;
|
|
1565
1656
|
lines.push("", `${project.dir}${project.name ? ` (${project.name})` : ""}`, ...own ? [` config ${own}`] : [], ...project.config.detected ? [` detected ${project.config.detected}`] : [], ...describeLibraries(project.config, model, io.cwd).map((line) => ` library ${line}`), ` files ${plural(model.stats.files.components, "component file")}, ${plural(model.stats.files.tokens, "token file")}, ${plural(model.stats.files.docs, "doc")}`, ` components ${roots.length} + ${ds.components.length - roots.length} parts${roots.length ? `: ${listNames(roots.map((c) => c.name))}` : ""}`, ` tokens ${tokens.length ? tokens.join(", ") : "none"}`, ...model.warnings.length ? [` warnings ${model.warnings.length}: ${model.warnings[0] ?? ""}`] : []);
|
|
1566
1657
|
}
|
|
1567
1658
|
if (!loaded.length) lines.push("", `No workspace package has a design system that zero config finds. See ${DOCS}/troubleshooting`);
|
|
@@ -1589,9 +1680,14 @@ function explainWorkspace(workspace, loaded, cwd) {
|
|
|
1589
1680
|
for (const { project, ds } of loaded) lines.push("", `${project.dir}${project.name ? ` (${project.name})` : ""}`, ...explainProject(ds, cwd, " "));
|
|
1590
1681
|
return lines;
|
|
1591
1682
|
}
|
|
1592
|
-
/**
|
|
1593
|
-
|
|
1594
|
-
|
|
1683
|
+
/**
|
|
1684
|
+
* ` (declared by @acme/ui)` after a config a design-system package declares, `, over
|
|
1685
|
+
* packages/ui/onsystem.config.json (declared by @acme/ui)` after one read over it; empty otherwise.
|
|
1686
|
+
*/
|
|
1687
|
+
function declaredBy(config, display) {
|
|
1688
|
+
const declared = config.designSystemPackage;
|
|
1689
|
+
if (!declared) return "";
|
|
1690
|
+
return `${config.configFile === declared.config ? "" : `, over ${display(declared.config)}`} (declared by ${declared.name})`;
|
|
1595
1691
|
}
|
|
1596
1692
|
/** `A, B, C` up to a dozen names, then `and 40 more`. */
|
|
1597
1693
|
function listNames(names, max = 12) {
|
|
@@ -303,6 +303,7 @@ declare const ruleOptionsSchema: z.ZodObject<{
|
|
|
303
303
|
type RuleOptions = z.infer<typeof ruleOptionsSchema>;
|
|
304
304
|
declare const configSchema: z.ZodObject<{
|
|
305
305
|
$schema: z.ZodOptional<z.ZodString>;
|
|
306
|
+
extends: z.ZodOptional<z.ZodBoolean>;
|
|
306
307
|
components: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
307
308
|
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
308
309
|
tokens: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
@@ -450,12 +451,21 @@ interface ResolvedConfig {
|
|
|
450
451
|
*/
|
|
451
452
|
base?: string | undefined;
|
|
452
453
|
/**
|
|
453
|
-
* The design-system package whose config this
|
|
454
|
-
* (`"onsystem": "./onsystem.config.json"`)
|
|
454
|
+
* The design-system package whose config this project reads: a dependency whose package.json
|
|
455
|
+
* declares it (`"onsystem": "./onsystem.config.json"`). A project with no config of its own
|
|
456
|
+
* takes it on (`configFile` is the package's); one with a config of its own, or a workspace
|
|
457
|
+
* root's, reads it under that one (`configFile` is that one), unless it sets `"extends": false`.
|
|
455
458
|
* Its paths are rebased from the package; its `tsconfig`, `overrides` and
|
|
456
459
|
* `includeDesignSystem` stay with it.
|
|
457
460
|
*/
|
|
458
461
|
designSystemPackage?: DesignSystemPackage | undefined;
|
|
462
|
+
/**
|
|
463
|
+
* The project's config sets no `components`, so what it leaves out of `components`,
|
|
464
|
+
* `importPath`, `docs`, `elements`, `tokens` and `libraries` comes from `designSystemPackage`'s.
|
|
465
|
+
*/
|
|
466
|
+
inherits?: boolean | undefined;
|
|
467
|
+
/** The project's config sets `"extends": false`, so no design-system package's config is read. */
|
|
468
|
+
extendsOff?: boolean | undefined;
|
|
459
469
|
/** `components` comes from the config file or `--components`, not from detection or the defaults. */
|
|
460
470
|
configured?: boolean | undefined;
|
|
461
471
|
/**
|