@pre.style/core 3.1.0-alpha.4 → 3.1.0-alpha.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 CHANGED
@@ -759,7 +759,7 @@ Namespace imports still let bundlers drop the classes you don't use. **Remove th
759
759
 
760
760
  ## Sharing values with prepended files
761
761
 
762
- Use `prependFiles`, in the `pre.style` field of your `package.json`, to add files to the start of every block and stylesheet file, for custom properties, SCSS variables, mixins, and so on. Each file is read once per build.
762
+ Use `prependFiles`, in the `pre.style` field of your `package.json`, to add files to the start of every block and stylesheet file, for custom properties, SCSS variables, mixins, and so on. Each file is read once per build. In dev, saving one updates every block and stylesheet file it's prepended to. Changes to the `pre.style` field itself need a restart of the dev server, which the plugin warns about.
763
763
 
764
764
  ```json
765
765
  {
@@ -922,13 +922,40 @@ Expanding means some shorthands produce many classes. For example, `font` also r
922
922
 
923
923
  ### Order
924
924
 
925
- Within one block, a later declaration overrides an earlier one, as in plain CSS. Across blocks, the CSS file is ordered so the usual rules hold:
925
+ Within one block, a later declaration overrides an earlier one, as in plain CSS (see [Fallbacks](#fallbacks) for the same property twice in a row). Across blocks, the CSS file is ordered so the usual rules hold:
926
926
 
927
927
  1. Hoisted at-rules (`@layer` statements, `@import`, `@namespace`), then other copied at-rules. With the `layer` option, everything but `@import` and `@namespace` is inside the layer.
928
928
  2. Classes on the element.
929
929
  3. Selector variants, with pseudo-classes in LVHFA order (`:link`, `:visited`, `:hover`, `:focus`, `:active`).
930
930
  4. At-rule groups. `@media` queries with only a `min-width` come last, smallest first (mobile-first).
931
931
 
932
+ ### Fallbacks
933
+
934
+ Declaring the same property more than once in a rule keeps the earlier values as fallbacks, for browsers that can't parse the later ones. They stay together in one class:
935
+
936
+ ```ts
937
+ css`
938
+ height: 100%;
939
+ height: 100svh;
940
+ `;
941
+ ```
942
+
943
+ ```css
944
+ .A {
945
+ height: 100%;
946
+ height: 100svh;
947
+ }
948
+ ```
949
+
950
+ Lightning CSS drops the fallbacks your [`targets`](#configuration) don't need, so set `targets` to keep them. Without `targets`, it assumes current browsers and writes only `height: 100svh` (which then shares a class with any other `height: 100svh`). Values it can't parse, such as `calc-size()`, always keep their fallbacks.
951
+
952
+ A few things aren't fallbacks:
953
+
954
+ - **A declaration in another rule**, such as `height: 100%; & { height: 100svh; }`, overrides the earlier one, since separate rules become separate classes.
955
+ - **A value with `var()`, `env()`, or `${…}`** replaces the declarations before it. Browsers only check these values when they're used, so they never fall back: an invalid value, or a `${…}` that is `null`, makes the property `unset`. To fall back from `${…}`, use a default (`height: var(${h}, 100%)`). A `${…}` followed by a static value is a real fallback (`height: ${h}; height: 100svh`).
956
+ - **Custom properties**, which accept any value.
957
+ - **Shorthands** are expanded into longhands, so `background: red; background: linear-gradient(…)` loses the `red`: the gradient's `background-color` (transparent) is a valid value in every browser.
958
+
932
959
  ## Compared to utility-first frameworks
933
960
 
934
961
  Utility-first frameworks such as [Tailwind CSS](https://tailwindcss.com/) also produce atomic CSS, so the same styles cost about the same in either. The differences are elsewhere:
@@ -1006,6 +1033,7 @@ preStyle({
1006
1033
  | --------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1007
1034
  | `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. |
1008
1035
  | `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. |
1036
+ | `sourceAttributes` | `true` in dev, except in test runs | In dev, mark each `className`/`class` attribute in JSX and Astro components with where it's written (as `data-pre-style-at` on elements and `data-pre-style-from` on components), so the [DevTools extension](#tracing-classes-with-the-devtools-extension) can link an element to the line that set its classes. Needs `sourceMap`. Off by default in Vite's `test` mode (as in Vitest), so rendered markup and snapshots don't change; set it to `true` to have them there too. Builds never have the attributes. |
1009
1037
  | `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. |
