@parity-app/cli 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +64 -2
- package/dist/bin.mjs +831 -0
- package/dist/client-DsTLQ2lT.mjs +286 -0
- package/dist/in-app.json +10 -0
- package/dist/index.d.mts +160 -0
- package/dist/index.mjs +1 -0
- package/dist/libraries/aria.lib.par +14516 -0
- package/dist/libraries/baseui.lib.par +8298 -0
- package/dist/libraries/buildkit-baseui.edits.json +3 -0
- package/dist/libraries/chakra.lib.par +539225 -0
- package/dist/libraries/fluent-icons.lib.par +2674 -0
- package/dist/libraries/fluent.lib.par +33703 -0
- package/dist/libraries/heroicons.lib.par +33 -0
- package/dist/libraries/hugeicons.lib.par +708 -0
- package/dist/libraries/lucide.lib.par +2123 -0
- package/dist/libraries/luma.lib.par +16362 -0
- package/dist/libraries/lyra.lib.par +16362 -0
- package/dist/libraries/maia.lib.par +16362 -0
- package/dist/libraries/mantine.lib.par +32935 -0
- package/dist/libraries/mira.lib.par +16362 -0
- package/dist/libraries/mui.lib.par +9817 -0
- package/dist/libraries/nova.lib.par +16363 -0
- package/dist/libraries/phosphor.lib.par +3060 -0
- package/dist/libraries/radix.lib.par +6890 -0
- package/dist/libraries/recharts.lib.par +5445 -0
- package/dist/libraries/rhea.lib.par +16362 -0
- package/dist/libraries/sera.lib.par +16362 -0
- package/dist/libraries/tabler.lib.par +5325 -0
- package/dist/libraries/vega.lib.par +16362 -0
- package/dist/published-BtL8RM6W.mjs +1 -0
- package/dist/published-CdXoUgfD.mjs +200 -0
- package/dist/src-8MhDV_sA.mjs +4780 -0
- package/dist/src-D9EMN48r.mjs +1306 -0
- package/dist/src-DXZqtm6z.mjs +53 -0
- package/dist/src-DYi23Ypb.mjs +1 -0
- package/dist/src-FF8eY6EI.mjs +2 -0
- package/package.json +50 -3
- package/skills/SKILL.md +96 -0
- package/skills/document.md +170 -0
- package/skills/execute.md +298 -0
- package/skills/guide/checking.md +56 -0
- package/skills/guide/components.md +48 -0
- package/skills/guide/design-system.md +52 -0
- package/skills/guide/mobile.md +32 -0
- package/skills/guide/states-and-modes.md +55 -0
- package/skills/guide/web-app.md +43 -0
|
@@ -0,0 +1,4780 @@
|
|
|
1
|
+
import { S as lengthToPixels, a as applyLibraryEdits, c as isSampleElement, f as parseLibraryEdits, l as isSampleRender, m as renderSlotOf, p as parseLibraryManifest, s as isMadeValue, u as libraryManifestExtension } from "./src-D9EMN48r.mjs";
|
|
2
|
+
import "./src-DXZqtm6z.mjs";
|
|
3
|
+
import { a as publishedVersions, n as packageFile, o as repositoryFiles, r as publishedSource } from "./published-CdXoUgfD.mjs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { basename, dirname, join, posix, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
import ts from "typescript";
|
|
13
|
+
import "semver";
|
|
14
|
+
import { execFile } from "node:child_process";
|
|
15
|
+
// ../core/src/icon-names.ts
|
|
16
|
+
/**
|
|
17
|
+
* Each export's icon, size and style, for a set whose names carry them (Fluent's `Add24Regular`). Digits before the
|
|
18
|
+
* style are a size only when they're one of the set's sizes and the name before them is an icon the set exports
|
|
19
|
+
* without a size (`CalendarDay` for `CalendarDay32Color`); otherwise they're part of the name, as in `Battery10` and
|
|
20
|
+
* `TextHeader1`. An icon the set ships only at fixed sizes (`CodeCs16Regular`), whose name no other export has at a
|
|
21
|
+
* size, reads as that icon at that size. Exports the pattern doesn't match are left out.
|
|
22
|
+
*/
|
|
23
|
+
function iconVariants(names, variants) {
|
|
24
|
+
const pattern = variants.pattern ? new RegExp(variants.pattern) : void 0;
|
|
25
|
+
const sizes = new Set(variants.sizes ?? []);
|
|
26
|
+
const parsed = names.flatMap((name) => {
|
|
27
|
+
if (!pattern) return [{
|
|
28
|
+
name,
|
|
29
|
+
base: name,
|
|
30
|
+
digits: ""
|
|
31
|
+
}];
|
|
32
|
+
const groups = pattern.exec(name)?.groups;
|
|
33
|
+
if (!groups) return [];
|
|
34
|
+
return [{
|
|
35
|
+
name,
|
|
36
|
+
base: groups.name ?? name,
|
|
37
|
+
digits: groups.size ?? "",
|
|
38
|
+
...groups.style ? { style: groups.style } : {}
|
|
39
|
+
}];
|
|
40
|
+
});
|
|
41
|
+
const bare = new Set(parsed.map(({ base, digits }) => base + digits));
|
|
42
|
+
const splits = ({ base, digits }) => digits.split("").flatMap((_, at) => {
|
|
43
|
+
const size = digits.slice(at);
|
|
44
|
+
return String(Number(size)) === size && (sizes.size === 0 || sizes.has(Number(size))) ? [{
|
|
45
|
+
label: base + digits.slice(0, at),
|
|
46
|
+
size: Number(size)
|
|
47
|
+
}] : [];
|
|
48
|
+
});
|
|
49
|
+
const read = /* @__PURE__ */ new Map();
|
|
50
|
+
for (const entry of parsed) {
|
|
51
|
+
const copy = entry.digits ? splits(entry).find((split) => bare.has(split.label)) : void 0;
|
|
52
|
+
read.set(entry.name, {
|
|
53
|
+
...copy ?? { label: entry.base + entry.digits },
|
|
54
|
+
style: entry.style
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const sized = new Set([...read.values()].flatMap((reading) => reading.size === void 0 ? [] : [reading.label]));
|
|
58
|
+
for (const entry of parsed) {
|
|
59
|
+
const reading = read.get(entry.name);
|
|
60
|
+
if (!entry.digits || reading.size !== void 0 || sized.has(reading.label)) continue;
|
|
61
|
+
const fixed = splits(entry)[0];
|
|
62
|
+
if (fixed) read.set(entry.name, {
|
|
63
|
+
...fixed,
|
|
64
|
+
style: entry.style,
|
|
65
|
+
fixed: true
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const preferred = variants.default?.size ?? 20;
|
|
69
|
+
const distance = (size) => Math.abs(size - preferred) - (size > preferred ? .5 : 0);
|
|
70
|
+
const nearest = /* @__PURE__ */ new Map();
|
|
71
|
+
for (const [name, reading] of read) {
|
|
72
|
+
if (!reading.fixed) continue;
|
|
73
|
+
const key = `${reading.label}\u0000${reading.style ?? ""}`;
|
|
74
|
+
const current = nearest.get(key);
|
|
75
|
+
if (!current || distance(reading.size) < distance(read.get(current).size)) nearest.set(key, name);
|
|
76
|
+
}
|
|
77
|
+
const listed = new Set(nearest.values());
|
|
78
|
+
const result = /* @__PURE__ */ new Map();
|
|
79
|
+
for (const [name, { label, size, style }] of read) result.set(name, {
|
|
80
|
+
label,
|
|
81
|
+
...size === void 0 ? {} : { size },
|
|
82
|
+
...style === void 0 ? {} : { style },
|
|
83
|
+
listed: size === void 0 || listed.has(name)
|
|
84
|
+
});
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
// ../code/src/checker.ts
|
|
88
|
+
const probeName = "__parity_probe__.ts";
|
|
89
|
+
const probeText = "import type { ReactNode } from \"react\"\nexport type ParityNode = ReactNode\n";
|
|
90
|
+
/** The settings components are read with: the scan's own over ones that read JSX, JavaScript and modern modules. */
|
|
91
|
+
function componentOptions(compilerOptions) {
|
|
92
|
+
return {
|
|
93
|
+
target: ts.ScriptTarget.ES2022,
|
|
94
|
+
module: ts.ModuleKind.ESNext,
|
|
95
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
96
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
97
|
+
strict: true,
|
|
98
|
+
skipLibCheck: true,
|
|
99
|
+
noEmit: true,
|
|
100
|
+
allowJs: true,
|
|
101
|
+
types: [],
|
|
102
|
+
...compilerOptions,
|
|
103
|
+
incremental: false
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** A file only the checker sees, naming React's `ReactNode` so it can tell what React renders. */
|
|
107
|
+
function reactProbe(root) {
|
|
108
|
+
return slashes(join(root, probeName));
|
|
109
|
+
}
|
|
110
|
+
/** A compiler host that also serves `reactProbe(root)`. */
|
|
111
|
+
function withReactProbe(host, root) {
|
|
112
|
+
const probe = reactProbe(root);
|
|
113
|
+
const getSourceFile = host.getSourceFile.bind(host);
|
|
114
|
+
const fileExists = host.fileExists.bind(host);
|
|
115
|
+
const readFile = host.readFile.bind(host);
|
|
116
|
+
host.getSourceFile = (fileName, language, onError, create) => slashes(fileName) === probe ? ts.createSourceFile(fileName, probeText, language, true) : getSourceFile(fileName, language, onError, create);
|
|
117
|
+
host.fileExists = (fileName) => slashes(fileName) === probe || fileExists(fileName);
|
|
118
|
+
host.readFile = (fileName) => slashes(fileName) === probe ? probeText : readFile(fileName);
|
|
119
|
+
return host;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Finds the components a package or an app exports, by asking TypeScript what each export is, as
|
|
123
|
+
* `proposals/component-detection.md` describes: a capitalised function taking one props object (or nothing) and
|
|
124
|
+
* returning something React renders, or a `forwardRef`, `memo` or `lazy` result, or a class with `render` and `props`.
|
|
125
|
+
* Members of namespace exports count too, as `Dialog.Root`.
|
|
126
|
+
*/
|
|
127
|
+
function findComponents(scan) {
|
|
128
|
+
const probe = reactProbe(scan.root);
|
|
129
|
+
const program = scan.program ?? (() => {
|
|
130
|
+
const options = componentOptions(scan.compilerOptions);
|
|
131
|
+
const host = withReactProbe(ts.createCompilerHost(options, true), scan.root);
|
|
132
|
+
return ts.createProgram({
|
|
133
|
+
rootNames: [...scan.entries, probe],
|
|
134
|
+
options,
|
|
135
|
+
host
|
|
136
|
+
});
|
|
137
|
+
})();
|
|
138
|
+
const checker = program.getTypeChecker();
|
|
139
|
+
const node = reactNodeType(checker, program.getSourceFile(probe));
|
|
140
|
+
const report = {
|
|
141
|
+
components: [],
|
|
142
|
+
ambiguous: [],
|
|
143
|
+
rejected: [],
|
|
144
|
+
reactTypes: node !== void 0
|
|
145
|
+
};
|
|
146
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
147
|
+
const seen = /* @__PURE__ */ new Map();
|
|
148
|
+
const visit = (name, symbol, from, namespace) => {
|
|
149
|
+
const short = name.slice(name.lastIndexOf(".") + 1);
|
|
150
|
+
if (!/^[A-Z]/.test(short) || !(symbol.flags & ts.SymbolFlags.Value)) return;
|
|
151
|
+
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
152
|
+
if (!declaration) return;
|
|
153
|
+
const type = checker.getTypeOfSymbolAtLocation(symbol, declaration);
|
|
154
|
+
let verdict = verdicts.get(type);
|
|
155
|
+
if (!verdict) {
|
|
156
|
+
verdict = judge(checker, type, node);
|
|
157
|
+
if (!(type.flags & ts.TypeFlags.Any)) verdicts.set(type, verdict);
|
|
158
|
+
}
|
|
159
|
+
const where = scan.defaultExports ? from : {};
|
|
160
|
+
if ("ambiguous" in verdict) return void report.ambiguous.push({
|
|
161
|
+
name,
|
|
162
|
+
reason: verdict.ambiguous,
|
|
163
|
+
...where
|
|
164
|
+
});
|
|
165
|
+
if ("not" in verdict) return void report.rejected.push({
|
|
166
|
+
name,
|
|
167
|
+
reason: verdict.not,
|
|
168
|
+
...where
|
|
169
|
+
});
|
|
170
|
+
const existing = seen.get(symbol);
|
|
171
|
+
if (existing) {
|
|
172
|
+
const declaredIn = (entry) => slashes(entry) === slashes(declaration.getSourceFile().fileName);
|
|
173
|
+
if (scan.defaultExports && declaredIn(from.entry) !== declaredIn(existing.entry) ? declaredIn(from.entry) : preferred(name, existing.name, namespace)) Object.assign(existing, {
|
|
174
|
+
name,
|
|
175
|
+
...from
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const file = declaration.getSourceFile().fileName;
|
|
180
|
+
const props = verdict.props ? ownProps(checker, verdict.props) : {};
|
|
181
|
+
const found = {
|
|
182
|
+
name,
|
|
183
|
+
as: verdict.as,
|
|
184
|
+
file,
|
|
185
|
+
group: namespace ?? groupOf(file, short),
|
|
186
|
+
requiredProps: verdict.requiredProps,
|
|
187
|
+
type: checker.typeToString(type),
|
|
188
|
+
...from,
|
|
189
|
+
props,
|
|
190
|
+
fingerprint: hash([verdict.as, ...Object.entries(props).map(([prop, kind]) => `${prop}:${kind}`).sort()].join("|")),
|
|
191
|
+
samples: verdict.props ? samplesFor(checker, verdict.props, verdict.requiredProps) : {}
|
|
192
|
+
};
|
|
193
|
+
seen.set(symbol, found);
|
|
194
|
+
report.components.push(found);
|
|
195
|
+
};
|
|
196
|
+
for (const entry of scan.entries) {
|
|
197
|
+
const file = program.getSourceFile(entry);
|
|
198
|
+
const moduleSymbol = file && checker.getSymbolAtLocation(file);
|
|
199
|
+
if (!moduleSymbol) continue;
|
|
200
|
+
for (const exported of checker.getExportsOfModule(moduleSymbol)) {
|
|
201
|
+
const target = exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported;
|
|
202
|
+
let name = exported.name;
|
|
203
|
+
if (name === "default") {
|
|
204
|
+
if (!scan.defaultExports) continue;
|
|
205
|
+
name = defaultExportName(target, entry);
|
|
206
|
+
}
|
|
207
|
+
if (isNamespace(target)) {
|
|
208
|
+
if (!/^[A-Z]/.test(name)) continue;
|
|
209
|
+
for (const member of checker.getExportsOfModule(target)) {
|
|
210
|
+
const resolved = member.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(member) : member;
|
|
211
|
+
visit(`${name}.${member.name}`, resolved, {
|
|
212
|
+
entry,
|
|
213
|
+
export: `${exported.name}.${member.name}`
|
|
214
|
+
}, name);
|
|
215
|
+
}
|
|
216
|
+
} else visit(name, target, {
|
|
217
|
+
entry,
|
|
218
|
+
export: exported.name
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const names = new Set(report.components.map((component) => component.name));
|
|
223
|
+
report.components = report.components.filter((component) => {
|
|
224
|
+
const dot = component.name.lastIndexOf(".");
|
|
225
|
+
if (dot === -1) return true;
|
|
226
|
+
const namespace = component.name.slice(0, dot).split(".").at(-1);
|
|
227
|
+
const member = component.name.slice(dot + 1);
|
|
228
|
+
if (!member.startsWith(namespace)) return true;
|
|
229
|
+
return !names.has(`${component.name.slice(0, dot)}.${member.slice(namespace.length) || "Root"}`);
|
|
230
|
+
});
|
|
231
|
+
report.components.sort((a, b) => a.name.localeCompare(b.name, "en"));
|
|
232
|
+
return report;
|
|
233
|
+
}
|
|
234
|
+
function judge(checker, type, node) {
|
|
235
|
+
if (type.flags & ts.TypeFlags.Any) return { ambiguous: "its type is any, so it can't be told apart from a helper" };
|
|
236
|
+
const written = checker.typeToString(type, void 0, ts.TypeFormatFlags.NoTruncation);
|
|
237
|
+
if (type.getProperty("Provider") && type.getProperty("Consumer")) return { not: "a context, which passes a value down rather than drawing anything" };
|
|
238
|
+
if (type.getProperty("$$typeof")) {
|
|
239
|
+
let as = "exotic";
|
|
240
|
+
if (/ForwardRef/.test(written)) as = "forwardRef";
|
|
241
|
+
else if (/Memo|NamedExotic/.test(written)) as = "memo";
|
|
242
|
+
else if (/Lazy/.test(written)) as = "lazy";
|
|
243
|
+
return {
|
|
244
|
+
as,
|
|
245
|
+
...propsOf(checker, type.getCallSignatures()[0])
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const constructs = type.getConstructSignatures();
|
|
249
|
+
if (constructs.length > 0) {
|
|
250
|
+
const instance = constructs[0].getReturnType();
|
|
251
|
+
if (instance.getProperty("render") && instance.getProperty("props")) {
|
|
252
|
+
const props = instance.getProperty("props");
|
|
253
|
+
const propsType = checker.getTypeOfSymbol(props);
|
|
254
|
+
return {
|
|
255
|
+
as: "class",
|
|
256
|
+
requiredProps: required(checker, propsType),
|
|
257
|
+
props: propsType
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return { not: "a class without React's `render` and `props`" };
|
|
261
|
+
}
|
|
262
|
+
const calls = type.getCallSignatures();
|
|
263
|
+
if (calls.length === 0) return { not: "not a function" };
|
|
264
|
+
for (const signature of calls) {
|
|
265
|
+
const parameters = signature.getParameters();
|
|
266
|
+
const needed = parameters.filter((parameter) => isRequired(parameter));
|
|
267
|
+
if (needed.length > 1) return { not: `takes ${needed.length} arguments, not one props object` };
|
|
268
|
+
const first = parameters[0];
|
|
269
|
+
if (first) {
|
|
270
|
+
const propsType = checker.getTypeOfSymbol(first);
|
|
271
|
+
if (isPrimitive(propsType)) return { not: `takes ${checker.typeToString(propsType)}, not props` };
|
|
272
|
+
}
|
|
273
|
+
const returned = signature.getReturnType();
|
|
274
|
+
if (returned.flags & ts.TypeFlags.Any) return { ambiguous: "returns any, so whether React can render it isn't known" };
|
|
275
|
+
if (node) {
|
|
276
|
+
const awaited = checker.getAwaitedType(returned) ?? returned;
|
|
277
|
+
if (!checker.isTypeAssignableTo(awaited, node)) return { not: `returns ${shorten(checker.typeToString(returned))}, which React doesn't render` };
|
|
278
|
+
if (isPrimitive(returned)) return { not: `returns ${checker.typeToString(returned)} only` };
|
|
279
|
+
} else if (!/Element|ReactNode|ReactPortal|null/.test(checker.typeToString(returned))) return { ambiguous: "React's types aren't installed, and its return type doesn't look like an element" };
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
as: "function",
|
|
283
|
+
...propsOf(checker, calls[0])
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function propsOf(checker, signature) {
|
|
287
|
+
const first = signature?.getParameters()[0];
|
|
288
|
+
if (!first) return { requiredProps: [] };
|
|
289
|
+
const props = checker.getTypeOfSymbol(first);
|
|
290
|
+
return {
|
|
291
|
+
requiredProps: required(checker, props),
|
|
292
|
+
props
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function required(checker, type) {
|
|
296
|
+
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return [];
|
|
297
|
+
return (type.isUnion() ? type.types : [type]).map((shape) => checker.getPropertiesOfType(shape).filter((property) => !(property.flags & ts.SymbolFlags.Optional)).map((property) => property.name)).reduce((all, names) => all.filter((name) => names.includes(name)));
|
|
298
|
+
}
|
|
299
|
+
function isRequired(parameter) {
|
|
300
|
+
const declaration = parameter.valueDeclaration;
|
|
301
|
+
if (!declaration || !ts.isParameter(declaration)) return true;
|
|
302
|
+
return !declaration.questionToken && !declaration.initializer && !declaration.dotDotDotToken;
|
|
303
|
+
}
|
|
304
|
+
const primitive = ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike | ts.TypeFlags.BigIntLike | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.Void;
|
|
305
|
+
function isPrimitive(type) {
|
|
306
|
+
return (type.isUnion() ? type.types : [type]).every((part) => (part.flags & primitive) !== 0);
|
|
307
|
+
}
|
|
308
|
+
function isNamespace(symbol) {
|
|
309
|
+
const valueKinds = ts.SymbolFlags.Function | ts.SymbolFlags.Variable | ts.SymbolFlags.Class;
|
|
310
|
+
return (symbol.flags & ts.SymbolFlags.ValueModule) !== 0 && (symbol.flags & valueKinds) === 0;
|
|
311
|
+
}
|
|
312
|
+
function reactNodeType(checker, probe) {
|
|
313
|
+
const moduleSymbol = probe && checker.getSymbolAtLocation(probe);
|
|
314
|
+
const alias = moduleSymbol && checker.getExportsOfModule(moduleSymbol).find((symbol) => symbol.name === "ParityNode");
|
|
315
|
+
if (!alias) return void 0;
|
|
316
|
+
const type = checker.getDeclaredTypeOfSymbol(alias);
|
|
317
|
+
return type.flags & ts.TypeFlags.Any ? void 0 : type;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Which of two names for one component to keep: a namespace's member over its flat twin (`Dialog.Root`, not
|
|
321
|
+
* `DialogRoot`); inside a namespace, the one that doesn't repeat it (`Dialog.Root`, not `Dialog.Dialog`); otherwise
|
|
322
|
+
* the shorter (`Heart`, not `LucideHeart`).
|
|
323
|
+
*/
|
|
324
|
+
function preferred(candidate, current, namespace) {
|
|
325
|
+
if (candidate.replace(".", "") === current.replace(".", "")) return candidate.includes(".");
|
|
326
|
+
if (namespace) {
|
|
327
|
+
const repeats = (name) => name.slice(namespace.length + 1).startsWith(namespace);
|
|
328
|
+
if (repeats(current) !== repeats(candidate)) return repeats(current);
|
|
329
|
+
}
|
|
330
|
+
return candidate.length < current.length || candidate.length === current.length && candidate < current;
|
|
331
|
+
}
|
|
332
|
+
const buildFolders = /* @__PURE__ */ new Set([
|
|
333
|
+
"dist",
|
|
334
|
+
"lib",
|
|
335
|
+
"esm",
|
|
336
|
+
"cjs",
|
|
337
|
+
"types",
|
|
338
|
+
"src",
|
|
339
|
+
"build",
|
|
340
|
+
"es",
|
|
341
|
+
"node",
|
|
342
|
+
"browser"
|
|
343
|
+
]);
|
|
344
|
+
/**
|
|
345
|
+
* A component's family, from where it's declared: its file (`Button/Button.d.ts` is Button), or for a file named
|
|
346
|
+
* `index`, its package or folder, as Fluent's `@fluentui/react-button` is Button.
|
|
347
|
+
*/
|
|
348
|
+
function groupOf(file, fallback) {
|
|
349
|
+
const name = basename(file).replace(/(\.d)?\.[cm]?[jt]sx?$/, "");
|
|
350
|
+
if (name !== "index") return pascal(name);
|
|
351
|
+
let folder = dirname(file);
|
|
352
|
+
while (buildFolders.has(basename(folder)) && dirname(folder) !== folder) folder = dirname(folder);
|
|
353
|
+
const base = basename(folder).replace(/^react-/, "");
|
|
354
|
+
return base ? pascal(base) : fallback;
|
|
355
|
+
}
|
|
356
|
+
function pascal(name) {
|
|
357
|
+
return name.split(/[-_.]/).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join("");
|
|
358
|
+
}
|
|
359
|
+
function defaultExportName(symbol, entry) {
|
|
360
|
+
const declared = symbol.valueDeclaration && ts.getNameOfDeclaration(symbol.valueDeclaration);
|
|
361
|
+
if (declared && ts.isIdentifier(declared) && /^[A-Z]/.test(declared.text)) return declared.text;
|
|
362
|
+
return pascal(basename(entry).replace(/\.[cm]?[jt]sx?$/, ""));
|
|
363
|
+
}
|
|
364
|
+
function shorten(text, length = 60) {
|
|
365
|
+
return text.length > length ? `${text.slice(0, length - 1)}…` : text;
|
|
366
|
+
}
|
|
367
|
+
function slashes(path) {
|
|
368
|
+
return path.replaceAll("\\", "/");
|
|
369
|
+
}
|
|
370
|
+
/** A path relative to a folder, with forward slashes. */
|
|
371
|
+
function relativePath(from, to) {
|
|
372
|
+
return slashes(relative(from, to));
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* A component's own props: those its package declares, not the HTML attributes and React props it passes on, which
|
|
376
|
+
* every component shares and which would make every fingerprint change with React's types.
|
|
377
|
+
*/
|
|
378
|
+
function ownProps(checker, type) {
|
|
379
|
+
const props = {};
|
|
380
|
+
for (const property of checker.getPropertiesOfType(type)) {
|
|
381
|
+
const declared = property.declarations?.[0]?.getSourceFile().fileName ?? "";
|
|
382
|
+
if (/[/\\]@types[/\\]react|[/\\]typescript[/\\]lib[/\\]/.test(declared)) continue;
|
|
383
|
+
const written = checker.typeToString(checker.getTypeOfSymbol(property), void 0, ts.TypeFormatFlags.NoTruncation);
|
|
384
|
+
const optional = property.flags & ts.SymbolFlags.Optional ? "?" : "";
|
|
385
|
+
props[`${property.name}${optional}`] = hash(written).slice(0, 6);
|
|
386
|
+
}
|
|
387
|
+
return Object.fromEntries(Object.entries(props).sort(([a], [b]) => a.localeCompare(b, "en")));
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Sample values for required props, from their types: the first of a set of strings, a word for a string or a
|
|
391
|
+
* node, a number, `true`, or an empty list. A function or an object is left out, since a sample can't say what it does.
|
|
392
|
+
*/
|
|
393
|
+
function samplesFor(checker, type, required) {
|
|
394
|
+
const samples = {};
|
|
395
|
+
for (const name of required) {
|
|
396
|
+
const property = type.getProperty(name);
|
|
397
|
+
if (!property) continue;
|
|
398
|
+
const value = sampleOf$1(checker, checker.getNonNullableType(checker.getTypeOfSymbol(property)), name);
|
|
399
|
+
if (value !== void 0) samples[name] = value;
|
|
400
|
+
}
|
|
401
|
+
return samples;
|
|
402
|
+
}
|
|
403
|
+
function sampleOf$1(checker, type, name) {
|
|
404
|
+
const parts = type.isUnion() ? type.types : [type];
|
|
405
|
+
const literal = parts.find((part) => part.isStringLiteral());
|
|
406
|
+
if (literal?.isStringLiteral()) return literal.value;
|
|
407
|
+
if (parts.some((part) => part.flags & ts.TypeFlags.String)) return /src|href|url/i.test(name) ? "#" : "Text";
|
|
408
|
+
if (parts.some((part) => part.flags & ts.TypeFlags.Number)) return 1;
|
|
409
|
+
if (parts.some((part) => part.flags & ts.TypeFlags.BooleanLike)) return true;
|
|
410
|
+
if (parts.some((part) => checker.isArrayType(part))) return [];
|
|
411
|
+
if (/ReactNode|ReactElement|Element\b/.test(checker.typeToString(type))) return "Text";
|
|
412
|
+
}
|
|
413
|
+
function hash(text) {
|
|
414
|
+
return createHash("sha1").update(text).digest("hex").slice(0, 8);
|
|
415
|
+
}
|
|
416
|
+
({ ...ts.sys });
|
|
417
|
+
ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.NonPrimitive | ts.TypeFlags.TypeParameter;
|
|
418
|
+
// ../project/src/jsonc.ts
|
|
419
|
+
/** Parses JSON with comments and trailing commas, as tsconfig files allow. Comment markers inside strings are kept. */
|
|
420
|
+
function parseJsonc(text) {
|
|
421
|
+
let output = "";
|
|
422
|
+
let index = 0;
|
|
423
|
+
while (index < text.length) {
|
|
424
|
+
const char = text[index];
|
|
425
|
+
if (char === "\"") {
|
|
426
|
+
const start = index++;
|
|
427
|
+
while (index < text.length && text[index] !== "\"") index += text[index] === "\\" ? 2 : 1;
|
|
428
|
+
output += text.slice(start, ++index);
|
|
429
|
+
} else if (char === "/" && text[index + 1] === "/") while (index < text.length && text[index] !== "\n") index++;
|
|
430
|
+
else if (char === "/" && text[index + 1] === "*") {
|
|
431
|
+
const end = text.indexOf("*/", index + 2);
|
|
432
|
+
index = end === -1 ? text.length : end + 2;
|
|
433
|
+
} else {
|
|
434
|
+
output += char;
|
|
435
|
+
index++;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return JSON.parse(output.replace(/,(\s*[}\]])/g, "$1"));
|
|
439
|
+
}
|
|
440
|
+
new Intl.Collator("en", {
|
|
441
|
+
numeric: true,
|
|
442
|
+
sensitivity: "base"
|
|
443
|
+
});
|
|
444
|
+
// ../project/src/libraries.ts
|
|
445
|
+
/**
|
|
446
|
+
* Where a dependency is installed, found the way Node finds packages: in `node_modules` here or in a folder above. It
|
|
447
|
+
* doesn't resolve `<name>/package.json`, because a package whose `exports` leaves that out can't be resolved that way.
|
|
448
|
+
*/
|
|
449
|
+
async function installedPackage(root, importName) {
|
|
450
|
+
for (let folder = resolve(root);; folder = dirname(folder)) {
|
|
451
|
+
const candidate = join(folder, "node_modules", importName);
|
|
452
|
+
if (await isFile(join(candidate, "package.json"))) return realpath(candidate);
|
|
453
|
+
if (dirname(folder) === folder) return void 0;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async function isFile(path) {
|
|
457
|
+
return stat(path).then((stats) => stats.isFile(), () => false);
|
|
458
|
+
}
|
|
459
|
+
// ../libgen/src/examples.ts
|
|
460
|
+
const run = promisify(execFile);
|
|
461
|
+
/** Example and story files: stories, demos, examples and docs' demo files, in TypeScript or JavaScript. */
|
|
462
|
+
const examplePath = /(?:\.stories\.[jt]sx?$|\.demo\.[\w.-]*\.?[jt]sx?$|-demo\.[jt]sx?$|\/(?:examples?|demos?|stories)\/.*\.[jt]sx?$)/;
|
|
463
|
+
const skipped = /(?:^|\/)(?:node_modules|dist|build|__tests__|test|tests)\//;
|
|
464
|
+
/**
|
|
465
|
+
* Downloads a library's examples at the commit its installed version was published from, so how its authors put its
|
|
466
|
+
* parts together can be read rather than guessed. Only the example files are fetched (a shallow, sparse git fetch with
|
|
467
|
+
* no other blobs), into a cache kept per commit. Without git, network or a matching commit it returns no files and
|
|
468
|
+
* says why, and generation goes on without them.
|
|
469
|
+
*/
|
|
470
|
+
async function fetchExamples(pkg, packageDir, hints = {}, cacheRoot = join(homedir(), ".parity", "cache", "examples")) {
|
|
471
|
+
const upstream = await readUpstream(packageDir);
|
|
472
|
+
const repo = normalizeRepo(hints.repo ?? upstream?.repository ?? repositoryOf(pkg));
|
|
473
|
+
if (!repo) return {
|
|
474
|
+
files: [],
|
|
475
|
+
note: "No repository is named, so no examples were read."
|
|
476
|
+
};
|
|
477
|
+
try {
|
|
478
|
+
const found = await findCommit(repo, pkg, upstream?.revision, cacheRoot);
|
|
479
|
+
if (!found) return {
|
|
480
|
+
files: [],
|
|
481
|
+
note: `No commit of ${repo} could be matched to ${pkg.name} ${pkg.version}.`
|
|
482
|
+
};
|
|
483
|
+
const checkedOut = await checkoutExamples(join(cacheRoot, `${slug(repo)}-${found.commit.slice(0, 12)}`), repo, found.commit, hints);
|
|
484
|
+
const files = checkedOut.filter((file) => !/\.mdx?$/.test(file.path));
|
|
485
|
+
const docs = checkedOut.filter((file) => /\.mdx?$/.test(file.path));
|
|
486
|
+
const empty = [
|
|
487
|
+
...hints.paths ?? [],
|
|
488
|
+
...hints.demos ?? [],
|
|
489
|
+
...hints.anatomy ?? []
|
|
490
|
+
].filter((folder) => !checkedOut.some((file) => file.path.replaceAll("\\", "/").includes(folder.replace(/\/$/, ""))));
|
|
491
|
+
return {
|
|
492
|
+
files,
|
|
493
|
+
docs,
|
|
494
|
+
source: {
|
|
495
|
+
repo,
|
|
496
|
+
commit: found.commit,
|
|
497
|
+
how: found.how,
|
|
498
|
+
files: files.length
|
|
499
|
+
},
|
|
500
|
+
...empty.length > 0 ? { note: `No examples in ${empty.join(", ")} at ${found.commit.slice(0, 10)}; the folder may have moved.` } : {}
|
|
501
|
+
};
|
|
502
|
+
} catch (error) {
|
|
503
|
+
return {
|
|
504
|
+
files: [],
|
|
505
|
+
note: `Examples weren't read from ${repo}: ${message$1(error)}`
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async function readUpstream(packageDir) {
|
|
510
|
+
try {
|
|
511
|
+
return JSON.parse(await readFile(join(packageDir, "upstream.json"), "utf8"));
|
|
512
|
+
} catch {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function repositoryOf(pkg) {
|
|
517
|
+
return typeof pkg.repository === "string" ? pkg.repository : pkg.repository?.url;
|
|
518
|
+
}
|
|
519
|
+
/** A repository URL git can fetch: `git+https://github.com/a/b.git` and `github:a/b` become `https://github.com/a/b`. */
|
|
520
|
+
function normalizeRepo(url) {
|
|
521
|
+
if (!url) return void 0;
|
|
522
|
+
const short = /^(?:github:)?([\w.-]+\/[\w.-]+)$/.exec(url);
|
|
523
|
+
if (short) return `https://github.com/${short[1].replace(/\.git$/, "")}`;
|
|
524
|
+
const https = /^(?:git\+)?(?:https?|git|ssh):\/\/(?:git@)?([^/]+)\/(.+?)(?:\.git)?\/?$/.exec(url);
|
|
525
|
+
return https ? `https://${https[1]}/${https[2]}` : void 0;
|
|
526
|
+
}
|
|
527
|
+
/** The commit the installed version was published from: recorded upstream, npm's `gitHead`, a tag, or by date. */
|
|
528
|
+
async function findCommit(repo, pkg, upstream, cacheRoot) {
|
|
529
|
+
if (upstream && /^[0-9a-f]{40}$/.test(upstream)) return {
|
|
530
|
+
commit: upstream,
|
|
531
|
+
how: "upstream"
|
|
532
|
+
};
|
|
533
|
+
const published = await npmVersion(pkg);
|
|
534
|
+
if (published?.gitHead) return {
|
|
535
|
+
commit: published.gitHead,
|
|
536
|
+
how: "gitHead"
|
|
537
|
+
};
|
|
538
|
+
const name = pkg.name ?? "";
|
|
539
|
+
const version = pkg.version ?? "";
|
|
540
|
+
const tags = /* @__PURE__ */ new Map();
|
|
541
|
+
const { stdout } = await git([
|
|
542
|
+
"ls-remote",
|
|
543
|
+
"--tags",
|
|
544
|
+
repo
|
|
545
|
+
], void 0, 6e4);
|
|
546
|
+
for (const line of stdout.split("\n")) {
|
|
547
|
+
const [sha, ref] = line.trim().split(/\s+/);
|
|
548
|
+
if (!sha || !ref) continue;
|
|
549
|
+
tags.set(ref.replace(/^refs\/tags\//, "").replace(/\^\{\}$/, ""), sha);
|
|
550
|
+
}
|
|
551
|
+
const short = name.replace(/^@[^/]+\//, "");
|
|
552
|
+
for (const tag of [
|
|
553
|
+
`v${version}`,
|
|
554
|
+
`${name}@${version}`,
|
|
555
|
+
`${short}@${version}`,
|
|
556
|
+
`${short}-v${version}`,
|
|
557
|
+
version
|
|
558
|
+
]) {
|
|
559
|
+
const sha = tags.get(tag);
|
|
560
|
+
if (sha) return {
|
|
561
|
+
commit: sha,
|
|
562
|
+
how: "tag"
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
if (!published?.time) return void 0;
|
|
566
|
+
const dir = join(cacheRoot, `${slug(repo)}-history`);
|
|
567
|
+
await mkdir(dir, { recursive: true });
|
|
568
|
+
if (!existsSync(join(dir, ".git"))) await git(["init", "-q"], dir);
|
|
569
|
+
await git([
|
|
570
|
+
"fetch",
|
|
571
|
+
"-q",
|
|
572
|
+
"--filter=tree:0",
|
|
573
|
+
`--shallow-since=${(/* @__PURE__ */ new Date(Date.parse(published.time) - 2592e5)).toISOString()}`,
|
|
574
|
+
repo,
|
|
575
|
+
"HEAD"
|
|
576
|
+
], dir, 3e5);
|
|
577
|
+
const { stdout: commit } = await git([
|
|
578
|
+
"rev-list",
|
|
579
|
+
"-1",
|
|
580
|
+
`--before=${published.time}`,
|
|
581
|
+
"FETCH_HEAD"
|
|
582
|
+
], dir);
|
|
583
|
+
return commit.trim() ? {
|
|
584
|
+
commit: commit.trim(),
|
|
585
|
+
how: "date"
|
|
586
|
+
} : void 0;
|
|
587
|
+
}
|
|
588
|
+
async function npmVersion(pkg) {
|
|
589
|
+
if (!pkg.name || !pkg.version) return void 0;
|
|
590
|
+
try {
|
|
591
|
+
const response = await fetch(`https://registry.npmjs.org/${pkg.name.replace("/", "%2F")}`, {
|
|
592
|
+
headers: { accept: "application/json" },
|
|
593
|
+
signal: AbortSignal.timeout(3e4)
|
|
594
|
+
});
|
|
595
|
+
if (!response.ok) return void 0;
|
|
596
|
+
const packument = await response.json();
|
|
597
|
+
const gitHead = packument.versions?.[pkg.version]?.gitHead;
|
|
598
|
+
return {
|
|
599
|
+
...gitHead ? { gitHead } : {},
|
|
600
|
+
...packument.time?.[pkg.version] ? { time: packument.time[pkg.version] } : {}
|
|
601
|
+
};
|
|
602
|
+
} catch {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Checks out only the example files of one commit, once: a later run reads them from the cache, unless the folders
|
|
608
|
+
* looked in have changed since.
|
|
609
|
+
*/
|
|
610
|
+
async function checkoutExamples(dir, repo, commit, { paths, demos = [], anatomy = [] }) {
|
|
611
|
+
const done = join(dir, ".parity-examples");
|
|
612
|
+
const key = [
|
|
613
|
+
commit,
|
|
614
|
+
"imports",
|
|
615
|
+
...paths ?? [],
|
|
616
|
+
...demos.map((demo) => `demos:${demo}`),
|
|
617
|
+
...anatomy.map((folder) => `anatomy:${folder}`)
|
|
618
|
+
].join("\n");
|
|
619
|
+
if (await readFile(done, "utf8").catch(() => void 0) !== key) {
|
|
620
|
+
await rm(dir, {
|
|
621
|
+
recursive: true,
|
|
622
|
+
force: true
|
|
623
|
+
});
|
|
624
|
+
await mkdir(dir, { recursive: true });
|
|
625
|
+
await git(["init", "-q"], dir);
|
|
626
|
+
await git([
|
|
627
|
+
"remote",
|
|
628
|
+
"add",
|
|
629
|
+
"origin",
|
|
630
|
+
repo
|
|
631
|
+
], dir);
|
|
632
|
+
await git([
|
|
633
|
+
"fetch",
|
|
634
|
+
"-q",
|
|
635
|
+
"--depth",
|
|
636
|
+
"1",
|
|
637
|
+
"--filter=blob:none",
|
|
638
|
+
"origin",
|
|
639
|
+
commit
|
|
640
|
+
], dir, 3e5);
|
|
641
|
+
const { stdout } = await git([
|
|
642
|
+
"ls-tree",
|
|
643
|
+
"-r",
|
|
644
|
+
"--name-only",
|
|
645
|
+
"FETCH_HEAD"
|
|
646
|
+
], dir, 12e4);
|
|
647
|
+
const wanted = stdout.split("\n").filter((path) => path && !skipped.test(path)).filter((path) => examplePath.test(path) && (!paths || paths.some((prefix) => path.startsWith(prefix))) || /\.[jt]sx$/.test(path) && demos.some((prefix) => path.startsWith(prefix)) || /\.mdx?$/.test(path) && anatomy.some((prefix) => path.startsWith(prefix))).slice(0, 5e3);
|
|
648
|
+
const checkout = async (paths) => {
|
|
649
|
+
await git([
|
|
650
|
+
"sparse-checkout",
|
|
651
|
+
"set",
|
|
652
|
+
"--no-cone",
|
|
653
|
+
"--stdin"
|
|
654
|
+
], dir, 6e4, paths.map((path) => `/${path}`).join("\n"));
|
|
655
|
+
await git([
|
|
656
|
+
"checkout",
|
|
657
|
+
"-q",
|
|
658
|
+
"FETCH_HEAD"
|
|
659
|
+
], dir, 6e5);
|
|
660
|
+
};
|
|
661
|
+
if (wanted.length > 0) {
|
|
662
|
+
await checkout(wanted);
|
|
663
|
+
const tree = stdout.split("\n").filter((path) => /\.[jt]sx?$/.test(path) && !skipped.test(path));
|
|
664
|
+
let scan = [...wanted];
|
|
665
|
+
for (let added = 0; scan.length > 0 && added < 500;) {
|
|
666
|
+
const texts = await Promise.all(scan.map((path) => readFile(join(dir, path), "utf8").catch(() => "")));
|
|
667
|
+
const imported = localImports(scan, texts, tree).filter((path) => !wanted.includes(path)).slice(0, 500 - added);
|
|
668
|
+
if (imported.length === 0) break;
|
|
669
|
+
wanted.push(...imported);
|
|
670
|
+
added += imported.length;
|
|
671
|
+
await checkout(wanted);
|
|
672
|
+
scan = imported;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
await writeFile(join(dir, ".parity-wanted"), wanted.join("\n"));
|
|
676
|
+
await writeFile(done, key);
|
|
677
|
+
}
|
|
678
|
+
const wanted = (await readFile(join(dir, ".parity-wanted"), "utf8")).split("\n").filter(Boolean);
|
|
679
|
+
const files = [];
|
|
680
|
+
for (const path of wanted) {
|
|
681
|
+
const text = await readFile(join(dir, path), "utf8").catch(() => void 0);
|
|
682
|
+
if (text !== void 0) files.push({
|
|
683
|
+
path,
|
|
684
|
+
text
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
return files;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* The files of a repository's tree that its example files import, relatively (`./utils`) or by a path the repository
|
|
691
|
+
* maps (`compositions/ui/toaster`, matched by the end of a file's path), but not packages (`react`, `@chakra-ui/react`).
|
|
692
|
+
*/
|
|
693
|
+
function localImports(paths, texts, tree) {
|
|
694
|
+
const local = localResolver(tree);
|
|
695
|
+
const found = /* @__PURE__ */ new Set();
|
|
696
|
+
paths.forEach((path, index) => {
|
|
697
|
+
for (const match of texts[index].matchAll(/\bfrom\s+["']([^"']+)["']/g)) {
|
|
698
|
+
const target = local(path, match[1]);
|
|
699
|
+
if (target) found.add(target);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
return [...found];
|
|
703
|
+
}
|
|
704
|
+
/** The file of a tree an import names, relatively or by a mapped path, or none for a package. */
|
|
705
|
+
function localResolver(tree) {
|
|
706
|
+
const byStem = /* @__PURE__ */ new Map();
|
|
707
|
+
for (const path of tree) byStem.set(path.replaceAll("\\", "/").replace(/(?:\/index)?\.[jt]sx?$/, ""), path);
|
|
708
|
+
const bySuffix = /* @__PURE__ */ new Map();
|
|
709
|
+
for (const [stemmed, target] of byStem) {
|
|
710
|
+
const candidate = stemmed.replace(/\/src(?=\/)/g, "");
|
|
711
|
+
for (let at = -1; at < candidate.length; at = candidate.indexOf("/", at + 1)) {
|
|
712
|
+
const suffix = candidate.slice(at + 1);
|
|
713
|
+
if (!bySuffix.has(suffix)) bySuffix.set(suffix, target);
|
|
714
|
+
if (!candidate.includes("/", at + 1)) break;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return (from, specifier) => {
|
|
718
|
+
if (specifier.startsWith(".")) return byStem.get(posix.join(posix.dirname(from.replaceAll("\\", "/")), specifier));
|
|
719
|
+
if (!specifier.startsWith("@/") && (specifier.startsWith("@") ? specifier.split("/").length < 3 : !specifier.includes("/"))) return void 0;
|
|
720
|
+
return bySuffix.get(specifier.replace(/^[@~]\//, ""));
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* The methods example files call on a value another file makes and exports (Chakra's `toaster.create({ title, type })`
|
|
725
|
+
* on `compositions/ui/toaster`'s `toaster`), by that file and the name it exports, with arguments the caller spells out:
|
|
726
|
+
* for each method, its fullest call.
|
|
727
|
+
*/
|
|
728
|
+
function methodCallsOn(files, sourceOf, pkg, reexported) {
|
|
729
|
+
const local = localResolver(files.map((file) => file.path));
|
|
730
|
+
const found = /* @__PURE__ */ new Map();
|
|
731
|
+
for (const file of files) {
|
|
732
|
+
if (!/\.\w+\(/.test(file.text) || !/\bfrom\s+["'](?:\.|[^"'@]+\/|@\/)/.test(file.text)) continue;
|
|
733
|
+
const source = sourceOf(file);
|
|
734
|
+
const imported = /* @__PURE__ */ new Map();
|
|
735
|
+
for (const statement of source.statements) {
|
|
736
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
737
|
+
const target = local(file.path, statement.moduleSpecifier.text);
|
|
738
|
+
const bindings = statement.importClause?.namedBindings;
|
|
739
|
+
if (!target || !bindings || !ts.isNamedImports(bindings)) continue;
|
|
740
|
+
for (const element of bindings.elements) imported.set(element.name.text, {
|
|
741
|
+
file: target,
|
|
742
|
+
name: (element.propertyName ?? element.name).text
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
if (imported.size === 0) continue;
|
|
746
|
+
const context = {
|
|
747
|
+
source,
|
|
748
|
+
resolve: () => void 0,
|
|
749
|
+
scope: declarations(source),
|
|
750
|
+
env: /* @__PURE__ */ new Map(),
|
|
751
|
+
...pkg ? { imports: packageImports(source, pkg, reexported) } : {}
|
|
752
|
+
};
|
|
753
|
+
const visit = (node) => {
|
|
754
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression)) {
|
|
755
|
+
const on = imported.get(node.expression.expression.text);
|
|
756
|
+
const args = node.arguments.map((argument) => evaluate(argument, context, 0));
|
|
757
|
+
if (on && args.length > 0 && !args.some((argument) => argument === void 0)) {
|
|
758
|
+
const method = node.expression.name.text;
|
|
759
|
+
const byName = found.get(on.file) ?? /* @__PURE__ */ new Map();
|
|
760
|
+
const calls = byName.get(on.name) ?? /* @__PURE__ */ new Map();
|
|
761
|
+
const call = {
|
|
762
|
+
method,
|
|
763
|
+
...args.length > 0 ? { args } : {}
|
|
764
|
+
};
|
|
765
|
+
const before = calls.get(method);
|
|
766
|
+
if (!before || JSON.stringify(call).length > JSON.stringify(before).length) calls.set(method, call);
|
|
767
|
+
byName.set(on.name, calls);
|
|
768
|
+
found.set(on.file, byName);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
ts.forEachChild(node, visit);
|
|
772
|
+
};
|
|
773
|
+
visit(source);
|
|
774
|
+
}
|
|
775
|
+
return new Map([...found].map(([path, byName]) => [path, new Map([...byName].map(([name, calls]) => [name, [...calls.values()].slice(0, 3)]))]));
|
|
776
|
+
}
|
|
777
|
+
async function git(args, cwd, timeout = 6e4, input) {
|
|
778
|
+
const child = run("git", args, {
|
|
779
|
+
cwd,
|
|
780
|
+
timeout,
|
|
781
|
+
maxBuffer: 268435456,
|
|
782
|
+
windowsHide: true
|
|
783
|
+
});
|
|
784
|
+
if (input !== void 0) {
|
|
785
|
+
child.child.stdin?.write(input);
|
|
786
|
+
child.child.stdin?.end();
|
|
787
|
+
}
|
|
788
|
+
const { stdout } = await child;
|
|
789
|
+
return { stdout: String(stdout) };
|
|
790
|
+
}
|
|
791
|
+
function slug(repo) {
|
|
792
|
+
return repo.replace(/^https?:\/\//, "").replace(/[^\w.-]+/g, "-");
|
|
793
|
+
}
|
|
794
|
+
function message$1(error) {
|
|
795
|
+
return (error instanceof Error ? error.message : String(error)).split("\n")[0].slice(0, 200);
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Reads how a library's components are used in its examples: each element whose tag is one of its components (as
|
|
799
|
+
* written, `Dialog.Content`, or with the dot left out, `Menu.Item` for `MenuItem`), every such element around it, and
|
|
800
|
+
* its props and children. Props are worked out where the file says what they are: literals, names it declares
|
|
801
|
+
* (`config={chartConfig}`), `useState`'s first value, and the items a list maps into elements.
|
|
802
|
+
*/
|
|
803
|
+
function readExamples(files, names, pkg, reexported = /* @__PURE__ */ new Set()) {
|
|
804
|
+
const known = new Set(names);
|
|
805
|
+
const flat = new Map(names.map((name) => [name.replaceAll(".", ""), name]));
|
|
806
|
+
const resolve = (tag) => known.has(tag) ? tag : flat.get(tag.replaceAll(".", ""));
|
|
807
|
+
const uses = /* @__PURE__ */ new Map();
|
|
808
|
+
const sources = /* @__PURE__ */ new Map();
|
|
809
|
+
/** How many uses of each part set each prop. */
|
|
810
|
+
const setBy = /* @__PURE__ */ new Map();
|
|
811
|
+
let kin = /* @__PURE__ */ new Map();
|
|
812
|
+
const sourceOf = (file) => {
|
|
813
|
+
const source = sources.get(file) ?? ts.createSourceFile(file.path, file.text, ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX);
|
|
814
|
+
sources.set(file, source);
|
|
815
|
+
return source;
|
|
816
|
+
};
|
|
817
|
+
const imported = importedWrappers(files, sourceOf, resolve);
|
|
818
|
+
const calledOn = methodCallsOn(files, sourceOf, pkg, reexported);
|
|
819
|
+
for (const sampling of [false, true]) {
|
|
820
|
+
if (sampling) kin = kinOf(uses);
|
|
821
|
+
const family = (name) => kin.get(name) ?? familyOf(name);
|
|
822
|
+
for (const file of files) {
|
|
823
|
+
if (!sampling && !names.some((name) => file.text.includes(name.split(".").at(-1)))) continue;
|
|
824
|
+
if (sampling && !sources.has(file)) continue;
|
|
825
|
+
const source = sourceOf(file);
|
|
826
|
+
readSource(source, uses, resolve, sampling ? family : void 0, setBy, pkg, imported(source), reexported, calledOn.get(file.path));
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
for (const [name, family] of kin) {
|
|
830
|
+
const use = uses.get(name);
|
|
831
|
+
if (use) use.family = family;
|
|
832
|
+
}
|
|
833
|
+
for (const use of uses.values()) {
|
|
834
|
+
if (!use.made || !use.sample) continue;
|
|
835
|
+
const missing = Object.entries(use.made).filter(([prop]) => !(prop in (use.sample.props ?? {})));
|
|
836
|
+
if (missing.length > 0) use.sample.props = {
|
|
837
|
+
...Object.fromEntries(missing),
|
|
838
|
+
...use.sample.props
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
for (const [name, use] of uses) {
|
|
842
|
+
const props = use.sample?.props;
|
|
843
|
+
if (!props || use.count < 5) continue;
|
|
844
|
+
for (const prop of Object.keys(props)) {
|
|
845
|
+
if (madeIn(props[prop])) continue;
|
|
846
|
+
const set = setBy.get(name)?.get(prop) ?? 0;
|
|
847
|
+
if (set >= 2 && set * 4 >= use.usePaths.length) continue;
|
|
848
|
+
use.rare = {
|
|
849
|
+
...use.rare,
|
|
850
|
+
[prop]: props[prop]
|
|
851
|
+
};
|
|
852
|
+
delete props[prop];
|
|
853
|
+
}
|
|
854
|
+
if (Object.keys(props).length === 0) delete use.sample.props;
|
|
855
|
+
}
|
|
856
|
+
return uses;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Reads one file's uses: where each part is placed, or with `family`, only the samples, each keeping its own family's
|
|
860
|
+
* parts.
|
|
861
|
+
*/
|
|
862
|
+
function readSource(source, uses, base, family, setBy, pkg, imported = /* @__PURE__ */ new Map(), reexported = /* @__PURE__ */ new Set(), calledOn) {
|
|
863
|
+
const aliases = pkg ? importAliases(source, pkg) : /* @__PURE__ */ new Map();
|
|
864
|
+
const own = base;
|
|
865
|
+
base = (tag) => own(aliases.get(tag) ?? tag);
|
|
866
|
+
const wrapping = new Map([...imported, ...wrappers(source, base)]);
|
|
867
|
+
const resolve = (tag) => base(tag) ?? wrapping.get(tag)?.name;
|
|
868
|
+
const wrapped = new Set([...wrapping.values()].map((wrapper) => wrapper.root));
|
|
869
|
+
const context = {
|
|
870
|
+
source,
|
|
871
|
+
resolve,
|
|
872
|
+
scope: declarations(source),
|
|
873
|
+
env: /* @__PURE__ */ new Map(),
|
|
874
|
+
wrapping,
|
|
875
|
+
...pkg ? { imports: packageImports(source, pkg, reexported) } : {},
|
|
876
|
+
...calledOn ? { calledOn } : {}
|
|
877
|
+
};
|
|
878
|
+
const placedHere = /* @__PURE__ */ new Set();
|
|
879
|
+
const hooked = hookResults(source);
|
|
880
|
+
const drawnFromHook = mappedComponents(source, hooked, base);
|
|
881
|
+
context.hooked = hooked;
|
|
882
|
+
context.hookItem = hookItem(source, hooked, context);
|
|
883
|
+
const drawnWith = drawnProps(source, drawnFromHook, context);
|
|
884
|
+
const visit = (node, around, mapped, env) => {
|
|
885
|
+
let here = around;
|
|
886
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
887
|
+
const tag = (ts.isJsxElement(node) ? node.openingElement : node).tagName.getText(source);
|
|
888
|
+
const name = resolve(tag);
|
|
889
|
+
if (name) {
|
|
890
|
+
const use = uses.get(name) ?? {
|
|
891
|
+
count: 0,
|
|
892
|
+
topLevel: 0,
|
|
893
|
+
parents: /* @__PURE__ */ new Map(),
|
|
894
|
+
paths: [],
|
|
895
|
+
usePaths: [],
|
|
896
|
+
sampleSize: -1,
|
|
897
|
+
fromHook: true
|
|
898
|
+
};
|
|
899
|
+
if (!family && !wrapped.has(node)) {
|
|
900
|
+
if (!mapped) use.fromHook = false;
|
|
901
|
+
const path = [...new Set(around)].filter((parent) => parent !== name);
|
|
902
|
+
if (use.usePaths.length < 300) use.usePaths.push(path);
|
|
903
|
+
const key = `${name}<${path.join("<")}`;
|
|
904
|
+
if (!placedHere.has(key)) {
|
|
905
|
+
placedHere.add(key);
|
|
906
|
+
use.count++;
|
|
907
|
+
const nearest = path[0];
|
|
908
|
+
if (nearest) use.parents.set(nearest, (use.parents.get(nearest) ?? 0) + 1);
|
|
909
|
+
else use.topLevel++;
|
|
910
|
+
if (use.paths.length < 300) use.paths.push(path);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
if (family) {
|
|
914
|
+
const sample = familySample(usedSample(node, {
|
|
915
|
+
...context,
|
|
916
|
+
env
|
|
917
|
+
}, 0), family(name), family);
|
|
918
|
+
const counts = setBy?.get(name) ?? /* @__PURE__ */ new Map();
|
|
919
|
+
for (const [prop, value] of Object.entries(sample.props ?? {})) {
|
|
920
|
+
counts.set(prop, (counts.get(prop) ?? 0) + 1);
|
|
921
|
+
if (madeIn(value) && !(prop in (use.made ?? {}))) use.made = {
|
|
922
|
+
...use.made,
|
|
923
|
+
[prop]: value
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
setBy?.set(name, counts);
|
|
927
|
+
const size = sampleWeight(sample);
|
|
928
|
+
if (size > use.sampleSize) {
|
|
929
|
+
use.sample = sample;
|
|
930
|
+
use.sampleSize = size;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
uses.set(name, use);
|
|
934
|
+
here = [name, ...around];
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
const declared = declaredName(node);
|
|
938
|
+
const inList = mapped || hookMapped(node, hooked, source) || drawnFromHook.has(declared ?? "");
|
|
939
|
+
let inner = env;
|
|
940
|
+
const callback = mapCallback(node);
|
|
941
|
+
const mappedList = callback && node.expression.expression;
|
|
942
|
+
const listed = mappedList ? listOf(mappedList, {
|
|
943
|
+
...context,
|
|
944
|
+
env
|
|
945
|
+
}) : void 0;
|
|
946
|
+
if (callback && Array.isArray(listed) && listed[0] !== void 0) {
|
|
947
|
+
inner = new Map(env);
|
|
948
|
+
bind(callback.parameters[0]?.name, listed[0], inner);
|
|
949
|
+
} else if (callback && hookMapped(node, hooked, source) && context.hookItem) {
|
|
950
|
+
inner = new Map(env);
|
|
951
|
+
bind(callback.parameters[0]?.name, itemFor(callback, context.hookItem, source), inner);
|
|
952
|
+
} else if (declared && drawnWith.has(declared)) {
|
|
953
|
+
inner = new Map(env);
|
|
954
|
+
bind(firstParameter(node), drawnWith.get(declared), inner);
|
|
955
|
+
}
|
|
956
|
+
ts.forEachChild(node, (child) => {
|
|
957
|
+
if (!ts.isJsxAttributes(child)) visit(child, here, inList, inner);
|
|
958
|
+
else ts.forEachChild(child, (attribute) => visit(attribute, rendersInside(attribute) ? here : [], inList, inner));
|
|
959
|
+
});
|
|
960
|
+
};
|
|
961
|
+
visit(source, [], false, /* @__PURE__ */ new Map());
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* The family each part joins beyond its name's: one the examples nearly always place, and place directly inside one
|
|
965
|
+
* family, belongs to it (`Row` and `Cell` to `Table`), so a table's sample keeps its rows and columns. Not when its own
|
|
966
|
+
* family is usually around it too, or into a layout box that holds every family.
|
|
967
|
+
*/
|
|
968
|
+
function kinOf(uses) {
|
|
969
|
+
const kin = /* @__PURE__ */ new Map();
|
|
970
|
+
const holds = /* @__PURE__ */ new Map();
|
|
971
|
+
for (const [name, use] of uses) for (const parent of use.parents.keys()) {
|
|
972
|
+
if (familyOf(parent) === familyOf(name)) continue;
|
|
973
|
+
holds.set(familyOf(parent), (holds.get(familyOf(parent)) ?? /* @__PURE__ */ new Set()).add(familyOf(name)));
|
|
974
|
+
}
|
|
975
|
+
const generic = (family) => (holds.get(family)?.size ?? 0) > 8;
|
|
976
|
+
for (let round = 0; round < 4; round++) {
|
|
977
|
+
let changed = false;
|
|
978
|
+
for (const [name, use] of uses) {
|
|
979
|
+
if (use.count < 2) continue;
|
|
980
|
+
if (use.paths.filter((path) => path.some((parent) => familyOf(parent) === familyOf(name))).length * 2 >= use.paths.length) continue;
|
|
981
|
+
const families = /* @__PURE__ */ new Map();
|
|
982
|
+
let placed = 0;
|
|
983
|
+
for (const [parent, count] of use.parents) {
|
|
984
|
+
const family = kin.get(parent) ?? familyOf(parent);
|
|
985
|
+
families.set(family, (families.get(family) ?? 0) + count);
|
|
986
|
+
placed += count;
|
|
987
|
+
}
|
|
988
|
+
const [family, count] = [...families].sort((a, b) => b[1] - a[1])[0] ?? [];
|
|
989
|
+
if (!family || !count || count < placed * .75 || family === familyOf(name) || kin.get(name) === family) continue;
|
|
990
|
+
if (!name.toLowerCase().includes(family.toLowerCase()) && (use.topLevel * 10 > use.count || generic(family))) continue;
|
|
991
|
+
kin.set(name, family);
|
|
992
|
+
changed = true;
|
|
993
|
+
}
|
|
994
|
+
if (!changed) break;
|
|
995
|
+
}
|
|
996
|
+
return kin;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* The wrappers a file imports from another example file beside it (React Aria's stories take `MyMenuItem` from
|
|
1000
|
+
* `./utils`). Only ones that pass their props on count: a wrapper's own element is sampled from its file, not this one.
|
|
1001
|
+
*/
|
|
1002
|
+
function importedWrappers(files, sourceOf, resolve) {
|
|
1003
|
+
const byPath = new Map(files.map((file) => [file.path.replaceAll("\\", "/"), file]));
|
|
1004
|
+
const declared = /* @__PURE__ */ new Map();
|
|
1005
|
+
return (source) => {
|
|
1006
|
+
const found = /* @__PURE__ */ new Map();
|
|
1007
|
+
for (const statement of source.statements) {
|
|
1008
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
1009
|
+
const specifier = statement.moduleSpecifier.text;
|
|
1010
|
+
const bindings = statement.importClause?.namedBindings;
|
|
1011
|
+
if (!specifier.startsWith(".") || !bindings || !ts.isNamedImports(bindings)) continue;
|
|
1012
|
+
const base = posix.join(posix.dirname(source.fileName.replaceAll("\\", "/")), specifier);
|
|
1013
|
+
const file = [
|
|
1014
|
+
"",
|
|
1015
|
+
".tsx",
|
|
1016
|
+
".ts",
|
|
1017
|
+
".jsx",
|
|
1018
|
+
".js",
|
|
1019
|
+
"/index.tsx",
|
|
1020
|
+
"/index.ts"
|
|
1021
|
+
].map((extension) => byPath.get(base + extension)).find((item) => item !== void 0);
|
|
1022
|
+
if (!file) continue;
|
|
1023
|
+
let theirs = declared.get(file);
|
|
1024
|
+
if (!theirs) {
|
|
1025
|
+
theirs = wrappers(sourceOf(file), resolve, false);
|
|
1026
|
+
declared.set(file, theirs);
|
|
1027
|
+
}
|
|
1028
|
+
for (const element of bindings.elements) {
|
|
1029
|
+
const wrapper = theirs.get((element.propertyName ?? element.name).text);
|
|
1030
|
+
if (wrapper?.spread) found.set(element.name.text, wrapper);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return found;
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* The example's own components that return one of the library's parts and no other element (`const CustomTab = (props)
|
|
1038
|
+
* => <Tab {...props} />`), each with the part, its element, and whether it hands its props on. With `usedHere`, only
|
|
1039
|
+
* those the file places, as against ones it declares for other files.
|
|
1040
|
+
*/
|
|
1041
|
+
function wrappers(source, resolve, usedHere = true) {
|
|
1042
|
+
const found = /* @__PURE__ */ new Map();
|
|
1043
|
+
const tags = /* @__PURE__ */ new Set();
|
|
1044
|
+
const visit = (node) => {
|
|
1045
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) tags.add(node.tagName.getText(source));
|
|
1046
|
+
const declared = declaredName(node);
|
|
1047
|
+
if (declared && !resolve(declared)) {
|
|
1048
|
+
let fn = ts.isVariableDeclaration(node) ? node.initializer && unwrap$1(node.initializer) : node;
|
|
1049
|
+
if (fn && ts.isCallExpression(fn)) fn = fn.arguments.find((argument) => ts.isFunctionLike(argument));
|
|
1050
|
+
const body = fn && (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn) || ts.isFunctionDeclaration(fn)) ? returned(fn) : void 0;
|
|
1051
|
+
const elements = body && ts.isJsxFragment(body) ? body.children.filter((child) => ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) : [];
|
|
1052
|
+
const root = elements.length === 1 ? elements[0] : body;
|
|
1053
|
+
const opening = root && openingOf(root);
|
|
1054
|
+
const name = opening && resolve(opening.tagName.getText(source));
|
|
1055
|
+
if (root && opening && name) {
|
|
1056
|
+
const spread = opening.attributes.properties.some(ts.isJsxSpreadAttribute);
|
|
1057
|
+
found.set(declared, {
|
|
1058
|
+
name,
|
|
1059
|
+
root,
|
|
1060
|
+
spread
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
ts.forEachChild(node, visit);
|
|
1065
|
+
};
|
|
1066
|
+
visit(source);
|
|
1067
|
+
if (usedHere) {
|
|
1068
|
+
for (const declared of found.keys()) if (!tags.has(declared)) found.delete(declared);
|
|
1069
|
+
}
|
|
1070
|
+
return found;
|
|
1071
|
+
}
|
|
1072
|
+
/** The names a file imports from a package or its subpaths (`@base-ui/react/toast`), with what each is there. */
|
|
1073
|
+
/** Whether a prop's value is one the package makes, or a list holding one. */
|
|
1074
|
+
function madeIn(value) {
|
|
1075
|
+
return isMadeValue(value) || Array.isArray(value) && value.some(isMadeValue);
|
|
1076
|
+
}
|
|
1077
|
+
/** The library's parts a file imports under names of its own, by those names. */
|
|
1078
|
+
function importAliases(source, pkg) {
|
|
1079
|
+
const found = /* @__PURE__ */ new Map();
|
|
1080
|
+
for (const statement of source.statements) {
|
|
1081
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
1082
|
+
const from = statement.moduleSpecifier.text;
|
|
1083
|
+
const bindings = statement.importClause?.namedBindings;
|
|
1084
|
+
if (from !== pkg && !from.startsWith(`${pkg}/`) || !bindings || !ts.isNamedImports(bindings)) continue;
|
|
1085
|
+
for (const element of bindings.elements) if (element.propertyName) found.set(element.name.text, element.propertyName.text);
|
|
1086
|
+
}
|
|
1087
|
+
return found;
|
|
1088
|
+
}
|
|
1089
|
+
function packageImports(source, pkg, reexported = /* @__PURE__ */ new Set()) {
|
|
1090
|
+
const found = /* @__PURE__ */ new Map();
|
|
1091
|
+
for (const statement of source.statements) {
|
|
1092
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
1093
|
+
const from = statement.moduleSpecifier.text;
|
|
1094
|
+
const own = from === pkg || from.startsWith(`${pkg}/`);
|
|
1095
|
+
const bindings = statement.importClause?.namedBindings;
|
|
1096
|
+
if (from.startsWith(".") || !bindings || !ts.isNamedImports(bindings)) continue;
|
|
1097
|
+
for (const element of bindings.elements) {
|
|
1098
|
+
const name = (element.propertyName ?? element.name).text;
|
|
1099
|
+
if (own) found.set(element.name.text, {
|
|
1100
|
+
import: from,
|
|
1101
|
+
export: name
|
|
1102
|
+
});
|
|
1103
|
+
else if (reexported.has(name)) found.set(element.name.text, {
|
|
1104
|
+
import: pkg,
|
|
1105
|
+
export: name
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return found;
|
|
1110
|
+
}
|
|
1111
|
+
function sampleOf(node, context, depth) {
|
|
1112
|
+
const opening = ts.isJsxElement(node) ? node.openingElement : node;
|
|
1113
|
+
const props = {};
|
|
1114
|
+
const skipped = (name) => name === "key" || name === "className" || name === "style" || name.startsWith("on");
|
|
1115
|
+
for (const attribute of opening.attributes.properties) {
|
|
1116
|
+
if (ts.isJsxSpreadAttribute(attribute)) {
|
|
1117
|
+
const spread = evaluate(attribute.expression, context, 0);
|
|
1118
|
+
if (typeof spread !== "object" || spread === null || Array.isArray(spread)) continue;
|
|
1119
|
+
for (const [name, value] of Object.entries(spread)) if (!skipped(name)) props[name] = value;
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
if (!ts.isJsxAttribute(attribute)) continue;
|
|
1123
|
+
const name = attribute.name.getText(context.source);
|
|
1124
|
+
if (skipped(name)) continue;
|
|
1125
|
+
const value = attributeValue(attribute.initializer, context);
|
|
1126
|
+
if (value !== void 0) props[name] = value;
|
|
1127
|
+
}
|
|
1128
|
+
const children = [];
|
|
1129
|
+
if (ts.isJsxElement(node) && depth < 4) collectChildren(node.children, context, depth, children);
|
|
1130
|
+
return {
|
|
1131
|
+
...Object.keys(props).length > 0 ? { props } : {},
|
|
1132
|
+
...children.length > 0 ? { children } : {}
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* An element's sample. The example's own wrapper that doesn't hand its props on (`<Item title="Docs">`) shows its own
|
|
1137
|
+
* element's props, and the children it's given when that element has none of its own.
|
|
1138
|
+
*/
|
|
1139
|
+
function usedSample(element, context, depth) {
|
|
1140
|
+
const tag = (ts.isJsxElement(element) ? element.openingElement : element).tagName.getText(context.source);
|
|
1141
|
+
const wrapper = context.wrapping?.get(tag);
|
|
1142
|
+
const here = sampleOf(element, context, depth);
|
|
1143
|
+
if (!wrapper || wrapper.spread) return here;
|
|
1144
|
+
const own = sampleOf(wrapper.root, context, depth);
|
|
1145
|
+
return {
|
|
1146
|
+
...own.props ? { props: own.props } : {},
|
|
1147
|
+
...own.children ?? here.children ? { children: own.children ?? here.children } : {}
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
function collectChildren(nodes, context, depth, children) {
|
|
1151
|
+
for (const child of nodes) {
|
|
1152
|
+
if (children.length >= 4) return;
|
|
1153
|
+
if (ts.isJsxText(child)) {
|
|
1154
|
+
const text = child.text.replace(/\s+/g, " ").trim();
|
|
1155
|
+
if (text) children.push(text.slice(0, 60));
|
|
1156
|
+
} else if (ts.isJsxFragment(child)) collectChildren(child.children, context, depth, children);
|
|
1157
|
+
else if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) pushElement(child, context, depth, children);
|
|
1158
|
+
else if (ts.isJsxExpression(child) && child.expression) pushExpression(unwrap$1(child.expression), context, depth, children);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
function pushElement(element, context, depth, children) {
|
|
1162
|
+
const tag = (ts.isJsxElement(element) ? element.openingElement : element).tagName.getText(context.source);
|
|
1163
|
+
const name = context.resolve(tag);
|
|
1164
|
+
if (name) {
|
|
1165
|
+
const sample = usedSample(element, context, depth + 1);
|
|
1166
|
+
const filled = ts.isJsxElement(element) && element.children.some((child) => ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child));
|
|
1167
|
+
children.push({
|
|
1168
|
+
component: name,
|
|
1169
|
+
...sample,
|
|
1170
|
+
...filled && !sample.children ? { children: [] } : {}
|
|
1171
|
+
});
|
|
1172
|
+
} else {
|
|
1173
|
+
const text = textIn(element);
|
|
1174
|
+
if (text) children.push(text);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
/** A `{...}` child: a list mapped into elements (a few of its items), a condition's element, or text. */
|
|
1178
|
+
function pushExpression(expression, context, depth, children) {
|
|
1179
|
+
if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
|
|
1180
|
+
const render = renderOf(expression, context, depth);
|
|
1181
|
+
if (render) children.push(render);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
if (ts.isJsxElement(expression) || ts.isJsxSelfClosingElement(expression)) {
|
|
1185
|
+
pushElement(expression, context, depth, children);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (ts.isJsxFragment(expression)) {
|
|
1189
|
+
collectChildren(expression.children, context, depth, children);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
1193
|
+
pushExpression(unwrap$1(expression.right), context, depth, children);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
if (ts.isConditionalExpression(expression)) {
|
|
1197
|
+
pushExpression(unwrap$1(expression.whenTrue), context, depth, children);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression) && expression.expression.name.text === "map") {
|
|
1201
|
+
const callback = expression.arguments[0];
|
|
1202
|
+
if (!callback || !(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) return;
|
|
1203
|
+
const body = returned(callback);
|
|
1204
|
+
if (!body) return;
|
|
1205
|
+
const list = listOf(expression.expression.expression, context);
|
|
1206
|
+
let items;
|
|
1207
|
+
if (Array.isArray(list)) items = list.slice(0, 3);
|
|
1208
|
+
else if (context.hookItem && context.hooked && hookMapped(expression, context.hooked, context.source)) items = [itemFor(callback, context.hookItem, context.source)];
|
|
1209
|
+
else items = [void 0];
|
|
1210
|
+
items.forEach((item, index) => {
|
|
1211
|
+
const env = new Map(context.env);
|
|
1212
|
+
if (item !== void 0) bind(callback.parameters[0]?.name, item, env);
|
|
1213
|
+
const second = callback.parameters[1]?.name;
|
|
1214
|
+
if (second && ts.isIdentifier(second)) env.set(second.text, index);
|
|
1215
|
+
pushExpression(body, {
|
|
1216
|
+
...context,
|
|
1217
|
+
env
|
|
1218
|
+
}, depth, children);
|
|
1219
|
+
});
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
const value = evaluate(expression, context, 0);
|
|
1223
|
+
if (typeof value === "string" || typeof value === "number") children.push(String(value).slice(0, 60));
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* A function that returns elements, as a sample's render function: what it returns, and whether it spreads its first
|
|
1227
|
+
* parameter onto them (`(params) => <TextField {...params} />`).
|
|
1228
|
+
*/
|
|
1229
|
+
function renderOf(callback, context, depth) {
|
|
1230
|
+
let body = returned(callback);
|
|
1231
|
+
if (body && ts.isConditionalExpression(body)) body = unwrap$1(body.whenTrue);
|
|
1232
|
+
if (!body || !(ts.isJsxElement(body) || ts.isJsxSelfClosingElement(body) || ts.isJsxFragment(body))) return void 0;
|
|
1233
|
+
const inner = [];
|
|
1234
|
+
pushExpression(body, context, depth, inner);
|
|
1235
|
+
if (inner.length === 0) return void 0;
|
|
1236
|
+
const first = callback.parameters[0]?.name;
|
|
1237
|
+
const opening = openingOf(body);
|
|
1238
|
+
const argument = first && ts.isIdentifier(first) ? first.text : void 0;
|
|
1239
|
+
const takes = (expression) => !!argument && !!expression && ts.isIdentifier(expression) && expression.text === argument;
|
|
1240
|
+
const attributes = opening?.attributes.properties ?? [];
|
|
1241
|
+
const spread = attributes.some((attribute) => ts.isJsxSpreadAttribute(attribute) && takes(attribute.expression));
|
|
1242
|
+
const pass = attributes.flatMap((attribute) => ts.isJsxAttribute(attribute) && attribute.initializer && ts.isJsxExpression(attribute.initializer) && takes(attribute.initializer.expression) ? [attribute.name.getText(context.source)] : []);
|
|
1243
|
+
return {
|
|
1244
|
+
$render: inner,
|
|
1245
|
+
...spread ? { spread: true } : {},
|
|
1246
|
+
...pass.length > 0 ? { pass } : {}
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
/** The JSX a callback returns, from an expression body or its first `return`. */
|
|
1250
|
+
function returned(callback) {
|
|
1251
|
+
if (!callback.body) return void 0;
|
|
1252
|
+
if (!ts.isBlock(callback.body)) return unwrap$1(callback.body);
|
|
1253
|
+
const found = callback.body.statements.find(ts.isReturnStatement);
|
|
1254
|
+
return found?.expression ? unwrap$1(found.expression) : void 0;
|
|
1255
|
+
}
|
|
1256
|
+
function bind(name, value, env) {
|
|
1257
|
+
if (!name) return;
|
|
1258
|
+
if (ts.isIdentifier(name)) env.set(name.text, value);
|
|
1259
|
+
else if (ts.isObjectBindingPattern(name) && typeof value === "object" && value !== null) for (const element of name.elements) {
|
|
1260
|
+
const key = element.propertyName ?? element.name;
|
|
1261
|
+
if (ts.isIdentifier(key) || ts.isStringLiteral(key)) bind(element.name, value[key.text], env);
|
|
1262
|
+
}
|
|
1263
|
+
else if (ts.isArrayBindingPattern(name) && Array.isArray(value)) name.elements.forEach((element, index) => {
|
|
1264
|
+
if (!ts.isOmittedExpression(element)) bind(element.name, value[index], env);
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
/** What each name a file declares is set to: `const x = ...`, and `const [x] = useState(...)`'s first value. */
|
|
1268
|
+
function declarations(source) {
|
|
1269
|
+
const scope = /* @__PURE__ */ new Map();
|
|
1270
|
+
const visit = (node) => {
|
|
1271
|
+
if (ts.isVariableDeclaration(node) && node.initializer) {
|
|
1272
|
+
const initializer = unwrap$1(node.initializer);
|
|
1273
|
+
if (ts.isIdentifier(node.name) && !scope.has(node.name.text)) scope.set(node.name.text, initializer);
|
|
1274
|
+
if (ts.isObjectBindingPattern(node.name) && ts.isCallExpression(initializer)) {
|
|
1275
|
+
for (const element of node.name.elements) if (ts.isIdentifier(element.name) && !scope.has(element.name.text)) scope.set(element.name.text, initializer);
|
|
1276
|
+
}
|
|
1277
|
+
const state = ts.isArrayBindingPattern(node.name) && ts.isCallExpression(initializer) && /(?:^|\.)use(?:State|Controllable\w*)$/.test(initializer.expression.getText(source));
|
|
1278
|
+
const first = state ? node.name.elements[0] : void 0;
|
|
1279
|
+
const argument = state ? initializer.arguments[0] : void 0;
|
|
1280
|
+
if (first && !ts.isOmittedExpression(first) && ts.isIdentifier(first.name) && argument) {
|
|
1281
|
+
if (!scope.has(first.name.text)) scope.set(first.name.text, unwrap$1(argument));
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
ts.forEachChild(node, visit);
|
|
1285
|
+
};
|
|
1286
|
+
visit(source);
|
|
1287
|
+
return scope;
|
|
1288
|
+
}
|
|
1289
|
+
/** Hooks that hold the example's own values, not data the library hands it. */
|
|
1290
|
+
const ownValueHooks = /(?:^|\.)use(?:State|Reducer|Memo|Callback|Ref|DeferredValue|Transition|Controllable\w*)$/;
|
|
1291
|
+
/**
|
|
1292
|
+
* The names a file binds to what a library hook returns (`const { toasts } = Toast.useToastManager()`), or to what a
|
|
1293
|
+
* library factory makes (`const manager = Toast.createToastManager()`), whose items reach the same lists.
|
|
1294
|
+
*/
|
|
1295
|
+
function hookResults(source) {
|
|
1296
|
+
const names = /* @__PURE__ */ new Set();
|
|
1297
|
+
const made = (expression) => isHookCall(expression, source) || ts.isCallExpression(expression) && /(?:^|\.)create[A-Z]\w*$/.test(expression.expression.getText(source));
|
|
1298
|
+
const visit = (node) => {
|
|
1299
|
+
if (ts.isVariableDeclaration(node) && node.initializer && made(unwrap$1(node.initializer))) {
|
|
1300
|
+
if (ts.isIdentifier(node.name)) names.add(node.name.text);
|
|
1301
|
+
else for (const element of node.name.elements) if (!ts.isOmittedExpression(element) && ts.isIdentifier(element.name)) names.add(element.name.text);
|
|
1302
|
+
}
|
|
1303
|
+
ts.forEachChild(node, visit);
|
|
1304
|
+
};
|
|
1305
|
+
visit(source);
|
|
1306
|
+
return names;
|
|
1307
|
+
}
|
|
1308
|
+
function isHookCall(expression, source) {
|
|
1309
|
+
if (!ts.isCallExpression(expression)) return false;
|
|
1310
|
+
const callee = expression.expression.getText(source);
|
|
1311
|
+
return /(?:^|\.)use[A-Z]\w*$/.test(callee) && !ownValueHooks.test(callee);
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* The example's own components drawn only for a hook's list items (`toasts.map((toast) => <MyToast toast={toast} />)`),
|
|
1315
|
+
* and those they draw in turn, so the library parts inside them count as mapped too.
|
|
1316
|
+
*/
|
|
1317
|
+
function mappedComponents(source, hooked, resolve) {
|
|
1318
|
+
const bodies = /* @__PURE__ */ new Map();
|
|
1319
|
+
const visit = (node) => {
|
|
1320
|
+
const name = declaredName(node);
|
|
1321
|
+
if (name) bodies.set(name, node);
|
|
1322
|
+
ts.forEachChild(node, visit);
|
|
1323
|
+
};
|
|
1324
|
+
visit(source);
|
|
1325
|
+
const tagsIn = (node, found = []) => {
|
|
1326
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
1327
|
+
const tag = node.tagName.getText(source);
|
|
1328
|
+
if (bodies.has(tag) && !resolve(tag)) found.push(tag);
|
|
1329
|
+
}
|
|
1330
|
+
ts.forEachChild(node, (child) => {
|
|
1331
|
+
tagsIn(child, found);
|
|
1332
|
+
});
|
|
1333
|
+
return found;
|
|
1334
|
+
};
|
|
1335
|
+
const found = /* @__PURE__ */ new Set();
|
|
1336
|
+
const queue = [];
|
|
1337
|
+
const lists = (node) => {
|
|
1338
|
+
if (hookMapped(node, hooked, source)) queue.push(...tagsIn(node));
|
|
1339
|
+
else ts.forEachChild(node, lists);
|
|
1340
|
+
};
|
|
1341
|
+
lists(source);
|
|
1342
|
+
while (queue.length > 0) {
|
|
1343
|
+
const next = queue.pop();
|
|
1344
|
+
if (found.has(next)) continue;
|
|
1345
|
+
found.add(next);
|
|
1346
|
+
queue.push(...tagsIn(bodies.get(next)));
|
|
1347
|
+
}
|
|
1348
|
+
return found;
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* A sample item for the lists a library keeps, across its examples: the fullest object they hand one of `values`, what
|
|
1352
|
+
* the package makes and exports (`toast.add({ title, description })` with Buildkit's `toast`).
|
|
1353
|
+
*/
|
|
1354
|
+
function exampleItem(files, values) {
|
|
1355
|
+
let best;
|
|
1356
|
+
if (values.size === 0) return void 0;
|
|
1357
|
+
for (const file of files) {
|
|
1358
|
+
if (![...values].some((value) => file.text.includes(`${value}.`))) continue;
|
|
1359
|
+
const source = ts.createSourceFile(file.path, file.text, ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX);
|
|
1360
|
+
const item = hookItem(source, values, {
|
|
1361
|
+
source,
|
|
1362
|
+
resolve: () => void 0,
|
|
1363
|
+
scope: declarations(source),
|
|
1364
|
+
env: /* @__PURE__ */ new Map()
|
|
1365
|
+
});
|
|
1366
|
+
if (item && Object.keys(item).length > Object.keys(best ?? {}).length) best = item;
|
|
1367
|
+
}
|
|
1368
|
+
return best;
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* A sample item for the lists a hook returns: the fullest object the file hands one of the hook's results
|
|
1372
|
+
* (`toastManager.add({ title, description })`), which is what the library puts in the list.
|
|
1373
|
+
*/
|
|
1374
|
+
function hookItem(source, hooked, context) {
|
|
1375
|
+
let best;
|
|
1376
|
+
const visit = (node) => {
|
|
1377
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
1378
|
+
let root = unwrap$1(node.expression.expression);
|
|
1379
|
+
while (ts.isPropertyAccessExpression(root)) root = unwrap$1(root.expression);
|
|
1380
|
+
const argument = node.arguments[0] && unwrap$1(node.arguments[0]);
|
|
1381
|
+
if (ts.isIdentifier(root) && hooked.has(root.text) && argument && ts.isObjectLiteralExpression(argument)) {
|
|
1382
|
+
const value = evaluate(argument, context, 0);
|
|
1383
|
+
if (value && Object.keys(value).length > Object.keys(best ?? {}).length) best = value;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
ts.forEachChild(node, visit);
|
|
1387
|
+
};
|
|
1388
|
+
visit(source);
|
|
1389
|
+
return best;
|
|
1390
|
+
}
|
|
1391
|
+
/** The item a map callback gets, with an `id` when it reads one (`key={toast.id}`), as lists key their items. */
|
|
1392
|
+
function itemFor(callback, item, source) {
|
|
1393
|
+
const name = callback.parameters[0]?.name;
|
|
1394
|
+
return !!name && ts.isIdentifier(name) && callback.getText(source).includes(`${name.text}.id`) && item.id === void 0 ? {
|
|
1395
|
+
id: "1",
|
|
1396
|
+
...item
|
|
1397
|
+
} : item;
|
|
1398
|
+
}
|
|
1399
|
+
/** The props each of the example's own components gets where a hook's list is mapped into it. */
|
|
1400
|
+
function drawnProps(source, drawn, context) {
|
|
1401
|
+
const found = /* @__PURE__ */ new Map();
|
|
1402
|
+
const { hooked, hookItem: item } = context;
|
|
1403
|
+
if (drawn.size === 0 || !item || !hooked) return found;
|
|
1404
|
+
const visit = (node) => {
|
|
1405
|
+
const callback = hookMapped(node, hooked, source) ? node.arguments[0] : void 0;
|
|
1406
|
+
if (!callback || !(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
|
|
1407
|
+
ts.forEachChild(node, visit);
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
const env = /* @__PURE__ */ new Map();
|
|
1411
|
+
bind(callback.parameters[0]?.name, itemFor(callback, item, source), env);
|
|
1412
|
+
const inner = (child) => {
|
|
1413
|
+
if (ts.isJsxOpeningElement(child) || ts.isJsxSelfClosingElement(child)) {
|
|
1414
|
+
const tag = child.tagName.getText(source);
|
|
1415
|
+
if (drawn.has(tag) && !found.has(tag)) {
|
|
1416
|
+
const props = {};
|
|
1417
|
+
for (const attribute of child.attributes.properties) {
|
|
1418
|
+
if (!ts.isJsxAttribute(attribute)) continue;
|
|
1419
|
+
const value = attributeValue(attribute.initializer, {
|
|
1420
|
+
...context,
|
|
1421
|
+
env
|
|
1422
|
+
});
|
|
1423
|
+
if (value !== void 0) props[attribute.name.getText(source)] = value;
|
|
1424
|
+
}
|
|
1425
|
+
found.set(tag, props);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
ts.forEachChild(child, inner);
|
|
1429
|
+
};
|
|
1430
|
+
inner(callback);
|
|
1431
|
+
};
|
|
1432
|
+
visit(source);
|
|
1433
|
+
return found;
|
|
1434
|
+
}
|
|
1435
|
+
/** A declared component's first parameter, for its props. */
|
|
1436
|
+
function firstParameter(node) {
|
|
1437
|
+
let fn = ts.isVariableDeclaration(node) ? node.initializer && unwrap$1(node.initializer) : node;
|
|
1438
|
+
if (fn && ts.isCallExpression(fn)) fn = fn.arguments.find((argument) => ts.isFunctionLike(argument));
|
|
1439
|
+
if (fn && (ts.isFunctionDeclaration(fn) || ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))) return fn.parameters[0]?.name;
|
|
1440
|
+
}
|
|
1441
|
+
/** The opening tag of a JSX element, or the element itself when it closes itself. */
|
|
1442
|
+
function openingOf(node) {
|
|
1443
|
+
if (ts.isJsxElement(node)) return node.openingElement;
|
|
1444
|
+
return ts.isJsxSelfClosingElement(node) ? node : void 0;
|
|
1445
|
+
}
|
|
1446
|
+
/** The name of a component a file declares: `function Name()` or `const Name = ...`, capitalised. */
|
|
1447
|
+
function declaredName(node) {
|
|
1448
|
+
let name;
|
|
1449
|
+
if (ts.isFunctionDeclaration(node) && node.name) name = node.name.text;
|
|
1450
|
+
else if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) name = node.name.text;
|
|
1451
|
+
return name && /^[A-Z]/.test(name) ? name : void 0;
|
|
1452
|
+
}
|
|
1453
|
+
/** Whether a prop is a render function (`render={({ node }) => ...}`), which the component draws inside itself. */
|
|
1454
|
+
function rendersInside(attribute) {
|
|
1455
|
+
const value = ts.isJsxAttribute(attribute) && attribute.initializer;
|
|
1456
|
+
const expression = value && ts.isJsxExpression(value) && value.expression && unwrap$1(value.expression);
|
|
1457
|
+
return !!expression && (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression));
|
|
1458
|
+
}
|
|
1459
|
+
/** The callback of a `list.map(callback)` call. */
|
|
1460
|
+
function mapCallback(node) {
|
|
1461
|
+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return void 0;
|
|
1462
|
+
if (node.expression.name.text !== "map") return void 0;
|
|
1463
|
+
const callback = node.arguments[0];
|
|
1464
|
+
return callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) ? callback : void 0;
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* A list a file maps: its value, or a field of the options a library factory or hook was given
|
|
1468
|
+
* (`createListCollection({ items })`, `useListCollection({ initialItems })`, where `initialItems` is what `items`
|
|
1469
|
+
* starts as). Only lists are read this way, so a prop given the collection itself isn't mistaken for its options.
|
|
1470
|
+
*/
|
|
1471
|
+
function listOf(expression, context) {
|
|
1472
|
+
const value = evaluate(expression, context, 0);
|
|
1473
|
+
if (value !== void 0) return value;
|
|
1474
|
+
expression = unwrap$1(expression);
|
|
1475
|
+
if (!ts.isPropertyAccessExpression(expression)) return void 0;
|
|
1476
|
+
let made = unwrap$1(expression.expression);
|
|
1477
|
+
if (ts.isIdentifier(made)) {
|
|
1478
|
+
const declared = context.scope.get(made.text);
|
|
1479
|
+
if (declared) made = unwrap$1(declared);
|
|
1480
|
+
}
|
|
1481
|
+
if (!ts.isCallExpression(made)) return void 0;
|
|
1482
|
+
if (!/(?:^|\.)(?:create|use)[A-Z]\w*$/.test(made.expression.getText(context.source))) return void 0;
|
|
1483
|
+
const argument = made.arguments[0] && unwrap$1(made.arguments[0]);
|
|
1484
|
+
if (!argument || !ts.isObjectLiteralExpression(argument)) return void 0;
|
|
1485
|
+
const options = evaluate(argument, context, 0);
|
|
1486
|
+
const field = expression.name.text;
|
|
1487
|
+
return options?.[field] ?? options?.[`initial${field[0].toUpperCase()}${field.slice(1)}`];
|
|
1488
|
+
}
|
|
1489
|
+
/** Whether a node is `list.map(...)` over a hook's result (`toasts.map`, `manager.toasts.map`, `useItems().map`). */
|
|
1490
|
+
function hookMapped(node, hooked, source) {
|
|
1491
|
+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return false;
|
|
1492
|
+
if (node.expression.name.text !== "map") return false;
|
|
1493
|
+
let list = unwrap$1(node.expression.expression);
|
|
1494
|
+
if (isHookCall(list, source)) return true;
|
|
1495
|
+
while (ts.isPropertyAccessExpression(list)) list = unwrap$1(list.expression);
|
|
1496
|
+
return ts.isIdentifier(list) && hooked.has(list.text);
|
|
1497
|
+
}
|
|
1498
|
+
function unwrap$1(expression) {
|
|
1499
|
+
while (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression) || ts.isNonNullExpression(expression) || ts.isTypeAssertionExpression(expression)) expression = expression.expression;
|
|
1500
|
+
return expression;
|
|
1501
|
+
}
|
|
1502
|
+
function attributeValue(initializer, context) {
|
|
1503
|
+
if (!initializer) return true;
|
|
1504
|
+
if (ts.isStringLiteral(initializer)) return initializer.text;
|
|
1505
|
+
if (ts.isJsxExpression(initializer) && initializer.expression) {
|
|
1506
|
+
const expression = unwrap$1(initializer.expression);
|
|
1507
|
+
if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) return renderOf(expression, context, 0);
|
|
1508
|
+
if (ts.isJsxElement(expression) || ts.isJsxSelfClosingElement(expression) || ts.isJsxFragment(expression)) {
|
|
1509
|
+
const inner = [];
|
|
1510
|
+
pushExpression(expression, context, 0, inner);
|
|
1511
|
+
return inner.length > 0 ? { $element: inner } : void 0;
|
|
1512
|
+
}
|
|
1513
|
+
const value = evaluate(expression, context, 0);
|
|
1514
|
+
return value !== void 0 && JSON.stringify(value).length <= 2e3 ? value : void 0;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* The value an expression has, when the file says: literals, arrays and objects of them, the names it declares, and
|
|
1519
|
+
* a mapped item's fields, and calls of the package's own functions as `$call`s. Anything else (other calls, functions,
|
|
1520
|
+
* imports) is undefined.
|
|
1521
|
+
*/
|
|
1522
|
+
function evaluate(expression, context, depth) {
|
|
1523
|
+
if (depth > 6) return void 0;
|
|
1524
|
+
expression = unwrap$1(expression);
|
|
1525
|
+
if (ts.isStringLiteralLike(expression)) return expression.text.slice(0, 200);
|
|
1526
|
+
if (ts.isTemplateExpression(expression)) {
|
|
1527
|
+
let text = expression.head.text;
|
|
1528
|
+
for (const span of expression.templateSpans) {
|
|
1529
|
+
const value = evaluate(span.expression, context, depth + 1);
|
|
1530
|
+
if (typeof value !== "string" && typeof value !== "number") return void 0;
|
|
1531
|
+
text += `${value}${span.literal.text}`;
|
|
1532
|
+
}
|
|
1533
|
+
return text.slice(0, 200);
|
|
1534
|
+
}
|
|
1535
|
+
if (ts.isNumericLiteral(expression)) return Number(expression.text);
|
|
1536
|
+
if (expression.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
1537
|
+
if (expression.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
1538
|
+
if (expression.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
1539
|
+
if (ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(expression.operand)) return -Number(expression.operand.text);
|
|
1540
|
+
if (ts.isIdentifier(expression)) {
|
|
1541
|
+
if (context.env.has(expression.text)) return context.env.get(expression.text);
|
|
1542
|
+
const declared = context.scope.get(expression.text);
|
|
1543
|
+
const value = declared ? evaluate(declared, context, depth + 1) : void 0;
|
|
1544
|
+
const methods = context.calledOn?.get(expression.text);
|
|
1545
|
+
return methods && isMadeValue(value) ? {
|
|
1546
|
+
...value,
|
|
1547
|
+
methods
|
|
1548
|
+
} : value;
|
|
1549
|
+
}
|
|
1550
|
+
if (ts.isArrayLiteralExpression(expression)) {
|
|
1551
|
+
const items = expression.elements.flatMap((element) => {
|
|
1552
|
+
if (!ts.isSpreadElement(element)) return [evaluate(element, context, depth + 1)];
|
|
1553
|
+
const spread = evaluate(element.expression, context, depth + 1);
|
|
1554
|
+
return Array.isArray(spread) ? spread : [];
|
|
1555
|
+
}).slice(0, 8).filter((item) => item !== void 0);
|
|
1556
|
+
return items.length > 0 ? items : void 0;
|
|
1557
|
+
}
|
|
1558
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
1559
|
+
const object = {};
|
|
1560
|
+
for (const property of expression.properties) {
|
|
1561
|
+
if (ts.isSpreadAssignment(property)) {
|
|
1562
|
+
const spread = evaluate(property.expression, context, depth + 1);
|
|
1563
|
+
if (typeof spread === "object" && spread !== null && !Array.isArray(spread)) Object.assign(object, spread);
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
const key = property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ? property.name.text : void 0;
|
|
1567
|
+
if (!key) continue;
|
|
1568
|
+
let value;
|
|
1569
|
+
if (ts.isPropertyAssignment(property)) value = evaluate(property.initializer, context, depth + 1);
|
|
1570
|
+
else if (ts.isShorthandPropertyAssignment(property)) value = evaluate(property.name, context, depth + 1);
|
|
1571
|
+
if (value !== void 0) object[key] = value;
|
|
1572
|
+
}
|
|
1573
|
+
return object;
|
|
1574
|
+
}
|
|
1575
|
+
if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression)) {
|
|
1576
|
+
const callee = context.imports?.get(expression.expression.text);
|
|
1577
|
+
if (!callee || !/^[a-z]/.test(callee.export) || /^use[A-Z]/.test(callee.export)) return void 0;
|
|
1578
|
+
const args = expression.arguments.map((argument) => evaluate(argument, context, depth + 1));
|
|
1579
|
+
if (args.some((argument) => argument === void 0)) return void 0;
|
|
1580
|
+
return {
|
|
1581
|
+
$call: {
|
|
1582
|
+
import: callee.import,
|
|
1583
|
+
export: callee.export
|
|
1584
|
+
},
|
|
1585
|
+
...args.length > 0 ? { args } : {}
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
if (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression)) {
|
|
1589
|
+
const object = evaluate(expression.expression, context, depth + 1);
|
|
1590
|
+
const key = ts.isPropertyAccessExpression(expression) ? expression.name.text : evaluate(expression.argumentExpression, context, depth + 1);
|
|
1591
|
+
if (typeof object !== "object" || object === null || typeof key !== "string" && typeof key !== "number") return;
|
|
1592
|
+
return object[key];
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
function textIn(node) {
|
|
1596
|
+
const parts = [];
|
|
1597
|
+
const visit = (child) => {
|
|
1598
|
+
if (ts.isJsxText(child)) parts.push(child.text);
|
|
1599
|
+
ts.forEachChild(child, visit);
|
|
1600
|
+
};
|
|
1601
|
+
visit(node);
|
|
1602
|
+
return parts.join(" ").replace(/\s+/g, " ").trim().slice(0, 60);
|
|
1603
|
+
}
|
|
1604
|
+
function sampleWeight(sample) {
|
|
1605
|
+
const children = sample.children ?? [];
|
|
1606
|
+
return children.length * 2 + Object.keys(sample.props ?? {}).length + children.reduce((sum, child) => {
|
|
1607
|
+
if (typeof child === "string") return sum;
|
|
1608
|
+
return sum + (isSampleRender(child) ? sampleWeight({ children: child.$render }) : sampleWeight(child));
|
|
1609
|
+
}, 0);
|
|
1610
|
+
}
|
|
1611
|
+
/** A component's family: its namespace (`Dialog` for `Dialog.Title`), else the first word of its name. */
|
|
1612
|
+
function familyOf(name) {
|
|
1613
|
+
const dot = name.lastIndexOf(".");
|
|
1614
|
+
if (dot !== -1) return name.slice(0, dot);
|
|
1615
|
+
const word = /^[A-Z][a-z0-9]*/.exec(name)?.[0] ?? name;
|
|
1616
|
+
return /[^s]s$/.test(word) ? word.slice(0, -1) : word;
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* The parents the examples put a component in, at any depth, outermost first: every library ancestor at least half
|
|
1620
|
+
* its placed uses have, in the order they're usually nested. A level some examples leave out (`DropdownMenuGroup`)
|
|
1621
|
+
* is still here when most have it, with its share, so it can be marked optional. As with `nearestParent`, most uses
|
|
1622
|
+
* must be placed at all, and `sameFamily` keeps only its own family's. `share` lowers the share a level needs, for a
|
|
1623
|
+
* part that fails without a level fewer than half show (`Slider.Control`). Shares are counted per file, so one long
|
|
1624
|
+
* demo can't outvote ten short ones, and per use, so a level most uses show isn't lost; a level either count keeps
|
|
1625
|
+
* is kept, with the larger share.
|
|
1626
|
+
*/
|
|
1627
|
+
function exampleLevels(uses, name, sameFamily, share = .5) {
|
|
1628
|
+
const use = uses.get(name);
|
|
1629
|
+
if (!use || use.paths.length === 0) return void 0;
|
|
1630
|
+
const family = familyOf(name);
|
|
1631
|
+
const count = (all) => {
|
|
1632
|
+
const paths = all.map((path) => sameFamily ? path.filter((parent) => familyOf(parent) === family) : path);
|
|
1633
|
+
const placed = paths.filter((path) => path.length > 0);
|
|
1634
|
+
if (placed.length * 2 < paths.length) return [];
|
|
1635
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1636
|
+
for (const path of placed) path.forEach((parent, depth) => {
|
|
1637
|
+
const entry = seen.get(parent) ?? {
|
|
1638
|
+
count: 0,
|
|
1639
|
+
depth: 0
|
|
1640
|
+
};
|
|
1641
|
+
entry.count++;
|
|
1642
|
+
entry.depth += depth;
|
|
1643
|
+
seen.set(parent, entry);
|
|
1644
|
+
});
|
|
1645
|
+
return [...seen].filter(([, entry]) => entry.count >= placed.length * share).map(([parent, entry]) => ({
|
|
1646
|
+
parent,
|
|
1647
|
+
share: entry.count / placed.length,
|
|
1648
|
+
depth: entry.depth / entry.count
|
|
1649
|
+
}));
|
|
1650
|
+
};
|
|
1651
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1652
|
+
for (const level of [...count(use.paths), ...count(use.usePaths)]) {
|
|
1653
|
+
const known = merged.get(level.parent);
|
|
1654
|
+
if (!known || level.share > known.share) merged.set(level.parent, level);
|
|
1655
|
+
}
|
|
1656
|
+
const levels = [...merged.values()].sort((a, b) => b.depth - a.depth).slice(-8).map(({ parent, share }) => ({
|
|
1657
|
+
parent,
|
|
1658
|
+
share
|
|
1659
|
+
}));
|
|
1660
|
+
return levels.length > 0 ? levels : void 0;
|
|
1661
|
+
}
|
|
1662
|
+
/** A sample with only its own family's components in it, as text where others were. */
|
|
1663
|
+
function familySample(sample, family, familyIn = familyOf) {
|
|
1664
|
+
const keep = (child) => {
|
|
1665
|
+
if (typeof child === "string") return child;
|
|
1666
|
+
if (isSampleRender(child)) {
|
|
1667
|
+
const inner = child.$render.flatMap((item) => keep(item) ?? []);
|
|
1668
|
+
return inner.length > 0 ? {
|
|
1669
|
+
...child,
|
|
1670
|
+
$render: inner
|
|
1671
|
+
} : void 0;
|
|
1672
|
+
}
|
|
1673
|
+
return familyIn(child.component) === family ? {
|
|
1674
|
+
...child,
|
|
1675
|
+
...child.children ? { children: child.children.flatMap((inner) => keep(inner) ?? []) } : {}
|
|
1676
|
+
} : void 0;
|
|
1677
|
+
};
|
|
1678
|
+
const children = (sample.children ?? []).flatMap((child) => keep(child) ?? []);
|
|
1679
|
+
return {
|
|
1680
|
+
...sample.props ? { props: sample.props } : {},
|
|
1681
|
+
...children.length > 0 ? { children } : {}
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
// ../libgen/src/anatomy.ts
|
|
1685
|
+
/**
|
|
1686
|
+
* Where the docs' Anatomy and Usage blocks put each part: every library ancestor of each place it's shown, nearest
|
|
1687
|
+
* first. The blocks are read as JSX, with their imports taken out.
|
|
1688
|
+
*/
|
|
1689
|
+
function readAnatomy(docs, names) {
|
|
1690
|
+
const blocks = [];
|
|
1691
|
+
for (const doc of docs) {
|
|
1692
|
+
const sections = doc.text.split(/^(?=#{2,3} )/m).filter((section) => /^#{2,3} (?:Anatomy|Usage)\b/.test(section));
|
|
1693
|
+
for (const section of sections) for (const [index, match] of [...section.matchAll(/```(?:jsx|tsx|js|ts)\b[^\n]*\n([\s\S]*?)```/g)].entries()) {
|
|
1694
|
+
const code = match[1].replace(/^import[\s\S]*?from\s+["'][^"']+["'];?$/gm, "").replace(/^import\s+["'][^"']+["'];?$/gm, "").trim();
|
|
1695
|
+
if (!code.startsWith("<")) continue;
|
|
1696
|
+
blocks.push({
|
|
1697
|
+
path: `${doc.path}#${index}`,
|
|
1698
|
+
text: `const anatomy = (<>\n${code}\n</>)`
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
const anatomy = /* @__PURE__ */ new Map();
|
|
1703
|
+
for (const [name, use] of readExamples(blocks, names)) {
|
|
1704
|
+
const placed = use.paths.filter((path) => path.length > 0);
|
|
1705
|
+
if (placed.length > 0) anatomy.set(name, placed);
|
|
1706
|
+
}
|
|
1707
|
+
return anatomy;
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Compares each part's chain with the docs. A part is placed right when one place the docs show has the nearest
|
|
1711
|
+
* parent the chain has, and every parent the chain needs (all but its optional ones); complete when the chain also
|
|
1712
|
+
* has every level that place does.
|
|
1713
|
+
*/
|
|
1714
|
+
function checkAnatomy(anatomy, list) {
|
|
1715
|
+
const check = {
|
|
1716
|
+
parts: 0,
|
|
1717
|
+
placed: 0,
|
|
1718
|
+
complete: 0,
|
|
1719
|
+
differ: []
|
|
1720
|
+
};
|
|
1721
|
+
for (const [name, paths] of anatomy) {
|
|
1722
|
+
const entry = list[name];
|
|
1723
|
+
if (!entry || entry.visual === false) continue;
|
|
1724
|
+
const known = paths.map((path) => path.filter((parent) => parent in list && !parent.endsWith("Portal"))).filter((path) => path.length > 0 && familyOf(path[0]) === familyOf(name));
|
|
1725
|
+
if (known.length === 0) continue;
|
|
1726
|
+
check.parts++;
|
|
1727
|
+
const chain = entry.parents ?? [];
|
|
1728
|
+
const needed = chain.filter((parent) => !entry.optionalParents?.includes(parent) && !parent.endsWith("Portal"));
|
|
1729
|
+
const right = known.filter((path) => path.length > 0 && chain.includes(path[0]) && needed.every((parent) => path.includes(parent)));
|
|
1730
|
+
if (right.length > 0) {
|
|
1731
|
+
check.placed++;
|
|
1732
|
+
if (right.some((path) => path.every((parent) => chain.includes(parent)))) check.complete++;
|
|
1733
|
+
} else {
|
|
1734
|
+
const docs = known.find((path) => path.length > 0);
|
|
1735
|
+
check.differ.push({
|
|
1736
|
+
name,
|
|
1737
|
+
docs: [...docs].reverse(),
|
|
1738
|
+
file: chain
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return check;
|
|
1743
|
+
}
|
|
1744
|
+
// ../libgen/src/hosts.ts
|
|
1745
|
+
/**
|
|
1746
|
+
* What each of the package's own functions renders, by the names the package exports: read from its compiled files,
|
|
1747
|
+
* following `jsx()` and `createElement()` calls and JSX. Only files that mention one of `names` are read. `calls`, when
|
|
1748
|
+
* given, collects each render with the props passed, by the part rendered. `made`, when given, collects the values
|
|
1749
|
+
* the package exports that a factory makes (`export const toast = createToastManager()`), by their exported names.
|
|
1750
|
+
*/
|
|
1751
|
+
async function readRenders(packageDir, names, calls, made) {
|
|
1752
|
+
const renders = /* @__PURE__ */ new Map();
|
|
1753
|
+
for (const file of await sourceFiles$1(packageDir)) {
|
|
1754
|
+
const text = await readFile(file, "utf8");
|
|
1755
|
+
if (!names.some((name) => text.includes(name))) continue;
|
|
1756
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX);
|
|
1757
|
+
const exportedAs = /* @__PURE__ */ new Map();
|
|
1758
|
+
for (const statement of source.statements) {
|
|
1759
|
+
if (!ts.isExportDeclaration(statement) || !statement.exportClause || !ts.isNamedExports(statement.exportClause)) continue;
|
|
1760
|
+
for (const element of statement.exportClause.elements) exportedAs.set((element.propertyName ?? element.name).getText(source), element.name.getText(source));
|
|
1761
|
+
}
|
|
1762
|
+
const named = (local) => exportedAs.get(local) ?? local;
|
|
1763
|
+
for (const statement of made ? source.statements : []) {
|
|
1764
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
1765
|
+
const exported = statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
1766
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1767
|
+
const init = declaration.initializer;
|
|
1768
|
+
if (!ts.isIdentifier(declaration.name) || !init || !ts.isCallExpression(init)) continue;
|
|
1769
|
+
if (!/(?:^|\.)create[A-Z]\w*$/.test(init.expression.getText(source))) continue;
|
|
1770
|
+
const local = declaration.name.text;
|
|
1771
|
+
if (exported || exportedAs.has(local)) made.add(named(local));
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
const visit = (node, owner, params) => {
|
|
1775
|
+
let here = owner;
|
|
1776
|
+
let own = params;
|
|
1777
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
1778
|
+
here = named(node.name.text);
|
|
1779
|
+
own = paramsOf(node);
|
|
1780
|
+
} else if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
1781
|
+
if (/^[A-Z]/.test(node.name.text)) {
|
|
1782
|
+
here = named(node.name.text);
|
|
1783
|
+
own = paramsOf(node.initializer);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
if (here) {
|
|
1787
|
+
const rendered = renderedBy(node, source);
|
|
1788
|
+
if (rendered) {
|
|
1789
|
+
const set = renders.get(here) ?? /* @__PURE__ */ new Set();
|
|
1790
|
+
set.add(named(rendered));
|
|
1791
|
+
renders.set(here, set);
|
|
1792
|
+
if (calls) {
|
|
1793
|
+
const list = calls.get(named(rendered)) ?? [];
|
|
1794
|
+
const children = renderedChildren(node, source, named);
|
|
1795
|
+
list.push({
|
|
1796
|
+
owner: here,
|
|
1797
|
+
props: passedProps(node, own),
|
|
1798
|
+
...children.length > 0 ? { children } : {}
|
|
1799
|
+
});
|
|
1800
|
+
calls.set(named(rendered), list);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
const callback = ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" ? node.arguments[0] : void 0;
|
|
1805
|
+
const item = callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) ? callback.parameters[0]?.name : void 0;
|
|
1806
|
+
ts.forEachChild(node, (child) => visit(child, here, child === callback && item && ts.isIdentifier(item) ? {
|
|
1807
|
+
...own,
|
|
1808
|
+
items: /* @__PURE__ */ new Set([...own.items, item.text])
|
|
1809
|
+
} : own));
|
|
1810
|
+
};
|
|
1811
|
+
visit(source, void 0, {
|
|
1812
|
+
locals: /* @__PURE__ */ new Map(),
|
|
1813
|
+
objects: /* @__PURE__ */ new Set(),
|
|
1814
|
+
items: /* @__PURE__ */ new Set()
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
return renders;
|
|
1818
|
+
}
|
|
1819
|
+
/** The components of the package that render each one, the reverse of `readRenders`. */
|
|
1820
|
+
function renderedIn(renders) {
|
|
1821
|
+
const owners = /* @__PURE__ */ new Map();
|
|
1822
|
+
for (const [owner, rendered] of renders) for (const name of rendered) if (name !== owner) owners.set(name, [...owners.get(name) ?? [], owner]);
|
|
1823
|
+
return owners;
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* The parts only a host renders: components the package's own code draws inside a host a decorator adds (`Toaster`
|
|
1827
|
+
* mapping the toast manager's list into `Toast`, `ToastTitle` and the rest), and nowhere else. A person never places
|
|
1828
|
+
* these, so they're not visual.
|
|
1829
|
+
*/
|
|
1830
|
+
function hostedParts(renders, hosts, names) {
|
|
1831
|
+
const reached = new Set(hosts);
|
|
1832
|
+
const queue = [...hosts];
|
|
1833
|
+
while (queue.length > 0) for (const next of renders.get(queue.pop()) ?? []) if (!reached.has(next)) {
|
|
1834
|
+
reached.add(next);
|
|
1835
|
+
queue.push(next);
|
|
1836
|
+
}
|
|
1837
|
+
const owners = renderedIn(renders);
|
|
1838
|
+
return names.filter((name) => !hosts.includes(name) && reached.has(name) && (owners.get(name) ?? []).every((owner) => reached.has(owner)));
|
|
1839
|
+
}
|
|
1840
|
+
/** The expression an attribute gives: `{expression}` or a string. */
|
|
1841
|
+
function attributeExpression(value) {
|
|
1842
|
+
if (value && ts.isJsxExpression(value)) return value.expression;
|
|
1843
|
+
return value && ts.isStringLiteral(value) ? value : void 0;
|
|
1844
|
+
}
|
|
1845
|
+
function paramsOf(node) {
|
|
1846
|
+
const params = {
|
|
1847
|
+
locals: /* @__PURE__ */ new Map(),
|
|
1848
|
+
objects: /* @__PURE__ */ new Set(),
|
|
1849
|
+
items: /* @__PURE__ */ new Set()
|
|
1850
|
+
};
|
|
1851
|
+
let fn = node;
|
|
1852
|
+
if (ts.isCallExpression(fn)) fn = fn.arguments.find((arg) => ts.isFunctionLike(arg));
|
|
1853
|
+
if (!fn || !(ts.isFunctionDeclaration(fn) || ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))) return params;
|
|
1854
|
+
const first = fn.parameters[0]?.name;
|
|
1855
|
+
if (!first) return params;
|
|
1856
|
+
if (ts.isIdentifier(first)) params.objects.add(first.text);
|
|
1857
|
+
else if (ts.isObjectBindingPattern(first)) for (const element of first.elements) {
|
|
1858
|
+
if (!ts.isIdentifier(element.name)) continue;
|
|
1859
|
+
if (element.dotDotDotToken) params.objects.add(element.name.text);
|
|
1860
|
+
else {
|
|
1861
|
+
const key = element.propertyName ?? element.name;
|
|
1862
|
+
if (ts.isIdentifier(key) || ts.isStringLiteral(key)) params.locals.set(element.name.text, key.text);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
return params;
|
|
1866
|
+
}
|
|
1867
|
+
/** The props a render passes: the owner's own props by name, literal values, and the rest as worked out. */
|
|
1868
|
+
function passedProps(node, params) {
|
|
1869
|
+
const props = {};
|
|
1870
|
+
const object = ts.isCallExpression(node) ? node.arguments[1] : void 0;
|
|
1871
|
+
const entries = [];
|
|
1872
|
+
if (object && ts.isObjectLiteralExpression(object)) {
|
|
1873
|
+
for (const property of object.properties) if (ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) entries.push([property.name.text, property.initializer]);
|
|
1874
|
+
else if (ts.isShorthandPropertyAssignment(property)) entries.push([property.name.text, property.name]);
|
|
1875
|
+
} else if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) for (const attribute of node.attributes.properties) {
|
|
1876
|
+
if (!ts.isJsxAttribute(attribute)) continue;
|
|
1877
|
+
const value = attribute.initializer;
|
|
1878
|
+
entries.push([attribute.name.getText(), attributeExpression(value)]);
|
|
1879
|
+
}
|
|
1880
|
+
for (const [key, value] of entries) {
|
|
1881
|
+
if (key === "children" || key === "className") continue;
|
|
1882
|
+
if (!value) props[key] = { value: true };
|
|
1883
|
+
else if (ts.isIdentifier(value) && params.items.has(value.text)) props[key] = { item: true };
|
|
1884
|
+
else if (ts.isIdentifier(value) && params.locals.has(value.text)) props[key] = { forward: params.locals.get(value.text) };
|
|
1885
|
+
else if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) && params.objects.has(value.expression.text)) props[key] = { forward: value.name.text };
|
|
1886
|
+
else if (ts.isStringLiteralLike(value)) props[key] = { value: value.text };
|
|
1887
|
+
else if (ts.isNumericLiteral(value)) props[key] = { value: Number(value.text) };
|
|
1888
|
+
else if (value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword) props[key] = { value: value.kind === ts.SyntaxKind.TrueKeyword };
|
|
1889
|
+
else props[key] = { computed: true };
|
|
1890
|
+
}
|
|
1891
|
+
return props;
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* The parts a `jsx(X, { children })` call puts inside X, through HTML elements, lists and conditions in between
|
|
1895
|
+
* (`_jsxs(ToastContent, { children: [_jsxs("div", { children: [_jsx(ToastTitle, {})] })] })`).
|
|
1896
|
+
*/
|
|
1897
|
+
function renderedChildren(node, source, named) {
|
|
1898
|
+
const childrenOf = (call) => {
|
|
1899
|
+
const object = call.arguments[1];
|
|
1900
|
+
if (!object || !ts.isObjectLiteralExpression(object)) return [];
|
|
1901
|
+
const children = object.properties.find((property) => ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === "children");
|
|
1902
|
+
return children ? walk(children.initializer) : [];
|
|
1903
|
+
};
|
|
1904
|
+
const walk = (expression) => {
|
|
1905
|
+
while (ts.isParenthesizedExpression(expression)) expression = expression.expression;
|
|
1906
|
+
if (ts.isArrayLiteralExpression(expression)) return expression.elements.flatMap(walk);
|
|
1907
|
+
if (ts.isConditionalExpression(expression)) return walk(expression.whenTrue);
|
|
1908
|
+
if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) return walk(expression.right);
|
|
1909
|
+
if (!ts.isCallExpression(expression)) return [];
|
|
1910
|
+
const first = expression.arguments[0];
|
|
1911
|
+
const rendered = renderedBy(expression, source);
|
|
1912
|
+
if (rendered) {
|
|
1913
|
+
const children = childrenOf(expression);
|
|
1914
|
+
return [{
|
|
1915
|
+
component: named(rendered),
|
|
1916
|
+
...children.length > 0 ? { children } : {}
|
|
1917
|
+
}];
|
|
1918
|
+
}
|
|
1919
|
+
return first && ts.isStringLiteral(first) && /^[a-z]/.test(first.text) ? childrenOf(expression) : [];
|
|
1920
|
+
};
|
|
1921
|
+
return ts.isCallExpression(node) ? childrenOf(node) : [];
|
|
1922
|
+
}
|
|
1923
|
+
/** The component a `jsx(X, ...)`, `createElement(X, ...)` or `<X>` renders, when it's a plain name. */
|
|
1924
|
+
function renderedBy(node, source) {
|
|
1925
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) return ts.isIdentifier(node.tagName) && /^[A-Z]/.test(node.tagName.text) ? node.tagName.text : void 0;
|
|
1926
|
+
if (!ts.isCallExpression(node)) return void 0;
|
|
1927
|
+
let callee = node.expression;
|
|
1928
|
+
while (ts.isParenthesizedExpression(callee)) callee = callee.expression;
|
|
1929
|
+
if (ts.isBinaryExpression(callee) && callee.operatorToken.kind === ts.SyntaxKind.CommaToken) callee = callee.right;
|
|
1930
|
+
const text = callee.getText(source);
|
|
1931
|
+
if (!/(?:^|\.)(?:_?jsxs?|_?jsxDEV|createElement)$/.test(text)) return void 0;
|
|
1932
|
+
const first = node.arguments[0];
|
|
1933
|
+
return first && ts.isIdentifier(first) && /^[A-Z]/.test(first.text) ? first.text : void 0;
|
|
1934
|
+
}
|
|
1935
|
+
/** The package's own script files, skipping type declarations, source maps and its dependencies. */
|
|
1936
|
+
async function sourceFiles$1(dir, found = [], depth = 0) {
|
|
1937
|
+
if (depth > 8 || found.length >= 4e3) return found;
|
|
1938
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
1939
|
+
for (const entry of entries) {
|
|
1940
|
+
const path = join(dir, entry.name);
|
|
1941
|
+
if (entry.isDirectory()) {
|
|
1942
|
+
if (entry.name !== "node_modules" && !entry.name.startsWith(".")) await sourceFiles$1(path, found, depth + 1);
|
|
1943
|
+
} else if (/\.(?:m?js|cjs|jsx)$/.test(entry.name) && (await stat(path)).size < 1e6) found.push(path);
|
|
1944
|
+
}
|
|
1945
|
+
return found;
|
|
1946
|
+
}
|
|
1947
|
+
// ../libgen/src/icon-exports.ts
|
|
1948
|
+
/**
|
|
1949
|
+
* The names an icon set's types say to insert where an icon is exported under several: one not marked deprecated over
|
|
1950
|
+
* one that is (Phosphor's `GearIcon` over `Gear`), then the name its declaration has over the aliases it's also
|
|
1951
|
+
* exported as (Lucide's `TriangleAlert` over `AlertTriangle`), then the shortest. Only the names that differ from the
|
|
1952
|
+
* shortest, which is what's inserted otherwise, are returned. Each group is one icon's names; `usable` says which names
|
|
1953
|
+
* the set's pattern allows.
|
|
1954
|
+
*/
|
|
1955
|
+
function preferredExports(typesFile, groups, usable) {
|
|
1956
|
+
const program = ts.createProgram([typesFile], {
|
|
1957
|
+
noEmit: true,
|
|
1958
|
+
skipLibCheck: true,
|
|
1959
|
+
types: []
|
|
1960
|
+
});
|
|
1961
|
+
const checker = program.getTypeChecker();
|
|
1962
|
+
const source = program.getSourceFile(typesFile);
|
|
1963
|
+
const module = source && checker.getSymbolAtLocation(source);
|
|
1964
|
+
if (!module) return [];
|
|
1965
|
+
const marks = /* @__PURE__ */ new Map();
|
|
1966
|
+
for (const symbol of checker.getExportsOfModule(module)) {
|
|
1967
|
+
const alias = (symbol.flags & ts.SymbolFlags.Alias) !== 0;
|
|
1968
|
+
const target = alias ? checker.getAliasedSymbol(symbol) : symbol;
|
|
1969
|
+
const tags = [...symbol.getJsDocTags(checker), ...alias ? target.getJsDocTags(checker) : []];
|
|
1970
|
+
marks.set(symbol.name, {
|
|
1971
|
+
deprecated: tags.some((tag) => tag.name === "deprecated"),
|
|
1972
|
+
declared: target.name === symbol.name
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
const shortest = (a, b) => a.length - b.length || byCodeUnit$1(a, b);
|
|
1976
|
+
const preferred = [];
|
|
1977
|
+
for (const group of groups) {
|
|
1978
|
+
const names = group.filter(usable);
|
|
1979
|
+
const fallback = [...names].sort(shortest)[0];
|
|
1980
|
+
const rank = (name) => {
|
|
1981
|
+
const mark = marks.get(name);
|
|
1982
|
+
return (mark?.deprecated ? 2 : 0) + (mark?.declared === false ? 1 : 0);
|
|
1983
|
+
};
|
|
1984
|
+
const best = [...names].sort((a, b) => rank(a) - rank(b) || shortest(a, b))[0];
|
|
1985
|
+
if (best && best !== fallback) preferred.push(best);
|
|
1986
|
+
}
|
|
1987
|
+
return preferred.sort();
|
|
1988
|
+
}
|
|
1989
|
+
/** The types file a specifier resolves to from a folder, as a bundler would resolve it. */
|
|
1990
|
+
function typesFor(specifier, from) {
|
|
1991
|
+
const resolved = ts.resolveModuleName(specifier, `${from}/index.ts`, {
|
|
1992
|
+
module: ts.ModuleKind.ESNext,
|
|
1993
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler
|
|
1994
|
+
}, ts.sys).resolvedModule?.resolvedFileName;
|
|
1995
|
+
return resolved && /\.d\.[cm]?ts$/.test(resolved) ? resolved : void 0;
|
|
1996
|
+
}
|
|
1997
|
+
/** Compares two strings by their UTF-16 code units, as `sort` does without a comparer. */
|
|
1998
|
+
function byCodeUnit$1(a, b) {
|
|
1999
|
+
if (a < b) return -1;
|
|
2000
|
+
return a > b ? 1 : 0;
|
|
2001
|
+
}
|
|
2002
|
+
// ../libgen/src/icon-styles.ts
|
|
2003
|
+
/** How many of a set's icons are drawn from each module to tell its styles apart. */
|
|
2004
|
+
const sampled = 5;
|
|
2005
|
+
/**
|
|
2006
|
+
* The styles of a set that keeps each in a module of its own, as Heroicons keeps `@heroicons/react/16/solid` beside
|
|
2007
|
+
* `24/outline`, by name, with the module each is imported from: the package's other entry points that export most of
|
|
2008
|
+
* the same icons, and draw them differently. One that draws them the same (Phosphor's `ssr`, Lucide's `icons`) is
|
|
2009
|
+
* another way in, not a style; one that doesn't load (Fluent's `fonts`) or exports few of them isn't one either.
|
|
2010
|
+
* Styles are named by their path in the package, so `16/solid` is `16 Solid`, and listed in the package's order. A set
|
|
2011
|
+
* with no such modules has none.
|
|
2012
|
+
*/
|
|
2013
|
+
async function styleModules(packageName, entries, main, load, draw) {
|
|
2014
|
+
const icons = (module) => Object.keys(module).filter((name) => /^[A-Z]/.test(name) && isComponent(module[name]));
|
|
2015
|
+
const base = await load(main).catch(() => void 0);
|
|
2016
|
+
if (!base) return void 0;
|
|
2017
|
+
const names = icons(base).sort();
|
|
2018
|
+
const sample = names.slice(0, sampled);
|
|
2019
|
+
const drawn = new Map(sample.map((name) => [name, draw(base[name])]));
|
|
2020
|
+
const found = [];
|
|
2021
|
+
for (const entry of entries) {
|
|
2022
|
+
if (entry === "." || entry.includes("*") || /\.[a-z]+$/i.test(entry)) continue;
|
|
2023
|
+
const specifier = packageName + entry.slice(1);
|
|
2024
|
+
if (specifier === main) {
|
|
2025
|
+
found.push({
|
|
2026
|
+
specifier,
|
|
2027
|
+
module: base
|
|
2028
|
+
});
|
|
2029
|
+
continue;
|
|
2030
|
+
}
|
|
2031
|
+
const module = await load(specifier).catch(() => void 0);
|
|
2032
|
+
if (!module) continue;
|
|
2033
|
+
const own = new Set(icons(module));
|
|
2034
|
+
const shared = sample.filter((name) => own.has(name));
|
|
2035
|
+
if (own.size === 0 || names.filter((name) => own.has(name)).length * 2 < names.length || shared.length === 0) continue;
|
|
2036
|
+
if (found.some((other) => shared.every((name) => other.module[name] === module[name]))) continue;
|
|
2037
|
+
if (shared.some((name) => draw(module[name]) === drawn.get(name))) continue;
|
|
2038
|
+
found.push({
|
|
2039
|
+
specifier,
|
|
2040
|
+
module
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
if (!found.some((style) => style.module === base)) found.unshift({
|
|
2044
|
+
specifier: main,
|
|
2045
|
+
module: base
|
|
2046
|
+
});
|
|
2047
|
+
if (found.length < 2) return void 0;
|
|
2048
|
+
return Object.fromEntries(found.map(({ specifier }) => [specifier.startsWith(`${packageName}/`) ? styleName(specifier.slice(packageName.length + 1)) : "Default", specifier]));
|
|
2049
|
+
}
|
|
2050
|
+
/** A style's name from its module's path in the package: `24/outline` is `24 Outline`. */
|
|
2051
|
+
function styleName(path) {
|
|
2052
|
+
return path.split(/[/_-]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(" ");
|
|
2053
|
+
}
|
|
2054
|
+
function isComponent(value) {
|
|
2055
|
+
return typeof value === "function" || typeof value === "object" && value !== null && "$$typeof" in value;
|
|
2056
|
+
}
|
|
2057
|
+
// ../libgen/src/js-data.ts
|
|
2058
|
+
/**
|
|
2059
|
+
* The data a JavaScript module declares at its top level, read with the TypeScript parser rather than run: arrays,
|
|
2060
|
+
* objects, strings, numbers and booleans, by the name each is declared as. Members of enums compiled the way esbuild
|
|
2061
|
+
* writes them (`((i) => (i.ARROWS = "arrows", i))(e || {})`) read as their values, so a published data file such as
|
|
2062
|
+
* Phosphor's icon list can be read without running code that came from the network. Anything else is left out.
|
|
2063
|
+
*/
|
|
2064
|
+
function moduleData(text) {
|
|
2065
|
+
const source = ts.createSourceFile("data.mjs", text, ts.ScriptTarget.Latest, false, ts.ScriptKind.JS);
|
|
2066
|
+
const declared = /* @__PURE__ */ new Map();
|
|
2067
|
+
for (const statement of source.statements) {
|
|
2068
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
2069
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
2070
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
|
|
2071
|
+
const value = compiledEnum(declaration.initializer) ?? literal(declaration.initializer, declared);
|
|
2072
|
+
if (value !== void 0) declared.set(declaration.name.text, value);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
return declared;
|
|
2076
|
+
}
|
|
2077
|
+
/** An enum as esbuild compiles it: an arrow that assigns each member to its parameter, called at once. */
|
|
2078
|
+
function compiledEnum(node) {
|
|
2079
|
+
const call = unwrap(node);
|
|
2080
|
+
if (!ts.isCallExpression(call)) return void 0;
|
|
2081
|
+
const arrow = unwrap(call.expression);
|
|
2082
|
+
if (!ts.isArrowFunction(arrow) || arrow.parameters.length !== 1) return void 0;
|
|
2083
|
+
const param = arrow.parameters[0].name;
|
|
2084
|
+
if (!ts.isIdentifier(param) || ts.isBlock(arrow.body)) return void 0;
|
|
2085
|
+
const members = {};
|
|
2086
|
+
const visit = (expression) => {
|
|
2087
|
+
const node = unwrap(expression);
|
|
2088
|
+
if (!ts.isBinaryExpression(node)) return;
|
|
2089
|
+
if (node.operatorToken.kind === ts.SyntaxKind.CommaToken) {
|
|
2090
|
+
visit(node.left);
|
|
2091
|
+
visit(node.right);
|
|
2092
|
+
} else if (node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(node.left) && ts.isIdentifier(node.left.expression) && node.left.expression.text === param.text) members[node.left.name.text] = literal(node.right, /* @__PURE__ */ new Map());
|
|
2093
|
+
};
|
|
2094
|
+
visit(arrow.body);
|
|
2095
|
+
return Object.keys(members).length > 0 ? members : void 0;
|
|
2096
|
+
}
|
|
2097
|
+
function unwrap(node) {
|
|
2098
|
+
return ts.isParenthesizedExpression(node) ? unwrap(node.expression) : node;
|
|
2099
|
+
}
|
|
2100
|
+
/** A literal's value, with names and enum members looked up in what's been declared; `undefined` for anything else. */
|
|
2101
|
+
function literal(node, declared) {
|
|
2102
|
+
const value = unwrap(node);
|
|
2103
|
+
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) return value.text;
|
|
2104
|
+
if (ts.isNumericLiteral(value)) return Number(value.text);
|
|
2105
|
+
if (value.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
2106
|
+
if (value.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
2107
|
+
if (ts.isPrefixUnaryExpression(value) && value.operator === ts.SyntaxKind.MinusToken) {
|
|
2108
|
+
const number = literal(value.operand, declared);
|
|
2109
|
+
return typeof number === "number" ? -number : void 0;
|
|
2110
|
+
}
|
|
2111
|
+
if (ts.isArrayLiteralExpression(value)) return value.elements.map((element) => literal(element, declared));
|
|
2112
|
+
if (ts.isObjectLiteralExpression(value)) {
|
|
2113
|
+
const object = {};
|
|
2114
|
+
for (const property of value.properties) {
|
|
2115
|
+
if (!ts.isPropertyAssignment(property)) continue;
|
|
2116
|
+
const name = property.name;
|
|
2117
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) object[name.text] = literal(property.initializer, declared);
|
|
2118
|
+
}
|
|
2119
|
+
return object;
|
|
2120
|
+
}
|
|
2121
|
+
if (ts.isIdentifier(value)) return declared.get(value.text);
|
|
2122
|
+
if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression)) {
|
|
2123
|
+
const owner = declared.get(value.expression.text);
|
|
2124
|
+
return owner && typeof owner === "object" ? owner[value.name.text] : void 0;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
// ../libgen/src/icon-tags.ts
|
|
2128
|
+
const published = {
|
|
2129
|
+
packageFile,
|
|
2130
|
+
publishedVersions,
|
|
2131
|
+
publishedSource,
|
|
2132
|
+
repositoryFiles
|
|
2133
|
+
};
|
|
2134
|
+
/**
|
|
2135
|
+
* Reads a set's tags from its published metadata and keys them by each icon's label. A companion package's version is
|
|
2136
|
+
* the icon package's own when that was published, or else the newest one before it, since companions are released
|
|
2137
|
+
* together but not always every time.
|
|
2138
|
+
*/
|
|
2139
|
+
async function readIconTags(source, iconPackage, icons, fetchers = published) {
|
|
2140
|
+
let data;
|
|
2141
|
+
let from;
|
|
2142
|
+
if ("files" in source) {
|
|
2143
|
+
const { repository, commit } = await fetchers.publishedSource(iconPackage.name, iconPackage.version);
|
|
2144
|
+
if (!repository || !commit) throw new Error(`npm doesn't say which repository and commit ${iconPackage.name}@${iconPackage.version} came from`);
|
|
2145
|
+
data = [(await fetchers.repositoryFiles(repository, commit, glob(source.files))).flatMap(({ text }) => parsed(text))];
|
|
2146
|
+
from = `${repository.replace(/^https:\/\//, "")}@${commit.slice(0, 12)}/${source.files}`;
|
|
2147
|
+
} else {
|
|
2148
|
+
const name = source.package ?? iconPackage.name;
|
|
2149
|
+
const version = name === iconPackage.name ? iconPackage.version : closestVersion(await fetchers.publishedVersions(name), iconPackage.version);
|
|
2150
|
+
if (!version) throw new Error(`${name} has no version at or before ${iconPackage.version}`);
|
|
2151
|
+
const text = await fetchers.packageFile(name, version, source.file);
|
|
2152
|
+
data = source.file.endsWith(".json") ? parsed(text) : [...moduleData(text).values()];
|
|
2153
|
+
from = `${name}@${version}/${source.file}`;
|
|
2154
|
+
}
|
|
2155
|
+
const tags = iconWords(data, icons);
|
|
2156
|
+
return {
|
|
2157
|
+
tags,
|
|
2158
|
+
source: from,
|
|
2159
|
+
matched: Object.keys(tags).length
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
function parsed(text) {
|
|
2163
|
+
try {
|
|
2164
|
+
return [JSON.parse(text)];
|
|
2165
|
+
} catch {
|
|
2166
|
+
return [];
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Words for each icon by label, from metadata of whatever shape a set publishes. Its table of icons is the one that
|
|
2171
|
+
* names most of the set's icons, wherever it sits: an object keyed by icon (`{"circle-alert": [...]}`), or a list of
|
|
2172
|
+
* records with a field that names each (`[{ name: "acorn", tags: [...] }]`). An icon's words are its lists of strings,
|
|
2173
|
+
* and the values of fields that sort icons into groups, such as a category. Fields that barely vary (the styles most
|
|
2174
|
+
* icons come in), words most icons have, and markers that aren't words (`*new*`) are left out. Fields that tell icons
|
|
2175
|
+
* apart come before ones that group many, since a set's first tags count most in search.
|
|
2176
|
+
*/
|
|
2177
|
+
function iconWords(data, icons) {
|
|
2178
|
+
const least = Math.max(1, Math.min(20, Math.floor(icons.length / 2)));
|
|
2179
|
+
let best;
|
|
2180
|
+
const consider = (entries) => {
|
|
2181
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2182
|
+
for (const [key, value] of entries) if (typeof key === "string" && !byKey.has(normalize$1(key))) byKey.set(normalize$1(key), value);
|
|
2183
|
+
const matched = /* @__PURE__ */ new Map();
|
|
2184
|
+
for (const icon of icons) {
|
|
2185
|
+
const key = keysFor(icon).find((candidate) => byKey.has(candidate));
|
|
2186
|
+
if (key) matched.set(icon.label, byKey.get(key));
|
|
2187
|
+
}
|
|
2188
|
+
if (matched.size > (best?.size ?? 0)) best = matched;
|
|
2189
|
+
};
|
|
2190
|
+
const visit = (value, depth) => {
|
|
2191
|
+
if (Array.isArray(value)) {
|
|
2192
|
+
const records = value.filter(isRecord);
|
|
2193
|
+
if (records.length < least) return;
|
|
2194
|
+
for (const field of new Set(records.flatMap((record) => Object.keys(record)))) if (records.some((record) => typeof record[field] === "string")) consider(records.map((record) => [record[field], record]));
|
|
2195
|
+
} else if (isRecord(value)) {
|
|
2196
|
+
const entries = Object.entries(value);
|
|
2197
|
+
if (entries.length >= least) consider(entries);
|
|
2198
|
+
else if (depth < 3) for (const [, inner] of entries) visit(inner, depth + 1);
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2201
|
+
for (const value of data) visit(value, 0);
|
|
2202
|
+
if (!best) return {};
|
|
2203
|
+
const fields = /* @__PURE__ */ new Map();
|
|
2204
|
+
const add = (field, group, label, words) => {
|
|
2205
|
+
if (words.length === 0) return;
|
|
2206
|
+
let found = fields.get(field);
|
|
2207
|
+
if (!found) fields.set(field, found = {
|
|
2208
|
+
group,
|
|
2209
|
+
byLabel: /* @__PURE__ */ new Map()
|
|
2210
|
+
});
|
|
2211
|
+
found.byLabel.set(label, words);
|
|
2212
|
+
};
|
|
2213
|
+
for (const [label, value] of best) if (Array.isArray(value)) add("", false, label, wordsOf(value));
|
|
2214
|
+
else if (isRecord(value)) {
|
|
2215
|
+
for (const [field, inner] of Object.entries(value)) if (Array.isArray(inner)) add(field, false, label, wordsOf(inner));
|
|
2216
|
+
else if (typeof inner === "string") add(field, true, label, wordsOf([inner]).filter((word) => /\p{L}/u.test(word)));
|
|
2217
|
+
}
|
|
2218
|
+
const matched = best.size;
|
|
2219
|
+
const kept = [...fields.values()].flatMap(({ group, byLabel }) => {
|
|
2220
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2221
|
+
for (const word of [...byLabel.values()].flat()) counts.set(word, (counts.get(word) ?? 0) + 1);
|
|
2222
|
+
const distinct = counts.size;
|
|
2223
|
+
if (distinct < Math.min(5, Math.ceil(matched / 10))) return [];
|
|
2224
|
+
if (group && (distinct > matched / 20 || Math.max(...counts.values()) > matched / 2)) return [];
|
|
2225
|
+
return [{
|
|
2226
|
+
byLabel,
|
|
2227
|
+
distinct
|
|
2228
|
+
}];
|
|
2229
|
+
});
|
|
2230
|
+
kept.sort((a, b) => b.distinct - a.distinct);
|
|
2231
|
+
const tags = {};
|
|
2232
|
+
const common = /* @__PURE__ */ new Map();
|
|
2233
|
+
for (const icon of icons) {
|
|
2234
|
+
const words = [...new Set(kept.flatMap(({ byLabel }) => byLabel.get(icon.label) ?? []))];
|
|
2235
|
+
if (words.length === 0) continue;
|
|
2236
|
+
tags[icon.label] = words;
|
|
2237
|
+
for (const word of words) common.set(word, (common.get(word) ?? 0) + 1);
|
|
2238
|
+
}
|
|
2239
|
+
const tagged = Object.keys(tags).length;
|
|
2240
|
+
if (tagged < 8) return tags;
|
|
2241
|
+
for (const [label, words] of Object.entries(tags)) {
|
|
2242
|
+
const specific = words.filter((word) => common.get(word) <= tagged / 4);
|
|
2243
|
+
if (specific.length > 0) tags[label] = specific;
|
|
2244
|
+
else delete tags[label];
|
|
2245
|
+
}
|
|
2246
|
+
return tags;
|
|
2247
|
+
}
|
|
2248
|
+
function isRecord(value) {
|
|
2249
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* A list's words, lowercased and without the punctuation that ends them: its strings and numbers (Tabler's `911`), when
|
|
2253
|
+
* it has any strings at all. One that starts with a symbol is a marker (`*new*`) or a symbol (`->`), not a word.
|
|
2254
|
+
*/
|
|
2255
|
+
function wordsOf(list) {
|
|
2256
|
+
if (!list.some((item) => typeof item === "string")) return [];
|
|
2257
|
+
return list.filter((item) => typeof item === "string" || typeof item === "number").map((item) => String(item).toLowerCase().replaceAll(/\s+/g, " ").trim().replace(/[^\p{L}\p{N}]+$/u, "")).filter((word) => /^[\p{L}\p{N}]/u.test(word));
|
|
2258
|
+
}
|
|
2259
|
+
/** A path where `*` is any one folder or file name, as a regular expression. */
|
|
2260
|
+
function glob(pattern) {
|
|
2261
|
+
const parts = pattern.split("*").map((part) => part.replaceAll(/[.+?^${}()|[\]\\]/g, "\\$&"));
|
|
2262
|
+
return new RegExp(`^${parts.join("[^/]+")}$`);
|
|
2263
|
+
}
|
|
2264
|
+
/** A key by its letters and digits, lowercased: `circle-alert`, `CircleAlert` and `circle_alert` are one key. */
|
|
2265
|
+
function normalize$1(key) {
|
|
2266
|
+
return key.toLowerCase().replaceAll(/[^a-z0-9]/g, "");
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* The keys an icon may be listed under: its label and each name, with and without an `Icon` before or after, and
|
|
2270
|
+
* without the `Ltr` or `Rtl` that names a mirrored copy (Fluent's `TextIndentIncreaseLtr`).
|
|
2271
|
+
*/
|
|
2272
|
+
function keysFor(icon) {
|
|
2273
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2274
|
+
for (const name of [icon.label, ...icon.names]) {
|
|
2275
|
+
const key = normalize$1(name);
|
|
2276
|
+
for (const bare of [
|
|
2277
|
+
key,
|
|
2278
|
+
key.replace(/^icon/, ""),
|
|
2279
|
+
key.replace(/icon$/, "")
|
|
2280
|
+
]) {
|
|
2281
|
+
keys.add(bare);
|
|
2282
|
+
keys.add(bare.replace(/(?:ltr|rtl)$/, ""));
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
keys.delete("");
|
|
2286
|
+
return [...keys];
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* A `.lib.par` as JSON, indented by two spaces, with each icon's tags on a line of their own, so a set's thousands of
|
|
2290
|
+
* icons stay readable and its file half the size.
|
|
2291
|
+
*/
|
|
2292
|
+
function manifestText(manifest) {
|
|
2293
|
+
const icons = manifest.icons;
|
|
2294
|
+
if (!icons?.tags) return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
2295
|
+
const marker = JSON.stringify("\0tags\0");
|
|
2296
|
+
const text = JSON.stringify({
|
|
2297
|
+
...manifest,
|
|
2298
|
+
icons: {
|
|
2299
|
+
...icons,
|
|
2300
|
+
tags: JSON.parse(marker)
|
|
2301
|
+
}
|
|
2302
|
+
}, null, 2);
|
|
2303
|
+
const lines = Object.entries(icons.tags).map(([name, tags]) => ` ${JSON.stringify(name)}: ${JSON.stringify(tags)}`);
|
|
2304
|
+
return `${text.replace(marker, `{\n${lines.join(",\n")}\n }`)}\n`;
|
|
2305
|
+
}
|
|
2306
|
+
/** The version itself when published, or else the newest release before it; prereleases aren't counted. */
|
|
2307
|
+
function closestVersion(published, wanted) {
|
|
2308
|
+
if (published.includes(wanted)) return wanted;
|
|
2309
|
+
const parts = (version) => version.split(".").map(Number);
|
|
2310
|
+
const target = parts(wanted.replace(/-.*$/, ""));
|
|
2311
|
+
const before = (a, b) => {
|
|
2312
|
+
for (let index = 0; index < 3; index++) if ((a[index] ?? 0) !== (b[index] ?? 0)) return (a[index] ?? 0) < (b[index] ?? 0);
|
|
2313
|
+
return false;
|
|
2314
|
+
};
|
|
2315
|
+
let best;
|
|
2316
|
+
for (const version of published) {
|
|
2317
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) continue;
|
|
2318
|
+
const candidate = parts(version);
|
|
2319
|
+
if (!before(candidate, target)) continue;
|
|
2320
|
+
if (!best || before(parts(best), candidate)) best = version;
|
|
2321
|
+
}
|
|
2322
|
+
return best;
|
|
2323
|
+
}
|
|
2324
|
+
// ../libgen/src/parts.ts
|
|
2325
|
+
const notVisual = /(?:Context|Consumer|Provider)$/;
|
|
2326
|
+
const maxDepth = 5;
|
|
2327
|
+
/**
|
|
2328
|
+
* Works out how a library's parts fit together, by rendering each with React's server renderer from the project, so
|
|
2329
|
+
* nothing needs a browser. A part that throws alone usually names what it needs ("`AccordionItem` must be used within
|
|
2330
|
+
* `Accordion`"), which is the strongest signal after a namespace export (`Accordion.Item` belongs in `Accordion.Root`).
|
|
2331
|
+
* Otherwise a shared name prefix (`AccordionItem` under `Accordion`) or a plural (`Tab` under `Tabs`) is tried. Each
|
|
2332
|
+
* chain is rendered again, so a suggestion that doesn't render is reported as such rather than trusted.
|
|
2333
|
+
*/
|
|
2334
|
+
async function relateParts(load, specifier, components, providers = [], edits = {}, examples = /* @__PURE__ */ new Map(), renderedBy = /* @__PURE__ */ new Map(), calls = /* @__PURE__ */ new Map(), listItem, appErrors = {}) {
|
|
2335
|
+
let React;
|
|
2336
|
+
let server;
|
|
2337
|
+
let module;
|
|
2338
|
+
try {
|
|
2339
|
+
React = await load("react");
|
|
2340
|
+
server = await load("react-dom/server");
|
|
2341
|
+
module = await load(specifier);
|
|
2342
|
+
} catch (error) {
|
|
2343
|
+
return {
|
|
2344
|
+
relations: /* @__PURE__ */ new Map(),
|
|
2345
|
+
tried: false,
|
|
2346
|
+
note: `Parts weren't tried: ${message(error)}`
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
const createElement = React.createElement ?? React.default?.createElement;
|
|
2350
|
+
const renderToStaticMarkup = server.renderToStaticMarkup ?? server.default?.renderToStaticMarkup;
|
|
2351
|
+
if (typeof createElement !== "function" || typeof renderToStaticMarkup !== "function") return {
|
|
2352
|
+
relations: /* @__PURE__ */ new Map(),
|
|
2353
|
+
tried: false,
|
|
2354
|
+
note: "Parts weren't tried: React's server renderer wasn't found."
|
|
2355
|
+
};
|
|
2356
|
+
const byName = new Map(components.map((component) => [component.name, component]));
|
|
2357
|
+
const wrappers = [];
|
|
2358
|
+
for (const provider of providers) {
|
|
2359
|
+
byName.delete(provider.component);
|
|
2360
|
+
try {
|
|
2361
|
+
let found = await load(provider.import);
|
|
2362
|
+
for (const part of provider.component.split(".")) found = found?.[part];
|
|
2363
|
+
if (found) wrappers.push({
|
|
2364
|
+
type: found,
|
|
2365
|
+
props: await trialValue(load, provider.props ?? {})
|
|
2366
|
+
});
|
|
2367
|
+
} catch {}
|
|
2368
|
+
}
|
|
2369
|
+
const value = (name) => {
|
|
2370
|
+
const component = byName.get(name);
|
|
2371
|
+
let found = module;
|
|
2372
|
+
for (const part of (component?.export ?? name).split(".")) found = found?.[part];
|
|
2373
|
+
return found;
|
|
2374
|
+
};
|
|
2375
|
+
const samples = /* @__PURE__ */ new Map();
|
|
2376
|
+
for (const component of components) {
|
|
2377
|
+
const edit = edits?.[component.name];
|
|
2378
|
+
samples.set(component.name, {
|
|
2379
|
+
...component.samples,
|
|
2380
|
+
...edit ? edit.sample?.props : {}
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
const madeProps = /* @__PURE__ */ new Map();
|
|
2384
|
+
for (const [name, use] of examples) {
|
|
2385
|
+
const props = Object.fromEntries(Object.entries(use.sample?.props ?? {}).filter(([, value]) => isMadeValue(value)));
|
|
2386
|
+
if (Object.keys(props).length === 0 || !samples.has(name)) continue;
|
|
2387
|
+
madeProps.set(name, props);
|
|
2388
|
+
samples.set(name, {
|
|
2389
|
+
...samples.get(name),
|
|
2390
|
+
...props
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
const made = /* @__PURE__ */ new Map();
|
|
2394
|
+
const byCall = /* @__PURE__ */ new Map();
|
|
2395
|
+
const callsIn = (value) => {
|
|
2396
|
+
if (isMadeValue(value)) return [value];
|
|
2397
|
+
if (Array.isArray(value)) return value.flatMap(callsIn);
|
|
2398
|
+
return typeof value === "object" && value !== null ? Object.values(value).flatMap(callsIn) : [];
|
|
2399
|
+
};
|
|
2400
|
+
for (const use of examples.values()) for (const call of callsIn(use.sample)) {
|
|
2401
|
+
const key = JSON.stringify(call);
|
|
2402
|
+
if (!byCall.has(key)) try {
|
|
2403
|
+
byCall.set(key, await trialValue(load, call));
|
|
2404
|
+
} catch {
|
|
2405
|
+
byCall.set(key, void 0);
|
|
2406
|
+
}
|
|
2407
|
+
made.set(call, byCall.get(key));
|
|
2408
|
+
}
|
|
2409
|
+
/** Each distinct call the examples make, to try as a value an owner works out. */
|
|
2410
|
+
const madeCalls = [...new Map([...made.keys()].map((call) => [JSON.stringify(call), call])).values()];
|
|
2411
|
+
const cloneElement = React.cloneElement ?? React.default?.cloneElement;
|
|
2412
|
+
/** A render function as the part calls it: its children, with what it's called with spread on when asked. */
|
|
2413
|
+
const live = (render) => (argument) => {
|
|
2414
|
+
const elements = render.$render.map((item) => build(item));
|
|
2415
|
+
if (elements.length === 1 && render.pass) return cloneElement(elements[0], Object.fromEntries(render.pass.map((prop) => [prop, argument])));
|
|
2416
|
+
if (render.spread && elements.length === 1 && typeof argument === "object" && argument !== null) return cloneElement(elements[0], argument);
|
|
2417
|
+
return elements.length === 1 ? elements[0] : elements;
|
|
2418
|
+
};
|
|
2419
|
+
const element = (value) => {
|
|
2420
|
+
const elements = value.$element.map((item) => build(item));
|
|
2421
|
+
return elements.length === 1 ? elements[0] : elements;
|
|
2422
|
+
};
|
|
2423
|
+
const liveValue = (item) => {
|
|
2424
|
+
if (isSampleRender(item)) return live(item);
|
|
2425
|
+
if (isSampleElement(item)) return element(item);
|
|
2426
|
+
if (isMadeValue(item)) return made.get(item);
|
|
2427
|
+
return Array.isArray(item) && item.some(isMadeValue) ? item.map((each) => isMadeValue(each) ? made.get(each) : each) : item;
|
|
2428
|
+
};
|
|
2429
|
+
const liveProps = (props) => Object.fromEntries(Object.entries(props ?? {}).map(([key, item]) => [key, liveValue(item)]));
|
|
2430
|
+
/**
|
|
2431
|
+
* A sample child's element. The part being tried takes its sample's props; a child a sample spells out (with props or
|
|
2432
|
+
* children) is as written, as the app inserts it, and only a bare one takes its own sample's.
|
|
2433
|
+
*/
|
|
2434
|
+
const build = (child, own = false) => {
|
|
2435
|
+
if (typeof child === "string") return child;
|
|
2436
|
+
if (isSampleRender(child)) return live(child);
|
|
2437
|
+
const type = /^[a-z]/.test(child.component) ? child.component : value(child.component);
|
|
2438
|
+
const bare = !child.props && !child.children;
|
|
2439
|
+
return createElement(type, liveProps({
|
|
2440
|
+
...own || bare ? samples.get(child.component) : {},
|
|
2441
|
+
...child.props
|
|
2442
|
+
}), ...(child.children ?? []).map((inner) => build(inner)));
|
|
2443
|
+
};
|
|
2444
|
+
/** Parts whose parents need the other parts their examples hold around them. */
|
|
2445
|
+
const withParts = /* @__PURE__ */ new Set();
|
|
2446
|
+
/**
|
|
2447
|
+
* The part inside the first parent's whole example, each level in place of the first like it there and the part in
|
|
2448
|
+
* place of the first like it at the bottom (a cell in a table's first row, beside that row's other cells and the
|
|
2449
|
+
* table's header), as the app inserts a part marked `parentParts`. A level the example doesn't hold comes alone.
|
|
2450
|
+
*/
|
|
2451
|
+
const inExample = (chain, name, part) => {
|
|
2452
|
+
const inside = (level, given) => {
|
|
2453
|
+
const target = level < chain.length ? chain[level] : name;
|
|
2454
|
+
const items = given ?? [];
|
|
2455
|
+
const at = items.findIndex((item) => typeof item === "object" && "component" in item && item.component === target);
|
|
2456
|
+
const found = at === -1 ? void 0 : items[at];
|
|
2457
|
+
let here = part;
|
|
2458
|
+
if (level < chain.length) {
|
|
2459
|
+
const written = found && (found.props || found.children);
|
|
2460
|
+
here = createElement(value(target), liveProps(written ? found.props : samples.get(target)), ...inside(level + 1, written ? found.children : examples.get(target)?.sample?.children));
|
|
2461
|
+
}
|
|
2462
|
+
return at === -1 ? [here] : items.map((item, index) => index === at ? here : build(item));
|
|
2463
|
+
};
|
|
2464
|
+
return createElement(value(chain[0]), liveProps(samples.get(chain[0])), ...inside(1, examples.get(chain[0])?.sample?.children));
|
|
2465
|
+
};
|
|
2466
|
+
/** Renders a part inside a chain of parents; the error it throws, or undefined when it draws. */
|
|
2467
|
+
const attempt = (chain, name, children, parts = withParts.has(name)) => render(placed(chain, name, build({
|
|
2468
|
+
component: name,
|
|
2469
|
+
...children ? { children } : {}
|
|
2470
|
+
}, true), parts));
|
|
2471
|
+
/** A part's element inside a chain of parents. */
|
|
2472
|
+
const placed = (chain, name, part, parts) => {
|
|
2473
|
+
const calling = chain.some((parent) => renderSlotOf(examples.get(parent)?.sample));
|
|
2474
|
+
if (parts && chain.length > 0 && !calling) return inExample(chain, name, part);
|
|
2475
|
+
let element = part;
|
|
2476
|
+
for (const parent of [...chain].reverse()) {
|
|
2477
|
+
const slot = renderSlotOf(examples.get(parent)?.sample);
|
|
2478
|
+
const inner = element;
|
|
2479
|
+
const render = slot ? (argument) => {
|
|
2480
|
+
const spread = slot.render.spread && typeof argument === "object" && argument ? argument : {};
|
|
2481
|
+
const named = Object.fromEntries((slot.render.pass ?? []).map((prop) => [prop, argument]));
|
|
2482
|
+
return slot.render.pass || slot.render.spread ? cloneElement(inner, {
|
|
2483
|
+
...spread,
|
|
2484
|
+
...named
|
|
2485
|
+
}) : inner;
|
|
2486
|
+
} : void 0;
|
|
2487
|
+
const props = liveProps(samples.get(parent));
|
|
2488
|
+
element = slot && slot.prop !== "children" ? createElement(value(parent), {
|
|
2489
|
+
...props,
|
|
2490
|
+
[slot.prop]: render
|
|
2491
|
+
}) : createElement(value(parent), props, render ?? element);
|
|
2492
|
+
}
|
|
2493
|
+
return element;
|
|
2494
|
+
};
|
|
2495
|
+
/** What an element renders on the server, or undefined when it throws. */
|
|
2496
|
+
const markup = (element) => {
|
|
2497
|
+
const quiet = [console.error, console.warn];
|
|
2498
|
+
console.error = () => void 0;
|
|
2499
|
+
console.warn = () => void 0;
|
|
2500
|
+
try {
|
|
2501
|
+
return renderToStaticMarkup(element).replace(/_R_\w+_/g, "");
|
|
2502
|
+
} catch {
|
|
2503
|
+
return;
|
|
2504
|
+
} finally {
|
|
2505
|
+
[console.error, console.warn] = quiet;
|
|
2506
|
+
}
|
|
2507
|
+
};
|
|
2508
|
+
const render = (element, wrapped = true) => {
|
|
2509
|
+
if (wrapped) for (const provider of [...usable].reverse()) element = createElement(provider.type, provider.props, element);
|
|
2510
|
+
const quiet = [console.error, console.warn];
|
|
2511
|
+
console.error = () => void 0;
|
|
2512
|
+
console.warn = () => void 0;
|
|
2513
|
+
try {
|
|
2514
|
+
if (/^<(?:item|submenutrigger|tablebody|tableheader|column|cell|loader|separator)[\s>]/.test(renderToStaticMarkup(element))) return "Renders only as a placeholder outside a collection.";
|
|
2515
|
+
return;
|
|
2516
|
+
} catch (error) {
|
|
2517
|
+
return message(error);
|
|
2518
|
+
} finally {
|
|
2519
|
+
[console.error, console.warn] = quiet;
|
|
2520
|
+
}
|
|
2521
|
+
};
|
|
2522
|
+
const usable = wrappers.filter((provider) => render(createElement(provider.type, provider.props, "x"), false) === void 0);
|
|
2523
|
+
const names = [...byName.keys()];
|
|
2524
|
+
const named = (error, self, chain, guess = true) => {
|
|
2525
|
+
const mentions = (text) => [...[...text.matchAll(/[`<"']([A-Z][\w.]*)/g)].map((match) => match[1]), ...[...text.matchAll(/\b([A-Z][\w.]*)/g)].map((match) => match[1])];
|
|
2526
|
+
const all = [...mentions(error.split(/\b(?:within|inside|outside|wrap|wrapped|placed in|used in)\b/i).slice(1).join(" ")), ...mentions(error)];
|
|
2527
|
+
for (const mention of all.flatMap((name) => [name.replace(/(?:Props)?(?:Provider|Context)$/, ""), name])) {
|
|
2528
|
+
const target = resolveName(mention.replace(/\.$/, ""), self, names);
|
|
2529
|
+
if (target && target !== self && !chain.includes(target)) return target;
|
|
2530
|
+
const provider = `${self}Provider`;
|
|
2531
|
+
if (target === self && !chain.includes(provider) && value(provider)) return provider;
|
|
2532
|
+
}
|
|
2533
|
+
if (!guess) return void 0;
|
|
2534
|
+
const content = namespaceOf(self) && !self.endsWith(".Root") && `${namespaceOf(self)}.Content`;
|
|
2535
|
+
return content && names.includes(content) && content !== self && !chain.includes(content) ? content : void 0;
|
|
2536
|
+
};
|
|
2537
|
+
const relations = /* @__PURE__ */ new Map();
|
|
2538
|
+
/** Parts whose sample has example props they render without, with the props they had before. */
|
|
2539
|
+
const loose = /* @__PURE__ */ new Map();
|
|
2540
|
+
const aloneErrors = /* @__PURE__ */ new Map();
|
|
2541
|
+
const failsAlone = (name) => {
|
|
2542
|
+
if (!aloneErrors.has(name)) aloneErrors.set(name, attempt([], name));
|
|
2543
|
+
return aloneErrors.get(name) !== void 0;
|
|
2544
|
+
};
|
|
2545
|
+
/** How many of each part's placed example uses have each parent, for marking the levels some leave out. */
|
|
2546
|
+
const shares = /* @__PURE__ */ new Map();
|
|
2547
|
+
const levelsOf = (name, sameFamily, taken, share) => (exampleLevels(examples, name, sameFamily, share) ?? []).filter((level) => names.includes(level.parent) && level.parent !== name && !taken.includes(level.parent));
|
|
2548
|
+
/**
|
|
2549
|
+
* The chain the examples show: every parent most of its uses are inside, at any depth, in their usual order. One
|
|
2550
|
+
* that draws alone takes only its own family's, since examples put everything in forms. Examples split across a
|
|
2551
|
+
* demo's own components show only the inner levels, so while the part still fails, the top one's own are added.
|
|
2552
|
+
*/
|
|
2553
|
+
const exampleChain = (name) => {
|
|
2554
|
+
const levels = levelsOf(name, !failsAlone(name), []);
|
|
2555
|
+
if (levels.length === 0) return void 0;
|
|
2556
|
+
shares.set(name, new Map(levels.map((level) => [level.parent, level.share])));
|
|
2557
|
+
const chain = levels.map((level) => level.parent);
|
|
2558
|
+
if (heldChain(name, chain)) return chain;
|
|
2559
|
+
while (chain.length < 10 && attempt(chain, name)) {
|
|
2560
|
+
const up = levelsOf(chain[0], false, chain);
|
|
2561
|
+
if (up.length === 0) break;
|
|
2562
|
+
chain.unshift(...up.map((level) => level.parent));
|
|
2563
|
+
}
|
|
2564
|
+
return chain;
|
|
2565
|
+
};
|
|
2566
|
+
/**
|
|
2567
|
+
* The chain, or the chain without the levels some examples leave out, that a part renders in with the other parts
|
|
2568
|
+
* the examples hold around it (a table's header beside the body a row is in).
|
|
2569
|
+
*/
|
|
2570
|
+
const heldChain = (name, chain) => {
|
|
2571
|
+
const usual = chain.filter((parent) => (shares.get(name)?.get(parent) ?? 1) >= .9);
|
|
2572
|
+
const adds = (each) => markup(placed(each, name, build({ component: name }, true), true)) !== markup(placed(each, name, null, true));
|
|
2573
|
+
return [chain, usual].find((each) => each.length > 0 && !attempt(each, name, void 0, true) && adds(each));
|
|
2574
|
+
};
|
|
2575
|
+
/**
|
|
2576
|
+
* The levels some examples leave out (fewer than nine in ten have them) that the part still renders without, taken
|
|
2577
|
+
* away one at a time so each is tried without the others too.
|
|
2578
|
+
*/
|
|
2579
|
+
const optionalOf = (name, chain) => {
|
|
2580
|
+
const shown = shares.get(name);
|
|
2581
|
+
const optional = [];
|
|
2582
|
+
for (const parent of [...chain].reverse()) {
|
|
2583
|
+
const share = shown?.get(parent);
|
|
2584
|
+
if (share === void 0 || share >= .9) continue;
|
|
2585
|
+
const without = chain.filter((other) => other !== parent && !optional.includes(other));
|
|
2586
|
+
if (!attempt(without, name)) optional.push(parent);
|
|
2587
|
+
}
|
|
2588
|
+
return optional;
|
|
2589
|
+
};
|
|
2590
|
+
/** A part's family, as the examples place it (`Table` for React Aria's `Row`). */
|
|
2591
|
+
const familyIn = (name) => examples.get(name)?.family ?? familyOf(name);
|
|
2592
|
+
/** Drops parents from other families that a part renders without: where an example put it, not what it needs. */
|
|
2593
|
+
const prune = (chain, name) => {
|
|
2594
|
+
for (let dropped = true; dropped;) {
|
|
2595
|
+
dropped = false;
|
|
2596
|
+
for (const parent of chain) {
|
|
2597
|
+
if (familyIn(parent) === familyIn(name) && !parent.endsWith("Portal")) continue;
|
|
2598
|
+
const without = chain.filter((other) => other !== parent);
|
|
2599
|
+
if (without.length > 0 && !attempt(without, name)) {
|
|
2600
|
+
chain = without;
|
|
2601
|
+
dropped = true;
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
return chain;
|
|
2606
|
+
};
|
|
2607
|
+
const chains = /* @__PURE__ */ new Map();
|
|
2608
|
+
const chainOf = (name, depth) => {
|
|
2609
|
+
if (chains.has(name)) return chains.get(name);
|
|
2610
|
+
const edit = edits?.[name];
|
|
2611
|
+
if (edit && edit.parents) return save(name, {
|
|
2612
|
+
chain: edit.parents,
|
|
2613
|
+
from: "edit"
|
|
2614
|
+
});
|
|
2615
|
+
chains.set(name, null);
|
|
2616
|
+
const shown = exampleChain(name);
|
|
2617
|
+
if (shown && !attempt(shown, name)) return save(name, {
|
|
2618
|
+
chain: prune(shown, name),
|
|
2619
|
+
from: "example"
|
|
2620
|
+
});
|
|
2621
|
+
const held = shown && attempt(shown, name) ? heldChain(name, shown) : void 0;
|
|
2622
|
+
if (held) {
|
|
2623
|
+
withParts.add(name);
|
|
2624
|
+
return save(name, {
|
|
2625
|
+
chain: held,
|
|
2626
|
+
from: "example"
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
const pruned = shown && prune(shown, name);
|
|
2630
|
+
if (pruned && pruned.length > 0 && !attempt(pruned, name)) return save(name, {
|
|
2631
|
+
chain: pruned,
|
|
2632
|
+
from: "example"
|
|
2633
|
+
});
|
|
2634
|
+
if (shown) for (const share of [1 / 3, 1 / 5]) {
|
|
2635
|
+
const wider = levelsOf(name, !failsAlone(name), [], share).map((level) => level.parent);
|
|
2636
|
+
if (wider.length > shown.length && !attempt(wider, name)) return save(name, {
|
|
2637
|
+
chain: prune(wider, name),
|
|
2638
|
+
from: "example"
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2641
|
+
const grown = shown ? grow(name, depth, shown, "example") : void 0;
|
|
2642
|
+
if (grown && !grown.error) return save(name, {
|
|
2643
|
+
chain: prune(grown.chain, name),
|
|
2644
|
+
from: grown.from ?? "example"
|
|
2645
|
+
});
|
|
2646
|
+
const fromErrors = grow(name, depth, [], void 0);
|
|
2647
|
+
const stray = fromErrors.chain.some((parent) => drawnParts.has(parent) && !drawnInside.get(parent)?.has(name));
|
|
2648
|
+
if (!fromErrors.error && fromErrors.chain.length > 0 && !stray) return save(name, {
|
|
2649
|
+
chain: fromErrors.chain,
|
|
2650
|
+
from: fromErrors.from ?? "prefix"
|
|
2651
|
+
});
|
|
2652
|
+
if (!shown && failsAlone(name)) for (const owner of renderedBy.get(name) ?? []) {
|
|
2653
|
+
if (!names.includes(owner) || owner === name) continue;
|
|
2654
|
+
const chain = [...(chainOf(owner, depth + 1)?.chain ?? []).filter((parent) => parent !== name), owner];
|
|
2655
|
+
if (!attempt(chain, name)) return save(name, {
|
|
2656
|
+
chain,
|
|
2657
|
+
from: "source"
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
const result = !grown || fromErrors.chain.length > 0 ? fromErrors : grown;
|
|
2661
|
+
const chain = result.error ? result.chain.filter((parent) => familyIn(parent) === familyIn(name)) : result.chain;
|
|
2662
|
+
return save(name, chain.length > 0 ? {
|
|
2663
|
+
chain,
|
|
2664
|
+
from: result.from ?? "prefix"
|
|
2665
|
+
} : null);
|
|
2666
|
+
};
|
|
2667
|
+
/**
|
|
2668
|
+
* The examples mostly use it with nothing of the library's around it (Chakra's `InputGroup`, around an `Input`), so its
|
|
2669
|
+
* name doesn't say where it goes.
|
|
2670
|
+
*/
|
|
2671
|
+
const standsAlone = (name) => {
|
|
2672
|
+
const use = examples.get(name);
|
|
2673
|
+
return !!use && use.count >= 2 && use.topLevel * 2 >= use.count;
|
|
2674
|
+
};
|
|
2675
|
+
const grow = (name, depth, start, startFrom) => {
|
|
2676
|
+
let chain = start;
|
|
2677
|
+
let from = startFrom;
|
|
2678
|
+
let error = attempt(chain, name);
|
|
2679
|
+
for (let step = 0; error && step < maxDepth && depth < maxDepth; step++) {
|
|
2680
|
+
const mentioned = named(error, name, chain);
|
|
2681
|
+
let next;
|
|
2682
|
+
let signal;
|
|
2683
|
+
if (mentioned) [next, signal] = [mentioned, "error"];
|
|
2684
|
+
else if (chain.length === 0 && !standsAlone(name)) [next, signal] = guessParent(name, names);
|
|
2685
|
+
if (!next) break;
|
|
2686
|
+
from ??= signal;
|
|
2687
|
+
const above = chainOf(next, depth + 1)?.chain ?? [];
|
|
2688
|
+
const rest = chain.filter((parent) => !above.includes(parent) && parent !== next);
|
|
2689
|
+
const inner = [.../* @__PURE__ */ new Set([
|
|
2690
|
+
...above,
|
|
2691
|
+
...rest,
|
|
2692
|
+
next
|
|
2693
|
+
])];
|
|
2694
|
+
const outer = [.../* @__PURE__ */ new Set([
|
|
2695
|
+
...above,
|
|
2696
|
+
next,
|
|
2697
|
+
...rest
|
|
2698
|
+
])];
|
|
2699
|
+
const bare = [.../* @__PURE__ */ new Set([...rest, next])];
|
|
2700
|
+
const rare = levelsOf(next, false, [name], .2).map((level) => level.parent);
|
|
2701
|
+
const wide = [.../* @__PURE__ */ new Set([
|
|
2702
|
+
...rare,
|
|
2703
|
+
...rest,
|
|
2704
|
+
next
|
|
2705
|
+
])];
|
|
2706
|
+
const tried = [
|
|
2707
|
+
inner,
|
|
2708
|
+
...rest.length > 0 ? [outer] : [],
|
|
2709
|
+
...above.length > 0 ? [bare] : [],
|
|
2710
|
+
...rare.some((level) => !inner.includes(level)) ? [wide] : []
|
|
2711
|
+
].map((candidate) => ({
|
|
2712
|
+
candidate,
|
|
2713
|
+
error: attempt(candidate, name)
|
|
2714
|
+
}));
|
|
2715
|
+
const pick = tried.find((option) => !option.error) ?? (tried[0].error === error ? tried.find((option) => option.error !== error) : void 0) ?? tried[0];
|
|
2716
|
+
chain = pick.candidate;
|
|
2717
|
+
error = pick.error;
|
|
2718
|
+
}
|
|
2719
|
+
if (chain.length === 0) {
|
|
2720
|
+
const [parent, signal] = guessParent(name, names);
|
|
2721
|
+
if (parent && signal === "namespace") return {
|
|
2722
|
+
chain: [parent],
|
|
2723
|
+
from: signal,
|
|
2724
|
+
error
|
|
2725
|
+
};
|
|
2726
|
+
}
|
|
2727
|
+
return {
|
|
2728
|
+
chain,
|
|
2729
|
+
from,
|
|
2730
|
+
error
|
|
2731
|
+
};
|
|
2732
|
+
};
|
|
2733
|
+
const save = (name, result) => {
|
|
2734
|
+
const chain = result?.chain.filter((parent) => parent !== name);
|
|
2735
|
+
const saved = result && chain && chain.length > 0 ? {
|
|
2736
|
+
...result,
|
|
2737
|
+
chain
|
|
2738
|
+
} : null;
|
|
2739
|
+
chains.set(name, saved);
|
|
2740
|
+
return saved;
|
|
2741
|
+
};
|
|
2742
|
+
const missing = [];
|
|
2743
|
+
/** Parts drawn from a list the library keeps that render with an item of it. */
|
|
2744
|
+
const drawnParts = /* @__PURE__ */ new Set();
|
|
2745
|
+
/** What the package puts inside each of them. */
|
|
2746
|
+
const drawnInside = /* @__PURE__ */ new Map();
|
|
2747
|
+
for (const component of components) {
|
|
2748
|
+
const relation = {};
|
|
2749
|
+
const short = component.name.slice(component.name.lastIndexOf(".") + 1);
|
|
2750
|
+
if (notVisual.test(short)) relation.visual = false;
|
|
2751
|
+
const found = value(component.name);
|
|
2752
|
+
if (typeof found !== "function" && (typeof found !== "object" || found === null)) {
|
|
2753
|
+
relations.set(component.name, { visual: false });
|
|
2754
|
+
missing.push(component.name);
|
|
2755
|
+
continue;
|
|
2756
|
+
}
|
|
2757
|
+
const alone = attempt([], component.name);
|
|
2758
|
+
if (alone) relation.alone = alone;
|
|
2759
|
+
const chain = chainOf(component.name, 0);
|
|
2760
|
+
if (chain && chain.chain.length > 0) {
|
|
2761
|
+
relation.parents = chain.chain;
|
|
2762
|
+
if (chain.from) relation.parentsFrom = chain.from;
|
|
2763
|
+
if (withParts.has(component.name)) relation.parentParts = true;
|
|
2764
|
+
if (chain.from === "example" && !attempt(chain.chain, component.name)) {
|
|
2765
|
+
const optional = optionalOf(component.name, chain.chain);
|
|
2766
|
+
if (optional.length > 0) relation.optionalParents = optional;
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
const edit = edits?.[component.name];
|
|
2770
|
+
if (edit && edit.visual === false) relation.visual = false;
|
|
2771
|
+
const editedChildren = edit ? edit.sample?.children : void 0;
|
|
2772
|
+
let inside = alone ? attempt(relation.parents ?? [], component.name, editedChildren) : void 0;
|
|
2773
|
+
if (editedChildren && !inside) relation.sample = { children: editedChildren };
|
|
2774
|
+
if (inside && /Children\.only|single React element|valid child|element child|should be an element|accepts ref/i.test(inside)) {
|
|
2775
|
+
const children = [{
|
|
2776
|
+
component: "span",
|
|
2777
|
+
children: ["Text"]
|
|
2778
|
+
}];
|
|
2779
|
+
if (!attempt(relation.parents ?? [], component.name, children)) {
|
|
2780
|
+
relation.sample = { children };
|
|
2781
|
+
inside = void 0;
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
if (Object.keys(component.samples).length > 0 && !inside) relation.sample = {
|
|
2785
|
+
...relation.sample,
|
|
2786
|
+
props: component.samples
|
|
2787
|
+
};
|
|
2788
|
+
const use = examples.get(component.name);
|
|
2789
|
+
let shown = use?.sample;
|
|
2790
|
+
if (shown && (shown.props || shown.children) && !editedChildren) {
|
|
2791
|
+
const before = samples.get(component.name);
|
|
2792
|
+
samples.set(component.name, {
|
|
2793
|
+
...before,
|
|
2794
|
+
...shown.props
|
|
2795
|
+
});
|
|
2796
|
+
let failed = attempt(relation.parents ?? [], component.name, shown.children);
|
|
2797
|
+
if (failed && inside && use?.rare) {
|
|
2798
|
+
samples.set(component.name, {
|
|
2799
|
+
...before,
|
|
2800
|
+
...shown.props,
|
|
2801
|
+
...use?.rare
|
|
2802
|
+
});
|
|
2803
|
+
failed = attempt(relation.parents ?? [], component.name, shown.children);
|
|
2804
|
+
if (!failed) shown = {
|
|
2805
|
+
...shown,
|
|
2806
|
+
props: {
|
|
2807
|
+
...shown.props,
|
|
2808
|
+
...use?.rare
|
|
2809
|
+
}
|
|
2810
|
+
};
|
|
2811
|
+
}
|
|
2812
|
+
if (failed || !inside) samples.set(component.name, before ?? {});
|
|
2813
|
+
if (!failed) {
|
|
2814
|
+
relation.sample = {
|
|
2815
|
+
props: {
|
|
2816
|
+
...component.samples,
|
|
2817
|
+
...shown.props
|
|
2818
|
+
},
|
|
2819
|
+
...shown.children ? { children: shown.children } : {}
|
|
2820
|
+
};
|
|
2821
|
+
if (Object.keys(relation.sample.props).length === 0) delete relation.sample.props;
|
|
2822
|
+
relation.sampleFrom = "example";
|
|
2823
|
+
if (!inside && shown.props) loose.set(component.name, component.samples);
|
|
2824
|
+
inside = void 0;
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
for (const call of inside ? calls.get(component.name) ?? [] : []) {
|
|
2828
|
+
if (!names.includes(call.owner)) continue;
|
|
2829
|
+
const owner = {
|
|
2830
|
+
...samples.get(call.owner),
|
|
2831
|
+
...examples.get(call.owner)?.sample?.props
|
|
2832
|
+
};
|
|
2833
|
+
const props = { ...component.samples };
|
|
2834
|
+
const worked = [];
|
|
2835
|
+
for (const [key, passed] of Object.entries(call.props)) {
|
|
2836
|
+
let value;
|
|
2837
|
+
if ("forward" in passed) value = owner[passed.forward];
|
|
2838
|
+
else if ("value" in passed) value = passed.value;
|
|
2839
|
+
if (value !== void 0) props[key] = value;
|
|
2840
|
+
else if (!(key in props)) worked.push(key);
|
|
2841
|
+
}
|
|
2842
|
+
const before = samples.get(component.name);
|
|
2843
|
+
const tries = [void 0, ...worked.length > 0 ? madeCalls : []];
|
|
2844
|
+
let failed;
|
|
2845
|
+
for (const made of tries) {
|
|
2846
|
+
if (made) props[worked[0]] = made;
|
|
2847
|
+
samples.set(component.name, props);
|
|
2848
|
+
failed = attempt(relation.parents ?? [], component.name);
|
|
2849
|
+
if (!failed) break;
|
|
2850
|
+
}
|
|
2851
|
+
if (failed) samples.set(component.name, before ?? {});
|
|
2852
|
+
else {
|
|
2853
|
+
relation.sample = {
|
|
2854
|
+
...relation.sample,
|
|
2855
|
+
props
|
|
2856
|
+
};
|
|
2857
|
+
inside = void 0;
|
|
2858
|
+
break;
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
const drawn = listItem ? (calls.get(component.name) ?? []).find((call) => Object.values(call.props).some((passed) => "item" in passed)) : void 0;
|
|
2862
|
+
const inDrawn = (relation.parents ?? []).some((parent) => drawnParts.has(parent));
|
|
2863
|
+
if (drawn || inside && inDrawn) {
|
|
2864
|
+
const before = samples.get(component.name);
|
|
2865
|
+
const props = { ...before };
|
|
2866
|
+
for (const [key, passed] of Object.entries(drawn?.props ?? {})) if ("item" in passed) props[key] = {
|
|
2867
|
+
id: "1",
|
|
2868
|
+
...listItem
|
|
2869
|
+
};
|
|
2870
|
+
samples.set(component.name, props);
|
|
2871
|
+
let parents = relation.parents ?? [];
|
|
2872
|
+
let failed = attempt(parents, component.name);
|
|
2873
|
+
for (let tries = 0; failed && tries < 2; tries++) {
|
|
2874
|
+
const needed = named(failed, component.name, parents);
|
|
2875
|
+
if (!needed) break;
|
|
2876
|
+
const next = attempt([needed, ...parents], component.name);
|
|
2877
|
+
if (next === failed) break;
|
|
2878
|
+
parents = [needed, ...parents];
|
|
2879
|
+
failed = next;
|
|
2880
|
+
}
|
|
2881
|
+
if (failed) samples.set(component.name, before ?? {});
|
|
2882
|
+
else {
|
|
2883
|
+
if (parents.length > (relation.parents?.length ?? 0)) {
|
|
2884
|
+
relation.parents = parents;
|
|
2885
|
+
relation.parentsFrom = "error";
|
|
2886
|
+
delete relation.optionalParents;
|
|
2887
|
+
}
|
|
2888
|
+
if (drawn) {
|
|
2889
|
+
drawnParts.add(component.name);
|
|
2890
|
+
const own = exportedChildren(drawn.children ?? [], names);
|
|
2891
|
+
drawnInside.set(component.name, new Set(componentsIn(own)));
|
|
2892
|
+
const children = own.length > 0 ? own : relation.sample?.children ?? [];
|
|
2893
|
+
relation.sample = {
|
|
2894
|
+
props,
|
|
2895
|
+
...children.length > 0 ? { children } : {}
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
inside = void 0;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
const madeHere = madeProps.get(component.name);
|
|
2902
|
+
if (madeHere) relation.sample = {
|
|
2903
|
+
...relation.sample,
|
|
2904
|
+
props: {
|
|
2905
|
+
...madeHere,
|
|
2906
|
+
...relation.sample?.props
|
|
2907
|
+
}
|
|
2908
|
+
};
|
|
2909
|
+
if (inside) relation.inside = inside;
|
|
2910
|
+
relations.set(component.name, relation);
|
|
2911
|
+
}
|
|
2912
|
+
for (const [name, relation] of relations) {
|
|
2913
|
+
if (!relation.inside || !relation.parents) continue;
|
|
2914
|
+
if (!attempt(relation.parents, name, relation.sample?.children)) delete relation.inside;
|
|
2915
|
+
}
|
|
2916
|
+
const asInserted = (chain, name, children) => {
|
|
2917
|
+
const saved = chain.map((parent) => [parent, samples.get(parent)]);
|
|
2918
|
+
for (const parent of chain) if (loose.has(parent)) samples.set(parent, {
|
|
2919
|
+
...samples.get(parent),
|
|
2920
|
+
...relations.get(parent)?.sample?.props
|
|
2921
|
+
});
|
|
2922
|
+
const error = attempt(chain, name, children);
|
|
2923
|
+
for (const [parent, sample] of saved) samples.set(parent, sample ?? {});
|
|
2924
|
+
return error;
|
|
2925
|
+
};
|
|
2926
|
+
for (const [name, relation] of relations) {
|
|
2927
|
+
if (!relation.parents || relation.inside || relation.visual === false) continue;
|
|
2928
|
+
if (!relation.parents.some((parent) => loose.has(parent))) continue;
|
|
2929
|
+
if (!asInserted(relation.parents, name, relation.sample?.children)) continue;
|
|
2930
|
+
const cleared = [];
|
|
2931
|
+
for (const parent of relation.parents) {
|
|
2932
|
+
const generated = loose.get(parent);
|
|
2933
|
+
const sample = relations.get(parent)?.sample;
|
|
2934
|
+
if (!generated || !sample) continue;
|
|
2935
|
+
loose.delete(parent);
|
|
2936
|
+
const made = Object.entries(sample.props ?? {}).filter(([, value]) => isMadeValue(value) || Array.isArray(value) && value.some(isMadeValue));
|
|
2937
|
+
const kept = {
|
|
2938
|
+
...generated,
|
|
2939
|
+
...Object.fromEntries(made)
|
|
2940
|
+
};
|
|
2941
|
+
if (made.length > 0) cleared.push([
|
|
2942
|
+
parent,
|
|
2943
|
+
generated,
|
|
2944
|
+
kept
|
|
2945
|
+
]);
|
|
2946
|
+
if (Object.keys(kept).length > 0) sample.props = kept;
|
|
2947
|
+
else delete sample.props;
|
|
2948
|
+
}
|
|
2949
|
+
if (cleared.length > 0) {
|
|
2950
|
+
const saved = cleared.map(([parent]) => [parent, samples.get(parent)]);
|
|
2951
|
+
for (const [parent, , kept] of cleared) samples.set(parent, kept);
|
|
2952
|
+
const fails = attempt(relation.parents, name, relation.sample?.children);
|
|
2953
|
+
for (const [parent, sample] of saved) samples.set(parent, sample ?? {});
|
|
2954
|
+
if (fails) for (const [parent, generated] of cleared) {
|
|
2955
|
+
const sample = relations.get(parent).sample;
|
|
2956
|
+
if (Object.keys(generated).length > 0) sample.props = generated;
|
|
2957
|
+
else delete sample.props;
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
for (const parent of relation.parents) {
|
|
2961
|
+
const sample = relations.get(parent)?.sample;
|
|
2962
|
+
if (sample && !sample.props && !sample.children) delete relations.get(parent).sample;
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
for (const [name, error] of Object.entries(appErrors)) {
|
|
2966
|
+
const relation = relations.get(name);
|
|
2967
|
+
if (!relation || relation.visual === false) continue;
|
|
2968
|
+
if (/void element|self-closing tag/.test(error)) {
|
|
2969
|
+
if (!relation.sample?.children) relation.sample = {
|
|
2970
|
+
...relation.sample,
|
|
2971
|
+
children: []
|
|
2972
|
+
};
|
|
2973
|
+
continue;
|
|
2974
|
+
}
|
|
2975
|
+
if (relation.parentsFrom === "edit") continue;
|
|
2976
|
+
const parents = relation.parents ?? [];
|
|
2977
|
+
const needed = named(error, name, parents, false);
|
|
2978
|
+
if (!needed) {
|
|
2979
|
+
const shown = exampleChain(name);
|
|
2980
|
+
if (shown && shown.join() !== parents.join() && !attempt(shown, name, relation.sample?.children)) {
|
|
2981
|
+
relation.parents = shown;
|
|
2982
|
+
relation.parentsFrom = "app";
|
|
2983
|
+
delete relation.optionalParents;
|
|
2984
|
+
}
|
|
2985
|
+
continue;
|
|
2986
|
+
}
|
|
2987
|
+
const outer = (relations.get(needed)?.parents ?? []).filter((parent) => parent !== name);
|
|
2988
|
+
relation.parents = namespaceOf(needed) === namespaceOf(name) ? [.../* @__PURE__ */ new Set([
|
|
2989
|
+
...outer,
|
|
2990
|
+
needed,
|
|
2991
|
+
...parents
|
|
2992
|
+
])] : [...parents, needed];
|
|
2993
|
+
relation.parentsFrom = "app";
|
|
2994
|
+
}
|
|
2995
|
+
completeChains(relations);
|
|
2996
|
+
for (const [name, relation] of relations) {
|
|
2997
|
+
if (relation.sample?.children || !relation.parents?.length || relation.visual === false) continue;
|
|
2998
|
+
for (const parent of [...relation.parents].reverse()) {
|
|
2999
|
+
const children = spelledOut(relations.get(parent)?.sample?.children ?? [], name);
|
|
3000
|
+
if (!children) continue;
|
|
3001
|
+
let parts = false;
|
|
3002
|
+
if (attempt(relation.parents, name, children)) parts = attempt(relation.parents, name, children, true) ? void 0 : true;
|
|
3003
|
+
if (parts !== void 0) {
|
|
3004
|
+
relation.sample = {
|
|
3005
|
+
...relation.sample,
|
|
3006
|
+
children
|
|
3007
|
+
};
|
|
3008
|
+
if (parts) relation.parentParts = true;
|
|
3009
|
+
delete relation.inside;
|
|
3010
|
+
}
|
|
3011
|
+
break;
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
const partsOf = /* @__PURE__ */ new Map();
|
|
3015
|
+
for (const [name, relation] of relations) {
|
|
3016
|
+
const parent = relation.parents?.at(-1);
|
|
3017
|
+
if (!parent || relation.inside || relation.visual === false) continue;
|
|
3018
|
+
partsOf.set(parent, [...partsOf.get(parent) ?? [], name]);
|
|
3019
|
+
}
|
|
3020
|
+
const depth = (name) => relations.get(name)?.parents?.length ?? 0;
|
|
3021
|
+
for (const [parent, parts] of [...partsOf].sort(([a], [b]) => depth(b) - depth(a))) {
|
|
3022
|
+
const relation = relations.get(parent);
|
|
3023
|
+
if (!relation || relation.inside || relation.sampleFrom === "example" && relation.sample?.children) continue;
|
|
3024
|
+
if (drawnParts.has(parent)) continue;
|
|
3025
|
+
const children = parts.slice(0, 3).map((part) => ({
|
|
3026
|
+
component: part,
|
|
3027
|
+
...relations.get(part)?.sample?.props ? { props: relations.get(part).sample.props } : {},
|
|
3028
|
+
...relations.get(part)?.sample?.children ? { children: relations.get(part).sample.children } : {}
|
|
3029
|
+
}));
|
|
3030
|
+
if (!attempt(relation.parents ?? [], parent, children)) relation.sample = {
|
|
3031
|
+
...relation.sample,
|
|
3032
|
+
children
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
for (const [name, relation] of relations) {
|
|
3036
|
+
if (relation.visual === false || relation.sample?.children) continue;
|
|
3037
|
+
const parents = relation.parents ?? [];
|
|
3038
|
+
const failed = attempt(parents, name, ["Text"]);
|
|
3039
|
+
if (failed && /void element|self-closing tag/.test(failed) && !attempt(parents, name, [])) relation.sample = {
|
|
3040
|
+
...relation.sample,
|
|
3041
|
+
children: []
|
|
3042
|
+
};
|
|
3043
|
+
const bare = failed && attempt(parents, name);
|
|
3044
|
+
if (bare && /children is not a function/.test(bare) && !attempt(parents, name, [{ $render: ["Text"] }])) {
|
|
3045
|
+
relation.sample = {
|
|
3046
|
+
...relation.sample,
|
|
3047
|
+
children: [{ $render: ["Text"] }]
|
|
3048
|
+
};
|
|
3049
|
+
delete relation.inside;
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
const textOnly = /* @__PURE__ */ new Set();
|
|
3053
|
+
for (const [name, relation] of relations) {
|
|
3054
|
+
const texts = relation.sample?.children;
|
|
3055
|
+
if (!texts?.length || !texts.every((child) => typeof child === "string")) continue;
|
|
3056
|
+
const parents = relation.parents ?? [];
|
|
3057
|
+
if (!attempt(parents, name, [{
|
|
3058
|
+
component: "p",
|
|
3059
|
+
children: texts
|
|
3060
|
+
}]) || attempt(parents, name, texts)) continue;
|
|
3061
|
+
const { children: _, ...sample } = relation.sample;
|
|
3062
|
+
relation.sample = {
|
|
3063
|
+
...sample,
|
|
3064
|
+
props: {
|
|
3065
|
+
...sample.props,
|
|
3066
|
+
children: texts.join("")
|
|
3067
|
+
}
|
|
3068
|
+
};
|
|
3069
|
+
textOnly.add(name);
|
|
3070
|
+
}
|
|
3071
|
+
const asText = (children) => children.map((child) => {
|
|
3072
|
+
if (typeof child !== "object" || !("component" in child) || !child.children) return child;
|
|
3073
|
+
const { children: inner, ...node } = child;
|
|
3074
|
+
if (textOnly.has(child.component) && inner.length > 0 && inner.every((item) => typeof item === "string")) return {
|
|
3075
|
+
...node,
|
|
3076
|
+
props: {
|
|
3077
|
+
...node.props,
|
|
3078
|
+
children: inner.join("")
|
|
3079
|
+
}
|
|
3080
|
+
};
|
|
3081
|
+
return {
|
|
3082
|
+
...node,
|
|
3083
|
+
children: asText(inner)
|
|
3084
|
+
};
|
|
3085
|
+
});
|
|
3086
|
+
if (textOnly.size > 0) {
|
|
3087
|
+
for (const relation of relations.values()) if (relation.sample?.children) relation.sample = {
|
|
3088
|
+
...relation.sample,
|
|
3089
|
+
children: asText(relation.sample.children)
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
for (const relation of relations.values()) {
|
|
3093
|
+
const nearest = relation.parents?.at(-1);
|
|
3094
|
+
if (nearest && relations.get(nearest)?.sample?.children?.length === 0 && relation.parentsFrom !== "edit") {
|
|
3095
|
+
delete relation.parents;
|
|
3096
|
+
delete relation.optionalParents;
|
|
3097
|
+
delete relation.parentsFrom;
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
const drawnTree = /* @__PURE__ */ new Set([...drawnParts, ...[...drawnInside.values()].flatMap((inside) => [...inside])]);
|
|
3101
|
+
return {
|
|
3102
|
+
relations,
|
|
3103
|
+
tried: true,
|
|
3104
|
+
...drawnTree.size > 0 ? { drawn: [...drawnTree] } : {},
|
|
3105
|
+
...missing.length > 0 ? { note: `Declared but not exported when it runs, so left out: ${missing.slice(0, 10).join(", ")}${missing.length > 10 ? ` and ${missing.length - 10} more` : ""}.` } : {}
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
/**
|
|
3109
|
+
* The parent a name suggests: its namespace's `Root` (`Accordion.Item`), the longest other component its name starts
|
|
3110
|
+
* with (`AccordionItem` under `Accordion`), that one's plural when there is one (`TabPanels` under `Tabs`), or its own
|
|
3111
|
+
* plural (`Tab` under `Tabs`).
|
|
3112
|
+
*/
|
|
3113
|
+
function guessParent(name, names) {
|
|
3114
|
+
const dot = name.lastIndexOf(".");
|
|
3115
|
+
if (dot !== -1) {
|
|
3116
|
+
const namespace = name.slice(0, dot);
|
|
3117
|
+
const member = name.slice(dot + 1);
|
|
3118
|
+
if (member === "Root" || member === "Provider") return [void 0, void 0];
|
|
3119
|
+
const root = [
|
|
3120
|
+
`${namespace}.Root`,
|
|
3121
|
+
`${namespace}.Provider`,
|
|
3122
|
+
namespace
|
|
3123
|
+
].find((candidate) => names.includes(candidate));
|
|
3124
|
+
return root ? [root, "namespace"] : [void 0, void 0];
|
|
3125
|
+
}
|
|
3126
|
+
const prefix = names.filter((other) => other !== name && !other.includes(".") && name.startsWith(other) && /^[A-Z]/.test(name.slice(other.length))).filter((other) => !notVisual.test(other)).sort((a, b) => b.length - a.length)[0];
|
|
3127
|
+
if (prefix) return [names.includes(`${prefix}s`) && `${prefix}s` !== name ? `${prefix}s` : prefix, "prefix"];
|
|
3128
|
+
if (names.includes(`${name}s`)) return [`${name}s`, "prefix"];
|
|
3129
|
+
return [void 0, void 0];
|
|
3130
|
+
}
|
|
3131
|
+
/**
|
|
3132
|
+
* A name an error mentions, as one of the library's components. A namespaced part only goes inside its own namespace,
|
|
3133
|
+
* where an internal name maps by its last words (`MenuContent` and `PopperContent` to `ContextMenu.Content`,
|
|
3134
|
+
* `MenuSub` to `ContextMenu.Sub`). Other parts match exactly, or with the dots left out (`Menu.SubmenuRoot`).
|
|
3135
|
+
*/
|
|
3136
|
+
function resolveName(mention, self, names) {
|
|
3137
|
+
const namespace = namespaceOf(self);
|
|
3138
|
+
if (namespace) {
|
|
3139
|
+
if (mention.startsWith(`${namespace}.`)) return names.includes(mention) ? mention : void 0;
|
|
3140
|
+
if (namespace.slice(namespace.lastIndexOf(".") + 1).endsWith(mention) && names.includes(`${namespace}.Root`)) return `${namespace}.Root`;
|
|
3141
|
+
const words = mention.replaceAll(".", "").split(/(?=[A-Z])/);
|
|
3142
|
+
for (let start = 0; start < words.length; start++) {
|
|
3143
|
+
const candidate = `${namespace}.${words.slice(start).join("")}`;
|
|
3144
|
+
if (names.includes(candidate)) return candidate;
|
|
3145
|
+
}
|
|
3146
|
+
return mention.includes(".") && names.includes(mention) ? mention : void 0;
|
|
3147
|
+
}
|
|
3148
|
+
if (names.includes(mention)) return mention;
|
|
3149
|
+
const flat = mention.replaceAll(".", "");
|
|
3150
|
+
const exact = names.find((name) => name.replaceAll(".", "") === flat);
|
|
3151
|
+
if (exact) return exact;
|
|
3152
|
+
const family = names.filter((name) => name !== self && self.startsWith(name) && /^[A-Z]/.test(self.slice(name.length))).sort((a, b) => b.length - a.length)[0];
|
|
3153
|
+
if (!family) return void 0;
|
|
3154
|
+
const words = mention.split(".").at(-1).split(/(?=[A-Z])/);
|
|
3155
|
+
if (words.join("") === "Root") return family;
|
|
3156
|
+
for (let start = 0; start < words.length; start++) {
|
|
3157
|
+
const candidate = `${family}${words.slice(start).join("")}`;
|
|
3158
|
+
if (candidate !== self && names.includes(candidate)) return candidate;
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
function namespaceOf(name) {
|
|
3162
|
+
const dot = name.lastIndexOf(".");
|
|
3163
|
+
return dot === -1 ? void 0 : name.slice(0, dot);
|
|
3164
|
+
}
|
|
3165
|
+
function message(error) {
|
|
3166
|
+
return (error instanceof Error ? error.message : String(error)).split("\n")[0].slice(0, 300);
|
|
3167
|
+
}
|
|
3168
|
+
/**
|
|
3169
|
+
* Starts each chain whose outermost parent needs parents of its own outside them (`SubmenuTrigger` in a `MenuSection`,
|
|
3170
|
+
* which goes in a `Menu`), as inserting the parent alone would, with the levels optional there optional here too.
|
|
3171
|
+
* Parents guessed round in a circle (a part among its parent's parents) are left as they are.
|
|
3172
|
+
*/
|
|
3173
|
+
function completeChains(relations) {
|
|
3174
|
+
for (const [name, relation] of relations) for (let round = 0; round < 4 && relation.parents?.length; round++) {
|
|
3175
|
+
const outer = relations.get(relation.parents[0]);
|
|
3176
|
+
if (outer?.parents?.includes(name)) break;
|
|
3177
|
+
const added = (outer?.parents ?? []).filter((parent) => !relation.parents.includes(parent));
|
|
3178
|
+
if (added.length === 0) break;
|
|
3179
|
+
relation.parents = [...added, ...relation.parents];
|
|
3180
|
+
const optional = (outer?.optionalParents ?? []).filter((parent) => added.includes(parent));
|
|
3181
|
+
if (optional.length > 0) relation.optionalParents = [.../* @__PURE__ */ new Set([...relation.optionalParents ?? [], ...optional])];
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
/** The children a sample spells out for the first `name` in it that has any, at any depth (a table's `Row` its `Cell`s). */
|
|
3185
|
+
function spelledOut(children, name) {
|
|
3186
|
+
for (const child of children) {
|
|
3187
|
+
if (typeof child === "string" || isSampleRender(child)) continue;
|
|
3188
|
+
if (child.component === name && child.children?.length) return child.children;
|
|
3189
|
+
const inner = spelledOut(child.children ?? [], name);
|
|
3190
|
+
if (inner) return inner;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
/**
|
|
3194
|
+
* A decorator's value outside a frame, for trying parts on the server: imports and calls are made, a mode's value is
|
|
3195
|
+
* its default, and the frame's own nodes are left undefined, as there's no frame. Direction and language take the
|
|
3196
|
+
* frame's defaults.
|
|
3197
|
+
*/
|
|
3198
|
+
async function trialValue(load, value) {
|
|
3199
|
+
if (Array.isArray(value)) return Promise.all(value.map((item) => trialValue(load, item)));
|
|
3200
|
+
if (typeof value !== "object" || value === null) return value;
|
|
3201
|
+
const record = value;
|
|
3202
|
+
if (typeof record.$import === "string") return exported(await load(record.$import), String(record.export ?? "default"));
|
|
3203
|
+
if (typeof record.$call === "object" && record.$call !== null) {
|
|
3204
|
+
const call = record.$call;
|
|
3205
|
+
const fn = exported(await load(call.import), call.export ?? "default");
|
|
3206
|
+
const args = await trialValue(load, record.args ?? []);
|
|
3207
|
+
if (typeof fn !== "function") return void 0;
|
|
3208
|
+
const made = fn(...args);
|
|
3209
|
+
for (const step of record.methods ?? []) {
|
|
3210
|
+
const method = made?.[step.method];
|
|
3211
|
+
if (typeof method !== "function") continue;
|
|
3212
|
+
const args = await trialValue(load, step.args ?? []);
|
|
3213
|
+
try {
|
|
3214
|
+
method.apply(made, args);
|
|
3215
|
+
} catch {}
|
|
3216
|
+
}
|
|
3217
|
+
return made;
|
|
3218
|
+
}
|
|
3219
|
+
if (typeof record.$global === "string") return trialValue(load, record.default);
|
|
3220
|
+
if (typeof record.$frame === "string") {
|
|
3221
|
+
if (record.$frame === "dir") return "ltr";
|
|
3222
|
+
return record.$frame === "lang" ? "en-US" : void 0;
|
|
3223
|
+
}
|
|
3224
|
+
if ("$function" in record) {
|
|
3225
|
+
const result = await trialValue(load, record.$function);
|
|
3226
|
+
return () => result;
|
|
3227
|
+
}
|
|
3228
|
+
const resolved = {};
|
|
3229
|
+
for (const [key, item] of Object.entries(record)) resolved[key] = await trialValue(load, item);
|
|
3230
|
+
return resolved;
|
|
3231
|
+
}
|
|
3232
|
+
function exported(module, name) {
|
|
3233
|
+
let found = module;
|
|
3234
|
+
for (const part of name.split(".")) found = found?.[part];
|
|
3235
|
+
return found ?? (name === "default" ? module : void 0);
|
|
3236
|
+
}
|
|
3237
|
+
/** The parts the package puts inside another, as sample children, keeping only those it exports. */
|
|
3238
|
+
function exportedChildren(children, names) {
|
|
3239
|
+
return children.flatMap((child) => {
|
|
3240
|
+
const inner = exportedChildren(child.children ?? [], names);
|
|
3241
|
+
if (!names.includes(child.component)) return inner;
|
|
3242
|
+
return [{
|
|
3243
|
+
component: child.component,
|
|
3244
|
+
...inner.length > 0 ? { children: inner } : {}
|
|
3245
|
+
}];
|
|
3246
|
+
});
|
|
3247
|
+
}
|
|
3248
|
+
/** Every component a sample's children name, at any depth. */
|
|
3249
|
+
function componentsIn(children) {
|
|
3250
|
+
return children.flatMap((child) => typeof child === "object" && "component" in child ? [child.component, ...componentsIn(child.children ?? [])] : []);
|
|
3251
|
+
}
|
|
3252
|
+
// ../libgen/src/presets.ts
|
|
3253
|
+
const createTheme = {
|
|
3254
|
+
import: "@mui/material/styles",
|
|
3255
|
+
export: "createTheme"
|
|
3256
|
+
};
|
|
3257
|
+
/** MUI's theme in CSS-variables mode, so its components read the custom properties the Tokens panel edits. */
|
|
3258
|
+
function muiTheme(mode) {
|
|
3259
|
+
return {
|
|
3260
|
+
$call: createTheme,
|
|
3261
|
+
args: [{
|
|
3262
|
+
cssVariables: {
|
|
3263
|
+
rootSelector: ":host",
|
|
3264
|
+
colorSchemeSelector: "class"
|
|
3265
|
+
},
|
|
3266
|
+
colorSchemes: {
|
|
3267
|
+
light: true,
|
|
3268
|
+
dark: true
|
|
3269
|
+
},
|
|
3270
|
+
defaultColorScheme: mode,
|
|
3271
|
+
components: Object.fromEntries([
|
|
3272
|
+
"MuiPopover",
|
|
3273
|
+
"MuiPopper",
|
|
3274
|
+
"MuiModal"
|
|
3275
|
+
].map((name) => [name, { defaultProps: { container: { $frame: "body" } } }]))
|
|
3276
|
+
}]
|
|
3277
|
+
};
|
|
3278
|
+
}
|
|
3279
|
+
const fluentThemes = {
|
|
3280
|
+
light: "webLightTheme",
|
|
3281
|
+
dark: "webDarkTheme",
|
|
3282
|
+
"teams-light": "teamsLightTheme",
|
|
3283
|
+
"teams-dark": "teamsDarkTheme",
|
|
3284
|
+
"teams-high-contrast": "teamsHighContrastTheme"
|
|
3285
|
+
};
|
|
3286
|
+
const presets = {
|
|
3287
|
+
...Object.fromEntries([
|
|
3288
|
+
"luma",
|
|
3289
|
+
"lyra",
|
|
3290
|
+
"maia",
|
|
3291
|
+
"mira",
|
|
3292
|
+
"nova",
|
|
3293
|
+
"rhea",
|
|
3294
|
+
"sera",
|
|
3295
|
+
"vega"
|
|
3296
|
+
].map((style) => {
|
|
3297
|
+
const name = `@buildkitgg/shadcn-baseui-${style}`;
|
|
3298
|
+
return [name, {
|
|
3299
|
+
id: style,
|
|
3300
|
+
name: `Shadcn ${style.charAt(0).toUpperCase()}${style.slice(1)}`,
|
|
3301
|
+
description: `Every shadcn component on Base UI, in the ${style} style, kept in step with upstream.`,
|
|
3302
|
+
styles: "tailwind.css",
|
|
3303
|
+
examples: {
|
|
3304
|
+
paths: ["apps/v4/examples/base/"],
|
|
3305
|
+
anatomy: ["apps/v4/content/docs/components/base/"]
|
|
3306
|
+
},
|
|
3307
|
+
decorators: [{ wrap: {
|
|
3308
|
+
import: name,
|
|
3309
|
+
component: "TooltipProvider"
|
|
3310
|
+
} }, { add: {
|
|
3311
|
+
import: name,
|
|
3312
|
+
component: "Toaster"
|
|
3313
|
+
} }]
|
|
3314
|
+
}];
|
|
3315
|
+
})),
|
|
3316
|
+
"radix-ui": {
|
|
3317
|
+
examples: { paths: ["apps/storybook/stories/"] },
|
|
3318
|
+
id: "radix",
|
|
3319
|
+
name: "Radix Primitives",
|
|
3320
|
+
description: "Unstyled, accessible primitives; styles come from the project's CSS.",
|
|
3321
|
+
decorators: [
|
|
3322
|
+
{ wrap: {
|
|
3323
|
+
import: "radix-ui",
|
|
3324
|
+
component: "Direction.Provider",
|
|
3325
|
+
props: { dir: { $frame: "dir" } }
|
|
3326
|
+
} },
|
|
3327
|
+
{ wrap: {
|
|
3328
|
+
import: "radix-ui",
|
|
3329
|
+
component: "Tooltip.Provider"
|
|
3330
|
+
} },
|
|
3331
|
+
{ wrap: {
|
|
3332
|
+
import: "radix-ui",
|
|
3333
|
+
component: "Toast.Provider"
|
|
3334
|
+
} },
|
|
3335
|
+
{ add: {
|
|
3336
|
+
import: "radix-ui",
|
|
3337
|
+
component: "Toast.Viewport"
|
|
3338
|
+
} }
|
|
3339
|
+
],
|
|
3340
|
+
notes: ["Portals take a container per component (`Dialog.Portal container`); Radix has no provider for all of them."]
|
|
3341
|
+
},
|
|
3342
|
+
"@base-ui/react": {
|
|
3343
|
+
examples: {
|
|
3344
|
+
paths: ["docs/"],
|
|
3345
|
+
anatomy: ["docs/src/app/(docs)/react/components/"]
|
|
3346
|
+
},
|
|
3347
|
+
id: "baseui",
|
|
3348
|
+
name: "Base UI",
|
|
3349
|
+
description: "Unstyled components from the makers of Radix, Floating UI and Material UI.",
|
|
3350
|
+
decorators: [{ wrap: {
|
|
3351
|
+
import: "@base-ui/react",
|
|
3352
|
+
component: "DirectionProvider",
|
|
3353
|
+
props: { direction: { $frame: "dir" } }
|
|
3354
|
+
} }, { wrap: {
|
|
3355
|
+
import: "@base-ui/react",
|
|
3356
|
+
component: "Toast.Provider"
|
|
3357
|
+
} }],
|
|
3358
|
+
notes: ["Portals take a container per component (`Dialog.Portal container`); Base UI has no provider for all of them."]
|
|
3359
|
+
},
|
|
3360
|
+
"react-aria-components": {
|
|
3361
|
+
examples: { paths: ["packages/react-aria-components/", "packages/dev/s2-docs/pages/react-aria/"] },
|
|
3362
|
+
id: "aria",
|
|
3363
|
+
name: "React Aria Components",
|
|
3364
|
+
description: "Adobe's unstyled, accessible components; styles come from the project's CSS.",
|
|
3365
|
+
decorators: [{ wrap: {
|
|
3366
|
+
import: "react-aria-components",
|
|
3367
|
+
component: "I18nProvider",
|
|
3368
|
+
props: { locale: { $frame: "lang" } }
|
|
3369
|
+
} }],
|
|
3370
|
+
notes: ["Toasts need the app's own `UNSTABLE_ToastQueue`, so there's no toast host to add."]
|
|
3371
|
+
},
|
|
3372
|
+
"@mui/material": {
|
|
3373
|
+
examples: {
|
|
3374
|
+
paths: [],
|
|
3375
|
+
demos: ["docs/data/material/components/"]
|
|
3376
|
+
},
|
|
3377
|
+
id: "mui",
|
|
3378
|
+
name: "Material UI",
|
|
3379
|
+
description: "Google's Material Design in React, styled with Emotion.",
|
|
3380
|
+
decorators: [
|
|
3381
|
+
{ wrap: {
|
|
3382
|
+
import: "@emotion/react",
|
|
3383
|
+
component: "CacheProvider",
|
|
3384
|
+
props: { value: {
|
|
3385
|
+
$call: {
|
|
3386
|
+
import: "@emotion/cache",
|
|
3387
|
+
export: "default"
|
|
3388
|
+
},
|
|
3389
|
+
args: [{
|
|
3390
|
+
key: "mui",
|
|
3391
|
+
container: { $frame: "root" },
|
|
3392
|
+
prepend: true
|
|
3393
|
+
}]
|
|
3394
|
+
} }
|
|
3395
|
+
} },
|
|
3396
|
+
{ wrap: {
|
|
3397
|
+
import: "@mui/material/styles",
|
|
3398
|
+
component: "ThemeProvider",
|
|
3399
|
+
props: {
|
|
3400
|
+
theme: {
|
|
3401
|
+
$global: "theme",
|
|
3402
|
+
values: { dark: muiTheme("dark") },
|
|
3403
|
+
default: muiTheme("light")
|
|
3404
|
+
},
|
|
3405
|
+
defaultMode: {
|
|
3406
|
+
$global: "theme",
|
|
3407
|
+
values: { dark: "dark" },
|
|
3408
|
+
default: "light"
|
|
3409
|
+
},
|
|
3410
|
+
colorSchemeNode: { $frame: "body" },
|
|
3411
|
+
storageManager: null,
|
|
3412
|
+
disableTransitionOnChange: true
|
|
3413
|
+
}
|
|
3414
|
+
} },
|
|
3415
|
+
{ wrap: {
|
|
3416
|
+
import: "@mui/material",
|
|
3417
|
+
component: "ScopedCssBaseline"
|
|
3418
|
+
} }
|
|
3419
|
+
],
|
|
3420
|
+
async theme(load) {
|
|
3421
|
+
const make = (await load("@mui/material/styles")).createTheme;
|
|
3422
|
+
const theme = make({
|
|
3423
|
+
cssVariables: { colorSchemeSelector: "class" },
|
|
3424
|
+
colorSchemes: {
|
|
3425
|
+
light: true,
|
|
3426
|
+
dark: true
|
|
3427
|
+
}
|
|
3428
|
+
});
|
|
3429
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
3430
|
+
for (const sheet of theme.generateStyleSheets()) for (const [selector, declarations] of Object.entries(sheet)) {
|
|
3431
|
+
const context = /\bdark\b/.test(selector) ? "dark" : "default";
|
|
3432
|
+
for (const [name, value] of Object.entries(declarations)) {
|
|
3433
|
+
if (!name.startsWith("--") || typeof value !== "string" && typeof value !== "number") continue;
|
|
3434
|
+
const property = name.slice(2);
|
|
3435
|
+
if (/Channel$|Opacity$/.test(property)) continue;
|
|
3436
|
+
const token = tokens.get(property) ?? {
|
|
3437
|
+
var: property,
|
|
3438
|
+
category: muiCategory(property),
|
|
3439
|
+
values: {}
|
|
3440
|
+
};
|
|
3441
|
+
token.values[context] = String(value);
|
|
3442
|
+
tokens.set(property, token);
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
const unit = tokens.get("mui-spacing")?.values.default;
|
|
3446
|
+
if (unit) tokens.set("spacing", {
|
|
3447
|
+
var: "spacing",
|
|
3448
|
+
category: "spacing",
|
|
3449
|
+
values: { default: unit }
|
|
3450
|
+
});
|
|
3451
|
+
const plain = make({});
|
|
3452
|
+
const breakpoints = plain.breakpoints.values;
|
|
3453
|
+
for (const [id, width] of Object.entries(breakpoints)) if (width > 0) tokens.set(`mui-breakpoint-${id}`, {
|
|
3454
|
+
var: `mui-breakpoint-${id}`,
|
|
3455
|
+
category: "breakpoint",
|
|
3456
|
+
values: { default: `${width}px` }
|
|
3457
|
+
});
|
|
3458
|
+
for (const [variant, style] of Object.entries(plain.typography)) {
|
|
3459
|
+
const { fontSize, lineHeight } = style ?? {};
|
|
3460
|
+
if (typeof fontSize !== "string") continue;
|
|
3461
|
+
const size = `mui-font-size-${variant}`;
|
|
3462
|
+
const leading = `mui-line-height-${variant}`;
|
|
3463
|
+
if (typeof lineHeight === "number" || typeof lineHeight === "string") tokens.set(leading, {
|
|
3464
|
+
var: leading,
|
|
3465
|
+
category: "leading",
|
|
3466
|
+
values: { default: String(lineHeight) }
|
|
3467
|
+
});
|
|
3468
|
+
tokens.set(size, {
|
|
3469
|
+
var: size,
|
|
3470
|
+
category: "font-size",
|
|
3471
|
+
values: { default: fontSize },
|
|
3472
|
+
...tokens.has(leading) ? { lineHeight: leading } : {}
|
|
3473
|
+
});
|
|
3474
|
+
}
|
|
3475
|
+
return {
|
|
3476
|
+
tokens: [...tokens.values()],
|
|
3477
|
+
breakpoints: Object.entries(breakpoints).filter(([, width]) => width > 0).map(([id, width]) => ({
|
|
3478
|
+
id,
|
|
3479
|
+
name: breakpointName(id),
|
|
3480
|
+
width
|
|
3481
|
+
}))
|
|
3482
|
+
};
|
|
3483
|
+
}
|
|
3484
|
+
},
|
|
3485
|
+
"@chakra-ui/react": {
|
|
3486
|
+
examples: {
|
|
3487
|
+
paths: ["apps/compositions/"],
|
|
3488
|
+
anatomy: ["apps/www/content/docs/components/"]
|
|
3489
|
+
},
|
|
3490
|
+
id: "chakra",
|
|
3491
|
+
name: "Chakra UI",
|
|
3492
|
+
description: "Accessible components with style props, on Emotion.",
|
|
3493
|
+
decorators: [
|
|
3494
|
+
{ wrap: {
|
|
3495
|
+
import: "@emotion/react",
|
|
3496
|
+
component: "CacheProvider",
|
|
3497
|
+
props: { value: {
|
|
3498
|
+
$call: {
|
|
3499
|
+
import: "@emotion/cache",
|
|
3500
|
+
export: "default"
|
|
3501
|
+
},
|
|
3502
|
+
args: [{
|
|
3503
|
+
key: "chakra",
|
|
3504
|
+
container: { $frame: "root" }
|
|
3505
|
+
}]
|
|
3506
|
+
} }
|
|
3507
|
+
} },
|
|
3508
|
+
{ wrap: {
|
|
3509
|
+
import: "@chakra-ui/react",
|
|
3510
|
+
component: "ChakraProvider",
|
|
3511
|
+
props: { value: {
|
|
3512
|
+
$import: "@chakra-ui/react",
|
|
3513
|
+
export: "defaultSystem"
|
|
3514
|
+
} }
|
|
3515
|
+
} },
|
|
3516
|
+
{ wrap: {
|
|
3517
|
+
import: "@chakra-ui/react",
|
|
3518
|
+
component: "EnvironmentProvider",
|
|
3519
|
+
props: { value: { $function: { $frame: "root" } } }
|
|
3520
|
+
} },
|
|
3521
|
+
{ style: {
|
|
3522
|
+
"font-family": "var(--chakra-fonts-body)",
|
|
3523
|
+
color: "var(--chakra-colors-fg)",
|
|
3524
|
+
"background-color": "var(--chakra-colors-bg)",
|
|
3525
|
+
"line-height": "1.5"
|
|
3526
|
+
} }
|
|
3527
|
+
],
|
|
3528
|
+
notes: ["Chakra's Toaster is a snippet the app generates (`npx @chakra-ui/cli snippet add toaster`), not an export."],
|
|
3529
|
+
async theme(load) {
|
|
3530
|
+
const system = (await load("@chakra-ui/react")).defaultSystem;
|
|
3531
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
3532
|
+
const visit = (styles, context) => {
|
|
3533
|
+
if (typeof styles !== "object" || styles === null) return;
|
|
3534
|
+
for (const [key, value] of Object.entries(styles)) if (key.startsWith("--") && (typeof value === "string" || typeof value === "number")) {
|
|
3535
|
+
const property = key.slice(2);
|
|
3536
|
+
const token = tokens.get(property) ?? {
|
|
3537
|
+
var: property,
|
|
3538
|
+
category: chakraCategory(property, String(value)),
|
|
3539
|
+
values: {}
|
|
3540
|
+
};
|
|
3541
|
+
token.values[context] = String(value);
|
|
3542
|
+
tokens.set(property, token);
|
|
3543
|
+
} else visit(value, /\bdark\b/.test(key) ? "dark" : context);
|
|
3544
|
+
};
|
|
3545
|
+
visit(system.getTokenCss(), "default");
|
|
3546
|
+
const unit = tokens.get("chakra-spacing-1")?.values.default;
|
|
3547
|
+
if (unit) tokens.set("spacing", {
|
|
3548
|
+
var: "spacing",
|
|
3549
|
+
category: "spacing",
|
|
3550
|
+
values: { default: unit }
|
|
3551
|
+
});
|
|
3552
|
+
return { tokens: [...tokens.values()] };
|
|
3553
|
+
}
|
|
3554
|
+
},
|
|
3555
|
+
"@mantine/core": {
|
|
3556
|
+
examples: { paths: ["packages/@docs/demos/"] },
|
|
3557
|
+
id: "mantine",
|
|
3558
|
+
name: "Mantine",
|
|
3559
|
+
description: "A full-featured component library with CSS modules and its own hooks.",
|
|
3560
|
+
styles: "styles.css",
|
|
3561
|
+
decorators: [{ wrap: {
|
|
3562
|
+
import: "@mantine/core",
|
|
3563
|
+
component: "MantineProvider",
|
|
3564
|
+
props: {
|
|
3565
|
+
forceColorScheme: {
|
|
3566
|
+
$global: "theme",
|
|
3567
|
+
values: { dark: "dark" },
|
|
3568
|
+
default: "light"
|
|
3569
|
+
},
|
|
3570
|
+
cssVariablesSelector: ":host",
|
|
3571
|
+
getRootElement: { $function: { $frame: "host" } },
|
|
3572
|
+
theme: { components: { Portal: { defaultProps: { target: { $frame: "body" } } } } }
|
|
3573
|
+
}
|
|
3574
|
+
} }],
|
|
3575
|
+
notes: ["`@mantine/notifications` has its own `Notifications` host; it's added once that package has a registry file."]
|
|
3576
|
+
},
|
|
3577
|
+
"@fluentui/react-components": {
|
|
3578
|
+
examples: { paths: ["packages/react-components/"] },
|
|
3579
|
+
id: "fluent",
|
|
3580
|
+
name: "Fluent UI",
|
|
3581
|
+
description: "Microsoft's Fluent 2 design system for React, styled with Griffel.",
|
|
3582
|
+
decorators: [
|
|
3583
|
+
{ wrap: {
|
|
3584
|
+
import: "@fluentui/react-components",
|
|
3585
|
+
component: "RendererProvider",
|
|
3586
|
+
props: {
|
|
3587
|
+
renderer: {
|
|
3588
|
+
$call: {
|
|
3589
|
+
import: "@griffel/shadow-dom",
|
|
3590
|
+
export: "createShadowDOMRenderer"
|
|
3591
|
+
},
|
|
3592
|
+
args: [{ $frame: "root" }]
|
|
3593
|
+
},
|
|
3594
|
+
targetDocument: { $frame: "document" }
|
|
3595
|
+
}
|
|
3596
|
+
} },
|
|
3597
|
+
{ wrap: {
|
|
3598
|
+
import: "@fluentui/react-components",
|
|
3599
|
+
component: "FluentProvider",
|
|
3600
|
+
props: {
|
|
3601
|
+
theme: {
|
|
3602
|
+
$global: "theme",
|
|
3603
|
+
values: Object.fromEntries(Object.entries(fluentThemes).map(([id, name]) => [id, {
|
|
3604
|
+
$import: "@fluentui/react-components",
|
|
3605
|
+
export: name
|
|
3606
|
+
}])),
|
|
3607
|
+
default: {
|
|
3608
|
+
$import: "@fluentui/react-components",
|
|
3609
|
+
export: "webLightTheme"
|
|
3610
|
+
}
|
|
3611
|
+
},
|
|
3612
|
+
dir: { $frame: "dir" },
|
|
3613
|
+
targetDocument: { $frame: "document" }
|
|
3614
|
+
}
|
|
3615
|
+
} },
|
|
3616
|
+
{ wrap: {
|
|
3617
|
+
import: "@fluentui/react-components",
|
|
3618
|
+
component: "PortalMountNodeProvider",
|
|
3619
|
+
props: { value: { $frame: "body" } }
|
|
3620
|
+
} }
|
|
3621
|
+
],
|
|
3622
|
+
notes: ["FluentProvider writes its theme into the page's head, which frames can't see, so each theme's variables come from this file's tokens."],
|
|
3623
|
+
async theme(load) {
|
|
3624
|
+
const fluent = await load("@fluentui/tokens");
|
|
3625
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
3626
|
+
for (const [context, name] of Object.entries(fluentThemes)) {
|
|
3627
|
+
const theme = fluent[name];
|
|
3628
|
+
if (!theme) continue;
|
|
3629
|
+
for (const [property, value] of Object.entries(theme)) {
|
|
3630
|
+
const token = tokens.get(property) ?? {
|
|
3631
|
+
var: property,
|
|
3632
|
+
category: fluentCategory(property),
|
|
3633
|
+
values: {}
|
|
3634
|
+
};
|
|
3635
|
+
if (context === "light") token.values.default = String(value);
|
|
3636
|
+
else token.values[context] = String(value);
|
|
3637
|
+
tokens.set(property, token);
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
for (const token of tokens.values()) {
|
|
3641
|
+
const step = /^fontSize(.+)$/.exec(token.var)?.[1];
|
|
3642
|
+
if (step && tokens.has(`lineHeight${step}`)) token.lineHeight = `lineHeight${step}`;
|
|
3643
|
+
}
|
|
3644
|
+
return { tokens: [...tokens.values()] };
|
|
3645
|
+
}
|
|
3646
|
+
},
|
|
3647
|
+
recharts: {
|
|
3648
|
+
id: "recharts",
|
|
3649
|
+
name: "Recharts",
|
|
3650
|
+
description: "Charts, axes, series and grids to compose, as shadcn's charts are."
|
|
3651
|
+
},
|
|
3652
|
+
"lucide-react": {
|
|
3653
|
+
id: "lucide",
|
|
3654
|
+
name: "Lucide",
|
|
3655
|
+
description: "Over 1,600 consistent, open source icons; shadcn's default icon set.",
|
|
3656
|
+
tags: {
|
|
3657
|
+
package: "lucide-static",
|
|
3658
|
+
file: "tags.json"
|
|
3659
|
+
}
|
|
3660
|
+
},
|
|
3661
|
+
"@hugeicons/core-free-icons": {
|
|
3662
|
+
id: "hugeicons",
|
|
3663
|
+
name: "Hugeicons",
|
|
3664
|
+
description: "Thousands of free icons as data, drawn by Hugeicons' React component.",
|
|
3665
|
+
icons: {
|
|
3666
|
+
render: {
|
|
3667
|
+
import: "@hugeicons/react",
|
|
3668
|
+
component: "HugeiconsIcon",
|
|
3669
|
+
prop: "icon"
|
|
3670
|
+
},
|
|
3671
|
+
match: "Icon$"
|
|
3672
|
+
}
|
|
3673
|
+
},
|
|
3674
|
+
"@heroicons/react": {
|
|
3675
|
+
id: "heroicons",
|
|
3676
|
+
name: "Heroicons",
|
|
3677
|
+
description: "Hand-crafted SVG icons from the makers of Tailwind CSS, outline and solid, at 24, 20 and 16px.",
|
|
3678
|
+
icons: {
|
|
3679
|
+
import: "@heroicons/react/24/outline",
|
|
3680
|
+
render: "export"
|
|
3681
|
+
}
|
|
3682
|
+
},
|
|
3683
|
+
"@phosphor-icons/react": {
|
|
3684
|
+
id: "phosphor",
|
|
3685
|
+
name: "Phosphor",
|
|
3686
|
+
description: "A flexible icon family with six weights, picked with the weight prop.",
|
|
3687
|
+
icons: {
|
|
3688
|
+
render: "export",
|
|
3689
|
+
variants: {
|
|
3690
|
+
prop: "weight",
|
|
3691
|
+
styles: [
|
|
3692
|
+
"thin",
|
|
3693
|
+
"light",
|
|
3694
|
+
"regular",
|
|
3695
|
+
"bold",
|
|
3696
|
+
"fill",
|
|
3697
|
+
"duotone"
|
|
3698
|
+
],
|
|
3699
|
+
default: { style: "regular" }
|
|
3700
|
+
}
|
|
3701
|
+
},
|
|
3702
|
+
tags: {
|
|
3703
|
+
package: "@phosphor-icons/core",
|
|
3704
|
+
file: "dist/index.mjs"
|
|
3705
|
+
}
|
|
3706
|
+
},
|
|
3707
|
+
"@tabler/icons-react": {
|
|
3708
|
+
id: "tabler",
|
|
3709
|
+
name: "Tabler Icons",
|
|
3710
|
+
description: "Over 5,000 outline and filled icons on a 24px grid.",
|
|
3711
|
+
icons: {
|
|
3712
|
+
render: "export",
|
|
3713
|
+
variants: {
|
|
3714
|
+
pattern: "^(?<name>Icon.+?)(?<style>Filled)?$",
|
|
3715
|
+
styles: ["Outline", "Filled"],
|
|
3716
|
+
default: { style: "Outline" }
|
|
3717
|
+
}
|
|
3718
|
+
},
|
|
3719
|
+
tags: {
|
|
3720
|
+
package: "@tabler/icons",
|
|
3721
|
+
file: "icons.json"
|
|
3722
|
+
}
|
|
3723
|
+
},
|
|
3724
|
+
"@fluentui/react-icons": {
|
|
3725
|
+
id: "fluent-icons",
|
|
3726
|
+
name: "Fluent icons",
|
|
3727
|
+
description: "Microsoft's Fluent System Icons, in Regular and Filled styles, some with a size in the name.",
|
|
3728
|
+
icons: {
|
|
3729
|
+
render: "export",
|
|
3730
|
+
variants: {
|
|
3731
|
+
pattern: "^(?<name>.+?)(?<size>\\d+)?(?<style>Regular|Filled|Color|Light)$",
|
|
3732
|
+
styles: [
|
|
3733
|
+
"Regular",
|
|
3734
|
+
"Filled",
|
|
3735
|
+
"Color",
|
|
3736
|
+
"Light"
|
|
3737
|
+
],
|
|
3738
|
+
sizes: [
|
|
3739
|
+
10,
|
|
3740
|
+
12,
|
|
3741
|
+
16,
|
|
3742
|
+
20,
|
|
3743
|
+
24,
|
|
3744
|
+
28,
|
|
3745
|
+
32,
|
|
3746
|
+
48
|
|
3747
|
+
],
|
|
3748
|
+
default: { style: "Regular" }
|
|
3749
|
+
}
|
|
3750
|
+
},
|
|
3751
|
+
tags: { files: "assets/*/metadata.json" }
|
|
3752
|
+
}
|
|
3753
|
+
};
|
|
3754
|
+
function breakpointName(id) {
|
|
3755
|
+
return {
|
|
3756
|
+
xs: "Extra small",
|
|
3757
|
+
sm: "Small",
|
|
3758
|
+
md: "Medium",
|
|
3759
|
+
lg: "Large",
|
|
3760
|
+
xl: "XL"
|
|
3761
|
+
}[id] ?? id.toUpperCase();
|
|
3762
|
+
}
|
|
3763
|
+
function muiCategory(name) {
|
|
3764
|
+
if (name.startsWith("mui-palette-")) return "color";
|
|
3765
|
+
if (name.startsWith("mui-shadows-")) return "shadow";
|
|
3766
|
+
if (name === "mui-shape-borderRadius") return "radius";
|
|
3767
|
+
if (name === "mui-spacing") return "spacing";
|
|
3768
|
+
if (name.startsWith("mui-font-")) return "font";
|
|
3769
|
+
if (name.startsWith("mui-zIndex-")) return "other";
|
|
3770
|
+
return "other";
|
|
3771
|
+
}
|
|
3772
|
+
function chakraCategory(name, value) {
|
|
3773
|
+
return {
|
|
3774
|
+
colors: "color",
|
|
3775
|
+
spacing: "spacing",
|
|
3776
|
+
radii: "radius",
|
|
3777
|
+
"font-sizes": "font-size",
|
|
3778
|
+
fonts: "font",
|
|
3779
|
+
"font-weights": "font-weight",
|
|
3780
|
+
"letter-spacings": "tracking",
|
|
3781
|
+
"line-heights": "leading",
|
|
3782
|
+
shadows: "shadow",
|
|
3783
|
+
breakpoints: "breakpoint",
|
|
3784
|
+
sizes: "container",
|
|
3785
|
+
durations: "motion",
|
|
3786
|
+
easings: "motion"
|
|
3787
|
+
}[/^chakra-([a-z]+(?:-[a-z]+)?)-/.exec(name)?.[1] ?? ""] ?? (/^(?:#|rgb|hsl|oklch|color-mix)/.test(value) ? "color" : "other");
|
|
3788
|
+
}
|
|
3789
|
+
function fluentCategory(name) {
|
|
3790
|
+
if (name.startsWith("color")) return "color";
|
|
3791
|
+
if (name.startsWith("spacing")) return "spacing";
|
|
3792
|
+
if (name.startsWith("borderRadius")) return "radius";
|
|
3793
|
+
if (name.startsWith("fontSize")) return "font-size";
|
|
3794
|
+
if (name.startsWith("lineHeight")) return "leading";
|
|
3795
|
+
if (name.startsWith("fontWeight")) return "font-weight";
|
|
3796
|
+
if (name.startsWith("fontFamily")) return "font";
|
|
3797
|
+
if (name.startsWith("shadow")) return "shadow";
|
|
3798
|
+
if (name.startsWith("duration") || name.startsWith("curve")) return "motion";
|
|
3799
|
+
return "other";
|
|
3800
|
+
}
|
|
3801
|
+
// ../libgen/src/tokens.ts
|
|
3802
|
+
/**
|
|
3803
|
+
* The category a custom property belongs in, from the words in its name, then its value: `--mantine-spacing-md` is
|
|
3804
|
+
* spacing, `--fontSizeBase300` a font size, and anything whose value is a colour a colour.
|
|
3805
|
+
*/
|
|
3806
|
+
function categorizeVar(name, value) {
|
|
3807
|
+
const words = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().split(/[-_]/);
|
|
3808
|
+
const has = (...sequence) => words.some((_, index) => sequence.every((word, offset) => words[index + offset] === word));
|
|
3809
|
+
if (has("breakpoint") || has("breakpoints") || has("screen")) return "breakpoint";
|
|
3810
|
+
const textSize = words[0] === "text" && ![
|
|
3811
|
+
"wrap",
|
|
3812
|
+
"color",
|
|
3813
|
+
"decoration",
|
|
3814
|
+
"shadow"
|
|
3815
|
+
].includes(words[1] ?? "");
|
|
3816
|
+
if (has("font", "size") || has("font", "sizes") || textSize) return "font-size";
|
|
3817
|
+
if (has("line", "height") || has("leading") || has("line", "heights")) return "leading";
|
|
3818
|
+
if (has("font", "weight") || has("font", "weights")) return "font-weight";
|
|
3819
|
+
if (has("letter", "spacing") || has("tracking")) return "tracking";
|
|
3820
|
+
if (has("font", "family") || has("font") || has("fonts")) return "font";
|
|
3821
|
+
if (has("radius") || has("radii") || has("rounded")) return "radius";
|
|
3822
|
+
if (has("shadow") || has("shadows") || has("elevation")) return "shadow";
|
|
3823
|
+
if (has("spacing") || has("space") || has("gap")) return "spacing";
|
|
3824
|
+
if ([
|
|
3825
|
+
"duration",
|
|
3826
|
+
"easing",
|
|
3827
|
+
"curve",
|
|
3828
|
+
"ease",
|
|
3829
|
+
"transition",
|
|
3830
|
+
"animation",
|
|
3831
|
+
"animate",
|
|
3832
|
+
"delay"
|
|
3833
|
+
].some((word) => has(word))) return "motion";
|
|
3834
|
+
if (has("container") && !has("color")) return "container";
|
|
3835
|
+
if (has("color") || has("colors") || has("palette") || isColor(value)) return "color";
|
|
3836
|
+
return "other";
|
|
3837
|
+
}
|
|
3838
|
+
const colorValue = /^(?:#[\da-f]{3,8}|(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color|color-mix|light-dark)\(|transparent$)/i;
|
|
3839
|
+
function isColor(value) {
|
|
3840
|
+
return colorValue.test(value.trim());
|
|
3841
|
+
}
|
|
3842
|
+
/** A theme a selector belongs to: the root's defaults, or a dark theme however it's written. Anything else, none. */
|
|
3843
|
+
function themeOfSelector(selector, media = "") {
|
|
3844
|
+
const parts = selector.split(",").map((part) => part.trim().replaceAll(/\s+/g, " "));
|
|
3845
|
+
const dark = /prefers-color-scheme:\s*dark/.test(media);
|
|
3846
|
+
const isRoot = (part) => /^(?::where\()?(?::root|:host|html)(?:\)|\s|$|\[|\.|,)/.test(part) || part === ":host";
|
|
3847
|
+
const isDark = (part) => /(?:^|[.\s])dark(?:[\s.:[]|$)|\[[\w-]+=["']?dark["']?\]/.test(part);
|
|
3848
|
+
const isLight = (part) => /(?:^|[.\s])light(?:[\s.:[]|$)|\[[\w-]+=["']?light["']?\]/.test(part);
|
|
3849
|
+
if (parts.some(isDark)) return "dark";
|
|
3850
|
+
if (parts.some((part) => isRoot(part) || isLight(part))) return dark ? "dark" : "default";
|
|
3851
|
+
}
|
|
3852
|
+
/** Splits CSS into nested blocks, each with its prelude and its own declarations. */
|
|
3853
|
+
function blocks(css) {
|
|
3854
|
+
let index = 0;
|
|
3855
|
+
const level = () => {
|
|
3856
|
+
let text = "";
|
|
3857
|
+
let declarations = "";
|
|
3858
|
+
const children = [];
|
|
3859
|
+
while (index < css.length) {
|
|
3860
|
+
const char = css[index];
|
|
3861
|
+
if (char === "/" && css[index + 1] === "*") {
|
|
3862
|
+
const end = css.indexOf("*/", index + 2);
|
|
3863
|
+
index = end === -1 ? css.length : end + 2;
|
|
3864
|
+
} else if (char === "\"" || char === "'") {
|
|
3865
|
+
const end = css.indexOf(char, index + 1);
|
|
3866
|
+
text += css.slice(index, end === -1 ? css.length : end + 1);
|
|
3867
|
+
index = end === -1 ? css.length : end + 1;
|
|
3868
|
+
} else if (char === "{") {
|
|
3869
|
+
index++;
|
|
3870
|
+
const split = text.lastIndexOf(";") + 1;
|
|
3871
|
+
declarations += text.slice(0, split);
|
|
3872
|
+
const inner = level();
|
|
3873
|
+
children.push({
|
|
3874
|
+
prelude: text.slice(split).trim(),
|
|
3875
|
+
...inner
|
|
3876
|
+
});
|
|
3877
|
+
text = "";
|
|
3878
|
+
} else if (char === "}") {
|
|
3879
|
+
index++;
|
|
3880
|
+
return {
|
|
3881
|
+
declarations: declarations + text,
|
|
3882
|
+
children
|
|
3883
|
+
};
|
|
3884
|
+
} else {
|
|
3885
|
+
text += char;
|
|
3886
|
+
index++;
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
return {
|
|
3890
|
+
declarations: declarations + text,
|
|
3891
|
+
children
|
|
3892
|
+
};
|
|
3893
|
+
};
|
|
3894
|
+
return level().children;
|
|
3895
|
+
}
|
|
3896
|
+
/**
|
|
3897
|
+
* The tokens a library's CSS defines: custom properties on the root, on a dark theme (a `.dark` class, a
|
|
3898
|
+
* `[data-…=dark]` attribute or a `prefers-color-scheme: dark` block), and in Tailwind's `@theme`. Properties scoped to
|
|
3899
|
+
* one component's class aren't tokens.
|
|
3900
|
+
*/
|
|
3901
|
+
function cssTokens(css) {
|
|
3902
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
3903
|
+
const visit = (list, media) => {
|
|
3904
|
+
for (const block of list) {
|
|
3905
|
+
if (/^@theme\b/.test(block.prelude)) declarations(block.declarations, "default");
|
|
3906
|
+
else if (block.prelude.startsWith("@")) {
|
|
3907
|
+
visit(block.children, block.prelude.startsWith("@media") ? `${media} ${block.prelude}` : media);
|
|
3908
|
+
continue;
|
|
3909
|
+
} else {
|
|
3910
|
+
const theme = themeOfSelector(block.prelude, media);
|
|
3911
|
+
if (theme) declarations(block.declarations, theme);
|
|
3912
|
+
}
|
|
3913
|
+
visit(block.children, media);
|
|
3914
|
+
}
|
|
3915
|
+
};
|
|
3916
|
+
const declarations = (text, theme) => {
|
|
3917
|
+
for (const match of text.matchAll(/--([\w-]+)\s*:\s*([^;]+)/g)) {
|
|
3918
|
+
const name = match[1];
|
|
3919
|
+
const value = match[2].trim();
|
|
3920
|
+
if (name.includes("--") || name.startsWith("tw-")) continue;
|
|
3921
|
+
const token = tokens.get(name) ?? {
|
|
3922
|
+
var: name,
|
|
3923
|
+
category: categorizeVar(name, value),
|
|
3924
|
+
values: {}
|
|
3925
|
+
};
|
|
3926
|
+
token.values[theme] = value;
|
|
3927
|
+
tokens.set(name, token);
|
|
3928
|
+
}
|
|
3929
|
+
for (const match of text.matchAll(/--([\w-]+)--line-height\s*:\s*([^;]+)/g)) {
|
|
3930
|
+
const token = tokens.get(match[1]);
|
|
3931
|
+
if (token && !token.lineHeight) token.lineHeight = match[2].trim().replace(/^var\(--([\w-]+)\)$/, "$1");
|
|
3932
|
+
}
|
|
3933
|
+
};
|
|
3934
|
+
visit(blocks(css), "");
|
|
3935
|
+
return [...tokens.values()];
|
|
3936
|
+
}
|
|
3937
|
+
/**
|
|
3938
|
+
* Pairs each font size with the line height of the same step, as Parity's type controls do with Tailwind's
|
|
3939
|
+
* `--text-sm--line-height`: `--mantine-font-size-md` with `--mantine-line-height-md`, `fontSizeBase300` with
|
|
3940
|
+
* `lineHeightBase300`.
|
|
3941
|
+
*/
|
|
3942
|
+
function pairLineHeights(tokens) {
|
|
3943
|
+
for (const token of tokens.values()) {
|
|
3944
|
+
if (token.category !== "font-size" || token.lineHeight) continue;
|
|
3945
|
+
const match = [
|
|
3946
|
+
token.var.replace("font-size", "line-height"),
|
|
3947
|
+
token.var.replace("fontSize", "lineHeight"),
|
|
3948
|
+
token.var.replace(/^text-/, "leading-")
|
|
3949
|
+
].find((name) => name !== token.var && tokens.get(name)?.category === "leading");
|
|
3950
|
+
if (match) token.lineHeight = match;
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
/** Breakpoints from `--breakpoint-*` tokens, or any token in that category, in pixels at 16px to the rem. */
|
|
3954
|
+
function breakpointsOf(tokens) {
|
|
3955
|
+
const names = {
|
|
3956
|
+
xs: "Extra small",
|
|
3957
|
+
sm: "Small",
|
|
3958
|
+
md: "Medium",
|
|
3959
|
+
lg: "Large",
|
|
3960
|
+
xl: "XL"
|
|
3961
|
+
};
|
|
3962
|
+
const found = [];
|
|
3963
|
+
for (const token of tokens) {
|
|
3964
|
+
if (token.category !== "breakpoint" || !token.values.default) continue;
|
|
3965
|
+
const width = lengthToPixels(token.values.default);
|
|
3966
|
+
if (width === void 0 || width <= 0) continue;
|
|
3967
|
+
const id = token.var.replace(/^.*breakpoints?-/, "");
|
|
3968
|
+
found.push({
|
|
3969
|
+
id,
|
|
3970
|
+
name: names[id] ?? id.toUpperCase(),
|
|
3971
|
+
width
|
|
3972
|
+
});
|
|
3973
|
+
}
|
|
3974
|
+
return found.sort((a, b) => a.width - b.width);
|
|
3975
|
+
}
|
|
3976
|
+
/**
|
|
3977
|
+
* Tokens written as DTCG (2025.10): grouped by Parity's category, each naming its custom property in
|
|
3978
|
+
* `$extensions.parity`, so the Tokens panel puts `--mantine-spacing-md` under Spacing and frames can declare it. One
|
|
3979
|
+
* theme is a token file; several make a resolver whose `theme` modifier has a context for each.
|
|
3980
|
+
*/
|
|
3981
|
+
function toDtcg(tokens, prefix = "") {
|
|
3982
|
+
const pathOf = /* @__PURE__ */ new Map();
|
|
3983
|
+
const taken = /* @__PURE__ */ new Set();
|
|
3984
|
+
for (const token of tokens) {
|
|
3985
|
+
let path = [token.category, ...nameParts(token.var, prefix, token.category)];
|
|
3986
|
+
if (path.length === 1 || taken.has(path.join("."))) path = [token.category, ...token.var.split("-").filter(Boolean)];
|
|
3987
|
+
while (taken.has(path.join("."))) path = [...path.slice(0, -1), `${path.at(-1)}_`];
|
|
3988
|
+
taken.add(path.join("."));
|
|
3989
|
+
pathOf.set(token.var, path);
|
|
3990
|
+
}
|
|
3991
|
+
const contexts = [...new Set(tokens.flatMap((token) => Object.keys(token.values)))].filter((c) => c !== "default");
|
|
3992
|
+
const groupsFor = (context) => {
|
|
3993
|
+
const root = {};
|
|
3994
|
+
for (const token of tokens) {
|
|
3995
|
+
const value = token.values[context];
|
|
3996
|
+
if (value === void 0 || context !== "default" && value === token.values.default) continue;
|
|
3997
|
+
const path = pathOf.get(token.var);
|
|
3998
|
+
let group = root;
|
|
3999
|
+
for (const part of path.slice(0, -1)) group = group[part] ??= {};
|
|
4000
|
+
const extension = { var: token.var };
|
|
4001
|
+
if (token.lineHeight) extension.lineHeight = token.lineHeight;
|
|
4002
|
+
group[path.at(-1)] = {
|
|
4003
|
+
...dtcgValue(value, token.category, pathOf),
|
|
4004
|
+
...token.description && context === "default" ? { $description: token.description } : {},
|
|
4005
|
+
$extensions: { parity: extension }
|
|
4006
|
+
};
|
|
4007
|
+
}
|
|
4008
|
+
return root;
|
|
4009
|
+
};
|
|
4010
|
+
if (contexts.length === 0) return groupsFor("default");
|
|
4011
|
+
return {
|
|
4012
|
+
version: "2025.10",
|
|
4013
|
+
sets: { tokens: { sources: [groupsFor("default")] } },
|
|
4014
|
+
modifiers: { theme: {
|
|
4015
|
+
default: "default",
|
|
4016
|
+
contexts: Object.fromEntries([["default", []], ...contexts.map((context) => [context, [groupsFor(context)]])])
|
|
4017
|
+
} },
|
|
4018
|
+
resolutionOrder: [{ $ref: "#/sets/tokens" }, { $ref: "#/modifiers/theme" }]
|
|
4019
|
+
};
|
|
4020
|
+
}
|
|
4021
|
+
/** A token's name below its category: `mantine-spacing-md` is `md`, `colorNeutralBackground1` is `neutralBackground1`. */
|
|
4022
|
+
function nameParts(name, prefix, category) {
|
|
4023
|
+
let rest = prefix && name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
4024
|
+
rest = rest.replace(/^-/, "");
|
|
4025
|
+
return (rest.replace({
|
|
4026
|
+
color: /^(?:colou?rs?|palette)[-_]?/i,
|
|
4027
|
+
spacing: /^(?:spacing|space)[-_]?/i,
|
|
4028
|
+
radius: /^(?:border-?radius|radius|radii|rounded)[-_]?/i,
|
|
4029
|
+
"font-size": /^(?:font-?size|text)[-_]?/i,
|
|
4030
|
+
font: /^(?:font-?family|fonts?)[-_]?/i,
|
|
4031
|
+
"font-weight": /^font-?weight[-_]?/i,
|
|
4032
|
+
tracking: /^(?:letter-?spacing|tracking)[-_]?/i,
|
|
4033
|
+
leading: /^(?:line-?height|leading)[-_]?/i,
|
|
4034
|
+
shadow: /^(?:shadows?|elevation)[-_]?/i,
|
|
4035
|
+
breakpoint: /^(?:breakpoints?|screens?)[-_]?/i,
|
|
4036
|
+
container: /^containers?[-_]?/i,
|
|
4037
|
+
effect: /^$/,
|
|
4038
|
+
motion: /^$/,
|
|
4039
|
+
other: /^$/
|
|
4040
|
+
}[category], "") || rest).split("-").filter(Boolean).map((part, index) => index === 0 ? part[0].toLowerCase() + part.slice(1) : part);
|
|
4041
|
+
}
|
|
4042
|
+
/** A CSS value as DTCG writes it, where DTCG can; anything else stays CSS, which Parity reads as written. */
|
|
4043
|
+
function dtcgValue(value, category, paths) {
|
|
4044
|
+
const reference = /^var\(--([\w-]+)\)$/.exec(value.trim())?.[1];
|
|
4045
|
+
if (reference && paths.has(reference)) return { $value: `{${paths.get(reference).join(".")}}` };
|
|
4046
|
+
const hex = /^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i.exec(value.trim())?.[1];
|
|
4047
|
+
if (hex && category === "color") {
|
|
4048
|
+
const full = hex.length <= 4 ? hex.split("").map((digit) => digit + digit).join("") : hex;
|
|
4049
|
+
const channels = [
|
|
4050
|
+
0,
|
|
4051
|
+
2,
|
|
4052
|
+
4
|
|
4053
|
+
].map((at) => Math.round(parseInt(full.slice(at, at + 2), 16) / 255 * 1e3) / 1e3);
|
|
4054
|
+
const alpha = full.length === 8 ? Math.round(parseInt(full.slice(6, 8), 16) / 255 * 1e3) / 1e3 : void 0;
|
|
4055
|
+
return {
|
|
4056
|
+
$type: "color",
|
|
4057
|
+
$value: {
|
|
4058
|
+
colorSpace: "srgb",
|
|
4059
|
+
components: channels,
|
|
4060
|
+
...alpha !== void 0 && alpha < 1 ? { alpha } : {},
|
|
4061
|
+
hex: `#${full.slice(0, 6).toLowerCase()}`
|
|
4062
|
+
}
|
|
4063
|
+
};
|
|
4064
|
+
}
|
|
4065
|
+
const dimension = /^(-?\d*\.?\d+)(px|rem|em)$/.exec(value.trim());
|
|
4066
|
+
if (dimension && [
|
|
4067
|
+
"spacing",
|
|
4068
|
+
"radius",
|
|
4069
|
+
"font-size",
|
|
4070
|
+
"breakpoint",
|
|
4071
|
+
"tracking",
|
|
4072
|
+
"container",
|
|
4073
|
+
"leading"
|
|
4074
|
+
].includes(category)) return {
|
|
4075
|
+
$type: "dimension",
|
|
4076
|
+
$value: {
|
|
4077
|
+
value: Number(dimension[1]),
|
|
4078
|
+
unit: dimension[2]
|
|
4079
|
+
}
|
|
4080
|
+
};
|
|
4081
|
+
const duration = /^(-?\d*\.?\d+)(ms|s)$/.exec(value.trim());
|
|
4082
|
+
if (duration && category === "motion") return {
|
|
4083
|
+
$type: "duration",
|
|
4084
|
+
$value: {
|
|
4085
|
+
value: Number(duration[1]),
|
|
4086
|
+
unit: duration[2]
|
|
4087
|
+
}
|
|
4088
|
+
};
|
|
4089
|
+
if (/^-?\d*\.?\d+$/.test(value.trim())) {
|
|
4090
|
+
const number = Number(value);
|
|
4091
|
+
if (category === "font-weight") return {
|
|
4092
|
+
$type: "fontWeight",
|
|
4093
|
+
$value: number
|
|
4094
|
+
};
|
|
4095
|
+
return {
|
|
4096
|
+
$type: "number",
|
|
4097
|
+
$value: number
|
|
4098
|
+
};
|
|
4099
|
+
}
|
|
4100
|
+
return { $value: value };
|
|
4101
|
+
}
|
|
4102
|
+
// ../libgen/src/index.ts
|
|
4103
|
+
/**
|
|
4104
|
+
* Describes a component library, an icon set or an app as a version 3 `.lib.par`: its components found with the type
|
|
4105
|
+
* checker, its tokens wherever it keeps them, written as DTCG in Parity's categories, its breakpoints, its icons' shape
|
|
4106
|
+
* and the decorators its components need.
|
|
4107
|
+
*/
|
|
4108
|
+
async function generateLibrary(options) {
|
|
4109
|
+
return options.package ? describePackage(options, options.package) : describeApp(options);
|
|
4110
|
+
}
|
|
4111
|
+
async function describePackage(options, importName) {
|
|
4112
|
+
const root = resolve(options.root);
|
|
4113
|
+
const dir = await installedPackage(root, importName);
|
|
4114
|
+
if (!dir) throw new Error(`${importName} isn't installed in ${root}. Install it with the project's package manager first.`);
|
|
4115
|
+
const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf8"));
|
|
4116
|
+
const name = pkg.name ?? importName;
|
|
4117
|
+
const version = pkg.version ?? "0.0.0";
|
|
4118
|
+
const preset = presets[name];
|
|
4119
|
+
const notes = [...preset?.notes ?? []];
|
|
4120
|
+
const iconImport = preset?.icons?.import;
|
|
4121
|
+
const componentImport = iconImport ?? preset?.components;
|
|
4122
|
+
const entry = typesEntry(dir, pkg, componentImport ? subpath(componentImport, name) : ".");
|
|
4123
|
+
if (!entry) notes.push("No type declarations were found, so no components could be read.");
|
|
4124
|
+
const scan = entry ? findComponents({
|
|
4125
|
+
entries: [entry],
|
|
4126
|
+
root
|
|
4127
|
+
}) : {
|
|
4128
|
+
components: [],
|
|
4129
|
+
ambiguous: [],
|
|
4130
|
+
rejected: [],
|
|
4131
|
+
reactTypes: false
|
|
4132
|
+
};
|
|
4133
|
+
if (entry && !scan.reactTypes) notes.push("React's types (@types/react) aren't installed here; install them for exact results.");
|
|
4134
|
+
const dataIcons = preset?.icons?.render !== void 0 && preset.icons.render !== "export";
|
|
4135
|
+
const isIcons = preset?.icons !== void 0 || looksLikeIcons(scan);
|
|
4136
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
4137
|
+
const styles = preset?.styles ?? stylesheetOf(dir, pkg);
|
|
4138
|
+
if (styles) for (const token of cssTokens(await readCss(exportedFile(dir, pkg, styles)))) tokens.set(token.var, token);
|
|
4139
|
+
let breakpoints;
|
|
4140
|
+
if (preset?.theme) try {
|
|
4141
|
+
const theme = await preset.theme(loader(root, dir));
|
|
4142
|
+
for (const token of theme.tokens) tokens.set(token.var, token);
|
|
4143
|
+
breakpoints = theme.breakpoints;
|
|
4144
|
+
} catch (error) {
|
|
4145
|
+
notes.push(`Its theme object couldn't be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
4146
|
+
}
|
|
4147
|
+
pairLineHeights(tokens);
|
|
4148
|
+
const dtcgFile = await dtcgIn(dir);
|
|
4149
|
+
breakpoints ??= breakpointsOf([...tokens.values()]);
|
|
4150
|
+
const report = baseReport(isIcons ? "icons" : "components", scan, [...tokens.values()], notes);
|
|
4151
|
+
report.package = {
|
|
4152
|
+
name,
|
|
4153
|
+
version
|
|
4154
|
+
};
|
|
4155
|
+
const manifest = {
|
|
4156
|
+
version: 3,
|
|
4157
|
+
id: options.id ?? preset?.id ?? name.replace(/^@[^/]+\//, "").replace(/^react-/, ""),
|
|
4158
|
+
name: options.name ?? preset?.name ?? name,
|
|
4159
|
+
...options.description ?? preset?.description ? { description: options.description ?? preset?.description } : {},
|
|
4160
|
+
package: {
|
|
4161
|
+
name,
|
|
4162
|
+
version,
|
|
4163
|
+
range: options.range ?? defaultRange(version)
|
|
4164
|
+
}
|
|
4165
|
+
};
|
|
4166
|
+
if (styles && !isIcons) manifest.styles = styles;
|
|
4167
|
+
if (breakpoints.length > 0) {
|
|
4168
|
+
manifest.breakpoints = breakpoints;
|
|
4169
|
+
report.breakpoints = breakpoints.length;
|
|
4170
|
+
}
|
|
4171
|
+
if (dtcgFile) {
|
|
4172
|
+
manifest.tokens = dtcgFile.tokens;
|
|
4173
|
+
notes.push(`Tokens are its own DTCG file, ${dtcgFile.path}.`);
|
|
4174
|
+
} else if (tokens.size > 0) manifest.tokens = toDtcg([...tokens.values()], commonPrefix([...tokens.keys()]));
|
|
4175
|
+
if (isIcons) {
|
|
4176
|
+
const icons = { render: preset?.icons?.render ?? "export" };
|
|
4177
|
+
if (iconImport) icons.import = iconImport;
|
|
4178
|
+
if (preset?.icons?.match) icons.match = preset.icons.match;
|
|
4179
|
+
if (!dataIcons) {
|
|
4180
|
+
const notIcons = scan.components.filter((component) => component.requiredProps.length > 0 && !component.name.includes(".")).map((component) => component.name);
|
|
4181
|
+
if (notIcons.length > 0) icons.exclude = notIcons;
|
|
4182
|
+
}
|
|
4183
|
+
if (preset?.icons?.variants) icons.variants = preset.icons.variants;
|
|
4184
|
+
if (!dataIcons) {
|
|
4185
|
+
const load = loader(root, dir);
|
|
4186
|
+
const modules = await styleModules(name, typeof pkg.exports === "object" && pkg.exports !== null ? Object.keys(pkg.exports) : [], iconImport ?? name, load, await iconDrawer(load));
|
|
4187
|
+
if (modules) {
|
|
4188
|
+
const main = Object.keys(modules).find((style) => modules[style] === (iconImport ?? name));
|
|
4189
|
+
icons.variants = {
|
|
4190
|
+
...icons.variants,
|
|
4191
|
+
modules,
|
|
4192
|
+
styles: Object.keys(modules),
|
|
4193
|
+
default: {
|
|
4194
|
+
...icons.variants?.default,
|
|
4195
|
+
style: main
|
|
4196
|
+
}
|
|
4197
|
+
};
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
const exported = await exportedIcons(loader(root, dir), iconImport ?? name, icons);
|
|
4201
|
+
icons.count = exported.count;
|
|
4202
|
+
const typesFile = typesFor(iconImport ?? name, root);
|
|
4203
|
+
if (typesFile) {
|
|
4204
|
+
const usable = icons.match ? new RegExp(icons.match) : /^[A-Z]/;
|
|
4205
|
+
const prefer = preferredExports(typesFile, exported.groups, (candidate) => usable.test(candidate));
|
|
4206
|
+
if (prefer.length > 0) icons.prefer = prefer;
|
|
4207
|
+
}
|
|
4208
|
+
if (preset?.tags && options.tags !== false) try {
|
|
4209
|
+
const found = await readIconTags(preset.tags, {
|
|
4210
|
+
name,
|
|
4211
|
+
version
|
|
4212
|
+
}, exported.icons);
|
|
4213
|
+
if (found.matched > 0) {
|
|
4214
|
+
icons.tags = found.tags;
|
|
4215
|
+
icons.tagSource = found.source;
|
|
4216
|
+
}
|
|
4217
|
+
report.iconTags = {
|
|
4218
|
+
matched: found.matched,
|
|
4219
|
+
of: exported.icons.length,
|
|
4220
|
+
source: found.source
|
|
4221
|
+
};
|
|
4222
|
+
} catch (error) {
|
|
4223
|
+
notes.push(`Its tags weren't read: ${error instanceof Error ? error.message : String(error)}`);
|
|
4224
|
+
}
|
|
4225
|
+
manifest.icons = icons;
|
|
4226
|
+
report.icons = icons.count;
|
|
4227
|
+
report.components = 0;
|
|
4228
|
+
} else {
|
|
4229
|
+
const providers = (preset?.decorators ?? []).flatMap((decorator) => decorator.wrap ? [decorator.wrap] : []);
|
|
4230
|
+
let uses = /* @__PURE__ */ new Map();
|
|
4231
|
+
let anatomy = /* @__PURE__ */ new Map();
|
|
4232
|
+
let exampleFiles = [];
|
|
4233
|
+
if (options.examples !== false) {
|
|
4234
|
+
const fetched = await fetchExamples(pkg, dir, preset?.examples);
|
|
4235
|
+
if (fetched.note) notes.push(fetched.note);
|
|
4236
|
+
const componentNames = scan.components.map((component) => component.name);
|
|
4237
|
+
exampleFiles = fetched.files;
|
|
4238
|
+
const own = pkg.name ? await loader(root, dir)(pkg.name).then((module) => new Set(Object.keys(module).filter((key) => typeof module[key] === "function"))).catch(() => /* @__PURE__ */ new Set()) : /* @__PURE__ */ new Set();
|
|
4239
|
+
uses = readExamples(fetched.files, componentNames, pkg.name, own);
|
|
4240
|
+
anatomy = readAnatomy(fetched.docs ?? [], componentNames);
|
|
4241
|
+
if (fetched.source) report.examples = {
|
|
4242
|
+
...fetched.source,
|
|
4243
|
+
used: uses.size
|
|
4244
|
+
};
|
|
4245
|
+
}
|
|
4246
|
+
const names = scan.components.map((component) => component.name);
|
|
4247
|
+
const calls = /* @__PURE__ */ new Map();
|
|
4248
|
+
const made = /* @__PURE__ */ new Set();
|
|
4249
|
+
const renders = await readRenders(dir, names, calls, made);
|
|
4250
|
+
const listItem = new Set([...calls].flatMap(([part, list]) => list.some((call) => Object.values(call.props).some((passed) => "item" in passed)) ? [part] : [])).size > 0 ? exampleItem(exampleFiles, made) : void 0;
|
|
4251
|
+
const parts = await relateParts(loader(root, dir), componentImport ?? name, scan.components, providers, options.trialEdits?.components, uses, renderedIn(renders), calls, listItem, options.appErrors);
|
|
4252
|
+
if (parts.note) notes.push(parts.note);
|
|
4253
|
+
const hosts = (preset?.decorators ?? []).flatMap((decorator) => decorator.add && !decorator.add.component.includes(".") ? [decorator.add.component] : []);
|
|
4254
|
+
const drawnTree = new Set(parts.drawn);
|
|
4255
|
+
if (hosts.length > 0) for (const hosted of hostedParts(renders, hosts, names)) {
|
|
4256
|
+
if (uses.has(hosted)) continue;
|
|
4257
|
+
const relation = parts.relations.get(hosted);
|
|
4258
|
+
if (listItem && drawnTree.has(hosted) && !relation?.inside) continue;
|
|
4259
|
+
parts.relations.set(hosted, {
|
|
4260
|
+
...relation,
|
|
4261
|
+
visual: false
|
|
4262
|
+
});
|
|
4263
|
+
}
|
|
4264
|
+
const fromHook = [...uses].filter(([, use]) => use.fromHook).map(([part]) => part);
|
|
4265
|
+
if (fromHook.length > 0) notes.push(`Drawn from a hook's list in every example, sampled with the item it's given: ${fromHook.join(", ")}`);
|
|
4266
|
+
const owners = renderedIn(renders);
|
|
4267
|
+
const internal = new Set(names.filter((part) => uses.size > 0 && !uses.has(part) && !anatomy.has(part) && !hosts.includes(part) && (owners.get(part) ?? []).some((owner) => owner !== part && names.includes(owner))));
|
|
4268
|
+
const list = {};
|
|
4269
|
+
for (const component of scan.components) {
|
|
4270
|
+
const inside = component.file.startsWith(dir.replaceAll(sep, "/")) || component.file.startsWith(dir);
|
|
4271
|
+
const relation = parts.relations.get(component.name);
|
|
4272
|
+
list[component.name] = {
|
|
4273
|
+
as: component.as,
|
|
4274
|
+
...inside && exportsByName(component.file, component.name) ? { declaration: relativePath(dir, component.file) } : {},
|
|
4275
|
+
group: component.group,
|
|
4276
|
+
...relation?.parents ? {
|
|
4277
|
+
parents: relation.parents,
|
|
4278
|
+
parentsFrom: relation.parentsFrom ?? "prefix"
|
|
4279
|
+
} : {},
|
|
4280
|
+
...relation?.optionalParents ? { optionalParents: relation.optionalParents } : {},
|
|
4281
|
+
...relation?.parentParts ? { parentParts: true } : {},
|
|
4282
|
+
...relation?.visual === false ? { visual: false } : {},
|
|
4283
|
+
...internal.has(component.name) ? { internal: true } : {},
|
|
4284
|
+
...relation?.sample ? { sample: relation.sample } : {},
|
|
4285
|
+
fingerprint: component.fingerprint,
|
|
4286
|
+
...Object.keys(component.props).length > 0 ? { props: component.props } : {}
|
|
4287
|
+
};
|
|
4288
|
+
}
|
|
4289
|
+
if (parts.tried) report.parts = summarizeParts(parts.relations);
|
|
4290
|
+
if (anatomy.size > 0) report.anatomy = checkAnatomy(anatomy, list);
|
|
4291
|
+
if (Object.keys(list).length > 0) manifest.components = {
|
|
4292
|
+
...componentImport ? { import: componentImport } : {},
|
|
4293
|
+
list
|
|
4294
|
+
};
|
|
4295
|
+
}
|
|
4296
|
+
if (preset?.decorators?.length) {
|
|
4297
|
+
manifest.decorators = preset.decorators;
|
|
4298
|
+
report.decorators = preset.decorators.map(describeDecorator);
|
|
4299
|
+
}
|
|
4300
|
+
parseLibraryManifest(manifest);
|
|
4301
|
+
return reviseLibrary({
|
|
4302
|
+
manifest,
|
|
4303
|
+
report
|
|
4304
|
+
}, options);
|
|
4305
|
+
}
|
|
4306
|
+
/**
|
|
4307
|
+
* Lays a freshly generated file over the one it replaces: reports which components are new, changed and removed,
|
|
4308
|
+
* keeps the old entries `only` leaves alone, and carries the hand edits over (or points at shared ones), reporting
|
|
4309
|
+
* any whose export has gone. Edits are never dropped silently.
|
|
4310
|
+
*/
|
|
4311
|
+
async function reviseLibrary(generated, options) {
|
|
4312
|
+
const manifest = { ...generated.manifest };
|
|
4313
|
+
const report = { ...generated.report };
|
|
4314
|
+
const previous = previousComponents(options.previous);
|
|
4315
|
+
const components = manifest.components;
|
|
4316
|
+
if (previous && components) {
|
|
4317
|
+
report.changes = compareComponents(previous, components.list);
|
|
4318
|
+
manifest.components = {
|
|
4319
|
+
...components,
|
|
4320
|
+
list: selectComponents(previous, components.list, report.changes, options.only ?? "all")
|
|
4321
|
+
};
|
|
4322
|
+
}
|
|
4323
|
+
const kept = options.previous?.edits;
|
|
4324
|
+
const edits = options.edits ?? kept;
|
|
4325
|
+
if (edits !== void 0) {
|
|
4326
|
+
manifest.edits = edits;
|
|
4327
|
+
const resolved = parseLibraryEdits(typeof edits === "string" ? parseJsonc(await readFile(resolve(options.editsBase ?? options.root, edits), "utf8")) : edits);
|
|
4328
|
+
const { orphans } = applyLibraryEdits(parseLibraryManifest(manifest), resolved);
|
|
4329
|
+
if (orphans.length > 0) report.orphanEdits = orphans;
|
|
4330
|
+
}
|
|
4331
|
+
return {
|
|
4332
|
+
manifest,
|
|
4333
|
+
report
|
|
4334
|
+
};
|
|
4335
|
+
}
|
|
4336
|
+
function previousComponents(previous) {
|
|
4337
|
+
if (typeof previous !== "object" || previous === null) return void 0;
|
|
4338
|
+
try {
|
|
4339
|
+
return parseLibraryManifest(previous).components?.list;
|
|
4340
|
+
} catch {
|
|
4341
|
+
return;
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
/**
|
|
4345
|
+
* What changed between two versions of a library's components, by fingerprint: new, removed, and changed ones with the
|
|
4346
|
+
* props added, removed or retyped. A file written before fingerprints counts every component as changed.
|
|
4347
|
+
*/
|
|
4348
|
+
function compareComponents(before, after) {
|
|
4349
|
+
const changes = {
|
|
4350
|
+
added: [],
|
|
4351
|
+
changed: [],
|
|
4352
|
+
removed: [],
|
|
4353
|
+
unchanged: 0
|
|
4354
|
+
};
|
|
4355
|
+
for (const [name, now] of Object.entries(after)) {
|
|
4356
|
+
const then = before[name];
|
|
4357
|
+
if (!then) changes.added.push(name);
|
|
4358
|
+
else if (then.fingerprint && then.fingerprint === now.fingerprint) changes.unchanged++;
|
|
4359
|
+
else {
|
|
4360
|
+
const old = propTypes(then.props);
|
|
4361
|
+
const fresh = propTypes(now.props);
|
|
4362
|
+
changes.changed.push({
|
|
4363
|
+
name,
|
|
4364
|
+
added: [...fresh.keys()].filter((prop) => !old.has(prop)),
|
|
4365
|
+
removed: [...old.keys()].filter((prop) => !fresh.has(prop)),
|
|
4366
|
+
retyped: [...fresh.keys()].filter((prop) => old.has(prop) && old.get(prop) !== fresh.get(prop)),
|
|
4367
|
+
...then.as === now.as ? {} : { as: `${then.as} to ${now.as}` }
|
|
4368
|
+
});
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
changes.removed = Object.keys(before).filter((name) => !(name in after));
|
|
4372
|
+
return changes;
|
|
4373
|
+
}
|
|
4374
|
+
/** Each prop by name, without the `?` that marks it optional, to its type hash and whether it's optional. */
|
|
4375
|
+
function propTypes(props) {
|
|
4376
|
+
return new Map(Object.entries(props ?? {}).map(([prop, kind]) => [prop.endsWith("?") ? prop.slice(0, -1) : prop, prop.endsWith("?") ? `${kind}?` : kind]));
|
|
4377
|
+
}
|
|
4378
|
+
/**
|
|
4379
|
+
* The components to write: every one found (`all`), or the file as it was plus new components (`new`) or plus
|
|
4380
|
+
* these named ones. Components the package no longer has are dropped either way, since they can't be imported.
|
|
4381
|
+
*/
|
|
4382
|
+
function selectComponents(before, after, changes, only) {
|
|
4383
|
+
if (only === "all") return after;
|
|
4384
|
+
const picked = new Set(only === "new" ? changes.added : only);
|
|
4385
|
+
const list = {};
|
|
4386
|
+
for (const name of Object.keys(after)) {
|
|
4387
|
+
const chosen = picked.has(name) ? after[name] : before[name];
|
|
4388
|
+
if (chosen) list[name] = chosen;
|
|
4389
|
+
}
|
|
4390
|
+
return list;
|
|
4391
|
+
}
|
|
4392
|
+
function summarizeParts(relations) {
|
|
4393
|
+
const summary = {
|
|
4394
|
+
failedAlone: 0,
|
|
4395
|
+
parents: {},
|
|
4396
|
+
fixed: 0,
|
|
4397
|
+
failing: [],
|
|
4398
|
+
notVisual: 0
|
|
4399
|
+
};
|
|
4400
|
+
for (const [name, relation] of relations) {
|
|
4401
|
+
if (relation.visual === false) summary.notVisual++;
|
|
4402
|
+
if (relation.parentsFrom) summary.parents[relation.parentsFrom] = (summary.parents[relation.parentsFrom] ?? 0) + 1;
|
|
4403
|
+
if (!relation.alone || relation.visual === false) continue;
|
|
4404
|
+
summary.failedAlone++;
|
|
4405
|
+
if (relation.inside) summary.failing.push({
|
|
4406
|
+
name,
|
|
4407
|
+
error: relation.inside
|
|
4408
|
+
});
|
|
4409
|
+
else summary.fixed++;
|
|
4410
|
+
}
|
|
4411
|
+
return summary;
|
|
4412
|
+
}
|
|
4413
|
+
async function describeApp(options) {
|
|
4414
|
+
const root = resolve(options.root);
|
|
4415
|
+
const pkg = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
4416
|
+
const notes = [];
|
|
4417
|
+
const config = appConfig(root);
|
|
4418
|
+
if (!config) notes.push("No tsconfig.json or jsconfig.json, so every .tsx and .jsx file under src was read.");
|
|
4419
|
+
const scan = findComponents({
|
|
4420
|
+
entries: (config?.fileNames ?? await sourceFiles(join(root, "src"))).filter((file) => /\.[jt]sx$/.test(file) && !/\.(stories|test|spec)\.[jt]sx$/.test(file) && !file.includes("/node_modules/")),
|
|
4421
|
+
root,
|
|
4422
|
+
...config ? { compilerOptions: config.options } : {},
|
|
4423
|
+
defaultExports: true
|
|
4424
|
+
});
|
|
4425
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4426
|
+
for (const component of scan.components) counts.set(component.name, (counts.get(component.name) ?? 0) + 1);
|
|
4427
|
+
const list = {};
|
|
4428
|
+
for (const component of scan.components) {
|
|
4429
|
+
const folder = basename(dirname(component.file));
|
|
4430
|
+
const id = (counts.get(component.name) ?? 0) > 1 ? `${folder}/${component.name}` : component.name;
|
|
4431
|
+
list[id] = {
|
|
4432
|
+
as: component.as,
|
|
4433
|
+
declaration: relativePath(root, component.file),
|
|
4434
|
+
group: component.group
|
|
4435
|
+
};
|
|
4436
|
+
}
|
|
4437
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
4438
|
+
for (const stylesheet of await appStylesheets(root)) for (const token of cssTokens(await readCss(join(root, stylesheet)))) tokens.set(token.var, token);
|
|
4439
|
+
pairLineHeights(tokens);
|
|
4440
|
+
const breakpoints = breakpointsOf([...tokens.values()]);
|
|
4441
|
+
const name = pkg.name ?? basename(root);
|
|
4442
|
+
const manifest = {
|
|
4443
|
+
version: 3,
|
|
4444
|
+
id: options.id ?? name.replace(/^@[^/]+\//, ""),
|
|
4445
|
+
name: options.name ?? name,
|
|
4446
|
+
...options.description ? { description: options.description } : {},
|
|
4447
|
+
...breakpoints.length > 0 ? { breakpoints } : {},
|
|
4448
|
+
...tokens.size > 0 ? { tokens: toDtcg([...tokens.values()]) } : {},
|
|
4449
|
+
...Object.keys(list).length > 0 ? { components: { list } } : {}
|
|
4450
|
+
};
|
|
4451
|
+
const report = baseReport("app", scan, [...tokens.values()], notes);
|
|
4452
|
+
report.breakpoints = breakpoints.length;
|
|
4453
|
+
parseLibraryManifest(manifest);
|
|
4454
|
+
return {
|
|
4455
|
+
manifest,
|
|
4456
|
+
report
|
|
4457
|
+
};
|
|
4458
|
+
}
|
|
4459
|
+
/**
|
|
4460
|
+
* Where an app's `.lib.par` goes: an existing one at its root, else `<package name>.lib.par`. Writing it there makes
|
|
4461
|
+
* the app a library Parity opens on the canvas.
|
|
4462
|
+
*/
|
|
4463
|
+
async function appManifestPath(root) {
|
|
4464
|
+
const existing = (await readdir(root)).find((file) => file.endsWith(libraryManifestExtension));
|
|
4465
|
+
if (existing) return join(root, existing);
|
|
4466
|
+
const pkg = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
4467
|
+
return join(root, `${(pkg.name ?? basename(root)).replace(/^@[^/]+\//, "")}${libraryManifestExtension}`);
|
|
4468
|
+
}
|
|
4469
|
+
/**
|
|
4470
|
+
* A generated manifest laid over the file already there: what generating finds replaces what it found before, and
|
|
4471
|
+
* what a person wrote (the canvas, decorators, a name or description) stays.
|
|
4472
|
+
*/
|
|
4473
|
+
function mergeManifest(existing, generated) {
|
|
4474
|
+
if (typeof existing !== "object" || existing === null) return generated;
|
|
4475
|
+
const kept = existing;
|
|
4476
|
+
const authored = [
|
|
4477
|
+
"$schema",
|
|
4478
|
+
"id",
|
|
4479
|
+
"name",
|
|
4480
|
+
"description",
|
|
4481
|
+
"decorators",
|
|
4482
|
+
"edits",
|
|
4483
|
+
"canvas"
|
|
4484
|
+
];
|
|
4485
|
+
const merged = { ...generated };
|
|
4486
|
+
for (const key of authored) if (kept[key] !== void 0) merged[key] = kept[key];
|
|
4487
|
+
return merged;
|
|
4488
|
+
}
|
|
4489
|
+
function baseReport(kind, scan, tokens, notes) {
|
|
4490
|
+
const counts = {};
|
|
4491
|
+
for (const token of tokens) counts[token.category] = (counts[token.category] ?? 0) + 1;
|
|
4492
|
+
return {
|
|
4493
|
+
kind,
|
|
4494
|
+
components: scan.components.length,
|
|
4495
|
+
ambiguous: scan.ambiguous,
|
|
4496
|
+
rejected: scan.rejected.length,
|
|
4497
|
+
tokens: counts,
|
|
4498
|
+
themes: [...new Set(tokens.flatMap((token) => Object.keys(token.values)))].filter((theme) => theme !== "default"),
|
|
4499
|
+
breakpoints: 0,
|
|
4500
|
+
decorators: [],
|
|
4501
|
+
providers: scan.components.map((component) => component.name).filter((name) => /(?:Provider|Toaster|Toast\.Viewport|Notifications)$/.test(name)).filter((name) => !/(?:Props|Root|Node|Adapter)Provider$/.test(name)),
|
|
4502
|
+
notes
|
|
4503
|
+
};
|
|
4504
|
+
}
|
|
4505
|
+
function describeDecorator(decorator) {
|
|
4506
|
+
const when = decorator.when ? ` when ${Object.entries(decorator.when).map(([key, value]) => `${key} is ${value}`).join(" and ")}` : "";
|
|
4507
|
+
if (decorator.wrap) return `wraps in ${decorator.wrap.component} from ${decorator.wrap.import}${when}`;
|
|
4508
|
+
if (decorator.add) return `adds ${decorator.add.component} from ${decorator.add.import}${when}`;
|
|
4509
|
+
if (decorator.class !== void 0) return `adds a class to the frame${when}`;
|
|
4510
|
+
if (decorator.attributes) return `sets ${Object.keys(decorator.attributes).join(", ")} on the frame${when}`;
|
|
4511
|
+
return `styles the frame${when}`;
|
|
4512
|
+
}
|
|
4513
|
+
/**
|
|
4514
|
+
* Whether a package is an icon set: hundreds of components that take no required props and nearly all share one type,
|
|
4515
|
+
* as Lucide's `LucideIcon` or Fluent's `FluentIcon`. A component library's components each have their own props.
|
|
4516
|
+
*/
|
|
4517
|
+
function looksLikeIcons(scan) {
|
|
4518
|
+
if (scan.components.length < 200) return false;
|
|
4519
|
+
const types = /* @__PURE__ */ new Map();
|
|
4520
|
+
for (const component of scan.components) if (component.requiredProps.length === 0) types.set(component.type, (types.get(component.type) ?? 0) + 1);
|
|
4521
|
+
return Math.max(0, ...types.values()) / scan.components.length > .9;
|
|
4522
|
+
}
|
|
4523
|
+
/** `^9.0.0` for 9.5.0, and `^0.4.0` for 0.4.1, since a 0.x minor can break. */
|
|
4524
|
+
function defaultRange(version) {
|
|
4525
|
+
const [major = "0", minor = "0"] = version.split(/[.-]/);
|
|
4526
|
+
return major === "0" ? `^0.${minor}.0` : `^${major}.0.0`;
|
|
4527
|
+
}
|
|
4528
|
+
function subpath(specifier, packageName) {
|
|
4529
|
+
return specifier === packageName ? "." : `.${specifier.slice(packageName.length)}`;
|
|
4530
|
+
}
|
|
4531
|
+
/** The type declarations for one of a package's entry points, from `exports`, else `types`, else `index.d.ts`. */
|
|
4532
|
+
function typesEntry(dir, pkg, entry) {
|
|
4533
|
+
const exported = typeof pkg.exports === "object" && pkg.exports !== null ? pkg.exports : {};
|
|
4534
|
+
const condition = (value) => {
|
|
4535
|
+
if (typeof value === "string") return /\.d\.[cm]?ts$/.test(value) ? value : void 0;
|
|
4536
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
4537
|
+
const record = value;
|
|
4538
|
+
for (const key of [
|
|
4539
|
+
"types",
|
|
4540
|
+
"import",
|
|
4541
|
+
"default",
|
|
4542
|
+
"require"
|
|
4543
|
+
]) {
|
|
4544
|
+
const found = condition(record[key]);
|
|
4545
|
+
if (found) return found;
|
|
4546
|
+
}
|
|
4547
|
+
};
|
|
4548
|
+
const candidates = [
|
|
4549
|
+
condition(exported[entry]),
|
|
4550
|
+
entry === "." ? pkg.types ?? pkg.typings : void 0,
|
|
4551
|
+
entry === "." ? "index.d.ts" : `${entry.slice(2)}/index.d.ts`,
|
|
4552
|
+
entry === "." ? void 0 : `${entry.slice(2)}.d.ts`
|
|
4553
|
+
];
|
|
4554
|
+
for (const candidate of candidates) if (candidate && existsSync(join(dir, candidate))) return join(dir, candidate).replaceAll("\\", "/");
|
|
4555
|
+
}
|
|
4556
|
+
/** The stylesheet a project imports to use a package: its `style` field, else the CSS its `exports` list first. */
|
|
4557
|
+
function stylesheetOf(dir, pkg) {
|
|
4558
|
+
if (pkg.style && existsSync(join(dir, pkg.style))) return pkg.style;
|
|
4559
|
+
const css = (typeof pkg.exports === "object" && pkg.exports !== null ? Object.keys(pkg.exports) : []).filter((key) => key.endsWith(".css")).map((key) => key.slice(2));
|
|
4560
|
+
const pick = [
|
|
4561
|
+
"tailwind.css",
|
|
4562
|
+
"styles.css",
|
|
4563
|
+
"style.css",
|
|
4564
|
+
"index.css",
|
|
4565
|
+
"theme.css"
|
|
4566
|
+
].find((name) => css.includes(name)) ?? css[0];
|
|
4567
|
+
if (!pick) return void 0;
|
|
4568
|
+
const target = pkg.exports[`./${pick}`];
|
|
4569
|
+
const file = typeof target === "string" ? target : target?.default;
|
|
4570
|
+
return typeof file === "string" && existsSync(join(dir, file)) ? pick : void 0;
|
|
4571
|
+
}
|
|
4572
|
+
/** The file an export such as `tailwind.css` points at: `dist/tailwind.css` for Buildkit's styles. */
|
|
4573
|
+
function exportedFile(dir, pkg, specifier) {
|
|
4574
|
+
const target = (typeof pkg.exports === "object" && pkg.exports !== null ? pkg.exports : {})[`./${specifier}`];
|
|
4575
|
+
const file = typeof target === "string" ? target : target?.default;
|
|
4576
|
+
return join(dir, typeof file === "string" ? file : specifier);
|
|
4577
|
+
}
|
|
4578
|
+
/** A stylesheet with its relative `@import`s inlined, so a Tailwind entry brings its theme file with it. */
|
|
4579
|
+
async function readCss(path, seen = /* @__PURE__ */ new Set()) {
|
|
4580
|
+
if (seen.has(path)) return "";
|
|
4581
|
+
seen.add(path);
|
|
4582
|
+
const source = await readFile(path, "utf8").catch(() => "");
|
|
4583
|
+
const parts = [];
|
|
4584
|
+
let last = 0;
|
|
4585
|
+
for (const match of source.matchAll(/@import\s+(?:url\()?["'](\.{1,2}\/[^"']+\.css)["']\)?[^;]*;/g)) {
|
|
4586
|
+
parts.push(source.slice(last, match.index), await readCss(join(dirname(path), match[1]), seen));
|
|
4587
|
+
last = match.index + match[0].length;
|
|
4588
|
+
}
|
|
4589
|
+
parts.push(source.slice(last));
|
|
4590
|
+
return parts.join("");
|
|
4591
|
+
}
|
|
4592
|
+
/** A DTCG token file the package ships, if it has one in the format; Buildkit's `tokens.json` isn't yet. */
|
|
4593
|
+
async function dtcgIn(dir) {
|
|
4594
|
+
for (const candidate of [
|
|
4595
|
+
"tokens.json",
|
|
4596
|
+
"dist/tokens.json",
|
|
4597
|
+
"tokens.resolver.json",
|
|
4598
|
+
"dist/tokens.resolver.json"
|
|
4599
|
+
]) {
|
|
4600
|
+
const text = await readFile(join(dir, candidate), "utf8").catch(() => void 0);
|
|
4601
|
+
if (!text) continue;
|
|
4602
|
+
try {
|
|
4603
|
+
const parsed = JSON.parse(text);
|
|
4604
|
+
if (Array.isArray(parsed.resolutionOrder) || /"\$value"\s*:/.test(text)) return {
|
|
4605
|
+
path: candidate,
|
|
4606
|
+
tokens: parsed
|
|
4607
|
+
};
|
|
4608
|
+
} catch {}
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
/** The prefix most custom properties share, as in `mantine-`, which token paths leave out. */
|
|
4612
|
+
function commonPrefix(names) {
|
|
4613
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4614
|
+
for (const name of names) {
|
|
4615
|
+
const first = /^([a-z]+)-/.exec(name)?.[1];
|
|
4616
|
+
if (first) counts.set(first, (counts.get(first) ?? 0) + 1);
|
|
4617
|
+
}
|
|
4618
|
+
const [best, count] = [...counts].sort((a, b) => b[1] - a[1])[0] ?? ["", 0];
|
|
4619
|
+
return count / Math.max(names.length, 1) > .5 ? `${best}-` : "";
|
|
4620
|
+
}
|
|
4621
|
+
/**
|
|
4622
|
+
* A set's icons as the Icons panel lists them: its matching exports, each alias of one icon (Lucide's `Heart` and
|
|
4623
|
+
* `HeartIcon`) once, counted, and grouped by label with every name each is exported under, for matching tags. The
|
|
4624
|
+
* package is loaded to tell aliases apart, since types can't.
|
|
4625
|
+
*/
|
|
4626
|
+
async function exportedIcons(load, specifier, icons) {
|
|
4627
|
+
const module = await load(specifier).catch(() => ({}));
|
|
4628
|
+
const pattern = icons.match ? new RegExp(icons.match) : /^[A-Z]/;
|
|
4629
|
+
const excluded = new Set(icons.exclude);
|
|
4630
|
+
const drawable = (value) => icons.render !== "export" ? value !== void 0 && typeof value !== "function" : typeof value === "function" || typeof value === "object" && value !== null && "$$typeof" in value;
|
|
4631
|
+
const byValue = /* @__PURE__ */ new Map();
|
|
4632
|
+
for (const [key, value] of Object.entries(module)) {
|
|
4633
|
+
if (!/^[A-Z]/.test(key) || excluded.has(key) || !drawable(value)) continue;
|
|
4634
|
+
const names = byValue.get(value);
|
|
4635
|
+
if (names) names.push(key);
|
|
4636
|
+
else byValue.set(value, [key]);
|
|
4637
|
+
}
|
|
4638
|
+
const kept = [...byValue.values()].flatMap((names) => {
|
|
4639
|
+
const name = names.filter((candidate) => pattern.test(candidate)).sort((a, b) => a.length - b.length || byCodeUnit(a, b))[0];
|
|
4640
|
+
return name ? [{
|
|
4641
|
+
name,
|
|
4642
|
+
names
|
|
4643
|
+
}] : [];
|
|
4644
|
+
});
|
|
4645
|
+
const read = icons.variants?.pattern ? iconVariants(kept.map(({ name }) => name), icons.variants) : void 0;
|
|
4646
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
4647
|
+
for (const { name, names } of kept) {
|
|
4648
|
+
const label = read?.get(name)?.label ?? name;
|
|
4649
|
+
const all = byLabel.get(label) ?? /* @__PURE__ */ new Set();
|
|
4650
|
+
for (const each of names) all.add(each);
|
|
4651
|
+
byLabel.set(label, all);
|
|
4652
|
+
}
|
|
4653
|
+
return {
|
|
4654
|
+
count: kept.length,
|
|
4655
|
+
icons: [...byLabel].map(([label, names]) => ({
|
|
4656
|
+
label,
|
|
4657
|
+
names: [...names]
|
|
4658
|
+
})),
|
|
4659
|
+
groups: kept.map(({ names }) => names)
|
|
4660
|
+
};
|
|
4661
|
+
}
|
|
4662
|
+
/** Compares two strings by their UTF-16 code units, as `sort` does without a comparer. */
|
|
4663
|
+
function byCodeUnit(a, b) {
|
|
4664
|
+
if (a < b) return -1;
|
|
4665
|
+
return a > b ? 1 : 0;
|
|
4666
|
+
}
|
|
4667
|
+
/** Draws an icon with the project's own React, to tell a set's styles apart; nothing draws where React can't load. */
|
|
4668
|
+
async function iconDrawer(load) {
|
|
4669
|
+
try {
|
|
4670
|
+
const React = await load("react");
|
|
4671
|
+
const server = await load("react-dom/server");
|
|
4672
|
+
const createElement = React.createElement ?? React.default?.createElement;
|
|
4673
|
+
const renderToStaticMarkup = server.renderToStaticMarkup ?? server.default?.renderToStaticMarkup;
|
|
4674
|
+
if (typeof createElement !== "function" || typeof renderToStaticMarkup !== "function") return () => void 0;
|
|
4675
|
+
return (component) => {
|
|
4676
|
+
try {
|
|
4677
|
+
return renderToStaticMarkup(createElement(component));
|
|
4678
|
+
} catch {
|
|
4679
|
+
return;
|
|
4680
|
+
}
|
|
4681
|
+
};
|
|
4682
|
+
} catch {
|
|
4683
|
+
return () => void 0;
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
/** Whether a declaration file exports a name directly, so the props reader can find it there. */
|
|
4687
|
+
function exportsByName(file, name) {
|
|
4688
|
+
if (name.includes(".")) return false;
|
|
4689
|
+
const source = ts.sys.readFile(file) ?? "";
|
|
4690
|
+
return new RegExp(`export\\s+(?:declare\\s+)?(?:const|function|class|let|var)\\s+${name}\\b|export\\s*\\{[^}]*\\b${name}\\b`).test(source);
|
|
4691
|
+
}
|
|
4692
|
+
/**
|
|
4693
|
+
* Imports a module from the project, as the project's code would, for reading theme objects and counting icons.
|
|
4694
|
+
* Packages a library depends on, such as Fluent's `@fluentui/tokens`, may only be reachable from the library itself.
|
|
4695
|
+
*/
|
|
4696
|
+
function loader(root, packageDir) {
|
|
4697
|
+
return async (specifier) => {
|
|
4698
|
+
for (const from of [root, packageDir]) {
|
|
4699
|
+
const esm = await resolveImport(from, specifier);
|
|
4700
|
+
const cjs = resolveRequire(from, specifier);
|
|
4701
|
+
const path = esm ?? cjs;
|
|
4702
|
+
if (!path) continue;
|
|
4703
|
+
try {
|
|
4704
|
+
return await import(pathToFileURL(path).href);
|
|
4705
|
+
} catch (error) {
|
|
4706
|
+
if (!cjs || cjs === path) throw error;
|
|
4707
|
+
return await import(pathToFileURL(cjs).href);
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
throw new Error(`Can't find ${specifier} from ${root}`);
|
|
4711
|
+
};
|
|
4712
|
+
}
|
|
4713
|
+
function resolveRequire(from, specifier) {
|
|
4714
|
+
try {
|
|
4715
|
+
return createRequire(join(from, "package.json")).resolve(specifier);
|
|
4716
|
+
} catch {
|
|
4717
|
+
return;
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
/** An ESM-only package's entry, from the `import` condition of its `exports`, which `require` can't see. */
|
|
4721
|
+
async function resolveImport(from, specifier) {
|
|
4722
|
+
const name = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
|
|
4723
|
+
const dir = await installedPackage(from, name);
|
|
4724
|
+
if (!dir) return void 0;
|
|
4725
|
+
const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf8"));
|
|
4726
|
+
const exported = typeof pkg.exports === "object" && pkg.exports !== null ? pkg.exports : {};
|
|
4727
|
+
const pick = (value) => {
|
|
4728
|
+
if (typeof value === "string") return /\.d\.[cm]?ts$/.test(value) ? void 0 : value;
|
|
4729
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
4730
|
+
const record = value;
|
|
4731
|
+
for (const key of [
|
|
4732
|
+
"node",
|
|
4733
|
+
"import",
|
|
4734
|
+
"default"
|
|
4735
|
+
]) {
|
|
4736
|
+
const found = pick(record[key]);
|
|
4737
|
+
if (found) return found;
|
|
4738
|
+
}
|
|
4739
|
+
};
|
|
4740
|
+
const file = pick(exported[subpath(specifier, name)]) ?? (specifier === name ? pkg.module : void 0);
|
|
4741
|
+
return file ? join(dir, file) : void 0;
|
|
4742
|
+
}
|
|
4743
|
+
function appConfig(root) {
|
|
4744
|
+
for (const name of [
|
|
4745
|
+
"tsconfig.app.json",
|
|
4746
|
+
"tsconfig.json",
|
|
4747
|
+
"jsconfig.json"
|
|
4748
|
+
]) {
|
|
4749
|
+
const path = join(root, name);
|
|
4750
|
+
if (!existsSync(path)) continue;
|
|
4751
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(path, {}, {
|
|
4752
|
+
...ts.sys,
|
|
4753
|
+
onUnRecoverableConfigFileDiagnostic: () => void 0
|
|
4754
|
+
});
|
|
4755
|
+
if (parsed && parsed.fileNames.length > 0) return parsed;
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
async function sourceFiles(folder) {
|
|
4759
|
+
const entries = await readdir(folder, { withFileTypes: true }).catch(() => []);
|
|
4760
|
+
const files = [];
|
|
4761
|
+
for (const entry of entries) {
|
|
4762
|
+
const path = join(folder, entry.name).replaceAll("\\", "/");
|
|
4763
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") files.push(...await sourceFiles(path));
|
|
4764
|
+
else if (/\.[jt]sx?$/.test(entry.name)) files.push(path);
|
|
4765
|
+
}
|
|
4766
|
+
return files;
|
|
4767
|
+
}
|
|
4768
|
+
/** An app's global stylesheets: shadcn's `components.json` names one; otherwise the usual places. */
|
|
4769
|
+
async function appStylesheets(root) {
|
|
4770
|
+
const shadcn = await readFile(join(root, "components.json"), "utf8").then((text) => JSON.parse(text), () => void 0);
|
|
4771
|
+
if (shadcn?.tailwind?.css) return [shadcn.tailwind.css];
|
|
4772
|
+
return [
|
|
4773
|
+
"src/index.css",
|
|
4774
|
+
"src/styles.css",
|
|
4775
|
+
"src/app.css",
|
|
4776
|
+
"src/globals.css",
|
|
4777
|
+
"app/globals.css"
|
|
4778
|
+
].filter((path) => existsSync(join(root, path)));
|
|
4779
|
+
}
|
|
4780
|
+
export { appManifestPath, generateLibrary, manifestText, mergeManifest, parseLibraryEdits, reviseLibrary, iconVariants as t };
|