synthesisui 0.16.80 → 0.16.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/import.js +230 -11
- package/dist/doctor/architecture.js +134 -0
- package/dist/doctor/call-sites.js +171 -0
- package/dist/doctor/component-gate.js +235 -0
- package/dist/doctor/transcribe.js +116 -17
- package/dist/doctor/variant-read.js +584 -0
- package/dist/progress.js +71 -0
- package/dist/skill-import.js +94 -1
- package/package.json +1 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IS THIS A COMPONENT OF A DESIGN SYSTEM?
|
|
3
|
+
*
|
|
4
|
+
* Nothing asked. The definition reader matched `export const X` / `export function X`
|
|
5
|
+
* for any capitalised name, and pointed at a whole monorepo it found **704
|
|
6
|
+
* components in a repo that has 37** (dono, 01/08). The other 667 were:
|
|
7
|
+
*
|
|
8
|
+
* ds-activation-page a route
|
|
9
|
+
* ds-auth-layout a Next layout
|
|
10
|
+
* ds-cache-providers a provider
|
|
11
|
+
* ds-approvals-monitoring-template
|
|
12
|
+
*
|
|
13
|
+
* A page is not a component of a design system, and it has no visual identity to
|
|
14
|
+
* draw - which is why the vitrine came back unreadable. The renderer was not the
|
|
15
|
+
* problem; 95% of what entered had nothing to render.
|
|
16
|
+
*
|
|
17
|
+
* WHAT THIS IS NOT. It is not a quality bar and it never asks whether something is
|
|
18
|
+
* good. Every test here is about CATEGORY: a route, a provider, a config object.
|
|
19
|
+
* And nothing disappears - every rejection carries a reason, and the import prints
|
|
20
|
+
* them grouped, because "649 seen, not imported, and here is why" is information and
|
|
21
|
+
* a silent drop is a lie.
|
|
22
|
+
*
|
|
23
|
+
* FEWER AND BETTER. 37 components that all draw is a transformative result; 704
|
|
24
|
+
* where 30 draw is a broken product. This is the pipeline choosing legibility over
|
|
25
|
+
* coverage.
|
|
26
|
+
*/
|
|
27
|
+
import { localOnlyNames } from "./components-scan.js";
|
|
28
|
+
/**
|
|
29
|
+
* Next's own reserved file names. Exact, not heuristic: the framework defines these,
|
|
30
|
+
* so a file called `page.tsx` IS a route in every Next codebase there is.
|
|
31
|
+
*/
|
|
32
|
+
const RESERVED_FILE = /(^|[/\\])(page|layout|template|default|error|global-error|not-found|loading|route|middleware|instrumentation|sitemap|robots|opengraph-image|icon|apple-icon|manifest)\.[jt]sx?$/;
|
|
33
|
+
/** Nuxt, SvelteKit and Remix equivalents, for the same exact reason. */
|
|
34
|
+
const RESERVED_FILE_OTHER = /(^|[/\\])(\+page|\+layout|\+server|_app|_document|_layout)\.[a-z]+$/;
|
|
35
|
+
/**
|
|
36
|
+
* A name that means WHOLE SCREEN in every codebase.
|
|
37
|
+
*
|
|
38
|
+
* Suffix-decisive, and deliberately short. `Page`, `Screen`, `Route` and `Template`
|
|
39
|
+
* name a destination; `Layout` names the frame around one. A component library does
|
|
40
|
+
* not export those, and the 704-component run was mostly these four words.
|
|
41
|
+
*
|
|
42
|
+
* `View` is NOT here on purpose: `TreeView` and `ListView` are components in a
|
|
43
|
+
* hundred libraries. It only counts when the file itself is a route.
|
|
44
|
+
*/
|
|
45
|
+
const SCREEN_SUFFIX = /(Page|Screen|Route|Template|Layout)$/;
|
|
46
|
+
/** Something that carries context down the tree rather than drawing. */
|
|
47
|
+
const PROVIDER_SUFFIX = /(Provider|Providers|Context|Boundary|Guard|Wrapper)$/;
|
|
48
|
+
/** Any JSX at all in the file. A capitalised export with none is a constant, a
|
|
49
|
+
* config object or a helper - `export const ROUTES = {…}` is not a component. */
|
|
50
|
+
const HAS_JSX = /<[A-Za-z][^>]*>|<>/;
|
|
51
|
+
/**
|
|
52
|
+
* Any sign that this file decides how something LOOKS.
|
|
53
|
+
*
|
|
54
|
+
* The generic catch, and the one that gets the providers whose names give nothing
|
|
55
|
+
* away. A file with no class, no style, no styled-anything and no variant helper is
|
|
56
|
+
* plumbing: it may render, but every pixel it produces was decided somewhere else.
|
|
57
|
+
*/
|
|
58
|
+
const HAS_STYLING = /className|class=|style=|styled[.(]|css`|cva\(|\btv\(|makeStyles|sx=|tw`/;
|
|
59
|
+
/** `createContext` + a `.Provider` in the return is a provider whatever it is
|
|
60
|
+
* called - `RootWrapper` and `CommonProviders` both read this way. */
|
|
61
|
+
const IS_CONTEXT = /createContext\s*[<(]/;
|
|
62
|
+
/** A lowercase JSX tag: an html element this file renders ITSELF. */
|
|
63
|
+
const OWN_ELEMENT = /<(?:[a-z][a-z0-9]*)(\s|>|\/)/;
|
|
64
|
+
/** Every capitalised JSX tag the file opens, dots included. */
|
|
65
|
+
const RENDERED_TAG = /<([A-Z][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)*)/g;
|
|
66
|
+
/** `import … from "some-package"` - not relative, not an alias of theirs. */
|
|
67
|
+
const THIRD_PARTY_IMPORT = /import\s+(?:type\s+)?([\s\S]*?)\s+from\s+["'](?![./]|@\/)([^"']+)["']/g;
|
|
68
|
+
/**
|
|
69
|
+
* A file whose every pixel belongs to somebody else's component.
|
|
70
|
+
*
|
|
71
|
+
* `apps/web-dashboard/src/components/ui/_legacy/_chakra-ui/accordion.tsx` forwards
|
|
72
|
+
* to `<Accordion.ItemTrigger>` and renders no element of its own; 205 files like it
|
|
73
|
+
* arrived as components of the system (dono, 01/08). The first version rejected them
|
|
74
|
+
* for "deciding nothing about how anything looks", which reached the right outcome
|
|
75
|
+
* with a false reason - `gap="4"` and `rotate={…}` ARE styling decisions, just not
|
|
76
|
+
* through a class.
|
|
77
|
+
*
|
|
78
|
+
* The truthful category is the one this pipeline already has a word for: EXTERNAL.
|
|
79
|
+
* A component that renders only a third party's components, with no element of its
|
|
80
|
+
* own, is an ADAPTER for that library. Its markup and its pixels are that library's,
|
|
81
|
+
* and a design system that claimed them would be claiming what it cannot re-theme.
|
|
82
|
+
*
|
|
83
|
+
* The test needs no list of style props, which is why it holds across Chakra, Panda,
|
|
84
|
+
* MUI and whatever comes next: no html element of its own AND it imports components
|
|
85
|
+
* from a package.
|
|
86
|
+
*/
|
|
87
|
+
function adapterFor(source, internal) {
|
|
88
|
+
// It renders an html element of its own: whatever else it does, some pixel here is
|
|
89
|
+
// this file's decision.
|
|
90
|
+
if (OWN_ELEMENT.test(source))
|
|
91
|
+
return null;
|
|
92
|
+
/**
|
|
93
|
+
* THE POLYMORPHIC ESCAPE, and it is not a detail.
|
|
94
|
+
*
|
|
95
|
+
* `function Text({ as: Component = "p" })` renders `<Component>` and no lowercase
|
|
96
|
+
* tag, so the first version of this test rejected the real `Text` and `Pill` of a
|
|
97
|
+
* real library as adapters (spec, 01/08). `Component` is a prop rename - a name the
|
|
98
|
+
* file BINDS - and a tag a file binds itself is not somebody else's component.
|
|
99
|
+
*
|
|
100
|
+
* The reachability reader already answers exactly this question, so it answers it
|
|
101
|
+
* here rather than being approximated a second time.
|
|
102
|
+
*/
|
|
103
|
+
const own = localOnlyNames(source);
|
|
104
|
+
const fromPackage = new Map();
|
|
105
|
+
THIRD_PARTY_IMPORT.lastIndex = 0;
|
|
106
|
+
for (const m of source.matchAll(THIRD_PARTY_IMPORT)) {
|
|
107
|
+
const spec = m[2];
|
|
108
|
+
if (internal.some((pre) => spec === pre || spec.startsWith(`${pre}/`))) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
for (const raw of m[1].replace(/[{}]/g, ",").split(",")) {
|
|
112
|
+
const local = (raw
|
|
113
|
+
.trim()
|
|
114
|
+
.split(/\s+as\s+/)
|
|
115
|
+
.pop() ?? "").trim();
|
|
116
|
+
if (/^[A-Z]/.test(local))
|
|
117
|
+
fromPackage.set(local, spec);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (fromPackage.size === 0)
|
|
121
|
+
return null;
|
|
122
|
+
RENDERED_TAG.lastIndex = 0;
|
|
123
|
+
let theirs = null;
|
|
124
|
+
let seen = 0;
|
|
125
|
+
for (const m of source.matchAll(RENDERED_TAG)) {
|
|
126
|
+
const root = m[1].split(".")[0];
|
|
127
|
+
if (own.has(root))
|
|
128
|
+
return null;
|
|
129
|
+
const spec = fromPackage.get(root);
|
|
130
|
+
// A tag from neither - their own component, imported relatively - means this file
|
|
131
|
+
// composes the system rather than adapting a library.
|
|
132
|
+
if (!spec)
|
|
133
|
+
return null;
|
|
134
|
+
theirs ??= spec;
|
|
135
|
+
seen += 1;
|
|
136
|
+
}
|
|
137
|
+
return seen > 0 ? theirs : null;
|
|
138
|
+
}
|
|
139
|
+
export function gateComponent(input) {
|
|
140
|
+
const { name, file, source } = input;
|
|
141
|
+
if (RESERVED_FILE.test(file) || RESERVED_FILE_OTHER.test(file)) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
why: "route",
|
|
145
|
+
because: `\`${file}\` is a file name the framework reserves - it is a route, not a component somebody can import`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (SCREEN_SUFFIX.test(name)) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
why: "screen",
|
|
152
|
+
because: `\`${name}\` names a whole screen or the frame around one. A design system is made of the pieces a screen is built FROM`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (PROVIDER_SUFFIX.test(name) || IS_CONTEXT.test(source)) {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
why: "provider",
|
|
159
|
+
because: `\`${name}\` carries context or wraps a tree rather than drawing anything of its own`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const adapter = adapterFor(source, input.internal ?? []);
|
|
163
|
+
if (adapter) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
why: "adapter",
|
|
167
|
+
because: `\`${name}\` renders \`${adapter}\`'s components and no element of its own - its markup and every pixel are that library's, so there is nothing here for this system to re-theme`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (!HAS_JSX.test(source)) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
why: "config",
|
|
174
|
+
because: `\`${name}\` is a capitalised export in a file with no markup - a constant, a config object or a helper`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (!HAS_STYLING.test(source)) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
why: "no-styling",
|
|
181
|
+
because: `\`${name}\` renders but decides nothing about how anything looks - every pixel it produces was decided somewhere else`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return { ok: true };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The rejections, grouped and counted, in the order that reads best out loud.
|
|
188
|
+
*
|
|
189
|
+
* NO SILENT CAPS. A run that quietly imports 40 of 704 reads as "your project has 40
|
|
190
|
+
* components", which is the one thing a census must never imply. Every name is
|
|
191
|
+
* carried; the printer shows the first few per group and says how many more.
|
|
192
|
+
*/
|
|
193
|
+
export const SKIP_LABEL = {
|
|
194
|
+
route: "routes and framework files",
|
|
195
|
+
screen: "whole screens, and the frames around them",
|
|
196
|
+
provider: "providers, contexts and wrappers",
|
|
197
|
+
adapter: "adapters for another library's components",
|
|
198
|
+
config: "constants, config and helpers",
|
|
199
|
+
"no-jsx": "capitalised exports with no markup",
|
|
200
|
+
"no-styling": "components that decide nothing about how anything looks",
|
|
201
|
+
};
|
|
202
|
+
export function groupSkips(skips) {
|
|
203
|
+
const order = [
|
|
204
|
+
"screen",
|
|
205
|
+
"route",
|
|
206
|
+
"provider",
|
|
207
|
+
"adapter",
|
|
208
|
+
"no-styling",
|
|
209
|
+
"config",
|
|
210
|
+
"no-jsx",
|
|
211
|
+
];
|
|
212
|
+
const by = new Map();
|
|
213
|
+
for (const s of skips) {
|
|
214
|
+
by.set(s.why, [...(by.get(s.why) ?? []), s.name]);
|
|
215
|
+
}
|
|
216
|
+
return order
|
|
217
|
+
.filter((why) => (by.get(why) ?? []).length > 0)
|
|
218
|
+
.map((why) => ({
|
|
219
|
+
why,
|
|
220
|
+
label: SKIP_LABEL[why],
|
|
221
|
+
names: (by.get(why) ?? []).sort(),
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* One line saying what the gate did, in the terms the decision was actually made on.
|
|
226
|
+
*
|
|
227
|
+
* The sentence a person needs in order to trust a number that just dropped by an
|
|
228
|
+
* order of magnitude - and to notice if it dropped too far.
|
|
229
|
+
*/
|
|
230
|
+
export function describeGate(kept, skipped) {
|
|
231
|
+
if (skipped === 0) {
|
|
232
|
+
return `${kept} component${kept === 1 ? "" : "s"} - every capitalised export here is one.`;
|
|
233
|
+
}
|
|
234
|
+
return `${kept} component${kept === 1 ? "" : "s"}, and ${skipped} export${skipped === 1 ? "" : "s"} left out. A page is not a component of a design system: it has no visual identity to draw, and importing one puts a name in your vitrine that nobody can compose with. Everything left out is listed below with the reason, so you can disagree with any of it.`;
|
|
235
|
+
}
|
|
@@ -91,6 +91,20 @@ const SPACING_PROPERTY = {
|
|
|
91
91
|
mx: "marginInline",
|
|
92
92
|
my: "marginBlock",
|
|
93
93
|
gap: "gap",
|
|
94
|
+
/**
|
|
95
|
+
* SIZE, which the first version had no entry for at all.
|
|
96
|
+
*
|
|
97
|
+
* A real `cva` puts the whole size axis in `h-8 px-3`, `h-12 px-6`, `h-16 px-10` -
|
|
98
|
+
* six options, and only the padding came through, so every size read as the same
|
|
99
|
+
* height (probe, 01/08). A control's height IS its size.
|
|
100
|
+
*/
|
|
101
|
+
h: "height",
|
|
102
|
+
w: "width",
|
|
103
|
+
size: "size",
|
|
104
|
+
"min-h": "minHeight",
|
|
105
|
+
"min-w": "minWidth",
|
|
106
|
+
"max-h": "maxHeight",
|
|
107
|
+
"max-w": "maxWidth",
|
|
94
108
|
};
|
|
95
109
|
/** Tailwind's radius scale. */
|
|
96
110
|
const RADIUS = {
|
|
@@ -221,6 +235,35 @@ export function readUtility(utility, declared) {
|
|
|
221
235
|
const keyword = TYPE_KEYWORD[core];
|
|
222
236
|
if (keyword)
|
|
223
237
|
return keyword;
|
|
238
|
+
/**
|
|
239
|
+
* AN ARBITRARY VALUE POINTING AT ONE OF THEIR OWN TOKENS.
|
|
240
|
+
*
|
|
241
|
+
* `rounded-[var(--radius-md)]`, `gap-[var(--spacing-sm)]`,
|
|
242
|
+
* `[font-size:var(--text-body-s)]`. This library writes half its scale that way, and
|
|
243
|
+
* none of it was read - the base of a real Button came back with two declarations out
|
|
244
|
+
* of fifteen (probe, 01/08).
|
|
245
|
+
*
|
|
246
|
+
* It is the STRONGEST kind of value in the file, not the weakest: an arbitrary value
|
|
247
|
+
* naming a custom property is somebody pointing at their own declared token by hand.
|
|
248
|
+
* The bracket is a Tailwind escape hatch; the `var()` inside it is a token ref.
|
|
249
|
+
*/
|
|
250
|
+
const arbitraryProp = /^\[([a-z-]+):(.+)\]$/.exec(core);
|
|
251
|
+
if (arbitraryProp) {
|
|
252
|
+
const property = arbitraryProp[1].replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
253
|
+
const value = resolveArbitrary(arbitraryProp[2], declared);
|
|
254
|
+
if (value)
|
|
255
|
+
return { property, value: value.value, token: value.token };
|
|
256
|
+
}
|
|
257
|
+
const bracket = core.indexOf("-[");
|
|
258
|
+
if (bracket !== -1 && core.endsWith("]")) {
|
|
259
|
+
const head = core.slice(0, bracket);
|
|
260
|
+
const inner = core.slice(bracket + 2, -1);
|
|
261
|
+
const property = COLOR_PROPERTY[head] ?? SPACING_PROPERTY[head] ?? BRACKET_PROPERTY[head];
|
|
262
|
+
const value = resolveArbitrary(inner, declared);
|
|
263
|
+
if (property && value) {
|
|
264
|
+
return { property, value: value.value, token: value.token };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
224
267
|
const dash = core.indexOf("-");
|
|
225
268
|
if (dash === -1)
|
|
226
269
|
return null;
|
|
@@ -299,6 +342,14 @@ export function readUtility(utility, declared) {
|
|
|
299
342
|
}
|
|
300
343
|
return null;
|
|
301
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* `opacity-50`, which a real `cva` uses for its whole disabled look:
|
|
347
|
+
* `disabled:opacity-50 disabled:grayscale-[0.5]`. Tailwind's own scale is the
|
|
348
|
+
* percentage, so the value is the number over one hundred.
|
|
349
|
+
*/
|
|
350
|
+
if (prefix === "opacity" && /^\d{1,3}$/.test(rest)) {
|
|
351
|
+
return { property: "opacity", value: String(Number(rest) / 100) };
|
|
352
|
+
}
|
|
302
353
|
const spaceProp = SPACING_PROPERTY[prefix];
|
|
303
354
|
if (spaceProp && SPACING[rest]) {
|
|
304
355
|
return { property: spaceProp, value: SPACING[rest] };
|
|
@@ -324,6 +375,71 @@ export function readUtility(utility, declared) {
|
|
|
324
375
|
* before it is the family, hyphens intact, because `royal-blue-500` is one
|
|
325
376
|
* family called `royal-blue`.
|
|
326
377
|
*/
|
|
378
|
+
/**
|
|
379
|
+
* Utility heads whose arbitrary value is not a colour or a length in the tables above.
|
|
380
|
+
* `rounded-[…]` is a radius, `shadow-[…]` an elevation, `text-[…]` ambiguous by design
|
|
381
|
+
* (a colour or a size) and resolved by what the value looks like.
|
|
382
|
+
*/
|
|
383
|
+
const BRACKET_PROPERTY = {
|
|
384
|
+
rounded: "borderRadius",
|
|
385
|
+
shadow: "boxShadow",
|
|
386
|
+
border: "borderWidth",
|
|
387
|
+
leading: "lineHeight",
|
|
388
|
+
tracking: "letterSpacing",
|
|
389
|
+
font: "fontFamily",
|
|
390
|
+
opacity: "opacity",
|
|
391
|
+
z: "zIndex",
|
|
392
|
+
grid: "gridTemplateColumns",
|
|
393
|
+
aspect: "aspectRatio",
|
|
394
|
+
};
|
|
395
|
+
/**
|
|
396
|
+
* What is inside the brackets, resolved.
|
|
397
|
+
*
|
|
398
|
+
* `var(--radius-md)` where they declared `--radius-md` is a REF - the value travels as
|
|
399
|
+
* the token they named it. A `var()` they never declared, or a plain literal, travels
|
|
400
|
+
* as the literal they typed, which keeps v1 a mirror and leaves normalising to a v2
|
|
401
|
+
* they approve.
|
|
402
|
+
*/
|
|
403
|
+
function resolveArbitrary(inner, declared) {
|
|
404
|
+
const raw = inner.replace(/_/g, " ").trim();
|
|
405
|
+
if (!raw)
|
|
406
|
+
return null;
|
|
407
|
+
const v = /^var\(\s*(--[a-zA-Z0-9_-]+)\s*(?:,[^)]*)?\)$/.exec(raw);
|
|
408
|
+
if (!v) {
|
|
409
|
+
// A literal in brackets is still a real value somebody typed.
|
|
410
|
+
return /[<>{}@;]/.test(raw) ? null : { value: raw };
|
|
411
|
+
}
|
|
412
|
+
const name = v[1];
|
|
413
|
+
if (!declared.has(name))
|
|
414
|
+
return { value: raw };
|
|
415
|
+
return { value: tokenRefFor(name), token: name };
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* A declared custom property, as a token ref in the document's own namespace.
|
|
419
|
+
*
|
|
420
|
+
* `--color-ocean-500` → `{color.ocean.500}`, `--radius-md` → `{radius.md}`,
|
|
421
|
+
* `--text-body-s` → `{typography.scale.body-s.fontSize}`. Their name is the path,
|
|
422
|
+
* which is the whole "your names travel unchanged" promise applied one level deeper.
|
|
423
|
+
*/
|
|
424
|
+
function tokenRefFor(name) {
|
|
425
|
+
const bare = name.replace(/^--/, "");
|
|
426
|
+
if (bare.startsWith("color-"))
|
|
427
|
+
return refFor(bare.slice("color-".length));
|
|
428
|
+
if (bare.startsWith("radius-"))
|
|
429
|
+
return `{radius.${bare.slice(7)}}`;
|
|
430
|
+
if (bare.startsWith("spacing-"))
|
|
431
|
+
return `{spacing.${bare.slice(8)}}`;
|
|
432
|
+
if (bare.startsWith("shadow-"))
|
|
433
|
+
return `{shadow.${bare.slice(7)}}`;
|
|
434
|
+
if (bare.startsWith("text-")) {
|
|
435
|
+
return `{typography.scale.${bare.slice(5)}.fontSize}`;
|
|
436
|
+
}
|
|
437
|
+
if (bare.startsWith("font-"))
|
|
438
|
+
return `{typography.families.${bare.slice(5)}}`;
|
|
439
|
+
// A namespace we do not model. The literal `var()` is still correct CSS against
|
|
440
|
+
// their own stylesheet, and inventing a ref would point at nothing.
|
|
441
|
+
return `var(${name})`;
|
|
442
|
+
}
|
|
327
443
|
function refFor(name) {
|
|
328
444
|
const m = /^(.*)-(\d{2,4})$/.exec(name);
|
|
329
445
|
if (!m)
|
|
@@ -482,20 +598,3 @@ export function rootClasses(source) {
|
|
|
482
598
|
.flatMap((m) => m[1].split(/\s+/))
|
|
483
599
|
.filter((c) => c.length > 0 && !c.includes("${"));
|
|
484
600
|
}
|
|
485
|
-
/**
|
|
486
|
-
* Transcribe the parts a skill named, using the same reader the root used.
|
|
487
|
-
*
|
|
488
|
-
* `declared` comes from the census itself on the `--census` path, so a run that
|
|
489
|
-
* only sends a file it was handed still resolves their tokens by name.
|
|
490
|
-
*/
|
|
491
|
-
export function transcribeParts(parts, declared) {
|
|
492
|
-
const out = {};
|
|
493
|
-
for (const part of parts) {
|
|
494
|
-
const name = part.name?.trim();
|
|
495
|
-
if (!name || typeof part.classes !== "string")
|
|
496
|
-
continue;
|
|
497
|
-
const t = transcribe(part.classes.split(/\s+/).filter(Boolean), declared);
|
|
498
|
-
out[name] = { base: t.base, dark: t.dark, states: t.states };
|
|
499
|
-
}
|
|
500
|
-
return out;
|
|
501
|
-
}
|