@pre.style/core 0.0.0-stage → 3.1.0-alpha.0
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/LICENSE +21 -0
- package/README.md +638 -2
- package/dist/cn.d.ts +4 -0
- package/dist/cn.js +59 -0
- package/dist/compiler/at-rules.d.ts +8 -0
- package/dist/compiler/at-rules.js +41 -0
- package/dist/compiler/block.d.ts +91 -0
- package/dist/compiler/block.js +316 -0
- package/dist/compiler/compiler.d.ts +51 -0
- package/dist/compiler/compiler.js +489 -0
- package/dist/compiler/config.d.ts +15 -0
- package/dist/compiler/config.js +52 -0
- package/dist/compiler/expand.d.ts +2 -0
- package/dist/compiler/expand.js +224 -0
- package/dist/compiler/index.d.ts +10 -0
- package/dist/compiler/index.js +5 -0
- package/dist/compiler/libraries.d.ts +14 -0
- package/dist/compiler/libraries.js +75 -0
- package/dist/compiler/mapper.d.ts +1 -0
- package/dist/compiler/mapper.js +160 -0
- package/dist/compiler/print.d.ts +7 -0
- package/dist/compiler/print.js +32 -0
- package/dist/compiler/selectors.d.ts +8 -0
- package/dist/compiler/selectors.js +130 -0
- package/dist/compiler/split.d.ts +5 -0
- package/dist/compiler/split.js +71 -0
- package/dist/compiler/stylesheet.d.ts +15 -0
- package/dist/compiler/stylesheet.js +236 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +19 -0
- package/dist/shorthands.d.ts +1 -0
- package/dist/shorthands.js +190 -0
- package/package.json +88 -4
- package/package.schema.json +109 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017-2026 Benjamin Solum
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,639 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pre.style
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/pre.style)
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { css } from '@pre.style/core';
|
|
9
|
+
|
|
10
|
+
const button = css`
|
|
11
|
+
background-color: #0071ba;
|
|
12
|
+
color: white;
|
|
13
|
+
padding-block: 0.5em;
|
|
14
|
+
|
|
15
|
+
&:hover {
|
|
16
|
+
background-color: #005a94;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@media (min-width: 768px) {
|
|
20
|
+
padding-block: 1em;
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
```
|
|
24
|
+
|
|
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
|
+
|
|
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}
|
|
33
|
+
@media (width>=768px) {
|
|
34
|
+
.G{padding-block-start:1em}
|
|
35
|
+
.F{padding-block-end:1em}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- **No runtime.** Blocks are replaced with class strings during the build. Nothing is shipped to parse or inject CSS.
|
|
40
|
+
- **Small CSS.** Each declaration is written once, however many components use it, so the CSS file grows slowly as your project grows.
|
|
41
|
+
- **Just CSS.** Blocks are standard CSS (with nesting) or SCSS. There's no new syntax to learn.
|
|
42
|
+
- **Fast.** Built on [Lightning CSS](https://lightningcss.dev/). Every block is parsed once, in one process.
|
|
43
|
+
|
|
44
|
+
## Getting started
|
|
45
|
+
|
|
46
|
+
pre.style works with [Vite](https://vite.dev/). It needs Node 24 or later and Vite 8.
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npm install @pre.style/core
|
|
50
|
+
npm install --save-dev @pre.style/vite lightningcss
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
To write SCSS blocks, also install Sass:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
npm install --save-dev sass
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Add the plugin to `vite.config.ts`:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { defineConfig } from 'vite';
|
|
63
|
+
import preStyle from '@pre.style/vite';
|
|
64
|
+
|
|
65
|
+
export default defineConfig({
|
|
66
|
+
plugins: [preStyle()],
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Then import `css` (or `scss`) from `@pre.style/core` and use the class strings:
|
|
71
|
+
|
|
72
|
+
```jsx
|
|
73
|
+
import { css } from '@pre.style/core';
|
|
74
|
+
|
|
75
|
+
export function Card({ children }) {
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
className={css`
|
|
79
|
+
border-radius: 8px;
|
|
80
|
+
padding: 1rem;
|
|
81
|
+
`}
|
|
82
|
+
>
|
|
83
|
+
{children}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
In a build, the plugin writes `pre.style.css` to your output directory and links it from `index.html`. In dev, the CSS is served by Vite and updates as you edit.
|
|
90
|
+
|
|
91
|
+
You can also write styles in `.css` and `.scss` files and import their classes, like CSS Modules. See [Stylesheet files](#stylesheet-files).
|
|
92
|
+
|
|
93
|
+
### Frameworks without `index.html`
|
|
94
|
+
|
|
95
|
+
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
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import 'virtual:pre.style.css';
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
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.
|
|
102
|
+
|
|
103
|
+
### Astro
|
|
104
|
+
|
|
105
|
+
Add the plugin to Astro's Vite config, and import the CSS in your layout's frontmatter:
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
// astro.config.mjs
|
|
109
|
+
import { defineConfig } from 'astro/config';
|
|
110
|
+
import preStyle from '@pre.style/vite';
|
|
111
|
+
|
|
112
|
+
export default defineConfig({
|
|
113
|
+
vite: { plugins: [preStyle()] },
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```astro
|
|
118
|
+
---
|
|
119
|
+
import { css } from '@pre.style/core';
|
|
120
|
+
import 'virtual:pre.style.css';
|
|
121
|
+
|
|
122
|
+
const title = css`
|
|
123
|
+
font-size: 2rem;
|
|
124
|
+
`;
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
<h1 class={title}>Hello</h1>
|
|
128
|
+
```
|
|
129
|
+
|
|
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.
|
|
131
|
+
|
|
132
|
+
## Writing blocks
|
|
133
|
+
|
|
134
|
+
A block styles one element. Each block is treated as its own small stylesheet, where declarations at the top level apply to the element.
|
|
135
|
+
|
|
136
|
+
### `css` and `scss`
|
|
137
|
+
|
|
138
|
+
`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
|
+
|
|
140
|
+
```js
|
|
141
|
+
import { scss } from '@pre.style/core';
|
|
142
|
+
|
|
143
|
+
const title = scss`
|
|
144
|
+
$size: 2rem;
|
|
145
|
+
font-size: $size;
|
|
146
|
+
margin-bottom: $size / 2;
|
|
147
|
+
`;
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Blocks can't contain `${…}` interpolation. pre.style needs to know every block's CSS at build time. Share values with [prepended files](#sharing-values-with-prepended-files) instead.
|
|
151
|
+
|
|
152
|
+
### Selectors
|
|
153
|
+
|
|
154
|
+
Use `&` for the element, as in native CSS nesting. Anything that isn't the bare element (pseudo-classes, pseudo-elements, and complex selectors) becomes a selector variant:
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
css`
|
|
158
|
+
color: gray;
|
|
159
|
+
|
|
160
|
+
&:hover { color: black; }
|
|
161
|
+
&::before { content: '→'; }
|
|
162
|
+
& > * + * { margin-top: 1rem; }
|
|
163
|
+
.dark & { color: white; }
|
|
164
|
+
a { text-decoration: underline; } /* the same as `& a` */
|
|
165
|
+
`;
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### At-rules
|
|
169
|
+
|
|
170
|
+
Declarations inside `@media`, `@supports`, `@container`, `@layer`, `@scope`, and `@starting-style` become atomic classes too, grouped under one copy of each at-rule in the output. They can be nested inside each other and inside selectors.
|
|
171
|
+
|
|
172
|
+
Other at-rules, such as `@keyframes`, `@font-face`, and `@property`, are copied to the output as they are. When several blocks define the same one, it's written once. For at-rules that replace each other, such as two `@keyframes` with the same name, only the last definition is kept.
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
css`
|
|
176
|
+
@keyframes fade-in {
|
|
177
|
+
from { opacity: 0; }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
animation: fade-in 200ms ease-out;
|
|
181
|
+
`;
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### `composes` in blocks
|
|
185
|
+
|
|
186
|
+
A block can build on classes from a [stylesheet file](#stylesheet-files), or on global class names, with `composes`, as in CSS Modules:
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
css`
|
|
190
|
+
composes: button from './button.css';
|
|
191
|
+
composes: legacy-btn from global;
|
|
192
|
+
color: white;
|
|
193
|
+
`;
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The block's class string then includes the composed classes, and its own declarations replace the composed ones they conflict with, as `cn` would. `composes` must come before the block's other declarations, and can't be used inside nested selectors or at-rules.
|
|
197
|
+
|
|
198
|
+
### In HTML
|
|
199
|
+
|
|
200
|
+
HTML can't import anything, so blocks in HTML files that Vite processes, such as `index.html`, are found by matching `` css` `` and `` scss` ``:
|
|
201
|
+
|
|
202
|
+
```html
|
|
203
|
+
<main class="css`padding: 1rem; max-width: 60rem;`">…</main>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Combining class strings with `cn`
|
|
207
|
+
|
|
208
|
+
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
|
+
|
|
210
|
+
```js
|
|
211
|
+
import { css, cn } from '@pre.style/core';
|
|
212
|
+
|
|
213
|
+
const button = css`
|
|
214
|
+
background-color: #0071ba;
|
|
215
|
+
color: white;
|
|
216
|
+
`;
|
|
217
|
+
const danger = css`
|
|
218
|
+
background-color: #c62828;
|
|
219
|
+
`;
|
|
220
|
+
|
|
221
|
+
const className = cn(button, isDanger && danger);
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`cn` takes the same arguments as [clsx](https://github.com/lukeed/clsx): strings, arrays, objects whose keys are included when their value is truthy, and falsy values, which are ignored.
|
|
225
|
+
|
|
226
|
+
Two classes conflict when they set the same property under the same at-rules and selector. A later shorthand also removes earlier longhands it covers.
|
|
227
|
+
|
|
228
|
+
When every argument is a block or a string literal, the plugin resolves the call during the build, so it costs nothing at runtime. Other calls use a small lookup table the plugin generates for your project.
|
|
229
|
+
|
|
230
|
+
> **Note:** `cn` removes `!important` declarations too. A later class that sets the same property wins, even when the earlier one is `!important`.
|
|
231
|
+
|
|
232
|
+
## Stylesheet files
|
|
233
|
+
|
|
234
|
+
Styles can also live in `.css` and `.scss` files, imported with the `pre.style` import attribute. Each class in the file is exported with its class string, like [CSS Modules](https://github.com/css-modules/css-modules):
|
|
235
|
+
|
|
236
|
+
```css
|
|
237
|
+
/* button.css */
|
|
238
|
+
.button {
|
|
239
|
+
background-color: #0071ba;
|
|
240
|
+
color: white;
|
|
241
|
+
|
|
242
|
+
&:hover {
|
|
243
|
+
background-color: #005a94;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.primary-button {
|
|
248
|
+
composes: button;
|
|
249
|
+
font-weight: 600;
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
```jsx
|
|
254
|
+
import { button, 'primary-button' as primaryButton } from './button.css' with { type: 'pre.style' };
|
|
255
|
+
|
|
256
|
+
<button className={primaryButton}>Save</button>;
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`.css` files are compiled like `css` blocks, and `.scss` files like `scss` blocks, into the same atomic classes and the same CSS file. Each export is a plain string constant, so bundlers drop the ones you don't use. A class that isn't a valid identifier (`primary-button`) is exported by its string name, so import it with `as`. Only imports with `with { type: 'pre.style' }` are compiled this way; plain `import './global.css'` and `.module.css` imports work as usual. Your `tsconfig.json` needs `"module"` set to `"esnext"`, `"preserve"`, or a `"node*"` setting for TypeScript to allow the attribute.
|
|
260
|
+
|
|
261
|
+
How a file becomes classes:
|
|
262
|
+
|
|
263
|
+
- **Each class is a block.** A class's rules, wherever they are in the file, are compiled as one block with the class replaced by `&`. `.button:hover` is `&:hover`, `@media (…) { .button { … } }` is an at-rule class of `button`, and a later rule for the class overrides an earlier one, as in plain CSS.
|
|
264
|
+
- **A selector belongs to the class it styles:** the first class in the rightmost part of the selector that has one. `.card .title` belongs to `title`, and `.list > li` to `list` (as `& > li`).
|
|
265
|
+
- **Selector lists** (`.a, .b { … }`) apply the declarations to each class.
|
|
266
|
+
- **Rules without a class,** such as `:root`, `body`, `@keyframes`, and `@font-face`, are written to the CSS file as they are. So are `@import` rules, as in blocks.
|
|
267
|
+
- **Empty classes** (`.empty {}`) are exported as `''`.
|
|
268
|
+
- **Everything is global.** There's no `:global()` or `:local()`. Classes that aren't exported from the file, such as `.dark` in `.dark .button`, are written as they are, so you add them to elements yourself.
|
|
269
|
+
- **Classes from prepended files aren't exported.** Their rules are written as they are.
|
|
270
|
+
- **Every class in an imported file is written to the CSS file,** even the ones you don't use, as with CSS Modules.
|
|
271
|
+
|
|
272
|
+
In `.scss` files, relative `@use`, `@forward`, and `@import` resolve from the file, and [prepended files](#sharing-values-with-prepended-files) work as they do in blocks. Classes a prepended mixin writes into the file are the file's own.
|
|
273
|
+
|
|
274
|
+
### Classes used as context
|
|
275
|
+
|
|
276
|
+
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
|
+
|
|
278
|
+
```css
|
|
279
|
+
.card { padding-top: 1rem; }
|
|
280
|
+
.title { color: black; }
|
|
281
|
+
.card:hover .title { color: red; }
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
```css
|
|
285
|
+
.D{padding-top:1rem}
|
|
286
|
+
.C{color:#000}
|
|
287
|
+
.A:hover .B{color:red}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`card` is `"D A"`, where `A` is its marker, and `title` is `"C B"`.
|
|
291
|
+
|
|
292
|
+
> [!IMPORTANT]
|
|
293
|
+
> **Watch out for compound selectors.** In `.a.b { … }`, the first class, `a`, gets the declarations, and `b` becomes a marker. `b`'s export is only its marker, so `b` on its own applies none of those styles: they apply to elements with both `a` and `b`. Prefer separate classes, or a selector variant on one class (`.a { &.is-open { … } }`), when you can.
|
|
294
|
+
|
|
295
|
+
### `composes` in stylesheet files
|
|
296
|
+
|
|
297
|
+
`composes` works as in CSS Modules:
|
|
298
|
+
|
|
299
|
+
```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; }
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
> [!IMPORTANT]
|
|
307
|
+
> **`composes` resolves conflicts, unlike CSS Modules.** CSS Modules only joins class names. pre.style resolves `composes` like `cn(composed, own)`: the class's own declarations replace the composed ones they conflict with, and those are dropped from the class string. Above, `primary` has `color: blue` and not `color: gray`. As in CSS Modules, `composes` must come before the rule's other declarations, can't be used in nested selectors or at-rules, and fails the build if classes compose each other in a cycle.
|
|
308
|
+
|
|
309
|
+
Classes composed `from global` are added to the class string as they are. Composed classes keep their markers.
|
|
310
|
+
|
|
311
|
+
### Using exports with `cn`
|
|
312
|
+
|
|
313
|
+
Stylesheet exports are plain class strings, so they work with `cn`. Calls whose arguments are all blocks, string literals, or stylesheet exports (named imports, or members of a namespace import such as `styles.button`) are resolved during the build.
|
|
314
|
+
|
|
315
|
+
### Types
|
|
316
|
+
|
|
317
|
+
TypeScript 7.1 adds [content mappers](https://devblogs.microsoft.com/typescript/announcing-typescript-7-1-beta/), which `@pre.style/core` provides, so named imports are typed with each file's exact classes. Register it in `tsconfig.json`:
|
|
318
|
+
|
|
319
|
+
```json
|
|
320
|
+
{
|
|
321
|
+
"contentMappers": [{ "package": "@pre.style/core", "extensions": [".css", ".scss"] }]
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Content mappers run code from a package, so `tsc` needs the `--runExternalCode` flag, and editors need the workspace to be trusted. Mappers are chosen by file extension, so every `.css` and `.scss` file in the program gets pre.style's types, including ones imported without the attribute.
|
|
326
|
+
|
|
327
|
+
Before TypeScript 7.1, add this declaration to your project instead (for example as `src/stylesheets.d.ts`):
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
declare module '*.css' {
|
|
331
|
+
const classes: { readonly [name: string]: string };
|
|
332
|
+
export = classes;
|
|
333
|
+
}
|
|
334
|
+
declare module '*.scss' {
|
|
335
|
+
const classes: { readonly [name: string]: string };
|
|
336
|
+
export = classes;
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
TypeScript can't type named imports from it, so use a namespace import:
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
import * as styles from './button.css' with { type: 'pre.style' };
|
|
344
|
+
|
|
345
|
+
styles.button;
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Namespace imports still let bundlers drop the classes you don't use. **Remove the declaration when you register the content mapper:** TypeScript prefers it over the mapper.
|
|
349
|
+
|
|
350
|
+
## Sharing values with prepended files
|
|
351
|
+
|
|
352
|
+
Use `prependFiles`, in the `pre.style` field of your `package.json`, to add files to the start of every block and stylesheet file, for custom properties, SCSS variables, mixins, and so on. Each file is read once per build.
|
|
353
|
+
|
|
354
|
+
```json
|
|
355
|
+
{
|
|
356
|
+
"pre.style": {
|
|
357
|
+
"prependFiles": {
|
|
358
|
+
"css": ["src/styles/tokens.css"],
|
|
359
|
+
"scss": ["src/styles/_variables.scss", "src/styles/_mixins.scss"]
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
An array applies the files to every block. An object applies `css` files only to `css` blocks and `.css` files, and `scss` files only to `scss` blocks and `.scss` files. Paths are relative to the `package.json`.
|
|
366
|
+
|
|
367
|
+
Rules the files output, such as `:root { --space: 4px }`, appear in the CSS file once. Keep prepended files to things that don't output CSS, or output it once (variables, mixins, custom properties). Large libraries slow the build down.
|
|
368
|
+
|
|
369
|
+
## Component libraries
|
|
370
|
+
|
|
371
|
+
A component library styled with pre.style ships its blocks uncompiled, and each app's pre.style plugin compiles them along with the app's own. The library's classes share names and the CSS file with the app's, so a `color: red` in the library and one in the app are the same class, and `cn` resolves conflicts between them.
|
|
372
|
+
|
|
373
|
+
### Using a library
|
|
374
|
+
|
|
375
|
+
Install it. The plugin finds dependencies with a `pre.style` field in their `package.json` (and the libraries those depend on) and compiles them. There's nothing to configure.
|
|
376
|
+
|
|
377
|
+
To compile a package that doesn't have the field, or to change how one is compiled, use the `libraries` setting in your `package.json`'s `pre.style` field. `true` uses the defaults, and `false` leaves a package out.
|
|
378
|
+
|
|
379
|
+
```json
|
|
380
|
+
{
|
|
381
|
+
"pre.style": {
|
|
382
|
+
"libraries": {
|
|
383
|
+
"legacy-ui": { "include": "dist/**/*.js" },
|
|
384
|
+
"some-ui": false
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
A library's class names depend on the whole app, so they differ between apps. Don't depend on them.
|
|
391
|
+
|
|
392
|
+
### Writing a library
|
|
393
|
+
|
|
394
|
+
- Ship ESM JavaScript (or TypeScript) with the blocks left as they are. Don't run pre.style on the library itself.
|
|
395
|
+
- List `@pre.style/core` as a peer dependency, so the library and the app share one copy.
|
|
396
|
+
- Add a `pre.style` field to `package.json`:
|
|
397
|
+
|
|
398
|
+
```json
|
|
399
|
+
{
|
|
400
|
+
"name": "@acme/ui",
|
|
401
|
+
"peerDependencies": {
|
|
402
|
+
"@pre.style/core": "^3.0.0"
|
|
403
|
+
},
|
|
404
|
+
"pre.style": {
|
|
405
|
+
"include": "dist/**/*.js",
|
|
406
|
+
"prependFiles": {
|
|
407
|
+
"css": ["dist/styles/tokens.css"],
|
|
408
|
+
"scss": ["dist/styles/_tokens.scss"]
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
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. |
|
|
419
|
+
|
|
420
|
+
`"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
|
+
|
|
422
|
+
To let apps use the library's tokens in their own blocks, export the token files, and apps can add them to their own `prependFiles`.
|
|
423
|
+
|
|
424
|
+
The plugin excludes libraries from Vite's dependency pre-bundling and from SSR externalization, so their blocks reach it in dev and on the server.
|
|
425
|
+
|
|
426
|
+
## How the CSS is written
|
|
427
|
+
|
|
428
|
+
### Class names
|
|
429
|
+
|
|
430
|
+
Each class is one declaration, under the at-rules and selector variant it was written in. Every class gets a name from one list, and the most-used classes get the shortest ones: one character for the first 53, then two characters for the next few thousand. Names don't say what a class is under; `cn` gets that from its lookup table.
|
|
431
|
+
|
|
432
|
+
Declarations that mean the same thing share a class: `color: blue` and `color: #00f` are the same class. So do equivalent nestings, such as `@media (…) { &:hover { … } }` and `&:hover { @media (…) { … } }`.
|
|
433
|
+
|
|
434
|
+
Production builds scan your files first, so names are stable between builds. In dev, names are assigned as files are served. **Dev class names can change between dev sessions and differ from production names, so don't depend on them.**
|
|
435
|
+
|
|
436
|
+
### Shorthands
|
|
437
|
+
|
|
438
|
+
Shorthand properties are expanded into their longhands, so `cn` can resolve conflicts between them:
|
|
439
|
+
|
|
440
|
+
```js
|
|
441
|
+
css`margin: auto;`;
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
```css
|
|
445
|
+
.D{margin-top:auto}
|
|
446
|
+
.C{margin-right:auto}
|
|
447
|
+
.A{margin-bottom:auto}
|
|
448
|
+
.B{margin-left:auto}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
`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
|
+
|
|
453
|
+
```js
|
|
454
|
+
css`padding: var(--space); padding-top: 0;`;
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
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.
|
|
458
|
+
|
|
459
|
+
### Order
|
|
460
|
+
|
|
461
|
+
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
|
+
|
|
463
|
+
1. Hoisted at-rules (`@layer` statements, `@import`, `@namespace`), then other copied at-rules.
|
|
464
|
+
2. Classes on the element.
|
|
465
|
+
3. Selector variants, with pseudo-classes in LVHFA order (`:link`, `:visited`, `:hover`, `:focus`, `:active`).
|
|
466
|
+
4. At-rule groups. `@media` queries with only a `min-width` come last, smallest first (mobile-first).
|
|
467
|
+
|
|
468
|
+
## Compared to utility-first frameworks
|
|
469
|
+
|
|
470
|
+
Utility-first frameworks such as [Tailwind CSS](https://tailwindcss.com/) also produce atomic CSS, so the same styles cost about the same in either. The differences are elsewhere:
|
|
471
|
+
|
|
472
|
+
- **CSS file.** Each declaration is written once in both, so the classes themselves come to about the same size. pre.style adds no reset or base styles, and no supporting rules beyond what your blocks contain. A framework's reset can be larger than all of a small app's classes.
|
|
473
|
+
- **Markup.** pre.style's class names are generated and one to three characters long, so class strings are much shorter than utility names like `px-5 py-4 rounded-xl`. Class strings end up in your JS and in every server-rendered page, so this saving grows with your markup. Some shorthands do produce many classes (see [Shorthands](#shorthands)).
|
|
474
|
+
- **Combining classes.** `cn` is resolved during the build when its arguments are known, and otherwise uses a small table generated for your project. Utility frameworks need a separate library that knows every utility to do the same.
|
|
475
|
+
- **In the browser.** No difference. Both are static CSS with single-class selectors and nothing running at runtime, so apart from download size the browser does the same work.
|
|
476
|
+
- **Build speed.** A utility framework only scans your files for class names. pre.style parses every block with Lightning CSS, and runs Sass for `scss` blocks. That takes more time during the build, but your users never see it.
|
|
477
|
+
|
|
478
|
+
## Configuration
|
|
479
|
+
|
|
480
|
+
Options live in one of two places. Each option is set in exactly one of them.
|
|
481
|
+
|
|
482
|
+
### In `package.json`
|
|
483
|
+
|
|
484
|
+
Options every tool that reads your styles has to agree on (the bundler plugin, and TypeScript's [content mapper](#types)) go in a `pre.style` field in your app's `package.json`. The plugin reads the `package.json` nearest Vite's root, and paths in it are relative to that file. All options are optional.
|
|
485
|
+
|
|
486
|
+
```json
|
|
487
|
+
{
|
|
488
|
+
"pre.style": {
|
|
489
|
+
"include": ["src/**/*.{ts,tsx}", "index.html"],
|
|
490
|
+
"prependFiles": ["src/styles/tokens.css"]
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
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). |
|
|
502
|
+
|
|
503
|
+
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
|
+
|
|
505
|
+
```json
|
|
506
|
+
{
|
|
507
|
+
"json.schemas": [
|
|
508
|
+
{
|
|
509
|
+
"fileMatch": ["package.json"],
|
|
510
|
+
"schema": {
|
|
511
|
+
"properties": {
|
|
512
|
+
"pre.style": { "$ref": "./node_modules/@pre.style/core/package.schema.json" }
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
]
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Plugin options
|
|
521
|
+
|
|
522
|
+
Options about how a build writes its output go to the plugin. All options are optional.
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
preStyle({
|
|
526
|
+
lightningcss: { minify: true, targets: { safari: 16 << 16 } },
|
|
527
|
+
sourceMap: true,
|
|
528
|
+
quotes: 'single-quote',
|
|
529
|
+
output: {
|
|
530
|
+
file: 'css/pre.style.css',
|
|
531
|
+
banner: '/* Generated by pre.style */',
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
```
|
|
535
|
+
|
|
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. |
|
|
544
|
+
|
|
545
|
+
The CSS isn't minified unless you set `lightningcss: { minify: true }`.
|
|
546
|
+
|
|
547
|
+
Before 3.1, `include`, `exclude`, `prependFiles`, `libraries`, and `quirksMode` were plugin options. Passing them to the plugin now fails with a message pointing to `package.json`.
|
|
548
|
+
|
|
549
|
+
### Quirks mode
|
|
550
|
+
|
|
551
|
+
> [!WARNING]
|
|
552
|
+
> **You probably don't want `quirksMode`.** It makes every project's output larger, and it only helps pages that can't have a doctype. If your pages start with `<!doctype html>`, as almost every modern page does, leave it off.
|
|
553
|
+
|
|
554
|
+
Pages without a doctype render in quirks mode, where class selectors ignore case. pre.style uses both uppercase and lowercase letters in class names, so `.A` and `.a` would apply to each other's elements. Add `<!doctype html>` to fix this. The plugin warns during the build when an HTML file it processes has no doctype.
|
|
555
|
+
|
|
556
|
+
If you can't add one, for example because your CSS is loaded into a page you don't control, set `"quirksMode": true` in your `package.json`'s `pre.style` field. Class names then use only lowercase letters, digits, `-`, and `_`. That leaves 27 one-character names instead of 53, and about 1,050 two-character names instead of 3,445, so names get longer once a project has more than about 27 classes.
|
|
557
|
+
|
|
558
|
+
## TypeScript
|
|
559
|
+
|
|
560
|
+
`css`, `scss`, and `cn` are typed. `css` and `scss` return `string`.
|
|
561
|
+
|
|
562
|
+
`css` and `scss` must be used as tagged templates (`` css`…` ``), and every block must be compiled by the plugin. If code that uses them runs without the plugin, for example in tests run directly in Node, they throw an error saying the transform wasn't applied, instead of silently rendering unstyled.
|
|
563
|
+
|
|
564
|
+
For stylesheet files, see [Types](#types).
|
|
565
|
+
|
|
566
|
+
## Errors
|
|
567
|
+
|
|
568
|
+
The build fails, with the file, line, and column, when:
|
|
569
|
+
|
|
570
|
+
- a block has invalid CSS syntax;
|
|
571
|
+
- Sass fails to compile an `scss` block;
|
|
572
|
+
- a block contains `${…}` interpolation;
|
|
573
|
+
- `css` or `scss` is used other than as a tagged template;
|
|
574
|
+
- a project has an `scss` block or file but Sass isn't installed;
|
|
575
|
+
- a stylesheet file has invalid CSS, or fails to compile with Sass;
|
|
576
|
+
- `composes` comes after other declarations, is used in a nested selector or at-rule, names a class or file that doesn't exist, or forms a cycle.
|
|
577
|
+
|
|
578
|
+
## Known limitations
|
|
579
|
+
|
|
580
|
+
- `@charset` is dropped. Lightning CSS always writes UTF-8.
|
|
581
|
+
- At-rules Lightning CSS doesn't parse, such as `@when`, `@function`, and `@custom-selector`, are copied to the output as they are.
|
|
582
|
+
- Nested pseudo-elements, such as `::before::marker`, aren't supported yet.
|
|
583
|
+
- Blocks in PHP and other templates aren't supported, because Vite doesn't output them.
|
|
584
|
+
- Component libraries are found among the app's dependencies and the dependencies of other libraries. A library used only through a package that isn't one (a wrapper without a `pre.style` field) isn't found automatically; add it with the `libraries` option.
|
|
585
|
+
- Before a build, Astro components are scanned like HTML, by matching `` css` `` and `` scss` ``. In a component that imports from `@pre.style/core`, text that only looks like a block, such as a code sample, is compiled too, and fails the build if it isn't valid CSS.
|
|
586
|
+
- Positions in `scss` blocks point to the start of the block, since Sass's output doesn't match the original lines. In `.scss` files, Sass errors have positions, but source maps don't.
|
|
587
|
+
- In `.scss` files, rules from modules loaded with `@use` are written as they are, like prepended files' rules, and their classes aren't exported: Sass writes them before the file's own rules.
|
|
588
|
+
- Stylesheet imports must be static strings (`import`, `export … from`, and `import('./file.css', { with: { type: 'pre.style' } })`), and can't have a query.
|
|
589
|
+
- TypeScript's content mapper handles every `.css` and `.scss` file in the program, since mappers are chosen by extension. A project that also imports Vite's `.module.css` files without the attribute gets pre.style's types for them.
|
|
590
|
+
- Pages need a doctype (`<!doctype html>`). Without one, browsers use quirks mode, where class selectors ignore case, so classes like `.A` and `.a` would apply to each other's elements. See [Quirks mode](#quirks-mode).
|
|
591
|
+
|
|
592
|
+
## Packages
|
|
593
|
+
|
|
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. |
|
|
598
|
+
|
|
599
|
+
## Development
|
|
600
|
+
|
|
601
|
+
The repo is an npm workspace with both packages under `packages/`.
|
|
602
|
+
|
|
603
|
+
### Priorities
|
|
604
|
+
|
|
605
|
+
pre.style aims to be very fast, with the smallest and most performant output possible. When weighing a change, the priorities are, in order:
|
|
606
|
+
|
|
607
|
+
1. **Output:** the size of what ships (the CSS file, class strings, and the `cn` lookup table) and how quickly the browser renders it.
|
|
608
|
+
2. **Build time:** how long the compiler and plugin take, in production builds and in dev.
|
|
609
|
+
|
|
610
|
+
A change that makes the output smaller or faster to render is worth a slower build. A faster build isn't worth a larger or slower output.
|
|
611
|
+
|
|
612
|
+
```sh
|
|
613
|
+
npm install
|
|
614
|
+
npm test # node --test, on the TypeScript source
|
|
615
|
+
npm run typecheck
|
|
616
|
+
npm run lint # Oxlint
|
|
617
|
+
npm run format # Oxfmt
|
|
618
|
+
npm run build
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
`demo/` has three apps for trying pre.style on the client and server: a React Router app in framework mode (`react/react-router`), a TanStack Start app (`react/tanstack-start`), and an Astro app with React islands (`astro`). They have the same pages, and share a small React component library (`react/ui`), which ships its source with the blocks still in it (and a component styled from a `.scss` stylesheet file) and its design tokens, for each app's pre.style plugin to compile. The demos use the packages' TypeScript source, so changes to them show up without a build.
|
|
622
|
+
|
|
623
|
+
```sh
|
|
624
|
+
npm run demo:react-router # dev server
|
|
625
|
+
npm run demo:react-router:build # production build
|
|
626
|
+
npm run demo:react-router:start # serve the production build
|
|
627
|
+
|
|
628
|
+
npm run demo:tanstack-start # likewise for TanStack Start
|
|
629
|
+
npm run demo:tanstack-start:build
|
|
630
|
+
npm run demo:tanstack-start:start
|
|
631
|
+
|
|
632
|
+
npm run demo:astro # likewise for Astro
|
|
633
|
+
npm run demo:astro:build
|
|
634
|
+
npm run demo:astro:start
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
## License
|
|
638
|
+
|
|
639
|
+
MIT
|
package/dist/cn.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type CnTable = Record<string, Record<string, string>>;
|
|
2
|
+
export type ClassValue = string | number | bigint | boolean | null | undefined | ClassDictionary | ClassValue[];
|
|
3
|
+
export type ClassDictionary = Record<string, unknown>;
|
|
4
|
+
export declare function createCn(table: CnTable): (...inputs: ClassValue[]) => string;
|