@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 +144 -71
- package/dist/compiler/block.d.ts +2 -0
- package/dist/compiler/block.js +9 -7
- package/dist/compiler/compiler.d.ts +3 -0
- package/dist/compiler/compiler.js +118 -23
- package/dist/compiler/config.d.ts +1 -0
- package/dist/compiler/config.js +10 -1
- package/dist/compiler/keyframes.d.ts +6 -0
- package/dist/compiler/keyframes.js +120 -0
- package/dist/compiler/libraries.d.ts +2 -0
- package/dist/compiler/libraries.js +3 -2
- package/dist/compiler/mapper.js +6 -1
- package/dist/compiler/stylesheet.js +8 -0
- package/package.json +1 -1
- package/package.schema.json +8 -0
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# pre.style
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package
|
|
3
|
+
[](https://www.npmjs.com/package/@pre.style/core)
|
|
4
4
|
|
|
5
5
|
Zero-runtime CSS-in-JS. Write plain CSS or SCSS next to your markup, and pre.style compiles it at build time into atomic CSS classes: one small class per declaration, shared across your whole project.
|
|
6
6
|
|
|
7
7
|
```js
|
|
8
|
-
import { css } from
|
|
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{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
|
|
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{
|
|
35
|
-
|
|
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
|
|
63
|
-
import preStyle from
|
|
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
|
|
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
|
|
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
|
|
110
|
-
import preStyle from
|
|
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 ``
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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 {
|
|
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 {
|
|
280
|
-
|
|
281
|
-
|
|
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{
|
|
286
|
-
|
|
287
|
-
|
|
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 {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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": [
|
|
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
|
|
384
|
+
declare module "*.css" {
|
|
331
385
|
const classes: { readonly [name: string]: string };
|
|
332
386
|
export = classes;
|
|
333
387
|
}
|
|
334
|
-
declare module
|
|
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
|
|
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
|
|
415
|
-
|
|
|
416
|
-
| `include`
|
|
417
|
-
| `exclude`
|
|
418
|
-
| `prependFiles`
|
|
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`
|
|
496
|
+
css`
|
|
497
|
+
margin: auto;
|
|
498
|
+
`;
|
|
442
499
|
```
|
|
443
500
|
|
|
444
501
|
```css
|
|
445
|
-
.D{
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
.
|
|
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`
|
|
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
|
|
496
|
-
|
|
|
497
|
-
| `include`
|
|
498
|
-
| `exclude`
|
|
499
|
-
| `prependFiles`
|
|
500
|
-
| `libraries`
|
|
501
|
-
| `quirksMode`
|
|
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": {
|
|
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
|
-
|
|
599
|
+
layer: "pre.style",
|
|
600
|
+
quotes: "single-quote",
|
|
529
601
|
output: {
|
|
530
|
-
file:
|
|
531
|
-
banner:
|
|
602
|
+
file: "css/pre.style.css",
|
|
603
|
+
banner: "/* Generated by pre.style */",
|
|
532
604
|
},
|
|
533
605
|
});
|
|
534
606
|
```
|
|
535
607
|
|
|
536
|
-
| Option
|
|
537
|
-
|
|
|
538
|
-
| `lightningcss`
|
|
539
|
-
| `sourceMap`
|
|
540
|
-
| `
|
|
541
|
-
| `
|
|
542
|
-
| `output.
|
|
543
|
-
| `output.
|
|
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
|
|
595
|
-
|
|
|
596
|
-
| `@pre.style/core`
|
|
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
|
|
package/dist/compiler/block.d.ts
CHANGED
|
@@ -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;
|
package/dist/compiler/block.js
CHANGED
|
@@ -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
|
-
|
|
86
|
-
|
|
87
|
-
globals: parsed.globals,
|
|
88
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
55
|
-
|
|
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 {
|
|
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 {
|
|
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),
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
//
|
|
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: [
|
|
389
|
-
: { declarations: [
|
|
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
|
|
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): {
|
package/dist/compiler/config.js
CHANGED
|
@@ -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([
|
|
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,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.
|
package/dist/compiler/mapper.js
CHANGED
|
@@ -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 }) => ({
|
|
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
package/package.schema.json
CHANGED
|
@@ -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",
|