synthesisui 0.16.92 → 0.16.94
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/dist/commands/import.js +66 -4
- package/dist/commands/mcp.js +88 -2
- package/dist/doctor/call-sites.js +52 -4
- package/dist/doctor/sketch.js +55 -0
- package/dist/doctor/transcribe.js +29 -4
- package/package.json +1 -1
package/dist/commands/import.js
CHANGED
|
@@ -4,7 +4,7 @@ import { resolveAnatomy, resolveFlatParts, safePartName, } from "../anatomy-read
|
|
|
4
4
|
import { readCredentials, readToken, resolveRegistry, sameRegistry, } from "../config.js";
|
|
5
5
|
import { architectureRule, describeArchitecture, describeChoice, detectArchitectures, } from "../doctor/architecture.js";
|
|
6
6
|
import { findBrokenRefs } from "../doctor/broken-refs.js";
|
|
7
|
-
import { nestingRules, propRules, readDefinitionProps, readNesting, } from "../doctor/call-sites.js";
|
|
7
|
+
import { nestingRules, propRules, readDefinitionProps, readNesting, readRuntime, } from "../doctor/call-sites.js";
|
|
8
8
|
import { asCatalogueTable, describeFallback, fetchCatalogue, } from "../doctor/catalogue-fetch.js";
|
|
9
9
|
import { describeClassStyle, detectClassStyle, } from "../doctor/class-style.js";
|
|
10
10
|
import { isNearDuplicate, isNeutral, lightness, } from "../doctor/color-distance.js";
|
|
@@ -293,6 +293,10 @@ export async function takeCensus(root, opts) {
|
|
|
293
293
|
* filtering them takes the vitrine from 271 to 29 (dono, 01/08).
|
|
294
294
|
*/
|
|
295
295
|
const screensOf = new Map();
|
|
296
|
+
/** Quem é dono do estado de cada componente, lido da definição. */
|
|
297
|
+
const runtimeOf = new Map();
|
|
298
|
+
/** Em quais projetos (--usage roots) cada componente aparece. */
|
|
299
|
+
const projectsOf = new Map();
|
|
296
300
|
/** Component → the axes its own `cva` declares. See `variant-read.ts`. */
|
|
297
301
|
const variantAxes = new Map();
|
|
298
302
|
/** How much came out of CSS Modules, for the coverage report. */
|
|
@@ -376,7 +380,11 @@ export async function takeCensus(root, opts) {
|
|
|
376
380
|
});
|
|
377
381
|
}
|
|
378
382
|
for (const d of found) {
|
|
379
|
-
|
|
383
|
+
const props = readDefinitionProps(d.name, src);
|
|
384
|
+
propsOf.set(d.name, props);
|
|
385
|
+
const runtime = readRuntime(props, src);
|
|
386
|
+
if (Object.keys(runtime).length > 0)
|
|
387
|
+
runtimeOf.set(d.name, runtime);
|
|
380
388
|
}
|
|
381
389
|
if (found.length > 0) {
|
|
382
390
|
countShape(src, found[0].name, shapes);
|
|
@@ -388,7 +396,30 @@ export async function takeCensus(root, opts) {
|
|
|
388
396
|
// component's own look, and everything below it is a part this slice does
|
|
389
397
|
// not attempt.
|
|
390
398
|
if (found.length > 0) {
|
|
391
|
-
|
|
399
|
+
/**
|
|
400
|
+
* EVERY COMPONENT THE FILE DEFINES, not just the first.
|
|
401
|
+
*
|
|
402
|
+
* One file in a real library defines two - `RadioCard` and
|
|
403
|
+
* `RadioCardGroup` - and the second arrived with an empty base while its
|
|
404
|
+
* `flex flex-col gap-3` sat six lines below the first component's return
|
|
405
|
+
* (audit, dono, 01/08). The file-level readings (the CSS module, the
|
|
406
|
+
* variant tables) belong to the PRIMARY definition; the root classes and
|
|
407
|
+
* the root tag are per component, scoped by name.
|
|
408
|
+
*/
|
|
409
|
+
for (const extra of found.slice(1)) {
|
|
410
|
+
const et = transcribe(rootClasses(src, extra.name), declaredValues);
|
|
411
|
+
const etag = rootTag(src, extra.name);
|
|
412
|
+
const esize = Object.keys(et.base).length +
|
|
413
|
+
Object.keys(et.dark).length +
|
|
414
|
+
Object.keys(et.states).length;
|
|
415
|
+
if (esize > 0 || etag) {
|
|
416
|
+
looks[extra.name] = {
|
|
417
|
+
...et,
|
|
418
|
+
...(etag ? { rootTag: etag } : {}),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const t = transcribe(rootClasses(src, found[0].name), declaredValues);
|
|
392
423
|
/**
|
|
393
424
|
* THE VARIANT STYLES, FOLDED IN.
|
|
394
425
|
*
|
|
@@ -472,7 +503,7 @@ export async function takeCensus(root, opts) {
|
|
|
472
503
|
Object.keys(states).length +
|
|
473
504
|
Object.keys(parts).length +
|
|
474
505
|
layers.length;
|
|
475
|
-
const tag = rootTag(src);
|
|
506
|
+
const tag = rootTag(src, found[0].name);
|
|
476
507
|
/**
|
|
477
508
|
* THE SKETCH travels with the look, so the reading phase never opens this
|
|
478
509
|
* file again: naming parts is a sentence over data the census already holds,
|
|
@@ -598,6 +629,17 @@ export async function takeCensus(root, opts) {
|
|
|
598
629
|
usageSeen.add(rel);
|
|
599
630
|
usageProgress?.step(rel);
|
|
600
631
|
scanComponentsInto(usageTally, rel, src, [...shared, ...uInternal]);
|
|
632
|
+
// WHERE THE PAIRS LIVE. A library composes almost none of itself inside
|
|
633
|
+
// itself - `Button` goes inside `Modal` in the APP, not in the library
|
|
634
|
+
// source - so measuring nesting only on the defined loop reported zero
|
|
635
|
+
// companions on a 128-component library (e2e, 01/08).
|
|
636
|
+
readNesting(rel, src, nesting);
|
|
637
|
+
// Which PROJECT composes it - the credibility panel's number, per root.
|
|
638
|
+
for (const m of src.matchAll(/<([A-Z][A-Za-z0-9_]*)/g)) {
|
|
639
|
+
const at = projectsOf.get(m[1]) ?? new Set();
|
|
640
|
+
at.add(u.label);
|
|
641
|
+
projectsOf.set(m[1], at);
|
|
642
|
+
}
|
|
601
643
|
}
|
|
602
644
|
}
|
|
603
645
|
usageProgress?.done();
|
|
@@ -907,10 +949,30 @@ export async function takeCensus(root, opts) {
|
|
|
907
949
|
for (const pair of nestingRules(nesting, systemOwns)) {
|
|
908
950
|
laws.set(pair.component, [...(laws.get(pair.component) ?? []), pair.text]);
|
|
909
951
|
}
|
|
952
|
+
/**
|
|
953
|
+
* OS ENCAIXES, ESTRUTURADOS. nestingRules já media os pares e virava só prosa -
|
|
954
|
+
* prosa não é clicável nem consultável (dono, 01/08). O slug segue a mesma
|
|
955
|
+
* kebab-ização que o contrato usa.
|
|
956
|
+
*/
|
|
957
|
+
const companionsOf = new Map();
|
|
958
|
+
for (const pair of nestingRules(nesting, systemOwns)) {
|
|
959
|
+
const kebab = (n) => n.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
960
|
+
const list = companionsOf.get(pair.component) ?? [];
|
|
961
|
+
if (!list.includes(kebab(pair.child)))
|
|
962
|
+
list.push(kebab(pair.child));
|
|
963
|
+
companionsOf.set(pair.component, list);
|
|
964
|
+
}
|
|
910
965
|
const components = merged.map((c) => ({
|
|
911
966
|
...c,
|
|
912
967
|
...(verdicts.get(c.name) ?? {}),
|
|
913
968
|
...(laws.has(c.name) ? { laws: laws.get(c.name) } : {}),
|
|
969
|
+
...(runtimeOf.has(c.name) ? { runtime: runtimeOf.get(c.name) } : {}),
|
|
970
|
+
...(companionsOf.has(c.name)
|
|
971
|
+
? { companions: companionsOf.get(c.name) }
|
|
972
|
+
: {}),
|
|
973
|
+
...(projectsOf.has(c.name)
|
|
974
|
+
? { projects: [...(projectsOf.get(c.name) ?? [])] }
|
|
975
|
+
: {}),
|
|
914
976
|
}));
|
|
915
977
|
const pkgRaw = await readFile(join(root, "package.json"), "utf8").catch(() => null);
|
|
916
978
|
let name = null;
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { relative, resolve } from "node:path";
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
3
|
import { readToken, resolveRegistry } from "../config.js";
|
|
4
4
|
import { fileRequest } from "../doctor/requests.js";
|
|
5
5
|
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
@@ -340,6 +340,54 @@ async function validateRecipe(name, recipe) {
|
|
|
340
340
|
* a published package can be read by anyone, and it goes stale - the crosswalk's own copy
|
|
341
341
|
* had 26 names against a catalogue of 41.
|
|
342
342
|
*/
|
|
343
|
+
/**
|
|
344
|
+
* O CONTADOR DO AGENTE, fire-and-forget: a tela do UI Kit mostra "vezes que esta receita
|
|
345
|
+
* foi consultada pelo Agente", e este é o único ponto onde a consulta acontece de
|
|
346
|
+
* verdade. Nunca bloqueia a resposta: sem sessão ou sem rede, não conta e não quebra -
|
|
347
|
+
* telemetria que atrasa a ferramenta é telemetria que alguém desliga.
|
|
348
|
+
*/
|
|
349
|
+
function countAgentRead(root, component, action) {
|
|
350
|
+
void (async () => {
|
|
351
|
+
try {
|
|
352
|
+
const token = await readToken();
|
|
353
|
+
if (!token || !component)
|
|
354
|
+
return;
|
|
355
|
+
// O slug vem do .lock do sistema instalado - a mesma resolução do sync.
|
|
356
|
+
const dsDir = join(root, "_synthesisui", "ds");
|
|
357
|
+
let slug = null;
|
|
358
|
+
const entries = await readdir(dsDir, { withFileTypes: true }).catch(() => []);
|
|
359
|
+
for (const e of entries) {
|
|
360
|
+
if (!e.isDirectory())
|
|
361
|
+
continue;
|
|
362
|
+
const raw = await readFile(join(dsDir, e.name, ".lock"), "utf8").catch(() => "");
|
|
363
|
+
try {
|
|
364
|
+
const lock = JSON.parse(raw);
|
|
365
|
+
if (lock.slug) {
|
|
366
|
+
slug = lock.slug;
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// lock ilegível - tenta o próximo
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (!slug)
|
|
375
|
+
return;
|
|
376
|
+
const base = resolveRegistry();
|
|
377
|
+
await fetch(`${base}/api/telemetry/agent-read`, {
|
|
378
|
+
method: "POST",
|
|
379
|
+
headers: {
|
|
380
|
+
Authorization: `Bearer ${token}`,
|
|
381
|
+
"content-type": "application/json",
|
|
382
|
+
},
|
|
383
|
+
body: JSON.stringify({ slug, component, action }),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// offline não conta e não quebra nada
|
|
388
|
+
}
|
|
389
|
+
})();
|
|
390
|
+
}
|
|
343
391
|
async function askCatalogue(want, post) {
|
|
344
392
|
const token = await readToken();
|
|
345
393
|
if (!token) {
|
|
@@ -399,6 +447,42 @@ async function describeComponent(root, name) {
|
|
|
399
447
|
? "A molecule in your own ladder - built out of your atoms, so reach for those inside it."
|
|
400
448
|
: "An organism in your own ladder - a whole region of a screen, so it is more likely to be composed than to be composed into.");
|
|
401
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* O ESTADO, ANTES DO CÓDIGO. Um agente que não sabe quem é dono do estado inventa:
|
|
452
|
+
* um Toggle controlado ganha useState interno, um Input não-controlado ganha value
|
|
453
|
+
* sem onChange (dono, 01/08). Isto é a receita dizendo o comportamento.
|
|
454
|
+
*/
|
|
455
|
+
if (recipe.runtime) {
|
|
456
|
+
const r = recipe.runtime;
|
|
457
|
+
const lines = [];
|
|
458
|
+
if (r.stateOwner === "caller") {
|
|
459
|
+
lines.push("State: the CALLER owns it - controlled only. Wire the pair below; do not add internal state.");
|
|
460
|
+
}
|
|
461
|
+
else if (r.stateOwner === "self") {
|
|
462
|
+
lines.push("State: the component manages itself. Do not wire external state unless you need to read it.");
|
|
463
|
+
}
|
|
464
|
+
else if (r.stateOwner === "either") {
|
|
465
|
+
lines.push("State: both modes. Pass the controlled pair to own it, or the default* prop to seed it and let the component manage.");
|
|
466
|
+
}
|
|
467
|
+
for (const c of r.controlled ?? []) {
|
|
468
|
+
lines.push(` controlled: ${c.prop} + ${c.event}`);
|
|
469
|
+
}
|
|
470
|
+
if ((r.uncontrolled ?? []).length > 0) {
|
|
471
|
+
lines.push(` uncontrolled seed: ${(r.uncontrolled ?? []).join(", ")}`);
|
|
472
|
+
}
|
|
473
|
+
for (const n of r.notes ?? [])
|
|
474
|
+
lines.push(` ${n}`);
|
|
475
|
+
if (lines.length > 0)
|
|
476
|
+
out.push("", ...lines);
|
|
477
|
+
}
|
|
478
|
+
/** OS ENCAIXES - o que costuma ir junto, medido ou derivado dos layouts. */
|
|
479
|
+
if ((recipe.companions ?? []).length > 0) {
|
|
480
|
+
out.push("", `Goes with: ${(recipe.companions ?? []).join(", ")} - measured pairings; call describe_component on one before composing them together.`);
|
|
481
|
+
}
|
|
482
|
+
if (recipe.usageStats && recipe.usageStats.count > 0) {
|
|
483
|
+
const u = recipe.usageStats;
|
|
484
|
+
out.push("", `In use: ${u.count} instance${u.count === 1 ? "" : "s"} across ${u.files} file${u.files === 1 ? "" : "s"}${u.projects?.length ? ` in ${u.projects.join(", ")}` : ""}.`);
|
|
485
|
+
}
|
|
402
486
|
// WHAT IT SITS ON. 17 of 23 components in a real library carry no surface of
|
|
403
487
|
// their own, because the surface belongs to what they return.
|
|
404
488
|
const p = recipe.preview;
|
|
@@ -492,8 +576,10 @@ async function callTool(root, name, args) {
|
|
|
492
576
|
case "list_components":
|
|
493
577
|
return text(await listComponents(root));
|
|
494
578
|
case "describe_component":
|
|
579
|
+
countAgentRead(root, String(args.name ?? ""), "describe");
|
|
495
580
|
return text(await describeComponent(root, String(args.name ?? "")));
|
|
496
581
|
case "add_component":
|
|
582
|
+
countAgentRead(root, String(args.name ?? ""), "add");
|
|
497
583
|
return text(await addComponent(root, String(args.name ?? "")));
|
|
498
584
|
case "recipe_vocabulary":
|
|
499
585
|
return text(await recipeVocabulary());
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* version tried to consume the arrow, and `([^)]*)` swallowed the destructure it was
|
|
38
38
|
* trying to reach, so every arrow component read as declaring no props (spec, 01/08).
|
|
39
39
|
*/
|
|
40
|
-
const DESTRUCTURED = /(?:function\s+([A-Z][A-Za-z0-9_]*)\s*\(\s*\{([^}]*)\}|(?:const|let)\s+([A-Z][A-Za-z0-9_]*)\s*(?::[^=]*)?=\s*(?:function\s*)?\(\s*\{([^}]*)\})/g;
|
|
40
|
+
const DESTRUCTURED = /(?:function\s+([A-Z][A-Za-z0-9_]*)\s*\(\s*\{([^}]*)\}|(?:const|let)\s+([A-Z][A-Za-z0-9_]*)\s*(?::[^=]*)?=\s*(?:function\s*)?\(\s*\{([^}]*)\}|(?:const|let)\s+([A-Z][A-Za-z0-9_]*)\s*=\s*(?:React\.)?(?:forwardRef|memo)(?:<[^>]*>)?\s*\(\s*(?:function\s*[A-Za-z0-9_]*\s*)?\(\s*\{([^}]*)\})/g;
|
|
41
41
|
/** `{...rest}` / `{...props}` on a JSX element - the forwarding, in the markup. */
|
|
42
42
|
const SPREAD_ONTO = /\{\s*\.\.\.\s*([A-Za-z_$][\w$]*)\s*\}/g;
|
|
43
43
|
/**
|
|
@@ -62,13 +62,16 @@ export function readDefinitionProps(name, source) {
|
|
|
62
62
|
*/
|
|
63
63
|
DESTRUCTURED.lastIndex = 0;
|
|
64
64
|
const matches = [...source.matchAll(DESTRUCTURED)];
|
|
65
|
-
|
|
65
|
+
// The third alternative is the forwardRef-inline idiom: a real Toggle is
|
|
66
|
+
// `const Toggle = React.forwardRef<…>(({ checked, onCheckedChange, … }, ref) =>`,
|
|
67
|
+
// and both earlier shapes read past it (test16, 01/08).
|
|
68
|
+
const exact = matches.filter((m) => (m[1] ?? m[3] ?? m[5]) === name);
|
|
66
69
|
const prefixed = matches.filter((m) => {
|
|
67
|
-
const found = m[1] ?? m[3];
|
|
70
|
+
const found = m[1] ?? m[3] ?? m[5];
|
|
68
71
|
return found !== name && found?.startsWith(name);
|
|
69
72
|
});
|
|
70
73
|
for (const m of exact.length > 0 ? exact : prefixed) {
|
|
71
|
-
const body = m[2] ?? m[4] ?? "";
|
|
74
|
+
const body = m[2] ?? m[4] ?? m[6] ?? "";
|
|
72
75
|
for (const raw of body.split(",")) {
|
|
73
76
|
const part = raw.trim();
|
|
74
77
|
if (!part)
|
|
@@ -187,3 +190,48 @@ export function propRules(name, read) {
|
|
|
187
190
|
}
|
|
188
191
|
return out;
|
|
189
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* WHO OWNS THE STATE - read off the definition, deterministically.
|
|
195
|
+
*
|
|
196
|
+
* The recipe said everything about the LOOK and nothing about behaviour, so an agent
|
|
197
|
+
* writing code in the client invented it: a controlled Toggle grew an internal
|
|
198
|
+
* useState, an uncontrolled Input got a value with no onChange (dono, 01/08). The
|
|
199
|
+
* patterns are machine-readable in the props themselves:
|
|
200
|
+
*
|
|
201
|
+
* checked + onChange the CONTROLLED pair - the caller owns the state
|
|
202
|
+
* defaultOpen, defaultValue the UNCONTROLLED seed - the component owns it
|
|
203
|
+
* useState in the body the component manages something itself
|
|
204
|
+
*
|
|
205
|
+
* `either` is both at once, which is exactly how a well-made input is written.
|
|
206
|
+
*/
|
|
207
|
+
export function readRuntime(read, source) {
|
|
208
|
+
const props = new Set(read.explicit);
|
|
209
|
+
const controlled = [];
|
|
210
|
+
for (const prop of read.explicit) {
|
|
211
|
+
if (/^(on[A-Z]|default[A-Z])/.test(prop))
|
|
212
|
+
continue;
|
|
213
|
+
const cap = prop[0].toUpperCase() + prop.slice(1);
|
|
214
|
+
// `checked`+`onChange` is the DOM's own spelling; `open`+`onOpenChange` is the
|
|
215
|
+
// primitive libraries'. Both are the same claim.
|
|
216
|
+
const event = [`on${cap}Change`, "onChange", `on${cap}`].find((e) => props.has(e));
|
|
217
|
+
if (event)
|
|
218
|
+
controlled.push({ prop, event });
|
|
219
|
+
}
|
|
220
|
+
const uncontrolled = read.explicit.filter((p) => /^default[A-Z]/.test(p));
|
|
221
|
+
const managesItself = /\buseState\s*[<(]/.test(source);
|
|
222
|
+
const out = {};
|
|
223
|
+
if (controlled.length > 0)
|
|
224
|
+
out.controlled = controlled;
|
|
225
|
+
if (uncontrolled.length > 0)
|
|
226
|
+
out.uncontrolled = uncontrolled;
|
|
227
|
+
if (controlled.length > 0 && (uncontrolled.length > 0 || managesItself)) {
|
|
228
|
+
out.stateOwner = "either";
|
|
229
|
+
}
|
|
230
|
+
else if (controlled.length > 0) {
|
|
231
|
+
out.stateOwner = "caller";
|
|
232
|
+
}
|
|
233
|
+
else if (managesItself || uncontrolled.length > 0) {
|
|
234
|
+
out.stateOwner = "self";
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
package/dist/doctor/sketch.js
CHANGED
|
@@ -23,6 +23,56 @@ import { scanTags } from "./css-modules.js";
|
|
|
23
23
|
const MAX_NODES = 60;
|
|
24
24
|
/** `className="..."` or className={"..."} / {`...`} with no interpolation. */
|
|
25
25
|
const CLASS_ATTR = /className=\{?["'`]([^"'`{}$]*)["'`]\}?/;
|
|
26
|
+
/**
|
|
27
|
+
* `className={cn("a b", "c", cond && "d", record[x])}` - the STATIC strings.
|
|
28
|
+
*
|
|
29
|
+
* The plain-attr regex above sees a call expression and reads nothing, and
|
|
30
|
+
* 19 of 36 components in a real library style exactly this way - so their
|
|
31
|
+
* elements arrived in the sketch with no classes at all, and the Tooltip's
|
|
32
|
+
* popup carried none when the base had to be transcribed by hand
|
|
33
|
+
* (not-expressed.md, dono, 01/08). The always-on string arguments ARE the
|
|
34
|
+
* element's resting classes; the conditional halves are variants, which the
|
|
35
|
+
* variant readers already read - so only TOP-LEVEL string literals count.
|
|
36
|
+
* Template literals with `${}` and computed values stay out: a class we
|
|
37
|
+
* cannot see in full is a class we would be guessing at.
|
|
38
|
+
*
|
|
39
|
+
* THE NAMESPACE COUNTS. Measured on the real library: 64 calls are a bare
|
|
40
|
+
* `cn(`, 11 are `twUtils.cn(` and 2 are `clsx(` - and the first version of this
|
|
41
|
+
* regex read only the bare form, so a Modal's whole surface (its fill, border,
|
|
42
|
+
* radius and padding) stayed invisible while its siblings read fine
|
|
43
|
+
* (e2e, dono, 01/08).
|
|
44
|
+
*/
|
|
45
|
+
const CLASS_CALL = /className=\{\s*(?:[A-Za-z_$][\w$]*\.)?(?:cn|clsx|classNames|cx|twMerge|twJoin|classcat)\(/;
|
|
46
|
+
function staticCallClasses(body) {
|
|
47
|
+
const call = CLASS_CALL.exec(body);
|
|
48
|
+
if (!call)
|
|
49
|
+
return null;
|
|
50
|
+
const start = call.index + call[0].length;
|
|
51
|
+
// Walk the call's arguments at depth 0, collecting bare string literals.
|
|
52
|
+
let depth = 1;
|
|
53
|
+
let argStart = start;
|
|
54
|
+
const args = [];
|
|
55
|
+
for (let i = start; i < body.length && depth > 0; i++) {
|
|
56
|
+
const ch = body[i];
|
|
57
|
+
if (ch === "(" || ch === "{" || ch === "[")
|
|
58
|
+
depth += 1;
|
|
59
|
+
else if (ch === ")" || ch === "}" || ch === "]") {
|
|
60
|
+
depth -= 1;
|
|
61
|
+
if (depth === 0)
|
|
62
|
+
args.push(body.slice(argStart, i));
|
|
63
|
+
}
|
|
64
|
+
else if (ch === "," && depth === 1) {
|
|
65
|
+
args.push(body.slice(argStart, i));
|
|
66
|
+
argStart = i + 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const classes = args
|
|
70
|
+
.map((a) => a.trim())
|
|
71
|
+
.filter((a) => /^(["'`]).*\1$/.test(a) && !a.includes("${"))
|
|
72
|
+
.map((a) => a.slice(1, -1).trim())
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
return classes.length > 0 ? classes.join(" ") : null;
|
|
75
|
+
}
|
|
26
76
|
/**
|
|
27
77
|
* The sketch of one component file.
|
|
28
78
|
*
|
|
@@ -47,6 +97,11 @@ export function sketchOf(source) {
|
|
|
47
97
|
const cls = CLASS_ATTR.exec(event.body);
|
|
48
98
|
if (cls?.[1].trim())
|
|
49
99
|
node.classes = cls[1].trim();
|
|
100
|
+
else {
|
|
101
|
+
const called = staticCallClasses(event.body);
|
|
102
|
+
if (called)
|
|
103
|
+
node.classes = called;
|
|
104
|
+
}
|
|
50
105
|
// Literal text: the slice between this open and the next event, when it is prose.
|
|
51
106
|
const next = events[i + 1];
|
|
52
107
|
if (next) {
|
|
@@ -1090,15 +1090,40 @@ export function transcribe(classes, declared) {
|
|
|
1090
1090
|
* which is still a signal, just a weaker one, so it travels as written and the
|
|
1091
1091
|
* platform decides what to do with it.
|
|
1092
1092
|
*/
|
|
1093
|
-
|
|
1094
|
-
|
|
1093
|
+
/**
|
|
1094
|
+
* WHERE *THIS* COMPONENT'S `return (` IS.
|
|
1095
|
+
*
|
|
1096
|
+
* Both readers below took the FIRST `return (` in the file, which is right for
|
|
1097
|
+
* the 34 files that define one component and wrong for the one that defines
|
|
1098
|
+
* two: `RadioCard.tsx` exports `RadioCard` and `RadioCardGroup`, and the group
|
|
1099
|
+
* - a real `flex flex-col gap-3` - arrived with an empty base and the screen
|
|
1100
|
+
* said "Not written yet" over it (audit, dono, 01/08).
|
|
1101
|
+
*
|
|
1102
|
+
* Scoped by NAME when a name is given: find that declaration, then its return.
|
|
1103
|
+
* Absent or unfound, the old behaviour stands exactly - which keeps every
|
|
1104
|
+
* single-component file reading byte-identically.
|
|
1105
|
+
*/
|
|
1106
|
+
function returnAt(source, name) {
|
|
1107
|
+
if (name) {
|
|
1108
|
+
const decl = new RegExp(`(?:function|const|let|var)\\s+${name}\\b|${name}\\s*[:=]\\s*(?:function|\\()`).exec(source);
|
|
1109
|
+
if (decl) {
|
|
1110
|
+
const from = source.slice(decl.index);
|
|
1111
|
+
const ret = from.search(/return\s*\(/);
|
|
1112
|
+
if (ret !== -1)
|
|
1113
|
+
return decl.index + ret;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return source.search(/return\s*\(/);
|
|
1117
|
+
}
|
|
1118
|
+
export function rootTag(source, name) {
|
|
1119
|
+
const ret = returnAt(source, name);
|
|
1095
1120
|
if (ret === -1)
|
|
1096
1121
|
return null;
|
|
1097
1122
|
const m = /<([A-Za-z][A-Za-z0-9.]*)/.exec(source.slice(ret));
|
|
1098
1123
|
return m ? m[1] : null;
|
|
1099
1124
|
}
|
|
1100
|
-
export function rootClasses(source) {
|
|
1101
|
-
const ret = source
|
|
1125
|
+
export function rootClasses(source, name) {
|
|
1126
|
+
const ret = returnAt(source, name);
|
|
1102
1127
|
if (ret === -1)
|
|
1103
1128
|
return [];
|
|
1104
1129
|
const open = source.indexOf("<", ret);
|
package/package.json
CHANGED