@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # pre.style
2
2
 
3
- [![npm version](https://badge.fury.io/js/pre.style.svg)](https://www.npmjs.com/package/@pre.style/core)
3
+ [![npm version](https://img.shields.io/npm/v/@pre.style/core/next)](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
- if (!this.#atoms.has(key)) {
395
- this.#atoms.set(key, {
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: sourceOf(block, atom.position),
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
- #atomRules(loc) {
424
- // Group atoms by their at-rules, nesting groups the same way.
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
- const { atom, source } = record;
441
- const className = this.#nameOf(record);
442
- let selectors = atom.variant
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(root);
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
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pre.style/core",
3
- "version": "3.1.0-alpha.1",
3
+ "version": "3.1.0-alpha.2",
4
4
  "description": "Zero-runtime CSS-in-JS that compiles css and scss blocks into atomic CSS classes.",
5
5
  "keywords": [
6
6
  "atomic-css",