synthesisui 0.16.96 → 0.16.98
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/anatomy-read.js +54 -8
- package/dist/commands/import.js +7 -2
- package/dist/commands/mcp.js +136 -5
- package/dist/doctor/sketch.js +17 -1
- package/dist/doctor/transcribe.js +252 -4
- package/dist/skill-import.js +26 -0
- package/package.json +1 -1
package/dist/anatomy-read.js
CHANGED
|
@@ -96,10 +96,41 @@ function formOf(node) {
|
|
|
96
96
|
* `declared` is their own tokens, so a `bg-ocean-500` on a nested part resolves
|
|
97
97
|
* to the name they gave it rather than to a literal.
|
|
98
98
|
*/
|
|
99
|
+
/**
|
|
100
|
+
* WHAT THE PARENT PAINTS ON A FRONTIER, kept as written.
|
|
101
|
+
*
|
|
102
|
+
* A `component` or `external` node names another component and stops - and what
|
|
103
|
+
* the parent adds at the call site is the PARENT's decision, which had nowhere
|
|
104
|
+
* to go. Measured (not-expressed.md, dono, 01/08): `MetricCard` composes
|
|
105
|
+
* `<Card className="flex-1 dark:bg-darkgray-300">` and the dark override was
|
|
106
|
+
* lost, so the preview drew the wrong dark; `Paginate` lost a `min-w-[120px]`
|
|
107
|
+
* floor on both its buttons.
|
|
108
|
+
*
|
|
109
|
+
* Kept as the raw string rather than transcribed: this is an override on top of
|
|
110
|
+
* a recipe we do not own, and the renderer applies it as a class. Reading it into
|
|
111
|
+
* declarations would mean deciding which of the two wins, which is the compiler's
|
|
112
|
+
* job and not this module's.
|
|
113
|
+
*/
|
|
114
|
+
function frontierClasses(node, sketch) {
|
|
115
|
+
const raw = typeof node.classes === "string"
|
|
116
|
+
? node.classes
|
|
117
|
+
: typeof node.at === "number"
|
|
118
|
+
? sketch?.[node.at]?.classes
|
|
119
|
+
: undefined;
|
|
120
|
+
const trimmed = raw?.trim();
|
|
121
|
+
return trimmed ? { classes: trimmed } : {};
|
|
122
|
+
}
|
|
99
123
|
export function resolveAnatomy(read, declared, deps,
|
|
100
124
|
/** Their name → the slug it reaches in this system. See `RefResolver`. */
|
|
101
125
|
resolve,
|
|
102
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* What the component returns, when the skill said and it is not a plain tag.
|
|
128
|
+
*
|
|
129
|
+
* A STRING or an object: `{ name, at }` / `{ name, classes }` carries what the
|
|
130
|
+
* PARENT paints on the root at the call site. `MetricCard` returns
|
|
131
|
+
* `<Card className="flex-1 dark:bg-darkgray-300">` and that override had
|
|
132
|
+
* nowhere to travel (not-expressed.md, dono, 01/08).
|
|
133
|
+
*/
|
|
103
134
|
root,
|
|
104
135
|
/** This component's sketch, so a node can name its classes by INDEX instead
|
|
105
136
|
* of retyping them - see `AnatomyRead.at`. */
|
|
@@ -168,9 +199,12 @@ sketch) {
|
|
|
168
199
|
* kebab, which is what a system generated by us would want anyway.
|
|
169
200
|
*/
|
|
170
201
|
const slug = resolve?.(name) ?? safePartName(name);
|
|
171
|
-
out.push(
|
|
172
|
-
|
|
173
|
-
|
|
202
|
+
out.push({
|
|
203
|
+
as,
|
|
204
|
+
ref: slug || safePartName(name),
|
|
205
|
+
...(slug && slug !== safePartName(name) ? { refName: name } : {}),
|
|
206
|
+
...frontierClasses(node, sketch),
|
|
207
|
+
});
|
|
174
208
|
continue;
|
|
175
209
|
}
|
|
176
210
|
const from = String(node.from ?? "").trim();
|
|
@@ -179,7 +213,12 @@ sketch) {
|
|
|
179
213
|
const version = deps?.[from];
|
|
180
214
|
if (!external.some((e) => e.from === from))
|
|
181
215
|
external.push({ from, version });
|
|
182
|
-
out.push(
|
|
216
|
+
out.push({
|
|
217
|
+
as,
|
|
218
|
+
from,
|
|
219
|
+
...(version ? { version } : {}),
|
|
220
|
+
...frontierClasses(node, sketch),
|
|
221
|
+
});
|
|
183
222
|
continue;
|
|
184
223
|
}
|
|
185
224
|
const node_ = { as };
|
|
@@ -258,15 +297,22 @@ sketch) {
|
|
|
258
297
|
* through the crosswalk exactly like a child frontier does.
|
|
259
298
|
*/
|
|
260
299
|
let rootOut;
|
|
261
|
-
const
|
|
300
|
+
const rootObj = typeof root === "object" && root ? root : null;
|
|
301
|
+
const rootRaw = String((rootObj ? rootObj.name : root) ?? "").trim();
|
|
302
|
+
// The parent's own override on the root it returns, by index or as written.
|
|
303
|
+
const rootPainted = rootObj
|
|
304
|
+
? (frontierClasses(rootObj, sketch).classes ??
|
|
305
|
+
"")
|
|
306
|
+
: "";
|
|
307
|
+
const paints = rootPainted ? { classes: rootPainted } : {};
|
|
262
308
|
if (rootRaw && /^[A-Z]/.test(rootRaw)) {
|
|
263
309
|
if (rootRaw.includes(".")) {
|
|
264
|
-
rootOut = { from: rootRaw };
|
|
310
|
+
rootOut = { from: rootRaw, ...paints };
|
|
265
311
|
}
|
|
266
312
|
else {
|
|
267
313
|
const slug = resolve?.(rootRaw) ?? safePartName(rootRaw);
|
|
268
314
|
if (slug)
|
|
269
|
-
rootOut = { ref: slug, name: rootRaw };
|
|
315
|
+
rootOut = { ref: slug, name: rootRaw, ...paints };
|
|
270
316
|
}
|
|
271
317
|
}
|
|
272
318
|
if (coerced > 0) {
|
package/dist/commands/import.js
CHANGED
|
@@ -409,11 +409,16 @@ export async function takeCensus(root, opts) {
|
|
|
409
409
|
for (const extra of found.slice(1)) {
|
|
410
410
|
const et = transcribe(rootClasses(src, extra.name), declaredValues);
|
|
411
411
|
const etag = rootTag(src, extra.name);
|
|
412
|
+
// ITS OWN MARKUP, sliced at the definition boundary - the whole file's
|
|
413
|
+
// sketch belonged to the first component and the second read an empty
|
|
414
|
+
// one (not-expressed.md, dono, 01/08).
|
|
415
|
+
const esketch = sketchOf(src, extra.name);
|
|
412
416
|
const esize = Object.keys(et.base).length +
|
|
413
417
|
Object.keys(et.dark).length +
|
|
414
418
|
Object.keys(et.states).length;
|
|
415
|
-
if (esize > 0 || etag) {
|
|
419
|
+
if (esize > 0 || etag || esketch.length > 0) {
|
|
416
420
|
looks[extra.name] = {
|
|
421
|
+
...(esketch.length > 0 ? { sketch: esketch } : {}),
|
|
417
422
|
...et,
|
|
418
423
|
...(etag ? { rootTag: etag } : {}),
|
|
419
424
|
};
|
|
@@ -510,7 +515,7 @@ export async function takeCensus(root, opts) {
|
|
|
510
515
|
* and a file read whose content is here is a pipeline optimization failure -
|
|
511
516
|
* the owner's rule, adopted verbatim (dono, 01/08).
|
|
512
517
|
*/
|
|
513
|
-
const sketch = sketchOf(src);
|
|
518
|
+
const sketch = sketchOf(src, found[0].name);
|
|
514
519
|
if (size > 0 || tag) {
|
|
515
520
|
looks[found[0].name] = {
|
|
516
521
|
...(sketch.length > 0 ? { sketch } : {}),
|
package/dist/commands/mcp.js
CHANGED
|
@@ -368,10 +368,135 @@ async function validateRecipe(name, recipe) {
|
|
|
368
368
|
* what would NOT survive. A list of 30 "this one is fine" lines is a wall the
|
|
369
369
|
* reader has to skim to find the two that matter.
|
|
370
370
|
*/
|
|
371
|
-
|
|
371
|
+
/**
|
|
372
|
+
* RESOLVE `at` BEFORE VALIDATING - the validator never sees the census.
|
|
373
|
+
*
|
|
374
|
+
* A node styled by `at` carries no `classes`, so it looks EMPTY to the validator
|
|
375
|
+
* and complete to the importer: four recipes came back BELOW THE FLOOR on a real
|
|
376
|
+
* run and all four were whole, because their pixels live in parts the sketch
|
|
377
|
+
* holds and the validator could not reach (not-expressed.md, dono, 01/08). The
|
|
378
|
+
* verdict was a false accusation about our own optimisation.
|
|
379
|
+
*
|
|
380
|
+
* This side HAS the census - the same file the lock and the sketch come from - so
|
|
381
|
+
* the index is resolved here, once, and the validator judges the recipe somebody
|
|
382
|
+
* is actually about to send. `classes` already present wins, exactly as the
|
|
383
|
+
* importer resolves it.
|
|
384
|
+
*/
|
|
385
|
+
async function resolveAtNodes(root, entries) {
|
|
386
|
+
let looks = {};
|
|
387
|
+
try {
|
|
388
|
+
const raw = await readFile(join(root, "_synthesisui", "census.json"), "utf8");
|
|
389
|
+
looks = JSON.parse(raw).looks ?? {};
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// No census: nothing to resolve, and validating the recipe as sent is still
|
|
393
|
+
// better than refusing to validate at all.
|
|
394
|
+
return entries;
|
|
395
|
+
}
|
|
396
|
+
/** Their spelling → the look, matched the way the contract kebabs a name. */
|
|
397
|
+
const lookFor = (name) => {
|
|
398
|
+
if (looks[name])
|
|
399
|
+
return looks[name];
|
|
400
|
+
const slug = name.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
401
|
+
for (const [theirs, look] of Object.entries(looks)) {
|
|
402
|
+
if (theirs.replace(/[^a-z0-9]/gi, "").toLowerCase() === slug)
|
|
403
|
+
return look;
|
|
404
|
+
}
|
|
405
|
+
return undefined;
|
|
406
|
+
};
|
|
407
|
+
return entries.map((entry) => {
|
|
408
|
+
const look = lookFor(String(entry.name ?? ""));
|
|
409
|
+
const sketch = look?.sketch;
|
|
410
|
+
/**
|
|
411
|
+
* THE STYLES THE IMPORT WILL MERGE - so the floor measured is the floor the
|
|
412
|
+
* component arrives with.
|
|
413
|
+
*
|
|
414
|
+
* The reading sends an ANATOMY: a tree of `at` indices, deliberately without
|
|
415
|
+
* the classes, because retyping them is the loss the index exists to prevent.
|
|
416
|
+
* The styles are the census's measured `base` and `layers`, merged at import.
|
|
417
|
+
* So the validator was judging a recipe whose look had not arrived yet, and
|
|
418
|
+
* all 35 components came back BELOW THE FLOOR with the same four lines - a
|
|
419
|
+
* verdict about a payload nobody will ever send (not-expressed.md, dono,
|
|
420
|
+
* 01/08).
|
|
421
|
+
*
|
|
422
|
+
* Merged UNDER what the payload carries: a base the reader authored on
|
|
423
|
+
* purpose still wins over the measurement.
|
|
424
|
+
*/
|
|
425
|
+
const recipe0 = entry.recipe;
|
|
426
|
+
const withLook = recipe0
|
|
427
|
+
? {
|
|
428
|
+
...recipe0,
|
|
429
|
+
...(look?.base && Object.keys(look.base).length > 0
|
|
430
|
+
? { base: { ...look.base, ...(recipe0.base ?? {}) } }
|
|
431
|
+
: {}),
|
|
432
|
+
...(look?.layers?.length
|
|
433
|
+
? {
|
|
434
|
+
layers: [
|
|
435
|
+
...look.layers,
|
|
436
|
+
...(recipe0.layers ?? []),
|
|
437
|
+
],
|
|
438
|
+
}
|
|
439
|
+
: {}),
|
|
440
|
+
/**
|
|
441
|
+
* THE ROOT TAG TRAVELS, so the validator can name the KIND.
|
|
442
|
+
*
|
|
443
|
+
* It falls back to `surface` when the payload declares none, and every
|
|
444
|
+
* one of 35 components got the surface checklist - including a Button,
|
|
445
|
+
* two fields, a control and two pills (not-expressed.md, dono, 01/08).
|
|
446
|
+
* The kind is arithmetic from the name and the root tag; the tag is
|
|
447
|
+
* here and the arithmetic lives on the side that owns the contract, so
|
|
448
|
+
* this sends the evidence rather than a second implementation of it.
|
|
449
|
+
*/
|
|
450
|
+
...(look?.rootTag ? { rootTag: look.rootTag } : {}),
|
|
451
|
+
}
|
|
452
|
+
: recipe0;
|
|
453
|
+
entry = { ...entry, recipe: withLook };
|
|
454
|
+
if (!sketch)
|
|
455
|
+
return entry;
|
|
456
|
+
const walk = (nodes) => {
|
|
457
|
+
if (!Array.isArray(nodes))
|
|
458
|
+
return nodes;
|
|
459
|
+
return nodes.map((raw) => {
|
|
460
|
+
const node = raw;
|
|
461
|
+
const at = typeof node.at === "number" ? node.at : null;
|
|
462
|
+
const classes = typeof node.classes === "string"
|
|
463
|
+
? node.classes
|
|
464
|
+
: at != null
|
|
465
|
+
? sketch[at]?.classes
|
|
466
|
+
: undefined;
|
|
467
|
+
return {
|
|
468
|
+
...node,
|
|
469
|
+
...(classes ? { classes } : {}),
|
|
470
|
+
...(node.children ? { children: walk(node.children) } : {}),
|
|
471
|
+
};
|
|
472
|
+
});
|
|
473
|
+
};
|
|
474
|
+
const recipe = entry.recipe;
|
|
475
|
+
if (!recipe)
|
|
476
|
+
return entry;
|
|
477
|
+
const anatomy = recipe.anatomy ??
|
|
478
|
+
recipe.preview?.parts;
|
|
479
|
+
if (!Array.isArray(anatomy))
|
|
480
|
+
return entry;
|
|
481
|
+
return {
|
|
482
|
+
...entry,
|
|
483
|
+
recipe: recipe.anatomy
|
|
484
|
+
? { ...recipe, anatomy: walk(recipe.anatomy) }
|
|
485
|
+
: {
|
|
486
|
+
...recipe,
|
|
487
|
+
preview: {
|
|
488
|
+
...recipe.preview,
|
|
489
|
+
parts: walk(recipe.preview.parts),
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
async function validateRecipes(root, entries) {
|
|
372
496
|
if (entries.length === 0)
|
|
373
497
|
return "Send at least one recipe: { recipes: [{ name, recipe }] }.";
|
|
374
|
-
const
|
|
498
|
+
const resolved = await resolveAtNodes(root, entries);
|
|
499
|
+
const answer = await askCatalogue("validate", { recipes: resolved });
|
|
375
500
|
if (!answer.ok)
|
|
376
501
|
return answer.because;
|
|
377
502
|
const body = answer.body;
|
|
@@ -662,10 +787,16 @@ async function callTool(root, name, args) {
|
|
|
662
787
|
return text(await addComponent(root, String(args.name ?? "")));
|
|
663
788
|
case "recipe_vocabulary":
|
|
664
789
|
return text(await recipeVocabulary());
|
|
665
|
-
case "validate_recipe":
|
|
666
|
-
|
|
790
|
+
case "validate_recipe": {
|
|
791
|
+
// Same resolution as the batch: an `at` the validator cannot see reads as
|
|
792
|
+
// an empty node and earns a false floor verdict (dono, 01/08).
|
|
793
|
+
const [only] = await resolveAtNodes(root, [
|
|
794
|
+
{ name: String(args.name ?? "this component"), recipe: args.recipe },
|
|
795
|
+
]);
|
|
796
|
+
return text(await validateRecipe(String(args.name ?? "this component"), only?.recipe ?? args.recipe));
|
|
797
|
+
}
|
|
667
798
|
case "validate_recipes":
|
|
668
|
-
return text(await validateRecipes(Array.isArray(args.recipes)
|
|
799
|
+
return text(await validateRecipes(root, Array.isArray(args.recipes)
|
|
669
800
|
? args.recipes
|
|
670
801
|
: []));
|
|
671
802
|
case "request_component": {
|
package/dist/doctor/sketch.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* same in an atomic library, a feature-foldered app, or a single flat directory.
|
|
20
20
|
*/
|
|
21
21
|
import { scanTags } from "./css-modules.js";
|
|
22
|
+
import { definitionSpan } from "./transcribe.js";
|
|
22
23
|
/**
|
|
23
24
|
* Past this a component is a page in disguise, and the gate already said no.
|
|
24
25
|
*
|
|
@@ -96,7 +97,22 @@ function staticCallClasses(body) {
|
|
|
96
97
|
* sitting directly between an open and its close - dynamic children are the caller's,
|
|
97
98
|
* and the slot form already says so.
|
|
98
99
|
*/
|
|
99
|
-
export function sketchOf(source) {
|
|
100
|
+
export function sketchOf(source, name) {
|
|
101
|
+
/**
|
|
102
|
+
* ONE COMPONENT'S MARKUP, when the file declares more than one.
|
|
103
|
+
*
|
|
104
|
+
* `RadioCard.tsx` declares `RadioCard` and `RadioCardGroup`, and this used to
|
|
105
|
+
* hand all sixteen nodes to whoever asked first - node 15 being
|
|
106
|
+
* `<RadioGroup className="flex flex-col gap-3">`, the OTHER component's root.
|
|
107
|
+
* So one component read a sketch with a stranger's markup in it and the other
|
|
108
|
+
* read an empty one (not-expressed.md, dono, 01/08).
|
|
109
|
+
*
|
|
110
|
+
* A name that is not in the file, or one whose body has no braces, reads the
|
|
111
|
+
* whole file - which is right for the 34 files that declare one component.
|
|
112
|
+
*/
|
|
113
|
+
const span = name ? definitionSpan(source, name) : null;
|
|
114
|
+
if (span)
|
|
115
|
+
source = source.slice(span.from, span.to);
|
|
100
116
|
const events = scanTags(source);
|
|
101
117
|
const out = [];
|
|
102
118
|
let depth = 0;
|
|
@@ -146,6 +146,34 @@ const SIZE_KEYWORD = {
|
|
|
146
146
|
px: "1px",
|
|
147
147
|
"0": "0",
|
|
148
148
|
};
|
|
149
|
+
/**
|
|
150
|
+
* `max-w-*` HAS ITS OWN RAMP - and it is not the spacing scale.
|
|
151
|
+
*
|
|
152
|
+
* Tailwind gives max-width a named ladder of its own (`xs` 20rem through `7xl`
|
|
153
|
+
* 80rem, plus `none` and `prose`), and the reader only knew the spacing scale
|
|
154
|
+
* and the keyword sizes - so `max-w-2xl` resolved to nothing and was reported as
|
|
155
|
+
* a class that "starts like a design decision and resolves to nothing"
|
|
156
|
+
* (not-expressed.md, dono, 01/08). It resolves to 42rem, and the class was fine;
|
|
157
|
+
* the table was short.
|
|
158
|
+
*
|
|
159
|
+
* Scoped to `max-w`/`max-h` on purpose: `w-2xl` is not a utility, and `p-2xl`
|
|
160
|
+
* means the spacing scale. A ramp applied on the wrong axis would invent a width.
|
|
161
|
+
*/
|
|
162
|
+
const MAX_WIDTH = {
|
|
163
|
+
none: "none",
|
|
164
|
+
xs: "20rem",
|
|
165
|
+
sm: "24rem",
|
|
166
|
+
md: "28rem",
|
|
167
|
+
lg: "32rem",
|
|
168
|
+
xl: "36rem",
|
|
169
|
+
"2xl": "42rem",
|
|
170
|
+
"3xl": "48rem",
|
|
171
|
+
"4xl": "56rem",
|
|
172
|
+
"5xl": "64rem",
|
|
173
|
+
"6xl": "72rem",
|
|
174
|
+
"7xl": "80rem",
|
|
175
|
+
prose: "65ch",
|
|
176
|
+
};
|
|
149
177
|
/** Axis-dependent: `w-screen` is 100vw and `h-screen` is 100vh. */
|
|
150
178
|
const SCREEN = {
|
|
151
179
|
w: "100vw",
|
|
@@ -635,6 +663,10 @@ export function readUtility(utility, declared) {
|
|
|
635
663
|
/^(w|h|min-w|min-h|max-w|max-h|size)$/.test(sizePrefix)) {
|
|
636
664
|
return { property: spaceProp, value: SIZE_KEYWORD[sizeRest] };
|
|
637
665
|
}
|
|
666
|
+
/** `max-w-2xl` - max-width's own named ramp, on its own axis only. */
|
|
667
|
+
if (MAX_WIDTH[sizeRest] && /^(max-w|max-h)$/.test(sizePrefix)) {
|
|
668
|
+
return { property: spaceProp, value: MAX_WIDTH[sizeRest] };
|
|
669
|
+
}
|
|
638
670
|
}
|
|
639
671
|
if (prefix === "rounded") {
|
|
640
672
|
/**
|
|
@@ -1103,18 +1135,234 @@ export function transcribe(classes, declared) {
|
|
|
1103
1135
|
* Absent or unfound, the old behaviour stands exactly - which keeps every
|
|
1104
1136
|
* single-component file reading byte-identically.
|
|
1105
1137
|
*/
|
|
1138
|
+
/**
|
|
1139
|
+
* AN EXPORT THAT IS AN ALIAS - follow it to the function it wraps.
|
|
1140
|
+
*
|
|
1141
|
+
* `export const Chat = Object.assign(React.forwardRef(ChatContainerComponent), …)`
|
|
1142
|
+
* declares the NAME the census knows and none of the markup: the styles live in
|
|
1143
|
+
* `ChatContainerComponent`, declared above. Measured on a real library, EIGHT of
|
|
1144
|
+
* 36 exports are this shape - seven `forwardRef(Fn)` and one
|
|
1145
|
+
* `Object.assign(forwardRef(Fn))` - and Chat arrived with an empty look, which
|
|
1146
|
+
* forced the one hand-read of the whole run (not-expressed.md, dono, 01/08).
|
|
1147
|
+
*
|
|
1148
|
+
* They used to work by accident: with no body brace to find, the reader fell
|
|
1149
|
+
* back to the first `return (` in the file, which for a one-component file is
|
|
1150
|
+
* the right one. Removing that fallback - correctly, because it let a helper
|
|
1151
|
+
* borrow a sibling's return - would have broken all eight. So the alias is
|
|
1152
|
+
* followed on purpose instead: the first capitalised identifier inside the call
|
|
1153
|
+
* is the function this name IS.
|
|
1154
|
+
*/
|
|
1155
|
+
function aliasTarget(source, name) {
|
|
1156
|
+
const decl = new RegExp(`(?:const|let|var)\\s+${name}\\s*(?::[^=]*)?=\\s*([^;\\n]{0,200})`).exec(source);
|
|
1157
|
+
if (!decl)
|
|
1158
|
+
return null;
|
|
1159
|
+
const rhs = decl[1];
|
|
1160
|
+
// Only a WRAPPER call aliases: `forwardRef`, `memo`, `Object.assign`,
|
|
1161
|
+
// `styled(...)`. A plain arrow is the component itself and has no target.
|
|
1162
|
+
if (!/^(?:React\.)?(?:forwardRef|memo|Object\.assign|styled)\b/.test(rhs))
|
|
1163
|
+
return null;
|
|
1164
|
+
for (const m of rhs.matchAll(/\b([A-Z][A-Za-z0-9_]*)\b/g)) {
|
|
1165
|
+
const candidate = m[1];
|
|
1166
|
+
if (/^(React|Object|Component)$/.test(candidate))
|
|
1167
|
+
continue;
|
|
1168
|
+
if (candidate === name)
|
|
1169
|
+
continue;
|
|
1170
|
+
// It has to be declared in this file, or it is a type argument or an import.
|
|
1171
|
+
if (new RegExp(`(?:function|const|let|var)\\s+${candidate}\\b`).test(source))
|
|
1172
|
+
return candidate;
|
|
1173
|
+
}
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1106
1176
|
function returnAt(source, name) {
|
|
1107
1177
|
if (name) {
|
|
1108
1178
|
const decl = new RegExp(`(?:function|const|let|var)\\s+${name}\\b|${name}\\s*[:=]\\s*(?:function|\\()`).exec(source);
|
|
1109
1179
|
if (decl) {
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1180
|
+
const own = ownReturn(source, decl.index);
|
|
1181
|
+
if (own !== -1)
|
|
1182
|
+
return own;
|
|
1183
|
+
/**
|
|
1184
|
+
* A BRACE-LESS ARROW returns its JSX straight after `=>`, so there is no
|
|
1185
|
+
* body and no `return` to find - `const X = () => (<div/>)`. That is a
|
|
1186
|
+
* real shape and it gets the concise position.
|
|
1187
|
+
*/
|
|
1188
|
+
const arrow = /=>\s*\(/.exec(source.slice(decl.index, decl.index + 400));
|
|
1189
|
+
if (arrow)
|
|
1190
|
+
return decl.index + arrow.index;
|
|
1191
|
+
/**
|
|
1192
|
+
* AN ALIAS POINTS AT THE FUNCTION THAT HAS THE MARKUP - follow it once.
|
|
1193
|
+
* `const Chat = Object.assign(forwardRef(ChatContainerComponent), …)`.
|
|
1194
|
+
*/
|
|
1195
|
+
const target = aliasTarget(source, name);
|
|
1196
|
+
if (target) {
|
|
1197
|
+
const viaAlias = ownReturn(source, source.search(new RegExp(`(?:function|const|let|var)\\s+${target}\\b`)));
|
|
1198
|
+
if (viaAlias !== -1)
|
|
1199
|
+
return viaAlias;
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* FOUND THE NAME AND IT RETURNS NO JSX IN PARENTHESES - so say nothing.
|
|
1203
|
+
*
|
|
1204
|
+
* The lexical fallback used to run here and it borrowed the NEXT
|
|
1205
|
+
* component's return: a `Helper` that returns a number handed back a
|
|
1206
|
+
* sibling's `<div className="p-2">`. That is the same defect the charts
|
|
1207
|
+
* had, one shape along - and my own spec caught it (dono, 01/08).
|
|
1208
|
+
*/
|
|
1209
|
+
return -1;
|
|
1114
1210
|
}
|
|
1115
1211
|
}
|
|
1116
1212
|
return source.search(/return\s*\(/);
|
|
1117
1213
|
}
|
|
1214
|
+
/**
|
|
1215
|
+
* THE COMPONENT'S OWN `return (`, not the first one after its name.
|
|
1216
|
+
*
|
|
1217
|
+
* Scoping by name was half the answer and the charts proved it: `LineChart` is
|
|
1218
|
+
* declared on line 99, and the first `return (` after that is on line 158 -
|
|
1219
|
+
* inside `CustomTooltip`, a closure NESTED IN ITS BODY. So all three charts
|
|
1220
|
+
* arrived with the tooltip's surface as their base: `bg-white/90 backdrop-blur-md
|
|
1221
|
+
* p-3 rounded-xl shadow-2xl` on what should be a card (not-expressed.md, dono,
|
|
1222
|
+
* 01/08).
|
|
1223
|
+
*
|
|
1224
|
+
* A function's own return sits at the body's OWN brace depth. So: walk from the
|
|
1225
|
+
* declaration, find the body's opening brace, then take the first `return (`
|
|
1226
|
+
* that appears while depth is exactly one - anything deeper belongs to a closure
|
|
1227
|
+
* this component happens to contain.
|
|
1228
|
+
*
|
|
1229
|
+
* Quote-aware, because a brace inside a string is not a brace. An arrow body
|
|
1230
|
+
* with no braces at all (`const X = () => (<div/>)`) has no depth to track and
|
|
1231
|
+
* falls through to the caller's lexical search, which is right for it.
|
|
1232
|
+
*/
|
|
1233
|
+
/**
|
|
1234
|
+
* WHERE ONE COMPONENT'S CODE STARTS AND STOPS, by name.
|
|
1235
|
+
*
|
|
1236
|
+
* A file that declares two components used to hand both bodies to whatever read
|
|
1237
|
+
* it first: `RadioCard.tsx` declares `RadioCard` and `RadioCardGroup`, and the
|
|
1238
|
+
* sketch attributed all sixteen nodes to `RadioCard` - node 15 being
|
|
1239
|
+
* `<RadioGroup className="flex flex-col gap-3">`, which is the OTHER component's
|
|
1240
|
+
* root (not-expressed.md, dono, 01/08).
|
|
1241
|
+
*
|
|
1242
|
+
* Returns null when the name is not in the file or has no braced body, and the
|
|
1243
|
+
* caller then reads the whole file - which is right for the 34 files that
|
|
1244
|
+
* declare one component.
|
|
1245
|
+
*/
|
|
1246
|
+
export function definitionSpan(source, name) {
|
|
1247
|
+
const decl = new RegExp(`(?:function|const|let|var)\\s+${name}\\b|${name}\\s*[:=]\\s*(?:function|\\()`).exec(source);
|
|
1248
|
+
if (!decl)
|
|
1249
|
+
return null;
|
|
1250
|
+
const brace = bodyBrace(source, decl.index);
|
|
1251
|
+
if (brace === -1) {
|
|
1252
|
+
// The export is an alias; the body belongs to what it wraps.
|
|
1253
|
+
const target = aliasTarget(source, name);
|
|
1254
|
+
if (target)
|
|
1255
|
+
return definitionSpan(source, target);
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
const end = matchingBrace(source, brace);
|
|
1259
|
+
return { from: decl.index, to: end === -1 ? source.length : end + 1 };
|
|
1260
|
+
}
|
|
1261
|
+
/** The `{` that opens a function body declared at `declAt`, or -1. */
|
|
1262
|
+
function bodyBrace(source, declAt) {
|
|
1263
|
+
/**
|
|
1264
|
+
* A BODY INLINE INSIDE A WRAPPER: `forwardRef<A, B>((props, ref) => { … })`.
|
|
1265
|
+
*
|
|
1266
|
+
* Toggle is written this way and the plain walk below cannot find it - the
|
|
1267
|
+
* wrapper's own parenthesis never closes before the body starts, so it looked
|
|
1268
|
+
* for a `{` that is not there and the component read empty. There is no named
|
|
1269
|
+
* function to alias to either: the body IS the argument.
|
|
1270
|
+
*/
|
|
1271
|
+
const head = source.slice(declAt, declAt + 400);
|
|
1272
|
+
const wrapper = /=\s*(?:React\.)?(?:forwardRef|memo|styled\([^)]*\))\s*(?:<[^>(]*>)?\s*\(/.exec(head);
|
|
1273
|
+
if (wrapper) {
|
|
1274
|
+
const arrow = /(?:=>|function\s*[A-Za-z0-9_]*\s*\([^)]*\))\s*\{/.exec(source.slice(declAt + wrapper.index));
|
|
1275
|
+
if (arrow) {
|
|
1276
|
+
const at = declAt + wrapper.index + arrow.index + arrow[0].length - 1;
|
|
1277
|
+
if (source[at] === "{")
|
|
1278
|
+
return at;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
let i = declAt;
|
|
1282
|
+
let paren = 0;
|
|
1283
|
+
let opened = false;
|
|
1284
|
+
for (; i < source.length; i++) {
|
|
1285
|
+
const ch = source[i];
|
|
1286
|
+
if (ch === "(") {
|
|
1287
|
+
paren += 1;
|
|
1288
|
+
opened = true;
|
|
1289
|
+
}
|
|
1290
|
+
else if (ch === ")") {
|
|
1291
|
+
paren -= 1;
|
|
1292
|
+
if (opened && paren <= 0) {
|
|
1293
|
+
i += 1;
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
else if (ch === "{" && !opened) {
|
|
1298
|
+
return -1; // `const X = { … }` is an object, not a body
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
return source.indexOf("{", i);
|
|
1302
|
+
}
|
|
1303
|
+
/** The `}` that closes the brace at `open`, quote-aware. */
|
|
1304
|
+
function matchingBrace(source, open) {
|
|
1305
|
+
let depth = 0;
|
|
1306
|
+
let quote = null;
|
|
1307
|
+
for (let j = open; j < source.length; j++) {
|
|
1308
|
+
const ch = source[j];
|
|
1309
|
+
if (quote) {
|
|
1310
|
+
if (ch === "\\")
|
|
1311
|
+
j += 1;
|
|
1312
|
+
else if (ch === quote)
|
|
1313
|
+
quote = null;
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
1317
|
+
quote = ch;
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
if (ch === "{")
|
|
1321
|
+
depth += 1;
|
|
1322
|
+
else if (ch === "}") {
|
|
1323
|
+
depth -= 1;
|
|
1324
|
+
if (depth === 0)
|
|
1325
|
+
return j;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
return -1;
|
|
1329
|
+
}
|
|
1330
|
+
function ownReturn(source, declAt) {
|
|
1331
|
+
const brace = bodyBrace(source, declAt);
|
|
1332
|
+
if (brace === -1)
|
|
1333
|
+
return -1;
|
|
1334
|
+
let depth = 0;
|
|
1335
|
+
let quote = null;
|
|
1336
|
+
for (let j = brace; j < source.length; j++) {
|
|
1337
|
+
const ch = source[j];
|
|
1338
|
+
if (quote) {
|
|
1339
|
+
if (ch === "\\")
|
|
1340
|
+
j += 1;
|
|
1341
|
+
else if (ch === quote)
|
|
1342
|
+
quote = null;
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
1346
|
+
quote = ch;
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
if (ch === "{")
|
|
1350
|
+
depth += 1;
|
|
1351
|
+
else if (ch === "}") {
|
|
1352
|
+
depth -= 1;
|
|
1353
|
+
// Out of the body without finding one: this component returns no JSX in
|
|
1354
|
+
// parentheses, and guessing from a sibling would be worse than nothing.
|
|
1355
|
+
if (depth === 0)
|
|
1356
|
+
return -1;
|
|
1357
|
+
}
|
|
1358
|
+
else if (depth === 1 &&
|
|
1359
|
+
ch === "r" &&
|
|
1360
|
+
/^return\s*\(/.test(source.slice(j))) {
|
|
1361
|
+
return j;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return -1;
|
|
1365
|
+
}
|
|
1118
1366
|
export function rootTag(source, name) {
|
|
1119
1367
|
const ret = returnAt(source, name);
|
|
1120
1368
|
if (ret === -1)
|
package/dist/skill-import.js
CHANGED
|
@@ -614,10 +614,21 @@ depth appears anyway.
|
|
|
614
614
|
"Modal": {
|
|
615
615
|
"root": "BaseDialog.Root",
|
|
616
616
|
"anatomy": [ … ]
|
|
617
|
+
},
|
|
618
|
+
"MetricCard": {
|
|
619
|
+
"root": { "name": "Card", "at": 1 },
|
|
620
|
+
"anatomy": [ … ]
|
|
617
621
|
}
|
|
618
622
|
}
|
|
619
623
|
\`\`\`
|
|
620
624
|
|
|
625
|
+
**The root takes what the PARENT paints on it.** When the returned root carries an override at
|
|
626
|
+
the call site - \`MetricCard\` returns \`<Card className="flex-1 dark:bg-darkgray-300">\` - send the
|
|
627
|
+
object form and point \`at\` the sketch node: \`{ "name": "Card", "at": 1 }\`. The string form still
|
|
628
|
+
works and means "no override". Sending the root as an anatomy node instead would make it a leaf
|
|
629
|
+
and orphan the content inside it, which is why it is a field on the root rather than a node
|
|
630
|
+
(not-expressed.md, dono, 01/08).
|
|
631
|
+
|
|
621
632
|
**Say what the component RETURNS when it is not a plain tag.** Measured on a real library
|
|
622
633
|
(dono, 01/08): 14 of 23 components return one of their own - \`ArticleCard\` returns a \`<Card>\`,
|
|
623
634
|
\`Button\` and \`Text\` return a \`<Component>\` - and 17 of 23 have no style of their own at all,
|
|
@@ -711,6 +722,21 @@ of. It previews as a block carrying its name, and it becomes a **rule** - \`Arti
|
|
|
711
722
|
That edge is a **fact, not a habit**: you saw it in the definition, so it is true once and for
|
|
712
723
|
all, and it arrives active without waiting for a third sighting.
|
|
713
724
|
|
|
725
|
+
**Send what the PARENT paints on it.** A frontier node takes \`at\` (or \`classes\`) like any other,
|
|
726
|
+
and it means something different there: not the child's look, which has a recipe of its own, but
|
|
727
|
+
the override the parent applies where it composes the child. Measured on a real library, four of
|
|
728
|
+
these were being lost:
|
|
729
|
+
|
|
730
|
+
\`\`\`
|
|
731
|
+
Paginate <Button className="min-w-[120px] dark:bg-ocean-800 …"> a 120px floor
|
|
732
|
+
MetricCard <Card className="flex-1 dark:bg-darkgray-300"> overrides Card's dark
|
|
733
|
+
ArticleCard <Tag className="w-fit">
|
|
734
|
+
Message <ReactMarkdown className="text-royal-blue-500 underline …">
|
|
735
|
+
\`\`\`
|
|
736
|
+
|
|
737
|
+
The MetricCard one is the sharpest: losing that override means the preview draws the wrong dark.
|
|
738
|
+
Same for an \`external\` node - a library's component still wears what their code puts on it.
|
|
739
|
+
|
|
714
740
|
Three things that are NOT edges, and sending them as such would be wrong:
|
|
715
741
|
|
|
716
742
|
- \`motion.div\`, \`Dialog.Root\`, \`Radio.Item\` - a library's namespace, not a component of theirs
|
package/package.json
CHANGED