@pathscale/rsbuild-plugin-ui-css-purge 0.9.5 → 0.9.6
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 +145 -145
- package/dist/index.d.ts +1 -1
- package/dist/index.js +18 -32
- package/dist/postbuild-purge.d.ts +1 -1
- package/dist/postbuild-purge.js +18 -32
- package/dist/scan-consumer.d.ts +2 -2
- package/package.json +15 -11
- package/src/generate-manifest.ts +239 -37
package/dist/scan-consumer.d.ts
CHANGED
|
@@ -60,5 +60,5 @@ declare function extractUIImports(ast: unknown): Map<string, string>;
|
|
|
60
60
|
declare function extractJSXUsages(ast: unknown, uiComponents: Map<string, string>): PropUsage[];
|
|
61
61
|
declare function buildSafelists(allUsages: PropUsage[], manifest: PurgeManifest): Safelists;
|
|
62
62
|
declare function scanConsumerSource(srcDir: string): Promise<PropUsage[]>;
|
|
63
|
-
export {
|
|
64
|
-
export
|
|
63
|
+
export type { ComponentPurgeRecord, LegacyComponentManifest as ComponentManifest, LegacyComponentManifest, PropUsage, PurgeDatabaseV2, PurgeManifest, Safelists, };
|
|
64
|
+
export { buildSafelists, extractJSXUsages, extractUIImports, scanConsumerSource, };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pathscale/rsbuild-plugin-ui-css-purge",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -14,20 +14,24 @@
|
|
|
14
14
|
"rsbuild-plugin-ui-css-purge": "dist/postbuild-purge.js",
|
|
15
15
|
"generate-manifest": "src/generate-manifest.ts"
|
|
16
16
|
},
|
|
17
|
-
"files": [
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
"postcss": "^8.5.9"
|
|
22
|
-
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src/generate-manifest.ts"
|
|
20
|
+
],
|
|
23
21
|
"scripts": {
|
|
24
22
|
"build": "bun run build.ts",
|
|
25
|
-
"
|
|
23
|
+
"format": "biome format . --write",
|
|
26
24
|
"lint": "biome check .",
|
|
27
|
-
"
|
|
25
|
+
"test": "bun test"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@swc/core": "^1.15.40",
|
|
29
|
+
"lightningcss": "^1.32.0",
|
|
30
|
+
"postcss": "^8.5.15"
|
|
28
31
|
},
|
|
29
32
|
"devDependencies": {
|
|
30
|
-
"@biomejs/biome": "
|
|
31
|
-
"bun
|
|
33
|
+
"@biomejs/biome": "2.4.16",
|
|
34
|
+
"@types/bun": "^1.3.14",
|
|
35
|
+
"typescript": "^6.0.3"
|
|
32
36
|
}
|
|
33
37
|
}
|
package/src/generate-manifest.ts
CHANGED
|
@@ -10,12 +10,21 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { Glob } from "bun";
|
|
13
|
-
import
|
|
13
|
+
import {
|
|
14
|
+
type AnimationName,
|
|
15
|
+
type AttrSelectorOperator,
|
|
16
|
+
type Combinator,
|
|
17
|
+
type Declaration,
|
|
18
|
+
type Selector,
|
|
19
|
+
type SelectorComponent,
|
|
20
|
+
transform,
|
|
21
|
+
type UnparsedProperty,
|
|
22
|
+
type Visitor,
|
|
23
|
+
type WebKitScrollbarPseudoElement,
|
|
24
|
+
} from "lightningcss";
|
|
14
25
|
|
|
15
26
|
// ── Types ──────────────────────────────────────────────────────────────────────
|
|
16
27
|
|
|
17
|
-
type ClassValue = string | readonly string[];
|
|
18
|
-
|
|
19
28
|
interface ComponentPurgeRecord {
|
|
20
29
|
key: string;
|
|
21
30
|
component: string;
|
|
@@ -70,8 +79,9 @@ interface CssFacts {
|
|
|
70
79
|
|
|
71
80
|
/** Matches Tailwind utility class prefixes — these should NOT appear as owned component classes. */
|
|
72
81
|
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)($|[
|
|
82
|
+
/^(-?)(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)($|[-:[.])/;
|
|
74
83
|
|
|
84
|
+
// TODO: possibly find a more reliable way of checking
|
|
75
85
|
function isTailwindUtility(cls: string): boolean {
|
|
76
86
|
return twPattern.test(cls);
|
|
77
87
|
}
|
|
@@ -196,40 +206,84 @@ function emptyCssFacts(): CssFacts {
|
|
|
196
206
|
};
|
|
197
207
|
}
|
|
198
208
|
|
|
209
|
+
const AnimationNameKeywords = new Set(["none", "initial", "inherit", "unset"]);
|
|
199
210
|
async function scanCssFacts(componentDir: string): Promise<CssFacts> {
|
|
200
211
|
const facts = emptyCssFacts();
|
|
201
212
|
const glob = new Glob("**/*.css");
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
const visitor: Visitor<never> = {
|
|
214
|
+
Rule(rule) {
|
|
215
|
+
if (rule.type === "keyframes") {
|
|
216
|
+
const { name } = rule.value;
|
|
217
|
+
facts.keyframes.declared.push(name.value);
|
|
213
218
|
}
|
|
214
|
-
}
|
|
219
|
+
},
|
|
215
220
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
for (const
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
|
|
221
|
+
Declaration(decl) {
|
|
222
|
+
// "animation" with var(), unparsed due to var() interfering with arg position detection
|
|
223
|
+
const animationNamesUnparsed = (p: UnparsedProperty): AnimationName[] => {
|
|
224
|
+
let valid = true;
|
|
225
|
+
const names: AnimationName[] = [];
|
|
226
|
+
if (p.propertyId.property !== "animation") return names;
|
|
227
|
+
for (const { value: token } of p.value) {
|
|
228
|
+
if (!(typeof token === "object" && "type" in token)) continue;
|
|
229
|
+
if (token.type === "comma") valid = true; // name only appears after comma or at start
|
|
230
|
+
if (token.type === "comma" || token.type === "white-space") continue;
|
|
231
|
+
if (valid && (token.type === "ident" || token.type === "string")) {
|
|
232
|
+
names.push(token);
|
|
226
233
|
}
|
|
234
|
+
valid = false;
|
|
235
|
+
}
|
|
236
|
+
return names;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const animationNames = (decl: Declaration): AnimationName[] => {
|
|
240
|
+
switch (decl.property) {
|
|
241
|
+
case "animation":
|
|
242
|
+
return decl.value.map((item) => item.name);
|
|
243
|
+
case "animation-name":
|
|
244
|
+
return decl.value;
|
|
245
|
+
case "unparsed":
|
|
246
|
+
return animationNamesUnparsed(decl.value);
|
|
247
|
+
default:
|
|
248
|
+
return [];
|
|
227
249
|
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
for (const name of animationNames(decl)) {
|
|
253
|
+
if (name.type === "none") continue;
|
|
254
|
+
if (AnimationNameKeywords.has(name.value)) continue;
|
|
255
|
+
facts.keyframes.referenced.push(name.value);
|
|
228
256
|
}
|
|
229
|
-
});
|
|
230
257
|
|
|
231
|
-
|
|
232
|
-
|
|
258
|
+
// variable declarations
|
|
259
|
+
(() => {
|
|
260
|
+
if (decl.property !== "custom") return;
|
|
261
|
+
if (!decl.value.name.startsWith("--")) return;
|
|
262
|
+
facts.cssVars.declared.push(decl.value.name);
|
|
263
|
+
})();
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
// var() statements
|
|
267
|
+
Variable(variable) {
|
|
268
|
+
facts.cssVars.referenced.push(variable.name.ident);
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
Selector(selector) {
|
|
272
|
+
facts.selectors.push(stringifySelector(selector));
|
|
273
|
+
const attrs = extractSelectorAttrs(selector).map(stringifySelectorAttr);
|
|
274
|
+
facts.attributeSelectors.push(...attrs);
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
for await (const relPath of glob.scan({ cwd: componentDir })) {
|
|
279
|
+
const css = await Bun.file(joinPath(componentDir, relPath)).text();
|
|
280
|
+
transform({
|
|
281
|
+
filename: relPath,
|
|
282
|
+
code: Buffer.from(css),
|
|
283
|
+
minify: false,
|
|
284
|
+
sourceMap: false,
|
|
285
|
+
errorRecovery: true,
|
|
286
|
+
visitor,
|
|
233
287
|
});
|
|
234
288
|
}
|
|
235
289
|
|
|
@@ -247,14 +301,162 @@ async function scanCssFacts(componentDir: string): Promise<CssFacts> {
|
|
|
247
301
|
};
|
|
248
302
|
}
|
|
249
303
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
304
|
+
type SelectorAttribute = Extract<SelectorComponent, { type: "attribute" }>;
|
|
305
|
+
function extractSelectorAttrs(selector: Selector): SelectorAttribute[] {
|
|
306
|
+
const attrs: SelectorAttribute[] = [];
|
|
307
|
+
const pseudo = (c: SelectorComponent) =>
|
|
308
|
+
c.type === "pseudo-class" || c.type === "pseudo-element";
|
|
309
|
+
const extract = (sel: Selector) => {
|
|
310
|
+
const prefixes = ["data-", "aria-"];
|
|
311
|
+
for (const comp of sel) {
|
|
312
|
+
if (comp.type !== "attribute") continue;
|
|
313
|
+
if (prefixes.every((p) => !comp.name.startsWith(p))) continue;
|
|
314
|
+
attrs.push(comp);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// From top level
|
|
319
|
+
extract(selector);
|
|
320
|
+
// From pseudo classes/elements
|
|
321
|
+
const ps = selector.filter(pseudo);
|
|
322
|
+
for (const p of ps) for (const s of extractSelectors(p)) extract(s);
|
|
323
|
+
|
|
324
|
+
return attrs;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
type PseudoClass = Extract<SelectorComponent, { type: "pseudo-class" }>;
|
|
328
|
+
type PseudoElement = Extract<SelectorComponent, { type: "pseudo-element" }>;
|
|
329
|
+
type Pseudo = PseudoClass | PseudoElement;
|
|
330
|
+
function extractSelectors(ps: Pseudo): Selector[] {
|
|
331
|
+
if (ps.kind === "host") return ps.selectors ? [ps.selectors] : [];
|
|
332
|
+
if ("selectors" in ps) return ps.selectors;
|
|
333
|
+
if ("selector" in ps) return [ps.selector];
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function extractStrings(ps: Pseudo): string[] {
|
|
338
|
+
if (Array.isArray(ps.type)) return ps.type;
|
|
339
|
+
if ("languages" in ps) return ps.languages;
|
|
340
|
+
if ("direction" in ps) return [ps.direction];
|
|
341
|
+
if ("state" in ps) return [ps.state];
|
|
342
|
+
if ("names" in ps) return ps.names;
|
|
343
|
+
if ("identifier" in ps) return [ps.identifier];
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const OperatorMap: Record<AttrSelectorOperator, string> = {
|
|
348
|
+
equal: "=",
|
|
349
|
+
includes: "~=",
|
|
350
|
+
"dash-match": "|=",
|
|
351
|
+
prefix: "^=",
|
|
352
|
+
suffix: "$=",
|
|
353
|
+
substring: "*=",
|
|
354
|
+
} as const;
|
|
355
|
+
|
|
356
|
+
const CombinatorMap: Record<Combinator, string> = {
|
|
357
|
+
child: " > ",
|
|
358
|
+
descendant: " ",
|
|
359
|
+
"next-sibling": " + ",
|
|
360
|
+
"later-sibling": " ~ ",
|
|
361
|
+
deep: " /deep/ ",
|
|
362
|
+
"deep-descendant": " >>> ",
|
|
363
|
+
"pseudo-element": "::",
|
|
364
|
+
part: "::part",
|
|
365
|
+
"slot-assignment": "::slotted",
|
|
366
|
+
} as const;
|
|
367
|
+
|
|
368
|
+
const WSElementMap: Record<WebKitScrollbarPseudoElement, string> = {
|
|
369
|
+
scrollbar: "::-webkit-scrollbar",
|
|
370
|
+
button: "::-webkit-scrollbar-button",
|
|
371
|
+
track: "::-webkit-scrollbar-track",
|
|
372
|
+
"track-piece": "::-webkit-scrollbar-track-piece",
|
|
373
|
+
thumb: "::-webkit-scrollbar-thumb",
|
|
374
|
+
corner: "::-webkit-scrollbar-corner",
|
|
375
|
+
resizer: "::-webkit-resizer",
|
|
376
|
+
} as const;
|
|
377
|
+
|
|
378
|
+
const ShortElements: Set<PseudoElement["kind"]> = new Set([
|
|
379
|
+
"before",
|
|
380
|
+
"after",
|
|
381
|
+
"first-letter",
|
|
382
|
+
"first-line",
|
|
383
|
+
]);
|
|
384
|
+
|
|
385
|
+
// TODO: use ToCSS from napi-rs module | use transform with dummy rule | use pure Rust | do not rely on stringified selectors and extract what we need
|
|
386
|
+
function stringifySelector(selector: Selector): string {
|
|
387
|
+
const selectors = (p: Pseudo) =>
|
|
388
|
+
extractSelectors(p).map(stringifySelector).concat(extractStrings(p));
|
|
389
|
+
|
|
390
|
+
return selector
|
|
391
|
+
.map((comp) => {
|
|
392
|
+
switch (comp.type) {
|
|
393
|
+
case "universal":
|
|
394
|
+
return "*";
|
|
395
|
+
case "nesting":
|
|
396
|
+
return "&";
|
|
397
|
+
case "type":
|
|
398
|
+
return comp.name;
|
|
399
|
+
case "id":
|
|
400
|
+
return `#${comp.name}`;
|
|
401
|
+
case "class":
|
|
402
|
+
return `.${comp.name}`;
|
|
403
|
+
case "combinator":
|
|
404
|
+
return CombinatorMap[comp.value];
|
|
405
|
+
case "attribute":
|
|
406
|
+
return stringifySelectorAttr(comp);
|
|
407
|
+
case "namespace":
|
|
408
|
+
if (comp.kind === "named") return `${comp.prefix}|`;
|
|
409
|
+
if (comp.kind === "any") return "*|";
|
|
410
|
+
return "|";
|
|
411
|
+
case "pseudo-class": {
|
|
412
|
+
const s = selectors(comp);
|
|
413
|
+
if (comp.kind === "custom") return `:${comp.name}`;
|
|
414
|
+
if (comp.kind === "webkit-scrollbar") return `:${comp.value}`;
|
|
415
|
+
if ("a" in comp && "b" in comp) return stringifyNth(comp);
|
|
416
|
+
if (s.length === 0) return `:${comp.kind}`;
|
|
417
|
+
return `:${comp.kind}(${s.join(", ")})`;
|
|
418
|
+
}
|
|
419
|
+
case "pseudo-element": {
|
|
420
|
+
const s = selectors(comp);
|
|
421
|
+
const p = ShortElements.has(comp.kind) ? ":" : "::";
|
|
422
|
+
if (comp.kind === "custom") return `${p}${comp.name}`;
|
|
423
|
+
if (comp.kind === "webkit-scrollbar") return WSElementMap[comp.value];
|
|
424
|
+
if (comp.kind === "part") return `${p}${comp.kind}(${s.join(" ")})`;
|
|
425
|
+
if (s.length === 0) return `${p}${comp.kind}`;
|
|
426
|
+
return `${p}${comp.kind}(${s.join(", ")})`;
|
|
427
|
+
}
|
|
428
|
+
default:
|
|
429
|
+
return "";
|
|
430
|
+
}
|
|
431
|
+
})
|
|
432
|
+
.join("");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function stringifySelectorAttr(attr: SelectorAttribute): string {
|
|
436
|
+
const { name, operation } = attr;
|
|
437
|
+
if (!operation) return `[${name}]`;
|
|
438
|
+
const { operator, value } = operation;
|
|
439
|
+
return `[${name}${OperatorMap[operator]}"${value}"]`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
type Nth = Extract<SelectorComponent, { a: number; b: number }>;
|
|
443
|
+
function stringifyNth(nth: Nth): string {
|
|
444
|
+
const signed = (n: number) => `${n >= 0 ? "+" : ""}${n}`;
|
|
445
|
+
const anb = (a: number, b: number): string => {
|
|
446
|
+
if (a === 0 && b === 0) return "0";
|
|
447
|
+
if (a === 1 && b === 0) return "n";
|
|
448
|
+
if (a === -1 && b === 0) return "-n";
|
|
449
|
+
if (b === 0) return `${a}n`;
|
|
450
|
+
if (a === 2 && b === 1) return "odd";
|
|
451
|
+
if (a === 0) return b.toString();
|
|
452
|
+
if (a === 1) return `n${signed(b)}`;
|
|
453
|
+
if (a === -1) return `-n${signed(b)}`;
|
|
454
|
+
return `${a}n${signed(b)}`;
|
|
455
|
+
};
|
|
456
|
+
let rule = anb(nth.a, nth.b);
|
|
457
|
+
const s = (("of" in nth && nth.of) || []).map(stringifySelector);
|
|
458
|
+
if (s.length > 0) rule += ` of ${s.join(", ")}`;
|
|
459
|
+
return `:${nth.kind}(${rule})`;
|
|
258
460
|
}
|
|
259
461
|
|
|
260
462
|
function createRecord(
|