@designtools/manifest 0.0.0-stage → 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 +27 -0
- package/LICENSE +21 -0
- package/README.md +174 -2
- package/dist/chunk-PQGMDEUF.js +1950 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +256 -0
- package/dist/index.d.ts +577 -0
- package/dist/index.js +70 -0
- package/package.json +73 -3
- package/schemas/rule.schema.json +31 -0
|
@@ -0,0 +1,1950 @@
|
|
|
1
|
+
// src/jsdoc.ts
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
var MANIFEST_TAGS = /* @__PURE__ */ new Set([
|
|
4
|
+
"name",
|
|
5
|
+
"description",
|
|
6
|
+
"category",
|
|
7
|
+
"example",
|
|
8
|
+
"see",
|
|
9
|
+
"deprecated",
|
|
10
|
+
"primitive",
|
|
11
|
+
"aria",
|
|
12
|
+
"nodocgen",
|
|
13
|
+
"status",
|
|
14
|
+
"use",
|
|
15
|
+
"avoid",
|
|
16
|
+
"instead",
|
|
17
|
+
"replaces",
|
|
18
|
+
"role",
|
|
19
|
+
"keyboard"
|
|
20
|
+
]);
|
|
21
|
+
var IGNORED_TAGS = /* @__PURE__ */ new Set([
|
|
22
|
+
"param",
|
|
23
|
+
"returns",
|
|
24
|
+
"return",
|
|
25
|
+
"default",
|
|
26
|
+
"defaultValue",
|
|
27
|
+
"remarks",
|
|
28
|
+
"internal",
|
|
29
|
+
"link",
|
|
30
|
+
"since",
|
|
31
|
+
"type",
|
|
32
|
+
"typedef",
|
|
33
|
+
"property",
|
|
34
|
+
"prop",
|
|
35
|
+
"template",
|
|
36
|
+
"throws",
|
|
37
|
+
"todo",
|
|
38
|
+
"public",
|
|
39
|
+
"private",
|
|
40
|
+
"beta",
|
|
41
|
+
"alpha",
|
|
42
|
+
"experimental",
|
|
43
|
+
"version",
|
|
44
|
+
"author"
|
|
45
|
+
]);
|
|
46
|
+
function isKnownTag(name) {
|
|
47
|
+
return MANIFEST_TAGS.has(name) || IGNORED_TAGS.has(name);
|
|
48
|
+
}
|
|
49
|
+
function leadingJSDoc(sf, node) {
|
|
50
|
+
const text2 = sf.getFullText();
|
|
51
|
+
const ranges = ts.getLeadingCommentRanges(text2, node.getFullStart()) ?? [];
|
|
52
|
+
const last = [...ranges].reverse().find((r) => text2.startsWith("/**", r.pos) && text2.slice(r.pos, r.end) !== "/**/");
|
|
53
|
+
if (!last) return void 0;
|
|
54
|
+
const parsed = parseJSDoc(text2.slice(last.pos, last.end));
|
|
55
|
+
return { ...parsed, line: sf.getLineAndCharacterOfPosition(last.pos).line + 1 };
|
|
56
|
+
}
|
|
57
|
+
function parseJSDoc(comment) {
|
|
58
|
+
const lines = comment.replace(/^\/\*\*/, "").replace(/\*\/$/, "").split("\n").map((l) => l.replace(/^\s*\* ?/, "").replace(/\s+$/, ""));
|
|
59
|
+
const description = [];
|
|
60
|
+
const tags2 = [];
|
|
61
|
+
let current;
|
|
62
|
+
const close = () => {
|
|
63
|
+
if (current) tags2.push({ name: current.name, text: trimBlock(current.lines) });
|
|
64
|
+
current = void 0;
|
|
65
|
+
};
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
const tag2 = /^\s*@([A-Za-z][\w-]*)(?=\s|$)\s?(.*)$/.exec(line);
|
|
68
|
+
if (tag2 && !(current?.name === "example" && !/^@/.test(line))) {
|
|
69
|
+
close();
|
|
70
|
+
current = { name: tag2[1], lines: [tag2[2]] };
|
|
71
|
+
} else if (current) {
|
|
72
|
+
current.lines.push(line);
|
|
73
|
+
} else {
|
|
74
|
+
description.push(line.trim());
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
close();
|
|
78
|
+
return { description: joinParagraphs(description), tags: tags2 };
|
|
79
|
+
}
|
|
80
|
+
function trimBlock(lines) {
|
|
81
|
+
let start = 0;
|
|
82
|
+
let end = lines.length;
|
|
83
|
+
while (start < end && lines[start].trim() === "") start++;
|
|
84
|
+
while (end > start && lines[end - 1].trim() === "") end--;
|
|
85
|
+
return lines.slice(start, end).join("\n");
|
|
86
|
+
}
|
|
87
|
+
function joinParagraphs(lines) {
|
|
88
|
+
return lines.join("\n").split(/\n{2,}/).map((p) => p.split("\n").map((l) => l.trim()).filter(Boolean).join(" ")).filter(Boolean).join("\n\n");
|
|
89
|
+
}
|
|
90
|
+
function tag(doc, name) {
|
|
91
|
+
return doc?.tags.find((t) => t.name === name)?.text;
|
|
92
|
+
}
|
|
93
|
+
function tags(doc, name) {
|
|
94
|
+
return doc?.tags.filter((t) => t.name === name).map((t) => t.text) ?? [];
|
|
95
|
+
}
|
|
96
|
+
function hasTag(doc, name) {
|
|
97
|
+
return doc?.tags.some((t) => t.name === name) ?? false;
|
|
98
|
+
}
|
|
99
|
+
function describe(doc) {
|
|
100
|
+
if (!doc) return void 0;
|
|
101
|
+
const parts = [doc.description, ...tags(doc, "description").map((t) => joinParagraphs(t.split("\n")))].filter(
|
|
102
|
+
Boolean
|
|
103
|
+
);
|
|
104
|
+
return parts.length ? parts.join("\n\n") : void 0;
|
|
105
|
+
}
|
|
106
|
+
function checkTags(source, doc, owner, warn) {
|
|
107
|
+
for (const t of doc?.tags ?? []) {
|
|
108
|
+
if (isKnownTag(t.name)) continue;
|
|
109
|
+
const near = [...MANIFEST_TAGS].find((known) => distance(known, t.name) <= 2);
|
|
110
|
+
warn({
|
|
111
|
+
file: source,
|
|
112
|
+
line: doc?.line,
|
|
113
|
+
message: `unknown JSDoc tag @${t.name} on ${owner}${near ? ` (did you mean @${near}?)` : ""}`
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function distance(a, b) {
|
|
118
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
119
|
+
for (let j = 1; j <= b.length; j++) d[0][j] = j;
|
|
120
|
+
for (let i = 1; i <= a.length; i++)
|
|
121
|
+
for (let j = 1; j <= b.length; j++)
|
|
122
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
123
|
+
return d[a.length][b.length];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/variants.ts
|
|
127
|
+
import ts2 from "typescript";
|
|
128
|
+
var LIBRARIES = {
|
|
129
|
+
"tailwind-variants": { tv: "tv", createTV: "tv" },
|
|
130
|
+
"class-variance-authority": { cva: "cva" },
|
|
131
|
+
cva: { cva: "cva" }
|
|
132
|
+
};
|
|
133
|
+
function variantCallees(sf) {
|
|
134
|
+
const out = /* @__PURE__ */ new Map();
|
|
135
|
+
for (const stmt of sf.statements) {
|
|
136
|
+
if (!ts2.isImportDeclaration(stmt) || !ts2.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
137
|
+
const lib = LIBRARIES[stmt.moduleSpecifier.text];
|
|
138
|
+
const named = stmt.importClause?.namedBindings;
|
|
139
|
+
if (!lib || !named || !ts2.isNamedImports(named)) continue;
|
|
140
|
+
for (const el of named.elements) {
|
|
141
|
+
const imported = (el.propertyName ?? el.name).text;
|
|
142
|
+
if (lib[imported]) out.set(el.name.text, lib[imported]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
function findVariantConfigs(sf) {
|
|
148
|
+
const callees = variantCallees(sf);
|
|
149
|
+
if (callees.size === 0) return [];
|
|
150
|
+
const out = [];
|
|
151
|
+
const visit = (node) => {
|
|
152
|
+
if (ts2.isVariableDeclaration(node) && ts2.isIdentifier(node.name) && node.initializer && ts2.isCallExpression(node.initializer) && ts2.isIdentifier(node.initializer.expression) && callees.has(node.initializer.expression.text)) {
|
|
153
|
+
const kind = callees.get(node.initializer.expression.text);
|
|
154
|
+
const args = node.initializer.arguments;
|
|
155
|
+
const cfg = kind === "tv" || args.length === 1 && args[0] && ts2.isObjectLiteralExpression(args[0]) ? args[0] : args[1];
|
|
156
|
+
const extend = cfg && ts2.isObjectLiteralExpression(cfg) ? property(cfg, "extend") : void 0;
|
|
157
|
+
const statement = node.parent?.parent && ts2.isVariableStatement(node.parent.parent) ? node.parent.parent : node;
|
|
158
|
+
const doc = leadingJSDoc(sf, statement);
|
|
159
|
+
out.push({
|
|
160
|
+
name: node.name.text,
|
|
161
|
+
kind,
|
|
162
|
+
entry: readConfig(sf, kind, node.name.text, cfg),
|
|
163
|
+
...extend && ts2.isIdentifier(extend) ? { extendsName: extend.text } : {},
|
|
164
|
+
doc,
|
|
165
|
+
description: describe(doc)
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
ts2.forEachChild(node, visit);
|
|
169
|
+
};
|
|
170
|
+
visit(sf);
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
function readConfig(sf, kind, name, cfg) {
|
|
174
|
+
const entry = { from: kind, config: name, axes: [] };
|
|
175
|
+
if (!cfg || !ts2.isObjectLiteralExpression(cfg)) return entry;
|
|
176
|
+
const defaults = /* @__PURE__ */ new Map();
|
|
177
|
+
const defaultsNode = property(cfg, "defaultVariants");
|
|
178
|
+
if (defaultsNode && ts2.isObjectLiteralExpression(defaultsNode)) {
|
|
179
|
+
for (const p of defaultsNode.properties) {
|
|
180
|
+
const key = propertyName(p);
|
|
181
|
+
const value = ts2.isPropertyAssignment(p) ? literal(p.initializer) : void 0;
|
|
182
|
+
if (key && value !== void 0) defaults.set(key, value);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const variants = property(cfg, "variants");
|
|
186
|
+
if (variants && ts2.isObjectLiteralExpression(variants)) {
|
|
187
|
+
for (const axisNode of variants.properties) {
|
|
188
|
+
const axisName = propertyName(axisNode);
|
|
189
|
+
if (!axisName || !ts2.isPropertyAssignment(axisNode)) continue;
|
|
190
|
+
const axis = { name: axisName, options: [] };
|
|
191
|
+
if (ts2.isObjectLiteralExpression(axisNode.initializer)) {
|
|
192
|
+
for (const opt of axisNode.initializer.properties) {
|
|
193
|
+
const optName = propertyName(opt);
|
|
194
|
+
if (!optName) continue;
|
|
195
|
+
const description = describe(leadingJSDoc(sf, opt));
|
|
196
|
+
axis.options.push(description ? { name: optName, description } : { name: optName });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const d = defaults.get(axisName);
|
|
200
|
+
if (d !== void 0) axis.default = d;
|
|
201
|
+
entry.axes.push(axis);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const compound = property(cfg, "compoundVariants");
|
|
205
|
+
if (compound && ts2.isArrayLiteralExpression(compound)) {
|
|
206
|
+
const conditions = [];
|
|
207
|
+
for (const el of compound.elements) {
|
|
208
|
+
if (!ts2.isObjectLiteralExpression(el)) continue;
|
|
209
|
+
const condition = {};
|
|
210
|
+
for (const p of el.properties) {
|
|
211
|
+
const key = propertyName(p);
|
|
212
|
+
if (!key || key === "class" || key === "className" || !ts2.isPropertyAssignment(p)) continue;
|
|
213
|
+
if (ts2.isArrayLiteralExpression(p.initializer)) {
|
|
214
|
+
const values = p.initializer.elements.map(literal).filter((v) => v !== void 0);
|
|
215
|
+
condition[key] = values;
|
|
216
|
+
} else {
|
|
217
|
+
const value = literal(p.initializer);
|
|
218
|
+
if (value !== void 0) condition[key] = value;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (Object.keys(condition).length) conditions.push(condition);
|
|
222
|
+
}
|
|
223
|
+
if (conditions.length) entry.compound = conditions;
|
|
224
|
+
}
|
|
225
|
+
if (kind === "tv") {
|
|
226
|
+
const slots = property(cfg, "slots");
|
|
227
|
+
if (slots && ts2.isObjectLiteralExpression(slots)) {
|
|
228
|
+
const parts = slots.properties.map(propertyName).filter((n) => !!n);
|
|
229
|
+
if (parts.length) entry.parts = parts;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return entry;
|
|
233
|
+
}
|
|
234
|
+
function property(obj, name) {
|
|
235
|
+
for (const p of obj.properties) {
|
|
236
|
+
if (propertyName(p) === name && ts2.isPropertyAssignment(p)) return p.initializer;
|
|
237
|
+
}
|
|
238
|
+
return void 0;
|
|
239
|
+
}
|
|
240
|
+
function propertyName(p) {
|
|
241
|
+
if (!p.name) return void 0;
|
|
242
|
+
if (ts2.isIdentifier(p.name) || ts2.isStringLiteral(p.name) || ts2.isNumericLiteral(p.name)) return p.name.text;
|
|
243
|
+
return void 0;
|
|
244
|
+
}
|
|
245
|
+
function literal(node) {
|
|
246
|
+
if (ts2.isStringLiteral(node) || ts2.isNoSubstitutionTemplateLiteral(node) || ts2.isNumericLiteral(node)) return node.text;
|
|
247
|
+
if (node.kind === ts2.SyntaxKind.TrueKeyword) return "true";
|
|
248
|
+
if (node.kind === ts2.SyntaxKind.FalseKeyword) return "false";
|
|
249
|
+
return void 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/examples.ts
|
|
253
|
+
import { readFileSync } from "fs";
|
|
254
|
+
import ts3 from "typescript";
|
|
255
|
+
function readExampleFile(path, source, warn, owner = "example") {
|
|
256
|
+
const sf = ts3.createSourceFile(path, readFileSync(path, "utf8"), ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
|
|
257
|
+
const exports = [];
|
|
258
|
+
const first = sf.statements[0];
|
|
259
|
+
const doc = first && ts3.isImportDeclaration(first) ? leadingJSDoc(sf, first) : void 0;
|
|
260
|
+
for (const stmt of sf.statements) {
|
|
261
|
+
if (!ts3.canHaveModifiers(stmt) || !(ts3.getModifiers(stmt) ?? []).some((m) => m.kind === ts3.SyntaxKind.ExportKeyword)) continue;
|
|
262
|
+
const stmtDoc = leadingJSDoc(sf, stmt);
|
|
263
|
+
checkTags(source, stmtDoc, owner, warn);
|
|
264
|
+
const add = (name, body) => {
|
|
265
|
+
const ref = { name };
|
|
266
|
+
const description = describe(stmtDoc);
|
|
267
|
+
if (description) ref.description = description;
|
|
268
|
+
const code = body && jsxOf(sf, body);
|
|
269
|
+
if (code) ref.code = code;
|
|
270
|
+
exports.push(ref);
|
|
271
|
+
};
|
|
272
|
+
if (ts3.isFunctionDeclaration(stmt) && stmt.name) add(stmt.name.text, stmt.body);
|
|
273
|
+
if (ts3.isVariableStatement(stmt)) {
|
|
274
|
+
for (const d of stmt.declarationList.declarations) if (ts3.isIdentifier(d.name)) add(d.name.text, d.initializer);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return { sf, doc, exports };
|
|
278
|
+
}
|
|
279
|
+
function classifyExamples(source, exports) {
|
|
280
|
+
const entry = { source, do: [], dont: [], other: [] };
|
|
281
|
+
for (const ex of exports) {
|
|
282
|
+
if (ex.name === "canonical") entry.canonical = ex;
|
|
283
|
+
else if (/^dont(?=[A-Z0-9_]|$)/.test(ex.name)) entry.dont.push(ex);
|
|
284
|
+
else if (/^do(?=[A-Z0-9_]|$)/.test(ex.name)) entry.do.push(ex);
|
|
285
|
+
else entry.other.push(ex);
|
|
286
|
+
}
|
|
287
|
+
return entry;
|
|
288
|
+
}
|
|
289
|
+
function jsxOf(sf, node) {
|
|
290
|
+
let expr;
|
|
291
|
+
if (ts3.isArrowFunction(node) || ts3.isFunctionExpression(node) || ts3.isFunctionDeclaration(node)) {
|
|
292
|
+
const body = node.body;
|
|
293
|
+
if (body && ts3.isBlock(body)) {
|
|
294
|
+
const ret = [...body.statements].reverse().find(ts3.isReturnStatement);
|
|
295
|
+
expr = ret?.expression;
|
|
296
|
+
} else expr = body;
|
|
297
|
+
} else if (ts3.isBlock(node)) {
|
|
298
|
+
expr = [...node.statements].reverse().find(ts3.isReturnStatement)?.expression;
|
|
299
|
+
} else {
|
|
300
|
+
expr = node;
|
|
301
|
+
}
|
|
302
|
+
while (expr && ts3.isParenthesizedExpression(expr)) expr = expr.expression;
|
|
303
|
+
if (!expr) return void 0;
|
|
304
|
+
return dedent(expr.getText(sf));
|
|
305
|
+
}
|
|
306
|
+
function dedent(text2) {
|
|
307
|
+
const lines = text2.split("\n");
|
|
308
|
+
const indents = lines.slice(1).filter((l) => l.trim()).map((l) => /^\s*/.exec(l)[0].length);
|
|
309
|
+
const cut = indents.length ? Math.min(...indents) : 0;
|
|
310
|
+
return [lines[0], ...lines.slice(1).map((l) => l.slice(Math.min(cut, /^\s*/.exec(l)[0].length)))].join("\n").trimEnd();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/components.ts
|
|
314
|
+
import { existsSync, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
315
|
+
import { basename, dirname, join, relative, resolve, sep } from "path";
|
|
316
|
+
import ts4 from "typescript";
|
|
317
|
+
import { createRequire } from "module";
|
|
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
|
+
}
|
|
328
|
+
var SKIP_FILE = /\.(examples|pattern|test|spec|stories)\.tsx$|\.d\.ts$/;
|
|
329
|
+
var SKIP_DIR = /* @__PURE__ */ new Set(["node_modules", ".next", "dist", "build", "coverage"]);
|
|
330
|
+
var BASE_UI = /[\\/]@base-ui[\\/]react[\\/]/;
|
|
331
|
+
function rel(root, file) {
|
|
332
|
+
return relative(root, file).split(sep).join("/");
|
|
333
|
+
}
|
|
334
|
+
function listComponentFiles(root, system, out) {
|
|
335
|
+
const start = resolve(root, system);
|
|
336
|
+
const skip = out ? resolve(root, out) : void 0;
|
|
337
|
+
const found = [];
|
|
338
|
+
const walk3 = (dir) => {
|
|
339
|
+
for (const name of readdirSync(dir).sort()) {
|
|
340
|
+
const path = join(dir, name);
|
|
341
|
+
if (statSync(path).isDirectory()) {
|
|
342
|
+
if (!SKIP_DIR.has(name) && !name.startsWith(".") && path !== skip) walk3(path);
|
|
343
|
+
} else if (name.endsWith(".tsx") && !SKIP_FILE.test(name)) {
|
|
344
|
+
found.push(path);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
if (!existsSync(start)) throw new Error(`system folder not found: ${system}`);
|
|
349
|
+
walk3(start);
|
|
350
|
+
return found;
|
|
351
|
+
}
|
|
352
|
+
function compilerOptions(root, tsconfig) {
|
|
353
|
+
const path = tsconfig ? resolve(root, tsconfig) : ts4.findConfigFile(root, ts4.sys.fileExists, "tsconfig.json");
|
|
354
|
+
if (path && existsSync(path)) {
|
|
355
|
+
const parsed = ts4.getParsedCommandLineOfConfigFile(
|
|
356
|
+
path,
|
|
357
|
+
{},
|
|
358
|
+
{ ...ts4.sys, onUnRecoverableConfigFileDiagnostic: () => {
|
|
359
|
+
} }
|
|
360
|
+
);
|
|
361
|
+
if (parsed) return { ...parsed.options, noEmit: true };
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
jsx: ts4.JsxEmit.ReactJSX,
|
|
365
|
+
module: ts4.ModuleKind.ESNext,
|
|
366
|
+
moduleResolution: ts4.ModuleResolutionKind.Bundler,
|
|
367
|
+
target: ts4.ScriptTarget.ES2022,
|
|
368
|
+
strict: true,
|
|
369
|
+
esModuleInterop: true,
|
|
370
|
+
skipLibCheck: true,
|
|
371
|
+
noEmit: true
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function readComponents(options, warn) {
|
|
375
|
+
const root = resolve(options.root);
|
|
376
|
+
const files = listComponentFiles(root, options.system, options.out);
|
|
377
|
+
if (files.length === 0) return [];
|
|
378
|
+
const compiler = compilerOptions(root, options.tsconfig);
|
|
379
|
+
const program = ts4.createProgram(files, compiler);
|
|
380
|
+
const parser = docgen.withCompilerOptions(compiler, {
|
|
381
|
+
savePropValueAsString: true,
|
|
382
|
+
shouldExtractLiteralValuesFromEnum: true,
|
|
383
|
+
shouldRemoveUndefinedFromOptional: true,
|
|
384
|
+
shouldIncludePropTagMap: true,
|
|
385
|
+
shouldSortUnions: false,
|
|
386
|
+
propFilter: () => true
|
|
387
|
+
});
|
|
388
|
+
const docs = parser.parseWithProgramProvider(files, () => program);
|
|
389
|
+
const unionOrder = unionOrderReader(program);
|
|
390
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
391
|
+
for (const doc of docs) {
|
|
392
|
+
if (!/^[A-Z]/.test(doc.displayName)) continue;
|
|
393
|
+
const key = resolve(doc.filePath);
|
|
394
|
+
byFile.set(key, [...byFile.get(key) ?? [], doc]);
|
|
395
|
+
}
|
|
396
|
+
const out = [];
|
|
397
|
+
for (const file of files) {
|
|
398
|
+
const found = byFile.get(file);
|
|
399
|
+
if (!found) continue;
|
|
400
|
+
const sf = program.getSourceFile(file);
|
|
401
|
+
if (!sf) continue;
|
|
402
|
+
const entries = readFile(root, sf, found, importedConfigs(program, compiler), unionOrder, warn);
|
|
403
|
+
attachExamples(root, file, entries, warn);
|
|
404
|
+
out.push(...entries.map((e) => e.entry));
|
|
405
|
+
}
|
|
406
|
+
for (const entry of out) {
|
|
407
|
+
const specifier = importSpecifier(root, entry.source, compiler);
|
|
408
|
+
if (specifier) entry.import = specifier;
|
|
409
|
+
}
|
|
410
|
+
return out.sort((a, b) => compare(a.name, b.name) || compare(a.source, b.source));
|
|
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
|
+
}
|
|
437
|
+
function compare(a, b) {
|
|
438
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
439
|
+
}
|
|
440
|
+
function importedConfigs(program, compiler) {
|
|
441
|
+
const cache = /* @__PURE__ */ new Map();
|
|
442
|
+
const configsIn = (file) => {
|
|
443
|
+
if (!cache.has(file)) {
|
|
444
|
+
const sf = program.getSourceFile(file) ?? ts4.createSourceFile(file, readFileSync2(file, "utf8"), ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
|
|
445
|
+
cache.set(file, findVariantConfigs(sf));
|
|
446
|
+
}
|
|
447
|
+
return cache.get(file);
|
|
448
|
+
};
|
|
449
|
+
return (sf, localName) => {
|
|
450
|
+
const imported = namedImports(sf).get(localName);
|
|
451
|
+
if (!imported) return void 0;
|
|
452
|
+
const resolved = ts4.resolveModuleName(imported.from, sf.fileName, compiler, ts4.sys).resolvedModule?.resolvedFileName;
|
|
453
|
+
if (!resolved || /[\\/]node_modules[\\/]/.test(resolved) || resolved.endsWith(".d.ts")) return void 0;
|
|
454
|
+
return configsIn(resolved).find((c) => c.name === imported.name);
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function namedImports(sf) {
|
|
458
|
+
const out = /* @__PURE__ */ new Map();
|
|
459
|
+
for (const stmt of sf.statements) {
|
|
460
|
+
if (!ts4.isImportDeclaration(stmt) || !ts4.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
461
|
+
const named = stmt.importClause?.namedBindings;
|
|
462
|
+
if (!named || !ts4.isNamedImports(named)) continue;
|
|
463
|
+
for (const el of named.elements) {
|
|
464
|
+
out.set(el.name.text, { from: stmt.moduleSpecifier.text, name: (el.propertyName ?? el.name).text });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
469
|
+
function readFile(root, sf, docs, imported, unionOrder, warn) {
|
|
470
|
+
const source = rel(root, sf.fileName);
|
|
471
|
+
const configs = findVariantConfigs(sf);
|
|
472
|
+
const resolveConfig = configResolver(sf, configs, imported);
|
|
473
|
+
const imports = importSources(sf);
|
|
474
|
+
const locals = localDeclarations(sf);
|
|
475
|
+
const out = [];
|
|
476
|
+
for (const config of configs) checkTags(source, config.doc, `variant config ${config.name}`, warn);
|
|
477
|
+
for (const doc of docs) {
|
|
478
|
+
const decl = findDeclaration(sf, doc.displayName);
|
|
479
|
+
const statement = decl && (ts4.isVariableDeclaration(decl) ? decl.parent.parent : decl);
|
|
480
|
+
const jsdoc = statement ? leadingJSDoc(sf, statement) : void 0;
|
|
481
|
+
if (hasTag(jsdoc, "nodocgen")) continue;
|
|
482
|
+
checkTags(source, jsdoc, doc.displayName, warn);
|
|
483
|
+
const paramTypes = decl ? parameterTypeText(sf, decl) : "";
|
|
484
|
+
const config = decl ? linkConfig(decl, paramTypes, resolveConfig) : void 0;
|
|
485
|
+
const axisNames = new Set(config?.entry.axes.map((a) => a.name) ?? []);
|
|
486
|
+
const props = readProps(doc, axisNames, unionOrder);
|
|
487
|
+
const entry = {
|
|
488
|
+
name: tag(jsdoc, "name") || doc.displayName,
|
|
489
|
+
export: doc.displayName,
|
|
490
|
+
source,
|
|
491
|
+
props: props.entries,
|
|
492
|
+
slots: decl ? [...slotsOf(decl, locals)].sort() : []
|
|
493
|
+
};
|
|
494
|
+
const description = describe(jsdoc) ?? config?.description;
|
|
495
|
+
if (description) entry.description = description;
|
|
496
|
+
const category = tag(jsdoc, "category");
|
|
497
|
+
if (category) entry.category = category;
|
|
498
|
+
readStatus(entry, jsdoc, source, warn);
|
|
499
|
+
const usage = {
|
|
500
|
+
use: tag(jsdoc, "use") || void 0,
|
|
501
|
+
avoid: tag(jsdoc, "avoid") || void 0,
|
|
502
|
+
instead: list(tags(jsdoc, "instead").join(","), /\s*,\s*/)
|
|
503
|
+
};
|
|
504
|
+
if (usage.use || usage.avoid || usage.instead.length) {
|
|
505
|
+
entry.usage = {
|
|
506
|
+
...usage.use ? { use: oneLine(usage.use) } : {},
|
|
507
|
+
...usage.avoid ? { avoid: oneLine(usage.avoid) } : {},
|
|
508
|
+
...usage.instead.length ? { instead: usage.instead } : {}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
const snippets = tags(jsdoc, "example").filter(Boolean);
|
|
512
|
+
if (snippets.length) entry.snippets = snippets;
|
|
513
|
+
const replaces = list(tags(jsdoc, "replaces").join(" "), /[\s,]+/);
|
|
514
|
+
if (replaces.length) entry.replaces = replaces;
|
|
515
|
+
const role = tag(jsdoc, "role");
|
|
516
|
+
const keyboard = list(tags(jsdoc, "keyboard").join(","), /\s*,\s*/);
|
|
517
|
+
if (role || keyboard.length) entry.a11y = { ...role ? { role: role.trim() } : {}, ...keyboard.length ? { keyboard } : {} };
|
|
518
|
+
const see = tags(jsdoc, "see").filter(Boolean);
|
|
519
|
+
if (see.length) entry.see = see;
|
|
520
|
+
const primitive = tag(jsdoc, "primitive");
|
|
521
|
+
if (primitive) entry.primitive = primitive;
|
|
522
|
+
const aria = tag(jsdoc, "aria");
|
|
523
|
+
if (aria) entry.aria = aria;
|
|
524
|
+
const inherits = inheritsFrom(paramTypes, imports);
|
|
525
|
+
if (inherits.length) entry.inherits = inherits;
|
|
526
|
+
const fromProps = () => variantsFromProps(props.order.filter((n) => props.own.has(n)), props.entries);
|
|
527
|
+
const fallback = config && config.entry.axes.length === 0 ? fromProps() : void 0;
|
|
528
|
+
const variants = fallback ? { ...fallback, ...config.entry.parts ? { parts: config.entry.parts } : {} } : config?.entry ?? fromProps();
|
|
529
|
+
if (variants) entry.variants = variants;
|
|
530
|
+
if (config) alignAxisProps(entry.props, config.entry);
|
|
531
|
+
out.push({ entry, position: decl?.getStart(sf) ?? Number.MAX_SAFE_INTEGER });
|
|
532
|
+
}
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
function findDeclaration(sf, name) {
|
|
536
|
+
for (const stmt of sf.statements) {
|
|
537
|
+
if (ts4.isFunctionDeclaration(stmt) && stmt.name?.text === name) return stmt;
|
|
538
|
+
if (ts4.isVariableStatement(stmt)) {
|
|
539
|
+
for (const d of stmt.declarationList.declarations) {
|
|
540
|
+
if (ts4.isIdentifier(d.name) && d.name.text === name) return d;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return void 0;
|
|
545
|
+
}
|
|
546
|
+
function isExported(node) {
|
|
547
|
+
return ts4.canHaveModifiers(node) && (ts4.getModifiers(node) ?? []).some((m) => m.kind === ts4.SyntaxKind.ExportKeyword);
|
|
548
|
+
}
|
|
549
|
+
function localDeclarations(sf) {
|
|
550
|
+
const out = /* @__PURE__ */ new Map();
|
|
551
|
+
for (const stmt of sf.statements) {
|
|
552
|
+
if (isExported(stmt)) continue;
|
|
553
|
+
if (ts4.isFunctionDeclaration(stmt) && stmt.name) out.set(stmt.name.text, stmt);
|
|
554
|
+
if (ts4.isVariableStatement(stmt)) {
|
|
555
|
+
for (const d of stmt.declarationList.declarations) if (ts4.isIdentifier(d.name)) out.set(d.name.text, d);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return out;
|
|
559
|
+
}
|
|
560
|
+
function importSources(sf) {
|
|
561
|
+
const out = /* @__PURE__ */ new Map();
|
|
562
|
+
for (const stmt of sf.statements) {
|
|
563
|
+
if (!ts4.isImportDeclaration(stmt) || !ts4.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
564
|
+
const from = stmt.moduleSpecifier.text;
|
|
565
|
+
const clause = stmt.importClause;
|
|
566
|
+
if (clause?.name) out.set(clause.name.text, from);
|
|
567
|
+
const named = clause?.namedBindings;
|
|
568
|
+
if (named && ts4.isNamespaceImport(named)) out.set(named.name.text, from);
|
|
569
|
+
if (named && ts4.isNamedImports(named)) {
|
|
570
|
+
for (const el of named.elements) {
|
|
571
|
+
const imported = (el.propertyName ?? el.name).text;
|
|
572
|
+
out.set(el.name.text, imported === "default" ? from : `${from}:${imported}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return out;
|
|
577
|
+
}
|
|
578
|
+
function functionOf(decl) {
|
|
579
|
+
if (ts4.isFunctionDeclaration(decl)) return decl;
|
|
580
|
+
let init = decl.initializer;
|
|
581
|
+
while (init && ts4.isCallExpression(init)) {
|
|
582
|
+
init = init.arguments.find((a) => ts4.isArrowFunction(a) || ts4.isFunctionExpression(a) || ts4.isCallExpression(a));
|
|
583
|
+
}
|
|
584
|
+
if (init && (ts4.isArrowFunction(init) || ts4.isFunctionExpression(init))) return init;
|
|
585
|
+
return void 0;
|
|
586
|
+
}
|
|
587
|
+
function parameterTypeText(sf, decl) {
|
|
588
|
+
const fn = functionOf(decl);
|
|
589
|
+
const param = fn?.parameters[0];
|
|
590
|
+
let typeNode = param?.type;
|
|
591
|
+
if (!typeNode && ts4.isVariableDeclaration(decl) && decl.type && ts4.isTypeReferenceNode(decl.type)) {
|
|
592
|
+
typeNode = decl.type.typeArguments?.[0];
|
|
593
|
+
}
|
|
594
|
+
if (!typeNode && ts4.isVariableDeclaration(decl) && decl.initializer && ts4.isCallExpression(decl.initializer)) {
|
|
595
|
+
const args = decl.initializer.typeArguments;
|
|
596
|
+
typeNode = args?.[args.length - 1];
|
|
597
|
+
}
|
|
598
|
+
if (!typeNode) return "";
|
|
599
|
+
const localTypes = /* @__PURE__ */ new Map();
|
|
600
|
+
for (const stmt of sf.statements) {
|
|
601
|
+
if (ts4.isInterfaceDeclaration(stmt) || ts4.isTypeAliasDeclaration(stmt)) localTypes.set(stmt.name.text, stmt);
|
|
602
|
+
}
|
|
603
|
+
const seen = /* @__PURE__ */ new Set();
|
|
604
|
+
const parts = [];
|
|
605
|
+
const expand = (node, depth) => {
|
|
606
|
+
parts.push(node.getText(sf));
|
|
607
|
+
if (depth >= 3) return;
|
|
608
|
+
const visit = (n) => {
|
|
609
|
+
if (ts4.isTypeReferenceNode(n) || ts4.isExpressionWithTypeArguments(n)) {
|
|
610
|
+
const name = ts4.isTypeReferenceNode(n) ? n.typeName.getText(sf) : n.expression.getText(sf);
|
|
611
|
+
const local = localTypes.get(name);
|
|
612
|
+
if (local && !seen.has(name)) {
|
|
613
|
+
seen.add(name);
|
|
614
|
+
expand(local, depth + 1);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
ts4.forEachChild(n, visit);
|
|
618
|
+
};
|
|
619
|
+
visit(node);
|
|
620
|
+
};
|
|
621
|
+
expand(typeNode, 0);
|
|
622
|
+
return parts.join("\n");
|
|
623
|
+
}
|
|
624
|
+
function configResolver(sf, configs, imported) {
|
|
625
|
+
const local = new Map(configs.map((c) => [c.name, c]));
|
|
626
|
+
const resolve8 = (name) => local.get(name) ?? imported(sf, name);
|
|
627
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
628
|
+
for (const stmt of sf.statements) {
|
|
629
|
+
if (!ts4.isVariableStatement(stmt)) continue;
|
|
630
|
+
for (const d of stmt.declarationList.declarations) {
|
|
631
|
+
if (ts4.isIdentifier(d.name) && d.initializer && ts4.isCallExpression(d.initializer) && ts4.isIdentifier(d.initializer.expression)) {
|
|
632
|
+
aliases.set(d.name.text, d.initializer.expression.text);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const find = (name) => resolve8(name) ?? (aliases.has(name) ? resolve8(aliases.get(name)) : void 0);
|
|
637
|
+
return (name) => {
|
|
638
|
+
const config = find(name);
|
|
639
|
+
return config && withBase(config, find, /* @__PURE__ */ new Set([config.name]));
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function withBase(config, find, seen) {
|
|
643
|
+
if (!config.extendsName || seen.has(config.extendsName)) return config;
|
|
644
|
+
const found = find(config.extendsName);
|
|
645
|
+
if (!found) return config;
|
|
646
|
+
seen.add(found.name);
|
|
647
|
+
const base = withBase(found, find, seen);
|
|
648
|
+
const axes = base.entry.axes.map((a) => ({ ...a, options: [...a.options] }));
|
|
649
|
+
for (const local of config.entry.axes) {
|
|
650
|
+
const inherited = axes.find((a) => a.name === local.name);
|
|
651
|
+
if (!inherited) {
|
|
652
|
+
axes.push(local);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
for (const o of local.options) {
|
|
656
|
+
const i = inherited.options.findIndex((x) => x.name === o.name);
|
|
657
|
+
if (i === -1) inherited.options.push(o);
|
|
658
|
+
else inherited.options[i] = o.description ? o : inherited.options[i];
|
|
659
|
+
}
|
|
660
|
+
if (local.default !== void 0) inherited.default = local.default;
|
|
661
|
+
}
|
|
662
|
+
const entry = { ...config.entry, axes };
|
|
663
|
+
const compound = [...base.entry.compound ?? [], ...config.entry.compound ?? []];
|
|
664
|
+
if (compound.length) entry.compound = compound;
|
|
665
|
+
const parts = [.../* @__PURE__ */ new Set([...base.entry.parts ?? [], ...config.entry.parts ?? []])];
|
|
666
|
+
if (parts.length) entry.parts = parts;
|
|
667
|
+
return { ...config, entry, description: config.description ?? base.description };
|
|
668
|
+
}
|
|
669
|
+
function linkConfig(decl, paramTypes, resolve8) {
|
|
670
|
+
for (const m of paramTypes.matchAll(/VariantProps\s*<\s*typeof\s+(\w+)\s*>/g)) {
|
|
671
|
+
const c = resolve8(m[1]);
|
|
672
|
+
if (c) return c;
|
|
673
|
+
}
|
|
674
|
+
let found;
|
|
675
|
+
const visit = (n) => {
|
|
676
|
+
if (found) return;
|
|
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);
|
|
679
|
+
if (!found) ts4.forEachChild(n, visit);
|
|
680
|
+
};
|
|
681
|
+
visit(decl);
|
|
682
|
+
return found;
|
|
683
|
+
}
|
|
684
|
+
var ELEMENT_OF = {
|
|
685
|
+
Anchor: "a",
|
|
686
|
+
Div: "div",
|
|
687
|
+
Span: "span",
|
|
688
|
+
Paragraph: "p",
|
|
689
|
+
Heading: "h1",
|
|
690
|
+
UList: "ul",
|
|
691
|
+
OList: "ol",
|
|
692
|
+
LI: "li",
|
|
693
|
+
Image: "img",
|
|
694
|
+
TextArea: "textarea"
|
|
695
|
+
};
|
|
696
|
+
function inheritsFrom(paramTypes, imports) {
|
|
697
|
+
const found = /* @__PURE__ */ new Set();
|
|
698
|
+
for (const m of paramTypes.matchAll(/ComponentProps(?:WithRef|WithoutRef)?\s*<\s*["']([a-z][\w-]*)["']\s*>/g)) {
|
|
699
|
+
found.add(m[1]);
|
|
700
|
+
}
|
|
701
|
+
for (const m of paramTypes.matchAll(/ComponentProps(?:WithRef|WithoutRef)?\s*<\s*typeof\s+([\w.]+)\s*>/g)) {
|
|
702
|
+
const [head, ...rest] = m[1].split(".");
|
|
703
|
+
const from = imports.get(head);
|
|
704
|
+
if (!from) found.add(m[1]);
|
|
705
|
+
else if (from.includes(":")) found.add(rest.length ? `${from}.${rest.join(".")}` : from);
|
|
706
|
+
else found.add(rest.length ? `${from}:${rest.join(".")}` : from);
|
|
707
|
+
}
|
|
708
|
+
for (const m of paramTypes.matchAll(/\w*HTMLAttributes\s*<\s*HTML(\w*)Element\s*>/g)) {
|
|
709
|
+
const el = m[1];
|
|
710
|
+
found.add(el ? ELEMENT_OF[el] ?? el.toLowerCase() : "element");
|
|
711
|
+
}
|
|
712
|
+
return [...found].sort();
|
|
713
|
+
}
|
|
714
|
+
function slotsOf(decl, locals) {
|
|
715
|
+
const slots = /* @__PURE__ */ new Set();
|
|
716
|
+
const seen = /* @__PURE__ */ new Set([decl]);
|
|
717
|
+
const visit = (n) => {
|
|
718
|
+
if (ts4.isJsxAttribute(n) && n.name.getText() === "data-slot" && n.initializer) {
|
|
719
|
+
const init = n.initializer;
|
|
720
|
+
if (ts4.isStringLiteral(init)) slots.add(init.text);
|
|
721
|
+
else if (ts4.isJsxExpression(init) && init.expression && ts4.isStringLiteralLike(init.expression))
|
|
722
|
+
slots.add(init.expression.text);
|
|
723
|
+
} else if (ts4.isPropertyAssignment(n) && ts4.isStringLiteral(n.name) && n.name.text === "data-slot" && ts4.isStringLiteralLike(n.initializer)) {
|
|
724
|
+
slots.add(n.initializer.text);
|
|
725
|
+
} else if (ts4.isIdentifier(n)) {
|
|
726
|
+
const local = locals.get(n.text);
|
|
727
|
+
if (local && !seen.has(local)) {
|
|
728
|
+
seen.add(local);
|
|
729
|
+
visit(local);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
ts4.forEachChild(n, visit);
|
|
733
|
+
};
|
|
734
|
+
visit(decl);
|
|
735
|
+
return slots;
|
|
736
|
+
}
|
|
737
|
+
function unionOrderReader(program) {
|
|
738
|
+
return (prop, values, componentFile) => {
|
|
739
|
+
const want = new Set(values);
|
|
740
|
+
const files = [.../* @__PURE__ */ new Set([...declarationsOf(prop).map((d) => d.fileName), componentFile])];
|
|
741
|
+
for (const file of files) {
|
|
742
|
+
const sf = program.getSourceFile(file);
|
|
743
|
+
if (!sf) continue;
|
|
744
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
745
|
+
sf.forEachChild((n) => {
|
|
746
|
+
if (ts4.isTypeAliasDeclaration(n)) aliases.set(n.name.text, n.type);
|
|
747
|
+
});
|
|
748
|
+
let found;
|
|
749
|
+
const visit = (n) => {
|
|
750
|
+
if (found) return;
|
|
751
|
+
if ((ts4.isPropertySignature(n) || ts4.isPropertyDeclaration(n)) && n.name && ts4.isIdentifier(n.name) && n.name.text === prop.name && n.type) {
|
|
752
|
+
const order = literalsOf(n.type, aliases, 0);
|
|
753
|
+
if (order && order.length === want.size && order.every((v) => want.has(v))) found = order;
|
|
754
|
+
}
|
|
755
|
+
ts4.forEachChild(n, visit);
|
|
756
|
+
};
|
|
757
|
+
visit(sf);
|
|
758
|
+
if (found) return found;
|
|
759
|
+
}
|
|
760
|
+
return void 0;
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
function literalsOf(node, aliases, depth) {
|
|
764
|
+
if (depth > 4) return void 0;
|
|
765
|
+
if (ts4.isParenthesizedTypeNode(node)) return literalsOf(node.type, aliases, depth + 1);
|
|
766
|
+
if (ts4.isTypeReferenceNode(node) && ts4.isIdentifier(node.typeName) && aliases.has(node.typeName.text))
|
|
767
|
+
return literalsOf(aliases.get(node.typeName.text), aliases, depth + 1);
|
|
768
|
+
const members = ts4.isUnionTypeNode(node) ? node.types : [node];
|
|
769
|
+
const out = [];
|
|
770
|
+
for (const m of members) {
|
|
771
|
+
if (ts4.isLiteralTypeNode(m)) {
|
|
772
|
+
if (ts4.isStringLiteral(m.literal) || ts4.isNumericLiteral(m.literal)) out.push(m.literal.text);
|
|
773
|
+
else if (m.literal.kind === ts4.SyntaxKind.NullKeyword) continue;
|
|
774
|
+
else return void 0;
|
|
775
|
+
} else if (m.kind === ts4.SyntaxKind.UndefinedKeyword) {
|
|
776
|
+
continue;
|
|
777
|
+
} else {
|
|
778
|
+
const inner = literalsOf(m, aliases, depth + 1);
|
|
779
|
+
if (!inner) return void 0;
|
|
780
|
+
out.push(...inner);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return out;
|
|
784
|
+
}
|
|
785
|
+
function ordered(values, source) {
|
|
786
|
+
return source ?? [...values].sort(compare);
|
|
787
|
+
}
|
|
788
|
+
function unionText(type, values) {
|
|
789
|
+
const raw = type.name === "enum" && Array.isArray(type.value) ? type.value.map((v) => String(v.value)) : splitUnion(type.name) ?? [];
|
|
790
|
+
const quoted = new Map(raw.map((r) => [r.replace(/^(["'`])(.*)\1$/, "$2"), r]));
|
|
791
|
+
const parts = values.map((v) => quoted.get(v) ?? JSON.stringify(v));
|
|
792
|
+
if (raw.includes("null")) parts.push("null");
|
|
793
|
+
return parts.join(" | ");
|
|
794
|
+
}
|
|
795
|
+
function readProps(doc, axes, unionOrder) {
|
|
796
|
+
const entries = {};
|
|
797
|
+
const order = [];
|
|
798
|
+
const own = /* @__PURE__ */ new Set();
|
|
799
|
+
for (const [name, prop] of Object.entries(doc.props)) {
|
|
800
|
+
if (!keepProp(prop, axes)) continue;
|
|
801
|
+
if (declaredInProject(prop)) own.add(name);
|
|
802
|
+
const checked = literalValues(prop.type);
|
|
803
|
+
const values = checked && ordered(checked, unionOrder(prop, checked, doc.filePath));
|
|
804
|
+
const entry = {
|
|
805
|
+
type: values ? unionText(prop.type, values) : typeText(prop.type, void 0),
|
|
806
|
+
required: prop.required
|
|
807
|
+
};
|
|
808
|
+
if (values) entry.values = values;
|
|
809
|
+
const def = prop.defaultValue?.value;
|
|
810
|
+
if (def !== void 0 && def !== null && def !== "") entry.default = String(def);
|
|
811
|
+
if (prop.description) entry.description = prop.description.trim();
|
|
812
|
+
const propTags = prop.tags ?? {};
|
|
813
|
+
if ("deprecated" in propTags) entry.deprecated = propTags.deprecated ?? "";
|
|
814
|
+
if (!own.has(name)) {
|
|
815
|
+
const pkg = packageOf(declarationsOf(prop));
|
|
816
|
+
if (pkg) entry.from = pkg;
|
|
817
|
+
}
|
|
818
|
+
entries[name] = entry;
|
|
819
|
+
order.push(name);
|
|
820
|
+
}
|
|
821
|
+
return { entries, order, own };
|
|
822
|
+
}
|
|
823
|
+
function declarationsOf(prop) {
|
|
824
|
+
return prop.declarations ?? (prop.parent ? [prop.parent] : []);
|
|
825
|
+
}
|
|
826
|
+
function packageOf(decls) {
|
|
827
|
+
for (const d of decls) {
|
|
828
|
+
const parts = d.fileName.split(/[\\/]node_modules[\\/]/);
|
|
829
|
+
if (parts.length < 2) continue;
|
|
830
|
+
const tail = parts[parts.length - 1].split(/[\\/]/);
|
|
831
|
+
return tail[0].startsWith("@") ? `${tail[0]}/${tail[1]}` : tail[0];
|
|
832
|
+
}
|
|
833
|
+
return void 0;
|
|
834
|
+
}
|
|
835
|
+
function declaredInProject(prop) {
|
|
836
|
+
const decls = declarationsOf(prop);
|
|
837
|
+
return decls.length === 0 || decls.some((d) => !/[\\/]node_modules[\\/]/.test(d.fileName));
|
|
838
|
+
}
|
|
839
|
+
function keepProp(prop, axes) {
|
|
840
|
+
if (axes.has(prop.name)) return true;
|
|
841
|
+
const decls = declarationsOf(prop);
|
|
842
|
+
if (decls.length === 0) return true;
|
|
843
|
+
return decls.some((d) => !/[\\/]node_modules[\\/]/.test(d.fileName) || BASE_UI.test(d.fileName));
|
|
844
|
+
}
|
|
845
|
+
function literalValues(type) {
|
|
846
|
+
const raws = type.name === "enum" && Array.isArray(type.value) ? type.value.map((v) => String(v.value)) : splitUnion(type.name);
|
|
847
|
+
if (!raws || raws.length < 2) return void 0;
|
|
848
|
+
const values = [];
|
|
849
|
+
for (const raw of raws) {
|
|
850
|
+
if (raw === "null" || raw === "undefined") continue;
|
|
851
|
+
const str = /^(["'`])(.*)\1$/.exec(raw);
|
|
852
|
+
if (str) values.push(str[2]);
|
|
853
|
+
else if (/^-?\d+(\.\d+)?$/.test(raw)) values.push(raw);
|
|
854
|
+
else return void 0;
|
|
855
|
+
}
|
|
856
|
+
return values.length ? values : void 0;
|
|
857
|
+
}
|
|
858
|
+
function splitUnion(text2) {
|
|
859
|
+
if (!text2.includes("|") || /[(){}<>\[\]]/.test(text2.replace(/(["'`]).*?\1/g, ""))) return void 0;
|
|
860
|
+
return text2.split("|").map((p) => p.trim()).filter(Boolean);
|
|
861
|
+
}
|
|
862
|
+
function typeText(type, values) {
|
|
863
|
+
if (values && type.name === "enum" && type.value) return type.value.map((v) => v.value).join(" | ");
|
|
864
|
+
return (type.raw ?? type.name).replace(/\s+/g, " ").trim();
|
|
865
|
+
}
|
|
866
|
+
function alignAxisProps(props, variants) {
|
|
867
|
+
for (const axis of variants.axes) {
|
|
868
|
+
const prop = props[axis.name];
|
|
869
|
+
if (!prop) continue;
|
|
870
|
+
if (prop.values) {
|
|
871
|
+
const order = axis.options.map((o) => o.name);
|
|
872
|
+
prop.values = [...prop.values].sort((a, b) => rank(order, a) - rank(order, b));
|
|
873
|
+
const nullable = /\|\s*null\b/.test(prop.type);
|
|
874
|
+
prop.type = [...prop.values.map((v) => JSON.stringify(v)), ...nullable ? ["null"] : []].join(" | ");
|
|
875
|
+
}
|
|
876
|
+
if (prop.default === void 0 && axis.default !== void 0) prop.default = axis.default;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
function rank(order, value) {
|
|
880
|
+
const i = order.indexOf(value);
|
|
881
|
+
return i === -1 ? order.length : i;
|
|
882
|
+
}
|
|
883
|
+
function variantsFromProps(order, props) {
|
|
884
|
+
const axes = [];
|
|
885
|
+
for (const name of order) {
|
|
886
|
+
const p = props[name];
|
|
887
|
+
if (!p.values || p.values.every((v) => v === "true" || v === "false")) continue;
|
|
888
|
+
if (!p.values.every((v) => !/^-?\d/.test(v))) continue;
|
|
889
|
+
const axis = { name, options: p.values.map((v) => ({ name: v })) };
|
|
890
|
+
if (p.default !== void 0) axis.default = p.default.replace(/^(["'`])(.*)\1$/, "$2");
|
|
891
|
+
axes.push(axis);
|
|
892
|
+
}
|
|
893
|
+
return axes.length ? { from: "props", axes } : void 0;
|
|
894
|
+
}
|
|
895
|
+
function attachExamples(root, file, components, warn) {
|
|
896
|
+
const path = join(dirname(file), basename(file, ".tsx") + ".examples.tsx");
|
|
897
|
+
if (!existsSync(path) || components.length === 0) return;
|
|
898
|
+
const source = rel(root, path);
|
|
899
|
+
const { exports } = readExampleFile(path, source, warn);
|
|
900
|
+
if (exports.length === 0) {
|
|
901
|
+
warn({ file: source, message: "examples file exports nothing: export canonical, do\u2026 and dont\u2026 examples" });
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
const stem = basename(file, ".tsx").replace(/[-_]/g, "").toLowerCase();
|
|
905
|
+
const owner = components.find((c) => c.entry.export.toLowerCase() === stem) ?? [...components].sort((a, b) => a.position - b.position)[0];
|
|
906
|
+
const examples = classifyExamples(source, exports);
|
|
907
|
+
if (!examples.canonical) warn({ file: source, message: `no canonical example for ${owner.entry.name}: export const canonical` });
|
|
908
|
+
owner.entry.examples = examples;
|
|
909
|
+
}
|
|
910
|
+
var STATUSES = ["stable", "emerging", "deprecated"];
|
|
911
|
+
function readStatus(entry, jsdoc, source, warn) {
|
|
912
|
+
const status = tag(jsdoc, "status");
|
|
913
|
+
if (status !== void 0) {
|
|
914
|
+
const [word, ...rest] = status.trim().split(/\s+/);
|
|
915
|
+
if (STATUSES.includes(word)) {
|
|
916
|
+
entry.status = word;
|
|
917
|
+
if (rest.length) entry.statusNote = oneLine(rest.join(" "));
|
|
918
|
+
} else {
|
|
919
|
+
warn({ file: source, line: jsdoc?.line, message: `@status "${word}" on ${entry.name} is not one of ${STATUSES.join(", ")}` });
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (hasTag(jsdoc, "deprecated")) {
|
|
923
|
+
entry.status = "deprecated";
|
|
924
|
+
const reason = tag(jsdoc, "deprecated");
|
|
925
|
+
if (reason) entry.statusNote = oneLine(reason);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
function list(text2, separator) {
|
|
929
|
+
return text2.split(separator).map((x) => x.trim()).filter(Boolean);
|
|
930
|
+
}
|
|
931
|
+
function oneLine(text2) {
|
|
932
|
+
return text2.replace(/\s*\n\s*/g, " ").trim();
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// src/guidance.ts
|
|
936
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
937
|
+
import { basename as basename2, dirname as dirname2, join as join2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
938
|
+
import ts5 from "typescript";
|
|
939
|
+
var rel2 = (root, file) => relative2(root, file).split(sep2).join("/");
|
|
940
|
+
function walk(dir, accept, skip) {
|
|
941
|
+
if (!existsSync2(dir)) return [];
|
|
942
|
+
const out = [];
|
|
943
|
+
for (const name of readdirSync2(dir).sort()) {
|
|
944
|
+
const path = join2(dir, name);
|
|
945
|
+
if (statSync2(path).isDirectory()) {
|
|
946
|
+
if (name !== "node_modules" && !name.startsWith(".") && path !== skip) out.push(...walk(path, accept, skip));
|
|
947
|
+
} else if (accept(name)) out.push(path);
|
|
948
|
+
}
|
|
949
|
+
return out;
|
|
950
|
+
}
|
|
951
|
+
function readJSON(path, source, warn) {
|
|
952
|
+
try {
|
|
953
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
954
|
+
} catch (e) {
|
|
955
|
+
warn({ file: source, message: `not valid JSON: ${e.message}` });
|
|
956
|
+
return void 0;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
var strings = (x) => Array.isArray(x) && x.every((v) => typeof v === "string") && x.length ? x : void 0;
|
|
960
|
+
var text = (x) => typeof x === "string" && x.trim() ? x.trim() : void 0;
|
|
961
|
+
var RULE_FIELDS = /* @__PURE__ */ new Set([
|
|
962
|
+
"$schema",
|
|
963
|
+
"kind",
|
|
964
|
+
"title",
|
|
965
|
+
"statement",
|
|
966
|
+
"rationale",
|
|
967
|
+
"threshold",
|
|
968
|
+
"appliesTo",
|
|
969
|
+
"exceptions",
|
|
970
|
+
"tokens",
|
|
971
|
+
"status",
|
|
972
|
+
"check"
|
|
973
|
+
]);
|
|
974
|
+
function readRules(root, dir, warn) {
|
|
975
|
+
const out = [];
|
|
976
|
+
for (const path of walk(resolve2(root, dir), (n) => n.endsWith(".rule.json"))) {
|
|
977
|
+
const source = rel2(root, path);
|
|
978
|
+
const raw = readJSON(path, source, warn);
|
|
979
|
+
if (!raw || typeof raw !== "object") continue;
|
|
980
|
+
for (const key of Object.keys(raw)) if (!RULE_FIELDS.has(key)) warn({ file: source, message: `unknown rule field "${key}"` });
|
|
981
|
+
const statement = text(raw.statement);
|
|
982
|
+
const title = text(raw.title);
|
|
983
|
+
if (!statement || !title) {
|
|
984
|
+
warn({ file: source, message: "a rule needs a title and a statement; skipped" });
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
const kind = raw.kind === "brand" || raw.kind === "interface" ? raw.kind : void 0;
|
|
988
|
+
if (!kind) warn({ file: source, message: `kind must be "brand" or "interface"; read as "interface"` });
|
|
989
|
+
const status = raw.status === "confirmed" || raw.status === "assumption" ? raw.status : void 0;
|
|
990
|
+
if (!status) warn({ file: source, message: `status must be "confirmed" or "assumption"; read as "assumption" until someone confirms it` });
|
|
991
|
+
const rule = {
|
|
992
|
+
id: basename2(path, ".rule.json"),
|
|
993
|
+
kind: kind ?? "interface",
|
|
994
|
+
title,
|
|
995
|
+
statement,
|
|
996
|
+
status: status ?? "assumption",
|
|
997
|
+
source
|
|
998
|
+
};
|
|
999
|
+
const rationale = text(raw.rationale);
|
|
1000
|
+
if (rationale) rule.rationale = rationale;
|
|
1001
|
+
const threshold = text(raw.threshold);
|
|
1002
|
+
if (threshold) rule.threshold = threshold;
|
|
1003
|
+
const appliesTo = strings(raw.appliesTo);
|
|
1004
|
+
if (appliesTo) rule.appliesTo = appliesTo;
|
|
1005
|
+
const exceptions = strings(raw.exceptions);
|
|
1006
|
+
if (exceptions) rule.exceptions = exceptions;
|
|
1007
|
+
const tokens = strings(raw.tokens);
|
|
1008
|
+
if (tokens) rule.tokens = tokens;
|
|
1009
|
+
const check = raw.check;
|
|
1010
|
+
if (check && typeof check === "object") {
|
|
1011
|
+
const kinds = ["test", "lint", "audit", "manual"];
|
|
1012
|
+
const k = kinds.find((x) => x === check.kind);
|
|
1013
|
+
if (!k) warn({ file: source, message: `check.kind must be one of ${kinds.join(", ")}` });
|
|
1014
|
+
else {
|
|
1015
|
+
rule.check = { kind: k };
|
|
1016
|
+
const ref = text(check.ref);
|
|
1017
|
+
if (ref) rule.check.ref = ref;
|
|
1018
|
+
const description = text(check.description);
|
|
1019
|
+
if (description) rule.check.description = description;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
out.push(rule);
|
|
1023
|
+
}
|
|
1024
|
+
return out.sort((a, b) => compare(a.id, b.id));
|
|
1025
|
+
}
|
|
1026
|
+
function readPatterns(root, system, components, compiler, warn, skip) {
|
|
1027
|
+
const out = [];
|
|
1028
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
1029
|
+
for (const c of components) bySource.set(c.source, [...bySource.get(c.source) ?? [], c]);
|
|
1030
|
+
for (const path of walk(resolve2(root, system), (n) => n.endsWith(".pattern.tsx"), skip && resolve2(root, skip))) {
|
|
1031
|
+
const source = rel2(root, path);
|
|
1032
|
+
const { sf, doc, exports } = readExampleFile(path, source, warn, "pattern composition");
|
|
1033
|
+
const id = basename2(path, ".pattern.tsx");
|
|
1034
|
+
const used = /* @__PURE__ */ new Set();
|
|
1035
|
+
for (const stmt of sf.statements) {
|
|
1036
|
+
if (!ts5.isImportDeclaration(stmt) || !ts5.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
1037
|
+
const resolved = ts5.resolveModuleName(stmt.moduleSpecifier.text, path, compiler, ts5.sys).resolvedModule?.resolvedFileName;
|
|
1038
|
+
const candidates = resolved ? bySource.get(rel2(root, resolved)) : void 0;
|
|
1039
|
+
const named = stmt.importClause?.namedBindings;
|
|
1040
|
+
if (!candidates || !named || !ts5.isNamedImports(named)) continue;
|
|
1041
|
+
for (const el of named.elements) {
|
|
1042
|
+
const imported = (el.propertyName ?? el.name).text;
|
|
1043
|
+
const c = candidates.find((x) => x.export === imported);
|
|
1044
|
+
if (c) used.add(c.name);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
if (exports.length === 0) warn({ file: source, message: "pattern exports no compositions" });
|
|
1048
|
+
const pattern = {
|
|
1049
|
+
id,
|
|
1050
|
+
name: tag(doc, "name")?.trim() || titleCase(id),
|
|
1051
|
+
components: [...used].sort(compare),
|
|
1052
|
+
examples: exports,
|
|
1053
|
+
source
|
|
1054
|
+
};
|
|
1055
|
+
const description = describe(doc);
|
|
1056
|
+
if (description) pattern.description = description;
|
|
1057
|
+
const status = tag(doc, "status")?.trim().split(/\s+/)[0];
|
|
1058
|
+
if (status) {
|
|
1059
|
+
if (["stable", "emerging", "deprecated"].includes(status)) pattern.status = status;
|
|
1060
|
+
else warn({ file: source, message: `@status "${status}" is not one of stable, emerging, deprecated` });
|
|
1061
|
+
}
|
|
1062
|
+
out.push(pattern);
|
|
1063
|
+
}
|
|
1064
|
+
return out.sort((a, b) => compare(a.name, b.name));
|
|
1065
|
+
}
|
|
1066
|
+
function titleCase(id) {
|
|
1067
|
+
const words = id.split(/[-_]+/).filter(Boolean);
|
|
1068
|
+
return words.map((w, i) => i === 0 ? w[0].toUpperCase() + w.slice(1) : w).join(" ");
|
|
1069
|
+
}
|
|
1070
|
+
function buildTaxonomy(root, curatedPath, inputs, warn) {
|
|
1071
|
+
const terms = /* @__PURE__ */ new Map();
|
|
1072
|
+
const add = (term) => {
|
|
1073
|
+
const key = term.name.toLowerCase();
|
|
1074
|
+
if (!terms.has(key)) terms.set(key, term);
|
|
1075
|
+
};
|
|
1076
|
+
for (const c of inputs.components) add({ name: c.name, kind: "component", ref: c.source });
|
|
1077
|
+
for (const p of inputs.patterns) add({ name: p.name, kind: "pattern", ref: p.source });
|
|
1078
|
+
for (const r of inputs.rules) add({ name: r.title, kind: "rule", ref: r.id });
|
|
1079
|
+
for (const t of inputs.tokens) {
|
|
1080
|
+
if (t.tier === "color" && !t.name.startsWith("--color-")) add({ name: t.name.slice(2), kind: "token", ref: t.name });
|
|
1081
|
+
}
|
|
1082
|
+
const notVocabulary = /^(\d*x*[sl]|sm|md|lg|none|default|true|false|auto|full|\d+)$/;
|
|
1083
|
+
for (const c of inputs.components) {
|
|
1084
|
+
for (const axis of c.variants?.axes ?? []) {
|
|
1085
|
+
add({ name: axis.name, kind: "variant", ref: c.name });
|
|
1086
|
+
for (const o of axis.options) if (!notVocabulary.test(o.name)) add({ name: o.name, kind: "variant", ref: axis.name });
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
const path = resolve2(root, curatedPath);
|
|
1090
|
+
if (existsSync2(path)) {
|
|
1091
|
+
const source = rel2(root, path);
|
|
1092
|
+
const raw = readJSON(path, source, warn);
|
|
1093
|
+
for (const t of raw?.terms ?? []) {
|
|
1094
|
+
const name = text(t.name);
|
|
1095
|
+
if (!name) {
|
|
1096
|
+
warn({ file: source, message: "a term needs a name" });
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
const wrong = strings(t.wrong);
|
|
1100
|
+
const note = text(t.note);
|
|
1101
|
+
const existing = terms.get(name.toLowerCase());
|
|
1102
|
+
if (existing) {
|
|
1103
|
+
if (wrong) existing.wrong = [.../* @__PURE__ */ new Set([...existing.wrong ?? [], ...wrong])];
|
|
1104
|
+
if (note) existing.note = note;
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
if (t.kind !== void 0 && t.kind !== "product") {
|
|
1108
|
+
warn({ file: source, message: `"${name}" is not a name in the system, so it is listed as a product term` });
|
|
1109
|
+
}
|
|
1110
|
+
terms.set(name.toLowerCase(), { name, kind: "product", ...wrong ? { wrong } : {}, ...note ? { note } : {} });
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
for (const t of terms.values()) {
|
|
1114
|
+
for (const w of t.wrong ?? []) {
|
|
1115
|
+
const clash = terms.get(w.toLowerCase());
|
|
1116
|
+
if (clash && clash !== t) warn({ file: curatedPath, message: `"${w}" is listed as wrong for ${t.name}, but it is the ${clash.kind} ${clash.name}` });
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return [...terms.values()].sort((a, b) => compare(a.name.toLowerCase(), b.name.toLowerCase()) || compare(a.kind, b.kind));
|
|
1120
|
+
}
|
|
1121
|
+
var ASSET_FILE = /\.(svg|png|jpe?g|webp|gif|avif)$/i;
|
|
1122
|
+
var ASSET_FIELDS = /* @__PURE__ */ new Set(["file", "kind", "name", "usage", "minSize", "clearSpace", "surfaces", "alt"]);
|
|
1123
|
+
function readAssets(root, brandDir, warn) {
|
|
1124
|
+
const dir = resolve2(root, brandDir);
|
|
1125
|
+
if (!existsSync2(dir)) return [];
|
|
1126
|
+
const indexPath = join2(dir, "assets.json");
|
|
1127
|
+
const notes = /* @__PURE__ */ new Map();
|
|
1128
|
+
if (existsSync2(indexPath)) {
|
|
1129
|
+
const raw = readJSON(indexPath, rel2(root, indexPath), warn);
|
|
1130
|
+
for (const a of raw?.assets ?? []) {
|
|
1131
|
+
const file = text(a.file);
|
|
1132
|
+
if (!file) {
|
|
1133
|
+
warn({ file: rel2(root, indexPath), message: "an asset needs a file" });
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
for (const key of Object.keys(a)) if (!ASSET_FIELDS.has(key)) warn({ file: rel2(root, indexPath), message: `unknown asset field "${key}" on ${file}` });
|
|
1137
|
+
notes.set(rel2(root, resolve2(dir, file)), a);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
const out = [];
|
|
1141
|
+
const found = /* @__PURE__ */ new Set();
|
|
1142
|
+
for (const path of walk(dir, (n) => ASSET_FILE.test(n))) {
|
|
1143
|
+
const file = rel2(root, path);
|
|
1144
|
+
found.add(file);
|
|
1145
|
+
const note = notes.get(file);
|
|
1146
|
+
if (!note) warn({ file, message: "brand asset has no entry in assets.json: say how to use it" });
|
|
1147
|
+
const kinds = ["logo", "icon", "image", "other"];
|
|
1148
|
+
const kind = kinds.find((k) => k === note?.kind) ?? kindFromFolder(rel2(dir, dirname2(path)));
|
|
1149
|
+
const entry = { file, kind, name: text(note?.name) ?? basename2(path).replace(ASSET_FILE, "") };
|
|
1150
|
+
for (const field of ["usage", "minSize", "clearSpace", "alt"]) {
|
|
1151
|
+
const v = text(note?.[field]);
|
|
1152
|
+
if (v) entry[field] = v;
|
|
1153
|
+
}
|
|
1154
|
+
const surfaces = strings(note?.surfaces);
|
|
1155
|
+
if (surfaces) entry.surfaces = surfaces;
|
|
1156
|
+
const size = intrinsicSize(path);
|
|
1157
|
+
if (size) Object.assign(entry, size);
|
|
1158
|
+
out.push(entry);
|
|
1159
|
+
}
|
|
1160
|
+
for (const file of notes.keys()) {
|
|
1161
|
+
if (!found.has(file)) warn({ file: rel2(root, indexPath), message: `assets.json lists ${file}, which is not in the brand folder` });
|
|
1162
|
+
}
|
|
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));
|
|
1166
|
+
}
|
|
1167
|
+
function kindFromFolder(folder) {
|
|
1168
|
+
const f = folder.toLowerCase();
|
|
1169
|
+
if (/logo/.test(f)) return "logo";
|
|
1170
|
+
if (/icon/.test(f)) return "icon";
|
|
1171
|
+
if (/imag|photo|illustration/.test(f)) return "image";
|
|
1172
|
+
return "other";
|
|
1173
|
+
}
|
|
1174
|
+
function intrinsicSize(path) {
|
|
1175
|
+
if (/\.svg$/i.test(path)) {
|
|
1176
|
+
const svg = readFileSync3(path, "utf8").slice(0, 4096);
|
|
1177
|
+
const box = /viewBox\s*=\s*["']\s*[-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)[\s,]+([\d.]+)/.exec(svg);
|
|
1178
|
+
if (box) return { width: Number(box[1]), height: Number(box[2]) };
|
|
1179
|
+
const w = /\swidth\s*=\s*["']([\d.]+)(px)?["']/.exec(svg);
|
|
1180
|
+
const h = /\sheight\s*=\s*["']([\d.]+)(px)?["']/.exec(svg);
|
|
1181
|
+
return w && h ? { width: Number(w[1]), height: Number(h[1]) } : void 0;
|
|
1182
|
+
}
|
|
1183
|
+
if (/\.png$/i.test(path)) {
|
|
1184
|
+
const head = readFileSync3(path).subarray(0, 24);
|
|
1185
|
+
if (head.length === 24 && head.toString("ascii", 12, 16) === "IHDR") return { width: head.readUInt32BE(16), height: head.readUInt32BE(20) };
|
|
1186
|
+
}
|
|
1187
|
+
return void 0;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// src/tokens.ts
|
|
1191
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1192
|
+
import { resolve as resolve3 } from "path";
|
|
1193
|
+
import postcss from "postcss";
|
|
1194
|
+
function readTokens(root, sources, warn) {
|
|
1195
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
1196
|
+
const fromMedia = /* @__PURE__ */ new Map();
|
|
1197
|
+
for (const source of sources) {
|
|
1198
|
+
let css;
|
|
1199
|
+
try {
|
|
1200
|
+
css = readFileSync4(resolve3(root, source), "utf8");
|
|
1201
|
+
} catch {
|
|
1202
|
+
warn({ file: source, message: "token stylesheet not found" });
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
const ast = postcss.parse(css, { from: source });
|
|
1206
|
+
const walk3 = (container, chain) => {
|
|
1207
|
+
let comment;
|
|
1208
|
+
let fresh = false;
|
|
1209
|
+
let previous;
|
|
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;
|
|
1217
|
+
if (node.type === "comment") {
|
|
1218
|
+
comment = cleanComment(node.text);
|
|
1219
|
+
fresh = true;
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
if (node.type === "decl") {
|
|
1223
|
+
if (!fresh && /\n[ \t]*\n/.test(node.raws.before ?? "")) comment = void 0;
|
|
1224
|
+
fresh = false;
|
|
1225
|
+
if (!node.prop.startsWith("--") || node.prop.includes("*")) continue;
|
|
1226
|
+
const { key, media } = contextOf(chain);
|
|
1227
|
+
const value = node.value.replace(/\s+/g, " ").trim();
|
|
1228
|
+
let entry = tokens.get(node.prop);
|
|
1229
|
+
if (!entry) {
|
|
1230
|
+
entry = { name: node.prop, tier: "other", values: {}, source };
|
|
1231
|
+
tokens.set(node.prop, entry);
|
|
1232
|
+
}
|
|
1233
|
+
const mediaKeys = fromMedia.get(node.prop) ?? /* @__PURE__ */ new Set();
|
|
1234
|
+
fromMedia.set(node.prop, mediaKeys);
|
|
1235
|
+
const existing = entry.values[key];
|
|
1236
|
+
const explicitAlready = existing !== void 0 && !mediaKeys.has(key);
|
|
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
|
+
}
|
|
1241
|
+
entry.values[key] = value;
|
|
1242
|
+
if (media) mediaKeys.add(key);
|
|
1243
|
+
else mediaKeys.delete(key);
|
|
1244
|
+
}
|
|
1245
|
+
if (comment && !entry.description) entry.description = comment;
|
|
1246
|
+
previous = entry;
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
fresh = false;
|
|
1250
|
+
comment = void 0;
|
|
1251
|
+
visitContainer(node, chain, walk3);
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
walk3(ast, []);
|
|
1255
|
+
}
|
|
1256
|
+
return [...tokens.values()].map((t) => ({ ...t, tier: tierOf(t.name, t.values) }));
|
|
1257
|
+
}
|
|
1258
|
+
function visitContainer(node, chain, walk3) {
|
|
1259
|
+
if (node.type === "rule") walk3(node, [...chain, { kind: "rule", name: "", value: node.selector }]);
|
|
1260
|
+
else if (node.type === "atrule" && node.nodes) walk3(node, [...chain, { kind: "atrule", name: node.name, value: node.params }]);
|
|
1261
|
+
}
|
|
1262
|
+
function contextOf(chain) {
|
|
1263
|
+
let mode;
|
|
1264
|
+
let selector;
|
|
1265
|
+
let p3 = false;
|
|
1266
|
+
let media = false;
|
|
1267
|
+
const conditions = [];
|
|
1268
|
+
for (const frame of chain) {
|
|
1269
|
+
if (frame.kind === "atrule") {
|
|
1270
|
+
const params = frame.value.replace(/\s+/g, " ").trim();
|
|
1271
|
+
if (frame.name === "theme" || frame.name === "layer") continue;
|
|
1272
|
+
if (frame.name !== "media") {
|
|
1273
|
+
conditions.push(`@${frame.name} ${params}`);
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
let known = false;
|
|
1277
|
+
if (/color-gamut\s*:\s*p3/.test(params)) {
|
|
1278
|
+
p3 = true;
|
|
1279
|
+
known = true;
|
|
1280
|
+
}
|
|
1281
|
+
const scheme = /prefers-color-scheme\s*:\s*(dark|light)/.exec(params);
|
|
1282
|
+
if (scheme) {
|
|
1283
|
+
mode = scheme[1];
|
|
1284
|
+
media = true;
|
|
1285
|
+
known = true;
|
|
1286
|
+
}
|
|
1287
|
+
const rest = params.replace(/\(\s*color-gamut\s*:\s*p3\s*\)/g, "").replace(/\(\s*prefers-color-scheme\s*:\s*(dark|light)\s*\)/g, "").replace(/^\s*(and\s+)*|(\s+and)*\s*$/g, "").replace(/\s+and\s+and\s+/g, " and ").trim();
|
|
1288
|
+
if (!known || rest) conditions.push(`@media ${known ? rest : params}`);
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const sel = frame.value.replace(/\s+/g, " ").trim();
|
|
1292
|
+
const positive = sel.replace(/:not\((?:[^()]|\([^()]*\))*\)/g, "");
|
|
1293
|
+
if (/\[data-theme=["']?dark["']?\]|\.dark(?![\w-])/.test(positive)) mode = "dark";
|
|
1294
|
+
else if (/\[data-theme=["']?light["']?\]|\.light(?![\w-])/.test(positive)) mode = "light";
|
|
1295
|
+
else if (positive.split(",").every((s) => /^\s*(:root|html|\*)\s*$/.test(s))) {
|
|
1296
|
+
} else selector = sel;
|
|
1297
|
+
}
|
|
1298
|
+
let key = selector ?? mode ?? "default";
|
|
1299
|
+
if (p3) key = `p3:${key}`;
|
|
1300
|
+
if (conditions.length) key = `${conditions.join(" ")}:${key}`;
|
|
1301
|
+
return { key, media };
|
|
1302
|
+
}
|
|
1303
|
+
function cleanComment(text2) {
|
|
1304
|
+
return text2.split(/\n\s*\n/).map(
|
|
1305
|
+
(p) => p.split("\n").map((l) => l.replace(/^\s*\*?\s?/, "").trim()).filter(Boolean).join(" ")
|
|
1306
|
+
).filter(Boolean).join("\n\n");
|
|
1307
|
+
}
|
|
1308
|
+
var PREFIX_TIERS = [
|
|
1309
|
+
[/^--color-/, "color"],
|
|
1310
|
+
[/^--(inset-|drop-|text-)?shadow-/, "shadow"],
|
|
1311
|
+
[/^--font-weight-/, "font-weight"],
|
|
1312
|
+
[/^--font-/, "font"],
|
|
1313
|
+
[/^--text-/, "text"],
|
|
1314
|
+
[/^--leading-/, "leading"],
|
|
1315
|
+
[/^--tracking-/, "tracking"],
|
|
1316
|
+
[/^--spacing(-|$)/, "spacing"],
|
|
1317
|
+
[/^--size-/, "size"],
|
|
1318
|
+
[/^--radius(-|$)/, "radius"],
|
|
1319
|
+
[/^--ease-/, "ease"],
|
|
1320
|
+
[/^--(duration|animate)-/, "duration"],
|
|
1321
|
+
[/^--motion-/, "motion"],
|
|
1322
|
+
[/^--breakpoint-/, "breakpoint"],
|
|
1323
|
+
[/^--container-/, "container"]
|
|
1324
|
+
];
|
|
1325
|
+
var COLOUR_VALUE = /^(#[0-9a-f]{3,8}\b|(oklch|oklab|lch|lab|rgba?|hsla?|hwb|color|color-mix|light-dark)\(|var\(--color-)/i;
|
|
1326
|
+
function tierOf(name, values) {
|
|
1327
|
+
for (const [re, tier] of PREFIX_TIERS) if (re.test(name)) return tier;
|
|
1328
|
+
const all = Object.values(values);
|
|
1329
|
+
if (all.length && all.every((v) => COLOUR_VALUE.test(v) || /^var\(--[\w-]+\)$/.test(v)) && all.some((v) => COLOUR_VALUE.test(v)))
|
|
1330
|
+
return "color";
|
|
1331
|
+
return "other";
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// src/types.ts
|
|
1335
|
+
var MANIFEST_SCHEMA = "designtools.manifest/1";
|
|
1336
|
+
|
|
1337
|
+
// src/build.ts
|
|
1338
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1339
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
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
|
|
1399
|
+
var GENERATOR_NAME = "@designtools/manifest";
|
|
1400
|
+
var VERSION = (() => {
|
|
1401
|
+
try {
|
|
1402
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
1403
|
+
return JSON.parse(readFileSync6(join3(here, "../package.json"), "utf8")).version;
|
|
1404
|
+
} catch {
|
|
1405
|
+
return "0.0.0";
|
|
1406
|
+
}
|
|
1407
|
+
})();
|
|
1408
|
+
var GENERATOR = `${GENERATOR_NAME}@${VERSION}`;
|
|
1409
|
+
function outDir(config) {
|
|
1410
|
+
return (config.out ?? `${config.system.replace(/\/+$/, "")}/manifest`).replace(/\/+$/, "");
|
|
1411
|
+
}
|
|
1412
|
+
function buildManifest(config) {
|
|
1413
|
+
const warnings = [];
|
|
1414
|
+
const warn = (w) => warnings.push(w);
|
|
1415
|
+
const out = outDir(config);
|
|
1416
|
+
const system = config.system.replace(/\/+$/, "");
|
|
1417
|
+
const head = { generator: GENERATOR, schema: MANIFEST_SCHEMA };
|
|
1418
|
+
const components = {
|
|
1419
|
+
...head,
|
|
1420
|
+
system,
|
|
1421
|
+
components: readComponents({ root: config.root, system: config.system, tsconfig: config.tsconfig, out }, warn)
|
|
1422
|
+
};
|
|
1423
|
+
const tokens = { ...head, sources: config.tokens, tokens: readTokens(config.root, config.tokens, warn) };
|
|
1424
|
+
const rules = {
|
|
1425
|
+
...head,
|
|
1426
|
+
rules: [...readRules(config.root, config.rules ?? `${system}/rules`, warn), ...config.usage ? usageRules(config.root, config.usage, warn) : []]
|
|
1427
|
+
};
|
|
1428
|
+
const patterns = {
|
|
1429
|
+
...head,
|
|
1430
|
+
patterns: readPatterns(config.root, system, components.components, compilerOptions(config.root, config.tsconfig), warn, out)
|
|
1431
|
+
};
|
|
1432
|
+
const taxonomy = {
|
|
1433
|
+
...head,
|
|
1434
|
+
terms: buildTaxonomy(
|
|
1435
|
+
config.root,
|
|
1436
|
+
config.taxonomy ?? `${system}/taxonomy.json`,
|
|
1437
|
+
{ components: components.components, tokens: tokens.tokens, patterns: patterns.patterns, rules: rules.rules },
|
|
1438
|
+
warn
|
|
1439
|
+
)
|
|
1440
|
+
};
|
|
1441
|
+
const assets = { ...head, assets: readAssets(config.root, config.brand ?? `${system}/brand`, warn) };
|
|
1442
|
+
return {
|
|
1443
|
+
components,
|
|
1444
|
+
tokens,
|
|
1445
|
+
rules,
|
|
1446
|
+
patterns,
|
|
1447
|
+
taxonomy,
|
|
1448
|
+
assets,
|
|
1449
|
+
files: {
|
|
1450
|
+
[`${out}/components.json`]: serialise(components),
|
|
1451
|
+
[`${out}/tokens.json`]: serialise(tokens),
|
|
1452
|
+
[`${out}/rules.json`]: serialise(rules),
|
|
1453
|
+
[`${out}/patterns.json`]: serialise(patterns),
|
|
1454
|
+
[`${out}/taxonomy.json`]: serialise(taxonomy),
|
|
1455
|
+
[`${out}/assets.json`]: serialise(assets)
|
|
1456
|
+
},
|
|
1457
|
+
warnings
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
function serialise(value) {
|
|
1461
|
+
return JSON.stringify(sortKeys(value), null, 2) + "\n";
|
|
1462
|
+
}
|
|
1463
|
+
function sortKeys(value) {
|
|
1464
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
1465
|
+
if (value && typeof value === "object") {
|
|
1466
|
+
const out = {};
|
|
1467
|
+
for (const key of Object.keys(value).sort()) {
|
|
1468
|
+
const v = value[key];
|
|
1469
|
+
if (v !== void 0) out[key] = sortKeys(v);
|
|
1470
|
+
}
|
|
1471
|
+
return out;
|
|
1472
|
+
}
|
|
1473
|
+
return value;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// src/agents.ts
|
|
1477
|
+
import { execFileSync } from "child_process";
|
|
1478
|
+
import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
|
|
1479
|
+
import { resolve as resolve5 } from "path";
|
|
1480
|
+
var AGENTS_START = "<!-- designtools:start \xB7 generated by designtools-manifest agents; edit outside these markers -->";
|
|
1481
|
+
var AGENTS_END = "<!-- designtools:end -->";
|
|
1482
|
+
function agentsFile(root, given, configured) {
|
|
1483
|
+
if (given) return given;
|
|
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";
|
|
1489
|
+
}
|
|
1490
|
+
function agentsSection(config) {
|
|
1491
|
+
const out = outDir(config);
|
|
1492
|
+
const lines = [
|
|
1493
|
+
AGENTS_START,
|
|
1494
|
+
"## Design system",
|
|
1495
|
+
"",
|
|
1496
|
+
`The design system in \`${config.system}\` is the source of truth for UI. Read it before building or changing a screen, and point at it rather than copying from it.`,
|
|
1497
|
+
"",
|
|
1498
|
+
`- Components: props, variants, usage, status and examples: \`${out}/components.json\``,
|
|
1499
|
+
`- Tokens: colour, type, space, radius, shadow and motion: \`${out}/tokens.json\`${config.tokens.length ? `, from ${config.tokens.map((t) => `\`${t}\``).join(" and ")}` : ""}`,
|
|
1500
|
+
`- Rules, brand and interface, each with its check: \`${out}/rules.json\``,
|
|
1501
|
+
`- Patterns, the product's recurring screens: \`${out}/patterns.json\``,
|
|
1502
|
+
`- Vocabulary, with the wrong forms to avoid: \`${out}/taxonomy.json\``,
|
|
1503
|
+
`- Brand assets and how to use them: \`${out}/assets.json\``
|
|
1504
|
+
];
|
|
1505
|
+
if (config.docs) lines.push(`- The docs and design language: ${config.docs} (add \`.md\` to any page for markdown)`);
|
|
1506
|
+
lines.push(
|
|
1507
|
+
"",
|
|
1508
|
+
"When you build:",
|
|
1509
|
+
"",
|
|
1510
|
+
"- Start from a component's canonical example, and follow its do and don't examples.",
|
|
1511
|
+
"- Change how a component looks with its variants, not a `className`.",
|
|
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.",
|
|
1513
|
+
"- Use the taxonomy's names, never its wrong forms, in code and in copy.",
|
|
1514
|
+
"- Carry status when you quote: say when a component is emerging or deprecated, or a rule is still an assumption.",
|
|
1515
|
+
AGENTS_END
|
|
1516
|
+
);
|
|
1517
|
+
return lines.join("\n") + "\n";
|
|
1518
|
+
}
|
|
1519
|
+
function withAgentsSection(existing, section) {
|
|
1520
|
+
if (!existing) return section;
|
|
1521
|
+
const start = existing.indexOf(AGENTS_START);
|
|
1522
|
+
const end = existing.indexOf(AGENTS_END);
|
|
1523
|
+
if (start !== -1 && end > start) {
|
|
1524
|
+
const after = existing.slice(end + AGENTS_END.length).replace(/^\r?\n/, "");
|
|
1525
|
+
return existing.slice(0, start) + section + after;
|
|
1526
|
+
}
|
|
1527
|
+
return existing.replace(/\s*$/, "\n\n") + section;
|
|
1528
|
+
}
|
|
1529
|
+
function readIfExists(path) {
|
|
1530
|
+
return existsSync4(path) ? readFileSync7(path, "utf8") : void 0;
|
|
1531
|
+
}
|
|
1532
|
+
function provenance(root) {
|
|
1533
|
+
let version = "unversioned";
|
|
1534
|
+
try {
|
|
1535
|
+
const pkg = JSON.parse(readFileSync7(resolve5(root, "package.json"), "utf8"));
|
|
1536
|
+
version = `${pkg.name ?? "project"}@${pkg.version ?? "0.0.0"}`;
|
|
1537
|
+
} catch {
|
|
1538
|
+
}
|
|
1539
|
+
const fromEnv = process.env.VERCEL_GIT_COMMIT_SHA ?? process.env.GITHUB_SHA;
|
|
1540
|
+
let commit = fromEnv ? fromEnv.slice(0, 7) : "uncommitted";
|
|
1541
|
+
if (!fromEnv) {
|
|
1542
|
+
try {
|
|
1543
|
+
const git = (...args) => execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], env: { ...process.env, LC_ALL: "C" } }).trim();
|
|
1544
|
+
commit = git("rev-parse", "--short=7", "HEAD");
|
|
1545
|
+
if (git("status", "--porcelain")) commit += "-dirty";
|
|
1546
|
+
} catch {
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return { version, commit, line: `Generated from ${version} at ${commit}` };
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// src/config.ts
|
|
1553
|
+
import { existsSync as existsSync5, readFileSync as readFileSync8 } from "fs";
|
|
1554
|
+
import { resolve as resolve6 } from "path";
|
|
1555
|
+
var CONFIG_FILE = "designtools.json";
|
|
1556
|
+
var PATH_FIELDS = ["tsconfig", "out", "rules", "taxonomy", "brand", "docs", "agents", "usage"];
|
|
1557
|
+
function loadConfig(flags) {
|
|
1558
|
+
const root = resolve6(flags.root ?? ".");
|
|
1559
|
+
const path = resolve6(root, CONFIG_FILE);
|
|
1560
|
+
let file = {};
|
|
1561
|
+
if (existsSync5(path)) {
|
|
1562
|
+
let parsed;
|
|
1563
|
+
try {
|
|
1564
|
+
parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
1565
|
+
} catch (e) {
|
|
1566
|
+
throw new Error(`${CONFIG_FILE} is not valid JSON: ${e.message}`);
|
|
1567
|
+
}
|
|
1568
|
+
file = parsed.manifest ?? {};
|
|
1569
|
+
}
|
|
1570
|
+
const system = flags.system ?? file.system;
|
|
1571
|
+
if (!system) {
|
|
1572
|
+
throw new Error(
|
|
1573
|
+
`no system folder. Pass --system <dir>, or add { "manifest": { "system": "src/ds" } } to ${CONFIG_FILE}.`
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
const tokens = flags.tokens?.length ? flags.tokens : file.tokens ?? [];
|
|
1577
|
+
if (!Array.isArray(tokens) || tokens.some((t) => typeof t !== "string")) {
|
|
1578
|
+
throw new Error(`manifest.tokens in ${CONFIG_FILE} must be a list of stylesheet paths`);
|
|
1579
|
+
}
|
|
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
|
+
}
|
|
1587
|
+
for (const field of PATH_FIELDS) {
|
|
1588
|
+
const value = flags[field] ?? file[field];
|
|
1589
|
+
if (value !== void 0) config[field] = value;
|
|
1590
|
+
}
|
|
1591
|
+
return config;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// src/diff.ts
|
|
1595
|
+
var CHANGE_CLASSES = ["breaking", "additive", "visual", "docs"];
|
|
1596
|
+
function diffManifests(before, after) {
|
|
1597
|
+
const changes = [
|
|
1598
|
+
...diffComponents(before.components, after.components),
|
|
1599
|
+
...diffTokens(before.tokens?.tokens ?? [], after.tokens?.tokens ?? []),
|
|
1600
|
+
...diffRecords("rule", before.rules?.rules, after.rules?.rules, (r) => r.id),
|
|
1601
|
+
...diffRecords("pattern", before.patterns?.patterns, after.patterns?.patterns, (p) => p.id),
|
|
1602
|
+
...diffRecords("term", before.taxonomy?.terms, after.taxonomy?.terms, (t) => `${t.kind}:${t.name}`),
|
|
1603
|
+
...diffRecords("asset", before.assets?.assets, after.assets?.assets, (a) => a.file)
|
|
1604
|
+
];
|
|
1605
|
+
const rank2 = (c) => CHANGE_CLASSES.indexOf(c);
|
|
1606
|
+
return changes.sort((a, b) => rank2(a.class) - rank2(b.class) || compare(a.subject, b.subject) || compare(a.message, b.message));
|
|
1607
|
+
}
|
|
1608
|
+
function keyed(manifest) {
|
|
1609
|
+
const out = /* @__PURE__ */ new Map();
|
|
1610
|
+
if (!manifest) return out;
|
|
1611
|
+
const prefix = manifest.system.replace(/\/+$/, "") + "/";
|
|
1612
|
+
for (const c of manifest.components) {
|
|
1613
|
+
const inner = c.source.startsWith(prefix) ? c.source.slice(prefix.length) : c.source;
|
|
1614
|
+
out.set(`${inner}#${c.export}`, c);
|
|
1615
|
+
}
|
|
1616
|
+
return out;
|
|
1617
|
+
}
|
|
1618
|
+
function diffComponents(before, after) {
|
|
1619
|
+
const a = keyed(before);
|
|
1620
|
+
const b = keyed(after);
|
|
1621
|
+
const out = [];
|
|
1622
|
+
for (const [key, old] of a) {
|
|
1623
|
+
const next = b.get(key);
|
|
1624
|
+
if (!next) out.push({ class: "breaking", subject: old.name, message: `component removed (${old.source})` });
|
|
1625
|
+
else out.push(...diffComponent(old, next));
|
|
1626
|
+
}
|
|
1627
|
+
for (const [key, next] of b) {
|
|
1628
|
+
if (!a.has(key)) out.push({ class: "additive", subject: next.name, message: `new component (${next.source})` });
|
|
1629
|
+
}
|
|
1630
|
+
return out;
|
|
1631
|
+
}
|
|
1632
|
+
function diffComponent(a, b) {
|
|
1633
|
+
const subject = b.name;
|
|
1634
|
+
const out = [];
|
|
1635
|
+
const add = (cls, message) => out.push({ class: cls, subject, message });
|
|
1636
|
+
if (a.name !== b.name) add("docs", `display name changed from ${a.name} to ${b.name}`);
|
|
1637
|
+
if ((a.status ?? "") !== (b.status ?? "")) {
|
|
1638
|
+
add("docs", `status ${a.status ?? "unset"} \u2192 ${b.status ?? "unset"}${b.statusNote ? `: ${b.statusNote}` : ""}`);
|
|
1639
|
+
}
|
|
1640
|
+
for (const field of ["description", "category", "primitive", "aria"]) {
|
|
1641
|
+
if ((a[field] ?? "") !== (b[field] ?? "")) add("docs", `${field} changed`);
|
|
1642
|
+
}
|
|
1643
|
+
for (const field of ["snippets", "see", "usage", "a11y", "replaces"]) {
|
|
1644
|
+
if (JSON.stringify(a[field] ?? null) !== JSON.stringify(b[field] ?? null)) add("docs", `${field} changed`);
|
|
1645
|
+
}
|
|
1646
|
+
const [lost, gained] = setDiff(a.inherits ?? [], b.inherits ?? []);
|
|
1647
|
+
for (const x of lost) add("breaking", `no longer accepts ${x} props`);
|
|
1648
|
+
for (const x of gained) add("additive", `now accepts ${x} props`);
|
|
1649
|
+
out.push(...diffProps(subject, a.props, b.props));
|
|
1650
|
+
const axesA = new Map((a.variants?.axes ?? []).map((x) => [x.name, x]));
|
|
1651
|
+
const axesB = new Map((b.variants?.axes ?? []).map((x) => [x.name, x]));
|
|
1652
|
+
for (const [name, axis] of axesA) {
|
|
1653
|
+
const next = axesB.get(name);
|
|
1654
|
+
if (!next) {
|
|
1655
|
+
add("breaking", `variant ${name} removed`);
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
const [gone, added] = setDiff(
|
|
1659
|
+
axis.options.map((o) => o.name),
|
|
1660
|
+
next.options.map((o) => o.name)
|
|
1661
|
+
);
|
|
1662
|
+
for (const o of gone) add("breaking", `variant option ${name}="${o}" removed`);
|
|
1663
|
+
for (const o of added) add("additive", `variant option ${name}="${o}" added`);
|
|
1664
|
+
if ((axis.default ?? "") !== (next.default ?? ""))
|
|
1665
|
+
add("visual", `default ${name} changed from ${axis.default ?? "none"} to ${next.default ?? "none"}`);
|
|
1666
|
+
const descA = new Map(axis.options.map((o) => [o.name, o.description ?? ""]));
|
|
1667
|
+
for (const o of next.options) {
|
|
1668
|
+
if (descA.has(o.name) && descA.get(o.name) !== (o.description ?? "")) add("docs", `variant option ${name}="${o.name}" description changed`);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
for (const name of axesB.keys()) if (!axesA.has(name)) add("additive", `variant ${name} added`);
|
|
1672
|
+
const [slotsGone, slotsAdded] = setDiff(a.slots, b.slots);
|
|
1673
|
+
for (const s of slotsGone) add("breaking", `data-slot "${s}" removed`);
|
|
1674
|
+
for (const s of slotsAdded) add("additive", `data-slot "${s}" added`);
|
|
1675
|
+
const names = (c) => c.examples ? [c.examples.canonical, ...c.examples.do, ...c.examples.dont, ...c.examples.other].flatMap((x) => x ? [x.name] : []) : [];
|
|
1676
|
+
const [examplesGone, examplesAdded] = setDiff(names(a), names(b));
|
|
1677
|
+
for (const x of examplesGone) add("docs", `example ${x} removed`);
|
|
1678
|
+
for (const x of examplesAdded) add("additive", `example ${x} added`);
|
|
1679
|
+
return out;
|
|
1680
|
+
}
|
|
1681
|
+
function diffProps(subject, a, b) {
|
|
1682
|
+
const out = [];
|
|
1683
|
+
const add = (cls, message) => out.push({ class: cls, subject, message });
|
|
1684
|
+
for (const [name, old] of Object.entries(a)) {
|
|
1685
|
+
const next = b[name];
|
|
1686
|
+
if (!next) {
|
|
1687
|
+
add("breaking", `prop ${name} removed`);
|
|
1688
|
+
continue;
|
|
1689
|
+
}
|
|
1690
|
+
if (!old.required && next.required) add("breaking", `prop ${name} is now required`);
|
|
1691
|
+
if (old.required && !next.required) add("additive", `prop ${name} is now optional`);
|
|
1692
|
+
if (old.values && next.values) {
|
|
1693
|
+
const [gone, added] = setDiff(old.values, next.values);
|
|
1694
|
+
for (const v of gone) add("breaking", `prop ${name} no longer accepts "${v}"`);
|
|
1695
|
+
for (const v of added) add("additive", `prop ${name} accepts "${v}"`);
|
|
1696
|
+
} else if (old.values && !next.values && widens(old.values, next.type)) {
|
|
1697
|
+
add("additive", `prop ${name} widened from ${old.type} to ${next.type}`);
|
|
1698
|
+
} else if (old.type !== next.type) {
|
|
1699
|
+
add("breaking", `prop ${name} type changed from ${old.type} to ${next.type}`);
|
|
1700
|
+
}
|
|
1701
|
+
if ((old.default ?? "") !== (next.default ?? ""))
|
|
1702
|
+
add("visual", `prop ${name} default changed from ${old.default ?? "none"} to ${next.default ?? "none"}`);
|
|
1703
|
+
if ((old.description ?? "") !== (next.description ?? "")) add("docs", `prop ${name} description changed`);
|
|
1704
|
+
if (old.deprecated === void 0 && next.deprecated !== void 0) add("docs", `prop ${name} deprecated`);
|
|
1705
|
+
}
|
|
1706
|
+
for (const [name, next] of Object.entries(b)) {
|
|
1707
|
+
if (name in a) continue;
|
|
1708
|
+
if (next.required) add("breaking", `new required prop ${name}`);
|
|
1709
|
+
else add("additive", `new prop ${name}`);
|
|
1710
|
+
}
|
|
1711
|
+
return out;
|
|
1712
|
+
}
|
|
1713
|
+
function diffTokens(a, b) {
|
|
1714
|
+
const out = [];
|
|
1715
|
+
const before = new Map(a.map((t) => [t.name, t]));
|
|
1716
|
+
const after = new Map(b.map((t) => [t.name, t]));
|
|
1717
|
+
for (const [name, old] of before) {
|
|
1718
|
+
const next = after.get(name);
|
|
1719
|
+
if (!next) {
|
|
1720
|
+
out.push({ class: "breaking", subject: name, message: "token removed" });
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
const contexts = [.../* @__PURE__ */ new Set([...Object.keys(old.values), ...Object.keys(next.values)])].sort(compare);
|
|
1724
|
+
for (const ctx of contexts) {
|
|
1725
|
+
const was = old.values[ctx];
|
|
1726
|
+
const now = next.values[ctx];
|
|
1727
|
+
if (was === now) continue;
|
|
1728
|
+
const where = ctx === "default" ? "" : ` (${ctx})`;
|
|
1729
|
+
out.push({
|
|
1730
|
+
class: "visual",
|
|
1731
|
+
subject: name,
|
|
1732
|
+
message: was === void 0 ? `now set${where}: ${now}` : now === void 0 ? `no longer set${where}` : `changed${where} from ${was} to ${now}`
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
if ((old.description ?? "") !== (next.description ?? "")) out.push({ class: "docs", subject: name, message: "description changed" });
|
|
1736
|
+
}
|
|
1737
|
+
for (const name of after.keys()) {
|
|
1738
|
+
if (!before.has(name)) out.push({ class: "additive", subject: name, message: "new token" });
|
|
1739
|
+
}
|
|
1740
|
+
return out;
|
|
1741
|
+
}
|
|
1742
|
+
function diffRecords(kind, before, after, key) {
|
|
1743
|
+
const out = [];
|
|
1744
|
+
const a = new Map((before ?? []).map((x) => [key(x), x]));
|
|
1745
|
+
const b = new Map((after ?? []).map((x) => [key(x), x]));
|
|
1746
|
+
for (const [k, old] of a) {
|
|
1747
|
+
const next = b.get(k);
|
|
1748
|
+
if (!next) out.push({ class: "docs", subject: k, message: `${kind} removed` });
|
|
1749
|
+
else if (JSON.stringify(old) !== JSON.stringify(next)) out.push({ class: "docs", subject: k, message: `${kind} changed` });
|
|
1750
|
+
}
|
|
1751
|
+
for (const k of b.keys()) if (!a.has(k)) out.push({ class: "additive", subject: k, message: `new ${kind}` });
|
|
1752
|
+
return out;
|
|
1753
|
+
}
|
|
1754
|
+
function widens(values, type) {
|
|
1755
|
+
const members = type.split("|").map((t) => t.trim());
|
|
1756
|
+
const numeric = (v) => /^-?\d+(\.\d+)?$/.test(v);
|
|
1757
|
+
return values.every((v) => numeric(v) ? members.includes("number") : members.includes("string"));
|
|
1758
|
+
}
|
|
1759
|
+
function setDiff(a, b) {
|
|
1760
|
+
const sa = new Set(a);
|
|
1761
|
+
const sb = new Set(b);
|
|
1762
|
+
return [a.filter((x) => !sb.has(x)), b.filter((x) => !sa.has(x))];
|
|
1763
|
+
}
|
|
1764
|
+
var HEADINGS = {
|
|
1765
|
+
breaking: "Breaking",
|
|
1766
|
+
additive: "Additive",
|
|
1767
|
+
visual: "Visual",
|
|
1768
|
+
docs: "Docs"
|
|
1769
|
+
};
|
|
1770
|
+
function formatReport(changes, base) {
|
|
1771
|
+
const count = (c) => changes.filter((x) => x.class === c).length;
|
|
1772
|
+
if (changes.length === 0) return `### Design system changes against \`${base}\`
|
|
1773
|
+
|
|
1774
|
+
No changes to components or tokens.
|
|
1775
|
+
`;
|
|
1776
|
+
const lines = [
|
|
1777
|
+
`### Design system changes against \`${base}\``,
|
|
1778
|
+
"",
|
|
1779
|
+
CHANGE_CLASSES.map((c) => c === "breaking" && count(c) ? `**${count(c)} breaking**` : `${count(c)} ${c}`).join(" \xB7 ")
|
|
1780
|
+
];
|
|
1781
|
+
for (const cls of CHANGE_CLASSES) {
|
|
1782
|
+
const group = changes.filter((c) => c.class === cls);
|
|
1783
|
+
if (!group.length) continue;
|
|
1784
|
+
lines.push("", `#### ${HEADINGS[cls]}`, "");
|
|
1785
|
+
for (const c of group) lines.push(`- \`${c.subject}\`: ${c.message}`);
|
|
1786
|
+
}
|
|
1787
|
+
return lines.join("\n") + "\n";
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// src/prose.ts
|
|
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";
|
|
1793
|
+
var NOT_SYSTEM = /* @__PURE__ */ new Set(["Fragment", "Suspense", "StrictMode", "Profiler", "Link", "Image", "Script", "Head", "Html", "Main", "NextScript", "Form"]);
|
|
1794
|
+
var LIST_COMPONENTS = 4;
|
|
1795
|
+
var LIST_TOKENS = 6;
|
|
1796
|
+
function defaultProseFiles(root, system, extra = []) {
|
|
1797
|
+
const out = [];
|
|
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
|
+
}
|
|
1806
|
+
return [...new Set(out)];
|
|
1807
|
+
}
|
|
1808
|
+
function walk2(root, dir, content = false) {
|
|
1809
|
+
if (!existsSync6(dir) || !statSync3(dir).isDirectory()) return [];
|
|
1810
|
+
const out = [];
|
|
1811
|
+
for (const name of readdirSync3(dir).sort()) {
|
|
1812
|
+
const path = join4(dir, name);
|
|
1813
|
+
if (statSync3(path).isDirectory()) {
|
|
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
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
return out;
|
|
1820
|
+
}
|
|
1821
|
+
function checkProse(root, files, inputs) {
|
|
1822
|
+
const components = new Set(inputs.components.flatMap((c) => [c.name, c.export]));
|
|
1823
|
+
const patterns = new Set(inputs.patterns.map((p) => p.name));
|
|
1824
|
+
const tokenNames = new Set(
|
|
1825
|
+
inputs.tokens.filter((t) => t.tier === "color" && !t.name.startsWith("--color-")).flatMap((t) => [t.name, t.name.slice(2)])
|
|
1826
|
+
);
|
|
1827
|
+
const allTokens = new Set(inputs.tokens.map((t) => t.name));
|
|
1828
|
+
const wrongComponent = /* @__PURE__ */ new Map();
|
|
1829
|
+
const wrongCode = /* @__PURE__ */ new Map();
|
|
1830
|
+
const wrongProse = [];
|
|
1831
|
+
for (const t of inputs.terms) {
|
|
1832
|
+
for (const w of t.wrong ?? []) {
|
|
1833
|
+
if (t.kind === "component" || t.kind === "pattern") wrongComponent.set(w, t);
|
|
1834
|
+
else if (t.kind === "product") wrongProse.push({ re: new RegExp(`(^|[^\\w-])${escape(w)}(?![\\w-])`, "i"), form: w, term: t });
|
|
1835
|
+
else wrongCode.set(w, t);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
const findings = [];
|
|
1839
|
+
for (const file of files) {
|
|
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);
|
|
1844
|
+
let inFence = false;
|
|
1845
|
+
let ignoring = false;
|
|
1846
|
+
let skipNext = false;
|
|
1847
|
+
let block;
|
|
1848
|
+
const closeBlock = () => {
|
|
1849
|
+
if (block && block.components.size >= LIST_COMPONENTS) {
|
|
1850
|
+
findings.push({ file, line: block.start, message: `restates ${block.components.size} components; point at components.json instead of listing them` });
|
|
1851
|
+
}
|
|
1852
|
+
if (block && block.tokens.size >= LIST_TOKENS) {
|
|
1853
|
+
findings.push({ file, line: block.start, message: `restates ${block.tokens.size} tokens; point at tokens.json instead of listing them` });
|
|
1854
|
+
}
|
|
1855
|
+
block = void 0;
|
|
1856
|
+
};
|
|
1857
|
+
lines.forEach((line, i) => {
|
|
1858
|
+
const n = i + 1;
|
|
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;
|
|
1869
|
+
if (!line.trim()) {
|
|
1870
|
+
closeBlock();
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
block ??= { start: n, components: /* @__PURE__ */ new Set(), tokens: /* @__PURE__ */ new Set() };
|
|
1874
|
+
for (const m of line.matchAll(/<([A-Z][A-Za-z0-9]*)(?=[\s/>.])/g)) {
|
|
1875
|
+
const name = m[1];
|
|
1876
|
+
if (components.has(name) || patterns.has(name) || NOT_SYSTEM.has(name)) continue;
|
|
1877
|
+
const wrong = wrongComponent.get(name);
|
|
1878
|
+
findings.push({
|
|
1879
|
+
file,
|
|
1880
|
+
line: n,
|
|
1881
|
+
message: wrong ? `<${name}>: the system calls this ${wrong.name}` : `<${name}> looks like a component, but the system has none by that name`
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
const code = inFence ? [line] : script ? [] : [...line.matchAll(/`([^`]+)`/g)].map((m) => m[1]);
|
|
1885
|
+
for (const span of code) {
|
|
1886
|
+
for (const word of span.match(/[A-Za-z][\w-]*/g) ?? []) {
|
|
1887
|
+
if (components.has(word)) block.components.add(word);
|
|
1888
|
+
if (tokenNames.has(word) || allTokens.has(`--${word}`) || allTokens.has(word)) block.tokens.add(word);
|
|
1889
|
+
const asComponent = wrongComponent.get(word);
|
|
1890
|
+
if (asComponent && !components.has(word) && !inFence) findings.push({ file, line: n, message: `\`${word}\`: the system calls this ${asComponent.name}` });
|
|
1891
|
+
const asCode = wrongCode.get(word);
|
|
1892
|
+
if (asCode) findings.push({ file, line: n, message: `\`${word}\`: the system calls this ${asCode.name}` });
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
if (!inFence) {
|
|
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);
|
|
1897
|
+
for (const { re, form, term } of wrongProse) if (re.test(line)) findings.push({ file, line: n, message: `"${form}": the product says "${term.name}"` });
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
closeBlock();
|
|
1901
|
+
}
|
|
1902
|
+
return findings.sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line || (a.message < b.message ? -1 : 1));
|
|
1903
|
+
}
|
|
1904
|
+
function stripGenerated(text2) {
|
|
1905
|
+
const start = text2.indexOf(AGENTS_START);
|
|
1906
|
+
const end = text2.indexOf(AGENTS_END);
|
|
1907
|
+
if (start === -1 || end < start) return text2;
|
|
1908
|
+
const blank = text2.slice(start, end + AGENTS_END.length).replace(/[^\n]/g, "");
|
|
1909
|
+
return text2.slice(0, start) + blank + text2.slice(end + AGENTS_END.length);
|
|
1910
|
+
}
|
|
1911
|
+
function escape(s) {
|
|
1912
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
export {
|
|
1916
|
+
MANIFEST_TAGS,
|
|
1917
|
+
parseJSDoc,
|
|
1918
|
+
findVariantConfigs,
|
|
1919
|
+
readExampleFile,
|
|
1920
|
+
classifyExamples,
|
|
1921
|
+
listComponentFiles,
|
|
1922
|
+
readComponents,
|
|
1923
|
+
readRules,
|
|
1924
|
+
readPatterns,
|
|
1925
|
+
buildTaxonomy,
|
|
1926
|
+
readAssets,
|
|
1927
|
+
readTokens,
|
|
1928
|
+
tierOf,
|
|
1929
|
+
MANIFEST_SCHEMA,
|
|
1930
|
+
GENERATOR_NAME,
|
|
1931
|
+
VERSION,
|
|
1932
|
+
GENERATOR,
|
|
1933
|
+
outDir,
|
|
1934
|
+
buildManifest,
|
|
1935
|
+
serialise,
|
|
1936
|
+
AGENTS_START,
|
|
1937
|
+
AGENTS_END,
|
|
1938
|
+
agentsFile,
|
|
1939
|
+
agentsSection,
|
|
1940
|
+
withAgentsSection,
|
|
1941
|
+
readIfExists,
|
|
1942
|
+
provenance,
|
|
1943
|
+
CONFIG_FILE,
|
|
1944
|
+
loadConfig,
|
|
1945
|
+
CHANGE_CLASSES,
|
|
1946
|
+
diffManifests,
|
|
1947
|
+
formatReport,
|
|
1948
|
+
defaultProseFiles,
|
|
1949
|
+
checkProse
|
|
1950
|
+
};
|