@pre.style/core 3.1.0-alpha.0 → 3.1.0-alpha.1

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,11 +1,11 @@
1
1
  # pre.style
2
2
 
3
- [![npm version](https://badge.fury.io/js/pre.style.svg)](https://www.npmjs.com/package/pre.style)
3
+ [![npm version](https://badge.fury.io/js/pre.style.svg)](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
 
7
7
  ```js
8
- import { css } from '@pre.style/core';
8
+ import { css } from "@pre.style/core";
9
9
 
10
10
  const button = css`
11
11
  background-color: #0071ba;
@@ -25,14 +25,28 @@ const button = css`
25
25
  After the build, `button` is a plain string, `"B C E D A G F"`. The block is replaced, and these classes are added to a single CSS file:
26
26
 
27
27
  ```css
28
- .B{background-color:#0071ba}
29
- .C{color:#fff}
30
- .E{padding-block-start:.5em}
31
- .D{padding-block-end:.5em}
32
- .A:hover{background-color:#005a94}
28
+ .B {
29
+ background-color: #0071ba;
30
+ }
31
+ .C {
32
+ color: #fff;
33
+ }
34
+ .E {
35
+ padding-block-start: 0.5em;
36
+ }
37
+ .D {
38
+ padding-block-end: 0.5em;
39
+ }
40
+ .A:hover {
41
+ background-color: #005a94;
42
+ }
33
43
  @media (width>=768px) {
34
- .G{padding-block-start:1em}
35
- .F{padding-block-end:1em}
44
+ .G {
45
+ padding-block-start: 1em;
46
+ }
47
+ .F {
48
+ padding-block-end: 1em;
49
+ }
36
50
  }
37
51
  ```
38
52
 
@@ -59,8 +73,8 @@ npm install --save-dev sass
59
73
  Add the plugin to `vite.config.ts`:
60
74
 
61
75
  ```ts
62
- import { defineConfig } from 'vite';
63
- import preStyle from '@pre.style/vite';
76
+ import { defineConfig } from "vite";
77
+ import preStyle from "@pre.style/vite";
64
78
 
65
79
  export default defineConfig({
66
80
  plugins: [preStyle()],
@@ -70,7 +84,7 @@ export default defineConfig({
70
84
  Then import `css` (or `scss`) from `@pre.style/core` and use the class strings:
71
85
 
72
86
  ```jsx
73
- import { css } from '@pre.style/core';
87
+ import { css } from "@pre.style/core";
74
88
 
75
89
  export function Card({ children }) {
76
90
  return (
@@ -95,7 +109,7 @@ You can also write styles in `.css` and `.scss` files and import their classes,
95
109
  Frameworks that render on the server, such as React Router's framework mode, don't use `index.html`. Import pre.style's CSS in your root module instead:
96
110
 
97
111
  ```ts
98
- import 'virtual:pre.style.css';
112
+ import "virtual:pre.style.css";
99
113
  ```
100
114
 
101
115
  Vite then bundles the CSS with your app's other CSS, and the framework links it when rendering on the server, both in builds and in dev. The plugin doesn't write `output.file` when the CSS is imported this way.
@@ -106,8 +120,8 @@ Add the plugin to Astro's Vite config, and import the CSS in your layout's front
106
120
 
107
121
  ```js
108
122
  // astro.config.mjs
109
- import { defineConfig } from 'astro/config';
110
- import preStyle from '@pre.style/vite';
123
+ import { defineConfig } from "astro/config";
124
+ import preStyle from "@pre.style/vite";
111
125
 
112
126
  export default defineConfig({
113
127
  vite: { plugins: [preStyle()] },
@@ -127,7 +141,7 @@ const title = css`
127
141
  <h1 class={title}>Hello</h1>
128
142
  ```
129
143
 
130
- Blocks work in frontmatter, in template expressions such as `` class={css`…`} ``, in `<script>` tags, and in the islands of any framework. Astro links the CSS on every page that uses the layout, prerendered or rendered on demand. Astro compiles components before the plugin sees them, but errors and source maps still point to the component.
144
+ Blocks work in frontmatter, in template expressions such as ``class={css`…`}``, in `<script>` tags, and in the islands of any framework. Astro links the CSS on every page that uses the layout, prerendered or rendered on demand. Astro compiles components before the plugin sees them, but errors and source maps still point to the component.
131
145
 
132
146
  ## Writing blocks
133
147
 
@@ -138,7 +152,7 @@ A block styles one element. Each block is treated as its own small stylesheet, w
138
152
  `css` blocks are plain CSS. `scss` blocks are compiled with Sass first, so you can use variables, mixins, functions, and everything else Sass supports:
139
153
 
140
154
  ```js
141
- import { scss } from '@pre.style/core';
155
+ import { scss } from "@pre.style/core";
142
156
 
143
157
  const title = scss`
144
158
  $size: 2rem;
@@ -161,7 +175,8 @@ css`
161
175
  &::before { content: '→'; }
162
176
  & > * + * { margin-top: 1rem; }
163
177
  .dark & { color: white; }
164
- a { text-decoration: underline; } /* the same as `& a` */
178
+ a { text-decoration: underline; } /* the same as ` &
179
+ a` */
165
180
  `;
166
181
  ```
167
182
 
@@ -174,20 +189,32 @@ Other at-rules, such as `@keyframes`, `@font-face`, and `@property`, are copied
174
189
  ```js
175
190
  css`
176
191
  @keyframes fade-in {
177
- from { opacity: 0; }
192
+ from {
193
+ opacity: 0;
194
+ }
178
195
  }
179
196
 
180
197
  animation: fade-in 200ms ease-out;
181
198
  `;
182
199
  ```
183
200
 
201
+ #### Local keyframes
202
+
203
+ By default, keyframes names are global, so two blocks that each define a different `@keyframes spin` collide, and the last one wins everywhere. Set `"keyframeLocalization": true` in your `package.json`'s `pre.style` field to give each `@keyframes` a generated name, as styled-components' `keyframes` helper does:
204
+
205
+ - A block's or stylesheet file's `@keyframes` get a short generated name, and its `animation` and `animation-name` declarations use it. In a stylesheet file, every class in the file shares the file's keyframes, as in CSS Modules.
206
+ - Keyframes with the same contents get the same name wherever they're defined, so they're written once.
207
+ - Keyframes from [prepended files](#sharing-values-with-prepended-files) are localized too. Every block gets the same copy, so prepended files are the way to share keyframes between blocks.
208
+ - Names a block or file doesn't define are left as they are, so `animation-name: spin` still refers to a global `@keyframes spin`, for example one in a CSS file that pre.style doesn't process. That also means a block can't use keyframes another block defines.
209
+ - Names in custom properties (`--anim: spin`) aren't renamed, because pre.style can't tell they name keyframes.
210
+
184
211
  ### `composes` in blocks
185
212
 
186
213
  A block can build on classes from a [stylesheet file](#stylesheet-files), or on global class names, with `composes`, as in CSS Modules:
187
214
 
188
215
  ```js
189
216
  css`
190
- composes: button from './button.css';
217
+ composes: button from "./button.css";
191
218
  composes: legacy-btn from global;
192
219
  color: white;
193
220
  `;
@@ -208,7 +235,7 @@ HTML can't import anything, so blocks in HTML files that Vite processes, such as
208
235
  Atomic classes all have the same specificity, so when two classes on one element set the same property, the order of the CSS file decides which wins, not the order in your `class` attribute. Use `cn` to combine class strings: when two classes conflict, it keeps the later one.
209
236
 
210
237
  ```js
211
- import { css, cn } from '@pre.style/core';
238
+ import { css, cn } from "@pre.style/core";
212
239
 
213
240
  const button = css`
214
241
  background-color: #0071ba;
@@ -251,7 +278,10 @@ Styles can also live in `.css` and `.scss` files, imported with the `pre.style`
251
278
  ```
252
279
 
253
280
  ```jsx
254
- import { button, 'primary-button' as primaryButton } from './button.css' with { type: 'pre.style' };
281
+ import {
282
+ button,
283
+ "primary-button" as primaryButton,
284
+ } from "./button.css" with { type: "pre.style" };
255
285
 
256
286
  <button className={primaryButton}>Save</button>;
257
287
  ```
@@ -276,15 +306,27 @@ In `.scss` files, relative `@use`, `@forward`, and `@import` resolve from the fi
276
306
  Atomic classes are shared, so no single class in a class string identifies one export. When an exported class is used as context for another, as `card` is in `.card:hover .title`, pre.style gives it a **marker**: one extra class, added to `card`'s class string, that the selector uses.
277
307
 
278
308
  ```css
279
- .card { padding-top: 1rem; }
280
- .title { color: black; }
281
- .card:hover .title { color: red; }
309
+ .card {
310
+ padding-top: 1rem;
311
+ }
312
+ .title {
313
+ color: black;
314
+ }
315
+ .card:hover .title {
316
+ color: red;
317
+ }
282
318
  ```
283
319
 
284
320
  ```css
285
- .D{padding-top:1rem}
286
- .C{color:#000}
287
- .A:hover .B{color:red}
321
+ .D {
322
+ padding-top: 1rem;
323
+ }
324
+ .C {
325
+ color: #000;
326
+ }
327
+ .A:hover .B {
328
+ color: red;
329
+ }
288
330
  ```
289
331
 
290
332
  `card` is `"D A"`, where `A` is its marker, and `title` is `"C B"`.
@@ -297,10 +339,20 @@ Atomic classes are shared, so no single class in a class string identifies one e
297
339
  `composes` works as in CSS Modules:
298
340
 
299
341
  ```css
300
- .base { color: gray; padding: 1rem; }
301
- .primary { composes: base; color: blue; }
302
- .wide { composes: card from './card.css'; }
303
- .legacy { composes: old-btn from global; }
342
+ .base {
343
+ color: gray;
344
+ padding: 1rem;
345
+ }
346
+ .primary {
347
+ composes: base;
348
+ color: blue;
349
+ }
350
+ .wide {
351
+ composes: card from "./card.css";
352
+ }
353
+ .legacy {
354
+ composes: old-btn from global;
355
+ }
304
356
  ```
305
357
 
306
358
  > [!IMPORTANT]
@@ -318,7 +370,9 @@ TypeScript 7.1 adds [content mappers](https://devblogs.microsoft.com/typescript/
318
370
 
319
371
  ```json
320
372
  {
321
- "contentMappers": [{ "package": "@pre.style/core", "extensions": [".css", ".scss"] }]
373
+ "contentMappers": [
374
+ { "package": "@pre.style/core", "extensions": [".css", ".scss"] }
375
+ ]
322
376
  }
323
377
  ```
324
378
 
@@ -327,11 +381,11 @@ Content mappers run code from a package, so `tsc` needs the `--runExternalCode`
327
381
  Before TypeScript 7.1, add this declaration to your project instead (for example as `src/stylesheets.d.ts`):
328
382
 
329
383
  ```ts
330
- declare module '*.css' {
384
+ declare module "*.css" {
331
385
  const classes: { readonly [name: string]: string };
332
386
  export = classes;
333
387
  }
334
- declare module '*.scss' {
388
+ declare module "*.scss" {
335
389
  const classes: { readonly [name: string]: string };
336
390
  export = classes;
337
391
  }
@@ -340,7 +394,7 @@ declare module '*.scss' {
340
394
  TypeScript can't type named imports from it, so use a namespace import:
341
395
 
342
396
  ```ts
343
- import * as styles from './button.css' with { type: 'pre.style' };
397
+ import * as styles from "./button.css" with { type: "pre.style" };
344
398
 
345
399
  styles.button;
346
400
  ```
@@ -411,11 +465,12 @@ A library's class names depend on the whole app, so they differ between apps. Do
411
465
  }
412
466
  ```
413
467
 
414
- | Field | Default | Description |
415
- | -------------- | ---------------------------- | ----------- |
416
- | `include` | Every JS, TS, and Astro file | Globs of the files with blocks, relative to the package. A glob or an array of them. Stylesheet files the library imports are found from its imports. |
417
- | `exclude` | | Globs removed from `include`. |
418
- | `prependFiles` | | Files added to the start of the library's blocks and stylesheet files, relative to the package. They apply only to the library's, not the app's, so the library's tokens and mixins are always available to it and never leak into the app. |
468
+ | Field | Default | Description |
469
+ | ---------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
470
+ | `include` | Every JS, TS, and Astro file | Globs of the files with blocks, relative to the package. A glob or an array of them. Stylesheet files the library imports are found from its imports. |
471
+ | `exclude` | | Globs removed from `include`. |
472
+ | `prependFiles` | | Files added to the start of the library's blocks and stylesheet files, relative to the package. They apply only to the library's, not the app's, so the library's tokens and mixins are always available to it and never leak into the app. |
473
+ | `keyframeLocalization` | `false` | Give each `@keyframes` in the library's blocks and stylesheet files a generated name. See [Local keyframes](#local-keyframes). The library's setting applies to its blocks, whatever the app sets, since the library's styles are written for it. |
419
474
 
420
475
  `"pre.style": true` uses the defaults. A library's field uses the same name and shape as an app's (see [Configuration](#configuration)). `libraries` and `quirksMode` only apply to the app, so in a library's field they're ignored, with a warning. A package that is both, such as a library with its own demo app, is read as the app when it's the Vite root, and as a library when it's a dependency.
421
476
 
@@ -438,20 +493,33 @@ Production builds scan your files first, so names are stable between builds. In
438
493
  Shorthand properties are expanded into their longhands, so `cn` can resolve conflicts between them:
439
494
 
440
495
  ```js
441
- css`margin: auto;`;
496
+ css`
497
+ margin: auto;
498
+ `;
442
499
  ```
443
500
 
444
501
  ```css
445
- .D{margin-top:auto}
446
- .C{margin-right:auto}
447
- .A{margin-bottom:auto}
448
- .B{margin-left:auto}
502
+ .D {
503
+ margin-top: auto;
504
+ }
505
+ .C {
506
+ margin-right: auto;
507
+ }
508
+ .A {
509
+ margin-bottom: auto;
510
+ }
511
+ .B {
512
+ margin-left: auto;
513
+ }
449
514
  ```
450
515
 
451
516
  `border` (which sets 12 longhands), shorthands whose values aren't known until runtime (values with `var()`, `env()`, or `attr()`), and shorthands set to a CSS-wide keyword (`font: inherit`, `margin: initial`) are kept as one class. They're written before longhands, so a longhand still wins:
452
517
 
453
518
  ```js
454
- css`padding: var(--space); padding-top: 0;`;
519
+ css`
520
+ padding: var(--space);
521
+ padding-top: 0;
522
+ `;
455
523
  ```
456
524
 
457
525
  Expanding means some shorthands produce many classes. For example, `font` also resets the `font-variant-*` properties, among others, so it becomes about 20 classes. The CSS file barely grows, because the classes are shared across your project, but class strings get longer.
@@ -460,7 +528,7 @@ Expanding means some shorthands produce many classes. For example, `font` also r
460
528
 
461
529
  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:
462
530
 
463
- 1. Hoisted at-rules (`@layer` statements, `@import`, `@namespace`), then other copied at-rules.
531
+ 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.
464
532
  2. Classes on the element.
465
533
  3. Selector variants, with pseudo-classes in LVHFA order (`:link`, `:visited`, `:hover`, `:focus`, `:active`).
466
534
  4. At-rule groups. `@media` queries with only a `min-width` come last, smallest first (mobile-first).
@@ -492,13 +560,14 @@ Options every tool that reads your styles has to agree on (the bundler plugin, a
492
560
  }
493
561
  ```
494
562
 
495
- | Option | Default | Description |
496
- | -------------- | ------------------------------------------------- | ----------- |
497
- | `include` | `**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts,html,astro}` | Globs of the files with blocks, relative to the `package.json`. A glob or an array of them. Scanned before a build to assign names; stylesheet files are found from the scanned files' imports. `node_modules` and the output directory are always skipped. |
498
- | `exclude` | | Globs removed from `include`. |
499
- | `prependFiles` | | Files added to the start of the app's blocks and stylesheet files. See [Sharing values with prepended files](#sharing-values-with-prepended-files). |
500
- | `libraries` | | Component libraries to compile, by package name, in addition to dependencies with a `pre.style` field. A config replaces the package's field, `true` uses the defaults, and `false` leaves the package out. See [Component libraries](#component-libraries). |
501
- | `quirksMode` | `false` | **You probably don't want this.** Use lowercase class names only, for pages in quirks mode. Names get longer. See [Quirks mode](#quirks-mode). |
563
+ | Option | Default | Description |
564
+ | ---------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
565
+ | `include` | `**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts,html,astro}` | Globs of the files with blocks, relative to the `package.json`. A glob or an array of them. Scanned before a build to assign names; stylesheet files are found from the scanned files' imports. `node_modules` and the output directory are always skipped. |
566
+ | `exclude` | | Globs removed from `include`. |
567
+ | `prependFiles` | | Files added to the start of the app's blocks and stylesheet files. See [Sharing values with prepended files](#sharing-values-with-prepended-files). |
568
+ | `libraries` | | Component libraries to compile, by package name, in addition to dependencies with a `pre.style` field. A config replaces the package's field, `true` uses the defaults, and `false` leaves the package out. See [Component libraries](#component-libraries). |
569
+ | `quirksMode` | `false` | **You probably don't want this.** Use lowercase class names only, for pages in quirks mode. Names get longer. See [Quirks mode](#quirks-mode). |
570
+ | `keyframeLocalization` | `false` | Give each `@keyframes` in the app's blocks and stylesheet files a generated name, used only in the block or file that defines it. See [Local keyframes](#local-keyframes). Libraries use their own setting. |
502
571
 
503
572
  The plugin warns about keys it doesn't know, and fails the build on values of the wrong type. For completion and validation in your editor, `@pre.style/core` includes a JSON Schema for the field, at `node_modules/@pre.style/core/package.schema.json`. In VS Code, add it to your settings:
504
573
 
@@ -509,7 +578,9 @@ The plugin warns about keys it doesn't know, and fails the build on values of th
509
578
  "fileMatch": ["package.json"],
510
579
  "schema": {
511
580
  "properties": {
512
- "pre.style": { "$ref": "./node_modules/@pre.style/core/package.schema.json" }
581
+ "pre.style": {
582
+ "$ref": "./node_modules/@pre.style/core/package.schema.json"
583
+ }
513
584
  }
514
585
  }
515
586
  }
@@ -525,22 +596,24 @@ Options about how a build writes its output go to the plugin. All options are op
525
596
  preStyle({
526
597
  lightningcss: { minify: true, targets: { safari: 16 << 16 } },
527
598
  sourceMap: true,
528
- quotes: 'single-quote',
599
+ layer: "pre.style",
600
+ quotes: "single-quote",
529
601
  output: {
530
- file: 'css/pre.style.css',
531
- banner: '/* Generated by pre.style */',
602
+ file: "css/pre.style.css",
603
+ banner: "/* Generated by pre.style */",
532
604
  },
533
605
  });
534
606
  ```
535
607
 
536
- | Option | Default | Description |
537
- | -------------- | ------------------------------------------------- | ----------- |
538
- | `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. |
539
- | `sourceMap` | `false` | 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`). 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. |
540
- | `quotes` | `'double-quote'` | Quotes around the class string that replaces each block in JS: `'single-quote'`, `'double-quote'`, or `'backtick'`. |
541
- | `output.file` | `'pre.style.css'` | Path and name of the CSS file, relative to Vite's `build.outDir`. |
542
- | `output.banner`| | Text added to the start of the CSS file. |
543
- | `output.footer`| | Text added to the end of the CSS file. |
608
+ | Option | Default | Description |
609
+ | --------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
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. |
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
+ | `quotes` | `'double-quote'` | Quotes around the class string that replaces each block in JS: `'single-quote'`, `'double-quote'`, or `'backtick'`. |
614
+ | `output.file` | `'pre.style.css'` | Path and name of the CSS file, relative to Vite's `build.outDir`. |
615
+ | `output.banner` | | Text added to the start of the CSS file. |
616
+ | `output.footer` | | Text added to the end of the CSS file. |
544
617
 
545
618
  The CSS isn't minified unless you set `lightningcss: { minify: true }`.
546
619
 
@@ -591,10 +664,10 @@ The build fails, with the file, line, and column, when:
591
664
 
592
665
  ## Packages
593
666
 
594
- | Package | Description |
595
- | ----------------------- | ------------------------------------------------------ |
596
- | `@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. |
597
- | `@pre.style/vite` | The Vite plugin. |
667
+ | Package | Description |
668
+ | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
669
+ | `@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
+ | `@pre.style/vite` | The Vite plugin. |
598
671
 
599
672
  ## Development
600
673
 
@@ -69,6 +69,7 @@ export interface ParseOptions {
69
69
  prepend: Record<Lang, string>;
70
70
  sass: SassCompiler | null;
71
71
  warn: (message: string) => void;
72
+ keyframeLocalization: boolean;
72
73
  }
73
74
  export declare const key: (value: unknown) => string;
74
75
  export declare function parseBlock(code: string, lang: Lang, options: ParseOptions): ParsedBlock;
@@ -85,6 +86,7 @@ export declare function addDeclarations(block: {
85
86
  importantDeclarations?: Declaration[];
86
87
  } | undefined, atRules: AtRuleSegment[], selectors: SelectorList, atoms: Atom[], composes: Compose[] | null, position: Position | null): void;
87
88
  export declare function addGlobal(rule: Rule, atRules: AtRuleSegment[], globals: GlobalRule[], position: ToPosition): void;
89
+ export declare function overrideKey(rule: Rule): string;
88
90
  export declare function propertyOf(declaration: Declaration): {
89
91
  property: string;
90
92
  shorthand: boolean;
@@ -1,6 +1,7 @@
1
1
  import { composeVisitors, transform } from 'lightningcss';
2
2
  import { atRuleInfo } from './at-rules.js';
3
3
  import { expand } from './expand.js';
4
+ import { localizeKeyframes } from './keyframes.js';
4
5
  import { compose, fromPlaceholder, isElement, PLACEHOLDER, repair } from './selectors.js';
5
6
  import { splitBlock } from './split.js';
6
7
  import { SHORTHANDS } from '../shorthands.js';
@@ -82,11 +83,11 @@ export function parseBlock(code, lang, options) {
82
83
  // Sass output positions don't match the block, so scss atoms have none.
83
84
  const position = (loc) => lang === 'css' && loc ? toBlock(loc.line, loc.column) : null;
84
85
  parseRules(css, options.lightningcss, position, (rules) => walk(rules, [], null, parsed, position));
85
- return {
86
- atoms: dropOverridden(parsed.atoms),
87
- globals: parsed.globals,
88
- composes: parsed.composes,
89
- };
86
+ const atoms = dropOverridden(parsed.atoms);
87
+ if (!options.keyframeLocalization)
88
+ return { atoms, globals: parsed.globals, composes: parsed.composes };
89
+ const local = localizeKeyframes(parsed.globals);
90
+ return { atoms: local.atoms(atoms), globals: local.globals, composes: parsed.composes };
90
91
  }
91
92
  // Parses CSS with Lightning CSS, and passes its rules to `collect` once the
92
93
  // user's visitor has changed them. `position` converts error locations.
@@ -276,11 +277,12 @@ export function addGlobal(rule, atRules, globals, position) {
276
277
  });
277
278
  }
278
279
  // The identity of an overridable at-rule: its type and name.
279
- function overrideKey(rule) {
280
+ export function overrideKey(rule) {
280
281
  const value = valueOf(rule);
281
282
  switch (rule.type) {
282
283
  case 'keyframes':
283
- return `keyframes ${key(value.name)}`;
284
+ // `@-webkit-keyframes x` doesn't replace `@keyframes x`.
285
+ return `keyframes ${key(value.vendorPrefix ?? [])} ${key(value.name)}`;
284
286
  case 'font-face': {
285
287
  // A font face is identified by its family, style, weight, and stretch.
286
288
  const descriptors = Object.fromEntries(value.properties
@@ -8,6 +8,7 @@ export type PrependFiles = string[] | {
8
8
  export interface Library {
9
9
  dir: string;
10
10
  prependFiles?: PrependFiles;
11
+ keyframeLocalization?: boolean;
11
12
  }
12
13
  export interface CompilerOptions {
13
14
  prependFiles?: PrependFiles;
@@ -17,6 +18,8 @@ export interface CompilerOptions {
17
18
  root?: string;
18
19
  warn?: (message: string) => void;
19
20
  quirksMode?: boolean;
21
+ keyframeLocalization?: boolean;
22
+ layer?: false | string;
20
23
  }
21
24
  export interface Block {
22
25
  code: string;
@@ -1,9 +1,11 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { dirname, relative, resolve as resolvePath, sep } from 'node:path';
3
+ import { transform } from 'lightningcss';
3
4
  import SweatMap from 'sweatmap';
4
5
  import { dropOverridden, parseBlock, PreStyleError, valueOf } from './block.js';
5
6
  import { print } from './print.js';
6
7
  import { lvhfaRank, withClass } from './selectors.js';
8
+ import { KEYFRAMES_PREFIX } from './keyframes.js';
7
9
  import { MARKER_PREFIX, markerKey, parseStylesheet } from './stylesheet.js';
8
10
  const RANGES = {
9
11
  Digits: { start: '0030', end: '0039' },
@@ -45,14 +47,22 @@ export class Compiler {
45
47
  // most deeply nested library first.
46
48
  #prepend;
47
49
  #libraries;
50
+ // The layer the output is wrapped in, as parsed, or null for none.
51
+ #layer;
48
52
  #blocks = new Map();
49
53
  #stylesheets = new Map();
50
54
  #root;
51
- // One name list for every class, and one for prefixes (the at-rules and
52
- // selector variant a class is under), which only the `cn` table uses.
55
+ // One name list for every class, one for prefixes (the at-rules and
56
+ // selector variant a class is under), which only the `cn` table uses, and
57
+ // one for localized keyframes.
53
58
  #names;
54
- // How often each class and prefix is used across scanned blocks.
55
- #counts = { atoms: new Map(), prefixes: new Map() };
59
+ // How often each class, prefix, and localized keyframes is used across
60
+ // scanned blocks.
61
+ #counts = {
62
+ atoms: new Map(),
63
+ prefixes: new Map(),
64
+ keyframes: new Map(),
65
+ };
56
66
  #atoms = new Map();
57
67
  #globals = new Map();
58
68
  // The current global for each overridable at-rule.
@@ -63,14 +73,23 @@ export class Compiler {
63
73
  this.#names = {
64
74
  atoms: new SweatMap(options.quirksMode ? QUIRKS_NAME_OPTIONS : NAME_OPTIONS),
65
75
  prefixes: new SweatMap(NAME_OPTIONS),
76
+ // Keyframes names are case-sensitive, even in quirks mode. They reach
77
+ // four characters only after about 227,000 names, so they're never
78
+ // `none` or a CSS-wide keyword.
79
+ keyframes: new SweatMap(NAME_OPTIONS),
66
80
  };
81
+ this.#layer = layerRule(options.layer);
67
82
  const root = options.root ?? process.cwd();
68
83
  this.#root = root;
69
84
  this.#prepend = readPrepend(root, options.prependFiles);
70
85
  this.#libraries = (options.libraries ?? [])
71
86
  .map((library) => {
72
87
  const dir = resolvePath(root, library.dir);
73
- return { dir: dir + sep, prepend: readPrepend(dir, library.prependFiles) };
88
+ return {
89
+ dir: dir + sep,
90
+ prepend: readPrepend(dir, library.prependFiles),
91
+ keyframeLocalization: library.keyframeLocalization ?? false,
92
+ };
74
93
  })
75
94
  .toSorted((a, b) => b.dir.length - a.dir.length);
76
95
  this.#parseOptions = {
@@ -90,17 +109,21 @@ export class Compiler {
90
109
  const parsed = this.#parse(block);
91
110
  this.#register(block, parsed);
92
111
  this.#count(this.#resolveBlock(block, parsed, []));
112
+ for (const global of parsed.globals)
113
+ this.#countKeyframes(global.key);
93
114
  }
94
115
  // Pass 1 of naming for a stylesheet file: counts what each class uses.
95
116
  scanStylesheet(sheet) {
96
117
  const parsed = this.#parseSheet(sheet);
97
118
  for (const name of parsed.classes.keys())
98
119
  this.#count(this.#resolveClass(sheet.file, parsed, name, [], []));
120
+ for (const global of parsed.globals)
121
+ this.#countKeyframes(global.key);
99
122
  }
100
123
  // Pass 2 of naming: gives the most-used entries the shortest names. Ties
101
124
  // are broken by key, so names don't depend on the order files are read.
102
125
  assignNames() {
103
- for (const list of ['atoms', 'prefixes']) {
126
+ for (const list of ['atoms', 'prefixes', 'keyframes']) {
104
127
  const sorted = [...this.#counts[list]].toSorted(([a, countA], [b, countB]) => countB - countA || (a < b ? -1 : a > b ? 1 : 0));
105
128
  for (const [key] of sorted)
106
129
  this.#names[list].set(key);
@@ -123,9 +146,9 @@ export class Compiler {
123
146
  // anything, for tools that only need the names (such as the TypeScript
124
147
  // content mapper).
125
148
  classNames(sheet) {
126
- const { prepend } = this.#library(sheet.file);
149
+ const { options } = this.#library(sheet.file);
127
150
  try {
128
- const parsed = parseStylesheet(sheet.code, sheet.lang, sheet.file, relative(this.#root, sheet.file), { ...this.#parseOptions, prepend });
151
+ const parsed = parseStylesheet(sheet.code, sheet.lang, sheet.file, relative(this.#root, sheet.file), options);
129
152
  return [...parsed.classes.keys()];
130
153
  }
131
154
  catch (error) {
@@ -160,8 +183,25 @@ export class Compiler {
160
183
  .filter((g) => g.global.hoist !== false)
161
184
  .toSorted((a, b) => a.global.hoist - b.global.hoist);
162
185
  const rest = globals.filter((g) => g.global.hoist === false);
163
- const rules = [...hoisted, ...rest].map((g) => withLoc(g.global.key.includes(MARKER_KEY) ? this.#named(g.global.rule) : g.global.rule, loc(g.source)));
186
+ const named = (g) => withLoc(hasPlaceholder(g.global.key) ? this.#named(g.global.rule) : g.global.rule, loc(g.source));
187
+ let rules = [...hoisted, ...rest].map(named);
164
188
  rules.push(...this.#atomRules(loc));
189
+ // `@import` and `@namespace` can't be inside a layer, so they stay
190
+ // before it. `@layer` statements go inside, so the layers they order
191
+ // are the same ones blocks' `@layer` rules add to.
192
+ if (this.#layer !== null) {
193
+ rules = [
194
+ ...rules.filter(outside),
195
+ {
196
+ type: 'layer-block',
197
+ value: {
198
+ ...valueOf(this.#layer),
199
+ rules: rules.filter((r) => !outside(r)),
200
+ loc: loc(null),
201
+ },
202
+ },
203
+ ];
204
+ }
165
205
  return print(rules, sources, this.#parseOptions.lightningcss, this.#options.sourceMap ?? false);
166
206
  }
167
207
  // Classes on the element have an empty prefix.
@@ -177,19 +217,29 @@ export class Compiler {
177
217
  }
178
218
  return table;
179
219
  }
180
- // A library's blocks and files get its prepended files instead of the
181
- // app's.
220
+ // A library's blocks and files get its prepended files and keyframes
221
+ // setting instead of the app's.
182
222
  #library(file) {
183
223
  const library = file === undefined ? -1 : this.#libraries.findIndex(({ dir }) => file.startsWith(dir));
184
- return { library, prepend: library === -1 ? this.#prepend : this.#libraries[library].prepend };
224
+ const { prepend, keyframeLocalization } = library === -1
225
+ ? { prepend: this.#prepend, keyframeLocalization: this.#options.keyframeLocalization }
226
+ : this.#libraries[library];
227
+ return {
228
+ library,
229
+ options: {
230
+ ...this.#parseOptions,
231
+ prepend,
232
+ keyframeLocalization: keyframeLocalization ?? false,
233
+ },
234
+ };
185
235
  }
186
236
  #parse(block) {
187
- const { library, prepend } = this.#library(block.file);
237
+ const { library, options } = this.#library(block.file);
188
238
  const cacheKey = `${library}\n${block.lang}\n${block.code}`;
189
239
  let parsed = this.#blocks.get(cacheKey);
190
240
  if (parsed === undefined) {
191
241
  try {
192
- parsed = parseBlock(block.code, block.lang, { ...this.#parseOptions, prepend });
242
+ parsed = parseBlock(block.code, block.lang, options);
193
243
  }
194
244
  catch (error) {
195
245
  throw error instanceof PreStyleError ? locate(error, block) : error;
@@ -201,15 +251,12 @@ export class Compiler {
201
251
  // Parses a stylesheet file and registers its classes and global rules, so
202
252
  // the output includes them once the file is used.
203
253
  #parseSheet(sheet) {
204
- const { library, prepend } = this.#library(sheet.file);
254
+ const { library, options } = this.#library(sheet.file);
205
255
  const cacheKey = `${library}\n${sheet.lang}\n${sheet.file}\n${sheet.code}`;
206
256
  let parsed = this.#stylesheets.get(cacheKey);
207
257
  if (parsed === undefined) {
208
258
  try {
209
- parsed = parseStylesheet(sheet.code, sheet.lang, sheet.file, relative(this.#root, sheet.file), {
210
- ...this.#parseOptions,
211
- prepend,
212
- });
259
+ parsed = parseStylesheet(sheet.code, sheet.lang, sheet.file, relative(this.#root, sheet.file), options);
213
260
  }
214
261
  catch (error) {
215
262
  throw error instanceof PreStyleError ? locate(error, fileBlock(sheet.file)) : error;
@@ -301,6 +348,7 @@ export class Compiler {
301
348
  const prefix = prefixKey(atom);
302
349
  if (prefix !== '')
303
350
  count(this.#counts.prefixes, prefix);
351
+ this.#countKeyframes(atom.declaration.key);
304
352
  }
305
353
  for (const extra of resolved.extras)
306
354
  if (extra.startsWith(MARKER_PREFIX))
@@ -312,7 +360,15 @@ export class Compiler {
312
360
  names.push(extra.startsWith(MARKER_PREFIX) ? this.#names.atoms.set(extra) : extra);
313
361
  return [...new Set(names)].join(' ');
314
362
  }
315
- // Replaces markers in selectors with their names.
363
+ // Counts the localized keyframes a JSON key names.
364
+ #countKeyframes(json) {
365
+ if (!json.includes(KEYFRAMES_KEY))
366
+ return;
367
+ for (const [, hash] of json.matchAll(KEYFRAMES_KEY_RE))
368
+ count(this.#counts.keyframes, KEYFRAMES_PREFIX + hash);
369
+ }
370
+ // Replaces markers in selectors, and localized keyframes names, with their
371
+ // names.
316
372
  #named(value) {
317
373
  const visit = (node) => {
318
374
  if (Array.isArray(node))
@@ -324,6 +380,10 @@ export class Compiler {
324
380
  typeof record.name === 'string' &&
325
381
  record.name.startsWith(MARKER_PREFIX))
326
382
  return { ...record, name: this.#names.atoms.set(record.name) };
383
+ if (record.type === 'ident' &&
384
+ typeof record.value === 'string' &&
385
+ record.value.startsWith(KEYFRAMES_PREFIX))
386
+ return { ...record, value: this.#names.keyframes.set(record.value) };
327
387
  return Object.fromEntries(Object.entries(record).map(([k, v]) => [k, visit(v)]));
328
388
  };
329
389
  return visit(value);
@@ -384,9 +444,12 @@ export class Compiler {
384
444
  : [[{ type: 'class', name: className }]];
385
445
  if (atom.variant?.key.includes(MARKER_KEY))
386
446
  selectors = this.#named(selectors);
447
+ const value = atom.declaration.key.includes(KEYFRAMES_KEY)
448
+ ? this.#named(atom.declaration.value)
449
+ : atom.declaration.value;
387
450
  const declarations = atom.declaration.important
388
- ? { importantDeclarations: [atom.declaration.value] }
389
- : { declarations: [atom.declaration.value] };
451
+ ? { importantDeclarations: [value] }
452
+ : { declarations: [value] };
390
453
  return {
391
454
  type: 'style',
392
455
  value: { selectors, declarations, rules: [], loc: loc(source) },
@@ -403,8 +466,40 @@ export class Compiler {
403
466
  return emit(root);
404
467
  }
405
468
  }
406
- // Markers as they appear in JSON keys, which escape the `\0`.
469
+ // Markers and localized keyframes names as they appear in JSON keys, which
470
+ // escape the `\0`.
407
471
  const MARKER_KEY = JSON.stringify(MARKER_PREFIX).slice(1, -1);
472
+ const KEYFRAMES_KEY = JSON.stringify(KEYFRAMES_PREFIX).slice(1, -1);
473
+ const KEYFRAMES_KEY_RE = new RegExp(`${RegExp.escape(KEYFRAMES_KEY)}([\\w-]+)`, 'g');
474
+ // Rules that can't be inside a layer.
475
+ const outside = (rule) => rule.type === 'import' || rule.type === 'namespace';
476
+ const hasPlaceholder = (json) => json.includes(MARKER_KEY) || json.includes(KEYFRAMES_KEY);
477
+ // Parses the `layer` option into an `@layer` block rule.
478
+ function layerRule(layer) {
479
+ if (layer === undefined || layer === false)
480
+ return null;
481
+ if (typeof layer !== 'string')
482
+ throw new PreStyleError('`layer` must be `false` or a string.', null);
483
+ let rule;
484
+ try {
485
+ transform({
486
+ filename: 'layer.css',
487
+ code: Buffer.from(`@layer ${layer} {}`),
488
+ visitor: {
489
+ StyleSheetExit(sheet) {
490
+ if (sheet.rules.length === 1)
491
+ rule = sheet.rules[0];
492
+ },
493
+ },
494
+ });
495
+ }
496
+ catch {
497
+ // Reported below.
498
+ }
499
+ if (rule?.type !== 'layer-block' || (layer.trim() === '') !== (rule.value.name == null))
500
+ throw new PreStyleError(`\`layer\` must be a layer name, such as "pre.style" or "base.components", or "" for an unnamed layer. Got ${JSON.stringify(layer)}.`, null);
501
+ return rule;
502
+ }
408
503
  // A stylesheet file, for positions and source maps.
409
504
  const fileBlock = (file) => ({ code: '', lang: 'css', file, line: 1, column: 1 });
410
505
  // Reads prepended files, relative to `dir`, once.
@@ -6,6 +6,7 @@ export interface PackageConfig {
6
6
  prependFiles?: PrependFiles;
7
7
  libraries?: Record<string, LibraryConfig | boolean>;
8
8
  quirksMode?: boolean;
9
+ keyframeLocalization?: boolean;
9
10
  }
10
11
  export declare function checkField(field: Record<string, unknown>, where: string, keys: Set<string>): string[];
11
12
  export declare function readConfig(root: string): {
@@ -1,6 +1,13 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
- const KEYS = new Set(['include', 'exclude', 'prependFiles', 'libraries', 'quirksMode']);
3
+ const KEYS = new Set([
4
+ 'include',
5
+ 'exclude',
6
+ 'prependFiles',
7
+ 'libraries',
8
+ 'quirksMode',
9
+ 'keyframeLocalization',
10
+ ]);
4
11
  const isStrings = (value) => typeof value === 'string' ||
5
12
  (Array.isArray(value) && value.every((item) => typeof item === 'string'));
6
13
  const isPrependFiles = (value) => (Array.isArray(value) && isStrings(value)) ||
@@ -22,6 +29,8 @@ export function checkField(field, where, keys) {
22
29
  fail('prependFiles', 'an array of paths, or an object with `css` and `scss` arrays');
23
30
  if (field.quirksMode !== undefined && typeof field.quirksMode !== 'boolean')
24
31
  fail('quirksMode', '`true` or `false`');
32
+ if (field.keyframeLocalization !== undefined && typeof field.keyframeLocalization !== 'boolean')
33
+ fail('keyframeLocalization', '`true` or `false`');
25
34
  if (field.libraries !== undefined &&
26
35
  (field.libraries === null ||
27
36
  typeof field.libraries !== 'object' ||
@@ -0,0 +1,6 @@
1
+ import type { Atom, GlobalRule } from './block.ts';
2
+ export declare const KEYFRAMES_PREFIX = "\0pre.style-keyframes:";
3
+ export declare function localizeKeyframes(globals: GlobalRule[]): {
4
+ globals: GlobalRule[];
5
+ atoms: (atoms: Atom[]) => Atom[];
6
+ };
@@ -0,0 +1,120 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { key, overrideKey, valueOf } from './block.js';
3
+ // With `keyframeLocalization`, each `@keyframes` name is replaced with a
4
+ // generated one, like styled-components' `keyframes`. In parsed rules, a
5
+ // local name is an ident with this prefix and a hash of every definition of
6
+ // the name in its scope (a block, or a stylesheet file), so identical
7
+ // keyframes share a name wherever they're defined. The compiler gives it a
8
+ // name when printing.
9
+ export const KEYFRAMES_PREFIX = '\0pre.style-keyframes:';
10
+ // Properties whose values name keyframes, without a vendor prefix.
11
+ const ANIMATION_PROPERTIES = new Set(['animation', 'animation-name']);
12
+ // Replaces each `@keyframes` name defined in a scope (in its globals, which
13
+ // include its prepended code) with a local name, in the definitions, and,
14
+ // through `atoms`, in `animation` and `animation-name` declarations. Names
15
+ // the scope doesn't define are left as they are.
16
+ export function localizeKeyframes(globals) {
17
+ const definitions = new Map();
18
+ for (const global of globals)
19
+ eachKeyframes(global.rule, (value) => {
20
+ const list = definitions.get(value.name.value) ?? [];
21
+ list.push(key({ vendorPrefix: value.vendorPrefix, keyframes: value.keyframes }));
22
+ definitions.set(value.name.value, list);
23
+ });
24
+ if (definitions.size === 0)
25
+ return { globals, atoms: (atoms) => atoms };
26
+ const local = new Map();
27
+ for (const [name, list] of definitions)
28
+ local.set(name, KEYFRAMES_PREFIX +
29
+ createHash('sha256').update(JSON.stringify(list)).digest('base64url').slice(0, 16));
30
+ return {
31
+ globals: globals.map((global) => {
32
+ let found = false;
33
+ const rule = mapRule(global.rule, (inner) => {
34
+ if (inner.type !== 'keyframes')
35
+ return inner;
36
+ found = true;
37
+ const value = valueOf(inner);
38
+ return {
39
+ ...inner,
40
+ value: {
41
+ ...value,
42
+ name: { type: 'ident', value: local.get(value.name.value) },
43
+ },
44
+ };
45
+ });
46
+ if (!found)
47
+ return global;
48
+ return {
49
+ ...global,
50
+ key: key(rule),
51
+ rule,
52
+ overrideKey: global.overrideKey === null ? null : overrideKey(rule),
53
+ };
54
+ }),
55
+ atoms: (atoms) => atoms.map((atom) => {
56
+ if (!ANIMATION_PROPERTIES.has(atom.property.replace(/^-\w+-/, '')))
57
+ return atom;
58
+ const value = renameIdents(atom.declaration.value, local);
59
+ if (value === atom.declaration.value)
60
+ return atom;
61
+ return {
62
+ ...atom,
63
+ declaration: {
64
+ ...atom.declaration,
65
+ key: key(value) + (atom.declaration.important ? '!' : ''),
66
+ value,
67
+ },
68
+ };
69
+ }),
70
+ };
71
+ }
72
+ // Calls `visit` for each `@keyframes` rule in a rule, including inside the
73
+ // at-rules a global keeps around it.
74
+ function eachKeyframes(rule, visit) {
75
+ mapRule(rule, (inner) => {
76
+ if (inner.type === 'keyframes')
77
+ visit(valueOf(inner));
78
+ return inner;
79
+ });
80
+ }
81
+ // Maps a rule and the rules nested in it.
82
+ function mapRule(rule, map) {
83
+ const value = valueOf(rule);
84
+ const mapped = Array.isArray(value.rules)
85
+ ? {
86
+ ...rule,
87
+ value: {
88
+ ...value,
89
+ rules: value.rules.map((r) => mapRule(r, map)),
90
+ },
91
+ }
92
+ : rule;
93
+ return map(mapped);
94
+ }
95
+ // Replaces names in idents and strings (parsed animation names, and tokens
96
+ // in unparsed values). Returns `value` itself when nothing changed.
97
+ function renameIdents(value, local) {
98
+ const visit = (node) => {
99
+ if (Array.isArray(node)) {
100
+ const mapped = node.map(visit);
101
+ return mapped.some((item, i) => item !== node[i]) ? mapped : node;
102
+ }
103
+ if (node === null || typeof node !== 'object')
104
+ return node;
105
+ const record = node;
106
+ if ((record.type === 'ident' || record.type === 'string') &&
107
+ typeof record.value === 'string' &&
108
+ local.has(record.value))
109
+ return { type: 'ident', value: local.get(record.value) };
110
+ let changed = false;
111
+ const entries = Object.entries(record).map(([k, v]) => {
112
+ const mapped = visit(v);
113
+ if (mapped !== v)
114
+ changed = true;
115
+ return [k, mapped];
116
+ });
117
+ return changed ? Object.fromEntries(entries) : node;
118
+ };
119
+ return visit(value);
120
+ }
@@ -3,6 +3,7 @@ export interface LibraryConfig {
3
3
  include?: string | string[];
4
4
  exclude?: string | string[];
5
5
  prependFiles?: PrependFiles;
6
+ keyframeLocalization?: boolean;
6
7
  }
7
8
  export interface ResolvedLibrary {
8
9
  name: string;
@@ -10,5 +11,6 @@ export interface ResolvedLibrary {
10
11
  include: string[];
11
12
  exclude: string[];
12
13
  prependFiles?: PrependFiles;
14
+ keyframeLocalization?: boolean;
13
15
  }
14
16
  export declare function findLibraries(root: string, overrides?: Record<string, LibraryConfig | boolean>, warn?: (message: string) => void): ResolvedLibrary[];
@@ -3,7 +3,7 @@ import { dirname, join } from 'node:path';
3
3
  import { checkField } from './config.js';
4
4
  // Keys a library's field shares with the app's. `libraries` and
5
5
  // `quirksMode` only apply to the app.
6
- const SHARED_KEYS = new Set(['include', 'exclude', 'prependFiles']);
6
+ const SHARED_KEYS = new Set(['include', 'exclude', 'prependFiles', 'keyframeLocalization']);
7
7
  const APP_KEYS = new Set(['libraries', 'quirksMode']);
8
8
  const DEFAULT_INCLUDE = ['**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts,astro}'];
9
9
  const readPackage = (dir) => JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
@@ -44,13 +44,14 @@ export function findLibraries(root, overrides = {}, warn = () => { }) {
44
44
  for (const warning of checkField(config, where, new Set([...SHARED_KEYS, ...APP_KEYS])))
45
45
  warn(warning);
46
46
  }
47
- const { include, exclude, prependFiles } = config === true ? {} : config;
47
+ const { include, exclude, prependFiles, keyframeLocalization } = config === true ? {} : config;
48
48
  libraries.push({
49
49
  name,
50
50
  dir,
51
51
  include: include === undefined ? DEFAULT_INCLUDE : [include].flat(),
52
52
  exclude: exclude === undefined ? [] : [exclude].flat(),
53
53
  prependFiles,
54
+ keyframeLocalization,
54
55
  });
55
56
  // A library's peer dependencies are installed by the app, and found the
56
57
  // same way from the library.
@@ -29,8 +29,13 @@ async function openProject(params) {
29
29
  const compiler = createCompiler({
30
30
  root: app.dir,
31
31
  prependFiles: app.config.prependFiles,
32
- libraries: libraries.map(({ dir, prependFiles }) => ({ dir, prependFiles })),
32
+ libraries: libraries.map(({ dir, prependFiles, keyframeLocalization }) => ({
33
+ dir,
34
+ prependFiles,
35
+ keyframeLocalization,
36
+ })),
33
37
  quirksMode: app.config.quirksMode,
38
+ keyframeLocalization: app.config.keyframeLocalization,
34
39
  warn: () => { },
35
40
  });
36
41
  projects.set(params.projectHandle, compiler);
@@ -1,6 +1,7 @@
1
1
  import { pathToFileURL, fileURLToPath } from 'node:url';
2
2
  import { atRuleInfo } from './at-rules.js';
3
3
  import { addDeclarations, addGlobal, dropOverridden, key, parseRules, PreStyleError, valueOf, walk, } from './block.js';
4
+ import { localizeKeyframes } from './keyframes.js';
4
5
  import { compose, isElement } from './selectors.js';
5
6
  // Classes used as context for another class (`.card:hover .title`), or in a
6
7
  // compound with it (`.a.b`), are replaced with a marker: one generated name
@@ -144,6 +145,13 @@ export function parseStylesheet(code, lang, file, id, options) {
144
145
  }
145
146
  for (const target of classes.values())
146
147
  target.atoms = dropOverridden(target.atoms);
148
+ // Keyframes are local to the whole file, as in CSS Modules.
149
+ if (options.keyframeLocalization) {
150
+ const local = localizeKeyframes(parsed.globals);
151
+ parsed.globals = local.globals;
152
+ for (const target of classes.values())
153
+ target.atoms = local.atoms(target.atoms);
154
+ }
147
155
  return parsed;
148
156
  }
149
157
  // Flattens rules into entries, in order: style rules with their full
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pre.style/core",
3
- "version": "3.1.0-alpha.0",
3
+ "version": "3.1.0-alpha.1",
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",
@@ -60,6 +60,10 @@
60
60
  },
61
61
  "prependFiles": {
62
62
  "$ref": "#/definitions/prependFiles"
63
+ },
64
+ "keyframeLocalization": {
65
+ "description": "Give each @keyframes in the library's blocks and stylesheet files a generated name, used only in the block or file that defines it, like styled-components' `keyframes`. The app's setting doesn't apply to libraries.",
66
+ "type": "boolean"
63
67
  }
64
68
  }
65
69
  }
@@ -84,6 +88,10 @@
84
88
  "prependFiles": {
85
89
  "$ref": "#/definitions/prependFiles"
86
90
  },
91
+ "keyframeLocalization": {
92
+ "description": "Give each @keyframes in the app's blocks and stylesheet files a generated name, used only in the block or file that defines it (including keyframes from prepended files), like styled-components' `keyframes`. Libraries use their own setting.",
93
+ "type": "boolean"
94
+ },
87
95
  "libraries": {
88
96
  "description": "App only. Component libraries to compile, by package name, in addition to dependencies with a \"pre.style\" field. A config replaces the package's field, `true` uses the defaults, and `false` leaves the package out.",
89
97
  "type": "object",