synthesisui 0.16.70 → 0.16.72
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 +12 -3
- package/dist/doctor/class-style.js +127 -0
- package/dist/doctor/transcribe.js +19 -0
- package/package.json +1 -1
package/dist/commands/import.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { basename, dirname, join, relative } from "node:path";
|
|
3
3
|
import { readCredentials, readToken, resolveRegistry, sameRegistry, } from "../config.js";
|
|
4
4
|
import { findBrokenRefs } from "../doctor/broken-refs.js";
|
|
5
|
+
import { describeClassStyle, detectClassStyle, } from "../doctor/class-style.js";
|
|
5
6
|
import { isNearDuplicate, isNeutral, lightness, } from "../doctor/color-distance.js";
|
|
6
7
|
import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
|
|
7
8
|
import { crosswalk, observedRules } from "../doctor/crosswalk.js";
|
|
@@ -10,7 +11,7 @@ import { describeConvention, describeRemainder, detectConventions, } from "../do
|
|
|
10
11
|
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
11
12
|
import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
|
|
12
13
|
import { buildTable } from "../doctor/tokens.js";
|
|
13
|
-
import { rootClasses, transcribe, } from "../doctor/transcribe.js";
|
|
14
|
+
import { rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
|
|
14
15
|
import { body, paint, section } from "../output.js";
|
|
15
16
|
import { walk, walkAll } from "./doctor.js";
|
|
16
17
|
/**
|
|
@@ -312,8 +313,10 @@ export async function takeCensus(root) {
|
|
|
312
313
|
const size = Object.keys(t.base).length +
|
|
313
314
|
Object.keys(t.dark).length +
|
|
314
315
|
Object.keys(t.states).length;
|
|
315
|
-
|
|
316
|
-
|
|
316
|
+
const tag = rootTag(src);
|
|
317
|
+
if (size > 0 || tag) {
|
|
318
|
+
looks[found[0].name] = tag ? { ...t, rootTag: tag } : t;
|
|
319
|
+
}
|
|
317
320
|
}
|
|
318
321
|
}
|
|
319
322
|
}
|
|
@@ -405,12 +408,14 @@ export async function takeCensus(root) {
|
|
|
405
408
|
}
|
|
406
409
|
const brokenRefs = findBrokenRefs(sources, declaredNames);
|
|
407
410
|
const conventions = detectConventions(sources);
|
|
411
|
+
const classStyle = detectClassStyle(sources);
|
|
408
412
|
return {
|
|
409
413
|
census: 1,
|
|
410
414
|
project: { name, stack: await detectStack(root) },
|
|
411
415
|
declared: Object.fromEntries(table.byName),
|
|
412
416
|
...(brokenRefs.length > 0 ? { brokenRefs } : {}),
|
|
413
417
|
...(conventions.length > 0 ? { conventions } : {}),
|
|
418
|
+
classStyle,
|
|
414
419
|
...(Object.keys(looks).length > 0 ? { looks } : {}),
|
|
415
420
|
...(schemes.alt.size > 0
|
|
416
421
|
? { declaredAlt: Object.fromEntries(schemes.alt) }
|
|
@@ -494,6 +499,10 @@ function sayConventions(c) {
|
|
|
494
499
|
if (remainder)
|
|
495
500
|
console.log(body(paint.faint(` ${remainder}`)));
|
|
496
501
|
}
|
|
502
|
+
if (c.classStyle) {
|
|
503
|
+
console.log("");
|
|
504
|
+
console.log(body(describeClassStyle(c.classStyle)));
|
|
505
|
+
}
|
|
497
506
|
console.log("");
|
|
498
507
|
console.log(body(paint.faint("Nothing here is a rule we brought - it is what your own files already do, and the system now knows it. None of the rest has to move today: this is a complement, not a correction.")));
|
|
499
508
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HOW THIS PROJECT NAMES A CLASS - and whether it names one at all.
|
|
3
|
+
*
|
|
4
|
+
* A compiled `.ds-metric-card` attaches to their markup, and a project whose own
|
|
5
|
+
* convention is `.metric-card__title` should not get a new name in its house for
|
|
6
|
+
* no reason it chose (dono, 01/08). So the convention has to be read, not
|
|
7
|
+
* assumed.
|
|
8
|
+
*
|
|
9
|
+
* Reading a real project first changed what this had to answer. Measured on a
|
|
10
|
+
* 670-stylesheet monorepo: 536 files are `*.module.scss` and 134 are not, and the
|
|
11
|
+
* class names inside them are `root`, `wrapper`, `editorText` - LOCAL names that
|
|
12
|
+
* a bundler hashes into `RootWrapper_root__x7f2a`.
|
|
13
|
+
*
|
|
14
|
+
* Which means there is no global convention there to follow, and our global
|
|
15
|
+
* class cannot collide with anything of theirs. The honest answer for that
|
|
16
|
+
* project is "nothing to change", and saying so is worth more than a concession
|
|
17
|
+
* nobody needed - inventing a problem is its own kind of dishonesty.
|
|
18
|
+
*
|
|
19
|
+
* Three worlds, and only the third asks anything of us:
|
|
20
|
+
*
|
|
21
|
+
* modules classes are local and hashed - ours are safe, and separate
|
|
22
|
+
* utility no component classes at all - ours are the only global ones
|
|
23
|
+
* global they DO name global classes, and we follow their shape
|
|
24
|
+
*/
|
|
25
|
+
const MODULE_FILE = /\.module\.(css|scss|sass|less)$/i;
|
|
26
|
+
const STYLESHEET = /\.(css|scss|sass|less)$/i;
|
|
27
|
+
/** A class selector at the start of a rule: `.metric-card__title {`. */
|
|
28
|
+
const CLASS_DECL = /^\s*\.([a-zA-Z][a-zA-Z0-9_-]*)\s*[,{:]/gm;
|
|
29
|
+
/** A Tailwind-shaped utility in markup - enough to know they exist. */
|
|
30
|
+
const UTILITY = /\b(?:bg|text|border|p|px|py|m|mx|my|gap|flex|grid|rounded|shadow)-[a-z0-9[\]./-]+/g;
|
|
31
|
+
/**
|
|
32
|
+
* Below this many global class declarations, a "convention" is a handful of
|
|
33
|
+
* one-off selectors rather than a house style - and adopting one from noise
|
|
34
|
+
* would rename everything we compile on the strength of three sightings.
|
|
35
|
+
*/
|
|
36
|
+
const MIN_GLOBAL_DECLS = 20;
|
|
37
|
+
/**
|
|
38
|
+
* Prefixes worth honouring have to be shared by most of what they declare.
|
|
39
|
+
* A project with `app-header` and `admin-nav` has two words, not a prefix.
|
|
40
|
+
*/
|
|
41
|
+
const PREFIX_SHARE = 0.6;
|
|
42
|
+
export function detectClassStyle(files) {
|
|
43
|
+
let moduleFiles = 0;
|
|
44
|
+
let globalFiles = 0;
|
|
45
|
+
let utilities = 0;
|
|
46
|
+
const globalDecls = [];
|
|
47
|
+
for (const { file, source } of files) {
|
|
48
|
+
if (MODULE_FILE.test(file)) {
|
|
49
|
+
moduleFiles += 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (STYLESHEET.test(file)) {
|
|
53
|
+
globalFiles += 1;
|
|
54
|
+
// Only a non-module stylesheet can hold a class anybody else must match.
|
|
55
|
+
for (const m of source.matchAll(CLASS_DECL))
|
|
56
|
+
globalDecls.push(m[1]);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
utilities += source.match(UTILITY)?.length ?? 0;
|
|
60
|
+
}
|
|
61
|
+
// Their own global classes come first - but only if they have a SHAPE.
|
|
62
|
+
//
|
|
63
|
+
// Counting them was not enough. A real repo returned `global` on the strength
|
|
64
|
+
// of a syntax-highlighting theme: `hll`, `field-name`, `document` cleared the
|
|
65
|
+
// threshold with no convention between them, and we would have renamed every
|
|
66
|
+
// class we compile to match vendor CSS (dono, 01/08). Many global classes with
|
|
67
|
+
// nothing in common is not a house style, it is a stylesheet.
|
|
68
|
+
if (globalDecls.length >= MIN_GLOBAL_DECLS) {
|
|
69
|
+
const shape = shapeOf(globalDecls);
|
|
70
|
+
if (shape) {
|
|
71
|
+
return {
|
|
72
|
+
kind: "global",
|
|
73
|
+
...shape,
|
|
74
|
+
agreed: globalDecls.length,
|
|
75
|
+
samples: [...new Set(globalDecls)].slice(0, 3),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (moduleFiles > globalFiles && moduleFiles > 0) {
|
|
80
|
+
return { kind: "modules", moduleFiles, globalFiles };
|
|
81
|
+
}
|
|
82
|
+
return { kind: "utility", utilities };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The shape their global classes share.
|
|
86
|
+
*
|
|
87
|
+
* BEM's `__` is unmistakable when present, so it is checked first and only
|
|
88
|
+
* accepted on a real share - two `__` selectors in a thousand is somebody's
|
|
89
|
+
* exception, not the house style.
|
|
90
|
+
*/
|
|
91
|
+
function shapeOf(decls) {
|
|
92
|
+
// BEM's `__` is unmistakable when it is the house style, and meaningless as
|
|
93
|
+
// somebody's two exceptions.
|
|
94
|
+
const bem = decls.filter((c) => c.includes("__")).length;
|
|
95
|
+
const isBem = bem / decls.length > 0.2;
|
|
96
|
+
// A prefix is the first delimited segment, when most of them share it. A
|
|
97
|
+
// project with `app-header` and `admin-nav` has two words, not a prefix.
|
|
98
|
+
const heads = new Map();
|
|
99
|
+
for (const c of decls) {
|
|
100
|
+
const head = c.split(/[-_]/)[0];
|
|
101
|
+
if (!head || head === c)
|
|
102
|
+
continue;
|
|
103
|
+
heads.set(head, (heads.get(head) ?? 0) + 1);
|
|
104
|
+
}
|
|
105
|
+
const top = [...heads.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
106
|
+
const hasPrefix = top != null && top[1] / decls.length >= PREFIX_SHARE;
|
|
107
|
+
// Neither established: there is nothing here for us to follow, and adopting
|
|
108
|
+
// "unprefixed, hyphenated" from a grab-bag would rename everything we compile
|
|
109
|
+
// for no reason anybody chose.
|
|
110
|
+
if (!isBem && !hasPrefix)
|
|
111
|
+
return null;
|
|
112
|
+
return {
|
|
113
|
+
prefix: hasPrefix ? `${top[0]}-` : "",
|
|
114
|
+
partSeparator: isBem ? "__" : "-",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** One line for the report, in their vocabulary and without asking for anything. */
|
|
118
|
+
export function describeClassStyle(style) {
|
|
119
|
+
if (style.kind === "modules") {
|
|
120
|
+
return `your components style themselves through CSS Modules (${style.moduleFiles} module stylesheets) - those class names are local and hashed, so ours stay global and cannot collide with yours`;
|
|
121
|
+
}
|
|
122
|
+
if (style.kind === "utility") {
|
|
123
|
+
return "your components style themselves with utilities, so there are no component class names of yours for ours to match";
|
|
124
|
+
}
|
|
125
|
+
const named = style.prefix ? `prefixed \`${style.prefix}\`` : "unprefixed";
|
|
126
|
+
return `your global classes are ${named} and join parts with \`${style.partSeparator}\` (e.g. ${style.samples.join(", ")}) - what we compile will be spelled the same way`;
|
|
127
|
+
}
|
|
@@ -309,6 +309,25 @@ export function transcribe(classes, declared) {
|
|
|
309
309
|
* nothing, and nothing is a contract with an empty base - which is exactly what
|
|
310
310
|
* the import already produces, so failing here costs no ground.
|
|
311
311
|
*/
|
|
312
|
+
/**
|
|
313
|
+
* The TAG of the element a component returns - `button`, `input`, `div`.
|
|
314
|
+
*
|
|
315
|
+
* Cheap, and it is half of what decides a preview's anatomy: a component whose
|
|
316
|
+
* root is a `<button>` is an action however it is named, and one whose root is a
|
|
317
|
+
* bare `<span>` is an indicator. The platform pairs this with the name; see
|
|
318
|
+
* `preview-kind.ts`.
|
|
319
|
+
*
|
|
320
|
+
* A composed root (`<Radio.Root>`) returns the component name, not an HTML tag -
|
|
321
|
+
* which is still a signal, just a weaker one, so it travels as written and the
|
|
322
|
+
* platform decides what to do with it.
|
|
323
|
+
*/
|
|
324
|
+
export function rootTag(source) {
|
|
325
|
+
const ret = source.search(/return\s*\(/);
|
|
326
|
+
if (ret === -1)
|
|
327
|
+
return null;
|
|
328
|
+
const m = /<([A-Za-z][A-Za-z0-9.]*)/.exec(source.slice(ret));
|
|
329
|
+
return m ? m[1] : null;
|
|
330
|
+
}
|
|
312
331
|
export function rootClasses(source) {
|
|
313
332
|
const ret = source.search(/return\s*\(/);
|
|
314
333
|
if (ret === -1)
|
package/package.json
CHANGED