@r0hitsharma/uikit-cli 0.12.0-rohit-fork-ci.1
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/README.md +286 -0
- package/dist/cli.js +225 -0
- package/dist/cli.sh +10 -0
- package/dist/command-executor.js +41 -0
- package/dist/commands/doctor.js +518 -0
- package/dist/commands/format.js +34 -0
- package/dist/commands/link.js +252 -0
- package/dist/commands/lint.js +43 -0
- package/dist/commands/register.js +35 -0
- package/dist/commands/unlink.js +68 -0
- package/dist/fs-utils.js +56 -0
- package/dist/link-validator.js +80 -0
- package/dist/logger.js +43 -0
- package/dist/package-discovery.js +218 -0
- package/dist/shell-utils.js +9 -0
- package/dist/tool-binaries.js +16 -0
- package/dist/types.js +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Design-system recipe class-name stems. A design-system recipe emits classes
|
|
5
|
+
* of the form `${className}--variant_x` (recipe) or `${className}__slot` (slot
|
|
6
|
+
* recipe) — and *only* when the `designSystemStaticCssRecipes` spread is wired
|
|
7
|
+
* into the consumer's Panda `staticCss`, because components apply these as
|
|
8
|
+
* runtime strings that Panda's extractor cannot see. So the presence of any one
|
|
9
|
+
* of these stems is the signal that `staticCss` is wired.
|
|
10
|
+
*
|
|
11
|
+
* The gate is deliberately "**at least one** present", not "all": a consumer
|
|
12
|
+
* only ships CSS for the recipes it actually uses, and a legitimately narrowed
|
|
13
|
+
* `staticCss` map is valid (and encouraged, for bundle size). We flag only the
|
|
14
|
+
* total-omission case — the headline failure where nothing runtime-selected
|
|
15
|
+
* emits at all. This list therefore does not need to be exhaustive or perfectly
|
|
16
|
+
* in sync with the library; it just needs a few stems any real consumer emits.
|
|
17
|
+
*/
|
|
18
|
+
export const DESIGN_SYSTEM_RECIPE_CLASSNAMES = [
|
|
19
|
+
'badge',
|
|
20
|
+
'button',
|
|
21
|
+
'drawer',
|
|
22
|
+
'dataTable',
|
|
23
|
+
'panel',
|
|
24
|
+
'statTile',
|
|
25
|
+
'toggleSwitch',
|
|
26
|
+
'segmentedControl',
|
|
27
|
+
'surfaceMessage',
|
|
28
|
+
'sidebarLayout',
|
|
29
|
+
];
|
|
30
|
+
/** True if any design-system recipe class is present (i.e. staticCss is wired). */
|
|
31
|
+
function hasAnyRecipeClass(css) {
|
|
32
|
+
return DESIGN_SYSTEM_RECIPE_CLASSNAMES.some((name) => css.includes(`.${name}--`) || css.includes(`.${name}__`));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Color-ish CSS properties whose value, if written as a bare `token.path`,
|
|
36
|
+
* means an unresolved semantic token: Panda emits the path verbatim (e.g.
|
|
37
|
+
* `color: text.subtle;`) which the browser silently drops. `var(...)`, hex,
|
|
38
|
+
* numeric and keyword values never contain a dotted lowercase identifier, so a
|
|
39
|
+
* dotted value on one of these properties is the tell.
|
|
40
|
+
*/
|
|
41
|
+
const UNRESOLVED_TOKEN_DECL = /(color|background|background-color|border-color|fill|stroke|outline-color)\s*:\s*([a-z][\w-]*(?:\.[a-z0-9][\w-]*)+)\s*[;}]/gi;
|
|
42
|
+
/**
|
|
43
|
+
* ── Roleless `colorPalette` detection ──
|
|
44
|
+
*
|
|
45
|
+
* Panda compiles `colorPalette: 'violet'` into a ruleset that *remaps* the
|
|
46
|
+
* generic palette custom properties onto that hue's tokens:
|
|
47
|
+
*
|
|
48
|
+
* .alert--colorPalette_violet {
|
|
49
|
+
* --colors-color-palette-50: var(--colors-violet-50); … 100…950
|
|
50
|
+
* }
|
|
51
|
+
*
|
|
52
|
+
* It emits one line per token that actually exists under the hue. So a hue with
|
|
53
|
+
* only the 50–950 scale (any Panda default hue the preset never gave role
|
|
54
|
+
* sub-tokens) maps the scale and nothing else, while a role-complete palette
|
|
55
|
+
* (`neutral`/`gray`/`green`/`red`/`amber`/`blue` in this design system) also maps
|
|
56
|
+
* `--colors-color-palette-solid-bg`, `-subtle-fg`, `-outline-border`, and the rest.
|
|
57
|
+
*
|
|
58
|
+
* Meanwhile a recipe that styles with a role emits the *reference* side:
|
|
59
|
+
*
|
|
60
|
+
* .alert--emphasis_solid { background: var(--colors-color-palette-solid-bg); }
|
|
61
|
+
*
|
|
62
|
+
* Pair a roleless palette with a role reference and the custom property is
|
|
63
|
+
* undefined in that scope: well-formed CSS, valid `var()` syntax, and the browser
|
|
64
|
+
* silently drops the declaration. Nothing else in doctor catches it — the token
|
|
65
|
+
* path resolved, and `staticCss` is wired.
|
|
66
|
+
*
|
|
67
|
+
* The stylesheet is therefore its own palette→roles map: the assignment rulesets
|
|
68
|
+
* ARE the preset's role tokens in generated form, which is why this check reads
|
|
69
|
+
* them out of the CSS rather than importing a table from the design system. A
|
|
70
|
+
* palette added to the preset, or one a consumer defines in their own preset
|
|
71
|
+
* extension, is covered the moment it appears in the CSS — with no list here to
|
|
72
|
+
* keep in sync, and no dependency from this CLI on the design-system package.
|
|
73
|
+
*
|
|
74
|
+
* DETECTION BOUNDARY. Resolving which palette is in scope for a declaration is
|
|
75
|
+
* the CSS cascade, and doctor does not simulate it. It resolves exactly one
|
|
76
|
+
* scope — a **recipe**, keyed by the class-name stem Panda derives from
|
|
77
|
+
* `className` — and reports a role reference only when a palette assigned
|
|
78
|
+
* anywhere in that same recipe's classes fails to define the role. Slots count
|
|
79
|
+
* as the same scope (`.chip__root--colorPalette_x` sets the properties; the
|
|
80
|
+
* `.chip__dismiss` slot inherits them), and a reference is attributed to the
|
|
81
|
+
* *rightmost* compound selector, the element the rule actually styles. Out of
|
|
82
|
+
* scope, deliberately, and reported as clean:
|
|
83
|
+
*
|
|
84
|
+
* - Atomic utilities (`.color-palette_violet` + `.bg_colorPalette\.solid\.bg`).
|
|
85
|
+
* Two unrelated classes; whether they land on the same element, and whether an
|
|
86
|
+
* ancestor already supplied the role, is not knowable from the stylesheet.
|
|
87
|
+
* - Cross-recipe cascade: a palette set on an outer recipe, a role consumed by an
|
|
88
|
+
* inner one.
|
|
89
|
+
* - `var(--colors-color-palette-…, fallback)`. A fallback means the declaration
|
|
90
|
+
* survives, so it is not a silent drop — the regex below requires a bare
|
|
91
|
+
* reference and skips these by construction.
|
|
92
|
+
*
|
|
93
|
+
* Those are false negatives by design. The check must never fail a healthy
|
|
94
|
+
* stylesheet, so every case where scope is ambiguous resolves to "no issue".
|
|
95
|
+
*/
|
|
96
|
+
/**
|
|
97
|
+
* Leaf rulesets — `selector { declarations }` with no nested braces. Declarations
|
|
98
|
+
* only ever live in leaves, so this is enough to read a stylesheet without a real
|
|
99
|
+
* CSS parser, and it sees through `@layer`/`@media` wrappers for free.
|
|
100
|
+
*/
|
|
101
|
+
const LEAF_RULESET = /([^{}]+)\{([^{}]*)\}/g;
|
|
102
|
+
/**
|
|
103
|
+
* A design-system recipe class: `.stem--variant_value`, `.stem__slot`, or
|
|
104
|
+
* `.stem__slot--variant_value`. Requires the `__`/`--` shape so Panda's atomic
|
|
105
|
+
* utilities (`.bg_surface`, `.c_text\.muted`) can never be mistaken for a recipe
|
|
106
|
+
* scope named after their property.
|
|
107
|
+
*/
|
|
108
|
+
const RECIPE_CLASS = /\.([A-Za-z][A-Za-z0-9]*)(?:__[A-Za-z0-9-]+(?:--[A-Za-z0-9_-]+)?|--[A-Za-z0-9_-]+)/g;
|
|
109
|
+
/** A class with no variant/slot suffix — a recipe's base rule, e.g. `.button`. */
|
|
110
|
+
const BARE_CLASS = /\.([A-Za-z][A-Za-z0-9]*)(?![\w-])/g;
|
|
111
|
+
/** `--colors-color-palette-<role>: var(--colors-<palette>-<role>)`. */
|
|
112
|
+
const PALETTE_ROLE_ASSIGNMENT = /--colors-color-palette-([a-z0-9-]+)\s*:\s*var\(\s*--colors-([a-z0-9-]+)\s*\)/gi;
|
|
113
|
+
/** A bare `var(--colors-color-palette-<role>)` — no fallback (see boundary above). */
|
|
114
|
+
const PALETTE_ROLE_REFERENCE = /var\(\s*--colors-color-palette-([a-z0-9-]+)\s*\)/gi;
|
|
115
|
+
function splitLeafRulesets(css) {
|
|
116
|
+
const leaves = [];
|
|
117
|
+
for (const match of css.matchAll(LEAF_RULESET)) {
|
|
118
|
+
// `match[1]` runs from just after the previous `}`, so it can carry leading
|
|
119
|
+
// blank lines. Anchor on the `{` instead — a body offset added to the match
|
|
120
|
+
// start would report a line too early by however many newlines precede the
|
|
121
|
+
// selector.
|
|
122
|
+
const [whole, selector, body] = match;
|
|
123
|
+
// Both groups are non-optional in LEAF_RULESET, so a match always carries
|
|
124
|
+
// them; the guard is what tells the compiler that.
|
|
125
|
+
if (selector === undefined || body === undefined)
|
|
126
|
+
continue;
|
|
127
|
+
leaves.push({
|
|
128
|
+
selector: selector.trim(),
|
|
129
|
+
body,
|
|
130
|
+
bodyIndex: (match.index ?? 0) + whole.indexOf('{') + 1,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return leaves;
|
|
134
|
+
}
|
|
135
|
+
/** Every recipe stem the stylesheet mentions with a slot or variant suffix. */
|
|
136
|
+
function collectRecipeStems(leaves) {
|
|
137
|
+
const stems = new Set();
|
|
138
|
+
for (const leaf of leaves) {
|
|
139
|
+
for (const match of leaf.selector.matchAll(RECIPE_CLASS)) {
|
|
140
|
+
const stem = match[1];
|
|
141
|
+
if (stem !== undefined)
|
|
142
|
+
stems.add(stem);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return stems;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Split a selector list on top-level commas only. A comma inside a functional
|
|
149
|
+
* pseudo-class (`:is(.a--x, .b--y)`, `:not(…)`, `:where(…)`) separates that
|
|
150
|
+
* pseudo-class's arguments, not selectors — splitting there yields fragments
|
|
151
|
+
* that are not selectors at all (`.badge--variant_solid:is(.a--x`), whose
|
|
152
|
+
* rightmost recipe class is an argument rather than the styled element, so the
|
|
153
|
+
* reference is attributed to a scope that does not exist. A backslash escapes
|
|
154
|
+
* the next character, since Panda escapes parens inside class names
|
|
155
|
+
* (`.w_calc\(100\%\)`).
|
|
156
|
+
*/
|
|
157
|
+
function splitSelectorList(selectorList) {
|
|
158
|
+
const selectors = [];
|
|
159
|
+
let depth = 0;
|
|
160
|
+
let start = 0;
|
|
161
|
+
for (let i = 0; i < selectorList.length; i++) {
|
|
162
|
+
const char = selectorList[i];
|
|
163
|
+
if (char === '\\')
|
|
164
|
+
i++;
|
|
165
|
+
else if (char === '(')
|
|
166
|
+
depth++;
|
|
167
|
+
else if (char === ')')
|
|
168
|
+
depth = Math.max(0, depth - 1);
|
|
169
|
+
else if (char === ',' && depth === 0) {
|
|
170
|
+
selectors.push(selectorList.slice(start, i));
|
|
171
|
+
start = i + 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
selectors.push(selectorList.slice(start));
|
|
175
|
+
return selectors;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Blank out functional-pseudo argument lists before the subject is read. Their
|
|
179
|
+
* contents narrow *when* the rule matches; the class naming the element it
|
|
180
|
+
* styles is always written outside the parens, so an argument must never win the
|
|
181
|
+
* scope. They also carry commas and whitespace that would otherwise read as
|
|
182
|
+
* selector-list and descendant separators.
|
|
183
|
+
*/
|
|
184
|
+
function stripPseudoArguments(selector) {
|
|
185
|
+
let stripped = selector;
|
|
186
|
+
let previous;
|
|
187
|
+
do {
|
|
188
|
+
previous = stripped;
|
|
189
|
+
stripped = stripped.replace(/(?<!\\)\([^()]*\)/g, '');
|
|
190
|
+
} while (stripped !== previous);
|
|
191
|
+
return stripped;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The recipe stem of the element a selector actually styles: its rightmost
|
|
195
|
+
* compound. `.dark .toggleSwitch__thumb` scopes to `toggleSwitch`, not to
|
|
196
|
+
* whatever the ancestors are — attributing a reference to an ancestor's recipe
|
|
197
|
+
* would invent a scope the declaration never had.
|
|
198
|
+
*/
|
|
199
|
+
function subjectRecipeStem(selector, stems) {
|
|
200
|
+
const compounds = stripPseudoArguments(selector)
|
|
201
|
+
.split(/[\s>+~]+/)
|
|
202
|
+
.filter(Boolean);
|
|
203
|
+
for (let i = compounds.length - 1; i >= 0; i--) {
|
|
204
|
+
const compound = compounds[i];
|
|
205
|
+
if (compound === undefined)
|
|
206
|
+
continue;
|
|
207
|
+
const recipeClasses = [...compound.matchAll(RECIPE_CLASS)];
|
|
208
|
+
const last = recipeClasses.at(-1)?.[1];
|
|
209
|
+
if (last !== undefined)
|
|
210
|
+
return last;
|
|
211
|
+
// A recipe's base rule (`.button { … }`) carries no suffix, so accept a bare
|
|
212
|
+
// class only when the stylesheet proves elsewhere that it is a recipe.
|
|
213
|
+
for (const match of [...compound.matchAll(BARE_CLASS)].reverse()) {
|
|
214
|
+
const bare = match[1];
|
|
215
|
+
if (bare !== undefined && stems.has(bare))
|
|
216
|
+
return bare;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The palette a ruleset assigns, plus the roles it maps. Panda writes the palette
|
|
223
|
+
* name into the *value* (`var(--colors-violet-solid-bg)`), which is read here
|
|
224
|
+
* rather than parsed out of the selector so an atomic `.color-palette_violet` and
|
|
225
|
+
* a recipe variant are handled by the same code.
|
|
226
|
+
*/
|
|
227
|
+
function readPaletteAssignment(body) {
|
|
228
|
+
let palette = null;
|
|
229
|
+
const roles = new Set();
|
|
230
|
+
for (const match of body.matchAll(PALETTE_ROLE_ASSIGNMENT)) {
|
|
231
|
+
const [, role, target] = match;
|
|
232
|
+
if (role === undefined || target === undefined)
|
|
233
|
+
continue;
|
|
234
|
+
if (!target.endsWith(`-${role}`))
|
|
235
|
+
continue;
|
|
236
|
+
const name = target.slice(0, -(role.length + 1));
|
|
237
|
+
palette ??= name;
|
|
238
|
+
if (name === palette)
|
|
239
|
+
roles.add(role);
|
|
240
|
+
}
|
|
241
|
+
return palette ? { palette, roles } : null;
|
|
242
|
+
}
|
|
243
|
+
function scopeFor(scopes, stem) {
|
|
244
|
+
let scope = scopes.get(stem);
|
|
245
|
+
if (!scope) {
|
|
246
|
+
scope = { palettes: new Map(), references: new Map() };
|
|
247
|
+
scopes.set(stem, scope);
|
|
248
|
+
}
|
|
249
|
+
return scope;
|
|
250
|
+
}
|
|
251
|
+
function collectPaletteScopes(leaves) {
|
|
252
|
+
const stems = collectRecipeStems(leaves);
|
|
253
|
+
const scopes = new Map();
|
|
254
|
+
for (const leaf of leaves) {
|
|
255
|
+
const assignment = readPaletteAssignment(leaf.body);
|
|
256
|
+
const references = [...leaf.body.matchAll(PALETTE_ROLE_REFERENCE)];
|
|
257
|
+
if (!assignment && references.length === 0)
|
|
258
|
+
continue;
|
|
259
|
+
for (const rawSelector of splitSelectorList(leaf.selector)) {
|
|
260
|
+
const selector = rawSelector.trim();
|
|
261
|
+
const stem = subjectRecipeStem(selector, stems);
|
|
262
|
+
if (!stem)
|
|
263
|
+
continue; // unknown scope — see DETECTION BOUNDARY
|
|
264
|
+
const scope = scopeFor(scopes, stem);
|
|
265
|
+
if (assignment) {
|
|
266
|
+
const known = scope.palettes.get(assignment.palette) ?? new Set();
|
|
267
|
+
for (const role of assignment.roles)
|
|
268
|
+
known.add(role);
|
|
269
|
+
scope.palettes.set(assignment.palette, known);
|
|
270
|
+
}
|
|
271
|
+
for (const match of references) {
|
|
272
|
+
const role = match[1];
|
|
273
|
+
if (role === undefined)
|
|
274
|
+
continue;
|
|
275
|
+
if (scope.references.has(role))
|
|
276
|
+
continue;
|
|
277
|
+
scope.references.set(role, {
|
|
278
|
+
selector,
|
|
279
|
+
index: leaf.bodyIndex + (match.index ?? 0),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return scopes;
|
|
285
|
+
}
|
|
286
|
+
function lineOf(css, index) {
|
|
287
|
+
return css.slice(0, index).split('\n').length;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Role references whose scope can assign a palette that does not define them.
|
|
291
|
+
*
|
|
292
|
+
* SEVERITY. A miss is an `error` only when it is definite — when *every* palette
|
|
293
|
+
* the scope can be set to lacks the role, so no combination of that scope's
|
|
294
|
+
* variants avoids the dropped declaration. The one-assignable-palette case is
|
|
295
|
+
* that condition at its smallest: the palette is the only one there is.
|
|
296
|
+
*
|
|
297
|
+
* When some assignable palette does define the role, the pairing is merely
|
|
298
|
+
* possible, and the stylesheet cannot say whether an app ever makes it: a recipe
|
|
299
|
+
* exposing `colorPalette: violet` alongside a `solid` emphasis variant may never
|
|
300
|
+
* combine the two. Reported as a `warn` — worth printing, since a real
|
|
301
|
+
* combination is silently dropped CSS, but not worth failing a build over an
|
|
302
|
+
* unproven one.
|
|
303
|
+
*/
|
|
304
|
+
function findRolelessPalettes(css, leaves) {
|
|
305
|
+
const issues = [];
|
|
306
|
+
for (const [scopeName, scope] of collectPaletteScopes(leaves)) {
|
|
307
|
+
for (const [role, site] of scope.references) {
|
|
308
|
+
const missing = [...scope.palettes]
|
|
309
|
+
.filter(([, roles]) => !roles.has(role))
|
|
310
|
+
.map(([palette]) => palette);
|
|
311
|
+
const definite = missing.length === scope.palettes.size;
|
|
312
|
+
for (const palette of missing) {
|
|
313
|
+
issues.push({
|
|
314
|
+
kind: 'roleless-color-palette',
|
|
315
|
+
severity: definite ? 'error' : 'warn',
|
|
316
|
+
line: lineOf(css, site.index),
|
|
317
|
+
scope: scopeName,
|
|
318
|
+
selector: site.selector,
|
|
319
|
+
palette,
|
|
320
|
+
role,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return issues;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Scan a generated Panda stylesheet for the silently-dropped-CSS failure class:
|
|
329
|
+
* recipe variants that were never emitted (missing `staticCss`), semantic tokens
|
|
330
|
+
* that resolved to a bare path (invalid declaration), and roleless `colorPalette`
|
|
331
|
+
* values (valid `var()`, undefined in scope). Pure so it can be unit-tested
|
|
332
|
+
* without a filesystem.
|
|
333
|
+
*/
|
|
334
|
+
export function scanGeneratedCss(css) {
|
|
335
|
+
const issues = [];
|
|
336
|
+
if (!hasAnyRecipeClass(css)) {
|
|
337
|
+
issues.push({ kind: 'missing-static-css', severity: 'error' });
|
|
338
|
+
}
|
|
339
|
+
const seen = new Set();
|
|
340
|
+
for (const match of css.matchAll(UNRESOLVED_TOKEN_DECL)) {
|
|
341
|
+
const index = match.index ?? 0;
|
|
342
|
+
const line = css.slice(0, index).split('\n').length;
|
|
343
|
+
const declaration = `${match[1]}: ${match[2]}`;
|
|
344
|
+
const key = `${line}:${declaration}`;
|
|
345
|
+
if (seen.has(key))
|
|
346
|
+
continue;
|
|
347
|
+
seen.add(key);
|
|
348
|
+
issues.push({
|
|
349
|
+
kind: 'unresolved-token',
|
|
350
|
+
severity: 'error',
|
|
351
|
+
line,
|
|
352
|
+
declaration,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
issues.push(...findRolelessPalettes(css, splitLeafRulesets(css)));
|
|
356
|
+
return {
|
|
357
|
+
ok: issues.every((issue) => issue.severity !== 'error'),
|
|
358
|
+
issues,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function missingStaticCssMessage(relative) {
|
|
362
|
+
return (`No design-system recipe classes found in ${relative}.\n` +
|
|
363
|
+
" The design system's recipes are applied by class name, so Panda's\n" +
|
|
364
|
+
' static extractor cannot see them — without `staticCss` they emit\n' +
|
|
365
|
+
' nothing, and runtime-selected variants (status tones, dense tables,\n' +
|
|
366
|
+
' drawer sizes) render unstyled. Spread the exported map into your\n' +
|
|
367
|
+
' Panda config `staticCss`:\n' +
|
|
368
|
+
" import { designSystemStaticCssRecipes } from '@r0hitsharma/design-system/recipes';\n" +
|
|
369
|
+
' staticCss: { recipes: { ...designSystemStaticCssRecipes } }\n' +
|
|
370
|
+
' then re-run `panda codegen`. (A narrowed subset is fine — this only\n' +
|
|
371
|
+
' flags the case where nothing is wired at all.)');
|
|
372
|
+
}
|
|
373
|
+
function unresolvedTokenMessage(issue, relative) {
|
|
374
|
+
return (`${relative}:${issue.line} unresolved token — \`${issue.declaration};\` ` +
|
|
375
|
+
'is an invalid declaration the browser drops. The token path does not ' +
|
|
376
|
+
'resolve to a `var(--…)`; check the token exists in the preset (or you ' +
|
|
377
|
+
'passed a token path where a finished value was expected).');
|
|
378
|
+
}
|
|
379
|
+
function rolelessColorPaletteMessage(issue, relative) {
|
|
380
|
+
return (`${relative}:${issue.line} roleless colorPalette — \`${issue.selector}\` ` +
|
|
381
|
+
`reads \`var(--colors-color-palette-${issue.role})\`, but the ` +
|
|
382
|
+
`\`${issue.scope}\` scope can be set to \`colorPalette: ${issue.palette}\`, ` +
|
|
383
|
+
`which defines no \`${issue.role}\` role. The custom property is then ` +
|
|
384
|
+
'undefined on that element: valid CSS the browser silently drops, so the ' +
|
|
385
|
+
'declaration just does not apply. ' +
|
|
386
|
+
(issue.severity === 'error'
|
|
387
|
+
? 'No palette this scope can take defines that role, so nothing avoids it. '
|
|
388
|
+
: `Warning only: \`${issue.scope}\` can also take a role-complete palette, ` +
|
|
389
|
+
'so this breaks only if the two are actually combined. ') +
|
|
390
|
+
'Either style with a role-complete palette (one whose ruleset maps ' +
|
|
391
|
+
`\`--colors-color-palette-${issue.role}\`), give \`${issue.palette}\` that ` +
|
|
392
|
+
"role in your preset's colorPalette tokens, or reference a role the " +
|
|
393
|
+
'palette does define.');
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* The console message for one finding. Exhaustive over `DoctorIssue` with no
|
|
397
|
+
* default branch, so adding a `kind` without a message fails to compile.
|
|
398
|
+
*/
|
|
399
|
+
function doctorIssueMessage(issue, relative) {
|
|
400
|
+
switch (issue.kind) {
|
|
401
|
+
case 'missing-static-css':
|
|
402
|
+
return missingStaticCssMessage(relative);
|
|
403
|
+
case 'unresolved-token':
|
|
404
|
+
return unresolvedTokenMessage(issue, relative);
|
|
405
|
+
case 'roleless-color-palette':
|
|
406
|
+
return rolelessColorPaletteMessage(issue, relative);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Locations a Panda project commonly writes `styles.css`, relative to the
|
|
411
|
+
* consumer root. First existing wins; an explicit path always takes precedence.
|
|
412
|
+
*/
|
|
413
|
+
const CSS_CANDIDATES = [
|
|
414
|
+
'styled-system/styles.css',
|
|
415
|
+
'src/styled-system/styles.css',
|
|
416
|
+
'app/styled-system/styles.css',
|
|
417
|
+
];
|
|
418
|
+
/**
|
|
419
|
+
* `uikit-cli doctor [path-to-styles.css] [--codegen]` — fails loudly on the
|
|
420
|
+
* silent CSS failures the library's authoring model otherwise hides.
|
|
421
|
+
*
|
|
422
|
+
* Point it at a generated stylesheet, or pass `--codegen` and it runs
|
|
423
|
+
* `panda cssgen --outfile` itself into a temp file — the mode PostCSS-plugin
|
|
424
|
+
* consumers need, since they never write a frozen `styled-system/styles.css`.
|
|
425
|
+
*/
|
|
426
|
+
export class DoctorCommand {
|
|
427
|
+
fs;
|
|
428
|
+
logger;
|
|
429
|
+
executor;
|
|
430
|
+
constructor(fs, logger, executor) {
|
|
431
|
+
this.fs = fs;
|
|
432
|
+
this.logger = logger;
|
|
433
|
+
this.executor = executor;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Generate a full stylesheet from the consumer's Panda config into a temp
|
|
437
|
+
* file (for PostCSS-plugin consumers with no frozen styles.css). Returns the
|
|
438
|
+
* temp path, or null if codegen failed.
|
|
439
|
+
*/
|
|
440
|
+
runCodegen(cwd) {
|
|
441
|
+
const outfile = path.join(os.tmpdir(), `uikit-doctor-${process.pid}.css`);
|
|
442
|
+
const command = `npx panda cssgen --outfile "${outfile}"`;
|
|
443
|
+
const result = this.executor.exec(command, { cwd, silent: true });
|
|
444
|
+
if (!result.success || !this.fs.exists(outfile)) {
|
|
445
|
+
this.logger.error(`Could not generate CSS with \`${command}\`.\n` +
|
|
446
|
+
' Ensure @pandacss/dev is installed and a panda config is present in\n' +
|
|
447
|
+
` ${cwd}. Underlying error:\n` +
|
|
448
|
+
` ${(result.stderr || 'panda produced no output file').trim()}`);
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
return outfile;
|
|
452
|
+
}
|
|
453
|
+
resolveCssPath(args, cwd) {
|
|
454
|
+
const explicit = args.find((arg) => !arg.startsWith('-'));
|
|
455
|
+
if (explicit) {
|
|
456
|
+
const resolved = path.resolve(cwd, explicit);
|
|
457
|
+
return this.fs.exists(resolved) ? resolved : null;
|
|
458
|
+
}
|
|
459
|
+
for (const candidate of CSS_CANDIDATES) {
|
|
460
|
+
const resolved = path.resolve(cwd, candidate);
|
|
461
|
+
if (this.fs.exists(resolved))
|
|
462
|
+
return resolved;
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
/** Returns true when the stylesheet is healthy. */
|
|
467
|
+
execute(args, cwd = process.cwd()) {
|
|
468
|
+
const useCodegen = args.includes('--codegen');
|
|
469
|
+
let cssPath;
|
|
470
|
+
let temp = false;
|
|
471
|
+
if (useCodegen) {
|
|
472
|
+
cssPath = this.runCodegen(cwd);
|
|
473
|
+
if (!cssPath)
|
|
474
|
+
return false; // runCodegen already logged
|
|
475
|
+
temp = true;
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
cssPath = this.resolveCssPath(args, cwd);
|
|
479
|
+
if (!cssPath) {
|
|
480
|
+
this.logger.error('Could not find a generated Panda stylesheet.\n' +
|
|
481
|
+
`Looked for: ${CSS_CANDIDATES.join(', ')} (relative to ${cwd}).\n` +
|
|
482
|
+
'Options:\n' +
|
|
483
|
+
' - run your `panda codegen` first, then re-run doctor;\n' +
|
|
484
|
+
' - pass the path explicitly: uikit-cli doctor path/to/styles.css;\n' +
|
|
485
|
+
' - if you use the Panda PostCSS plugin (no frozen styles.css),\n' +
|
|
486
|
+
' pass --codegen and doctor will run `panda cssgen` itself.');
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const css = this.fs.readFile(cssPath);
|
|
491
|
+
if (temp)
|
|
492
|
+
this.fs.removeDir(cssPath, { force: true });
|
|
493
|
+
const report = scanGeneratedCss(css);
|
|
494
|
+
const relative = temp
|
|
495
|
+
? '(panda cssgen)'
|
|
496
|
+
: path.relative(cwd, cssPath) || cssPath;
|
|
497
|
+
if (report.issues.length === 0) {
|
|
498
|
+
this.logger.info(`✓ ${relative}: no silently-dropped CSS detected.`);
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
for (const issue of report.issues) {
|
|
502
|
+
const message = doctorIssueMessage(issue, relative);
|
|
503
|
+
if (issue.severity === 'error')
|
|
504
|
+
this.logger.error(message);
|
|
505
|
+
else
|
|
506
|
+
this.logger.warn(message);
|
|
507
|
+
}
|
|
508
|
+
if (report.ok) {
|
|
509
|
+
// Only warnings were printed: say so rather than claiming a clean sheet.
|
|
510
|
+
const count = report.issues.length;
|
|
511
|
+
const summary = count === 1
|
|
512
|
+
? 'The warning above is an unproven combination, so it does not fail'
|
|
513
|
+
: `The ${count} warnings above are unproven combinations, so they do not fail`;
|
|
514
|
+
this.logger.info(`✓ ${relative}: nothing definitely dropped. ${summary} the check.`);
|
|
515
|
+
}
|
|
516
|
+
return report.ok;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { shellEscape } from '../shell-utils.js';
|
|
3
|
+
import { resolveCliBinary } from '../tool-binaries.js';
|
|
4
|
+
/**
|
|
5
|
+
* Format command - forwards to oxfmt with config detection
|
|
6
|
+
*/
|
|
7
|
+
export class FormatCommand {
|
|
8
|
+
executor;
|
|
9
|
+
fs;
|
|
10
|
+
constructor(executor, fs) {
|
|
11
|
+
this.executor = executor;
|
|
12
|
+
this.fs = fs;
|
|
13
|
+
}
|
|
14
|
+
execute(args) {
|
|
15
|
+
const modifiedArgs = [...args];
|
|
16
|
+
const defaultConfig = './.oxfmtrc.ts';
|
|
17
|
+
// Add default config if not specified and file exists
|
|
18
|
+
if (!this.hasConfigFlag(modifiedArgs)) {
|
|
19
|
+
const configPath = path.join(process.cwd(), '.oxfmtrc.ts');
|
|
20
|
+
if (this.fs.exists(configPath)) {
|
|
21
|
+
modifiedArgs.unshift(defaultConfig);
|
|
22
|
+
modifiedArgs.unshift('-c');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const escapedArgs = modifiedArgs.map((arg) => shellEscape(arg)).join(' ');
|
|
26
|
+
const oxfmtBinary = shellEscape(resolveCliBinary('oxfmt', 'bin/oxfmt'));
|
|
27
|
+
return this.executor.exec(`${oxfmtBinary} ${escapedArgs}`.trim(), {
|
|
28
|
+
cwd: process.cwd(),
|
|
29
|
+
}).success;
|
|
30
|
+
}
|
|
31
|
+
hasConfigFlag(args) {
|
|
32
|
+
return args.some((arg) => arg === '-c' || arg === '--config' || arg.startsWith('--config='));
|
|
33
|
+
}
|
|
34
|
+
}
|