synthesisui 0.16.101 → 0.16.102
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 +71 -6
- package/dist/doctor/imports.js +73 -0
- package/dist/doctor/sketch.js +18 -0
- package/dist/frontier-kind.js +200 -0
- 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,6 +1627,23 @@ 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
1648
|
const read = census.reading?.components;
|
|
1629
1649
|
if (!read)
|
|
@@ -1658,27 +1678,72 @@ async function resolveReadParts(census, root) {
|
|
|
1658
1678
|
crosswalked.set(c.name, safePartName(target));
|
|
1659
1679
|
}
|
|
1660
1680
|
const resolveRef = (name) => crosswalked.get(name) ?? null;
|
|
1661
|
-
for (const
|
|
1681
|
+
for (const component of Object.keys(looks)) {
|
|
1682
|
+
const entry = read[component];
|
|
1662
1683
|
const anatomy = entry?.anatomy;
|
|
1684
|
+
/**
|
|
1685
|
+
* THE SKETCH IS THE ANATOMY WHEN NOBODY AUTHORED ONE.
|
|
1686
|
+
*
|
|
1687
|
+
* Measured on the owner's census before this existed: 36 of 36 components had
|
|
1688
|
+
* a sketch, 259 of their nodes carried a class string, and 0 of 36 recipes
|
|
1689
|
+
* carried a tree - because a tree only appeared when the skill wrote one out
|
|
1690
|
+
* component by component. `Modal` therefore shipped with an empty recipe: its
|
|
1691
|
+
* root is an unstyled `BaseDialog.Root` and its backdrop, popup and close
|
|
1692
|
+
* button are three levels below it (dono, 02/08).
|
|
1693
|
+
*
|
|
1694
|
+
* An AUTHORED read still wins. The skill can see the props, the prop types
|
|
1695
|
+
* and the call sites, so its names beat derived ones - this only fills the
|
|
1696
|
+
* silence, which was total.
|
|
1697
|
+
*/
|
|
1698
|
+
const derived = Array.isArray(anatomy) && anatomy.length > 0
|
|
1699
|
+
? null
|
|
1700
|
+
: anatomyFromSketch(looks[component]?.sketch, resolveRef, (pkg) => deps[pkg]);
|
|
1701
|
+
if (derived?.read.length) {
|
|
1702
|
+
for (const note of derived.notes)
|
|
1703
|
+
notes.add(note);
|
|
1704
|
+
}
|
|
1663
1705
|
const resolved = Array.isArray(anatomy) && anatomy.length > 0
|
|
1664
1706
|
? resolveAnatomy(anatomy, declared, deps, resolveRef, entry?.root,
|
|
1665
1707
|
// The component's own sketch, so a node naming itself by index reads
|
|
1666
1708
|
// the exact class string the census measured (dono, 01/08).
|
|
1667
1709
|
looks[component]?.sketch)
|
|
1668
|
-
:
|
|
1669
|
-
?
|
|
1670
|
-
:
|
|
1710
|
+
: derived && derived.read.length > 0
|
|
1711
|
+
? resolveAnatomy(derived.read, declared, deps, resolveRef, entry?.root, looks[component]?.sketch)
|
|
1712
|
+
: Array.isArray(entry?.parts) && entry.parts.length > 0
|
|
1713
|
+
? resolveFlatParts(entry.parts, declared)
|
|
1714
|
+
: null;
|
|
1671
1715
|
if (!resolved)
|
|
1672
1716
|
continue;
|
|
1673
1717
|
const hasParts = Object.keys(resolved.parts).length > 0;
|
|
1674
1718
|
if (!hasParts && resolved.tree.length === 0)
|
|
1675
1719
|
continue;
|
|
1676
1720
|
const look = looks[component];
|
|
1721
|
+
/**
|
|
1722
|
+
* THE DEPENDENCIES, INCLUDING THE ONES WE DRAW OURSELVES.
|
|
1723
|
+
*
|
|
1724
|
+
* A headless primitive is drawn rather than named - right for the appearance,
|
|
1725
|
+
* a lie about the dependency. Their Modal reconstructed as divs looks the same
|
|
1726
|
+
* and has no focus trap and no escape key, so the package is declared with the
|
|
1727
|
+
* split said out loud: the pixels are theirs, the behaviour is the library's.
|
|
1728
|
+
*/
|
|
1729
|
+
const pinned = deps ?? {};
|
|
1730
|
+
const libraries = [
|
|
1731
|
+
...resolved.external,
|
|
1732
|
+
...(derived?.libraries ?? [])
|
|
1733
|
+
.filter((lib) => !resolved.external.some((e) => e.from === lib.from))
|
|
1734
|
+
.map((lib) => ({
|
|
1735
|
+
from: lib.from,
|
|
1736
|
+
...(pinned[lib.from] ? { version: pinned[lib.from] } : {}),
|
|
1737
|
+
...(lib.kind === "headless" || lib.kind === "icon"
|
|
1738
|
+
? { kind: lib.kind }
|
|
1739
|
+
: {}),
|
|
1740
|
+
})),
|
|
1741
|
+
];
|
|
1677
1742
|
const carried = {
|
|
1678
1743
|
...(hasParts ? { parts: resolved.parts } : {}),
|
|
1679
1744
|
...(resolved.tree.length > 0 ? { tree: resolved.tree } : {}),
|
|
1680
1745
|
...(resolved.composes.length > 0 ? { composes: resolved.composes } : {}),
|
|
1681
|
-
...(
|
|
1746
|
+
...(libraries.length > 0 ? { external: libraries } : {}),
|
|
1682
1747
|
...(resolved.root ? { root: resolved.root } : {}),
|
|
1683
1748
|
};
|
|
1684
1749
|
looks[component] = look
|
|
@@ -1704,7 +1769,7 @@ async function resolveReadParts(census, root) {
|
|
|
1704
1769
|
if (resolved.tree.length > 0)
|
|
1705
1770
|
shaped += 1;
|
|
1706
1771
|
edges += resolved.composes.length;
|
|
1707
|
-
for (const e of
|
|
1772
|
+
for (const e of libraries)
|
|
1708
1773
|
libs.add(e.from);
|
|
1709
1774
|
for (const n of resolved.notes)
|
|
1710
1775
|
notes.add(n);
|
|
@@ -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,10 @@
|
|
|
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*\}/;
|
|
23
26
|
/**
|
|
24
27
|
* Past this a component is a page in disguise, and the gate already said no.
|
|
25
28
|
*
|
|
@@ -114,6 +117,12 @@ function staticCallClasses(body) {
|
|
|
114
117
|
* and the slot form already says so.
|
|
115
118
|
*/
|
|
116
119
|
export function sketchOf(source, name) {
|
|
120
|
+
/**
|
|
121
|
+
* READ THE IMPORTS BEFORE SLICING - they live at the top of the file, and the
|
|
122
|
+
* span below cuts them off. One map per file, so a 16-node component costs one
|
|
123
|
+
* pass over the header rather than sixteen lookups into the disk.
|
|
124
|
+
*/
|
|
125
|
+
const imports = importMap(source);
|
|
117
126
|
/**
|
|
118
127
|
* ONE COMPONENT'S MARKUP, when the file declares more than one.
|
|
119
128
|
*
|
|
@@ -141,6 +150,12 @@ export function sketchOf(source, name) {
|
|
|
141
150
|
if (out.length >= MAX_NODES)
|
|
142
151
|
break;
|
|
143
152
|
const node = { tag: event.tag, depth };
|
|
153
|
+
// What a capitalised tag IS, from the file's own imports - see `SketchNode.from`.
|
|
154
|
+
if (/^[A-Z]/.test(event.tag)) {
|
|
155
|
+
const pkg = packageOfTag(event.tag, imports);
|
|
156
|
+
if (pkg)
|
|
157
|
+
node.from = pkg;
|
|
158
|
+
}
|
|
144
159
|
const cls = CLASS_ATTR.exec(event.body);
|
|
145
160
|
if (cls?.[1].trim())
|
|
146
161
|
node.classes = cls[1].trim();
|
|
@@ -162,6 +177,9 @@ export function sketchOf(source, name) {
|
|
|
162
177
|
!between.includes("<")) {
|
|
163
178
|
node.text = between;
|
|
164
179
|
}
|
|
180
|
+
// The caller's content, as a fact - see `SketchNode.slot`.
|
|
181
|
+
if (CHILDREN.test(between))
|
|
182
|
+
node.slot = true;
|
|
165
183
|
}
|
|
166
184
|
out.push(node);
|
|
167
185
|
depth += 1;
|
|
@@ -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/package.json
CHANGED