@pathscale/rsbuild-plugin-ui-css-purge 0.9.2 → 0.9.4
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 +80 -70
- package/dist/generate-manifest.d.ts +4 -4
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1435 -73
- package/dist/postbuild-purge.d.ts +51 -39
- package/dist/postbuild-purge.js +444 -320
- package/dist/scan-consumer.d.ts +38 -11
- package/package.json +31 -32
- package/src/generate-manifest.ts +428 -183
package/src/generate-manifest.ts
CHANGED
|
@@ -1,223 +1,468 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Lib-side database generator.
|
|
2
|
+
* Lib-side purge database generator.
|
|
3
3
|
*
|
|
4
|
-
* Reads all `*.classes.ts` files from @pathscale/ui's component tree
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Reads all `*.classes.ts` files from @pathscale/ui's component tree and
|
|
5
|
+
* produces a versioned `purge-manifest.json` database consumed by the postbuild
|
|
6
|
+
* purge script.
|
|
7
7
|
*
|
|
8
8
|
* Usage: bun run src/generate-manifest.ts <path-to-ui-src/components>
|
|
9
9
|
* Output: purge-manifest.json in cwd (or pass --out <path>)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { Glob } from "bun";
|
|
13
|
-
import
|
|
13
|
+
import postcss from "postcss";
|
|
14
14
|
|
|
15
15
|
// ── Types ──────────────────────────────────────────────────────────────────────
|
|
16
16
|
|
|
17
17
|
type ClassValue = string | readonly string[];
|
|
18
18
|
|
|
19
|
-
interface
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
interface ComponentPurgeRecord {
|
|
20
|
+
key: string;
|
|
21
|
+
component: string;
|
|
22
|
+
part?: string;
|
|
23
|
+
classes: {
|
|
24
|
+
always: string[];
|
|
25
|
+
byProp: Record<string, string[] | Record<string, string[]>>;
|
|
26
|
+
};
|
|
27
|
+
attrs?: Record<string, Record<string, string>>;
|
|
28
|
+
attributeSelectors: string[];
|
|
29
|
+
selectors: string[];
|
|
30
|
+
cssVars: {
|
|
31
|
+
declared: string[];
|
|
32
|
+
referenced: string[];
|
|
33
|
+
};
|
|
34
|
+
keyframes: {
|
|
35
|
+
declared: string[];
|
|
36
|
+
referenced: string[];
|
|
37
|
+
};
|
|
38
|
+
deps?: string[];
|
|
26
39
|
}
|
|
27
40
|
|
|
28
|
-
|
|
41
|
+
interface PurgeDatabaseV2 {
|
|
42
|
+
version: 2;
|
|
43
|
+
components: Record<string, ComponentPurgeRecord>;
|
|
44
|
+
shared: {
|
|
45
|
+
selectors: { selector: string; components: string[] }[];
|
|
46
|
+
cssVars: Record<string, { declaredBy: string[]; referencedBy: string[] }>;
|
|
47
|
+
keyframes: Record<string, { declaredBy: string[]; referencedBy: string[] }>;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ClassWalkResult {
|
|
52
|
+
classes: ComponentPurgeRecord["classes"];
|
|
53
|
+
attrs?: Record<string, Record<string, string>>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface CssFacts {
|
|
57
|
+
selectors: string[];
|
|
58
|
+
attributeSelectors: string[];
|
|
59
|
+
cssVars: {
|
|
60
|
+
declared: string[];
|
|
61
|
+
referenced: string[];
|
|
62
|
+
};
|
|
63
|
+
keyframes: {
|
|
64
|
+
declared: string[];
|
|
65
|
+
referenced: string[];
|
|
66
|
+
};
|
|
67
|
+
}
|
|
29
68
|
|
|
30
69
|
// ── Tailwind utility filter ───────────────────────────────────────────────────
|
|
31
70
|
|
|
32
|
-
/** Matches Tailwind utility class prefixes — these should NOT appear
|
|
33
|
-
const twPattern =
|
|
71
|
+
/** Matches Tailwind utility class prefixes — these should NOT appear as owned component classes. */
|
|
72
|
+
const twPattern =
|
|
73
|
+
/^(-?)(flex|grid|gap|items|justify|self|place|order|col|row|auto|basis|grow|shrink|space|overflow|relative|absolute|fixed|sticky|static|block|inline|hidden|visible|invisible|z|inset|top|right|bottom|left|float|clear|isolate|object|aspect|container|columns|break|box|display|table|caption|border|rounded|outline|ring|shadow|opacity|mix|bg|from|via|to|text|font|leading|tracking|indent|align|whitespace|word|hyphens|content|list|decoration|underline|overline|line|no-underline|uppercase|lowercase|capitalize|normal|italic|not-italic|antialiased|subpixel|truncate|w|h|min|max|p|px|py|pt|pr|pb|pl|m|mx|my|mt|mr|mb|ml|size|scroll|snap|touch|select|resize|cursor|caret|pointer|will|appearance|accent|transition|duration|delay|ease|animate|scale|rotate|translate|skew|transform|origin|filter|blur|brightness|contrast|drop|grayscale|hue|invert|saturate|sepia|backdrop|sr|forced|print|motion|lg|md|sm|xl|2xl|dark|hover|focus|active|disabled|first|last|odd|even|group|peer)($|[-:\[.])/;
|
|
34
74
|
|
|
35
75
|
function isTailwindUtility(cls: string): boolean {
|
|
36
|
-
|
|
76
|
+
return twPattern.test(cls);
|
|
37
77
|
}
|
|
38
78
|
|
|
39
79
|
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
40
80
|
|
|
41
|
-
/** Flatten a class value
|
|
42
|
-
function flattenClasses(val:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Check if a value is a plain object (not array, not null). */
|
|
81
|
+
/** Flatten a class value into individual non-Tailwind component class names. */
|
|
82
|
+
function flattenClasses(val: unknown): string[] {
|
|
83
|
+
let classes: string[];
|
|
84
|
+
if (typeof val === "string") {
|
|
85
|
+
classes = val.split(/\s+/).filter(Boolean);
|
|
86
|
+
} else if (Array.isArray(val)) {
|
|
87
|
+
classes = val.flatMap((s) => flattenClasses(s));
|
|
88
|
+
} else if (val !== null && typeof val === "object") {
|
|
89
|
+
classes = Object.values(val).flatMap((v) => flattenClasses(v));
|
|
90
|
+
} else {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
return [...new Set(classes.filter((cls) => !isTailwindUtility(cls)))];
|
|
94
|
+
}
|
|
95
|
+
|
|
57
96
|
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
58
|
-
|
|
97
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
59
98
|
}
|
|
60
99
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const byProp: Record<string, string[] | Record<string, string[]>> = {};
|
|
68
|
-
let attrs: Record<string, Record<string, string>> | undefined;
|
|
69
|
-
|
|
70
|
-
for (const [slot, value] of Object.entries(obj)) {
|
|
71
|
-
if (slot === "base") {
|
|
72
|
-
always.push(...flattenClasses(value as ClassValue));
|
|
73
|
-
} else if (slot === "attrs") {
|
|
74
|
-
if (isRecord(value)) {
|
|
75
|
-
attrs = {};
|
|
76
|
-
for (const [propName, attrMap] of Object.entries(value)) {
|
|
77
|
-
if (isRecord(attrMap)) {
|
|
78
|
-
attrs[propName] = attrMap as Record<string, string>;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} else if (slot === "flag") {
|
|
83
|
-
if (isRecord(value)) {
|
|
84
|
-
for (const [propName, classVal] of Object.entries(value)) {
|
|
85
|
-
byProp[propName] = flattenClasses(classVal as ClassValue);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
} else if (isRecord(value)) {
|
|
89
|
-
const enumMap: Record<string, string[]> = {};
|
|
90
|
-
for (const [enumKey, classVal] of Object.entries(value)) {
|
|
91
|
-
enumMap[enumKey] = flattenClasses(classVal as ClassValue);
|
|
92
|
-
}
|
|
93
|
-
byProp[slot] = enumMap;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const manifest: ComponentManifest = { classes: { always, byProp } };
|
|
98
|
-
if (attrs && Object.keys(attrs).length > 0) {
|
|
99
|
-
manifest.attrs = attrs;
|
|
100
|
-
}
|
|
101
|
-
return manifest;
|
|
100
|
+
function kebabToPascal(s: string): string {
|
|
101
|
+
return s
|
|
102
|
+
.split("-")
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
105
|
+
.join("");
|
|
102
106
|
}
|
|
103
107
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
function joinPath(base: string, rel: string): string {
|
|
109
|
+
return `${base.replace(/\/+$/, "")}/${rel.replace(/^\/+/, "")}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function dirname(filePath: string): string {
|
|
113
|
+
const parts = filePath.split(/[\\/]/);
|
|
114
|
+
parts.pop();
|
|
115
|
+
return parts.length === 0 ? "." : parts.join("/");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function basename(filePath: string, suffix = ""): string {
|
|
119
|
+
const name = filePath.split(/[\\/]/).pop() ?? filePath;
|
|
120
|
+
return suffix && name.endsWith(suffix) ? name.slice(0, -suffix.length) : name;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function resolvePath(filePath: string): string {
|
|
124
|
+
if (filePath.startsWith("/")) return filePath;
|
|
125
|
+
return joinPath(process.cwd(), filePath);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function canonicalComponentFromRelPath(relPath: string): string {
|
|
129
|
+
const stem = basename(relPath, ".classes.ts");
|
|
130
|
+
const dir = dirname(relPath);
|
|
131
|
+
const dirParts = dir === "." ? [] : dir.split(/[\\/]/).filter(Boolean);
|
|
132
|
+
const source =
|
|
133
|
+
stem === "classes" && dirParts.length > 0
|
|
134
|
+
? (dirParts.at(-1) ?? stem)
|
|
135
|
+
: stem;
|
|
136
|
+
return kebabToPascal(source);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function walkClassesObject(obj: Record<string, unknown>): ClassWalkResult {
|
|
140
|
+
const always: string[] = [];
|
|
141
|
+
const byProp: Record<string, string[] | Record<string, string[]>> = {};
|
|
142
|
+
let attrs: Record<string, Record<string, string>> | undefined;
|
|
143
|
+
|
|
144
|
+
for (const [slot, value] of Object.entries(obj)) {
|
|
145
|
+
if (slot === "base") {
|
|
146
|
+
always.push(...flattenClasses(value));
|
|
147
|
+
} else if (slot === "attrs") {
|
|
148
|
+
if (isRecord(value)) {
|
|
149
|
+
attrs = {};
|
|
150
|
+
for (const [propName, attrMap] of Object.entries(value)) {
|
|
151
|
+
if (isRecord(attrMap))
|
|
152
|
+
attrs[propName] = attrMap as Record<string, string>;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} else if (slot === "flag") {
|
|
156
|
+
if (isRecord(value)) {
|
|
157
|
+
for (const [propName, classVal] of Object.entries(value)) {
|
|
158
|
+
byProp[propName] = flattenClasses(classVal);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} else if (isRecord(value)) {
|
|
162
|
+
const enumMap: Record<string, string[]> = {};
|
|
163
|
+
for (const [enumKey, classVal] of Object.entries(value)) {
|
|
164
|
+
enumMap[enumKey] = flattenClasses(classVal);
|
|
165
|
+
}
|
|
166
|
+
byProp[slot] = enumMap;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { classes: { always: [...new Set(always)], byProp }, attrs };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const KNOWN_SLOTS = new Set([
|
|
174
|
+
"base",
|
|
175
|
+
"variant",
|
|
176
|
+
"size",
|
|
177
|
+
"flag",
|
|
178
|
+
"color",
|
|
179
|
+
"tone",
|
|
180
|
+
"attrs",
|
|
181
|
+
]);
|
|
109
182
|
|
|
110
183
|
function isCompound(obj: Record<string, unknown>): boolean {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
184
|
+
for (const key of Object.keys(obj)) {
|
|
185
|
+
if (KNOWN_SLOTS.has(key)) return false;
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function emptyCssFacts(): CssFacts {
|
|
191
|
+
return {
|
|
192
|
+
selectors: [],
|
|
193
|
+
attributeSelectors: [],
|
|
194
|
+
cssVars: { declared: [], referenced: [] },
|
|
195
|
+
keyframes: { declared: [], referenced: [] },
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function scanCssFacts(componentDir: string): Promise<CssFacts> {
|
|
200
|
+
const facts = emptyCssFacts();
|
|
201
|
+
const glob = new Glob("**/*.css");
|
|
202
|
+
|
|
203
|
+
for await (const relPath of glob.scan({ cwd: componentDir })) {
|
|
204
|
+
const css = await Bun.file(joinPath(componentDir, relPath)).text();
|
|
205
|
+
const root = postcss.parse(css);
|
|
206
|
+
|
|
207
|
+
root.walkRules((rule) => {
|
|
208
|
+
for (const selector of rule.selectors) {
|
|
209
|
+
facts.selectors.push(selector);
|
|
210
|
+
for (const attr of extractAttrsFromSelector(selector)) {
|
|
211
|
+
facts.attributeSelectors.push(attr);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
root.walkDecls((decl) => {
|
|
217
|
+
if (decl.prop.startsWith("--")) facts.cssVars.declared.push(decl.prop);
|
|
218
|
+
for (const ref of decl.value.matchAll(/var\(\s*(--[a-zA-Z0-9_-]+)/g)) {
|
|
219
|
+
facts.cssVars.referenced.push(ref[1]);
|
|
220
|
+
}
|
|
221
|
+
if (/^animation(-name)?$/.test(decl.prop)) {
|
|
222
|
+
for (const part of decl.value.split(",")) {
|
|
223
|
+
const name = part.trim().split(/\s+/)[0];
|
|
224
|
+
if (name && !["none", "initial", "inherit", "unset"].includes(name)) {
|
|
225
|
+
facts.keyframes.referenced.push(name);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
root.walkAtRules("keyframes", (atRule) => {
|
|
232
|
+
facts.keyframes.declared.push(atRule.params.trim());
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
selectors: [...new Set(facts.selectors)].sort(),
|
|
238
|
+
attributeSelectors: [...new Set(facts.attributeSelectors)].sort(),
|
|
239
|
+
cssVars: {
|
|
240
|
+
declared: [...new Set(facts.cssVars.declared)].sort(),
|
|
241
|
+
referenced: [...new Set(facts.cssVars.referenced)].sort(),
|
|
242
|
+
},
|
|
243
|
+
keyframes: {
|
|
244
|
+
declared: [...new Set(facts.keyframes.declared)].sort(),
|
|
245
|
+
referenced: [...new Set(facts.keyframes.referenced)].sort(),
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function extractAttrsFromSelector(selector: string): string[] {
|
|
251
|
+
const matches = selector.matchAll(
|
|
252
|
+
/\[(data-[a-zA-Z0-9_-]+|aria-[a-zA-Z0-9_-]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\]]+)))?\]/g,
|
|
253
|
+
);
|
|
254
|
+
return [...matches].map((m) => {
|
|
255
|
+
const value = m[2] ?? m[3] ?? m[4];
|
|
256
|
+
return value === undefined ? `[${m[1]}]` : `[${m[1]}="${value.trim()}"]`;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function createRecord(
|
|
261
|
+
key: string,
|
|
262
|
+
component: string,
|
|
263
|
+
part: string | undefined,
|
|
264
|
+
classFacts: ClassWalkResult,
|
|
265
|
+
cssFacts: CssFacts,
|
|
266
|
+
): ComponentPurgeRecord {
|
|
267
|
+
const record: ComponentPurgeRecord = {
|
|
268
|
+
key,
|
|
269
|
+
component,
|
|
270
|
+
part,
|
|
271
|
+
classes: classFacts.classes,
|
|
272
|
+
attributeSelectors: cssFacts.attributeSelectors,
|
|
273
|
+
selectors: cssFacts.selectors,
|
|
274
|
+
cssVars: cssFacts.cssVars,
|
|
275
|
+
keyframes: cssFacts.keyframes,
|
|
276
|
+
};
|
|
277
|
+
if (classFacts.attrs && Object.keys(classFacts.attrs).length > 0) {
|
|
278
|
+
record.attrs = classFacts.attrs;
|
|
279
|
+
}
|
|
280
|
+
return record;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function updateShared(db: PurgeDatabaseV2) {
|
|
284
|
+
const selectorOwners = new Map<string, Set<string>>();
|
|
285
|
+
|
|
286
|
+
for (const [key, record] of Object.entries(db.components)) {
|
|
287
|
+
for (const selector of record.selectors) {
|
|
288
|
+
const owners = selectorOwners.get(selector) ?? new Set<string>();
|
|
289
|
+
owners.add(key);
|
|
290
|
+
selectorOwners.set(selector, owners);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (const varName of record.cssVars.declared) {
|
|
294
|
+
const entry = db.shared.cssVars[varName] ?? {
|
|
295
|
+
declaredBy: [],
|
|
296
|
+
referencedBy: [],
|
|
297
|
+
};
|
|
298
|
+
entry.declaredBy.push(key);
|
|
299
|
+
db.shared.cssVars[varName] = entry;
|
|
300
|
+
}
|
|
301
|
+
for (const varName of record.cssVars.referenced) {
|
|
302
|
+
const entry = db.shared.cssVars[varName] ?? {
|
|
303
|
+
declaredBy: [],
|
|
304
|
+
referencedBy: [],
|
|
305
|
+
};
|
|
306
|
+
entry.referencedBy.push(key);
|
|
307
|
+
db.shared.cssVars[varName] = entry;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const name of record.keyframes.declared) {
|
|
311
|
+
const entry = db.shared.keyframes[name] ?? {
|
|
312
|
+
declaredBy: [],
|
|
313
|
+
referencedBy: [],
|
|
314
|
+
};
|
|
315
|
+
entry.declaredBy.push(key);
|
|
316
|
+
db.shared.keyframes[name] = entry;
|
|
317
|
+
}
|
|
318
|
+
for (const name of record.keyframes.referenced) {
|
|
319
|
+
const entry = db.shared.keyframes[name] ?? {
|
|
320
|
+
declaredBy: [],
|
|
321
|
+
referencedBy: [],
|
|
322
|
+
};
|
|
323
|
+
entry.referencedBy.push(key);
|
|
324
|
+
db.shared.keyframes[name] = entry;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
db.shared.selectors = [...selectorOwners.entries()]
|
|
329
|
+
.map(([selector, owners]) => ({ selector, components: [...owners].sort() }))
|
|
330
|
+
.sort((a, b) => a.selector.localeCompare(b.selector));
|
|
331
|
+
|
|
332
|
+
for (const entry of Object.values(db.shared.cssVars)) {
|
|
333
|
+
entry.declaredBy = [...new Set(entry.declaredBy)].sort();
|
|
334
|
+
entry.referencedBy = [...new Set(entry.referencedBy)].sort();
|
|
335
|
+
}
|
|
336
|
+
for (const entry of Object.values(db.shared.keyframes)) {
|
|
337
|
+
entry.declaredBy = [...new Set(entry.declaredBy)].sort();
|
|
338
|
+
entry.referencedBy = [...new Set(entry.referencedBy)].sort();
|
|
339
|
+
}
|
|
115
340
|
}
|
|
116
341
|
|
|
117
342
|
// ── Main ───────────────────────────────────────────────────────────────────────
|
|
118
343
|
|
|
119
344
|
async function main() {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
345
|
+
const args = process.argv.slice(2);
|
|
346
|
+
let componentsDir = args[0];
|
|
347
|
+
let outPath = "purge-manifest.json";
|
|
348
|
+
|
|
349
|
+
const outIdx = args.indexOf("--out");
|
|
350
|
+
if (outIdx !== -1 && args[outIdx + 1]) {
|
|
351
|
+
outPath = args[outIdx + 1];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!componentsDir) {
|
|
355
|
+
console.error(
|
|
356
|
+
"Usage: bun run src/generate-manifest.ts <path-to-components-dir> [--out <path>]",
|
|
357
|
+
);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
componentsDir = resolvePath(componentsDir);
|
|
362
|
+
console.log(`Scanning ${componentsDir} for *.classes.ts files...`);
|
|
363
|
+
|
|
364
|
+
const db: PurgeDatabaseV2 = {
|
|
365
|
+
version: 2,
|
|
366
|
+
components: {},
|
|
367
|
+
shared: {
|
|
368
|
+
selectors: [],
|
|
369
|
+
cssVars: {},
|
|
370
|
+
keyframes: {},
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
const glob = new Glob("**/*.classes.ts");
|
|
374
|
+
|
|
375
|
+
for await (const relPath of glob.scan({ cwd: componentsDir })) {
|
|
376
|
+
const fullPath = joinPath(componentsDir, relPath);
|
|
377
|
+
const mod = await import(fullPath);
|
|
378
|
+
const classesExport = mod.CLASSES;
|
|
379
|
+
|
|
380
|
+
if (!classesExport || typeof classesExport !== "object") {
|
|
381
|
+
console.warn(` SKIP ${relPath} - no CLASSES export found`);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const component = canonicalComponentFromRelPath(relPath);
|
|
386
|
+
const componentDir = dirname(fullPath);
|
|
387
|
+
const cssFacts = await scanCssFacts(componentDir);
|
|
388
|
+
|
|
389
|
+
if (isCompound(classesExport as Record<string, unknown>)) {
|
|
390
|
+
for (const [partName, partObj] of Object.entries(
|
|
391
|
+
classesExport as Record<string, unknown>,
|
|
392
|
+
)) {
|
|
393
|
+
if (isRecord(partObj)) {
|
|
394
|
+
const key = `${component}.${partName}`;
|
|
395
|
+
db.components[key] = createRecord(
|
|
396
|
+
key,
|
|
397
|
+
component,
|
|
398
|
+
partName,
|
|
399
|
+
walkClassesObject(partObj),
|
|
400
|
+
cssFacts,
|
|
401
|
+
);
|
|
402
|
+
console.log(` ok ${key}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
db.components[component] = createRecord(
|
|
407
|
+
component,
|
|
408
|
+
component,
|
|
409
|
+
undefined,
|
|
410
|
+
walkClassesObject(classesExport as Record<string, unknown>),
|
|
411
|
+
cssFacts,
|
|
412
|
+
);
|
|
413
|
+
console.log(` ok ${component}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
await attachDependencies(componentsDir, db);
|
|
418
|
+
updateShared(db);
|
|
419
|
+
|
|
420
|
+
const json = JSON.stringify(db, null, 2);
|
|
421
|
+
await Bun.write(outPath, json);
|
|
422
|
+
console.log(
|
|
423
|
+
`\nWrote ${outPath} (${Object.keys(db.components).length} records, ${json.length} bytes)`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function attachDependencies(componentsDir: string, db: PurgeDatabaseV2) {
|
|
428
|
+
const dirs = new Set<string>();
|
|
429
|
+
const classesGlob = new Glob("*/*.classes.ts");
|
|
430
|
+
for await (const relPath of classesGlob.scan({ cwd: componentsDir })) {
|
|
431
|
+
dirs.add(relPath.split(/[\\/]/)[0]);
|
|
432
|
+
}
|
|
433
|
+
const dirToPascal = new Map(
|
|
434
|
+
[...dirs].map((dir) => [dir, kebabToPascal(dir)]),
|
|
435
|
+
);
|
|
436
|
+
const importRegex = /from\s+["']\.\.\/([^/"']+)/g;
|
|
437
|
+
const depGlob = new Glob("**/*.{tsx,ts,js,mjs}");
|
|
438
|
+
|
|
439
|
+
for (const dir of dirs) {
|
|
440
|
+
const component = dirToPascal.get(dir);
|
|
441
|
+
if (!component) continue;
|
|
442
|
+
const componentDeps = new Set<string>();
|
|
443
|
+
const scanDir = joinPath(componentsDir, dir);
|
|
444
|
+
|
|
445
|
+
for await (const relFile of depGlob.scan({ cwd: scanDir })) {
|
|
446
|
+
const code = await Bun.file(joinPath(scanDir, relFile)).text();
|
|
447
|
+
for (const match of code.matchAll(importRegex)) {
|
|
448
|
+
const depDir = match[1];
|
|
449
|
+
if (depDir === "types" || depDir === "utils" || depDir === "..")
|
|
450
|
+
continue;
|
|
451
|
+
const dep = dirToPascal.get(depDir);
|
|
452
|
+
if (dep && dep !== component) componentDeps.add(dep);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (componentDeps.size > 0) {
|
|
457
|
+
const deps = [...componentDeps].sort();
|
|
458
|
+
for (const record of Object.values(db.components)) {
|
|
459
|
+
if (record.component === component) record.deps = deps;
|
|
460
|
+
}
|
|
461
|
+
console.log(` deps ${component}: ${deps.join(", ")}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
219
464
|
}
|
|
220
465
|
|
|
221
466
|
if (import.meta.main) {
|
|
222
|
-
|
|
467
|
+
main();
|
|
223
468
|
}
|