synthesisui 0.16.101 → 0.16.103
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-from-sketch.js +492 -0
- package/dist/anatomy-read.js +27 -3
- package/dist/commands/import.js +106 -10
- package/dist/doctor/imports.js +73 -0
- package/dist/doctor/sketch.js +64 -0
- package/dist/frontier-kind.js +200 -0
- package/dist/skill-import.js +25 -23
- package/package.json +1 -1
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE SKETCH BECOMES THE ANATOMY, WITHOUT ASKING ANYBODY.
|
|
3
|
+
*
|
|
4
|
+
* Measured on the owner's own census (dono, 02/08):
|
|
5
|
+
*
|
|
6
|
+
* medido 36/36 (100%) sketch with tag + depth + classes, 259 nodes
|
|
7
|
+
* carrying a class string
|
|
8
|
+
* convertido 0/36 ( 0%) `preview.parts` empty on every single recipe
|
|
9
|
+
*
|
|
10
|
+
* The tree only ever existed when the SKILL authored `reading.components[X].anatomy`
|
|
11
|
+
* by hand, one component at a time. A run that did not do that - which is every
|
|
12
|
+
* automated run - produced 35 recipes holding nothing but the root's own
|
|
13
|
+
* declarations. For `Modal` that is literally nothing: its root is a
|
|
14
|
+
* `<BaseDialog.Root>` with no classes, and its whole appearance (backdrop, popup,
|
|
15
|
+
* close) sits three levels down, measured, in the census, and thrown away.
|
|
16
|
+
*
|
|
17
|
+
* So this derives the anatomy from the sketch. Every input is a FACT the census
|
|
18
|
+
* already holds - the tag, the nesting depth, the class string, the package the
|
|
19
|
+
* tag was imported from - and nothing here is inferred from a component's name.
|
|
20
|
+
*
|
|
21
|
+
* IT EMITS THE SKILL'S OWN INPUT SHAPE (`AnatomyRead[]`), on purpose: `resolveAnatomy`
|
|
22
|
+
* then does the part naming, the transcription, the crosswalk, the frontier
|
|
23
|
+
* bookkeeping and the budget exactly as it does for an authored read. One new
|
|
24
|
+
* producer, no new consumer, and an authored read still wins where one exists.
|
|
25
|
+
*
|
|
26
|
+
* Classes travel by `at` (the sketch INDEX) rather than being copied out: a
|
|
27
|
+
* retyped ten-class string is where a `hover:` goes missing, and six components
|
|
28
|
+
* lost their states to exactly that (audit, dono, 01/08).
|
|
29
|
+
*/
|
|
30
|
+
import { frontierOf, rendersNothing, } from "./frontier-kind.js";
|
|
31
|
+
/** An svg's insides are geometry, not anatomy: the glyph is the node. */
|
|
32
|
+
const SVG_INTERNALS = new Set([
|
|
33
|
+
"path",
|
|
34
|
+
"circle",
|
|
35
|
+
"rect",
|
|
36
|
+
"line",
|
|
37
|
+
"polyline",
|
|
38
|
+
"polygon",
|
|
39
|
+
"ellipse",
|
|
40
|
+
"g",
|
|
41
|
+
"defs",
|
|
42
|
+
"lineargradient",
|
|
43
|
+
"radialgradient",
|
|
44
|
+
"stop",
|
|
45
|
+
"clippath",
|
|
46
|
+
"mask",
|
|
47
|
+
"use",
|
|
48
|
+
"symbol",
|
|
49
|
+
"text",
|
|
50
|
+
"tspan",
|
|
51
|
+
"filter",
|
|
52
|
+
"feblend",
|
|
53
|
+
"animate",
|
|
54
|
+
]);
|
|
55
|
+
/** Not a visual node at all: a script or style island, and `<head>` furniture. */
|
|
56
|
+
const NOT_VISUAL = new Set(["style", "script", "noscript", "template", "head"]);
|
|
57
|
+
/**
|
|
58
|
+
* The HTML tag → form table, and the name a tag earns.
|
|
59
|
+
*
|
|
60
|
+
* Both come from the element's own content model, which is why they can be a
|
|
61
|
+
* table: an `<h3>` is a heading in any library, and a `<td>` is a cell. The
|
|
62
|
+
* name is what the part is CALLED in the stylesheet, so it uses the vocabulary
|
|
63
|
+
* their own markup used rather than a word we picked.
|
|
64
|
+
*/
|
|
65
|
+
const BY_TAG = {
|
|
66
|
+
img: { as: "image", name: "image" },
|
|
67
|
+
picture: { as: "image", name: "image" },
|
|
68
|
+
video: { as: "image", name: "media" },
|
|
69
|
+
canvas: { as: "image", name: "canvas" },
|
|
70
|
+
svg: { as: "icon", name: "icon" },
|
|
71
|
+
h1: { as: "heading", name: "title" },
|
|
72
|
+
h2: { as: "heading", name: "title" },
|
|
73
|
+
h3: { as: "heading", name: "title" },
|
|
74
|
+
h4: { as: "heading", name: "title" },
|
|
75
|
+
h5: { as: "heading", name: "title" },
|
|
76
|
+
h6: { as: "heading", name: "title" },
|
|
77
|
+
p: { as: "text", name: "text" },
|
|
78
|
+
small: { as: "text", name: "caption" },
|
|
79
|
+
strong: { as: "text", name: "text" },
|
|
80
|
+
em: { as: "text", name: "text" },
|
|
81
|
+
code: { as: "text", name: "code" },
|
|
82
|
+
pre: { as: "text", name: "code" },
|
|
83
|
+
blockquote: { as: "text", name: "quote" },
|
|
84
|
+
label: { as: "text", name: "label" },
|
|
85
|
+
legend: { as: "text", name: "legend" },
|
|
86
|
+
button: { as: "button", name: "action" },
|
|
87
|
+
a: { as: "button", name: "link" },
|
|
88
|
+
summary: { as: "button", name: "summary" },
|
|
89
|
+
input: { as: "field", name: "input" },
|
|
90
|
+
textarea: { as: "field", name: "input" },
|
|
91
|
+
select: { as: "field", name: "input" },
|
|
92
|
+
option: { as: "text", name: "option" },
|
|
93
|
+
table: { as: "stack", name: "table" },
|
|
94
|
+
thead: { as: "stack", name: "head" },
|
|
95
|
+
tbody: { as: "stack", name: "body" },
|
|
96
|
+
tfoot: { as: "stack", name: "foot" },
|
|
97
|
+
tr: { as: "row", name: "row" },
|
|
98
|
+
td: { as: "text", name: "cell" },
|
|
99
|
+
th: { as: "text", name: "column" },
|
|
100
|
+
caption: { as: "text", name: "caption" },
|
|
101
|
+
ul: { as: "stack", name: "list" },
|
|
102
|
+
ol: { as: "stack", name: "list" },
|
|
103
|
+
li: { as: "text", name: "item" },
|
|
104
|
+
dl: { as: "stack", name: "list" },
|
|
105
|
+
dt: { as: "text", name: "term" },
|
|
106
|
+
dd: { as: "text", name: "definition" },
|
|
107
|
+
form: { as: "stack", name: "form" },
|
|
108
|
+
fieldset: { as: "stack", name: "group" },
|
|
109
|
+
header: { as: "stack", name: "header" },
|
|
110
|
+
footer: { as: "stack", name: "footer" },
|
|
111
|
+
main: { as: "stack", name: "body" },
|
|
112
|
+
nav: { as: "row", name: "nav" },
|
|
113
|
+
aside: { as: "stack", name: "aside" },
|
|
114
|
+
section: { as: "stack" },
|
|
115
|
+
article: { as: "stack" },
|
|
116
|
+
details: { as: "stack", name: "details" },
|
|
117
|
+
figure: { as: "stack", name: "figure" },
|
|
118
|
+
figcaption: { as: "text", name: "caption" },
|
|
119
|
+
hr: { as: "icon", name: "divider" },
|
|
120
|
+
span: { as: "text", name: "text" },
|
|
121
|
+
div: { as: "stack" },
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* WHAT THE CALLER PUTS IN A SLOT, from the parent's content model.
|
|
125
|
+
*
|
|
126
|
+
* `expects` is meant to be their words, and a derived read has none - but an
|
|
127
|
+
* HTML content model is not a guess: the children of a `<tr>` ARE cells and the
|
|
128
|
+
* children of a `<tbody>` ARE rows. Where the model says nothing general, this
|
|
129
|
+
* says nothing rather than inventing a description.
|
|
130
|
+
*/
|
|
131
|
+
const EXPECTS = {
|
|
132
|
+
tr: "the cells",
|
|
133
|
+
thead: "the rows",
|
|
134
|
+
tbody: "the rows",
|
|
135
|
+
table: "the rows",
|
|
136
|
+
ul: "the items",
|
|
137
|
+
ol: "the items",
|
|
138
|
+
dl: "the terms",
|
|
139
|
+
select: "the options",
|
|
140
|
+
optgroup: "the options",
|
|
141
|
+
nav: "the links",
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Tags with no direction of their own: the client's classes decide.
|
|
145
|
+
*
|
|
146
|
+
* A `<tr>` is a row and a `<tbody>` is a stack whatever anybody writes on them -
|
|
147
|
+
* that is the table model, not a style. A `<div>` is neither until its own
|
|
148
|
+
* `flex-col` says so, and reading that from the class list is the whole reason
|
|
149
|
+
* `arranged` outranks any shape we would infer.
|
|
150
|
+
*/
|
|
151
|
+
const NEUTRAL = new Set([
|
|
152
|
+
"div",
|
|
153
|
+
"section",
|
|
154
|
+
"article",
|
|
155
|
+
"form",
|
|
156
|
+
"fieldset",
|
|
157
|
+
"header",
|
|
158
|
+
"footer",
|
|
159
|
+
"main",
|
|
160
|
+
"aside",
|
|
161
|
+
"figure",
|
|
162
|
+
"nav",
|
|
163
|
+
"details",
|
|
164
|
+
]);
|
|
165
|
+
/** `flex-col`, `grid` and `block` arrange DOWN; a bare `flex` arranges ACROSS. */
|
|
166
|
+
function arrangement(classes) {
|
|
167
|
+
const list = ` ${classes ?? ""} `;
|
|
168
|
+
if (/\s(flex-col|flex-col-reverse|grid|block|table)\s/.test(list))
|
|
169
|
+
return "stack";
|
|
170
|
+
if (/\s(flex|inline-flex|flex-row|flex-row-reverse)\s/.test(list))
|
|
171
|
+
return "row";
|
|
172
|
+
return "stack";
|
|
173
|
+
}
|
|
174
|
+
/** `ChevronDown` → `chevron-down`; `BaseDialog.Popup` → `popup`. */
|
|
175
|
+
function nameOfTag(tag) {
|
|
176
|
+
const member = tag.includes(".") ? (tag.split(".").pop() ?? tag) : tag;
|
|
177
|
+
return member
|
|
178
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
179
|
+
.toLowerCase()
|
|
180
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
181
|
+
.replace(/^-|-$/g, "");
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A HEADLESS PRIMITIVE'S OWN MEMBER NAME, when it says what the thing is.
|
|
185
|
+
*
|
|
186
|
+
* `Root`, `Trigger`, `Popup`, `Panel`, `Backdrop`, `Item`, `Indicator`, `Thumb`
|
|
187
|
+
* is the shared vocabulary of every headless library, and it is the client's own
|
|
188
|
+
* word for that element - so it makes a better part name than anything we would
|
|
189
|
+
* pick, and a better form than "div with children".
|
|
190
|
+
*/
|
|
191
|
+
const MEMBER_FORM = {
|
|
192
|
+
trigger: "button",
|
|
193
|
+
close: "button",
|
|
194
|
+
submit: "button",
|
|
195
|
+
arrow: "icon",
|
|
196
|
+
indicator: "icon",
|
|
197
|
+
thumb: "icon",
|
|
198
|
+
icon: "icon",
|
|
199
|
+
separator: "icon",
|
|
200
|
+
label: "text",
|
|
201
|
+
value: "text",
|
|
202
|
+
description: "text",
|
|
203
|
+
title: "heading",
|
|
204
|
+
input: "field",
|
|
205
|
+
control: "field",
|
|
206
|
+
text: "text",
|
|
207
|
+
caption: "text",
|
|
208
|
+
option: "text",
|
|
209
|
+
image: "image",
|
|
210
|
+
avatar: "image",
|
|
211
|
+
track: "stack",
|
|
212
|
+
range: "icon",
|
|
213
|
+
bar: "icon",
|
|
214
|
+
handle: "icon",
|
|
215
|
+
checkbox: "icon",
|
|
216
|
+
radio: "icon",
|
|
217
|
+
tab: "button",
|
|
218
|
+
backdrop: "stack",
|
|
219
|
+
overlay: "stack",
|
|
220
|
+
popup: "stack",
|
|
221
|
+
panel: "stack",
|
|
222
|
+
content: "stack",
|
|
223
|
+
list: "stack",
|
|
224
|
+
viewport: "stack",
|
|
225
|
+
group: "stack",
|
|
226
|
+
item: "row",
|
|
227
|
+
row: "row",
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* Derive the anatomy of ONE component from its sketch.
|
|
231
|
+
*
|
|
232
|
+
* `defines` is the census crosswalk (their name → the slug it reaches), used to
|
|
233
|
+
* tell their own library imported through an alias from a third party the table
|
|
234
|
+
* has never seen. See `frontierOf`.
|
|
235
|
+
*
|
|
236
|
+
* Node 0 is skipped: it is the element the component RETURNS, its classes are
|
|
237
|
+
* already the recipe's `base`, and emitting it again would draw the component
|
|
238
|
+
* inside a copy of itself.
|
|
239
|
+
*/
|
|
240
|
+
export function anatomyFromSketch(sketch, defines,
|
|
241
|
+
/**
|
|
242
|
+
* Their manifest: a package name → the range they pinned. This is what tells
|
|
243
|
+
* `@frontend-hub/ui: workspace:*` (their own library, one folder over) apart
|
|
244
|
+
* from `recharts: ^2.12.0`, since both are spelled the same way. See
|
|
245
|
+
* `frontierOf`.
|
|
246
|
+
*/
|
|
247
|
+
versionOf) {
|
|
248
|
+
const notes = [];
|
|
249
|
+
const libraries = [];
|
|
250
|
+
if (!Array.isArray(sketch) || sketch.length === 0)
|
|
251
|
+
return { read: [], notes, libraries };
|
|
252
|
+
/** Children of each index, from the depth column. */
|
|
253
|
+
const kids = sketch.map(() => []);
|
|
254
|
+
const roots = [];
|
|
255
|
+
const stack = [];
|
|
256
|
+
for (let i = 0; i < sketch.length; i++) {
|
|
257
|
+
const depth = Number(sketch[i]?.depth ?? 0);
|
|
258
|
+
while (stack.length > depth)
|
|
259
|
+
stack.pop();
|
|
260
|
+
const parent = stack.length > 0 ? stack[stack.length - 1] : -1;
|
|
261
|
+
if (parent >= 0)
|
|
262
|
+
kids[parent].push(i);
|
|
263
|
+
else
|
|
264
|
+
roots.push(i);
|
|
265
|
+
stack.push(i);
|
|
266
|
+
}
|
|
267
|
+
const build = (index, depth) => {
|
|
268
|
+
const node = sketch[index];
|
|
269
|
+
if (!node)
|
|
270
|
+
return [];
|
|
271
|
+
const tag = String(node.tag ?? "");
|
|
272
|
+
const lower = tag.toLowerCase();
|
|
273
|
+
const classes = node.classes?.trim() || undefined;
|
|
274
|
+
/**
|
|
275
|
+
* THE CHILDREN THAT DRAW SOMETHING. An `<svg>` full of `<path>`s is a glyph,
|
|
276
|
+
* not a stack of two shapes, and counting its geometry as content turned every
|
|
277
|
+
* inline icon into a container - their Tooltip's arrow previewed as a box
|
|
278
|
+
* holding a box (probe, 02/08).
|
|
279
|
+
*/
|
|
280
|
+
const children = (kids[index] ?? []).filter((child) => {
|
|
281
|
+
const tag = String(sketch[child]?.tag ?? "").toLowerCase();
|
|
282
|
+
return !SVG_INTERNALS.has(tag) && !NOT_VISUAL.has(tag);
|
|
283
|
+
});
|
|
284
|
+
if (NOT_VISUAL.has(lower)) {
|
|
285
|
+
notes.push(`\`<${tag}>\` is a ${lower} island - its rules are real and this contract has no form that holds them`);
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
// An svg's insides are geometry: the glyph is the node, and it stops here.
|
|
289
|
+
if (SVG_INTERNALS.has(lower))
|
|
290
|
+
return [];
|
|
291
|
+
const capitalised = /^[A-Z]/.test(tag);
|
|
292
|
+
const kind = capitalised
|
|
293
|
+
? frontierOf(tag, node.from, defines, versionOf)
|
|
294
|
+
: "own";
|
|
295
|
+
// The dependency, whatever we decide to draw - see `DerivedAnatomy.libraries`.
|
|
296
|
+
if (node.from &&
|
|
297
|
+
kind !== "own" &&
|
|
298
|
+
!libraries.some((l) => l.from === node.from))
|
|
299
|
+
libraries.push({ from: node.from, kind });
|
|
300
|
+
/**
|
|
301
|
+
* DECLARED IN THIS FILE, so there is no package and no recipe to point at.
|
|
302
|
+
*
|
|
303
|
+
* 20 of 181 capitalised nodes in the owner's library are this: `ToolbarButton`,
|
|
304
|
+
* `Shimmer`, `CustomLegend`. They are the client's own markup wearing the
|
|
305
|
+
* client's own classes, so they are DRAWN - the opposite of a frontier. The
|
|
306
|
+
* crosswalk would happily resolve `ToolbarButton` to `button` and turn a local
|
|
307
|
+
* helper into a reference to a component it is not.
|
|
308
|
+
*/
|
|
309
|
+
const local = capitalised && !node.from;
|
|
310
|
+
/**
|
|
311
|
+
* A WRAPPER THAT RENDERS NOTHING passes its children up in its own place: a
|
|
312
|
+
* portal teleports and a provider is context. Keeping it would draw a box
|
|
313
|
+
* that does not exist in the client's output.
|
|
314
|
+
*/
|
|
315
|
+
if ((kind === "motion" || kind === "headless") &&
|
|
316
|
+
rendersNothing(tag, classes)) {
|
|
317
|
+
return children.flatMap((child) => build(child, depth));
|
|
318
|
+
}
|
|
319
|
+
// `<AnimatePresence>` and friends: no element of their own.
|
|
320
|
+
if (kind === "motion" && !classes && children.length > 0) {
|
|
321
|
+
return children.flatMap((child) => build(child, depth));
|
|
322
|
+
}
|
|
323
|
+
const descend = () => children.flatMap((child) => build(child, depth + 1));
|
|
324
|
+
/**
|
|
325
|
+
* THEIR OWN COMPONENT, and a library that draws itself: a reference, and it
|
|
326
|
+
* stops. What their code nests INSIDE it is that component's content, which
|
|
327
|
+
* the frontier has no field for - counted rather than silently dropped.
|
|
328
|
+
*/
|
|
329
|
+
if (kind === "own" && capitalised && !local) {
|
|
330
|
+
return [
|
|
331
|
+
{
|
|
332
|
+
as: "component",
|
|
333
|
+
ref: tag,
|
|
334
|
+
...(classes ? { at: index } : {}),
|
|
335
|
+
...(children.length > 0 ? { children: descend() } : {}),
|
|
336
|
+
},
|
|
337
|
+
];
|
|
338
|
+
}
|
|
339
|
+
if (kind === "opaque") {
|
|
340
|
+
/**
|
|
341
|
+
* An opaque library draws its own insides, so anything nested under it in
|
|
342
|
+
* their code is a CHILD they passed to it - a legend, an axis, a fallback -
|
|
343
|
+
* and it is theirs. What we do not claim is the drawing.
|
|
344
|
+
*/
|
|
345
|
+
return [
|
|
346
|
+
{
|
|
347
|
+
as: "external",
|
|
348
|
+
from: String(node.from ?? tag),
|
|
349
|
+
...(classes ? { at: index } : {}),
|
|
350
|
+
...(children.length > 0 ? { children: descend() } : {}),
|
|
351
|
+
},
|
|
352
|
+
];
|
|
353
|
+
}
|
|
354
|
+
if (kind === "icon") {
|
|
355
|
+
return [{ as: "icon", name: nameOfTag(tag), at: index }];
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* EVERYTHING ELSE IS DRAWN: plain HTML, a headless primitive, and a
|
|
359
|
+
* component the file declares itself. All three wear the client's classes
|
|
360
|
+
* and all three are ours to reproduce.
|
|
361
|
+
*/
|
|
362
|
+
const member = tag.includes(".")
|
|
363
|
+
? (tag.split(".").pop() ?? "").toLowerCase()
|
|
364
|
+
: "";
|
|
365
|
+
const byTag = BY_TAG[lower];
|
|
366
|
+
const hasKids = children.length > 0;
|
|
367
|
+
let as;
|
|
368
|
+
let name;
|
|
369
|
+
if (capitalised) {
|
|
370
|
+
// A primitive's member name is the client's own word for the element.
|
|
371
|
+
name = nameOfTag(tag);
|
|
372
|
+
/**
|
|
373
|
+
* THE LAST WORD OF THE MEMBER IS THE NOUN. `ItemText` is text, `ItemIndicator`
|
|
374
|
+
* is an indicator, `ScrollUpArrow` is an arrow - every headless library
|
|
375
|
+
* compounds its member names the same way, and reading only the whole string
|
|
376
|
+
* made a Select's item label preview as a glyph (probe, 02/08).
|
|
377
|
+
*/
|
|
378
|
+
const noun = name.split("-").pop() ?? name;
|
|
379
|
+
const form = MEMBER_FORM[member] ?? MEMBER_FORM[name] ?? MEMBER_FORM[noun];
|
|
380
|
+
if (form === "row" || form === "stack")
|
|
381
|
+
as = arrangement(classes);
|
|
382
|
+
else if (form)
|
|
383
|
+
as = form;
|
|
384
|
+
else
|
|
385
|
+
as = hasKids ? arrangement(classes) : node.text ? "text" : "icon";
|
|
386
|
+
}
|
|
387
|
+
else if (byTag) {
|
|
388
|
+
name = byTag.name;
|
|
389
|
+
if (NEUTRAL.has(lower)) {
|
|
390
|
+
/**
|
|
391
|
+
* A leaf box with no content of its own is a drawn SHAPE - a dot, a
|
|
392
|
+
* track, a divider - and `icon` is the form for "a bare shape with no
|
|
393
|
+
* text". An empty `stack` would preview as nothing at all.
|
|
394
|
+
*/
|
|
395
|
+
as =
|
|
396
|
+
hasKids || node.slot
|
|
397
|
+
? arrangement(classes)
|
|
398
|
+
: node.text
|
|
399
|
+
? "text"
|
|
400
|
+
: "icon";
|
|
401
|
+
}
|
|
402
|
+
else
|
|
403
|
+
as = byTag.as;
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
as = hasKids ? arrangement(classes) : "icon";
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* ANYTHING HOLDING ELEMENTS ARRANGES THEM - the contract gives children to
|
|
410
|
+
* `row` and `stack` only, and a leaf that has some gets them hoisted out to
|
|
411
|
+
* become its siblings, which destroys the nesting the client wrote.
|
|
412
|
+
*
|
|
413
|
+
* This is the same law the usage generator applies one level up: a trigger
|
|
414
|
+
* that wraps a heading is not a `<button>`, because a button may not contain
|
|
415
|
+
* one. The part is still called `trigger`, which is what carries the role.
|
|
416
|
+
*/
|
|
417
|
+
if (hasKids && as !== "row" && as !== "stack")
|
|
418
|
+
as = arrangement(classes);
|
|
419
|
+
// A plain structural box earns a name only when it carries decisions - and
|
|
420
|
+
// it must earn one, because the name is what the class string travels on.
|
|
421
|
+
if (!name && classes)
|
|
422
|
+
name = "box";
|
|
423
|
+
/**
|
|
424
|
+
* A NODE WHOSE CONTENT IS THE CALLER'S is a slot, and it keeps its own name
|
|
425
|
+
* and classes - the border and the hover on a `<tr>{children}</tr>` are real
|
|
426
|
+
* and belong on that element.
|
|
427
|
+
*/
|
|
428
|
+
if (node.slot && !hasKids) {
|
|
429
|
+
const only = EXPECTS[lower];
|
|
430
|
+
return [
|
|
431
|
+
{
|
|
432
|
+
as: "slot",
|
|
433
|
+
...(name ? { name } : {}),
|
|
434
|
+
at: index,
|
|
435
|
+
...(only ? { expects: only } : {}),
|
|
436
|
+
},
|
|
437
|
+
];
|
|
438
|
+
}
|
|
439
|
+
const built = {
|
|
440
|
+
as,
|
|
441
|
+
...(name ? { name } : {}),
|
|
442
|
+
...(classes ? { at: index } : {}),
|
|
443
|
+
...(node.text ? { text: node.text } : {}),
|
|
444
|
+
};
|
|
445
|
+
const inside = hasKids ? descend() : [];
|
|
446
|
+
/**
|
|
447
|
+
* MIXED CONTENT IS BOTH: their markup AND the caller's. A panel that renders
|
|
448
|
+
* a header and then `{children}` has two kinds of content and dropping either
|
|
449
|
+
* one is a hole somebody has to explain.
|
|
450
|
+
*/
|
|
451
|
+
const expects = EXPECTS[lower];
|
|
452
|
+
const withSlot = node.slot
|
|
453
|
+
? [...inside, { as: "slot", ...(expects ? { expects } : {}) }]
|
|
454
|
+
: inside;
|
|
455
|
+
if (withSlot.length > 0) {
|
|
456
|
+
built.children = withSlot;
|
|
457
|
+
if (as !== "row" && as !== "stack")
|
|
458
|
+
built.as = arrangement(classes);
|
|
459
|
+
}
|
|
460
|
+
return [built];
|
|
461
|
+
};
|
|
462
|
+
/**
|
|
463
|
+
* The component's own root is node 0 and its classes are the recipe's `base`.
|
|
464
|
+
* A root that renders nothing (a portal, a provider) hands its children over,
|
|
465
|
+
* which is how a Tooltip's popup reaches the tree at all.
|
|
466
|
+
*
|
|
467
|
+
* A component returning a FRAGMENT has several depth-0 elements and only the
|
|
468
|
+
* first one's classes became `base`, so the rest are emitted as siblings
|
|
469
|
+
* instead of being dropped with the fragment.
|
|
470
|
+
*/
|
|
471
|
+
const top = roots.length > 0 ? roots[0] : 0;
|
|
472
|
+
const read = [
|
|
473
|
+
...(kids[top] ?? []).flatMap((child) => build(child, 0)),
|
|
474
|
+
...roots.slice(1).flatMap((sibling) => build(sibling, 0)),
|
|
475
|
+
];
|
|
476
|
+
/**
|
|
477
|
+
* THE ROOT'S OWN `{children}` - the component whose entire content is the
|
|
478
|
+
* caller's.
|
|
479
|
+
*
|
|
480
|
+
* `<tr className="border-b hover:bg-ocean-50/30">{children}</tr>` is the whole
|
|
481
|
+
* of a real DataTableRow, and skipping node 0 skips the one thing it holds. Six
|
|
482
|
+
* components in the owner's library are exactly this shape, and they previewed
|
|
483
|
+
* as empty boxes: a Chat that says nothing about messages reads as a component
|
|
484
|
+
* we failed to extract (dono, 01/08).
|
|
485
|
+
*/
|
|
486
|
+
const rootNode = sketch[top];
|
|
487
|
+
if (rootNode?.slot) {
|
|
488
|
+
const only = EXPECTS[String(rootNode.tag ?? "").toLowerCase()];
|
|
489
|
+
read.push({ as: "slot", ...(only ? { expects: only } : {}) });
|
|
490
|
+
}
|
|
491
|
+
return { read, notes, libraries };
|
|
492
|
+
}
|
package/dist/anatomy-read.js
CHANGED
|
@@ -56,7 +56,17 @@ const STRUCTURAL = /^(root|wrapper|base|el|outer)$/i;
|
|
|
56
56
|
/** A pathological transcription of somebody's DOM, bounded. Six levels reaches
|
|
57
57
|
* any real component; past that it is layout divs all the way down. */
|
|
58
58
|
const MAX_DEPTH = 6;
|
|
59
|
-
|
|
59
|
+
/**
|
|
60
|
+
* The same ceiling the sketch measured, for the same reason.
|
|
61
|
+
*
|
|
62
|
+
* 48 was the budget for an anatomy somebody TYPED, where 48 nodes is a very long
|
|
63
|
+
* message. A DERIVED anatomy is as long as their component is, and the sketch
|
|
64
|
+
* already measured that ceiling on a real library: exactly one component passes
|
|
65
|
+
* 60 nodes (`TextEditor`, 67) and nothing passes 150. At 48 that component lost
|
|
66
|
+
* its editable region - the whole point of it - which is the same truncation the
|
|
67
|
+
* sketch cap was raised to stop (dono, 02/08).
|
|
68
|
+
*/
|
|
69
|
+
const MAX_NODES = 150;
|
|
60
70
|
/**
|
|
61
71
|
* `Actions.Generate` → `actions-generate`, and never `actions.generate`.
|
|
62
72
|
*
|
|
@@ -182,9 +192,21 @@ sketch) {
|
|
|
182
192
|
const { as, coerced: wasCoerced } = formOf(node);
|
|
183
193
|
if (wasCoerced)
|
|
184
194
|
coerced += 1;
|
|
185
|
-
|
|
186
|
-
|
|
195
|
+
/**
|
|
196
|
+
* A FRONTIER carries a reference, not a name and not a style block: the
|
|
197
|
+
* appearance there belongs to somebody else's recipe.
|
|
198
|
+
*
|
|
199
|
+
* WHAT IT DOES CARRY IS WHAT THEIR CODE PUTS INSIDE IT. `<Card>` wrapping a
|
|
200
|
+
* header and a chart is the parent's markup, wearing the parent's classes,
|
|
201
|
+
* and stopping at the name dropped 55 of 369 elements across a real library
|
|
202
|
+
* - 15% of its markup, including the whole visible content of two charts
|
|
203
|
+
* (probe, 02/08).
|
|
204
|
+
*/
|
|
187
205
|
if (FRONTIERS.has(as)) {
|
|
206
|
+
const held = Array.isArray(node.children)
|
|
207
|
+
? walk(node.children, depth + 1)
|
|
208
|
+
: [];
|
|
209
|
+
const holds = held.length > 0 ? { children: held } : {};
|
|
188
210
|
if (as === "component") {
|
|
189
211
|
const name = String(node.ref ?? "").trim();
|
|
190
212
|
if (!name)
|
|
@@ -204,6 +226,7 @@ sketch) {
|
|
|
204
226
|
ref: slug || safePartName(name),
|
|
205
227
|
...(slug && slug !== safePartName(name) ? { refName: name } : {}),
|
|
206
228
|
...frontierClasses(node, sketch),
|
|
229
|
+
...holds,
|
|
207
230
|
});
|
|
208
231
|
continue;
|
|
209
232
|
}
|
|
@@ -218,6 +241,7 @@ sketch) {
|
|
|
218
241
|
from,
|
|
219
242
|
...(version ? { version } : {}),
|
|
220
243
|
...frontierClasses(node, sketch),
|
|
244
|
+
...holds,
|
|
221
245
|
});
|
|
222
246
|
continue;
|
|
223
247
|
}
|
package/dist/commands/import.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, join, relative } from "node:path";
|
|
3
|
+
import { anatomyFromSketch } from "../anatomy-from-sketch.js";
|
|
3
4
|
import { resolveAnatomy, resolveFlatParts, safePartName, } from "../anatomy-read.js";
|
|
4
5
|
import { readCredentials, readToken, resolveRegistry, sameRegistry, } from "../config.js";
|
|
5
6
|
import { architectureRule, describeArchitecture, describeChoice, detectArchitectures, } from "../doctor/architecture.js";
|
|
@@ -421,6 +422,7 @@ export async function takeCensus(root, opts) {
|
|
|
421
422
|
...(esketch.length > 0 ? { sketch: esketch } : {}),
|
|
422
423
|
...et,
|
|
423
424
|
...(etag ? { rootTag: etag } : {}),
|
|
425
|
+
...rootPackage(etag, esketch),
|
|
424
426
|
};
|
|
425
427
|
}
|
|
426
428
|
}
|
|
@@ -526,6 +528,7 @@ export async function takeCensus(root, opts) {
|
|
|
526
528
|
...(Object.keys(parts).length > 0 ? { parts } : {}),
|
|
527
529
|
...(tree.length > 0 ? { tree } : {}),
|
|
528
530
|
...(tag ? { rootTag: tag } : {}),
|
|
531
|
+
...rootPackage(tag, sketch),
|
|
529
532
|
...(layers.length > 0 ? { layers } : {}),
|
|
530
533
|
...(Object.keys(v.axes).length > 0 ? { declaredAxes: v.axes } : {}),
|
|
531
534
|
...(Object.keys(defaults).length > 0 ? { defaults } : {}),
|
|
@@ -1624,9 +1627,39 @@ function sayReach(c) {
|
|
|
1624
1627
|
* Their declared tokens come from the census itself, so a run that only sends a
|
|
1625
1628
|
* file it was handed still resolves a `bg-ocean-500` by their own name.
|
|
1626
1629
|
*/
|
|
1630
|
+
/**
|
|
1631
|
+
* THE PACKAGE THE ROOT TAG CAME FROM, read off the sketch that just measured it.
|
|
1632
|
+
*
|
|
1633
|
+
* The sketch stamps every capitalised tag with its own import specifier, and node
|
|
1634
|
+
* 0 IS the returned element - so this costs a lookup rather than a second parse.
|
|
1635
|
+
* The tag guard is there because a component whose root the sketch could not reach
|
|
1636
|
+
* (a return the scanner did not follow) must say nothing rather than borrow the
|
|
1637
|
+
* first node's package.
|
|
1638
|
+
*/
|
|
1639
|
+
function rootPackage(tag, sketch) {
|
|
1640
|
+
if (!tag || !/^[A-Z]/.test(tag))
|
|
1641
|
+
return {};
|
|
1642
|
+
const first = sketch[0];
|
|
1643
|
+
return first && first.tag === tag && first.from
|
|
1644
|
+
? { rootFrom: first.from }
|
|
1645
|
+
: {};
|
|
1646
|
+
}
|
|
1627
1647
|
async function resolveReadParts(census, root) {
|
|
1628
|
-
|
|
1629
|
-
|
|
1648
|
+
/**
|
|
1649
|
+
* A READING IS OPTIONAL HERE. THE SKETCH IS NOT.
|
|
1650
|
+
*
|
|
1651
|
+
* This returned on line one when nobody had authored a reading, and a DERIVED
|
|
1652
|
+
* anatomy needs no reading at all - so the deriver shipped and never ran on the
|
|
1653
|
+
* path the owner actually uses. Measured on his own re-import (02/08): the census
|
|
1654
|
+
* carried `rootFrom` on 13 looks and `from` on 162 sketch nodes - the new fields
|
|
1655
|
+
* arrived - and `tree` on 0 of 36. Two components then read as empty contracts
|
|
1656
|
+
* and came back below the floor.
|
|
1657
|
+
*
|
|
1658
|
+
* "Medido 100%, convertido 0%", one week after the law that names it. The guard
|
|
1659
|
+
* is now about having something to convert, not about who asked.
|
|
1660
|
+
*/
|
|
1661
|
+
const read = census.reading?.components ?? {};
|
|
1662
|
+
if (!census.looks || Object.keys(census.looks).length === 0)
|
|
1630
1663
|
return;
|
|
1631
1664
|
const declared = new Map(Object.entries(census.declared));
|
|
1632
1665
|
// The manifest is read ONCE for the whole run: the version belongs in a rule's
|
|
@@ -1658,27 +1691,72 @@ async function resolveReadParts(census, root) {
|
|
|
1658
1691
|
crosswalked.set(c.name, safePartName(target));
|
|
1659
1692
|
}
|
|
1660
1693
|
const resolveRef = (name) => crosswalked.get(name) ?? null;
|
|
1661
|
-
for (const
|
|
1694
|
+
for (const component of Object.keys(looks)) {
|
|
1695
|
+
const entry = read[component];
|
|
1662
1696
|
const anatomy = entry?.anatomy;
|
|
1697
|
+
/**
|
|
1698
|
+
* THE SKETCH IS THE ANATOMY WHEN NOBODY AUTHORED ONE.
|
|
1699
|
+
*
|
|
1700
|
+
* Measured on the owner's census before this existed: 36 of 36 components had
|
|
1701
|
+
* a sketch, 259 of their nodes carried a class string, and 0 of 36 recipes
|
|
1702
|
+
* carried a tree - because a tree only appeared when the skill wrote one out
|
|
1703
|
+
* component by component. `Modal` therefore shipped with an empty recipe: its
|
|
1704
|
+
* root is an unstyled `BaseDialog.Root` and its backdrop, popup and close
|
|
1705
|
+
* button are three levels below it (dono, 02/08).
|
|
1706
|
+
*
|
|
1707
|
+
* An AUTHORED read still wins. The skill can see the props, the prop types
|
|
1708
|
+
* and the call sites, so its names beat derived ones - this only fills the
|
|
1709
|
+
* silence, which was total.
|
|
1710
|
+
*/
|
|
1711
|
+
const derived = Array.isArray(anatomy) && anatomy.length > 0
|
|
1712
|
+
? null
|
|
1713
|
+
: anatomyFromSketch(looks[component]?.sketch, resolveRef, (pkg) => deps[pkg]);
|
|
1714
|
+
if (derived?.read.length) {
|
|
1715
|
+
for (const note of derived.notes)
|
|
1716
|
+
notes.add(note);
|
|
1717
|
+
}
|
|
1663
1718
|
const resolved = Array.isArray(anatomy) && anatomy.length > 0
|
|
1664
1719
|
? resolveAnatomy(anatomy, declared, deps, resolveRef, entry?.root,
|
|
1665
1720
|
// The component's own sketch, so a node naming itself by index reads
|
|
1666
1721
|
// the exact class string the census measured (dono, 01/08).
|
|
1667
1722
|
looks[component]?.sketch)
|
|
1668
|
-
:
|
|
1669
|
-
?
|
|
1670
|
-
:
|
|
1723
|
+
: derived && derived.read.length > 0
|
|
1724
|
+
? resolveAnatomy(derived.read, declared, deps, resolveRef, entry?.root, looks[component]?.sketch)
|
|
1725
|
+
: Array.isArray(entry?.parts) && entry.parts.length > 0
|
|
1726
|
+
? resolveFlatParts(entry.parts, declared)
|
|
1727
|
+
: null;
|
|
1671
1728
|
if (!resolved)
|
|
1672
1729
|
continue;
|
|
1673
1730
|
const hasParts = Object.keys(resolved.parts).length > 0;
|
|
1674
1731
|
if (!hasParts && resolved.tree.length === 0)
|
|
1675
1732
|
continue;
|
|
1676
1733
|
const look = looks[component];
|
|
1734
|
+
/**
|
|
1735
|
+
* THE DEPENDENCIES, INCLUDING THE ONES WE DRAW OURSELVES.
|
|
1736
|
+
*
|
|
1737
|
+
* A headless primitive is drawn rather than named - right for the appearance,
|
|
1738
|
+
* a lie about the dependency. Their Modal reconstructed as divs looks the same
|
|
1739
|
+
* and has no focus trap and no escape key, so the package is declared with the
|
|
1740
|
+
* split said out loud: the pixels are theirs, the behaviour is the library's.
|
|
1741
|
+
*/
|
|
1742
|
+
const pinned = deps ?? {};
|
|
1743
|
+
const libraries = [
|
|
1744
|
+
...resolved.external,
|
|
1745
|
+
...(derived?.libraries ?? [])
|
|
1746
|
+
.filter((lib) => !resolved.external.some((e) => e.from === lib.from))
|
|
1747
|
+
.map((lib) => ({
|
|
1748
|
+
from: lib.from,
|
|
1749
|
+
...(pinned[lib.from] ? { version: pinned[lib.from] } : {}),
|
|
1750
|
+
...(lib.kind === "headless" || lib.kind === "icon"
|
|
1751
|
+
? { kind: lib.kind }
|
|
1752
|
+
: {}),
|
|
1753
|
+
})),
|
|
1754
|
+
];
|
|
1677
1755
|
const carried = {
|
|
1678
1756
|
...(hasParts ? { parts: resolved.parts } : {}),
|
|
1679
1757
|
...(resolved.tree.length > 0 ? { tree: resolved.tree } : {}),
|
|
1680
1758
|
...(resolved.composes.length > 0 ? { composes: resolved.composes } : {}),
|
|
1681
|
-
...(
|
|
1759
|
+
...(libraries.length > 0 ? { external: libraries } : {}),
|
|
1682
1760
|
...(resolved.root ? { root: resolved.root } : {}),
|
|
1683
1761
|
};
|
|
1684
1762
|
looks[component] = look
|
|
@@ -1704,15 +1782,19 @@ async function resolveReadParts(census, root) {
|
|
|
1704
1782
|
if (resolved.tree.length > 0)
|
|
1705
1783
|
shaped += 1;
|
|
1706
1784
|
edges += resolved.composes.length;
|
|
1707
|
-
for (const e of
|
|
1785
|
+
for (const e of libraries)
|
|
1708
1786
|
libs.add(e.from);
|
|
1709
1787
|
for (const n of resolved.notes)
|
|
1710
1788
|
notes.add(n);
|
|
1711
1789
|
}
|
|
1712
|
-
const
|
|
1790
|
+
const authored = Object.keys(read).length;
|
|
1791
|
+
const total = authored || Object.keys(census.looks ?? {}).length;
|
|
1713
1792
|
if (named > 0 || shaped > 0) {
|
|
1714
1793
|
console.log("");
|
|
1715
|
-
console.log(body(
|
|
1794
|
+
console.log(body(
|
|
1795
|
+
// "Your reading" only when there WAS one - otherwise the names came off
|
|
1796
|
+
// their own markup, and claiming otherwise credits somebody's absent work.
|
|
1797
|
+
`${named} part${named === 1 ? "" : "s"} named across ${total} component${total === 1 ? "" : "s"}${authored > 0 ? " - your reading and their markup" : " - read off their own markup"}, so each previews as what it IS rather than as a text box`));
|
|
1716
1798
|
}
|
|
1717
1799
|
if (shaped > 0) {
|
|
1718
1800
|
console.log(body(`${shaped} of them arrived with a SHAPE - the parts nested as they nest in your code, so the preview is the component and not a row of labels`));
|
|
@@ -1824,6 +1906,20 @@ export async function runImport(opts) {
|
|
|
1824
1906
|
if (scope)
|
|
1825
1907
|
census.scope = scope;
|
|
1826
1908
|
}
|
|
1909
|
+
/**
|
|
1910
|
+
* DERIVE THE ANATOMY ON THE MEASURING PATH TOO.
|
|
1911
|
+
*
|
|
1912
|
+
* The send path resolves the reading above; a plain `--dry` measure never
|
|
1913
|
+
* reached that function, so the census on disk - the file the skill reads to
|
|
1914
|
+
* author its reading, and the file every one of our real-repo specs reads -
|
|
1915
|
+
* carried a sketch for every component and a tree for none. The person's own
|
|
1916
|
+
* re-import proved it: 36 sketches, 162 stamped nodes, 0 trees (dono, 02/08).
|
|
1917
|
+
*
|
|
1918
|
+
* Written into the census means the skill sees the shape it would otherwise
|
|
1919
|
+
* have to author from scratch, and can spend its judgement on the names.
|
|
1920
|
+
*/
|
|
1921
|
+
if (!opts.census)
|
|
1922
|
+
await resolveReadParts(census, root);
|
|
1827
1923
|
summarize(census);
|
|
1828
1924
|
// Only when nobody has scoped yet. Telling someone to scope to the folder
|
|
1829
1925
|
// they just scoped to reads as the tool not having noticed.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHERE EACH NAME IN A FILE CAME FROM.
|
|
3
|
+
*
|
|
4
|
+
* The sketch recorded `tag: "Dialog.Root"` and threw away the one fact that says
|
|
5
|
+
* what it is. `Dialog.Root` out of `@base-ui/react` is an unstyled div waiting
|
|
6
|
+
* for the client's classes; `Dialog.Root` out of a styled kit draws itself. Same
|
|
7
|
+
* identifier, opposite handling - so the package has to travel with the node, and
|
|
8
|
+
* the reader already has the file open when it could be stamping it.
|
|
9
|
+
*
|
|
10
|
+
* Regex over the import statements rather than a parse: an import is one of five
|
|
11
|
+
* shapes, all of them flat, and the reader is deliberately AST-free everywhere
|
|
12
|
+
* else in this module for the same reason (speed on a repo of 704 files).
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* `import X, { a, b as c } from "pkg"` / `import * as NS from "pkg"`.
|
|
16
|
+
*
|
|
17
|
+
* The two clause slots are the default and the named block, in either order, and
|
|
18
|
+
* `type` imports are skipped by the caller's own filter below - a type never
|
|
19
|
+
* appears as a tag.
|
|
20
|
+
*/
|
|
21
|
+
const IMPORT = /import\s+(?!type\s)([\w$]+|\*\s+as\s+[\w$]+|\{[^}]*\})(?:\s*,\s*(\{[^}]*\}|[\w$]+))?\s+from\s*["']([^"']+)["']/g;
|
|
22
|
+
/**
|
|
23
|
+
* Every local name this file binds, mapped to the specifier it came from.
|
|
24
|
+
*
|
|
25
|
+
* An alias is stored under the LOCAL name, because the local name is what the
|
|
26
|
+
* JSX says: `import { Select as BaseSelect }` means the sketch's `BaseSelect.Item`
|
|
27
|
+
* has to find `@base-ui/react/select`.
|
|
28
|
+
*/
|
|
29
|
+
export function importMap(source) {
|
|
30
|
+
const out = {};
|
|
31
|
+
IMPORT.lastIndex = 0;
|
|
32
|
+
let match = IMPORT.exec(source);
|
|
33
|
+
while (match) {
|
|
34
|
+
const pkg = match[3];
|
|
35
|
+
for (const clause of [match[1], match[2]]) {
|
|
36
|
+
if (!clause)
|
|
37
|
+
continue;
|
|
38
|
+
if (clause.startsWith("*")) {
|
|
39
|
+
const ns = clause.split(/\s+as\s+/)[1]?.trim();
|
|
40
|
+
if (ns)
|
|
41
|
+
out[ns] = pkg;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!clause.startsWith("{")) {
|
|
45
|
+
out[clause.trim()] = pkg;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const raw of clause.slice(1, -1).split(",")) {
|
|
49
|
+
const entry = raw.trim().replace(/^type\s+/, "");
|
|
50
|
+
if (!entry)
|
|
51
|
+
continue;
|
|
52
|
+
const [, local] = entry.split(/\s+as\s+/);
|
|
53
|
+
const name = (local ?? entry).trim();
|
|
54
|
+
if (name)
|
|
55
|
+
out[name] = pkg;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
match = IMPORT.exec(source);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The specifier a TAG resolves to, or undefined when the file declares it itself.
|
|
64
|
+
*
|
|
65
|
+
* Undefined is a real answer and not a failure: 20 of 181 capitalised nodes in
|
|
66
|
+
* the owner's library are declared in the same file (`ToolbarButton`, `Shimmer`,
|
|
67
|
+
* `CustomLegend`), and those are the client's own markup wearing the client's own
|
|
68
|
+
* classes - which is the opposite of a third party.
|
|
69
|
+
*/
|
|
70
|
+
export function packageOfTag(tag, imports) {
|
|
71
|
+
const head = tag.includes(".") ? tag.split(".")[0] : tag;
|
|
72
|
+
return imports[head];
|
|
73
|
+
}
|
package/dist/doctor/sketch.js
CHANGED
|
@@ -19,7 +19,54 @@
|
|
|
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 { importMap, packageOfTag } from "./imports.js";
|
|
22
23
|
import { definitionSpan } from "./transcribe.js";
|
|
24
|
+
/** `{children}` / `{props.children}` - the caller's content, spelled either way. */
|
|
25
|
+
const CHILDREN = /\{\s*(?:props\.)?children\s*\}/;
|
|
26
|
+
/**
|
|
27
|
+
* THE TEXT THAT SITS DIRECTLY INSIDE ONE ELEMENT - its own gaps, not its
|
|
28
|
+
* children's contents.
|
|
29
|
+
*
|
|
30
|
+
* The first version of the slot check read only the span between an element's
|
|
31
|
+
* open tag and the next tag of any kind, so it saw `{children}` only when it was
|
|
32
|
+
* the FIRST thing inside. A real panel renders a header and THEN the caller's
|
|
33
|
+
* content, which is the mixed case the reader has a form for - and it was
|
|
34
|
+
* invisible. Found by driving `runImport` over a Base-UI dialog whose `{children}`
|
|
35
|
+
* follows its close button (02/08).
|
|
36
|
+
*
|
|
37
|
+
* Attribution matters as much as detection: `{children}` inside a CHILD is that
|
|
38
|
+
* child's slot, so the walk collects the gaps at this element's own level and
|
|
39
|
+
* skips whole subtrees.
|
|
40
|
+
*/
|
|
41
|
+
function directText(source, events, index) {
|
|
42
|
+
const open = events[index];
|
|
43
|
+
const openEnd = source.indexOf(">", open.at);
|
|
44
|
+
if (openEnd === -1)
|
|
45
|
+
return "";
|
|
46
|
+
let from = openEnd + 1;
|
|
47
|
+
const gaps = [];
|
|
48
|
+
let rel = 0;
|
|
49
|
+
for (let j = index + 1; j < events.length; j++) {
|
|
50
|
+
const event = events[j];
|
|
51
|
+
if (event.kind === "open") {
|
|
52
|
+
if (rel === 0)
|
|
53
|
+
gaps.push(source.slice(from, event.at));
|
|
54
|
+
rel += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
rel -= 1;
|
|
58
|
+
// Our own close: the last gap runs up to it, and the element ends here.
|
|
59
|
+
if (rel < 0) {
|
|
60
|
+
gaps.push(source.slice(from, event.at));
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
if (rel === 0) {
|
|
64
|
+
const closeEnd = source.indexOf(">", event.at);
|
|
65
|
+
from = closeEnd === -1 ? event.at : closeEnd + 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return gaps.join(" ");
|
|
69
|
+
}
|
|
23
70
|
/**
|
|
24
71
|
* Past this a component is a page in disguise, and the gate already said no.
|
|
25
72
|
*
|
|
@@ -114,6 +161,12 @@ function staticCallClasses(body) {
|
|
|
114
161
|
* and the slot form already says so.
|
|
115
162
|
*/
|
|
116
163
|
export function sketchOf(source, name) {
|
|
164
|
+
/**
|
|
165
|
+
* READ THE IMPORTS BEFORE SLICING - they live at the top of the file, and the
|
|
166
|
+
* span below cuts them off. One map per file, so a 16-node component costs one
|
|
167
|
+
* pass over the header rather than sixteen lookups into the disk.
|
|
168
|
+
*/
|
|
169
|
+
const imports = importMap(source);
|
|
117
170
|
/**
|
|
118
171
|
* ONE COMPONENT'S MARKUP, when the file declares more than one.
|
|
119
172
|
*
|
|
@@ -141,6 +194,12 @@ export function sketchOf(source, name) {
|
|
|
141
194
|
if (out.length >= MAX_NODES)
|
|
142
195
|
break;
|
|
143
196
|
const node = { tag: event.tag, depth };
|
|
197
|
+
// What a capitalised tag IS, from the file's own imports - see `SketchNode.from`.
|
|
198
|
+
if (/^[A-Z]/.test(event.tag)) {
|
|
199
|
+
const pkg = packageOfTag(event.tag, imports);
|
|
200
|
+
if (pkg)
|
|
201
|
+
node.from = pkg;
|
|
202
|
+
}
|
|
144
203
|
const cls = CLASS_ATTR.exec(event.body);
|
|
145
204
|
if (cls?.[1].trim())
|
|
146
205
|
node.classes = cls[1].trim();
|
|
@@ -163,6 +222,11 @@ export function sketchOf(source, name) {
|
|
|
163
222
|
node.text = between;
|
|
164
223
|
}
|
|
165
224
|
}
|
|
225
|
+
// The caller's content, as a fact - see `SketchNode.slot`. Read from the
|
|
226
|
+
// element's DIRECT text, so a `{children}` after a header still counts and one
|
|
227
|
+
// inside a child belongs to that child.
|
|
228
|
+
if (CHILDREN.test(directText(source, events, i)))
|
|
229
|
+
node.slot = true;
|
|
166
230
|
out.push(node);
|
|
167
231
|
depth += 1;
|
|
168
232
|
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT A PACKAGE IS, SO A NODE KNOWS WHETHER WE CAN DRAW IT.
|
|
3
|
+
*
|
|
4
|
+
* The reader had exactly two answers for a capitalised element: `component` (one
|
|
5
|
+
* of theirs) or `external` (somebody else's, drawn as a named block and stopped).
|
|
6
|
+
* Measured across the owner's library - 372 sketch nodes, resolved against each
|
|
7
|
+
* file's own imports (dono, 02/08):
|
|
8
|
+
*
|
|
9
|
+
* 191 (51%) HTML
|
|
10
|
+
* 69 (18%) headless primitives @base-ui/react/* 36 carry classes
|
|
11
|
+
* 35 ( 9%) one of theirs @ui/lib/SignalUI/* 32 carry classes
|
|
12
|
+
* 34 ( 9%) icon glyphs lucide-react 13 carry classes
|
|
13
|
+
* 20 ( 5%) local to the file ToolbarButton, Shimmer 4 carry classes
|
|
14
|
+
* 20 ( 5%) genuinely opaque recharts, tiptap 5 carry classes
|
|
15
|
+
* 3 ( 0%) motion wrappers AnimatePresence 0 carry classes
|
|
16
|
+
*
|
|
17
|
+
* 122 of 372 nodes - a third of the library's markup - fell into `external` and
|
|
18
|
+
* stopped there. Seven components (20% of the library) had a headless primitive
|
|
19
|
+
* as their ROOT, so `collapsible-card` and `radio-card` carried six and eight
|
|
20
|
+
* real declarations behind a "3rd-party" badge and a chrome box.
|
|
21
|
+
*
|
|
22
|
+
* THE DISTINCTION IS NOT COSMETIC. A headless library ships behaviour and no
|
|
23
|
+
* appearance: every pixel of a Base-UI dialog is in the CLIENT's class list, so
|
|
24
|
+
* the whole visual truth is ours to read and draw. An opaque library ships the
|
|
25
|
+
* appearance itself: `<SketchPicker>` draws a colour wheel we cannot reproduce
|
|
26
|
+
* from a class string, and pretending otherwise would be inventing it.
|
|
27
|
+
*
|
|
28
|
+
* DECIDED BY PACKAGE, NEVER BY COMPONENT NAME. `Dialog.Root` from
|
|
29
|
+
* `@base-ui/react` and `Dialog.Root` from a styled kit are the same identifier
|
|
30
|
+
* and opposite answers, so the identifier cannot be the input. Nothing here
|
|
31
|
+
* matches a component name, which is what keeps it working on a library nobody
|
|
32
|
+
* here has seen.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Behaviour without appearance. Matched as a PREFIX, so `@base-ui/react/dialog`
|
|
36
|
+
* and `@radix-ui/react-dialog` both land without an entry each.
|
|
37
|
+
*/
|
|
38
|
+
const HEADLESS = [
|
|
39
|
+
"@base-ui/react",
|
|
40
|
+
"@base-ui-components/react",
|
|
41
|
+
"@mui/base",
|
|
42
|
+
"radix-ui",
|
|
43
|
+
"@radix-ui/react-",
|
|
44
|
+
"@headlessui/",
|
|
45
|
+
"react-aria",
|
|
46
|
+
"@react-aria/",
|
|
47
|
+
"@react-stately/",
|
|
48
|
+
"@ariakit/",
|
|
49
|
+
"ariakit",
|
|
50
|
+
"@reach/",
|
|
51
|
+
"@zag-js/",
|
|
52
|
+
"@ark-ui/",
|
|
53
|
+
"vaul",
|
|
54
|
+
"cmdk",
|
|
55
|
+
"downshift",
|
|
56
|
+
"@tanstack/react-table",
|
|
57
|
+
"@tanstack/react-virtual",
|
|
58
|
+
];
|
|
59
|
+
/** Glyph packages: one element, one path, a size and a colour. */
|
|
60
|
+
const ICONS = [
|
|
61
|
+
"lucide-react",
|
|
62
|
+
"lucide-preact",
|
|
63
|
+
"react-icons",
|
|
64
|
+
"react-feather",
|
|
65
|
+
"@radix-ui/react-icons",
|
|
66
|
+
"@heroicons/",
|
|
67
|
+
"@tabler/icons",
|
|
68
|
+
"@mui/icons-material",
|
|
69
|
+
"@ant-design/icons",
|
|
70
|
+
"@phosphor-icons/",
|
|
71
|
+
"phosphor-react",
|
|
72
|
+
"@fortawesome/",
|
|
73
|
+
"react-bootstrap-icons",
|
|
74
|
+
"@iconify/",
|
|
75
|
+
"iconoir-react",
|
|
76
|
+
];
|
|
77
|
+
/** Animation wrappers whose own element is either absent or a plain div. */
|
|
78
|
+
const MOTION = [
|
|
79
|
+
"framer-motion",
|
|
80
|
+
"motion",
|
|
81
|
+
"@react-spring/",
|
|
82
|
+
"react-transition-group",
|
|
83
|
+
];
|
|
84
|
+
/**
|
|
85
|
+
* A relative or aliased specifier is the client's own code, whatever it is
|
|
86
|
+
* called. `@/`, `~/` and `#` are the three alias spellings a bundler hands out.
|
|
87
|
+
*/
|
|
88
|
+
function isPath(pkg) {
|
|
89
|
+
return (pkg.startsWith(".") ||
|
|
90
|
+
pkg.startsWith("/") ||
|
|
91
|
+
pkg.startsWith("@/") ||
|
|
92
|
+
pkg.startsWith("~") ||
|
|
93
|
+
pkg.startsWith("#"));
|
|
94
|
+
}
|
|
95
|
+
const startsWithAny = (pkg, list) => list.some((entry) => pkg === entry || pkg.startsWith(entry));
|
|
96
|
+
/**
|
|
97
|
+
* The kind of a package specifier, from the table and nothing else.
|
|
98
|
+
*
|
|
99
|
+
* An UNKNOWN bare specifier answers `opaque`, because that is what it is: a
|
|
100
|
+
* third-party package we have no reason to believe we can draw. Their own
|
|
101
|
+
* library imported through a tsconfig alias (`@ui/lib/SignalUI/atoms/Card`) also
|
|
102
|
+
* lands here, and the caller corrects it with the one fact that settles it - the
|
|
103
|
+
* census crosswalk already knows whether the NAME is a component they define.
|
|
104
|
+
* See `frontierOf`.
|
|
105
|
+
*/
|
|
106
|
+
export function frontierKind(pkg) {
|
|
107
|
+
const id = (pkg ?? "").trim();
|
|
108
|
+
if (!id)
|
|
109
|
+
return "own";
|
|
110
|
+
if (isPath(id))
|
|
111
|
+
return "own";
|
|
112
|
+
if (startsWithAny(id, ICONS))
|
|
113
|
+
return "icon";
|
|
114
|
+
if (startsWithAny(id, HEADLESS))
|
|
115
|
+
return "headless";
|
|
116
|
+
if (startsWithAny(id, MOTION))
|
|
117
|
+
return "motion";
|
|
118
|
+
return "opaque";
|
|
119
|
+
}
|
|
120
|
+
/** `@scope/name/sub` → `@scope/name`; `name/sub` → `name`. */
|
|
121
|
+
function packageRoot(pkg) {
|
|
122
|
+
const parts = pkg.split("/");
|
|
123
|
+
return pkg.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* A range that means "this is in this repository": a workspace sibling, a linked
|
|
127
|
+
* folder, a tarball on disk. The one fact that tells their own published library
|
|
128
|
+
* apart from a third party, since both are spelled `@scope/name`.
|
|
129
|
+
*/
|
|
130
|
+
const LOCAL_RANGE = /^(workspace:|file:|link:|portal:)/;
|
|
131
|
+
/**
|
|
132
|
+
* The kind of ONE ELEMENT: the table, then their manifest, then the crosswalk.
|
|
133
|
+
*
|
|
134
|
+
* ORDER IS THE WHOLE TRICK, and getting it wrong is a bug a spec caught here
|
|
135
|
+
* before a person did. `Tooltip` is a component they define AND a component
|
|
136
|
+
* `recharts` exports, and their PieChart imports the recharts one - so a
|
|
137
|
+
* crosswalk consulted too early claims a third-party chart tooltip as theirs.
|
|
138
|
+
*
|
|
139
|
+
* So the question the manifest answers comes first: is this specifier a package
|
|
140
|
+
* they installed, or a path into their own source? `recharts: ^2.12.0` is a third
|
|
141
|
+
* party; `@frontend-hub/ui: workspace:*` is their own library one folder over;
|
|
142
|
+
* `@ui/lib/SignalUI/atoms/Card` is not in the manifest at all, because it is a
|
|
143
|
+
* tsconfig alias. Only then does the crosswalk get asked.
|
|
144
|
+
*
|
|
145
|
+
* `defines` is the census's crosswalk: their name → the slug it reaches, or null.
|
|
146
|
+
* `versionOf` is their manifest: a package name → the range they pinned.
|
|
147
|
+
*/
|
|
148
|
+
export function frontierOf(tag, pkg, defines, versionOf) {
|
|
149
|
+
const kind = frontierKind(pkg);
|
|
150
|
+
if (kind !== "opaque" || !pkg)
|
|
151
|
+
return kind;
|
|
152
|
+
const head = tag.includes(".") ? tag.split(".")[0] : tag;
|
|
153
|
+
const version = versionOf?.(packageRoot(pkg));
|
|
154
|
+
if (version)
|
|
155
|
+
return LOCAL_RANGE.test(version) ? "own" : "opaque";
|
|
156
|
+
/**
|
|
157
|
+
* NO MANIFEST ANSWER, so the specifier's own shape is what is left. An alias
|
|
158
|
+
* into their source addresses a FILE (`@ui/lib/SignalUI/atoms/Card`); a package
|
|
159
|
+
* they never declared addresses a package (`recharts`, `@scope/name`). Asking
|
|
160
|
+
* the crosswalk only for the first shape is what keeps `Tooltip` honest when
|
|
161
|
+
* the manifest was unreadable.
|
|
162
|
+
*/
|
|
163
|
+
const deep = pkg.split("/").length > (pkg.startsWith("@") ? 2 : 1);
|
|
164
|
+
return deep && defines?.(head) ? "own" : "opaque";
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Members of a headless namespace that render NOTHING - a portal, a positioner,
|
|
168
|
+
* a context provider.
|
|
169
|
+
*
|
|
170
|
+
* This is the one place a member NAME is read, and it is the shared vocabulary
|
|
171
|
+
* of every headless library rather than one library's private spelling: Base-UI,
|
|
172
|
+
* Radix, Ark and Ariakit all spell a portal `Portal`. It is also gated on the
|
|
173
|
+
* node carrying no classes at all, so a `Positioner` the client styled survives
|
|
174
|
+
* as a real box - which is exactly what happens in their Select.
|
|
175
|
+
*/
|
|
176
|
+
const NON_RENDERING = new Set([
|
|
177
|
+
"portal",
|
|
178
|
+
"positioner",
|
|
179
|
+
"provider",
|
|
180
|
+
"presence",
|
|
181
|
+
"animatepresence",
|
|
182
|
+
"anchor",
|
|
183
|
+
"fragment",
|
|
184
|
+
/**
|
|
185
|
+
* `Root` earns its place here only when it carries nothing. A headless `Root`
|
|
186
|
+
* is sometimes the visible surface - their `Collapsible.Root` holds the card's
|
|
187
|
+
* six declarations - and sometimes a bare context wrapper, as their
|
|
188
|
+
* `BaseSelect.Root` is. The class list is what separates the two, and the gate
|
|
189
|
+
* above reads exactly that: styled roots survive, empty ones step aside rather
|
|
190
|
+
* than becoming a part literally named `root`.
|
|
191
|
+
*/
|
|
192
|
+
"root",
|
|
193
|
+
]);
|
|
194
|
+
/** `BaseDialog.Portal` → true. `BaseSelect.Positioner` with `z-50 pt-1` → false. */
|
|
195
|
+
export function rendersNothing(tag, classes) {
|
|
196
|
+
if (classes?.trim())
|
|
197
|
+
return false;
|
|
198
|
+
const member = tag.includes(".") ? tag.split(".").pop() : tag;
|
|
199
|
+
return NON_RENDERING.has((member ?? "").toLowerCase());
|
|
200
|
+
}
|
package/dist/skill-import.js
CHANGED
|
@@ -148,11 +148,11 @@ file to pick between. Nothing is sent by \`--dry\`. Read the file.
|
|
|
148
148
|
### 2. Walk them through the decisions - ALL of them, before any reading
|
|
149
149
|
|
|
150
150
|
**Every question this skill asks is answerable from the census you just took.** Source, name,
|
|
151
|
-
primary, theme, canvas,
|
|
152
|
-
|
|
151
|
+
primary, theme, canvas, architecture - the numbers behind all six are in the file on their disk,
|
|
152
|
+
and none of them needs the anatomy.
|
|
153
153
|
|
|
154
154
|
They used to be asked AFTER the reading, and that put six hundred lines of work between the
|
|
155
|
-
person's first answer and their second (dono, 01/08). A person who answers
|
|
155
|
+
person's first answer and their second (dono, 01/08). A person who answers six questions in
|
|
156
156
|
ninety seconds and then watches a bar has had a good four minutes; the same person interrupted
|
|
157
157
|
twice in the middle of a silence has had a bad twenty.
|
|
158
158
|
|
|
@@ -160,7 +160,7 @@ So: ask everything here, in one pass, and **do not come back**. If something you
|
|
|
160
160
|
the reading contradicts an answer, you do not reopen the question - you say so in the report at
|
|
161
161
|
the end and let them change it there.
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
Six decisions are theirs. **Ask them as separate questions with selectable options** - use your
|
|
164
164
|
question tool, one call per decision, so they pick instead of reading a wall and composing a
|
|
165
165
|
reply. A single block containing everything is a report, and a report gets read, not answered.
|
|
166
166
|
|
|
@@ -244,35 +244,37 @@ canvas #222326 darkgray-500 - what DashboardLayout paints on <main>
|
|
|
244
244
|
Say it and let them object. Offering five near-blacks to pick between is asking someone to
|
|
245
245
|
re-decide something their code already decided.
|
|
246
246
|
|
|
247
|
-
**2f. Our kit
|
|
248
|
-
|
|
249
|
-
has nothing to weigh and should never see the question.
|
|
247
|
+
**2f. Our kit** - do not ask. A project that already has a component library gets **theirs
|
|
248
|
+
alone**, and you say so in one line:
|
|
250
249
|
|
|
251
250
|
\`\`\`
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
add ours to yours (recommended) 68 components, and the 23 governed by your
|
|
256
|
-
own rules are the 23 you wrote
|
|
257
|
-
mine alone 37 components, all of them yours. Our
|
|
258
|
-
layouts that need a component you do not
|
|
259
|
-
have stay out too. Add any of ours later
|
|
260
|
-
from the library, one at a time
|
|
251
|
+
This system is yours alone - the 37 components you wrote, and none of ours.
|
|
252
|
+
Add any of ours later from the library, one at a time.
|
|
261
253
|
\`\`\`
|
|
262
254
|
|
|
263
|
-
Send it as \`"standards": "none"\` in the reading
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
255
|
+
Send it as \`"standards": "none"\` in the reading. Every time, with one exception below.
|
|
256
|
+
|
|
257
|
+
**Why this stopped being a question.** Somebody who points us at their own library is telling us
|
|
258
|
+
what their system is, and 45 components of ours landing beside 37 of theirs makes a system of 82
|
|
259
|
+
where only 37 are governed by anything they wrote - the number on their own screen stops being
|
|
260
|
+
about them. The kit is one click away in the library afterwards, so choosing wrong here costs a
|
|
261
|
+
click and choosing to ask costs a pause in the middle of their import.
|
|
262
|
+
|
|
263
|
+
**The exception, and it is not a preference.** An app with NO components of its own - \`--scope\`
|
|
264
|
+
pointed at an app rather than a library, and the CLI reported no component library - must NOT get
|
|
265
|
+
\`"standards": "none"\`: theirs alone plus nothing of theirs is a system with zero components. Leave
|
|
266
|
+
the field out there, exactly as before.
|
|
267
267
|
|
|
268
268
|
Two things to be straight about, because both are true and neither is obvious:
|
|
269
269
|
|
|
270
|
-
- **the foundations come from the seed
|
|
270
|
+
- **the foundations still come from the seed.** Their palette, their scales, their type - all
|
|
271
271
|
theirs - but the seven type slots, the easing and the neutral floor arrive from ours, because
|
|
272
272
|
the alternative to a type scale is no components at all rather than their components.
|
|
273
273
|
- **our layouts are written in our recipes.** A landing that composes \`button\`, \`card\` and
|
|
274
|
-
\`badge\` needs those to exist.
|
|
275
|
-
|
|
274
|
+
\`badge\` needs those to exist. Theirs alone keeps the layouts whose recipes they happen to have
|
|
275
|
+
and drops the rest - a layout pointing at a recipe nobody has renders as holes. Say that number
|
|
276
|
+
when the CLI reports it, so a shorter list of layouts is a fact they were told rather than one
|
|
277
|
+
they discover.
|
|
276
278
|
|
|
277
279
|
**2g. Which architecture has priority** - ask only when the CLI reported more than
|
|
278
280
|
one, which it does under "How this project is organised".
|
package/package.json
CHANGED