@designtools/adherence 0.0.0-stage → 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # @designtools/adherence
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 3da728f: The default exclude leaves out the `(design)` route group (`app/(design)/**`, `src/app/(design)/**`), where the skills put the docs and every other design page. A project laid out that way had its docs and copied blocks counted as product, and its headline share understated by nine points in the first end-to-end run.
8
+
9
+ - Layout is not off-system: a bracketed width, measure, grid template, aspect ratio or z-index (`max-w-[40rem]`, `grid-cols-[auto_1fr]`) is art direction the system deliberately does not tokenise. An `auto` margin (`mt-auto`) is not a spacing override.
10
+
11
+ - A status fill used as text (`text-destructive`, `text-success`, `text-warning`) is reported as off-system (`status-text`), pointing to its `-subdued-foreground` or, for a warning, neutral text with an icon.
12
+
13
+ ## 0.1.0
14
+
15
+ ### Minor Changes
16
+
17
+ - f16f305: First release. `designtools-adherence` reports how closely a React product follows its design system, reading the `@designtools/manifest` JSON and nothing else: the share of replaceable elements that come from the system (system component uses against the raw elements a component `replaces`), classes that bypass the tokens (palette steps and arbitrary values, through eslint-plugin-better-tailwindcss), and classNames that override a system component's colour, spacing or radius. Per file, per Next route and in total, as markdown for a pull request or deterministic JSON for docs. Warnings only: it exits 0 whatever it finds.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Made by Many Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,100 @@
1
- # Temporary Holding Version
1
+ # @designtools/adherence
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Reports how closely a React product follows its design system: how much of what a system component could render comes from the system, which classes bypass the tokens, and where a component's `className` fights its variants. It reads the JSON that [`@designtools/manifest`](../manifest) writes, so it needs no configuration of its own.
4
+
5
+ ```bash
6
+ npx @designtools/adherence # markdown, for a pull request comment
7
+ npx @designtools/adherence --format json # the JSON report
8
+ npx @designtools/adherence --json adherence.json # markdown to stdout, JSON to a file
9
+ ```
10
+
11
+ A report, never a gate. Every finding is a warning, and it exits 0 whatever it finds; 2 means it could not run at all. Use it to see where a product drifts and which components are missing a variant, not to fail a build.
12
+
13
+ Deterministic: the same source gives the same bytes, sorted throughout, with no timestamps.
14
+
15
+ ## Setup
16
+
17
+ None beyond the manifest. It finds `components.json` from `designtools.json` at the project root, as the other tools do, and reads `tokens.json` beside it:
18
+
19
+ ```json
20
+ {
21
+ "manifest": { "system": "src/ds" },
22
+ "adherence": { "exclude": ["app/legacy/**"] }
23
+ }
24
+ ```
25
+
26
+ `--manifest <components.json>` reads a manifest from elsewhere, and `--root` sets the project root.
27
+
28
+ ## What it scans
29
+
30
+ The product: `app/` and `src/`, by default. Never the system folder, the docs and design pages under `app/design/` or the `app/(design)/` route group, tests, `__tests__` folders, fixtures, examples, stories or `node_modules`. `--include <glob>` replaces the product globs, `--exclude <glob>` adds to what is left out; repeat either, or record them under `adherence` in `designtools.json`. A Next folder name in a glob means the folder: `--exclude "app/(app)/system/**"` leaves out that route group.
31
+
32
+ ## What it reports
33
+
34
+ **Component use.** Every JSX use of a system component, with its props, and every raw element that some component `replaces`. A component declares what it stands in for with `@replaces` in its JSDoc (`@replaces button`, `@replaces input[type=checkbox]`, `@replaces a[href]`), which the manifest records. Which imports are the system comes from the manifest too: an import that resolves, relatively or through a tsconfig `paths` alias, to a component's source file is that component, and one that resolves to another file in the system folder, such as a barrel `index.ts`, is matched by export name. Namespace imports (`<DS.Button>`) and sub-components (`<Select.Trigger>`) count.
35
+
36
+ From these comes the share: system component uses ÷ (system component uses + raw replaceable elements). Until a component declares `@replaces`, there are no raw elements to count and the report says so.
37
+
38
+ **Off-system values.** Classes that bypass the tokens:
39
+
40
+ | Kind | Examples |
41
+ | --- | --- |
42
+ | palette | `bg-blue-500`, `hover:text-primary-500/80`, `bg-(--color-primary-500)`: any `--color-<family>-<step>` ramp step in `tokens.json`, and Tailwind's default palette, used directly |
43
+ | arbitrary | `p-[13px]`, `md:bg-[#fff]`, `h-[18px]`, `[mask-type:alpha]`. A token reference in brackets, `w-[var(--size-md)]`, is not one, and neither is layout the system does not tokenise: widths and measures (`w-`, `min-w-`, `max-w-`), grid templates (`grid-cols-`, `grid-rows-`, `columns-`), `aspect-` and `z-` |
44
+ | status text | `text-destructive`, `text-success`, `md:text-warning`: a status fill used as text. The fill is measured against its own label, not the page, and fails there (red on a dark page, 2.4:1). Use `-subdued-foreground`, which the generator guarantees on every background of its mode, or neutral text with an icon for a warning |
45
+
46
+ They are found wherever Tailwind classes live, `className`, `cn()`, `clsx()`, `tv()`, `cva()` and the like, by [eslint-plugin-better-tailwindcss](https://github.com/schoero/eslint-plugin-better-tailwindcss)'s `no-restricted-classes` rule, run through ESLint's Node API with an in-memory config generated from `tokens.json`. The project's own ESLint setup is neither read nor changed.
47
+
48
+ **Overrides.** A `className` on a system component that changes its colour (`bg-`, `text-`, `border-`, `ring-` … with a colour), spacing (`p-`, `m-`, `gap-`, `space-`) or radius (`rounded-`). That usually means someone fought the component instead of choosing a variant, or that the component is missing one. Classes inside `cn()` count; layout and size (`w-full`, `flex-1`, `text-sm`, and an `auto` margin such as `mt-auto`) do not.
49
+
50
+ All of it per file, per route and in total. A route is a Next app router page, `app/**/page.tsx`, with route groups dropped (`app/(shop)/checkout/page.tsx` is `/checkout`). A route counts its page, the files beside it, and files in folders under it that lead to no other page, such as a private `_components` folder. A layout shared by several routes counts per file only. Per component, it also counts which props are passed and which variant options are picked, and lists the components the product never uses.
51
+
52
+ ## The report
53
+
54
+ The markdown is a pull request comment: the summary, a table of routes, then each file's findings with line numbers, and the components by use. It stays under GitHub's comment limit; past it, the remaining files are in the JSON.
55
+
56
+ The JSON, abridged:
57
+
58
+ ```json
59
+ {
60
+ "generator": "@designtools/adherence@0.1.0",
61
+ "schema": "designtools.adherence/1",
62
+ "manifest": "src/ds/manifest/components.json",
63
+ "system": "src/ds",
64
+ "summary": { "systemUses": 10, "rawElements": 5, "share": 0.6667, "offSystemValues": 6, "overrides": 2, "files": 7, "filesWithFindings": 5 },
65
+ "routes": [
66
+ { "route": "/checkout", "page": "app/(shop)/checkout/page.tsx", "files": ["app/(shop)/checkout/_parts/summary.tsx", "app/(shop)/checkout/page.tsx"], "counts": { "systemUses": 4, "rawElements": 1, "share": 0.8, "offSystemValues": 4, "overrides": 1 } }
67
+ ],
68
+ "files": [
69
+ {
70
+ "file": "app/(shop)/checkout/page.tsx",
71
+ "route": "/checkout",
72
+ "raw": [{ "element": "input", "selector": "input[type=checkbox]", "replacedBy": ["Checkbox"], "line": 10, "column": 10 }],
73
+ "offSystem": [{ "class": "p-[13px]", "kind": "arbitrary", "line": 7, "column": 22 }],
74
+ "overrides": [{ "component": "Button", "classes": [{ "class": "bg-destructive", "kind": "colour" }, { "class": "px-6", "kind": "spacing" }], "line": 13, "column": 8 }]
75
+ }
76
+ ]
77
+ }
78
+ ```
79
+
80
+ The full schema is in [`src/types.ts`](src/types.ts), which has no imports so it can be copied, for the "Adherence summary" docs block in [`@designtools/blocks`](../blocks) to read the report through the same types.
81
+
82
+ ## Programmatic use
83
+
84
+ ```ts
85
+ import { buildReport, formatMarkdown, loadConfig } from "@designtools/adherence";
86
+
87
+ const { report } = await buildReport(loadConfig({ root: "." }));
88
+ report.summary.share; // 0.6667
89
+ formatMarkdown(report); // the pull request comment
90
+ ```
91
+
92
+ ## How it is built
93
+
94
+ It wraps existing tools rather than reimplementing them. Component use comes from [react-scanner](https://github.com/moroshko/react-scanner), kept behind one small module (`src/scanner.ts`) because it has had no release since October 2024 and pins TypeScript 5.6 for its parser: anything that produces the same raw report can replace it. Off-system values come from eslint-plugin-better-tailwindcss under ESLint 10. The override check is the only rule of its own, and it reads react-scanner's raw report.
95
+
96
+ A file either scanner cannot parse is listed under `skipped`, and its numbers are missing; it never fails the run.
97
+
98
+ ## Licence
99
+
100
+ [MIT](LICENSE), © Made by Many Ltd. Its dependencies are MIT too, except TypeScript, which typescript-eslint's parser needs: Apache-2.0.