@pre.style/core 3.1.0-alpha.1 → 3.1.0-alpha.2
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 +12 -3
- package/dist/compiler/compiler.d.ts +19 -0
- package/dist/compiler/compiler.js +100 -25
- package/dist/compiler/index.d.ts +1 -1
- package/dist/compiler/selectors.d.ts +3 -0
- package/dist/compiler/selectors.js +38 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pre.style
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@pre.style/core)
|
|
4
4
|
|
|
5
5
|
Zero-runtime CSS-in-JS. Write plain CSS or SCSS next to your markup, and pre.style compiles it at build time into atomic CSS classes: one small class per declaration, shared across your whole project.
|
|
6
6
|
|
|
@@ -252,7 +252,7 @@ const className = cn(button, isDanger && danger);
|
|
|
252
252
|
|
|
253
253
|
Two classes conflict when they set the same property under the same at-rules and selector. A later shorthand also removes earlier longhands it covers.
|
|
254
254
|
|
|
255
|
-
When every argument is a block or a string literal, the plugin resolves the call during the build, so it costs nothing at runtime. Other calls use a small lookup table the plugin generates for your project.
|
|
255
|
+
When every argument is a block or a string literal, the plugin resolves the call during the build, so it costs nothing at runtime. Arguments can also name a top-level `const` holding one of these, or holding another call that's resolved this way (`const base = css`…`;` then `cn(base, …)`). Other calls use a small lookup table the plugin generates for your project.
|
|
256
256
|
|
|
257
257
|
> **Note:** `cn` removes `!important` declarations too. A later class that sets the same property wins, even when the earlier one is `!important`.
|
|
258
258
|
|
|
@@ -608,7 +608,7 @@ preStyle({
|
|
|
608
608
|
| Option | Default | Description |
|
|
609
609
|
| --------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
610
610
|
| `lightningcss` | | [Lightning CSS options](https://lightningcss.dev/transforms.html), such as `minify`, `targets`, `drafts`, and `visitor`. A `visitor` runs before pre.style processes each block. `filename`, `code`, `sourceMap`, `projectRoot`, `inputSourceMap`, and `cssModules` are ignored. |
|
|
611
|
-
| `sourceMap` | `true` in dev, `false` in builds | Generate a source map for the CSS, mapping each class to the first block it appears in. In dev, it's shown in the browser (the plugin turns on Vite's `css.devSourcemap`, unless you set it). In builds, it's written next to `output.file`. When you import `virtual:pre.style.css`, builds have no CSS source map, because Vite doesn't write them for bundled CSS. |
|
|
611
|
+
| `sourceMap` | `true` in dev, `false` in builds | Generate a source map for the CSS, mapping each class to the first block it appears in (a source map can only point to one place; the [DevTools extension](#tracing-classes-with-the-devtools-extension) lists them all). In dev, it's shown in the browser (the plugin turns on Vite's `css.devSourcemap`, unless you set it). In builds, it's written next to `output.file`. When you import `virtual:pre.style.css`, builds have no CSS source map, because Vite doesn't write them for bundled CSS. |
|
|
612
612
|
| `layer` | `false` | Wrap the CSS in a [cascade layer](https://developer.mozilla.org/docs/Web/CSS/@layer): `false` for none, `""` for an unnamed layer, or the layer's name, such as `"pre.style"` or `"vendor.pre-style"`. `@import` and `@namespace` rules stay before the layer, since they can't be inside one. `@layer` rules from blocks become layers nested in it. Browsers without cascade layers (Chrome before 99, Safari before 15.4, Firefox before 97) ignore everything inside it. |
|
|
613
613
|
| `quotes` | `'double-quote'` | Quotes around the class string that replaces each block in JS: `'single-quote'`, `'double-quote'`, or `'backtick'`. |
|
|
614
614
|
| `output.file` | `'pre.style.css'` | Path and name of the CSS file, relative to Vite's `build.outDir`. |
|
|
@@ -619,6 +619,14 @@ The CSS isn't minified unless you set `lightningcss: { minify: true }`.
|
|
|
619
619
|
|
|
620
620
|
Before 3.1, `include`, `exclude`, `prependFiles`, `libraries`, and `quirksMode` were plugin options. Passing them to the plugin now fails with a message pointing to `package.json`.
|
|
621
621
|
|
|
622
|
+
### Tracing classes with the DevTools extension
|
|
623
|
+
|
|
624
|
+
Classes are shared, so one class can come from many blocks, and a source map can only point to one of them. The [pre.style DevTools extension](packages/devtools) adds a **pre.style** tab to the Elements panel in Chrome and Firefox. For the selected element, it lists each pre.style rule like the Styles pane does, with a link to every block that uses it.
|
|
625
|
+
|
|
626
|
+
It works with the Vite dev server while `sourceMap` is on, which is the default in dev. Setting `sourceMap: false` turns it off. It only runs on `localhost`, `*.localhost`, loopback addresses, and hostnames you add in its settings, and sends other sites no requests.
|
|
627
|
+
|
|
628
|
+
Its links open your editor through Vite, which guesses which one you use. When the dev server runs in VS Code's terminal, the plugin sets the `LAUNCH_EDITOR` environment variable to `code` if it isn't set, which also applies to Vite's error overlay. To choose an editor yourself, set `LAUNCH_EDITOR`.
|
|
629
|
+
|
|
622
630
|
### Quirks mode
|
|
623
631
|
|
|
624
632
|
> [!WARNING]
|
|
@@ -668,6 +676,7 @@ The build fails, with the file, line, and column, when:
|
|
|
668
676
|
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
669
677
|
| `@pre.style/core` | `css`, `scss`, and `cn`, the compiler at `@pre.style/core/compiler` for bundler plugins, TypeScript's content mapper for stylesheet files, and the JSON Schema for the `package.json` field. |
|
|
670
678
|
| `@pre.style/vite` | The Vite plugin. |
|
|
679
|
+
| [pre.style DevTools](packages/devtools) | A side pane for the Elements panel in Chrome and Firefox that lists every block using each class. Not published to the extension stores yet. |
|
|
671
680
|
|
|
672
681
|
## Development
|
|
673
682
|
|
|
@@ -37,6 +37,23 @@ export interface CompiledStylesheet {
|
|
|
37
37
|
exports: [name: string, classString: string][];
|
|
38
38
|
dependencies: string[];
|
|
39
39
|
}
|
|
40
|
+
export interface Source {
|
|
41
|
+
file: string;
|
|
42
|
+
line: number;
|
|
43
|
+
column: number;
|
|
44
|
+
}
|
|
45
|
+
export interface ClassUsage {
|
|
46
|
+
className: string;
|
|
47
|
+
selector: string;
|
|
48
|
+
atRules: string[];
|
|
49
|
+
declaration: string;
|
|
50
|
+
property: string;
|
|
51
|
+
covers: string[];
|
|
52
|
+
important: boolean;
|
|
53
|
+
specificity: [ids: number, classes: number, types: number];
|
|
54
|
+
order: number;
|
|
55
|
+
sources: Source[];
|
|
56
|
+
}
|
|
40
57
|
export declare function createCompiler(options?: CompilerOptions): Promise<Compiler>;
|
|
41
58
|
export declare class Compiler {
|
|
42
59
|
#private;
|
|
@@ -48,6 +65,8 @@ export declare class Compiler {
|
|
|
48
65
|
classString(block: Block): string;
|
|
49
66
|
dependencies(block: Block): string[];
|
|
50
67
|
classNames(sheet: Stylesheet): string[];
|
|
68
|
+
usages(classNames: Iterable<string>): ClassUsage[];
|
|
69
|
+
forgetSources(file: string): void;
|
|
51
70
|
stylesheet(sheet: Stylesheet): CompiledStylesheet;
|
|
52
71
|
css(): Printed;
|
|
53
72
|
cnTable(): CnTable;
|
|
@@ -4,9 +4,10 @@ import { transform } from 'lightningcss';
|
|
|
4
4
|
import SweatMap from 'sweatmap';
|
|
5
5
|
import { dropOverridden, parseBlock, PreStyleError, valueOf } from './block.js';
|
|
6
6
|
import { print } from './print.js';
|
|
7
|
-
import { lvhfaRank, withClass } from './selectors.js';
|
|
7
|
+
import { lvhfaRank, specificity, withClass } from './selectors.js';
|
|
8
8
|
import { KEYFRAMES_PREFIX } from './keyframes.js';
|
|
9
9
|
import { MARKER_PREFIX, markerKey, parseStylesheet } from './stylesheet.js';
|
|
10
|
+
import { SHORTHANDS } from '../shorthands.js';
|
|
10
11
|
const RANGES = {
|
|
11
12
|
Digits: { start: '0030', end: '0039' },
|
|
12
13
|
Uppercase: { start: '0041', end: '005A' },
|
|
@@ -155,6 +156,34 @@ export class Compiler {
|
|
|
155
156
|
throw error instanceof PreStyleError ? locate(error, fileBlock(sheet.file)) : error;
|
|
156
157
|
}
|
|
157
158
|
}
|
|
159
|
+
// Where each of the given classes is used, in output order. Classes that
|
|
160
|
+
// aren't in the output are left out. Sources are only kept with `sourceMap`
|
|
161
|
+
// on, and only list what has been compiled so far.
|
|
162
|
+
usages(classNames) {
|
|
163
|
+
const wanted = new Set(classNames);
|
|
164
|
+
const usages = [];
|
|
165
|
+
let order = 0;
|
|
166
|
+
const walk = (group) => {
|
|
167
|
+
for (const record of sortAtoms(group.atoms)) {
|
|
168
|
+
order++;
|
|
169
|
+
if (record.className !== null && wanted.has(record.className))
|
|
170
|
+
usages.push(this.#usage(record, order));
|
|
171
|
+
}
|
|
172
|
+
for (const child of sortGroups([...group.children.values()]))
|
|
173
|
+
walk(child);
|
|
174
|
+
};
|
|
175
|
+
walk(this.#atomGroups());
|
|
176
|
+
return usages;
|
|
177
|
+
}
|
|
178
|
+
// Drops the places a file uses atoms, because it changed. They're added back
|
|
179
|
+
// as its blocks are compiled again. The atoms stay, like the rest of dev's
|
|
180
|
+
// output, which only grows.
|
|
181
|
+
forgetSources(file) {
|
|
182
|
+
for (const record of this.#atoms.values())
|
|
183
|
+
for (const [key, source] of record.sources)
|
|
184
|
+
if (source.file === file)
|
|
185
|
+
record.sources.delete(key);
|
|
186
|
+
}
|
|
158
187
|
// Each class of a stylesheet file, with its class string.
|
|
159
188
|
stylesheet(sheet) {
|
|
160
189
|
const parsed = this.#parseSheet(sheet);
|
|
@@ -391,14 +420,21 @@ export class Compiler {
|
|
|
391
420
|
#register(block, parsed) {
|
|
392
421
|
for (const atom of parsed.atoms) {
|
|
393
422
|
const key = atomKey(atom);
|
|
394
|
-
|
|
395
|
-
|
|
423
|
+
const source = sourceOf(block, atom.position);
|
|
424
|
+
let record = this.#atoms.get(key);
|
|
425
|
+
if (record === undefined) {
|
|
426
|
+
record = {
|
|
396
427
|
atom,
|
|
397
428
|
className: null,
|
|
398
429
|
order: this.#order++,
|
|
399
|
-
source
|
|
400
|
-
|
|
430
|
+
source,
|
|
431
|
+
sources: new Map(),
|
|
432
|
+
printed: null,
|
|
433
|
+
};
|
|
434
|
+
this.#atoms.set(key, record);
|
|
401
435
|
}
|
|
436
|
+
if (source !== null && this.#options.sourceMap)
|
|
437
|
+
record.sources.set(`${source.file}:${source.line}:${source.column}`, source);
|
|
402
438
|
}
|
|
403
439
|
for (const global of parsed.globals) {
|
|
404
440
|
if (this.#globals.has(global.key))
|
|
@@ -420,8 +456,8 @@ export class Compiler {
|
|
|
420
456
|
#nameOf(record) {
|
|
421
457
|
return (record.className ??= this.#names.atoms.set(atomKey(record.atom)));
|
|
422
458
|
}
|
|
423
|
-
|
|
424
|
-
|
|
459
|
+
// Atoms grouped by their at-rules, nesting groups the same way.
|
|
460
|
+
#atomGroups() {
|
|
425
461
|
const root = { segment: null, order: 0, atoms: [], children: new Map() };
|
|
426
462
|
for (const record of this.#atoms.values()) {
|
|
427
463
|
let group = root;
|
|
@@ -435,25 +471,35 @@ export class Compiler {
|
|
|
435
471
|
}
|
|
436
472
|
group.atoms.push(record);
|
|
437
473
|
}
|
|
474
|
+
return root;
|
|
475
|
+
}
|
|
476
|
+
#atomSelectors(record) {
|
|
477
|
+
const { atom } = record;
|
|
478
|
+
const className = this.#nameOf(record);
|
|
479
|
+
const selectors = atom.variant
|
|
480
|
+
? withClass(atom.variant.selectors, className)
|
|
481
|
+
: [[{ type: 'class', name: className }]];
|
|
482
|
+
return atom.variant?.key.includes(MARKER_KEY) ? this.#named(selectors) : selectors;
|
|
483
|
+
}
|
|
484
|
+
#atomRule(record, loc) {
|
|
485
|
+
const { atom } = record;
|
|
486
|
+
const value = atom.declaration.key.includes(KEYFRAMES_KEY)
|
|
487
|
+
? this.#named(atom.declaration.value)
|
|
488
|
+
: atom.declaration.value;
|
|
489
|
+
const declarations = atom.declaration.important
|
|
490
|
+
? { importantDeclarations: [value] }
|
|
491
|
+
: { declarations: [value] };
|
|
492
|
+
return {
|
|
493
|
+
type: 'style',
|
|
494
|
+
value: { selectors: this.#atomSelectors(record), declarations, rules: [], loc },
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
#atomRules(loc) {
|
|
438
498
|
const emit = (group) => {
|
|
439
499
|
const rules = sortAtoms(group.atoms).map((record) => {
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
? withClass(atom.variant.selectors, className)
|
|
444
|
-
: [[{ type: 'class', name: className }]];
|
|
445
|
-
if (atom.variant?.key.includes(MARKER_KEY))
|
|
446
|
-
selectors = this.#named(selectors);
|
|
447
|
-
const value = atom.declaration.key.includes(KEYFRAMES_KEY)
|
|
448
|
-
? this.#named(atom.declaration.value)
|
|
449
|
-
: atom.declaration.value;
|
|
450
|
-
const declarations = atom.declaration.important
|
|
451
|
-
? { importantDeclarations: [value] }
|
|
452
|
-
: { declarations: [value] };
|
|
453
|
-
return {
|
|
454
|
-
type: 'style',
|
|
455
|
-
value: { selectors, declarations, rules: [], loc: loc(source) },
|
|
456
|
-
};
|
|
500
|
+
// The first place it's still used, since `forgetSources` drops places.
|
|
501
|
+
const [source = record.source] = record.sources.values();
|
|
502
|
+
return this.#atomRule(record, loc(source));
|
|
457
503
|
});
|
|
458
504
|
for (const child of sortGroups([...group.children.values()])) {
|
|
459
505
|
rules.push({
|
|
@@ -463,7 +509,36 @@ export class Compiler {
|
|
|
463
509
|
}
|
|
464
510
|
return rules;
|
|
465
511
|
};
|
|
466
|
-
return emit(
|
|
512
|
+
return emit(this.#atomGroups());
|
|
513
|
+
}
|
|
514
|
+
#usage(record, order) {
|
|
515
|
+
const { atom } = record;
|
|
516
|
+
const loc = { source_index: 0, line: 0, column: 1 };
|
|
517
|
+
const rule = this.#atomRule(record, loc);
|
|
518
|
+
// Printed for reading, so without the user's `minify` and `targets`.
|
|
519
|
+
const { drafts, nonStandard, errorRecovery } = this.#parseOptions.lightningcss;
|
|
520
|
+
const text = (rules) => print([rules], [], { drafts, nonStandard, errorRecovery }, false).code;
|
|
521
|
+
const code = (record.printed ??= text(rule));
|
|
522
|
+
const open = code.indexOf(' {\n');
|
|
523
|
+
return {
|
|
524
|
+
className: record.className,
|
|
525
|
+
selector: code.slice(0, open).replaceAll('\n', ' '),
|
|
526
|
+
atRules: atom.atRules.map((segment) => {
|
|
527
|
+
const wrapped = { ...valueOf(segment.rule), rules: [rule] };
|
|
528
|
+
const printed = text({ type: segment.rule.type, value: wrapped });
|
|
529
|
+
return printed.slice(0, printed.indexOf(' {\n'));
|
|
530
|
+
}),
|
|
531
|
+
declaration: code
|
|
532
|
+
.slice(open + 3, code.lastIndexOf('\n}'))
|
|
533
|
+
.trim()
|
|
534
|
+
.replace(/;$/, ''),
|
|
535
|
+
property: atom.property,
|
|
536
|
+
covers: Object.hasOwn(SHORTHANDS, atom.property) ? SHORTHANDS[atom.property] : [],
|
|
537
|
+
important: atom.declaration.important,
|
|
538
|
+
specificity: specificity(this.#atomSelectors(record)),
|
|
539
|
+
order,
|
|
540
|
+
sources: [...record.sources.values()],
|
|
541
|
+
};
|
|
467
542
|
}
|
|
468
543
|
}
|
|
469
544
|
// Markers and localized keyframes names as they appear in JSON keys, which
|
package/dist/compiler/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { Compiler, createCompiler } from './compiler.ts';
|
|
2
|
-
export type { Block, CompiledStylesheet, CompilerOptions, Library, PrependFiles, Stylesheet, } from './compiler.ts';
|
|
2
|
+
export type { Block, ClassUsage, CompiledStylesheet, CompilerOptions, Library, PrependFiles, Source, Stylesheet, } from './compiler.ts';
|
|
3
3
|
export { PreStyleError } from './block.ts';
|
|
4
4
|
export type { Lang, LightningOptions, Position } from './block.ts';
|
|
5
5
|
export type { Printed } from './print.ts';
|
|
@@ -6,3 +6,6 @@ export declare function compose(parent: SelectorList, child: SelectorList): Sele
|
|
|
6
6
|
export declare function withClass(selectors: SelectorList, className: string): SelectorList;
|
|
7
7
|
export declare function lvhfaRank(selectors: SelectorList): number;
|
|
8
8
|
export declare function repair<T>(selectors: T): T;
|
|
9
|
+
type Specificity = [ids: number, classes: number, types: number];
|
|
10
|
+
export declare function specificity(selectors: SelectorList): Specificity;
|
|
11
|
+
export {};
|
|
@@ -128,3 +128,41 @@ export function repair(selectors) {
|
|
|
128
128
|
};
|
|
129
129
|
return fix(selectors);
|
|
130
130
|
}
|
|
131
|
+
// A selector list's specificity: its most specific selector's.
|
|
132
|
+
export function specificity(selectors) {
|
|
133
|
+
let best = [0, 0, 0];
|
|
134
|
+
for (const selector of selectors) {
|
|
135
|
+
const current = ofSelector(selector);
|
|
136
|
+
const difference = current[0] - best[0] || current[1] - best[1] || current[2] - best[2];
|
|
137
|
+
if (difference > 0)
|
|
138
|
+
best = current;
|
|
139
|
+
}
|
|
140
|
+
return best;
|
|
141
|
+
}
|
|
142
|
+
function ofSelector(selector) {
|
|
143
|
+
const total = [0, 0, 0];
|
|
144
|
+
const add = (other) => {
|
|
145
|
+
total[0] += other[0];
|
|
146
|
+
total[1] += other[1];
|
|
147
|
+
total[2] += other[2];
|
|
148
|
+
};
|
|
149
|
+
for (const component of selector) {
|
|
150
|
+
if (component.type === 'id')
|
|
151
|
+
total[0]++;
|
|
152
|
+
else if (component.type === 'class' || component.type === 'attribute')
|
|
153
|
+
total[1]++;
|
|
154
|
+
else if (component.type === 'type' || component.type === 'pseudo-element')
|
|
155
|
+
total[2]++;
|
|
156
|
+
else if (component.type === 'pseudo-class' && component.kind !== 'where') {
|
|
157
|
+
// `:is()`, `:not()`, and `:has()` count as their most specific argument.
|
|
158
|
+
if (component.selectors)
|
|
159
|
+
add(specificity(component.selectors));
|
|
160
|
+
else {
|
|
161
|
+
total[1]++;
|
|
162
|
+
if (component.of)
|
|
163
|
+
add(specificity(component.of));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return total;
|
|
168
|
+
}
|