onsystem 0.7.0 → 0.9.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/README.md +19 -3
- package/dist/cli.js +513 -87
- package/dist/{config-1XSTYI44.d.ts → config-B98w8bcg.d.ts} +12 -0
- package/dist/{design-system-C4bhqLLw.js → design-system-Bykei5W0.js} +1838 -184
- package/dist/eslint-worker.d.ts +1 -1
- package/dist/eslint-worker.js +3 -5
- package/dist/eslint.js +1 -1
- package/dist/index.d.ts +27 -4
- package/dist/index.js +2 -2
- package/dist/{stdio-BojAQ6JP.js → stdio-B7u6dqLd.js} +347 -86
- package/dist/{workspace-Z0tZf_Dv.js → workspace-Dp1q8qZ8.js} +1 -1
- package/package.json +6 -4
- package/schema.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-
|
|
1
|
+
import { $ as LEGACY_CONFIG_FILES, At as relativePath, Ct as closest, Dt as SCRIPT_FILE, Et as unique, Mt as defaultProjectConfig, Ot as isInside, Tt as truncate, X as DEFAULT_SEVERITY, _t as detectProject, at as loadConfig, ct as formatPx, d as descriptionOf, dt as LIBRARY_IDS, et as RULE_IDS, f as searchComponents, ft as libraryTitle, h as uncheckedNotice, n as DesignSystemHost, pt as notReadYet, q as CONFIG_FILES, rt as excludedBy, u as buildSearchIndex, ut as LIBRARIES, v as NAME, vt as findDesignSystemPackage, wt as plural, y as VERSION, z as TOKEN_CATEGORIES } from "./design-system-Bykei5W0.js";
|
|
2
|
+
import { n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-Dp1q8qZ8.js";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -47,7 +47,10 @@ function explainProject(ds, cwd, indent = "") {
|
|
|
47
47
|
field("importPath", importPaths(ds));
|
|
48
48
|
field("libraries", config.libraryIds === void 0 ? `every library onsystem reads (${LIBRARY_IDS.join(", ")})` : config.libraryIds !== false && config.libraryIds.length ? config.libraryIds.join(", ") : `none ("libraries": ${JSON.stringify(config.libraryIds)})`);
|
|
49
49
|
if (config.tailwindConfig) field("tailwind", display(config.root, config.tailwindConfig));
|
|
50
|
-
if (config.designSystems)
|
|
50
|
+
if (config.designSystems) {
|
|
51
|
+
const ranked = config.designSystems.filter((d) => !d.registry).length > 1;
|
|
52
|
+
field("primary", config.designSystems.map((d, i) => `${i ? "then " : ""}${d.source}${d.registry ? ", read-only" : ranked ? ` (${d.imports} imports)` : ""}`).join("; "));
|
|
53
|
+
}
|
|
51
54
|
const changed = RULE_IDS.filter((id) => config.rules[id].severity !== DEFAULT_SEVERITY[id]);
|
|
52
55
|
field("rules", `${RULE_IDS.map((id) => `${id} ${config.rules[id].severity}`).join(", ")}${changed.length ? ` (changed: ${changed.join(", ")})` : ""}`);
|
|
53
56
|
if (config.overrides?.length) for (const override of config.overrides) field("override", `${override.files.join(", ")}: ${Object.entries(override.rules).map(([id, rule]) => `${id} ${rule.severity}`).join(", ")}`);
|
|
@@ -229,24 +232,133 @@ function importStatement(component) {
|
|
|
229
232
|
function variantSummary(component) {
|
|
230
233
|
return component.variants.map((v) => `${v.name}: ${v.values.join(" | ")}${v.default ? ` (default ${v.default})` : ""}`);
|
|
231
234
|
}
|
|
235
|
+
/** An icon: a component that renders an <svg>. */
|
|
236
|
+
function isIcon(component) {
|
|
237
|
+
return component.element === "svg";
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Icons from one import, or from the modules of one folder, are listed by name on one line from
|
|
241
|
+
* this many on: a spinner or a logo is listed like the rest.
|
|
242
|
+
*/
|
|
243
|
+
const ICON_LINE = 5;
|
|
244
|
+
/**
|
|
245
|
+
* What list_components shows of `items`, after `filter`. `packageOf`: at a workspace root, the
|
|
246
|
+
* package an item comes from, so two apps' `@/components/icons` are not taken for one.
|
|
247
|
+
*/
|
|
248
|
+
function listComponents(items, component, filter = {}, packageOf = () => "") {
|
|
249
|
+
const query = filter.query?.trim().toLowerCase();
|
|
250
|
+
const matching = items.filter((item) => {
|
|
251
|
+
const c = component(item);
|
|
252
|
+
if (filter.kind && filter.kind === "icon" !== isIcon(c)) return false;
|
|
253
|
+
const texts = [
|
|
254
|
+
c.name,
|
|
255
|
+
...c.aliases,
|
|
256
|
+
...c.subcomponents,
|
|
257
|
+
descriptionOf(c) ?? ""
|
|
258
|
+
];
|
|
259
|
+
return !query || texts.some((text) => text.toLowerCase().includes(query));
|
|
260
|
+
});
|
|
261
|
+
const icons = [];
|
|
262
|
+
if (!filter.kind) {
|
|
263
|
+
const group = (groups, item, from, folder) => {
|
|
264
|
+
const key = `${packageOf(item)}\0${from}`;
|
|
265
|
+
const known = groups.get(key) ?? {
|
|
266
|
+
from,
|
|
267
|
+
items: [],
|
|
268
|
+
...folder ? { folder } : {}
|
|
269
|
+
};
|
|
270
|
+
known.items.push(item);
|
|
271
|
+
groups.set(key, known);
|
|
272
|
+
};
|
|
273
|
+
const byImport = /* @__PURE__ */ new Map();
|
|
274
|
+
for (const item of matching) {
|
|
275
|
+
const c = component(item);
|
|
276
|
+
if (isIcon(c)) group(byImport, item, c.library ?? c.importPath);
|
|
277
|
+
}
|
|
278
|
+
const byFolder = /* @__PURE__ */ new Map();
|
|
279
|
+
for (const found of byImport.values()) {
|
|
280
|
+
if (found.items.length >= ICON_LINE) {
|
|
281
|
+
icons.push(found);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
for (const item of found.items) {
|
|
285
|
+
const c = component(item);
|
|
286
|
+
const folder = c.importPath.slice(0, Math.max(0, c.importPath.lastIndexOf("/")));
|
|
287
|
+
if (!c.library && folder) group(byFolder, item, folder, true);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
icons.push(...[...byFolder.values()].filter((found) => found.items.length >= ICON_LINE));
|
|
291
|
+
}
|
|
292
|
+
const inLine = new Set(icons.flatMap((found) => found.items));
|
|
293
|
+
const rest = matching.filter((item) => !inLine.has(item));
|
|
294
|
+
const shown = rest.slice(0, filter.limit ?? rest.length);
|
|
295
|
+
return {
|
|
296
|
+
shown,
|
|
297
|
+
icons,
|
|
298
|
+
matched: matching.length,
|
|
299
|
+
omitted: rest.length - shown.length
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/** `query "chart" and kind "icon"`, or '' without filters. */
|
|
303
|
+
function filterText(filter) {
|
|
304
|
+
return [filter.query?.trim() ? `query "${filter.query.trim()}"` : "", filter.kind ? `kind "${filter.kind}"` : ""].filter(Boolean).join(" and ");
|
|
305
|
+
}
|
|
306
|
+
/** The first line of a list: how many components there are, or how many match the filter. */
|
|
307
|
+
function listHead(roots, parts, listed, filter) {
|
|
308
|
+
const filtered = filterText(filter);
|
|
309
|
+
if (filtered) return `${listed.matched} of ${plural(roots, "component")} match ${filtered}`;
|
|
310
|
+
return `${plural(roots, "component")}${parts ? ` (+${plural(parts, "part")})` : ""}`;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* `128 icons from "@dub/ui" (each renders <svg>; import { Name } from "@dub/ui"): Apple, Bolt`,
|
|
314
|
+
* or for a module per icon, `6 icons in "@/components/icons" (each renders <svg>; import { Name }
|
|
315
|
+
* from "@/components/icons/<module>"): CheckIcon (check), PlusIcon (plus)`.
|
|
316
|
+
*/
|
|
317
|
+
function iconLine(group) {
|
|
318
|
+
const { from, items } = group;
|
|
319
|
+
const count = plural(items.length, "icon");
|
|
320
|
+
if (!group.folder) return `${count} from "${from}" (each renders <svg>; import { Name } from "${from}"): ${items.map((c) => c.name).join(", ")}`;
|
|
321
|
+
return `${count} in "${from}" (each renders <svg>; import { Name } from "${from}/<module>"): ${items.map((c) => `${c.name} (${c.importPath.slice(from.length + 1)})`).join(", ")}`;
|
|
322
|
+
}
|
|
323
|
+
/** What a list says at its end about the icons it named and the components `limit` left out. */
|
|
324
|
+
function listTail(listed) {
|
|
325
|
+
const lines = [];
|
|
326
|
+
if (listed.icons.length) lines.push("", "Pass kind \"icon\" to list the icons one by one.");
|
|
327
|
+
if (listed.omitted) lines.push("", `${listed.omitted.toLocaleString("en-US")} more not shown: pass a higher \`limit\`, or a \`query\`.`);
|
|
328
|
+
return lines;
|
|
329
|
+
}
|
|
232
330
|
/** `project`: the folder of a workspace root's project the list is for. */
|
|
233
|
-
function renderComponentList(ds, project) {
|
|
331
|
+
function renderComponentList(ds, project, filter = {}) {
|
|
234
332
|
const roots = ds.roots();
|
|
235
|
-
|
|
333
|
+
const where = project ? ` in ${project}` : "";
|
|
334
|
+
if (!roots.length) return `No components found${where}. Check the "components" globs in the config (root: ${ds.root}).`;
|
|
335
|
+
const listed = listComponents(roots, (c) => c, filter);
|
|
336
|
+
if (!listed.matched) return `No components match ${filterText(filter)}${where} (${roots.length} in all). Call list_components without filters to see them.`;
|
|
236
337
|
const parts = ds.components.length - roots.length;
|
|
237
|
-
const lines = [`${
|
|
238
|
-
const own =
|
|
239
|
-
for (const component of own) lines.push(...listItem(component));
|
|
240
|
-
for (const library of new Set(
|
|
338
|
+
const lines = [`${listHead(roots.length, parts, listed, filter)}${where}. Call get_component before using one.`, ""];
|
|
339
|
+
const own = listed.shown.filter((c) => !c.library);
|
|
340
|
+
for (const component of own) lines.push(...listItem(component, registryNote(ds, component)));
|
|
341
|
+
for (const library of new Set(listed.shown.flatMap((c) => c.library ? [c.library] : []))) {
|
|
241
342
|
if (own.length) lines.push("");
|
|
242
343
|
lines.push(`From ${library} (import { Name } from "${library}"):`, "");
|
|
243
|
-
for (const component of
|
|
344
|
+
for (const component of listed.shown.filter((c) => c.library === library)) lines.push(...listItem(component, "", false));
|
|
345
|
+
}
|
|
346
|
+
for (const group of listed.icons) {
|
|
347
|
+
if (lines.at(-1) !== "") lines.push("");
|
|
348
|
+
lines.push(iconLine(group));
|
|
244
349
|
}
|
|
350
|
+
lines.push(...listTail(listed));
|
|
245
351
|
return lines.join("\n");
|
|
246
352
|
}
|
|
353
|
+
/** ` (from @signoff-ui, read-only)` for a component copied from a shadcn registry. */
|
|
354
|
+
function registryNote(ds, component) {
|
|
355
|
+
const from = ds.registryOf(component);
|
|
356
|
+
return from ? ` (from ${from.registry}, read-only)` : "";
|
|
357
|
+
}
|
|
247
358
|
function listItem(component, note = "", withImport = true) {
|
|
248
359
|
const element = component.element ? ` <${component.element}>` : "";
|
|
249
|
-
const
|
|
360
|
+
const description = firstSentence(descriptionOf(component));
|
|
361
|
+
const lines = [`${component.name}${element}${note}${description ? ` — ${description}` : ""}`];
|
|
250
362
|
for (const variant of variantSummary(component)) lines.push(` ${variant}`);
|
|
251
363
|
if (component.subcomponents.length) lines.push(` parts: ${component.subcomponents.join(", ")}`);
|
|
252
364
|
if (withImport) lines.push(` ${importStatement(component)}`);
|
|
@@ -260,22 +372,44 @@ function listOf$1(items, max = 3) {
|
|
|
260
372
|
}
|
|
261
373
|
const PICK_PROJECT = "This is a monorepo root: pass `path`, the file you are editing, to get_component, get_tokens and check_ui so they answer for its project.";
|
|
262
374
|
/** Every project's components at a workspace root, grouped by the package they come from. */
|
|
263
|
-
function renderCatalogList(catalog) {
|
|
375
|
+
function renderCatalogList(catalog, filter = {}) {
|
|
264
376
|
const roots = catalog.roots();
|
|
265
377
|
if (!roots.length) return `No components found in any project of this monorepo (root: ${catalog.root}). Run \`onsystem inspect\` there to see what was looked at.`;
|
|
266
|
-
const
|
|
267
|
-
|
|
378
|
+
const listed = listComponents(roots, (e) => e.component, filter, (e) => e.package);
|
|
379
|
+
if (!listed.matched) return `No components match ${filterText(filter)} in any project (${roots.length} in all). Call list_components without filters to see them.`;
|
|
380
|
+
const packages = new Set(roots.map((e) => e.package));
|
|
268
381
|
const parts = catalog.entries.length - roots.length;
|
|
269
|
-
const lines = [`${
|
|
270
|
-
|
|
271
|
-
|
|
382
|
+
const lines = [`${listHead(roots.length, parts, listed, filter)} from ${plural(packages.size, "package")}, used by ${plural(catalog.projects.length, "project")}. ${PICK_PROJECT}`];
|
|
383
|
+
const byPackage = /* @__PURE__ */ new Map();
|
|
384
|
+
const section = (pkg) => {
|
|
385
|
+
const known = byPackage.get(pkg) ?? {
|
|
386
|
+
entries: [],
|
|
387
|
+
icons: []
|
|
388
|
+
};
|
|
389
|
+
byPackage.set(pkg, known);
|
|
390
|
+
return known;
|
|
391
|
+
};
|
|
392
|
+
for (const entry of listed.shown) section(entry.package).entries.push(entry);
|
|
393
|
+
for (const group of listed.icons) {
|
|
394
|
+
const [first] = group.items;
|
|
395
|
+
const items = group.items.map((e) => e.component);
|
|
396
|
+
if (first) section(first.package).icons.push(iconLine({
|
|
397
|
+
...group,
|
|
398
|
+
items
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
for (const [pkg, { entries, icons }] of byPackage) {
|
|
402
|
+
const projects = [...new Set(roots.filter((e) => e.package === pkg).flatMap((e) => e.projects))];
|
|
272
403
|
lines.push("", `## ${pkg} (used by ${listOf$1(projects)})`);
|
|
273
404
|
for (const entry of entries) {
|
|
274
|
-
const others = entry.ambiguous ?
|
|
405
|
+
const others = entry.ambiguous ? roots.filter((e) => e !== entry && e.component.name === entry.component.name) : [];
|
|
275
406
|
const note = others.length ? ` (also in ${[...new Set(others.map((e) => e.package))].join(", ")})` : "";
|
|
276
407
|
lines.push(...listItem(entry.component, note));
|
|
277
408
|
}
|
|
409
|
+
if (entries.length && icons.length) lines.push("");
|
|
410
|
+
lines.push(...icons);
|
|
278
411
|
}
|
|
412
|
+
lines.push(...listTail(listed));
|
|
279
413
|
return lines.join("\n");
|
|
280
414
|
}
|
|
281
415
|
/** Several components under one name at a workspace root, and how to pick one. */
|
|
@@ -354,18 +488,26 @@ const REACT_ARIA_NOTABLE = [
|
|
|
354
488
|
"isReadOnly",
|
|
355
489
|
"autoFocus"
|
|
356
490
|
];
|
|
491
|
+
/**
|
|
492
|
+
* A prop's description longer than this is cut to its first sentence: a primitive library's
|
|
493
|
+
* JSDoc (Base UI's, Radix's) runs to paragraphs per prop, and the structured content has it all.
|
|
494
|
+
*/
|
|
495
|
+
const PROP_LINE = 160;
|
|
357
496
|
function renderProp(prop) {
|
|
358
497
|
const optional = prop.required ? "" : "?";
|
|
359
498
|
const fallback = prop.default !== void 0 ? ` = ${prop.default}` : "";
|
|
360
499
|
const deprecated = prop.deprecated ? ` DEPRECATED${prop.deprecated === true ? "" : `: ${prop.deprecated}`}.` : "";
|
|
361
|
-
const
|
|
500
|
+
const text = prop.description?.replace(/\s*\n\s*/g, " ").trim();
|
|
501
|
+
const line = text && text.length > PROP_LINE ? firstSentence(text, PROP_LINE) : text;
|
|
502
|
+
const description = line ? ` — ${line}` : "";
|
|
362
503
|
return `- ${prop.name}${optional}: ${prop.type}${fallback}${description}${deprecated}`;
|
|
363
504
|
}
|
|
364
505
|
/** `origin`: at a workspace root, the package the component comes from and who uses it. */
|
|
365
506
|
function renderComponent(ds, component, origin) {
|
|
366
507
|
const lines = [`# ${component.name}`];
|
|
367
508
|
if (component.deprecated) lines.push(`DEPRECATED${component.deprecated === true ? "" : `: ${component.deprecated}`}`);
|
|
368
|
-
|
|
509
|
+
const description = descriptionOf(component);
|
|
510
|
+
if (description) lines.push(description);
|
|
369
511
|
lines.push("", importStatement(component));
|
|
370
512
|
const facts = [
|
|
371
513
|
component.element ? `Renders <${component.element}>` : void 0,
|
|
@@ -376,6 +518,15 @@ function renderComponent(ds, component, origin) {
|
|
|
376
518
|
].filter(Boolean);
|
|
377
519
|
lines.push(facts.join(" · "));
|
|
378
520
|
if (origin) lines.push(`From ${origin.package}, used by ${listOf$1(origin.projects)}.`);
|
|
521
|
+
const registry = ds.registryOf(component);
|
|
522
|
+
if (registry) lines.push(`Installed from the shadcn registry ${registry.registry} into ${registry.folder}: use it as it is. Its code is the registry's, so change it upstream, not here; \`npx shadcn add --overwrite\` replaces the file.`);
|
|
523
|
+
if (component.subcomponents.length) {
|
|
524
|
+
lines.push("", "## Parts");
|
|
525
|
+
for (const name of component.subcomponents) {
|
|
526
|
+
const part = ds.getComponent(name);
|
|
527
|
+
lines.push(`- ${name}${part?.description ? ` — ${firstSentence(part.description)}` : ""}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
379
530
|
lines.push("", "## Props");
|
|
380
531
|
if (!component.props.length && !component.inherits.length) lines.push("No props.");
|
|
381
532
|
for (const prop of component.props) lines.push(renderProp(prop));
|
|
@@ -401,13 +552,6 @@ function renderComponent(ds, component, origin) {
|
|
|
401
552
|
lines.push(` when ${when}: ${compound.classes}`);
|
|
402
553
|
}
|
|
403
554
|
}
|
|
404
|
-
if (component.subcomponents.length) {
|
|
405
|
-
lines.push("", "## Parts");
|
|
406
|
-
for (const name of component.subcomponents) {
|
|
407
|
-
const part = ds.getComponent(name);
|
|
408
|
-
lines.push(`- ${name}${part?.description ? ` — ${firstSentence(part.description)}` : ""}`);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
555
|
const tokens = ds.relatedTokens(component);
|
|
412
556
|
if (tokens.length) {
|
|
413
557
|
lines.push("", "## Design tokens used");
|
|
@@ -430,31 +574,63 @@ function renderComponent(ds, component, origin) {
|
|
|
430
574
|
}
|
|
431
575
|
return lines.join("\n");
|
|
432
576
|
}
|
|
577
|
+
/** The first line of search results: best first, or, when none clearly matches, that none does. */
|
|
578
|
+
function searchHead(query, hits, where = "") {
|
|
579
|
+
return hits.every((hit) => hit.weak) ? `No component clearly matches "${query}"${where}. The nearest have only a word or two in common with it:` : `Components for "${query}"${where}, best first:`;
|
|
580
|
+
}
|
|
581
|
+
/** The last line of search results. */
|
|
582
|
+
function searchTail(hits) {
|
|
583
|
+
return hits.every((hit) => hit.weak) ? "If none of them fits, the design system may have no component for this: ask, or build it from the components it has (list_components). Call get_component before using one." : "Call get_component for props, variants and examples.";
|
|
584
|
+
}
|
|
585
|
+
/** `1. CardHeader (part of Card) — Top section of a card.` */
|
|
586
|
+
function searchItem(index, component, from = "") {
|
|
587
|
+
const parent = component.parent ? ` (part of ${component.parent})` : "";
|
|
588
|
+
const description = firstSentence(descriptionOf(component));
|
|
589
|
+
return `${index + 1}. ${component.name}${parent}${from}${description ? ` — ${description}` : ""}`;
|
|
590
|
+
}
|
|
433
591
|
function renderSearch(query, hits) {
|
|
434
592
|
if (!hits.length) return `No components match "${query}". Try other words, or list_components to browse.`;
|
|
435
|
-
const lines = [
|
|
593
|
+
const lines = [searchHead(query, hits), ""];
|
|
436
594
|
hits.forEach((hit, i) => {
|
|
437
595
|
const c = hit.component;
|
|
438
|
-
|
|
439
|
-
lines.push(`${i + 1}. ${c.name}${parent} — ${firstSentence(c.description) || "No description."}`);
|
|
596
|
+
lines.push(searchItem(i, c));
|
|
440
597
|
lines.push(` score ${hit.score} · matched: ${hit.matched.join(", ")} · ${importStatement(c)}`);
|
|
441
598
|
});
|
|
442
|
-
lines.push("",
|
|
599
|
+
lines.push("", searchTail(hits));
|
|
443
600
|
return lines.join("\n");
|
|
444
601
|
}
|
|
445
602
|
/** Search hits at a workspace root, each with the package it comes from. */
|
|
446
603
|
function renderCatalogSearch(query, hits) {
|
|
447
604
|
if (!hits.length) return `No components match "${query}" in any project. Try other words, or list_components to browse.`;
|
|
448
|
-
const lines = [
|
|
605
|
+
const lines = [searchHead(query, hits, " across the monorepo's projects"), ""];
|
|
449
606
|
hits.forEach((hit, i) => {
|
|
450
607
|
const c = hit.component;
|
|
451
|
-
|
|
452
|
-
lines.push(`${i + 1}. ${c.name}${parent} from ${hit.entry.package} — ${firstSentence(c.description) || "No description."}`);
|
|
608
|
+
lines.push(searchItem(i, c, ` from ${hit.entry.package}`));
|
|
453
609
|
lines.push(` score ${hit.score} · matched: ${hit.matched.join(", ")} · used by ${listOf$1(hit.entry.projects)} · ${importStatement(c)}`);
|
|
454
610
|
});
|
|
455
|
-
lines.push("",
|
|
611
|
+
lines.push("", `${searchTail(hits)} ${PICK_PROJECT}`);
|
|
456
612
|
return lines.join("\n");
|
|
457
613
|
}
|
|
614
|
+
/**
|
|
615
|
+
* Where a token is listed in get_tokens: under its category, or, for a color of a scoped
|
|
616
|
+
* family (`chart-*`, `sidebar-*`, the syntax colors one file uses), under that family, with
|
|
617
|
+
* what it is for. An agent looking for a green with no `--success` in the theme otherwise finds
|
|
618
|
+
* `chart-2` among the colors, takes its dark value, and check_ui has nothing against it.
|
|
619
|
+
*/
|
|
620
|
+
function tokenGroup(ds, token) {
|
|
621
|
+
const family = ds.tokenIndex.scopedFamilyOf(token);
|
|
622
|
+
return family === void 0 ? token.category : `${SCOPED}${family}`;
|
|
623
|
+
}
|
|
624
|
+
/** Marks a scoped family's group: no category starts with it. */
|
|
625
|
+
const SCOPED = "\0";
|
|
626
|
+
function groupHeading(ds, group, count) {
|
|
627
|
+
if (!group.startsWith(SCOPED)) return `## ${group} (${count})`;
|
|
628
|
+
const family = group.slice(1);
|
|
629
|
+
if (family === "chart") return `## chart (${count}): for charts only, not for status colors`;
|
|
630
|
+
if (family === "sidebar") return `## sidebar (${count}): for the sidebar only, not for status colors`;
|
|
631
|
+
const [file] = ds.tokenIndex.familyFiles.get(family) ?? [];
|
|
632
|
+
return `## ${family} (${count}): for one part of the UI only${file ? ` (${file} uses them)` : ""}, not for status colors`;
|
|
633
|
+
}
|
|
458
634
|
/** Tokens at a workspace root, grouped by category, each with the package that defines it. */
|
|
459
635
|
function renderCatalogTokens(catalog, tokens, filter = {}) {
|
|
460
636
|
if (!catalog.tokens.length) return `No design tokens found in any project of this monorepo (root: ${catalog.root}). Set "tokens" in a project's config: https://onsystem.vercel.app/docs/configuration`;
|
|
@@ -463,10 +639,18 @@ function renderCatalogTokens(catalog, tokens, filter = {}) {
|
|
|
463
639
|
return `No tokens match${what ? ` ${what}` : ""} in any project; call get_tokens without filters to see them.`;
|
|
464
640
|
}
|
|
465
641
|
const groups = /* @__PURE__ */ new Map();
|
|
466
|
-
for (const t of tokens)
|
|
642
|
+
for (const t of tokens) {
|
|
643
|
+
const key = tokenGroup(t.ds, t.token);
|
|
644
|
+
const group = groups.get(key) ?? {
|
|
645
|
+
ds: t.ds,
|
|
646
|
+
list: []
|
|
647
|
+
};
|
|
648
|
+
group.list.push(t);
|
|
649
|
+
groups.set(key, group);
|
|
650
|
+
}
|
|
467
651
|
const lines = [PICK_PROJECT, ""];
|
|
468
|
-
for (const [
|
|
469
|
-
lines.push(
|
|
652
|
+
for (const [key, { ds, list }] of groups) {
|
|
653
|
+
lines.push(groupHeading(ds, key, list.length));
|
|
470
654
|
for (const { token, package: pkg } of list) {
|
|
471
655
|
const usage = token.usage.length ? ` → ${token.usage.join(", ")}` : "";
|
|
472
656
|
lines.push(`- ${token.name}: ${token.value}${usage} (${pkg})`);
|
|
@@ -485,10 +669,13 @@ function renderTokens(ds, tokens, filter = {}) {
|
|
|
485
669
|
return `No tokens match${what ? ` ${what}` : ""}. The design system has ${categories}; call get_tokens without filters to see them.`;
|
|
486
670
|
}
|
|
487
671
|
const groups = /* @__PURE__ */ new Map();
|
|
488
|
-
for (const token of tokens)
|
|
672
|
+
for (const token of tokens) {
|
|
673
|
+
const group = tokenGroup(ds, token);
|
|
674
|
+
groups.set(group, [...groups.get(group) ?? [], token]);
|
|
675
|
+
}
|
|
489
676
|
const lines = [];
|
|
490
|
-
for (const [
|
|
491
|
-
lines.push(
|
|
677
|
+
for (const [group, list] of groups) {
|
|
678
|
+
lines.push(groupHeading(ds, group, list.length));
|
|
492
679
|
for (const token of list) {
|
|
493
680
|
const px = token.category === "spacing" || token.category === "radius" ? ds.tokenIndex.toPx(token.value) : void 0;
|
|
494
681
|
const value = px !== void 0 && !/^-?[\d.]+px$/.test(token.value) ? `${token.value} = ${formatPx(px)}` : token.value;
|
|
@@ -538,7 +725,7 @@ const INSTRUCTIONS = `This server describes the project's design system: its Rea
|
|
|
538
725
|
When writing or changing UI in this project:
|
|
539
726
|
1. Find components with search_components (by intent) or list_components.
|
|
540
727
|
2. Read get_component before using a component: use only the props and variant values it lists.
|
|
541
|
-
3. Style with tokens from get_tokens, never with hex/rgb colors, arbitrary px values or Tailwind's default palette.
|
|
728
|
+
3. Style with tokens from get_tokens, never with hex/rgb colors, arbitrary px values or Tailwind's default palette. chart-* and sidebar-* tokens are for charts and the sidebar only: for a status color the theme has no token for (success, warning, info), add a token or keep the Tailwind color as check_ui says, and never use a chart-* or sidebar-* token for it.
|
|
542
729
|
4. Run check_ui on every snippet or file you write and fix all errors before you finish.
|
|
543
730
|
In a monorepo served from its root, pass \`path\` (the file you are editing) to list_components, get_component, search_components and get_tokens, and \`path\` or \`filename\` to check_ui, so each answer comes from that file's own project.`;
|
|
544
731
|
const READ_ONLY = {
|
|
@@ -566,6 +753,13 @@ const MAX_CODE = 1e6;
|
|
|
566
753
|
const DEFAULT_LIMIT = 50;
|
|
567
754
|
const MAX_LIMIT = 1e3;
|
|
568
755
|
/**
|
|
756
|
+
* check_ui's answer for a path or filename that is not JS or TS. `fix`: what to do instead,
|
|
757
|
+
* for a `filename` given with code (the code itself may be fine).
|
|
758
|
+
*/
|
|
759
|
+
function notScript(name, fix = "") {
|
|
760
|
+
return errorResult(`check_ui checks .tsx, .jsx, .ts and .js files; ${name} is not one, so it was not checked.${fix}`);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
569
763
|
* An integer, without the ±9007199254740991 bounds zod adds to the JSON Schema,
|
|
570
764
|
* which every client would otherwise read in each tool definition.
|
|
571
765
|
*/
|
|
@@ -661,8 +855,8 @@ const componentSummarySchema = z.object({
|
|
|
661
855
|
description: z.string().optional(),
|
|
662
856
|
importPath: z.string(),
|
|
663
857
|
element: z.string().optional(),
|
|
664
|
-
variants: z.record(z.string(), z.array(z.string())),
|
|
665
|
-
subcomponents: z.array(z.string()),
|
|
858
|
+
variants: z.record(z.string(), z.array(z.string())).optional().describe("Left out when none."),
|
|
859
|
+
subcomponents: z.array(z.string()).optional().describe("Left out when none."),
|
|
666
860
|
...originShape
|
|
667
861
|
});
|
|
668
862
|
const tokenSchema = z.object({
|
|
@@ -719,14 +913,58 @@ function origin(entry) {
|
|
|
719
913
|
projects: entry.projects
|
|
720
914
|
};
|
|
721
915
|
}
|
|
916
|
+
/** A component in list_components, without the fields it has nothing in. */
|
|
722
917
|
function summary(c) {
|
|
918
|
+
const description = descriptionOf(c);
|
|
723
919
|
return {
|
|
724
920
|
name: c.name,
|
|
725
|
-
...
|
|
921
|
+
...description ? { description } : {},
|
|
726
922
|
importPath: c.importPath,
|
|
727
923
|
...c.element ? { element: c.element } : {},
|
|
728
|
-
variants: Object.fromEntries(c.variants.map((v) => [v.name, v.values])),
|
|
729
|
-
subcomponents: c.subcomponents
|
|
924
|
+
...c.variants.length ? { variants: Object.fromEntries(c.variants.map((v) => [v.name, v.values])) } : {},
|
|
925
|
+
...c.subcomponents.length ? { subcomponents: c.subcomponents } : {}
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
/** list_components' structured content: what its text lists, as it lists it. */
|
|
929
|
+
function listContent(listed, component, originOf) {
|
|
930
|
+
return {
|
|
931
|
+
components: listed.shown.map((item) => ({
|
|
932
|
+
...summary(component(item)),
|
|
933
|
+
...originOf?.(item)
|
|
934
|
+
})),
|
|
935
|
+
...listed.icons.length ? { icons: listed.icons.map(({ from, items, folder }) => ({
|
|
936
|
+
from,
|
|
937
|
+
names: items.map((item) => component(item).name),
|
|
938
|
+
...folder ? { modules: items.map((item) => component(item).importPath) } : {},
|
|
939
|
+
...originOf && items[0] ? { package: originOf(items[0]).package } : {}
|
|
940
|
+
})) } : {},
|
|
941
|
+
...listed.omitted ? { omitted: listed.omitted } : {}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Said above a component asked for by a name that is not how its design system writes it:
|
|
946
|
+
* `Card.Header` finds CardHeader, and code written as asked would not compile.
|
|
947
|
+
*/
|
|
948
|
+
function nameNotice(asked, component) {
|
|
949
|
+
if (asked === component.name || component.aliases.includes(asked)) return "";
|
|
950
|
+
if (!asked.includes(".") && !component.name.includes(".")) return "";
|
|
951
|
+
return `There is no \`${asked}\` in this design system: write \`<${component.name}>\`.\n\n`;
|
|
952
|
+
}
|
|
953
|
+
function resourceDescription(component) {
|
|
954
|
+
const description = descriptionOf(component);
|
|
955
|
+
return description ? { description } : {};
|
|
956
|
+
}
|
|
957
|
+
/** A search_components result. */
|
|
958
|
+
function result(hit) {
|
|
959
|
+
const description = descriptionOf(hit.component);
|
|
960
|
+
return {
|
|
961
|
+
name: hit.component.name,
|
|
962
|
+
...hit.component.parent ? { parent: hit.component.parent } : {},
|
|
963
|
+
...description ? { description } : {},
|
|
964
|
+
importPath: hit.component.importPath,
|
|
965
|
+
score: hit.score,
|
|
966
|
+
matched: hit.matched,
|
|
967
|
+
...hit.weak ? { weak: true } : {}
|
|
730
968
|
};
|
|
731
969
|
}
|
|
732
970
|
/** Builds the MCP server: five tools, two resources and one prompt. Transport-agnostic. */
|
|
@@ -784,33 +1022,51 @@ function createServer({ getDesignSystem, getProjects }) {
|
|
|
784
1022
|
};
|
|
785
1023
|
server.registerTool("list_components", {
|
|
786
1024
|
title: "List design-system components",
|
|
787
|
-
description: "List every component in the project design system with a one-line description, the native element it renders, its variant values, its parts and its import. Call this before writing UI in this project. At a monorepo root, pass `path` for the project of the file you are editing; without it, every project is listed, each component with its package.",
|
|
788
|
-
inputSchema: z.object({
|
|
789
|
-
|
|
1025
|
+
description: "List every component in the project design system with a one-line description, the native element it renders, its variant values, its parts and its import. Icons (components that render an <svg>) are named on one line per import, or per folder for icons with a module each, five or more to a line. Call this before writing UI in this project. `query`, `kind` and `limit` narrow a long list. At a monorepo root, pass `path` for the project of the file you are editing; without it, every project is listed, each component with its package.",
|
|
1026
|
+
inputSchema: z.object({
|
|
1027
|
+
query: z.string().max(MAX_QUERY).optional().describe("Only components with this text in their name, their parts' names or their description."),
|
|
1028
|
+
kind: z.enum(["component", "icon"]).optional().describe("\"icon\": only the components that render an <svg>, one by one. \"component\": all the others."),
|
|
1029
|
+
limit: z.number().int().min(1).max(MAX_LIMIT).optional().describe("Most components to list one by one."),
|
|
1030
|
+
path: projectPath
|
|
1031
|
+
}),
|
|
1032
|
+
outputSchema: z.object({
|
|
1033
|
+
components: z.array(componentSummarySchema).describe("The components listed one by one, after `query`, `kind` and `limit`."),
|
|
1034
|
+
icons: z.array(z.object({
|
|
1035
|
+
from: z.string().describe("The module they are imported from, or, with `modules`, the folder of their modules."),
|
|
1036
|
+
names: z.array(z.string()),
|
|
1037
|
+
modules: z.array(z.string()).optional().describe("For icons with a module each: each one's module, in the order of `names`."),
|
|
1038
|
+
package: originShape.package
|
|
1039
|
+
})).optional().describe("Without `kind`: icons, the components that render an <svg>, by name, five or more per module, or per folder for icons with a module each."),
|
|
1040
|
+
omitted: int().optional().describe("Matching components left out by `limit`.")
|
|
1041
|
+
}),
|
|
790
1042
|
annotations: READ_ONLY
|
|
791
|
-
}, async ({ path: target }) => {
|
|
1043
|
+
}, async ({ query, kind, limit, path: target }) => {
|
|
792
1044
|
const found = await scoped(target);
|
|
793
1045
|
if (isResult(found)) return found;
|
|
1046
|
+
const filter = {
|
|
1047
|
+
query,
|
|
1048
|
+
kind,
|
|
1049
|
+
limit
|
|
1050
|
+
};
|
|
794
1051
|
if ("catalog" in found) {
|
|
795
1052
|
const { catalog } = found;
|
|
1053
|
+
const listed = listComponents(catalog.roots(), (entry) => entry.component, filter, (entry) => entry.package);
|
|
796
1054
|
return {
|
|
797
1055
|
content: [{
|
|
798
1056
|
type: "text",
|
|
799
|
-
text: renderCatalogList(catalog)
|
|
1057
|
+
text: renderCatalogList(catalog, filter)
|
|
800
1058
|
}],
|
|
801
|
-
structuredContent:
|
|
802
|
-
...summary(entry.component),
|
|
803
|
-
...origin(entry)
|
|
804
|
-
})) }
|
|
1059
|
+
structuredContent: listContent(listed, (entry) => entry.component, origin)
|
|
805
1060
|
};
|
|
806
1061
|
}
|
|
807
1062
|
const { ds } = found;
|
|
1063
|
+
const listed = listComponents(ds.roots(), (c) => c, filter);
|
|
808
1064
|
return {
|
|
809
1065
|
content: [{
|
|
810
1066
|
type: "text",
|
|
811
|
-
text: renderComponentList(ds, found.project?.dir)
|
|
1067
|
+
text: renderComponentList(ds, found.project?.dir, filter)
|
|
812
1068
|
}],
|
|
813
|
-
structuredContent:
|
|
1069
|
+
structuredContent: listContent(listed, (c) => c)
|
|
814
1070
|
};
|
|
815
1071
|
});
|
|
816
1072
|
server.registerTool("get_component", {
|
|
@@ -835,13 +1091,15 @@ function createServer({ getDesignSystem, getProjects }) {
|
|
|
835
1091
|
return errorResult(`No component named "${name}" in any project.` + (guess ? ` Did you mean "${guess}"?` : "") + ` Available: ${names.join(", ")}.`);
|
|
836
1092
|
}
|
|
837
1093
|
if (matches.length > 1) return errorResult(renderAmbiguous(name, matches));
|
|
1094
|
+
const description = descriptionOf(entry.component);
|
|
838
1095
|
return {
|
|
839
1096
|
content: [{
|
|
840
1097
|
type: "text",
|
|
841
|
-
text: renderComponent(entry.ds, entry.component, entry)
|
|
1098
|
+
text: nameNotice(name, entry.component) + renderComponent(entry.ds, entry.component, entry)
|
|
842
1099
|
}],
|
|
843
1100
|
structuredContent: {
|
|
844
1101
|
...entry.component,
|
|
1102
|
+
...description ? { description } : {},
|
|
845
1103
|
import: importStatement(entry.component),
|
|
846
1104
|
tokens: entry.ds.relatedTokens(entry.component).map((t) => t.name),
|
|
847
1105
|
...origin(entry)
|
|
@@ -861,13 +1119,15 @@ function createServer({ getDesignSystem, getProjects }) {
|
|
|
861
1119
|
};
|
|
862
1120
|
}
|
|
863
1121
|
const shown = fromRoot(ds, component, found.root);
|
|
1122
|
+
const description = descriptionOf(component);
|
|
864
1123
|
return {
|
|
865
1124
|
content: [{
|
|
866
1125
|
type: "text",
|
|
867
|
-
text: renderComponent(ds, shown)
|
|
1126
|
+
text: nameNotice(name, component) + renderComponent(ds, shown)
|
|
868
1127
|
}],
|
|
869
1128
|
structuredContent: {
|
|
870
1129
|
...shown,
|
|
1130
|
+
...description ? { description } : {},
|
|
871
1131
|
import: importStatement(component),
|
|
872
1132
|
tokens: ds.relatedTokens(component).map((t) => t.name)
|
|
873
1133
|
}
|
|
@@ -875,7 +1135,7 @@ function createServer({ getDesignSystem, getProjects }) {
|
|
|
875
1135
|
});
|
|
876
1136
|
server.registerTool("search_components", {
|
|
877
1137
|
title: "Search components by intent",
|
|
878
|
-
description: "Find components by what you want to build, e.g. \"confirm a destructive action\", \"status label\", \"text field with validation\". Ranks names, descriptions, docs, props and variant values with BM25 (local, no API key). Use it when you do not know which component fits.",
|
|
1138
|
+
description: "Find components by what you want to build, e.g. \"confirm a destructive action\", \"status label\", \"text field with validation\". Ranks names, descriptions, docs, props and variant values with BM25 (local, no API key). When no component clearly matches, it says so and lists the nearest, marked `weak`. Use it when you do not know which component fits.",
|
|
879
1139
|
inputSchema: z.object({
|
|
880
1140
|
query: z.string().min(1).max(MAX_QUERY).describe("What the UI should do, in plain words."),
|
|
881
1141
|
limit: z.number().int().min(1).max(20).default(5).describe("Maximum results."),
|
|
@@ -888,6 +1148,7 @@ function createServer({ getDesignSystem, getProjects }) {
|
|
|
888
1148
|
importPath: z.string(),
|
|
889
1149
|
score: z.number(),
|
|
890
1150
|
matched: z.array(z.string()),
|
|
1151
|
+
weak: z.boolean().optional().describe("Set when no component clearly matches the query: each result has only a word or two in common with it."),
|
|
891
1152
|
...originShape
|
|
892
1153
|
})) }),
|
|
893
1154
|
annotations: READ_ONLY
|
|
@@ -902,12 +1163,7 @@ function createServer({ getDesignSystem, getProjects }) {
|
|
|
902
1163
|
text: renderCatalogSearch(query, hits)
|
|
903
1164
|
}],
|
|
904
1165
|
structuredContent: { results: hits.map((h) => ({
|
|
905
|
-
|
|
906
|
-
...h.component.parent ? { parent: h.component.parent } : {},
|
|
907
|
-
...h.component.description ? { description: h.component.description } : {},
|
|
908
|
-
importPath: h.component.importPath,
|
|
909
|
-
score: h.score,
|
|
910
|
-
matched: h.matched,
|
|
1166
|
+
...result(h),
|
|
911
1167
|
...origin(h.entry)
|
|
912
1168
|
})) }
|
|
913
1169
|
};
|
|
@@ -919,19 +1175,12 @@ function createServer({ getDesignSystem, getProjects }) {
|
|
|
919
1175
|
type: "text",
|
|
920
1176
|
text: renderSearch(query, hits)
|
|
921
1177
|
}],
|
|
922
|
-
structuredContent: { results: hits.map(
|
|
923
|
-
name: h.component.name,
|
|
924
|
-
...h.component.parent ? { parent: h.component.parent } : {},
|
|
925
|
-
...h.component.description ? { description: h.component.description } : {},
|
|
926
|
-
importPath: h.component.importPath,
|
|
927
|
-
score: h.score,
|
|
928
|
-
matched: h.matched
|
|
929
|
-
})) }
|
|
1178
|
+
structuredContent: { results: hits.map(result) }
|
|
930
1179
|
};
|
|
931
1180
|
});
|
|
932
1181
|
server.registerTool("get_tokens", {
|
|
933
1182
|
title: "Get design tokens",
|
|
934
|
-
description: "List design tokens with resolved values, dark-mode values and how to use each (Tailwind class or CSS variable). Filter by category (color, spacing, radius, typography, shadow, other) or a substring. Use these instead of hex codes, px values or Tailwind default palette classes.",
|
|
1183
|
+
description: "List design tokens with resolved values, dark-mode values and how to use each (Tailwind class or CSS variable). Filter by category (color, spacing, radius, typography, shadow, other) or a substring. Use these instead of hex codes, px values or Tailwind default palette classes. Chart and sidebar colors are listed under their own headings: they are for charts and the sidebar, not for status colors.",
|
|
935
1184
|
inputSchema: z.object({
|
|
936
1185
|
category: z.enum(TOKEN_CATEGORIES).optional().describe("Only tokens of this category."),
|
|
937
1186
|
query: z.string().max(MAX_QUERY).optional().describe("Substring to match in names, usages or descriptions."),
|
|
@@ -996,10 +1245,10 @@ function createServer({ getDesignSystem, getProjects }) {
|
|
|
996
1245
|
});
|
|
997
1246
|
server.registerTool("check_ui", {
|
|
998
1247
|
title: "Check UI code against the design system",
|
|
999
|
-
description: "Lint TSX/JSX against the design system. Reports hardcoded colors, spacing and radius (with the nearest token), native elements that have a design-system component, unknown components, props and variant values, and icon-only buttons without an accessible name. Each diagnostic has line/column, a rule id, a message and a suggested fix. Run it on every snippet or file you write and fix all errors. Pass `code` for unsaved code,
|
|
1248
|
+
description: "Lint TSX/JSX against the design system. Reports hardcoded colors, spacing and radius (with the nearest token), native elements that have a design-system component, unknown components, props and variant values, and icon-only buttons without an accessible name. Each diagnostic has line/column, a rule id, a message and a suggested fix. Run it on every snippet or file you write and fix all errors. Pass `path` for a file in the project, or `code` for unsaved code, with `path` (or `filename`) naming the file it is for or its project folder.",
|
|
1000
1249
|
inputSchema: z.object({
|
|
1001
1250
|
code: z.string().max(MAX_CODE).optional().describe(`TSX/JSX source to check (up to ${MAX_CODE.toLocaleString("en")} characters).`),
|
|
1002
|
-
path: z.string().max(MAX_PATH).optional().describe("Path of a
|
|
1251
|
+
path: z.string().max(MAX_PATH).optional().describe("Path of a .tsx, .jsx, .ts or .js file, relative to the project root (the monorepo root, when served from there) or absolute: the file to check, or, with `code`, the file the code is for, as `filename`. With `code`, a project folder only picks the project."),
|
|
1003
1252
|
filename: z.string().max(MAX_PATH).optional().describe("Name to report `code` under; a .jsx extension parses it as JSX. At a monorepo root, the path the code will be saved under, which picks the project whose design system checks it. When the server was started outside your project, pass it as an absolute path: the project is found from it."),
|
|
1004
1253
|
limit: z.number().int().min(1).max(MAX_LIMIT).default(DEFAULT_LIMIT).describe(`Most diagnostics to return, errors first (default ${DEFAULT_LIMIT}). The counts and byRule cover all of them.`)
|
|
1005
1254
|
}),
|
|
@@ -1014,7 +1263,7 @@ function createServer({ getDesignSystem, getProjects }) {
|
|
|
1014
1263
|
errors: int(),
|
|
1015
1264
|
warnings: int()
|
|
1016
1265
|
})).describe("Every finding per rule, shown or not."),
|
|
1017
|
-
skipped: z.string().optional().describe("Why the file was not checked: it renders an image (next/og) or an email."),
|
|
1266
|
+
skipped: z.string().optional().describe("Why the file was not checked: it renders an image (next/og) or an email, or the config's `exclude` leaves it out."),
|
|
1018
1267
|
unchecked: z.object({
|
|
1019
1268
|
names: z.array(z.string()),
|
|
1020
1269
|
modules: z.array(z.string())
|
|
@@ -1022,10 +1271,14 @@ function createServer({ getDesignSystem, getProjects }) {
|
|
|
1022
1271
|
notice: z.string().optional().describe("Set when no components or no color tokens were found, so rules did not run, when components came from modules the model does not include, when the project depends on a component library onsystem does not read yet, or when its dependencies are not installed.")
|
|
1023
1272
|
}),
|
|
1024
1273
|
annotations: READ_ONLY
|
|
1025
|
-
}, async ({ code, path: filePath, filename, limit }) => {
|
|
1274
|
+
}, async ({ code, path: filePath, filename: given, limit }) => {
|
|
1026
1275
|
if (code === void 0 && !filePath) return errorResult("Pass either `code` or `path`.");
|
|
1027
|
-
|
|
1276
|
+
if (code !== void 0 && given !== void 0 && !SCRIPT_FILE.test(given)) return notScript(given, " Pass as `filename` the .tsx path the code is for, or leave `filename` out.");
|
|
1277
|
+
const folder = code !== void 0 && filePath && !SCRIPT_FILE.test(filePath) ? filePath : void 0;
|
|
1278
|
+
const filename = code === void 0 || folder !== void 0 ? given : given ?? (filePath || void 0);
|
|
1279
|
+
let routeBy = code === void 0 ? filePath : filename ?? folder;
|
|
1028
1280
|
const set = await getProjects?.(routeBy);
|
|
1281
|
+
if (set && folder !== void 0 && routeBy === folder && path.resolve(set.root, folder) === set.root && !set.dirs.includes(".")) routeBy = void 0;
|
|
1029
1282
|
let ds;
|
|
1030
1283
|
if (!set) ds = await getDesignSystem(routeBy);
|
|
1031
1284
|
else if (routeBy) {
|
|
@@ -1048,6 +1301,7 @@ function createServer({ getDesignSystem, getProjects }) {
|
|
|
1048
1301
|
const absolute = path.resolve(base, filePath);
|
|
1049
1302
|
const outside = errorResult(`${filePath} is outside the project root (${base}).`);
|
|
1050
1303
|
if (!isInside(base, absolute)) return outside;
|
|
1304
|
+
if (!SCRIPT_FILE.test(absolute)) return notScript(filePath);
|
|
1051
1305
|
try {
|
|
1052
1306
|
const [root, real] = await Promise.all([fsp.realpath(base), fsp.realpath(absolute)]);
|
|
1053
1307
|
if (!isInside(root, real)) return outside;
|
|
@@ -1071,7 +1325,14 @@ function createServer({ getDesignSystem, getProjects }) {
|
|
|
1071
1325
|
file = relativePath(ds.root, absolute);
|
|
1072
1326
|
shown = relativePath(base, absolute);
|
|
1073
1327
|
}
|
|
1074
|
-
const
|
|
1328
|
+
const excluded = (filename !== void 0 || code === void 0) && !path.isAbsolute(file) ? excludedBy(ds.config, file) : void 0;
|
|
1329
|
+
const checked = excluded ? {
|
|
1330
|
+
file,
|
|
1331
|
+
diagnostics: [],
|
|
1332
|
+
errorCount: 0,
|
|
1333
|
+
warningCount: 0,
|
|
1334
|
+
skipped: `It is left out by "${excluded}" in the config's exclude, as in check, the ESLint plugin and the Claude Code hook.`
|
|
1335
|
+
} : ds.check(source, file);
|
|
1075
1336
|
const result = file === shown ? checked : {
|
|
1076
1337
|
...checked,
|
|
1077
1338
|
file: shown
|
|
@@ -1105,14 +1366,14 @@ function createServer({ getDesignSystem, getProjects }) {
|
|
|
1105
1366
|
uri: `ds://components/${c.name}${ambiguous ? `?path=${encodeURIComponent(projects[0] ?? ".")}` : ""}`,
|
|
1106
1367
|
name: c.name,
|
|
1107
1368
|
title: c.name,
|
|
1108
|
-
...c
|
|
1369
|
+
...resourceDescription(c),
|
|
1109
1370
|
mimeType: "text/markdown"
|
|
1110
1371
|
})) };
|
|
1111
1372
|
return { resources: found.ds.components.map((c) => ({
|
|
1112
1373
|
uri: `ds://components/${c.name}`,
|
|
1113
1374
|
name: c.name,
|
|
1114
1375
|
title: c.name,
|
|
1115
|
-
...c
|
|
1376
|
+
...resourceDescription(c),
|
|
1116
1377
|
mimeType: "text/markdown"
|
|
1117
1378
|
})) };
|
|
1118
1379
|
},
|
|
@@ -1193,8 +1454,8 @@ function createServer({ getDesignSystem, getProjects }) {
|
|
|
1193
1454
|
|
|
1194
1455
|
Work like this:
|
|
1195
1456
|
1. Find components: search_components for each piece of the UI (for example "confirm a destructive action"), or list_components for an overview.
|
|
1196
|
-
2. Read the contract: get_component for every component you use. Use only the props and variant values it lists, and compose parts
|
|
1197
|
-
3. Style with tokens: get_tokens when you need a color, spacing or radius. Never write hex/rgb colors, arbitrary px values or Tailwind default palette classes such as bg-blue-500.
|
|
1457
|
+
2. Read the contract: get_component for every component you use. Use only the props and variant values it lists, and compose its parts as its docs and examples show, when it has them.
|
|
1458
|
+
3. Style with tokens: get_tokens when you need a color, spacing or radius. Never write hex/rgb colors, arbitrary px values or Tailwind default palette classes such as bg-blue-500. chart-* and sidebar-* tokens are for charts and the sidebar only: for a status color the theme has no token for (success, warning, info), add a token or keep the Tailwind color as check_ui says, and never use a chart-* or sidebar-* token for it.
|
|
1198
1459
|
4. Use design-system components instead of native elements (<Button>, not <button>).
|
|
1199
1460
|
5. Verify: run check_ui on the code you wrote, fix every error, and run it again until it reports none.
|
|
1200
1461
|
|