@designtools/manifest 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +9 -4
- package/dist/{chunk-EFGVJ5ZQ.js → chunk-PQGMDEUF.js} +181 -40
- package/dist/cli.js +6 -4
- package/dist/index.d.ts +43 -7
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @designtools/manifest
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3da728f: From the first end-to-end run:
|
|
8
|
+
|
|
9
|
+
- A comment after a declaration on the same line (`--size-xl: 3rem; /* 48px controls */`) now describes that declaration. It used to become the next token's description, so every description in a scale written that way landed one token late.
|
|
10
|
+
- A token re-declared in a later stylesheet records it in `overriddenIn`.
|
|
11
|
+
- Each component records `import`, the specifier an app imports it by through the tsconfig path alias that reaches the system folder (`@ds/button/button`), so the import line in the docs and the agent markdown resolves.
|
|
12
|
+
- Brand assets keep the order `assets.json` gives them, so the default logo leads.
|
|
13
|
+
- `prose-check` reads the places listed in `manifest.prose` (a folder contributes its markdown and `*content.ts(x)` files), treats a TypeScript template literal as prose rather than code, and honours `prose-check-ignore`, `prose-check-ignore-next-line` and `prose-check-ignore-start` … `-end` for pages that name wrong forms on purpose.
|
|
14
|
+
- A prop's JSDoc no longer leaks into other components. react-docgen-typescript caches a prop by the file of its first declaration, so Alert's documented `children` (first declared by React) gave its description, type and declarations to every component with `children`. Each component now reads its props afresh.
|
|
15
|
+
- `agents` writes to `AGENTS.md` when `CLAUDE.md` imports it (`@AGENTS.md`, as `next dev` writes it), so every agent reads the section; `manifest.agents` in `designtools.json` names another file. The section no longer forbids every ramp step: ramps are for charts, illustration and a decorative colour, never text, surfaces or actions.
|
|
16
|
+
- `manifest.usage` names the file `@designtools/tokens --usage` writes; every colour that fails somewhere as text or a shape becomes a rule in rules.json (`usage-<colour>`), marked with the new status `measured`: a fact from the tokens, not a client decision. Status fills are left to the default status-text rule.
|
|
17
|
+
|
|
3
18
|
## 0.1.0
|
|
4
19
|
|
|
5
20
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -28,6 +28,8 @@ Record the system folder and the token stylesheets once, in `designtools.json` a
|
|
|
28
28
|
|
|
29
29
|
The manifest is written inside the system folder (`src/ds/manifest/`), so it moves with the system if the system moves to its own package. Flags override the file: `--system`, `--tokens` (repeat it), `--tsconfig`, `--out`, `--root`.
|
|
30
30
|
|
|
31
|
+
List every stylesheet that declares a token, in the order the app imports them: the generated colour tiers, the authored scale, and wherever the font families live (`base.css` on the suite's default stack). A token re-declared in a later file takes that file's value and records it in `overriddenIn`. `prose` adds places for `prose-check` to read (see [For agents](#for-agents)).
|
|
32
|
+
|
|
31
33
|
## What it reads
|
|
32
34
|
|
|
33
35
|
**Components.** Every exported component in a `.tsx` file under the system folder. Fixtures, tests, stories and `.d.ts` files are skipped. Props come from [react-docgen-typescript](https://github.com/styleguidist/react-docgen-typescript), run once over a single TypeScript program, so types resolve across files and through the project's path aliases. A component's own props are kept, and so are those of a Base UI primitive it wraps. Props inherited from the DOM, ARIA or React are left out, and recorded as what it `inherits` (`"span"`, `"@base-ui/react/select:Select.Root"`).
|
|
@@ -60,7 +62,7 @@ The manifest is written inside the system folder (`src/ds/manifest/`), so it mov
|
|
|
60
62
|
|
|
61
63
|
Any other tag is a warning, with a suggestion when it is close to one of these.
|
|
62
64
|
|
|
63
|
-
**Tokens.** Every custom property in the listed stylesheets: the colour tiers `@designtools/tokens` generates and the scale you author beside them. Each token records its value in every context it is declared in: `default` (`@theme`, `:root`), `light`, `dark`, `p3:` for the wide-gamut layer, and any other selector as written. A `prefers-color-scheme` fallback never overrides an explicit dark block. The comment above a declaration, or above the run of declarations it starts, becomes its description. Tiers follow Tailwind's namespaces (`--text-*`, `--radius-*`, …); semantic colours like `--primary` are recognised by their values.
|
|
65
|
+
**Tokens.** Every custom property in the listed stylesheets: the colour tiers `@designtools/tokens` generates and the scale you author beside them. Each token records its value in every context it is declared in: `default` (`@theme`, `:root`), `light`, `dark`, `p3:` for the wide-gamut layer, and any other selector as written. A `prefers-color-scheme` fallback never overrides an explicit dark block. The comment above a declaration, or above the run of declarations it starts, becomes its description; a comment after a declaration on the same line describes that declaration alone. Tiers follow Tailwind's namespaces (`--text-*`, `--radius-*`, …); semantic colours like `--primary` are recognised by their values.
|
|
64
66
|
|
|
65
67
|
**Rules.** One `<id>.rule.json` per rule in `<system>/rules`, brand and interface in the same format: title, statement, rationale, threshold, what it applies to, exceptions, the tokens it rests on, whether it is `confirmed` or still an `assumption`, and the check that proves it. [`schemas/rule.schema.json`](schemas/rule.schema.json) validates them in an editor.
|
|
66
68
|
|
|
@@ -68,7 +70,9 @@ Any other tag is a warning, with a suggestion when it is close to one of these.
|
|
|
68
70
|
|
|
69
71
|
**Taxonomy.** The canonical names come from everything else: components, variant axes and their meaningful options, semantic colours, patterns and rules. `<system>/taxonomy.json` adds the wrong forms (`{ "name": "destructive", "wrong": ["error", "danger"] }`) and product terms the code never names (`{ "name": "sign in", "kind": "product", "wrong": ["log in"] }`).
|
|
70
72
|
|
|
71
|
-
**
|
|
73
|
+
**Measured usage rules.** With `manifest.usage` naming the file `@designtools/tokens --usage` writes, every colour that fails somewhere, as text (4.5:1 at AA) or as a shape (3:1), gets a rule in rules.json: `usage-primary`, "`--primary` reads as text on … On … it is neither, down to 1.06:1 …". They are marked `measured`, a fact from the tokens that changes when they do, and sit beside the authored rules on the rules page. A brand colour's limits are then a brand decision, written as a confirmed brand rule.
|
|
74
|
+
|
|
75
|
+
**Brand assets.** Every image in `<system>/brand`, with an `assets.json` there saying how to use each: kind, usage, minimum size, clear space, the surfaces it may sit on, alt text. SVG and PNG sizes are read from the files. They keep the order `assets.json` gives them, so the default logo can lead. A file the index does not mention is listed anyway, after them, with a warning.
|
|
72
76
|
|
|
73
77
|
## The files
|
|
74
78
|
|
|
@@ -84,6 +88,7 @@ Any other tag is a warning, with a suggestion when it is close to one of these.
|
|
|
84
88
|
"name": "Badge",
|
|
85
89
|
"export": "Badge",
|
|
86
90
|
"source": "src/ds/badge/badge.tsx",
|
|
91
|
+
"import": "@ds/badge/badge",
|
|
87
92
|
"description": "A pill: soft tinted background, strong text of the same hue.",
|
|
88
93
|
"inherits": ["span"],
|
|
89
94
|
"props": {
|
|
@@ -149,9 +154,9 @@ Components are matched by their path inside the system folder, so moving the who
|
|
|
149
154
|
|
|
150
155
|
## For agents
|
|
151
156
|
|
|
152
|
-
`agents` writes a section of `CLAUDE.md`
|
|
157
|
+
`agents` writes a section of `CLAUDE.md` or `AGENTS.md` between markers, and leaves the rest of the file alone. It picks `AGENTS.md` when `CLAUDE.md` imports it (`@AGENTS.md`, as `next dev` writes it), so every agent reads the section and Claude reaches it through the import; otherwise `CLAUDE.md` if there is one. `manifest.agents` in `designtools.json`, or `--file`, names another. It points agents at each manifest file and the docs, and lists nothing: a list of components or tokens written into prose is a second copy that drifts.
|
|
153
158
|
|
|
154
|
-
`prose-check` reads `CLAUDE.md`, `AGENTS.md`, `.claude/`, `docs/` and the system folder's markdown, and warns about a component tag the system does not have (`<Modal>`), a wrong form from the taxonomy (`Dropdown` for `Select`, "log in" for "sign in"), and a paragraph or list that restates several components or tokens. The generated section is skipped.
|
|
159
|
+
`prose-check` reads `CLAUDE.md`, `AGENTS.md`, `.claude/`, `docs/` and the system folder's markdown, plus anything listed in `manifest.prose` (a folder there contributes its markdown and its `*content.ts(x)` files, which is where a docs site keeps its editorial copy), and warns about a component tag the system does not have (`<Modal>`), a wrong form from the taxonomy (`Dropdown` for `Select`, "log in" for "sign in"), and a paragraph or list that restates several components or tokens. The generated section is skipped. In TypeScript a backtick opens a template literal, so its words are read as prose, not code. A page that names wrong forms on purpose, such as a voice page saying "never the customer", marks them: `prose-check-ignore` on a line skips it, `prose-check-ignore-next-line` skips the next, and `prose-check-ignore-start` … `prose-check-ignore-end` skip a block, in any comment syntax.
|
|
155
160
|
|
|
156
161
|
`provenance` prints the project's version and the commit it was built at (`VERCEL_GIT_COMMIT_SHA` or `GITHUB_SHA` when set, else git), for the header of a generated docs page. A generated page never carries a review date.
|
|
157
162
|
|
|
@@ -316,6 +316,15 @@ import { basename, dirname, join, relative, resolve, sep } from "path";
|
|
|
316
316
|
import ts4 from "typescript";
|
|
317
317
|
import { createRequire } from "module";
|
|
318
318
|
var docgen = createRequire(import.meta.url)("react-docgen-typescript");
|
|
319
|
+
var parserProto = docgen.Parser.prototype;
|
|
320
|
+
if (!parserProto.__designtoolsUncached) {
|
|
321
|
+
const getPropsInfo = parserProto.getPropsInfo;
|
|
322
|
+
parserProto.getPropsInfo = function(...args) {
|
|
323
|
+
this.propertiesOfPropsCache.clear();
|
|
324
|
+
return getPropsInfo.apply(this, args);
|
|
325
|
+
};
|
|
326
|
+
parserProto.__designtoolsUncached = true;
|
|
327
|
+
}
|
|
319
328
|
var SKIP_FILE = /\.(examples|pattern|test|spec|stories)\.tsx$|\.d\.ts$/;
|
|
320
329
|
var SKIP_DIR = /* @__PURE__ */ new Set(["node_modules", ".next", "dist", "build", "coverage"]);
|
|
321
330
|
var BASE_UI = /[\\/]@base-ui[\\/]react[\\/]/;
|
|
@@ -394,8 +403,37 @@ function readComponents(options, warn) {
|
|
|
394
403
|
attachExamples(root, file, entries, warn);
|
|
395
404
|
out.push(...entries.map((e) => e.entry));
|
|
396
405
|
}
|
|
406
|
+
for (const entry of out) {
|
|
407
|
+
const specifier = importSpecifier(root, entry.source, compiler);
|
|
408
|
+
if (specifier) entry.import = specifier;
|
|
409
|
+
}
|
|
397
410
|
return out.sort((a, b) => compare(a.name, b.name) || compare(a.source, b.source));
|
|
398
411
|
}
|
|
412
|
+
function importSpecifier(root, source, compiler) {
|
|
413
|
+
const paths = compiler.paths;
|
|
414
|
+
if (!paths) return void 0;
|
|
415
|
+
const base = compiler.pathsBasePath ?? compiler.baseUrl ?? root;
|
|
416
|
+
const file = resolve(root, source).replace(/\.(tsx?|jsx?|mts|cts)$/, "").replace(/\/index$/, "");
|
|
417
|
+
let best;
|
|
418
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
419
|
+
for (const target of targets) {
|
|
420
|
+
const star = target.indexOf("*");
|
|
421
|
+
const absolute = resolve(base, star === -1 ? target : target.slice(0, star));
|
|
422
|
+
if (star === -1) {
|
|
423
|
+
if (absolute.replace(/\.(tsx?|jsx?)$/, "").replace(/\/index$/, "") === file && !pattern.includes("*")) {
|
|
424
|
+
return pattern;
|
|
425
|
+
}
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
const prefix = target.slice(0, star).endsWith("/") ? `${absolute}/` : absolute;
|
|
429
|
+
const suffix = target.slice(star + 1).replace(/\.(tsx?|jsx?)$/, "");
|
|
430
|
+
if (!file.startsWith(prefix) || !file.endsWith(suffix)) continue;
|
|
431
|
+
const middle = file.slice(prefix.length, file.length - suffix.length);
|
|
432
|
+
if (!best || prefix.length > best.prefix) best = { specifier: pattern.replace("*", middle), prefix: prefix.length };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return best?.specifier;
|
|
436
|
+
}
|
|
399
437
|
function compare(a, b) {
|
|
400
438
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
401
439
|
}
|
|
@@ -585,7 +623,7 @@ function parameterTypeText(sf, decl) {
|
|
|
585
623
|
}
|
|
586
624
|
function configResolver(sf, configs, imported) {
|
|
587
625
|
const local = new Map(configs.map((c) => [c.name, c]));
|
|
588
|
-
const
|
|
626
|
+
const resolve8 = (name) => local.get(name) ?? imported(sf, name);
|
|
589
627
|
const aliases = /* @__PURE__ */ new Map();
|
|
590
628
|
for (const stmt of sf.statements) {
|
|
591
629
|
if (!ts4.isVariableStatement(stmt)) continue;
|
|
@@ -595,7 +633,7 @@ function configResolver(sf, configs, imported) {
|
|
|
595
633
|
}
|
|
596
634
|
}
|
|
597
635
|
}
|
|
598
|
-
const find = (name) =>
|
|
636
|
+
const find = (name) => resolve8(name) ?? (aliases.has(name) ? resolve8(aliases.get(name)) : void 0);
|
|
599
637
|
return (name) => {
|
|
600
638
|
const config = find(name);
|
|
601
639
|
return config && withBase(config, find, /* @__PURE__ */ new Set([config.name]));
|
|
@@ -628,16 +666,16 @@ function withBase(config, find, seen) {
|
|
|
628
666
|
if (parts.length) entry.parts = parts;
|
|
629
667
|
return { ...config, entry, description: config.description ?? base.description };
|
|
630
668
|
}
|
|
631
|
-
function linkConfig(decl, paramTypes,
|
|
669
|
+
function linkConfig(decl, paramTypes, resolve8) {
|
|
632
670
|
for (const m of paramTypes.matchAll(/VariantProps\s*<\s*typeof\s+(\w+)\s*>/g)) {
|
|
633
|
-
const c =
|
|
671
|
+
const c = resolve8(m[1]);
|
|
634
672
|
if (c) return c;
|
|
635
673
|
}
|
|
636
674
|
let found;
|
|
637
675
|
const visit = (n) => {
|
|
638
676
|
if (found) return;
|
|
639
|
-
if (ts4.isCallExpression(n) && ts4.isIdentifier(n.expression)) found =
|
|
640
|
-
else if (ts4.isPropertyAccessExpression(n) && ts4.isIdentifier(n.expression)) found =
|
|
677
|
+
if (ts4.isCallExpression(n) && ts4.isIdentifier(n.expression)) found = resolve8(n.expression.text);
|
|
678
|
+
else if (ts4.isPropertyAccessExpression(n) && ts4.isIdentifier(n.expression)) found = resolve8(n.expression.text);
|
|
641
679
|
if (!found) ts4.forEachChild(n, visit);
|
|
642
680
|
};
|
|
643
681
|
visit(decl);
|
|
@@ -1122,7 +1160,9 @@ function readAssets(root, brandDir, warn) {
|
|
|
1122
1160
|
for (const file of notes.keys()) {
|
|
1123
1161
|
if (!found.has(file)) warn({ file: rel2(root, indexPath), message: `assets.json lists ${file}, which is not in the brand folder` });
|
|
1124
1162
|
}
|
|
1125
|
-
|
|
1163
|
+
const order = [...notes.keys()];
|
|
1164
|
+
const rank2 = (file) => order.includes(file) ? order.indexOf(file) : order.length;
|
|
1165
|
+
return out.sort((a, b) => rank2(a.file) - rank2(b.file) || compare(a.file, b.file));
|
|
1126
1166
|
}
|
|
1127
1167
|
function kindFromFolder(folder) {
|
|
1128
1168
|
const f = folder.toLowerCase();
|
|
@@ -1166,7 +1206,14 @@ function readTokens(root, sources, warn) {
|
|
|
1166
1206
|
const walk3 = (container, chain) => {
|
|
1167
1207
|
let comment;
|
|
1168
1208
|
let fresh = false;
|
|
1209
|
+
let previous;
|
|
1169
1210
|
for (const node of container.nodes ?? []) {
|
|
1211
|
+
if (node.type === "comment" && previous && !/\n/.test(node.raws.before ?? "")) {
|
|
1212
|
+
previous.description = cleanComment(node.text);
|
|
1213
|
+
previous = void 0;
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
previous = void 0;
|
|
1170
1217
|
if (node.type === "comment") {
|
|
1171
1218
|
comment = cleanComment(node.text);
|
|
1172
1219
|
fresh = true;
|
|
@@ -1188,11 +1235,15 @@ function readTokens(root, sources, warn) {
|
|
|
1188
1235
|
const existing = entry.values[key];
|
|
1189
1236
|
const explicitAlready = existing !== void 0 && !mediaKeys.has(key);
|
|
1190
1237
|
if (!(media && explicitAlready)) {
|
|
1238
|
+
if (existing !== void 0 && existing !== value && source !== entry.source && !entry.overriddenIn?.includes(source)) {
|
|
1239
|
+
entry.overriddenIn = [...entry.overriddenIn ?? [], source];
|
|
1240
|
+
}
|
|
1191
1241
|
entry.values[key] = value;
|
|
1192
1242
|
if (media) mediaKeys.add(key);
|
|
1193
1243
|
else mediaKeys.delete(key);
|
|
1194
1244
|
}
|
|
1195
1245
|
if (comment && !entry.description) entry.description = comment;
|
|
1246
|
+
previous = entry;
|
|
1196
1247
|
continue;
|
|
1197
1248
|
}
|
|
1198
1249
|
fresh = false;
|
|
@@ -1284,14 +1335,72 @@ function tierOf(name, values) {
|
|
|
1284
1335
|
var MANIFEST_SCHEMA = "designtools.manifest/1";
|
|
1285
1336
|
|
|
1286
1337
|
// src/build.ts
|
|
1287
|
-
import { readFileSync as
|
|
1338
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1288
1339
|
import { dirname as dirname3, join as join3 } from "path";
|
|
1289
1340
|
import { fileURLToPath } from "url";
|
|
1341
|
+
|
|
1342
|
+
// src/usage.ts
|
|
1343
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
1344
|
+
import { resolve as resolve4 } from "path";
|
|
1345
|
+
var STATUS = /^(success|warning|destructive)$|-subdued-foreground$/;
|
|
1346
|
+
function usageRules(root, file, warn) {
|
|
1347
|
+
const path = resolve4(root, file);
|
|
1348
|
+
if (!existsSync3(path)) {
|
|
1349
|
+
warn({ file, message: "usage file not found: run @designtools/tokens with --usage" });
|
|
1350
|
+
return [];
|
|
1351
|
+
}
|
|
1352
|
+
let usage;
|
|
1353
|
+
try {
|
|
1354
|
+
usage = JSON.parse(readFileSync5(path, "utf8"));
|
|
1355
|
+
} catch (e) {
|
|
1356
|
+
warn({ file, message: `usage file is not valid JSON: ${e.message}` });
|
|
1357
|
+
return [];
|
|
1358
|
+
}
|
|
1359
|
+
if (usage.schema !== "designtools.usage/1") {
|
|
1360
|
+
warn({ file, message: `usage file has schema ${usage.schema}; this manifest reads designtools.usage/1` });
|
|
1361
|
+
return [];
|
|
1362
|
+
}
|
|
1363
|
+
const colours = [.../* @__PURE__ */ new Set([...Object.keys(usage.light), ...Object.keys(usage.dark)])].filter((c) => !STATUS.test(c)).sort(compare);
|
|
1364
|
+
const rules = [];
|
|
1365
|
+
for (const colour of colours) {
|
|
1366
|
+
const by = { text: [], shape: [], none: [] };
|
|
1367
|
+
let worst;
|
|
1368
|
+
for (const mode of ["light", "dark"]) {
|
|
1369
|
+
for (const [ground, { ratio, use }] of Object.entries(usage[mode][colour] ?? {})) {
|
|
1370
|
+
by[use].push(`${ground} (${mode})`);
|
|
1371
|
+
if (use !== "text" && (!worst || ratio < worst.ratio)) worst = { ratio, where: `${ground} in ${mode} mode` };
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (!by.shape.length && !by.none.length) continue;
|
|
1375
|
+
const list2 = (xs) => xs.length > 1 ? `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}` : xs[0];
|
|
1376
|
+
const parts = [
|
|
1377
|
+
by.text.length ? `\`--${colour}\` reads as text on ${list2(by.text)}.` : `\`--${colour}\` is never text on these backgrounds.`,
|
|
1378
|
+
by.shape.length ? `On ${list2(by.shape)} it is a shape only (an icon, a border, a meter), not text.` : "",
|
|
1379
|
+
by.none.length ? `On ${list2(by.none)} it is neither${worst ? `, down to ${worst.ratio}:1 on ${worst.where}` : ""}: use it there only as a fill with its own label.` : ""
|
|
1380
|
+
].filter(Boolean);
|
|
1381
|
+
rules.push({
|
|
1382
|
+
id: `usage-${colour}`,
|
|
1383
|
+
kind: "interface",
|
|
1384
|
+
title: `Where ${colour} can go`,
|
|
1385
|
+
statement: parts.join(" "),
|
|
1386
|
+
rationale: "Measured from the generated tokens; it changes when they do. No palette makes every colour work on every background, so a brand colour's limits are a brand decision: confirm them as a brand rule.",
|
|
1387
|
+
threshold: `${usage.floors.text}:1 as text, ${usage.floors.shape}:1 as a shape (WCAG 2.2 1.4.3 and 1.4.11)`,
|
|
1388
|
+
appliesTo: [colour],
|
|
1389
|
+
tokens: [`--${colour}`],
|
|
1390
|
+
status: "measured",
|
|
1391
|
+
check: { kind: "test", ref: "mxa-tests", description: "axe measures contrast on every rendered page; this says where to look first." },
|
|
1392
|
+
source: file
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
return rules;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// src/build.ts
|
|
1290
1399
|
var GENERATOR_NAME = "@designtools/manifest";
|
|
1291
1400
|
var VERSION = (() => {
|
|
1292
1401
|
try {
|
|
1293
1402
|
const here = dirname3(fileURLToPath(import.meta.url));
|
|
1294
|
-
return JSON.parse(
|
|
1403
|
+
return JSON.parse(readFileSync6(join3(here, "../package.json"), "utf8")).version;
|
|
1295
1404
|
} catch {
|
|
1296
1405
|
return "0.0.0";
|
|
1297
1406
|
}
|
|
@@ -1312,7 +1421,10 @@ function buildManifest(config) {
|
|
|
1312
1421
|
components: readComponents({ root: config.root, system: config.system, tsconfig: config.tsconfig, out }, warn)
|
|
1313
1422
|
};
|
|
1314
1423
|
const tokens = { ...head, sources: config.tokens, tokens: readTokens(config.root, config.tokens, warn) };
|
|
1315
|
-
const rules = {
|
|
1424
|
+
const rules = {
|
|
1425
|
+
...head,
|
|
1426
|
+
rules: [...readRules(config.root, config.rules ?? `${system}/rules`, warn), ...config.usage ? usageRules(config.root, config.usage, warn) : []]
|
|
1427
|
+
};
|
|
1316
1428
|
const patterns = {
|
|
1317
1429
|
...head,
|
|
1318
1430
|
patterns: readPatterns(config.root, system, components.components, compilerOptions(config.root, config.tsconfig), warn, out)
|
|
@@ -1363,14 +1475,17 @@ function sortKeys(value) {
|
|
|
1363
1475
|
|
|
1364
1476
|
// src/agents.ts
|
|
1365
1477
|
import { execFileSync } from "child_process";
|
|
1366
|
-
import { existsSync as
|
|
1367
|
-
import { resolve as
|
|
1478
|
+
import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
|
|
1479
|
+
import { resolve as resolve5 } from "path";
|
|
1368
1480
|
var AGENTS_START = "<!-- designtools:start \xB7 generated by designtools-manifest agents; edit outside these markers -->";
|
|
1369
1481
|
var AGENTS_END = "<!-- designtools:end -->";
|
|
1370
|
-
function agentsFile(root, given) {
|
|
1482
|
+
function agentsFile(root, given, configured) {
|
|
1371
1483
|
if (given) return given;
|
|
1372
|
-
if (
|
|
1373
|
-
|
|
1484
|
+
if (configured) return configured;
|
|
1485
|
+
const claude = readIfExists(resolve5(root, "CLAUDE.md"));
|
|
1486
|
+
if (claude === void 0) return "AGENTS.md";
|
|
1487
|
+
if (/^\s*@AGENTS\.md\s*$/m.test(claude)) return "AGENTS.md";
|
|
1488
|
+
return "CLAUDE.md";
|
|
1374
1489
|
}
|
|
1375
1490
|
function agentsSection(config) {
|
|
1376
1491
|
const out = outDir(config);
|
|
@@ -1394,7 +1509,7 @@ function agentsSection(config) {
|
|
|
1394
1509
|
"",
|
|
1395
1510
|
"- Start from a component's canonical example, and follow its do and don't examples.",
|
|
1396
1511
|
"- Change how a component looks with its variants, not a `className`.",
|
|
1397
|
-
"- Use semantic tokens
|
|
1512
|
+
"- Use semantic tokens for colour in UI. Never a hex value or an arbitrary value. A ramp step (`--color-<name>-500`) is for charts, illustration and a decorative colour that has no semantic slot, never for text, surfaces or actions.",
|
|
1398
1513
|
"- Use the taxonomy's names, never its wrong forms, in code and in copy.",
|
|
1399
1514
|
"- Carry status when you quote: say when a component is emerging or deprecated, or a rule is still an assumption.",
|
|
1400
1515
|
AGENTS_END
|
|
@@ -1412,12 +1527,12 @@ function withAgentsSection(existing, section) {
|
|
|
1412
1527
|
return existing.replace(/\s*$/, "\n\n") + section;
|
|
1413
1528
|
}
|
|
1414
1529
|
function readIfExists(path) {
|
|
1415
|
-
return
|
|
1530
|
+
return existsSync4(path) ? readFileSync7(path, "utf8") : void 0;
|
|
1416
1531
|
}
|
|
1417
1532
|
function provenance(root) {
|
|
1418
1533
|
let version = "unversioned";
|
|
1419
1534
|
try {
|
|
1420
|
-
const pkg = JSON.parse(
|
|
1535
|
+
const pkg = JSON.parse(readFileSync7(resolve5(root, "package.json"), "utf8"));
|
|
1421
1536
|
version = `${pkg.name ?? "project"}@${pkg.version ?? "0.0.0"}`;
|
|
1422
1537
|
} catch {
|
|
1423
1538
|
}
|
|
@@ -1435,18 +1550,18 @@ function provenance(root) {
|
|
|
1435
1550
|
}
|
|
1436
1551
|
|
|
1437
1552
|
// src/config.ts
|
|
1438
|
-
import { existsSync as
|
|
1439
|
-
import { resolve as
|
|
1553
|
+
import { existsSync as existsSync5, readFileSync as readFileSync8 } from "fs";
|
|
1554
|
+
import { resolve as resolve6 } from "path";
|
|
1440
1555
|
var CONFIG_FILE = "designtools.json";
|
|
1441
|
-
var PATH_FIELDS = ["tsconfig", "out", "rules", "taxonomy", "brand", "docs"];
|
|
1556
|
+
var PATH_FIELDS = ["tsconfig", "out", "rules", "taxonomy", "brand", "docs", "agents", "usage"];
|
|
1442
1557
|
function loadConfig(flags) {
|
|
1443
|
-
const root =
|
|
1444
|
-
const path =
|
|
1558
|
+
const root = resolve6(flags.root ?? ".");
|
|
1559
|
+
const path = resolve6(root, CONFIG_FILE);
|
|
1445
1560
|
let file = {};
|
|
1446
|
-
if (
|
|
1561
|
+
if (existsSync5(path)) {
|
|
1447
1562
|
let parsed;
|
|
1448
1563
|
try {
|
|
1449
|
-
parsed = JSON.parse(
|
|
1564
|
+
parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
1450
1565
|
} catch (e) {
|
|
1451
1566
|
throw new Error(`${CONFIG_FILE} is not valid JSON: ${e.message}`);
|
|
1452
1567
|
}
|
|
@@ -1463,6 +1578,12 @@ function loadConfig(flags) {
|
|
|
1463
1578
|
throw new Error(`manifest.tokens in ${CONFIG_FILE} must be a list of stylesheet paths`);
|
|
1464
1579
|
}
|
|
1465
1580
|
const config = { root, system, tokens };
|
|
1581
|
+
if (file.prose !== void 0) {
|
|
1582
|
+
if (!Array.isArray(file.prose) || file.prose.some((p) => typeof p !== "string")) {
|
|
1583
|
+
throw new Error(`manifest.prose in ${CONFIG_FILE} must be a list of files or folders`);
|
|
1584
|
+
}
|
|
1585
|
+
config.prose = file.prose;
|
|
1586
|
+
}
|
|
1466
1587
|
for (const field of PATH_FIELDS) {
|
|
1467
1588
|
const value = flags[field] ?? file[field];
|
|
1468
1589
|
if (value !== void 0) config[field] = value;
|
|
@@ -1667,25 +1788,33 @@ No changes to components or tokens.
|
|
|
1667
1788
|
}
|
|
1668
1789
|
|
|
1669
1790
|
// src/prose.ts
|
|
1670
|
-
import { existsSync as
|
|
1671
|
-
import { join as join4, relative as relative3, resolve as
|
|
1791
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
|
|
1792
|
+
import { join as join4, relative as relative3, resolve as resolve7, sep as sep3 } from "path";
|
|
1672
1793
|
var NOT_SYSTEM = /* @__PURE__ */ new Set(["Fragment", "Suspense", "StrictMode", "Profiler", "Link", "Image", "Script", "Head", "Html", "Main", "NextScript", "Form"]);
|
|
1673
1794
|
var LIST_COMPONENTS = 4;
|
|
1674
1795
|
var LIST_TOKENS = 6;
|
|
1675
|
-
function defaultProseFiles(root, system) {
|
|
1796
|
+
function defaultProseFiles(root, system, extra = []) {
|
|
1676
1797
|
const out = [];
|
|
1677
|
-
for (const f of ["CLAUDE.md", "AGENTS.md"]) if (
|
|
1678
|
-
for (const dir of [".claude", "docs", system]) out.push(...walk2(root,
|
|
1798
|
+
for (const f of ["CLAUDE.md", "AGENTS.md"]) if (existsSync6(resolve7(root, f))) out.push(f);
|
|
1799
|
+
for (const dir of [".claude", "docs", system]) out.push(...walk2(root, resolve7(root, dir)));
|
|
1800
|
+
for (const p of extra) {
|
|
1801
|
+
const path = resolve7(root, p);
|
|
1802
|
+
if (!existsSync6(path)) continue;
|
|
1803
|
+
if (statSync3(path).isDirectory()) out.push(...walk2(root, path, true));
|
|
1804
|
+
else out.push(relative3(root, path).split(sep3).join("/"));
|
|
1805
|
+
}
|
|
1679
1806
|
return [...new Set(out)];
|
|
1680
1807
|
}
|
|
1681
|
-
function walk2(root, dir) {
|
|
1682
|
-
if (!
|
|
1808
|
+
function walk2(root, dir, content = false) {
|
|
1809
|
+
if (!existsSync6(dir) || !statSync3(dir).isDirectory()) return [];
|
|
1683
1810
|
const out = [];
|
|
1684
1811
|
for (const name of readdirSync3(dir).sort()) {
|
|
1685
1812
|
const path = join4(dir, name);
|
|
1686
1813
|
if (statSync3(path).isDirectory()) {
|
|
1687
|
-
if (name !== "node_modules" && name !== "manifest") out.push(...walk2(root, path));
|
|
1688
|
-
} else if (/\.mdx?$/.test(name)
|
|
1814
|
+
if (name !== "node_modules" && name !== "manifest" && name !== ".next") out.push(...walk2(root, path, content));
|
|
1815
|
+
} else if (/\.mdx?$/.test(name) || content && /content\.tsx?$/.test(name)) {
|
|
1816
|
+
out.push(relative3(root, path).split(sep3).join("/"));
|
|
1817
|
+
}
|
|
1689
1818
|
}
|
|
1690
1819
|
return out;
|
|
1691
1820
|
}
|
|
@@ -1708,10 +1837,13 @@ function checkProse(root, files, inputs) {
|
|
|
1708
1837
|
}
|
|
1709
1838
|
const findings = [];
|
|
1710
1839
|
for (const file of files) {
|
|
1711
|
-
const path =
|
|
1712
|
-
if (!
|
|
1713
|
-
const lines = stripGenerated(
|
|
1840
|
+
const path = resolve7(root, file);
|
|
1841
|
+
if (!existsSync6(path)) continue;
|
|
1842
|
+
const lines = stripGenerated(readFileSync9(path, "utf8")).split(/\r?\n/);
|
|
1843
|
+
const script = /\.[cm]?[jt]sx?$/.test(file);
|
|
1714
1844
|
let inFence = false;
|
|
1845
|
+
let ignoring = false;
|
|
1846
|
+
let skipNext = false;
|
|
1715
1847
|
let block;
|
|
1716
1848
|
const closeBlock = () => {
|
|
1717
1849
|
if (block && block.components.size >= LIST_COMPONENTS) {
|
|
@@ -1724,7 +1856,16 @@ function checkProse(root, files, inputs) {
|
|
|
1724
1856
|
};
|
|
1725
1857
|
lines.forEach((line, i) => {
|
|
1726
1858
|
const n = i + 1;
|
|
1727
|
-
if (
|
|
1859
|
+
if (/prose-check-ignore-start/.test(line)) ignoring = true;
|
|
1860
|
+
if (/prose-check-ignore-end/.test(line)) {
|
|
1861
|
+
ignoring = false;
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
const skip = ignoring || skipNext || /prose-check-ignore(?!-)/.test(line);
|
|
1865
|
+
skipNext = /prose-check-ignore-next-line/.test(line);
|
|
1866
|
+
if (skip) return;
|
|
1867
|
+
if (script && /^\s*(import|export\s+(type|interface))\b/.test(line)) return;
|
|
1868
|
+
if (!script && /^\s*(```|~~~)/.test(line)) inFence = !inFence;
|
|
1728
1869
|
if (!line.trim()) {
|
|
1729
1870
|
closeBlock();
|
|
1730
1871
|
return;
|
|
@@ -1740,7 +1881,7 @@ function checkProse(root, files, inputs) {
|
|
|
1740
1881
|
message: wrong ? `<${name}>: the system calls this ${wrong.name}` : `<${name}> looks like a component, but the system has none by that name`
|
|
1741
1882
|
});
|
|
1742
1883
|
}
|
|
1743
|
-
const code = inFence ? [line] : [...line.matchAll(/`([^`]+)`/g)].map((m) => m[1]);
|
|
1884
|
+
const code = inFence ? [line] : script ? [] : [...line.matchAll(/`([^`]+)`/g)].map((m) => m[1]);
|
|
1744
1885
|
for (const span of code) {
|
|
1745
1886
|
for (const word of span.match(/[A-Za-z][\w-]*/g) ?? []) {
|
|
1746
1887
|
if (components.has(word)) block.components.add(word);
|
|
@@ -1752,7 +1893,7 @@ function checkProse(root, files, inputs) {
|
|
|
1752
1893
|
}
|
|
1753
1894
|
}
|
|
1754
1895
|
if (!inFence) {
|
|
1755
|
-
for (const word of line.replace(/`[^`]*`/g, " ").match(/\b[A-Z][A-Za-z0-9]+\b/g) ?? []) if (components.has(word)) block.components.add(word);
|
|
1896
|
+
for (const word of (script ? line : line.replace(/`[^`]*`/g, " ")).match(/\b[A-Z][A-Za-z0-9]+\b/g) ?? []) if (components.has(word)) block.components.add(word);
|
|
1756
1897
|
for (const { re, form, term } of wrongProse) if (re.test(line)) findings.push({ file, line: n, message: `"${form}": the product says "${term.name}"` });
|
|
1757
1898
|
}
|
|
1758
1899
|
});
|
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
provenance,
|
|
15
15
|
readIfExists,
|
|
16
16
|
withAgentsSection
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-PQGMDEUF.js";
|
|
18
18
|
|
|
19
19
|
// src/cli.ts
|
|
20
20
|
import { execFileSync } from "child_process";
|
|
@@ -43,7 +43,9 @@ Options
|
|
|
43
43
|
--tsconfig <file> tsconfig.json for resolving imports (found from the root when absent)
|
|
44
44
|
--out <dir> Where the manifest goes (default: <system>/manifest)
|
|
45
45
|
--root <dir> Project root (default: the current directory)
|
|
46
|
-
--file <file> agents: the file to write (default:
|
|
46
|
+
--file <file> agents: the file to write (default: manifest.agents in designtools.json;
|
|
47
|
+
else AGENTS.md when CLAUDE.md imports it with @AGENTS.md; else
|
|
48
|
+
CLAUDE.md if it exists; else AGENTS.md)
|
|
47
49
|
--check agents: compare instead of writing
|
|
48
50
|
--json diff, prose-check, provenance: print JSON
|
|
49
51
|
--fail-on <class> diff: exit 1 if any change of this class is found (breaking, additive, visual, docs)
|
|
@@ -186,7 +188,7 @@ switch (command) {
|
|
|
186
188
|
process.exit(ok ? 0 : 1);
|
|
187
189
|
}
|
|
188
190
|
case "agents": {
|
|
189
|
-
const file = agentsFile(config.root, values.file);
|
|
191
|
+
const file = agentsFile(config.root, values.file, config.agents);
|
|
190
192
|
const path = resolve(config.root, file);
|
|
191
193
|
const existing = readIfExists(path);
|
|
192
194
|
const next = withAgentsSection(existing, agentsSection(config));
|
|
@@ -203,7 +205,7 @@ switch (command) {
|
|
|
203
205
|
process.exit(0);
|
|
204
206
|
}
|
|
205
207
|
case "prose-check": {
|
|
206
|
-
const files = rest.length ? rest : defaultProseFiles(config.root, config.system);
|
|
208
|
+
const files = rest.length ? rest : defaultProseFiles(config.root, config.system, config.prose);
|
|
207
209
|
const findings = checkProse(config.root, files, {
|
|
208
210
|
components: built.components.components,
|
|
209
211
|
patterns: built.patterns.patterns,
|
package/dist/index.d.ts
CHANGED
|
@@ -34,6 +34,12 @@ interface ComponentEntry {
|
|
|
34
34
|
export: string;
|
|
35
35
|
/** The file that exports it. */
|
|
36
36
|
source: string;
|
|
37
|
+
/**
|
|
38
|
+
* The module specifier an app imports it by, through the tsconfig path alias
|
|
39
|
+
* that reaches the system folder (`@ds/button/button`). Absent when no alias does,
|
|
40
|
+
* in which case `source` is the only honest answer.
|
|
41
|
+
*/
|
|
42
|
+
import?: string;
|
|
37
43
|
/** The component's JSDoc, or its variant config's when the component has none. */
|
|
38
44
|
description?: string;
|
|
39
45
|
/** `@category`. Blocks group components without one under "Components". */
|
|
@@ -156,6 +162,8 @@ interface TokenEntry {
|
|
|
156
162
|
description?: string;
|
|
157
163
|
/** The stylesheet that first declares it. */
|
|
158
164
|
source: string;
|
|
165
|
+
/** Later stylesheets whose declaration replaced a value, in the order read: an override layer. */
|
|
166
|
+
overriddenIn?: string[];
|
|
159
167
|
}
|
|
160
168
|
/** rules.json: brand and interface rules in one format. */
|
|
161
169
|
interface RulesManifest extends ManifestFile {
|
|
@@ -178,8 +186,11 @@ interface RuleEntry {
|
|
|
178
186
|
exceptions?: string[];
|
|
179
187
|
/** Tokens it rests on, e.g. `--size-2xl`. */
|
|
180
188
|
tokens?: string[];
|
|
181
|
-
/**
|
|
182
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Confirmed with the client, an assumption still to confirm, or measured: generated from the
|
|
191
|
+
* tokens (where a colour can be text), a fact that changes when they do, not a decision.
|
|
192
|
+
*/
|
|
193
|
+
status: "confirmed" | "assumption" | "measured";
|
|
183
194
|
/** How it is proved: a test, a lint rule, an audit, or a person. */
|
|
184
195
|
check?: {
|
|
185
196
|
kind: "test" | "lint" | "audit" | "manual";
|
|
@@ -298,6 +309,19 @@ interface ManifestConfig {
|
|
|
298
309
|
brand?: string;
|
|
299
310
|
/** Where the docs are served, for the agent pointer file, e.g. `/design/system`. */
|
|
300
311
|
docs?: string;
|
|
312
|
+
/**
|
|
313
|
+
* The usage file `@designtools/tokens --usage` writes: every colour on every background with a
|
|
314
|
+
* verdict. Its failures become measured usage rules in rules.json.
|
|
315
|
+
*/
|
|
316
|
+
usage?: string;
|
|
317
|
+
/** The agent file the pointer section goes in, when the default choice is wrong for the project. */
|
|
318
|
+
agents?: string;
|
|
319
|
+
/**
|
|
320
|
+
* More places for prose-check to read, beside the agent files, `.claude/`, `docs/`
|
|
321
|
+
* and the system folder: files of any kind, or folders, whose markdown and
|
|
322
|
+
* `*content.ts(x)` files (the docs site's editorial copy) are read.
|
|
323
|
+
*/
|
|
324
|
+
prose?: string[];
|
|
301
325
|
}
|
|
302
326
|
interface BuiltManifest {
|
|
303
327
|
components: ComponentsManifest;
|
|
@@ -422,7 +446,8 @@ declare function classifyExamples(source: string, exports: ExampleRef[]): Exampl
|
|
|
422
446
|
*
|
|
423
447
|
* The comment above a declaration, or above the run of declarations it starts,
|
|
424
448
|
* is that token's description: the scale's comments already say why each value
|
|
425
|
-
* is what it is.
|
|
449
|
+
* is what it is. A comment on the same line after a declaration describes that
|
|
450
|
+
* declaration (`--size-xl: 3rem; /* 48px controls *\/`), and does not start a run.
|
|
426
451
|
*/
|
|
427
452
|
|
|
428
453
|
declare function readTokens(root: string, sources: string[], warn: (w: Warning) => void): TokenEntry[];
|
|
@@ -458,7 +483,8 @@ declare function buildTaxonomy(root: string, curatedPath: string, inputs: {
|
|
|
458
483
|
/**
|
|
459
484
|
* Brand assets from one folder. `assets.json` in it says how to use each file;
|
|
460
485
|
* files it does not mention are listed too, with a warning, so nothing in the
|
|
461
|
-
* folder goes unseen.
|
|
486
|
+
* folder goes unseen. The order is the author's (the wordmark first, because it
|
|
487
|
+
* is the default), then anything unlisted by file name.
|
|
462
488
|
*/
|
|
463
489
|
declare function readAssets(root: string, brandDir: string, warn: (w: Warning) => void): AssetEntry[];
|
|
464
490
|
|
|
@@ -471,8 +497,13 @@ declare function readAssets(root: string, brandDir: string, warn: (w: Warning) =
|
|
|
471
497
|
|
|
472
498
|
declare const AGENTS_START = "<!-- designtools:start \u00B7 generated by designtools-manifest agents; edit outside these markers -->";
|
|
473
499
|
declare const AGENTS_END = "<!-- designtools:end -->";
|
|
474
|
-
/**
|
|
475
|
-
|
|
500
|
+
/**
|
|
501
|
+
* The file the pointer goes in: the flag, then `manifest.agents` in designtools.json,
|
|
502
|
+
* then AGENTS.md when CLAUDE.md imports it (`@AGENTS.md`, as `next dev` writes it), so
|
|
503
|
+
* every agent reads the section and Claude still reaches it through the import; else
|
|
504
|
+
* CLAUDE.md if the project has one, else AGENTS.md.
|
|
505
|
+
*/
|
|
506
|
+
declare function agentsFile(root: string, given?: string, configured?: string): string;
|
|
476
507
|
declare function agentsSection(config: ManifestConfig & {
|
|
477
508
|
docs?: string;
|
|
478
509
|
}): string;
|
|
@@ -500,6 +531,11 @@ declare function provenance(root: string): Provenance;
|
|
|
500
531
|
* a wrong form from the taxonomy (`Modal` for `Dialog`, "log in" for "sign in")
|
|
501
532
|
* a restated list: a paragraph or list naming several components or tokens,
|
|
502
533
|
* which is a second copy of the manifest that will drift
|
|
534
|
+
*
|
|
535
|
+
* A page that names wrong forms on purpose (a voice page: "never the customer")
|
|
536
|
+
* says so: `prose-check-ignore` on a line skips it, `prose-check-ignore-next-line`
|
|
537
|
+
* skips the one after, and `prose-check-ignore-start` … `prose-check-ignore-end`
|
|
538
|
+
* skip everything between, in whatever comment syntax the file uses.
|
|
503
539
|
*/
|
|
504
540
|
|
|
505
541
|
interface ProseFinding {
|
|
@@ -513,7 +549,7 @@ interface ProseInputs {
|
|
|
513
549
|
tokens: TokenEntry[];
|
|
514
550
|
terms: TermEntry[];
|
|
515
551
|
}
|
|
516
|
-
declare function defaultProseFiles(root: string, system: string): string[];
|
|
552
|
+
declare function defaultProseFiles(root: string, system: string, extra?: string[]): string[];
|
|
517
553
|
declare function checkProse(root: string, files: string[], inputs: ProseInputs): ProseFinding[];
|
|
518
554
|
|
|
519
555
|
/**
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@designtools/manifest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Reads a React design system (components, variants, data-slots, examples, tokens, rules, patterns, taxonomy and brand assets) into committed JSON that docs, checks and agents read from. Deterministic CLI: build, check, diff, agents, prose-check.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|