@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 +17 -0
- package/LICENSE +21 -0
- package/README.md +99 -2
- package/dist/chunk-7FM4ZQLW.js +1030 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +113 -0
- package/dist/index.d.ts +473 -0
- package/dist/index.js +62 -0
- package/package.json +69 -3
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
UsageError,
|
|
4
|
+
VERSION,
|
|
5
|
+
buildReport,
|
|
6
|
+
formatMarkdown,
|
|
7
|
+
loadConfig,
|
|
8
|
+
serialise
|
|
9
|
+
} from "./chunk-7FM4ZQLW.js";
|
|
10
|
+
|
|
11
|
+
// src/cli.ts
|
|
12
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
13
|
+
import { dirname, resolve } from "path";
|
|
14
|
+
import { parseArgs } from "util";
|
|
15
|
+
var HELP = `designtools-adherence ${VERSION}
|
|
16
|
+
|
|
17
|
+
How closely a product follows its design system: how much of what a system
|
|
18
|
+
component could render comes from the system, which classes bypass the tokens,
|
|
19
|
+
and where a component's className fights its variants. Warnings only: it exits
|
|
20
|
+
0 whatever it finds.
|
|
21
|
+
|
|
22
|
+
Usage
|
|
23
|
+
designtools-adherence Print a markdown report, for a pull request comment
|
|
24
|
+
designtools-adherence --format json Print the JSON report
|
|
25
|
+
designtools-adherence --json report.json Also write the JSON report to a file
|
|
26
|
+
|
|
27
|
+
Options
|
|
28
|
+
--manifest <file> The components.json to read (default: from designtools.json)
|
|
29
|
+
--include <glob> Product files to scan; repeat for each (default: app/** and src/**)
|
|
30
|
+
--exclude <glob> Files to leave out, on top of the system folder, app/design, app/(design),
|
|
31
|
+
tests, fixtures, examples and stories; repeat for each
|
|
32
|
+
--format <md|json> What to print (default: md)
|
|
33
|
+
--json <file> Write the JSON report to a file as well
|
|
34
|
+
--root <dir> Project root (default: the current directory)
|
|
35
|
+
--timings Print how long each stage took, to stderr
|
|
36
|
+
-h, --help Show this help
|
|
37
|
+
-v, --version Show the version
|
|
38
|
+
|
|
39
|
+
It reads the manifest that designtools-manifest builds, found from designtools.json:
|
|
40
|
+
{ "manifest": { "system": "src/ds" }, "adherence": { "exclude": ["app/legacy/**"] } }
|
|
41
|
+
`;
|
|
42
|
+
function fail(message) {
|
|
43
|
+
process.stderr.write(`designtools-adherence: ${message}
|
|
44
|
+
`);
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
47
|
+
var args;
|
|
48
|
+
function parse() {
|
|
49
|
+
return parseArgs({
|
|
50
|
+
allowPositionals: false,
|
|
51
|
+
options: {
|
|
52
|
+
manifest: { type: "string" },
|
|
53
|
+
include: { type: "string", multiple: true },
|
|
54
|
+
exclude: { type: "string", multiple: true },
|
|
55
|
+
format: { type: "string" },
|
|
56
|
+
json: { type: "string" },
|
|
57
|
+
root: { type: "string" },
|
|
58
|
+
timings: { type: "boolean" },
|
|
59
|
+
help: { type: "boolean", short: "h" },
|
|
60
|
+
version: { type: "boolean", short: "v" }
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
args = parse();
|
|
66
|
+
} catch (e) {
|
|
67
|
+
fail(`${e.message}
|
|
68
|
+
|
|
69
|
+
${HELP}`);
|
|
70
|
+
}
|
|
71
|
+
var { values } = args;
|
|
72
|
+
if (values.help) {
|
|
73
|
+
process.stdout.write(HELP);
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
if (values.version) {
|
|
77
|
+
process.stdout.write(`${VERSION}
|
|
78
|
+
`);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
var format = values.format ?? "md";
|
|
82
|
+
if (format !== "md" && format !== "json") fail(`--format must be md or json`);
|
|
83
|
+
var config;
|
|
84
|
+
try {
|
|
85
|
+
config = loadConfig({ root: values.root, manifest: values.manifest, include: values.include, exclude: values.exclude });
|
|
86
|
+
} catch (e) {
|
|
87
|
+
fail(e.message);
|
|
88
|
+
}
|
|
89
|
+
var built;
|
|
90
|
+
try {
|
|
91
|
+
built = await buildReport(config);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
if (e instanceof UsageError) fail(e.message);
|
|
94
|
+
process.stderr.write(`designtools-adherence: could not finish the report: ${e.message}
|
|
95
|
+
`);
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
var json = serialise(built.report);
|
|
99
|
+
if (values.json) {
|
|
100
|
+
const path = resolve(values.json);
|
|
101
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
102
|
+
writeFileSync(path, json);
|
|
103
|
+
}
|
|
104
|
+
process.stdout.write(format === "json" ? json : formatMarkdown(built.report));
|
|
105
|
+
if (values.timings) {
|
|
106
|
+
const t = built.timings;
|
|
107
|
+
const ms = (n) => `${Math.round(n)}ms`;
|
|
108
|
+
process.stderr.write(
|
|
109
|
+
`timings: files ${ms(t.files)}, components ${ms(t.components)}, discipline ${ms(t.discipline)}, total ${ms(t.total)} (${built.report.summary.files} files)
|
|
110
|
+
`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
process.exit(0);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration: `designtools.json` at the project root, overridden by flags.
|
|
3
|
+
*
|
|
4
|
+
* {
|
|
5
|
+
* "manifest": { "system": "src/ds" },
|
|
6
|
+
* "adherence": { "include": ["app/**", "src/**"], "exclude": ["app/legacy/**"] }
|
|
7
|
+
* }
|
|
8
|
+
*
|
|
9
|
+
* The manifest section says where components.json is (`out`, or `<system>/manifest`),
|
|
10
|
+
* which is all adherence needs: the system's components, sources and replaced
|
|
11
|
+
* elements all come from there.
|
|
12
|
+
*/
|
|
13
|
+
declare const CONFIG_FILE = "designtools.json";
|
|
14
|
+
/** The report cannot run as asked: no manifest, an unreadable one, a bad setting. The CLI exits 2. */
|
|
15
|
+
declare class UsageError extends Error {
|
|
16
|
+
}
|
|
17
|
+
/** Product source, by default: the app router and src, whatever the system folder is. */
|
|
18
|
+
declare const DEFAULT_INCLUDE: string[];
|
|
19
|
+
/**
|
|
20
|
+
* Never product, whatever `--include` says: the system folder is added at run
|
|
21
|
+
* time, the docs pages, tests, fixtures, examples, stories and type declarations.
|
|
22
|
+
*/
|
|
23
|
+
declare const DEFAULT_EXCLUDE: string[];
|
|
24
|
+
interface AdherenceConfig {
|
|
25
|
+
/** Project root, absolute. Every other path is relative to it. */
|
|
26
|
+
root: string;
|
|
27
|
+
/** components.json, relative to root (or absolute when it lives outside it). */
|
|
28
|
+
manifest: string;
|
|
29
|
+
/** tokens.json beside it; absent when there is none. */
|
|
30
|
+
tokens?: string;
|
|
31
|
+
/** tsconfig.json for path aliases; found at the root when absent. */
|
|
32
|
+
tsconfig?: string;
|
|
33
|
+
include: string[];
|
|
34
|
+
/** The defaults, the system folder, and anything added. */
|
|
35
|
+
exclude: string[];
|
|
36
|
+
}
|
|
37
|
+
interface ConfigFlags {
|
|
38
|
+
root?: string;
|
|
39
|
+
/** A components.json to read instead of the one designtools.json points at. */
|
|
40
|
+
manifest?: string;
|
|
41
|
+
include?: string[];
|
|
42
|
+
exclude?: string[];
|
|
43
|
+
}
|
|
44
|
+
declare function loadConfig(flags: ConfigFlags): AdherenceConfig;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The adherence report: what `designtools-adherence` writes with `--json`, and
|
|
48
|
+
* what the "Adherence summary" docs block renders.
|
|
49
|
+
*
|
|
50
|
+
* This file has no imports on purpose, so @designtools/blocks can carry a copy
|
|
51
|
+
* and read the report through the same types the tool wrote it with.
|
|
52
|
+
*
|
|
53
|
+
* Every path is relative to the project root, with forward slashes. Lines and
|
|
54
|
+
* columns start at 1. Every array is sorted (files and routes by path, findings
|
|
55
|
+
* by line then column, components and elements by name), and there are no
|
|
56
|
+
* timestamps, so the same source gives the same bytes.
|
|
57
|
+
*/
|
|
58
|
+
declare const ADHERENCE_SCHEMA = "designtools.adherence/1";
|
|
59
|
+
interface AdherenceReport {
|
|
60
|
+
/** `@designtools/adherence@<version>`. */
|
|
61
|
+
generator: string;
|
|
62
|
+
schema: string;
|
|
63
|
+
/** The components.json the system was read from. */
|
|
64
|
+
manifest: string;
|
|
65
|
+
/** The system folder, which is never scanned as product. */
|
|
66
|
+
system: string;
|
|
67
|
+
/** The globs that chose the product files, as given or defaulted. */
|
|
68
|
+
include: string[];
|
|
69
|
+
exclude: string[];
|
|
70
|
+
summary: Summary;
|
|
71
|
+
/** One per Next app router page (`app/**\/page.tsx`), sorted by route. */
|
|
72
|
+
routes: RouteEntry[];
|
|
73
|
+
/** Every scanned file with at least one use, raw element or finding, sorted by path. */
|
|
74
|
+
files: FileEntry[];
|
|
75
|
+
/** Every system component, used or not, sorted by name. */
|
|
76
|
+
components: ComponentUsage[];
|
|
77
|
+
/** Every raw element some component `replaces`, sorted by selector. */
|
|
78
|
+
elements: ElementUsage[];
|
|
79
|
+
/** Files a scanner could not parse, sorted by path. Their numbers are missing from the report. */
|
|
80
|
+
skipped: SkippedFile[];
|
|
81
|
+
}
|
|
82
|
+
/** The numbers every level of the report shares: the whole product, a route, a file. */
|
|
83
|
+
interface Counts {
|
|
84
|
+
/** JSX uses of system components, sub-components included. */
|
|
85
|
+
systemUses: number;
|
|
86
|
+
/** Raw elements that some system component `replaces`, such as `<button>` when Button replaces it. */
|
|
87
|
+
rawElements: number;
|
|
88
|
+
/**
|
|
89
|
+
* systemUses ÷ (systemUses + rawElements), from 0 to 1, to four decimal places.
|
|
90
|
+
* Null when both are 0: there was nothing to measure.
|
|
91
|
+
*/
|
|
92
|
+
share: number | null;
|
|
93
|
+
/** Classes that bypass the tokens: palette steps and arbitrary values. */
|
|
94
|
+
offSystemValues: number;
|
|
95
|
+
/** System components given a className that changes colour, spacing or radius. */
|
|
96
|
+
overrides: number;
|
|
97
|
+
}
|
|
98
|
+
interface Summary extends Counts {
|
|
99
|
+
/** Product files scanned. */
|
|
100
|
+
files: number;
|
|
101
|
+
/** Files with at least one raw element, off-system value or override. */
|
|
102
|
+
filesWithFindings: number;
|
|
103
|
+
offSystem: Record<OffSystemKind, number>;
|
|
104
|
+
overrideKinds: Record<OverrideKind, number>;
|
|
105
|
+
/** System components used at least once. */
|
|
106
|
+
componentsUsed: number;
|
|
107
|
+
/** System components never used in the product, by name. */
|
|
108
|
+
componentsUnused: string[];
|
|
109
|
+
/**
|
|
110
|
+
* The raw elements counted, from every component's `replaces`. Empty means no
|
|
111
|
+
* component declares any, so `share` measures nothing yet.
|
|
112
|
+
*/
|
|
113
|
+
replaceable: string[];
|
|
114
|
+
}
|
|
115
|
+
interface RouteEntry {
|
|
116
|
+
/** The URL path: route groups and parallel-route slots removed, dynamic segments kept. `/companies/[key]`. */
|
|
117
|
+
route: string;
|
|
118
|
+
/** The page file. */
|
|
119
|
+
page: string;
|
|
120
|
+
/**
|
|
121
|
+
* The files counted for the route: the page, files beside it, and files in
|
|
122
|
+
* folders under it that lead to no other page. Layouts shared by several
|
|
123
|
+
* routes are counted per file only.
|
|
124
|
+
*/
|
|
125
|
+
files: string[];
|
|
126
|
+
counts: Counts;
|
|
127
|
+
}
|
|
128
|
+
interface FileEntry {
|
|
129
|
+
file: string;
|
|
130
|
+
/** The route the file is counted under, when it has one. */
|
|
131
|
+
route?: string;
|
|
132
|
+
counts: Counts;
|
|
133
|
+
uses: ComponentUse[];
|
|
134
|
+
raw: RawElement[];
|
|
135
|
+
offSystem: OffSystemValue[];
|
|
136
|
+
overrides: Override[];
|
|
137
|
+
}
|
|
138
|
+
/** A literal prop value, or the kind of expression it was, e.g. `(Identifier)`. */
|
|
139
|
+
type PropValue = string | number | boolean | null;
|
|
140
|
+
interface ComponentUse {
|
|
141
|
+
/** The component's name in the manifest. */
|
|
142
|
+
component: string;
|
|
143
|
+
/** The element as written, when it differs: `Select.Trigger`, `DS.Button`. */
|
|
144
|
+
as?: string;
|
|
145
|
+
line: number;
|
|
146
|
+
column: number;
|
|
147
|
+
/** Props as written, sorted. A className built from expressions keeps its literal classes only. */
|
|
148
|
+
props: Record<string, PropValue>;
|
|
149
|
+
/** True when it also takes `{...spread}` props, whose names are unknown. */
|
|
150
|
+
spread: boolean;
|
|
151
|
+
}
|
|
152
|
+
interface RawElement {
|
|
153
|
+
/** The tag as written: `button`. */
|
|
154
|
+
element: string;
|
|
155
|
+
/** The `replaces` selector it matched: `button`, `input[type=checkbox]`. */
|
|
156
|
+
selector: string;
|
|
157
|
+
/** The components that replace it, most specific first. */
|
|
158
|
+
replacedBy: string[];
|
|
159
|
+
line: number;
|
|
160
|
+
column: number;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* `palette`: a primitive ramp step used directly, `bg-blue-500` or `text-primary-500`.
|
|
164
|
+
* `arbitrary`: a value in brackets, `p-[13px]` or `bg-[#fff]`.
|
|
165
|
+
* `status-text`: a status fill used as text, `text-destructive`: use `text-destructive-subdued-foreground`,
|
|
166
|
+
* which reads on every background, or neutral text with an icon for a warning.
|
|
167
|
+
*/
|
|
168
|
+
type OffSystemKind = "palette" | "arbitrary" | "status-text";
|
|
169
|
+
interface OffSystemValue {
|
|
170
|
+
/** The class as written, variants included: `hover:bg-blue-500`. */
|
|
171
|
+
class: string;
|
|
172
|
+
kind: OffSystemKind;
|
|
173
|
+
line: number;
|
|
174
|
+
column: number;
|
|
175
|
+
}
|
|
176
|
+
type OverrideKind = "colour" | "spacing" | "radius";
|
|
177
|
+
interface Override {
|
|
178
|
+
component: string;
|
|
179
|
+
/** Each class in the className that overrides the component, with what it changes. */
|
|
180
|
+
classes: {
|
|
181
|
+
class: string;
|
|
182
|
+
kind: OverrideKind;
|
|
183
|
+
}[];
|
|
184
|
+
line: number;
|
|
185
|
+
column: number;
|
|
186
|
+
}
|
|
187
|
+
interface ComponentUsage {
|
|
188
|
+
name: string;
|
|
189
|
+
source: string;
|
|
190
|
+
uses: number;
|
|
191
|
+
/** Files it is used in. */
|
|
192
|
+
files: number;
|
|
193
|
+
/** How many uses pass each prop, by prop name. */
|
|
194
|
+
props: Record<string, number>;
|
|
195
|
+
/**
|
|
196
|
+
* Per variant axis, how many uses pick each option. A use that leaves the prop
|
|
197
|
+
* out counts under the axis's default, or `(unset)`; one that computes it
|
|
198
|
+
* counts under `(dynamic)`.
|
|
199
|
+
*/
|
|
200
|
+
variants?: Record<string, Record<string, number>>;
|
|
201
|
+
}
|
|
202
|
+
interface ElementUsage {
|
|
203
|
+
/** The `replaces` selector. */
|
|
204
|
+
selector: string;
|
|
205
|
+
/** Raw uses in the product. */
|
|
206
|
+
uses: number;
|
|
207
|
+
files: number;
|
|
208
|
+
replacedBy: string[];
|
|
209
|
+
}
|
|
210
|
+
interface SkippedFile {
|
|
211
|
+
file: string;
|
|
212
|
+
reason: string;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Build the adherence report in memory. Nothing here writes to disk; nothing
|
|
217
|
+
* here decides an exit code. Every finding is a warning.
|
|
218
|
+
*/
|
|
219
|
+
|
|
220
|
+
declare const GENERATOR_NAME = "@designtools/adherence";
|
|
221
|
+
declare const VERSION: string;
|
|
222
|
+
declare const GENERATOR: string;
|
|
223
|
+
interface Timings {
|
|
224
|
+
files: number;
|
|
225
|
+
components: number;
|
|
226
|
+
discipline: number;
|
|
227
|
+
total: number;
|
|
228
|
+
}
|
|
229
|
+
interface BuiltReport {
|
|
230
|
+
report: AdherenceReport;
|
|
231
|
+
/** Milliseconds per stage, for the CLI's `--timings`; never part of the report. */
|
|
232
|
+
timings: Timings;
|
|
233
|
+
}
|
|
234
|
+
declare function buildReport(config: AdherenceConfig): Promise<BuiltReport>;
|
|
235
|
+
declare function counts(systemUses: number, rawElements: number, offSystemValues: number, overrides: number): Counts;
|
|
236
|
+
/** The report as committed or posted: two-space JSON with a trailing newline. */
|
|
237
|
+
declare function serialise(report: AdherenceReport): string;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The report as markdown, for a pull request comment. Kept under GitHub's
|
|
241
|
+
* comment limit: past it, files are cut and the JSON report has the rest.
|
|
242
|
+
*/
|
|
243
|
+
|
|
244
|
+
declare function formatMarkdown(report: AdherenceReport): string;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Which files are product: walk from each include glob's base, keep what an
|
|
248
|
+
* include matches and no exclude does. One list feeds both scanners, so they
|
|
249
|
+
* always agree on what was looked at.
|
|
250
|
+
*/
|
|
251
|
+
declare function listProductFiles(root: string, includeGlobs: string[], excludeGlobs: string[]): string[];
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Next app router pages to routes, and product files to the route they count under.
|
|
255
|
+
*
|
|
256
|
+
* A page is `app/**\/page.{tsx,jsx,ts,js,mdx}` (or under `src/app`). Its route
|
|
257
|
+
* drops route groups `(group)`, parallel-route slots `@slot` and the `(.)`
|
|
258
|
+
* of an intercepting segment, and keeps dynamic segments as written.
|
|
259
|
+
*
|
|
260
|
+
* A file counts under the nearest page above it, as long as no folder on the
|
|
261
|
+
* way leads to another page: a page's own
|
|
262
|
+
* components, private `_folders` and loading or error files count with it;
|
|
263
|
+
* a layout in a group folder shared by several pages counts per file only.
|
|
264
|
+
*/
|
|
265
|
+
interface Routes {
|
|
266
|
+
/** Page file → route. */
|
|
267
|
+
pages: Map<string, string>;
|
|
268
|
+
/** Product file → the page it counts under. */
|
|
269
|
+
owner: Map<string, string>;
|
|
270
|
+
}
|
|
271
|
+
declare function mapRoutes(files: string[]): Routes;
|
|
272
|
+
/** `/(app)/companies/[key]` → `/companies/[key]`; `` → `/`. */
|
|
273
|
+
declare function routeOf(dir: string): string;
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Resolve an import to a project file, the way the project's bundler would for
|
|
277
|
+
* the cases that matter here: relative paths and tsconfig `paths` aliases
|
|
278
|
+
* (`@/components/button`, `@ds/button`), with or without an extension, or a
|
|
279
|
+
* folder's index. A package import resolves to nothing: the system is a folder
|
|
280
|
+
* in the project, and the manifest says which.
|
|
281
|
+
*/
|
|
282
|
+
type Resolver = (moduleName: string, fromFile: string) => string | undefined;
|
|
283
|
+
/** A resolver for files in `root`, returning paths relative to it; cached per import. */
|
|
284
|
+
declare function createResolver(root: string, tsconfig?: string): Resolver;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The one place react-scanner is used, so it can be swapped: it has had no
|
|
288
|
+
* release since October 2024 and pins TypeScript 5.6 for its parser. Anything
|
|
289
|
+
* that produces a `RawReport` for a list of files can stand in for it.
|
|
290
|
+
*
|
|
291
|
+
* react-scanner is run over every JSX element (no `components` or
|
|
292
|
+
* `importedFrom` filter), sub-components included, so intrinsic elements
|
|
293
|
+
* like `<button>` come back too. Its raw report is narrowed to the files asked
|
|
294
|
+
* for, made relative to the root, and flattened into instances.
|
|
295
|
+
*
|
|
296
|
+
* Two of its habits are contained here: it prints "Scanned N files" and
|
|
297
|
+
* "Failed to parse" to the console, which would corrupt a report written to
|
|
298
|
+
* stdout, and it throws when a folder has no files.
|
|
299
|
+
*/
|
|
300
|
+
/** react-scanner's raw report: component name → instances, with nested sub-components. */
|
|
301
|
+
interface RawReport {
|
|
302
|
+
[name: string]: RawReportEntry;
|
|
303
|
+
}
|
|
304
|
+
interface RawReportEntry {
|
|
305
|
+
instances?: RawInstance[];
|
|
306
|
+
components?: RawReport;
|
|
307
|
+
}
|
|
308
|
+
interface RawInstance {
|
|
309
|
+
importInfo?: {
|
|
310
|
+
imported?: string;
|
|
311
|
+
local: string;
|
|
312
|
+
moduleName: string;
|
|
313
|
+
importType: string;
|
|
314
|
+
};
|
|
315
|
+
props: Record<string, unknown>;
|
|
316
|
+
propsSpread: boolean;
|
|
317
|
+
location: {
|
|
318
|
+
file: string;
|
|
319
|
+
start: {
|
|
320
|
+
line: number;
|
|
321
|
+
column: number;
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/** One JSX element, flattened out of the raw report. */
|
|
326
|
+
interface Instance {
|
|
327
|
+
/** As written: `button`, `Button`, `Select.Trigger`. */
|
|
328
|
+
name: string;
|
|
329
|
+
file: string;
|
|
330
|
+
line: number;
|
|
331
|
+
/** From 1. */
|
|
332
|
+
column: number;
|
|
333
|
+
importInfo?: RawInstance["importInfo"];
|
|
334
|
+
props: Record<string, unknown>;
|
|
335
|
+
spread: boolean;
|
|
336
|
+
}
|
|
337
|
+
interface ScanResult {
|
|
338
|
+
report: RawReport;
|
|
339
|
+
instances: Instance[];
|
|
340
|
+
/** Files the parser gave up on, relative. */
|
|
341
|
+
failed: string[];
|
|
342
|
+
}
|
|
343
|
+
/** Scan `files` (relative to `root`) for JSX elements. */
|
|
344
|
+
declare function scanJsx(root: string, files: string[]): Promise<ScanResult>;
|
|
345
|
+
declare function flatten(report: RawReport, prefix?: string): Instance[];
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Component use, from react-scanner's instances: which elements are system
|
|
349
|
+
* components and which are raw elements a system component replaces.
|
|
350
|
+
*
|
|
351
|
+
* Which imports are the system comes from the manifest alone: an import that
|
|
352
|
+
* resolves (relative, or through a tsconfig alias) to a component's source
|
|
353
|
+
* file is that component; one that resolves to another file in the system
|
|
354
|
+
* folder, such as a barrel `index.ts`, is matched by export name.
|
|
355
|
+
*/
|
|
356
|
+
|
|
357
|
+
/** The parts of a components.json entry this reads. Accepts the manifest's schema 1 and `designtools.manifest/1`. */
|
|
358
|
+
interface ManifestComponent {
|
|
359
|
+
name: string;
|
|
360
|
+
export: string;
|
|
361
|
+
source: string;
|
|
362
|
+
replaces?: string[];
|
|
363
|
+
variants?: {
|
|
364
|
+
axes: {
|
|
365
|
+
name: string;
|
|
366
|
+
default?: string;
|
|
367
|
+
options: {
|
|
368
|
+
name: string;
|
|
369
|
+
}[];
|
|
370
|
+
}[];
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
interface ComponentsFile {
|
|
374
|
+
system: string;
|
|
375
|
+
components: ManifestComponent[];
|
|
376
|
+
}
|
|
377
|
+
/** A `replaces` entry: `button`, `input[type=checkbox]`, `a[href]`. */
|
|
378
|
+
interface Selector {
|
|
379
|
+
text: string;
|
|
380
|
+
tag: string;
|
|
381
|
+
attrs: {
|
|
382
|
+
name: string;
|
|
383
|
+
value?: string;
|
|
384
|
+
}[];
|
|
385
|
+
}
|
|
386
|
+
declare function parseSelector(text: string): Selector | undefined;
|
|
387
|
+
interface Classified {
|
|
388
|
+
uses: Map<string, (ComponentUse & {
|
|
389
|
+
entry: ManifestComponent;
|
|
390
|
+
})[]>;
|
|
391
|
+
raw: Map<string, RawElement[]>;
|
|
392
|
+
/** Selector → components that replace it, most specific selectors first. */
|
|
393
|
+
replaceable: Map<string, {
|
|
394
|
+
selector: Selector;
|
|
395
|
+
components: string[];
|
|
396
|
+
}>;
|
|
397
|
+
}
|
|
398
|
+
declare function classify(instances: Instance[], manifest: ComponentsFile, resolve: Resolver): Classified;
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Overrides: a className on a system component that changes its colour,
|
|
402
|
+
* spacing or radius. That usually means someone fought the component instead
|
|
403
|
+
* of picking a variant, or the component is missing one.
|
|
404
|
+
*
|
|
405
|
+
* Reads the className react-scanner recorded for each system component use
|
|
406
|
+
* (the literal classes, including those inside `cn()`), and classifies each
|
|
407
|
+
* class by its utility, with variants and `!` stripped:
|
|
408
|
+
* colour bg-, text-, border-, ring-, fill- … with a colour value: a token
|
|
409
|
+
* colour from tokens.json, a palette step, a Tailwind keyword
|
|
410
|
+
* colour, or a bracketed colour
|
|
411
|
+
* spacing p-, m-, gap-, space- in any direction
|
|
412
|
+
* radius rounded, rounded-*
|
|
413
|
+
* Layout, size and type classes (`w-full`, `flex-1`, `text-sm`) are left
|
|
414
|
+
* alone. Margins count as spacing: they are often placement, but they are
|
|
415
|
+
* also how a component's own rhythm gets fought.
|
|
416
|
+
*/
|
|
417
|
+
|
|
418
|
+
interface OverrideContext {
|
|
419
|
+
/** Colour names usable as a utility value: `primary`, `primary-subdued`, `canvas-foreground`. */
|
|
420
|
+
colours: Set<string>;
|
|
421
|
+
/** Palette families: Tailwind's and the tokens' ramps. */
|
|
422
|
+
families: string[];
|
|
423
|
+
}
|
|
424
|
+
/** Colour names from tokens.json: every `--color-<name>`. */
|
|
425
|
+
declare function colourContext(tokens: {
|
|
426
|
+
name: string;
|
|
427
|
+
}[], families: string[]): OverrideContext;
|
|
428
|
+
declare function findOverrides(uses: ComponentUse[], ctx: OverrideContext): Override[];
|
|
429
|
+
declare function overrideKind(cls: string, ctx: OverrideContext): OverrideKind | undefined;
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Token discipline: classes that bypass the system's tokens, found by
|
|
433
|
+
* eslint-plugin-better-tailwindcss's `no-restricted-classes` run through
|
|
434
|
+
* ESLint's Node API with an in-memory flat config. Nothing of the project's
|
|
435
|
+
* own ESLint setup is read, and nothing is written.
|
|
436
|
+
*
|
|
437
|
+
* The restrictions are generated, not configured:
|
|
438
|
+
* palette a primitive ramp step used as a utility, `bg-blue-500` or
|
|
439
|
+
* `text-primary-500`: any `--color-<family>-<step>` in tokens.json,
|
|
440
|
+
* and Tailwind's default palette, whose steps a project can
|
|
441
|
+
* reach even when it never meant to.
|
|
442
|
+
* arbitrary a value in brackets, `p-[13px]`, `bg-[#fff]`, `[mask-type:alpha]`.
|
|
443
|
+
* `[var(--token)]` is a token, so it passes, unless the token is
|
|
444
|
+
* a palette step.
|
|
445
|
+
*
|
|
446
|
+
* The plugin finds class strings wherever Tailwind classes live: className,
|
|
447
|
+
* cn(), clsx(), tv(), cva() and the like.
|
|
448
|
+
*/
|
|
449
|
+
|
|
450
|
+
/** Tailwind v4's default palette families. */
|
|
451
|
+
declare const TAILWIND_FAMILIES: string[];
|
|
452
|
+
/** Utilities that take a colour. */
|
|
453
|
+
declare const COLOUR_UTILITIES: string[];
|
|
454
|
+
interface Restriction {
|
|
455
|
+
pattern: string;
|
|
456
|
+
message: string;
|
|
457
|
+
}
|
|
458
|
+
/** The `no-restricted-classes` options for a set of palette families. */
|
|
459
|
+
declare function restrictionsFor(families: string[]): Restriction[];
|
|
460
|
+
/** Ramp families in tokens.json: `--color-primary-500` → `primary`. */
|
|
461
|
+
declare function familiesFromTokens(tokens: {
|
|
462
|
+
name: string;
|
|
463
|
+
}[]): string[];
|
|
464
|
+
interface DisciplineResult {
|
|
465
|
+
values: Map<string, OffSystemValue[]>;
|
|
466
|
+
failed: {
|
|
467
|
+
file: string;
|
|
468
|
+
reason: string;
|
|
469
|
+
}[];
|
|
470
|
+
}
|
|
471
|
+
declare function checkDiscipline(root: string, files: string[], families: string[]): Promise<DisciplineResult>;
|
|
472
|
+
|
|
473
|
+
export { ADHERENCE_SCHEMA, type AdherenceConfig, type AdherenceReport, type BuiltReport, COLOUR_UTILITIES, CONFIG_FILE, type ComponentUsage, type ComponentUse, type ComponentsFile, type ConfigFlags, type Counts, DEFAULT_EXCLUDE, DEFAULT_INCLUDE, type DisciplineResult, type ElementUsage, type FileEntry, GENERATOR, GENERATOR_NAME, type Instance, type ManifestComponent, type OffSystemKind, type OffSystemValue, type Override, type OverrideContext, type OverrideKind, type PropValue, type RawElement, type RawInstance, type RawReport, type RawReportEntry, type Resolver, type Restriction, type RouteEntry, type ScanResult, type Selector, type SkippedFile, type Summary, TAILWIND_FAMILIES, type Timings, UsageError, VERSION, buildReport, checkDiscipline, classify, colourContext, counts, createResolver, familiesFromTokens, findOverrides, flatten, formatMarkdown, listProductFiles, loadConfig, mapRoutes, overrideKind, parseSelector, restrictionsFor, routeOf, scanJsx, serialise };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ADHERENCE_SCHEMA,
|
|
3
|
+
COLOUR_UTILITIES,
|
|
4
|
+
CONFIG_FILE,
|
|
5
|
+
DEFAULT_EXCLUDE,
|
|
6
|
+
DEFAULT_INCLUDE,
|
|
7
|
+
GENERATOR,
|
|
8
|
+
GENERATOR_NAME,
|
|
9
|
+
TAILWIND_FAMILIES,
|
|
10
|
+
UsageError,
|
|
11
|
+
VERSION,
|
|
12
|
+
buildReport,
|
|
13
|
+
checkDiscipline,
|
|
14
|
+
classify,
|
|
15
|
+
colourContext,
|
|
16
|
+
counts,
|
|
17
|
+
createResolver,
|
|
18
|
+
familiesFromTokens,
|
|
19
|
+
findOverrides,
|
|
20
|
+
flatten,
|
|
21
|
+
formatMarkdown,
|
|
22
|
+
listProductFiles,
|
|
23
|
+
loadConfig,
|
|
24
|
+
mapRoutes,
|
|
25
|
+
overrideKind,
|
|
26
|
+
parseSelector,
|
|
27
|
+
restrictionsFor,
|
|
28
|
+
routeOf,
|
|
29
|
+
scanJsx,
|
|
30
|
+
serialise
|
|
31
|
+
} from "./chunk-7FM4ZQLW.js";
|
|
32
|
+
export {
|
|
33
|
+
ADHERENCE_SCHEMA,
|
|
34
|
+
COLOUR_UTILITIES,
|
|
35
|
+
CONFIG_FILE,
|
|
36
|
+
DEFAULT_EXCLUDE,
|
|
37
|
+
DEFAULT_INCLUDE,
|
|
38
|
+
GENERATOR,
|
|
39
|
+
GENERATOR_NAME,
|
|
40
|
+
TAILWIND_FAMILIES,
|
|
41
|
+
UsageError,
|
|
42
|
+
VERSION,
|
|
43
|
+
buildReport,
|
|
44
|
+
checkDiscipline,
|
|
45
|
+
classify,
|
|
46
|
+
colourContext,
|
|
47
|
+
counts,
|
|
48
|
+
createResolver,
|
|
49
|
+
familiesFromTokens,
|
|
50
|
+
findOverrides,
|
|
51
|
+
flatten,
|
|
52
|
+
formatMarkdown,
|
|
53
|
+
listProductFiles,
|
|
54
|
+
loadConfig,
|
|
55
|
+
mapRoutes,
|
|
56
|
+
overrideKind,
|
|
57
|
+
parseSelector,
|
|
58
|
+
restrictionsFor,
|
|
59
|
+
routeOf,
|
|
60
|
+
scanJsx,
|
|
61
|
+
serialise
|
|
62
|
+
};
|