1010
1038
  | `quotes` | `'double-quote'` | Quotes around the class string that replaces each block in JS: `'single-quote'`, `'double-quote'`, or `'backtick'`. |
1011
1039
  | `output.file` | `'pre.style.css'` | Path and name of the CSS file, relative to Vite's `build.outDir`. |
@@ -1020,7 +1048,15 @@ Before 3.1, `include`, `exclude`, `prependFiles`, `libraries`, and `quirksMode`
1020
1048
 
1021
1049
  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.
1022
1050
 
1023
- 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.
1051
+ It works with the Vite dev server while `sourceMap` is on, which is the default in dev. Setting `sourceMap: false` turns it off.
1052
+
1053
+ It also links to where the element got its classes: the `className` or `class` attribute on the element, and the one on the component it was passed to, when the component passes its props on. For this, the plugin adds `data-pre-style-at` and `data-pre-style-from` attributes to your JSX and Astro components in dev (not in builds or test runs), which you'll see in the Elements panel. If you'd rather not see them, turn them off with the `sourceAttributes` option.
1054
+
1055
+ ```ts
1056
+ preStyle({ sourceAttributes: false });
1057
+ ```
1058
+
1059
+ It only runs on `localhost`, `*.localhost`, loopback addresses, and hostnames you add in its settings, and sends other sites no requests.
1024
1060
 
1025
1061
  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`.
1026
1062
 
@@ -16,15 +16,17 @@ export interface Atom {
16
16
  key: string;
17
17
  selectors: SelectorList;
18
18
  } | null;
19
- declaration: {
20
- key: string;
21
- value: Declaration;
22
- important: boolean;
23
- };
19
+ declaration: AtomDeclaration;
24
20
  property: string;
25
21
  shorthand: boolean;
26
22
  position: Position | null;
27
23
  }
24
+ export interface AtomDeclaration {
25
+ key: string;
26
+ values: Declaration[];
27
+ important: boolean;
28
+ }
29
+ export declare const atomDeclaration: (values: Declaration[], important: boolean) => AtomDeclaration;
28
30
  export interface GlobalRule {
29
31
  key: string;
30
32
  rule: Rule;
@@ -77,6 +79,7 @@ export interface ParseOptions {
77
79
  export declare const key: (value: unknown) => string;
78
80
  export declare function parseBlock(code: string, lang: Lang, options: ParseOptions): ParsedBlock;
79
81
  export declare function parseRules(css: string, lightningcss: LightningOptions, position: ToPosition, collect: (rules: Rule[]) => void): void;
82
+ export declare function pruneFallbacks(atoms: Atom[], lightningcss: LightningOptions): Atom[];
80
83
  export declare function dropOverridden(atoms: Atom[]): Atom[];
81
84
  export type ToPosition = (loc: {
82
85
  line: number;
@@ -5,9 +5,16 @@ import { localizeCustomProperties } from './custom-properties.js';
5
5
  import { SLOT_PREFIX } from './interpolate.js';
6
6
  import { localizeKeyframes } from './keyframes.js';
7
7
  import { compose, fromPlaceholder, isElement, PLACEHOLDER, repair } from './selectors.js';
8
+ import { print } from './print.js';
8
9
  import { splitBlock } from './split.js';
9
10
  import { extractVariables } from './variables.js';
10
11
  import { SHORTHANDS } from '../shorthands.js';
12
+ // An atom's declaration. A single declaration's key is its own key.
13
+ export const atomDeclaration = (values, important) => ({
14
+ key: key(values.length === 1 ? values[0] : values) + (important ? '!' : ''),
15
+ values,
16
+ important,
17
+ });
11
18
  export class PreStyleError extends Error {
12
19
  position;
13
20
  constructor(message, position, options) {
@@ -86,7 +93,7 @@ export function parseBlock(code, lang, options) {
86
93
  // Sass output positions don't match the block, so scss atoms have none.
87
94
  const position = (loc) => lang === 'css' && loc ? toBlock(loc.line, loc.column) : null;
88
95
  parseRules(css, options.lightningcss, position, (rules) => walk(rules, [], null, parsed, position));
89
- let atoms = dropOverridden(parsed.atoms);
96
+ let atoms = dropOverridden(pruneFallbacks(parsed.atoms, options.lightningcss));
90
97
  let { globals } = parsed;
91
98
  if (options.customPropertyLocalization) {
92
99
  const local = localizeCustomProperties(atoms, globals, `block\n${code}`, lang, options);
@@ -127,6 +134,60 @@ export function parseRules(css, lightningcss, position, collect) {
127
134
  throw new PreStyleError(`Invalid CSS: ${error.message}`, loc ? position({ line: loc.line - 1, column: loc.column }) : null, { cause: error });
128
135
  }
129
136
  }
137
+ // Drops the fallbacks in each stack that Lightning CSS wouldn't print for the
138
+ // user's targets, so a stack that collapses to one declaration shares its
139
+ // class. Values Lightning CSS can't parse (such as `calc-size()`) are kept.
140
+ // Printing and parsing back changes some values' trees, so the round trip
141
+ // only decides which of the original declarations are kept.
142
+ export function pruneFallbacks(atoms, lightningcss) {
143
+ if (atoms.every((atom) => atom.declaration.values.length === 1))
144
+ return atoms;
145
+ const { visitor: _visitor, minify: _minify, ...options } = lightningcss;
146
+ // The stack as Lightning CSS prints it, parsed back, as keys. Declarations
147
+ // it adds for other properties (vendor prefixes) are left out.
148
+ const printed = (stack, important, property) => {
149
+ const block = important ? { importantDeclarations: stack } : { declarations: stack };
150
+ const rule = {
151
+ type: 'style',
152
+ value: {
153
+ selectors: [[{ type: 'class', name: 'x' }]],
154
+ declarations: block,
155
+ rules: [],
156
+ loc: { source_index: 0, line: 0, column: 1 },
157
+ },
158
+ };
159
+ let values = [];
160
+ parseRules(print([rule], [], options, false).code, options, () => null, (rules) => {
161
+ const parsed = valueOf(rules[0]).declarations ?? {};
162
+ values = (important ? parsed.importantDeclarations : parsed.declarations) ?? [];
163
+ });
164
+ return values.filter((value) => propertyOf(value).property === property).map(key);
165
+ };
166
+ return atoms.map((atom) => {
167
+ const { values: stack, important } = atom.declaration;
168
+ if (stack.length === 1)
169
+ return atom;
170
+ let kept;
171
+ try {
172
+ const remaining = printed(stack, important, atom.property);
173
+ if (remaining.length === stack.length)
174
+ return atom;
175
+ kept = stack.filter((value) => {
176
+ const own = printed([value], important, atom.property).at(-1);
177
+ if (own !== remaining[0])
178
+ return false;
179
+ remaining.shift();
180
+ return true;
181
+ });
182
+ }
183
+ catch {
184
+ return atom;
185
+ }
186
+ if (kept.length === 0)
187
+ return atom;
188
+ return { ...atom, declaration: atomDeclaration(kept, important) };
189
+ });
190
+ }
130
191
  // Within a block, a later declaration overrides an earlier one that sets the
131
192
  // same property under the same at-rules and selector variant, as in CSS.
132
193
  // `!important` declarations are collected after normal ones in each rule,
@@ -225,24 +286,43 @@ export function addDeclarations(block, atRules, selectors, atoms, composes, posi
225
286
  composes.push(parseComposes(d, position));
226
287
  normal = normal.slice(own);
227
288
  }
289
+ // Declarations of the same property in this list are stacked, in order, as
290
+ // fallbacks, at the place of the last one. A declaration that always parses
291
+ // replaces the stack, since nothing before it can ever apply.
228
292
  const add = (declarations = [], important) => {
293
+ const stacks = new Map();
294
+ const list = [];
229
295
  for (const declaration of declarations) {
230
296
  for (const value of expand(declaration)) {
231
297
  const { property, shorthand } = propertyOf(value);
232
- atoms.push({
233
- atRules,
234
- variant,
235
- declaration: { key: key(value) + (important ? '!' : ''), value, important },
236
- property,
237
- shorthand,
238
- position,
239
- });
298
+ const stack = stacks.get(property);
299
+ if (stack)
300
+ list.splice(list.findIndex((item) => item.values === stack), 1);
301
+ const values = stack && !alwaysParses(value, property) ? [...stack, value] : [value];
302
+ stacks.set(property, values);
303
+ list.push({ property, shorthand, values });
240
304
  }
241
305
  }
306
+ for (const { property, shorthand, values } of list)
307
+ atoms.push({
308
+ atRules,
309
+ variant,
310
+ declaration: atomDeclaration(values, important),
311
+ property,
312
+ shorthand,
313
+ position,
314
+ });
242
315
  };
243
316
  add(normal, false);
244
317
  add(block?.importantDeclarations, true);
245
318
  }
319
+ // Whether a browser always parses a declaration, so it can't fall back to an
320
+ // earlier one: custom properties take any value, and `var()` and `env()`
321
+ // (including `${…}`, which becomes a `var()`) are only checked once they're
322
+ // substituted, when an invalid value makes the property `unset`.
323
+ function alwaysParses(value, property) {
324
+ return property.startsWith('--') || /"type":"(?:var|env)"/.test(key(value));
325
+ }
246
326
  const ident = (t) => t?.type === 'token' && t.value.type === 'ident' ? t.value.value : null;
247
327
  // Reads `composes: a b`, `composes: a from './file.css'`, or
248
328
  // `composes: a from global`.
@@ -83,6 +83,8 @@ export declare class Compiler {
83
83
  classNames(sheet: Stylesheet): string[];
84
84
  usages(classNames: Iterable<string>): ClassUsage[];
85
85
  forgetSources(file: string): void;
86
+ prependFiles(): string[];
87
+ fileChanged(file: string): boolean;
86
88
  stylesheet(sheet: Stylesheet): CompiledStylesheet;
87
89
  css(): Printed;
88
90
  cnTable(): CnTable;
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
2
2
  import { dirname, relative, resolve as resolvePath, sep } from 'node:path';
3
3
  import { transform } from 'lightningcss';
4
4
  import SweatMap from 'sweatmap';
5
- import { dropOverridden, parseBlock, PreStyleError, valueOf } from './block.js';
5
+ import { atomDeclaration, dropOverridden, parseBlock, PreStyleError, valueOf } from './block.js';
6
6
  import { interpolate } from './interpolate.js';
7
7
  import { print } from './print.js';
8
8
  import { lvhfaRank, specificity, withClass } from './selectors.js';
@@ -171,9 +171,10 @@ export class Compiler {
171
171
  };
172
172
  });
173
173
  }
174
- // The files a block's class string depends on, through `composes … from`.
174
+ // The files a block's class string depends on: its prepended files, and
175
+ // files it composes from.
175
176
  dependencies(block) {
176
- const dependencies = [];
177
+ const dependencies = [...this.#library(block.file).prependFiles[block.lang]];
177
178
  this.#resolveBlock(block, this.#parse(block), dependencies);
178
179
  return [...new Set(dependencies)];
179
180
  }
@@ -218,10 +219,45 @@ export class Compiler {
218
219
  if (source.file === file)
219
220
  record.sources.delete(key);
220
221
  }
222
+ // Every prepended file, the app's and each library's, as absolute paths.
223
+ prependFiles() {
224
+ return [...new Set(this.#prepends().flatMap(({ files: { css, scss } }) => [...css, ...scss]))];
225
+ }
226
+ // Call when a file changes. Drops the places it uses atoms (see
227
+ // `forgetSources`) and, if it's a prepended file, reads it again, so blocks
228
+ // and stylesheet files compiled after this use its new code. Returns whether
229
+ // it was a prepended file.
230
+ fileChanged(file) {
231
+ this.forgetSources(file);
232
+ let changed = false;
233
+ for (const prepended of this.#prepends()) {
234
+ if (![...prepended.files.css, ...prepended.files.scss].includes(file))
235
+ continue;
236
+ try {
237
+ prepended.code = readPrependCode(prepended.files);
238
+ }
239
+ catch (error) {
240
+ // Kept as it was, until the file can be read again.
241
+ this.#parseOptions.warn(`Can't read ${file}, a prepended file, so blocks use its last contents: ${error.message}`);
242
+ continue;
243
+ }
244
+ changed = true;
245
+ }
246
+ if (changed) {
247
+ // Parsed with the old code. Cache keys hold the code they parsed, but
248
+ // not the code prepended to it.
249
+ this.#blocks.clear();
250
+ this.#stylesheets.clear();
251
+ }
252
+ return changed;
253
+ }
221
254
  // Each class of a stylesheet file, with its class string.
222
255
  stylesheet(sheet) {
223
256
  const parsed = this.#parseSheet(sheet);
224
- const dependencies = [...parsed.dependencies];
257
+ const dependencies = [
258
+ ...this.#library(sheet.file).prependFiles[sheet.lang],
259
+ ...parsed.dependencies,
260
+ ];
225
261
  const exports = [];
226
262
  for (const name of parsed.classes.keys())
227
263
  exports.push([
@@ -287,6 +323,10 @@ export class Compiler {
287
323
  variables[this.#names.variables.set(key)] = property ? this.#propertyName(property) : '';
288
324
  return variables;
289
325
  }
326
+ // The app's prepended files, then each library's.
327
+ #prepends() {
328
+ return [this.#prepend, ...this.#libraries.map((library) => library.prepend)];
329
+ }
290
330
  // A library's blocks and files get its prepended files and keyframes
291
331
  // setting instead of the app's.
292
332
  #library(file) {
@@ -300,9 +340,10 @@ export class Compiler {
300
340
  : this.#libraries[library];
301
341
  return {
302
342
  library,
343
+ prependFiles: prepend.files,
303
344
  options: {
304
345
  ...this.#parseOptions,
305
- prepend,
346
+ prepend: prepend.code,
306
347
  keyframeLocalization: keyframeLocalization ?? false,
307
348
  customPropertyLocalization: customPropertyLocalization ?? false,
308
349
  },
@@ -544,12 +585,12 @@ export class Compiler {
544
585
  }
545
586
  #atomRule(record, loc) {
546
587
  const { atom } = record;
547
- const value = hasGenerated(atom.declaration.key)
548
- ? this.#named(atom.declaration.value)
549
- : atom.declaration.value;
588
+ const values = hasGenerated(atom.declaration.key)
589
+ ? this.#named(atom.declaration.values)
590
+ : atom.declaration.values;
550
591
  const declarations = atom.declaration.important
551
- ? { importantDeclarations: [value] }
552
- : { declarations: [value] };
592
+ ? { importantDeclarations: values }
593
+ : { declarations: values };
553
594
  return {
554
595
  type: 'style',
555
596
  value: { selectors: this.#atomSelectors(record), declarations, rules: [], loc },
@@ -574,6 +615,20 @@ export class Compiler {
574
615
  };
575
616
  return emit(this.#atomGroups());
576
617
  }
618
+ // An atom's rule as text. Without the user's targets, Lightning CSS would
619
+ // drop a stack's fallbacks, so each declaration is printed on its own.
620
+ #printedStack(record, rule, text) {
621
+ const code = text(rule);
622
+ const { values, important } = record.atom.declaration;
623
+ if (values.length === 1)
624
+ return code;
625
+ const lines = values.map((value) => {
626
+ const atom = { ...record.atom, declaration: atomDeclaration([value], important) };
627
+ const own = text(this.#atomRule({ ...record, atom }, valueOf(rule).loc));
628
+ return own.slice(own.indexOf(' {\n') + 3, own.lastIndexOf('\n}'));
629
+ });
630
+ return `${code.slice(0, code.indexOf(' {\n'))} {\n${lines.join('\n')}\n}`;
631
+ }
577
632
  #usage(record, order) {
578
633
  const { atom } = record;
579
634
  const loc = { source_index: 0, line: 0, column: 1 };
@@ -581,7 +636,7 @@ export class Compiler {
581
636
  // Printed for reading, so without the user's `minify` and `targets`.
582
637
  const { drafts, nonStandard, errorRecovery } = this.#parseOptions.lightningcss;
583
638
  const text = (rules) => print([rules], [], { drafts, nonStandard, errorRecovery }, false).code;
584
- const code = (record.printed ??= text(rule));
639
+ const code = (record.printed ??= this.#printedStack(record, rule, text));
585
640
  const open = code.indexOf(' {\n');
586
641
  return {
587
642
  className: record.className,
@@ -595,7 +650,9 @@ export class Compiler {
595
650
  declaration: code
596
651
  .slice(open + 3, code.lastIndexOf('\n}'))
597
652
  .trim()
598
- .replace(/;$/, ''),
653
+ .replace(/;$/, '')
654
+ // Fallbacks, on one line.
655
+ .replaceAll(/;\n\s*/g, '; '),
599
656
  property: this.#propertyName(atom.property),
600
657
  covers: Object.hasOwn(SHORTHANDS, atom.property) ? SHORTHANDS[atom.property] : [],
601
658
  important: atom.declaration.important,
@@ -645,13 +702,19 @@ function layerRule(layer) {
645
702
  }
646
703
  // A stylesheet file, for positions and source maps.
647
704
  const fileBlock = (file) => ({ code: '', lang: 'css', file, line: 1, column: 1 });
648
- // Reads prepended files, relative to `dir`, once.
705
+ // Reads prepended files, relative to `dir`.
649
706
  function readPrepend(dir, files) {
650
- const read = (list = []) => list.map((file) => readFileSync(resolvePath(dir, file), 'utf8')).join('\n');
651
- return Array.isArray(files)
652
- ? { css: read(files), scss: read(files) }
653
- : { css: read(files?.css), scss: read(files?.scss) };
707
+ const resolve = (list = []) => list.map((file) => resolvePath(dir, file));
708
+ const paths = Array.isArray(files)
709
+ ? { css: resolve(files), scss: resolve(files) }
710
+ : { css: resolve(files?.css), scss: resolve(files?.scss) };
711
+ return { files: paths, code: readPrependCode(paths) };
654
712
  }
713
+ const readFiles = (files) => files.map((file) => readFileSync(file, 'utf8')).join('\n');
714
+ const readPrependCode = (files) => ({
715
+ css: readFiles(files.css),
716
+ scss: readFiles(files.scss),
717
+ });
655
718
  const count = (map, key) => map.set(key, (map.get(key) ?? 0) + 1);
656
719
  // The at-rules and selector variant an atom is under. Empty for atoms on the
657
720
  // element.
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { key, overrideKey, parseRules, valueOf, walk } from './block.js';
2
+ import { atomDeclaration, key, overrideKey, parseRules, valueOf, walk } from './block.js';
3
3
  import { VARIABLE_PREFIX } from './variables.js';
4
4
  // With `customPropertyLocalization`, a custom property that a block or
5
5
  // stylesheet file declares gets a generated name, used only in that block or
@@ -103,7 +103,7 @@ export function localizeCustomProperties(atoms, globals, scope, lang, options) {
103
103
  local.set(name, VARIABLE_PREFIX + hash);
104
104
  };
105
105
  for (const atom of atoms)
106
- eachDeclared(atom.declaration.value, declared);
106
+ eachDeclared(atom.declaration.values, declared);
107
107
  const own = (global) => !shared.keys.has(global.key);
108
108
  for (const global of globals)
109
109
  if (own(global))
@@ -125,23 +125,19 @@ export function localizeCustomProperties(atoms, globals, scope, lang, options) {
125
125
  };
126
126
  }),
127
127
  atoms: (list) => list.map((atom) => {
128
- const value = rename(atom.declaration.value, local);
128
+ const values = rename(atom.declaration.values, local);
129
129
  const atRules = atom.atRules.map((segment) => {
130
130
  const rule = rename(segment.rule, local);
131
131
  return rule === segment.rule ? segment : { key: key(valueKey(rule)), rule };
132
132
  });
133
- const unchanged = value === atom.declaration.value && atRules.every((s, i) => s === atom.atRules[i]);
133
+ const unchanged = values === atom.declaration.values && atRules.every((s, i) => s === atom.atRules[i]);
134
134
  if (unchanged)
135
135
  return atom;
136
136
  return {
137
137
  ...atom,
138
138
  atRules,
139
139
  property: local.get(atom.property) ?? atom.property,
140
- declaration: {
141
- ...atom.declaration,
142
- key: key(value) + (atom.declaration.important ? '!' : ''),
143
- value,
144
- },
140
+ declaration: atomDeclaration(values, atom.declaration.important),
145
141
  };
146
142
  }),
147
143
  };
@@ -6,6 +6,6 @@ export type { Slot } from './interpolate.ts';
6
6
  export type { Printed } from './print.ts';
7
7
  export { checkField, readConfig } from './config.ts';
8
8
  export type { PackageConfig } from './config.ts';
9
- export { findLibraries } from './libraries.ts';
9
+ export { configFiles, findLibraries } from './libraries.ts';
10
10
  export type { LibraryConfig, ResolvedLibrary } from './libraries.ts';
11
11
  export { isIdentifier } from './stylesheet.ts';
@@ -1,5 +1,5 @@
1
1
  export { Compiler, createCompiler, interpolatedBlock } from './compiler.js';
2
2
  export { PreStyleError } from './block.js';
3
3
  export { checkField, readConfig } from './config.js';
4
- export { findLibraries } from './libraries.js';
4
+ export { configFiles, findLibraries } from './libraries.js';
5
5
  export { isIdentifier } from './stylesheet.js';
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { key, overrideKey, valueOf } from './block.js';
2
+ import { atomDeclaration, key, overrideKey, valueOf } from './block.js';
3
3
  // With `keyframeLocalization`, each `@keyframes` name is replaced with a
4
4
  // generated one, like styled-components' `keyframes`. In parsed rules, a
5
5
  // local name is an ident with this prefix and a hash of every definition of
@@ -55,17 +55,10 @@ export function localizeKeyframes(globals) {
55
55
  atoms: (atoms) => atoms.map((atom) => {
56
56
  if (!ANIMATION_PROPERTIES.has(atom.property.replace(/^-\w+-/, '')))
57
57
  return atom;
58
- const value = renameIdents(atom.declaration.value, local);
59
- if (value === atom.declaration.value)
58
+ const values = renameIdents(atom.declaration.values, local);
59
+ if (values === atom.declaration.values)
60
60
  return atom;
61
- return {
62
- ...atom,
63
- declaration: {
64
- ...atom.declaration,
65
- key: key(value) + (atom.declaration.important ? '!' : ''),
66
- value,
67
- },
68
- };
61
+ return { ...atom, declaration: atomDeclaration(values, atom.declaration.important) };
69
62
  }),
70
63
  };
71
64
  }
@@ -16,3 +16,6 @@ export interface ResolvedLibrary {
16
16
  customPropertyLocalization?: boolean;
17
17
  }
18
18
  export declare function findLibraries(root: string, overrides?: Record<string, LibraryConfig | boolean>, warn?: (message: string) => void): ResolvedLibrary[];
19
+ export declare const configFiles: (appDir: string, libraries: {
20
+ dir: string;
21
+ }[]) => string[];
@@ -81,3 +81,9 @@ export function findLibraries(root, overrides = {}, warn = () => { }) {
81
81
  visit(name, root);
82
82
  return libraries;
83
83
  }
84
+ // The package.json files whose `pre.style` fields configure the compiler: the
85
+ // app's and each library's.
86
+ export const configFiles = (appDir, libraries) => [
87
+ join(appDir, 'package.json'),
88
+ ...libraries.map(({ dir }) => join(dir, 'package.json')),
89
+ ];
@@ -10,17 +10,16 @@
10
10
  // "contentMappers": [{ "package": "@pre.style/core", "extensions": [".css", ".scss"] }]
11
11
  import { createHash } from 'node:crypto';
12
12
  import { existsSync, readFileSync } from 'node:fs';
13
- import { dirname, join, resolve } from 'node:path';
13
+ import { dirname } from 'node:path';
14
14
  import { PreStyleError } from './block.js';
15
15
  import { createCompiler } from './compiler.js';
16
16
  import { readConfig } from './config.js';
17
- import { findLibraries } from './libraries.js';
17
+ import { configFiles, findLibraries } from './libraries.js';
18
18
  import { isIdentifier } from './stylesheet.js';
19
19
  // Each open project's compiler, by its handle.
20
20
  const projects = new Map();
21
21
  // Diagnostic codes, for errors in a stylesheet file.
22
22
  const INVALID_STYLESHEET = 9001;
23
- const prependPaths = (dir, files) => (Array.isArray(files) ? files : [...(files?.css ?? []), ...(files?.scss ?? [])]).map((file) => resolve(dir, file));
24
23
  async function openProject(params) {
25
24
  // The project's package.json, nearest its tsconfig.json.
26
25
  const root = params.configFileName ? dirname(params.configFileName) : process.cwd();
@@ -41,15 +40,10 @@ async function openProject(params) {
41
40
  warn: () => { },
42
41
  });
43
42
  projects.set(params.projectHandle, compiler);
44
- await compiler;
45
43
  // Changes to these files change which classes files export.
46
44
  const watchedFiles = [
47
- join(app.dir, 'package.json'),
48
- ...prependPaths(app.dir, app.config.prependFiles),
49
- ...libraries.flatMap(({ dir, prependFiles }) => [
50
- join(dir, 'package.json'),
51
- ...prependPaths(dir, prependFiles),
52
- ]),
45
+ ...configFiles(app.dir, libraries),
46
+ ...(await compiler).prependFiles(),
53
47
  ].filter((file) => existsSync(file));
54
48
  const hash = createHash('sha256');
55
49
  for (const file of watchedFiles)
@@ -1,6 +1,6 @@
1
1
  import { pathToFileURL, fileURLToPath } from 'node:url';
2
2
  import { atRuleInfo } from './at-rules.js';
3
- import { addDeclarations, addGlobal, dropOverridden, key, parseRules, PreStyleError, valueOf, walk, } from './block.js';
3
+ import { addDeclarations, addGlobal, dropOverridden, pruneFallbacks, key, parseRules, PreStyleError, valueOf, walk, } from './block.js';
4
4
  import { localizeCustomProperties } from './custom-properties.js';
5
5
  import { localizeKeyframes } from './keyframes.js';
6
6
  import { compose, isElement } from './selectors.js';
@@ -145,7 +145,7 @@ export function parseStylesheet(code, lang, file, id, options) {
145
145
  }
146
146
  }
147
147
  for (const target of classes.values())
148
- target.atoms = dropOverridden(target.atoms);
148
+ target.atoms = dropOverridden(pruneFallbacks(target.atoms, options.lightningcss));
149
149
  // Custom properties are local to the whole file, like keyframes.
150
150
  if (options.customPropertyLocalization) {
151
151
  const all = [...classes.values()].flatMap((target) => target.atoms);
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { transform } from 'lightningcss';
3
- import { key, overrideKey, PreStyleError, valueOf } from './block.js';
3
+ import { atomDeclaration, key, overrideKey, PreStyleError, valueOf } from './block.js';
4
4
  import { atRuleInfo } from './at-rules.js';
5
5
  import { SLOT_PREFIX } from './interpolate.js';
6
6
  import { print } from './print.js';
@@ -43,7 +43,9 @@ const IGNORED = (token) => token.type === 'token' && token.value?.type === 'whit
43
43
  function directSlot(atom) {
44
44
  if (atom.atRules.length > 0 || atom.variant !== null || atom.declaration.important)
45
45
  return null;
46
- const declaration = atom.declaration.value;
46
+ if (atom.declaration.values.length !== 1)
47
+ return null;
48
+ const declaration = atom.declaration.values[0];
47
49
  // Lightning CSS also uses `custom` for properties it doesn't parse, such as
48
50
  // `content` with a `var()`.
49
51
  if (declaration.property !== 'custom' || !atom.property.startsWith('--'))
@@ -251,22 +253,17 @@ export function extractVariables(atoms, globals) {
251
253
  }
252
254
  continue;
253
255
  }
254
- const value = structuredClone(atom.declaration.value);
256
+ const values = structuredClone(atom.declaration.values);
255
257
  const selector = selectorOf(atom);
256
258
  let index = 0;
257
- eachSlot(value, (node) => {
259
+ // Counted across the whole stack, so `height: ${a}; height: ${b}` is two
260
+ // variables.
261
+ eachSlot(values, (node) => {
258
262
  const { slot, syntax } = parseSlot(node);
259
263
  const identity = atomIdentity(atom, index++);
260
264
  useVariable(node, identity, slot, syntax, variables, added, atom.position, selector);
261
265
  });
262
- keptAtoms.push({
263
- ...atom,
264
- declaration: {
265
- ...atom.declaration,
266
- key: key(value) + (atom.declaration.important ? '!' : ''),
267
- value,
268
- },
269
- });
266
+ keptAtoms.push({ ...atom, declaration: atomDeclaration(values, atom.declaration.important) });
270
267
  }
271
268
  return { atoms: keptAtoms, globals: [...kept, ...added.values()], variables };
272
269
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pre.style/core",
3
- "version": "3.1.0-alpha.4",
3
+ "version": "3.1.0-alpha.6",
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",