@pre.style/core 3.0.0-alpha.0 → 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/README.md +216 -33
- package/dist/compiler/block.d.ts +28 -0
- package/dist/compiler/block.js +82 -24
- package/dist/compiler/compiler.d.ts +13 -0
- package/dist/compiler/compiler.js +190 -16
- package/dist/compiler/config.d.ts +15 -0
- package/dist/compiler/config.js +52 -0
- package/dist/compiler/index.d.ts +6 -1
- package/dist/compiler/index.js +3 -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/stylesheet.d.ts +15 -0
- package/dist/compiler/stylesheet.js +236 -0
- package/package.json +15 -3
- package/package.schema.json +109 -0
package/README.md
CHANGED
|
@@ -88,6 +88,8 @@ export function Card({ children }) {
|
|
|
88
88
|
|
|
89
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
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
|
+
|
|
91
93
|
### Frameworks without `index.html`
|
|
92
94
|
|
|
93
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:
|
|
@@ -179,6 +181,20 @@ css`
|
|
|
179
181
|
`;
|
|
180
182
|
```
|
|
181
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
|
+
|
|
182
198
|
### In HTML
|
|
183
199
|
|
|
184
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` ``:
|
|
@@ -213,20 +229,140 @@ When every argument is a block or a string literal, the plugin resolves the call
|
|
|
213
229
|
|
|
214
230
|
> **Note:** `cn` removes `!important` declarations too. A later class that sets the same property wins, even when the earlier one is `!important`.
|
|
215
231
|
|
|
216
|
-
##
|
|
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:
|
|
217
262
|
|
|
218
|
-
|
|
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`):
|
|
219
328
|
|
|
220
329
|
```ts
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
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
|
+
}
|
|
227
363
|
```
|
|
228
364
|
|
|
229
|
-
An array applies the files to every block. An object applies `css` files only to `css` blocks, and `scss` files only to `scss` blocks.
|
|
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`.
|
|
230
366
|
|
|
231
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.
|
|
232
368
|
|
|
@@ -238,15 +374,17 @@ A component library styled with pre.style ships its blocks uncompiled, and each
|
|
|
238
374
|
|
|
239
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.
|
|
240
376
|
|
|
241
|
-
To compile a package that doesn't have the field, or to change how one is compiled, use the `libraries`
|
|
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.
|
|
242
378
|
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
379
|
+
```json
|
|
380
|
+
{
|
|
381
|
+
"pre.style": {
|
|
382
|
+
"libraries": {
|
|
383
|
+
"legacy-ui": { "include": "dist/**/*.js" },
|
|
384
|
+
"some-ui": false
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
250
388
|
```
|
|
251
389
|
|
|
252
390
|
A library's class names depend on the whole app, so they differ between apps. Don't depend on them.
|
|
@@ -275,10 +413,11 @@ A library's class names depend on the whole app, so they differ between apps. Do
|
|
|
275
413
|
|
|
276
414
|
| Field | Default | Description |
|
|
277
415
|
| -------------- | ---------------------------- | ----------- |
|
|
278
|
-
| `include` | Every JS, TS, and Astro file | Globs of the files with blocks, relative to the package. |
|
|
279
|
-
| `
|
|
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. |
|
|
280
419
|
|
|
281
|
-
`"pre.style": true` uses the defaults.
|
|
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.
|
|
282
421
|
|
|
283
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`.
|
|
284
423
|
|
|
@@ -338,12 +477,52 @@ Utility-first frameworks such as [Tailwind CSS](https://tailwindcss.com/) also p
|
|
|
338
477
|
|
|
339
478
|
## Configuration
|
|
340
479
|
|
|
341
|
-
|
|
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.
|
|
342
523
|
|
|
343
524
|
```ts
|
|
344
525
|
preStyle({
|
|
345
|
-
include: ['src/**/*.{ts,tsx}', 'index.html'],
|
|
346
|
-
prependFiles: ['src/styles/tokens.css'],
|
|
347
526
|
lightningcss: { minify: true, targets: { safari: 16 << 16 } },
|
|
348
527
|
sourceMap: true,
|
|
349
528
|
quotes: 'single-quote',
|
|
@@ -356,13 +535,8 @@ preStyle({
|
|
|
356
535
|
|
|
357
536
|
| Option | Default | Description |
|
|
358
537
|
| -------------- | ------------------------------------------------- | ----------- |
|
|
359
|
-
| `include` | `**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts,html,astro}` | Globs of the files with blocks, relative to Vite's root. Scanned before a build to assign names. `node_modules` and the output directory are always skipped. |
|
|
360
|
-
| `exclude` | | Globs removed from `include`. |
|
|
361
|
-
| `prependFiles` | | Files added to the start of the app's blocks. See [Sharing values with prepended files](#sharing-values-with-prepended-files). |
|
|
362
|
-
| `libraries` | | Component libraries to compile, by package name, in addition to dependencies with a `pre.style` field. A config replaces the package's field, and `false` leaves the package out. See [Component libraries](#component-libraries). |
|
|
363
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. |
|
|
364
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. |
|
|
365
|
-
| `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). |
|
|
366
540
|
| `quotes` | `'double-quote'` | Quotes around the class string that replaces each block in JS: `'single-quote'`, `'double-quote'`, or `'backtick'`. |
|
|
367
541
|
| `output.file` | `'pre.style.css'` | Path and name of the CSS file, relative to Vite's `build.outDir`. |
|
|
368
542
|
| `output.banner`| | Text added to the start of the CSS file. |
|
|
@@ -370,6 +544,8 @@ preStyle({
|
|
|
370
544
|
|
|
371
545
|
The CSS isn't minified unless you set `lightningcss: { minify: true }`.
|
|
372
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
|
+
|
|
373
549
|
### Quirks mode
|
|
374
550
|
|
|
375
551
|
> [!WARNING]
|
|
@@ -377,7 +553,7 @@ The CSS isn't minified unless you set `lightningcss: { minify: true }`.
|
|
|
377
553
|
|
|
378
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.
|
|
379
555
|
|
|
380
|
-
If you can't add one, for example because your CSS is loaded into a page you don't control, set `quirksMode: true
|
|
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.
|
|
381
557
|
|
|
382
558
|
## TypeScript
|
|
383
559
|
|
|
@@ -385,6 +561,8 @@ If you can't add one, for example because your CSS is loaded into a page you don
|
|
|
385
561
|
|
|
386
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.
|
|
387
563
|
|
|
564
|
+
For stylesheet files, see [Types](#types).
|
|
565
|
+
|
|
388
566
|
## Errors
|
|
389
567
|
|
|
390
568
|
The build fails, with the file, line, and column, when:
|
|
@@ -393,7 +571,9 @@ The build fails, with the file, line, and column, when:
|
|
|
393
571
|
- Sass fails to compile an `scss` block;
|
|
394
572
|
- a block contains `${…}` interpolation;
|
|
395
573
|
- `css` or `scss` is used other than as a tagged template;
|
|
396
|
-
- a project has an `scss` block but Sass isn't installed
|
|
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.
|
|
397
577
|
|
|
398
578
|
## Known limitations
|
|
399
579
|
|
|
@@ -403,14 +583,17 @@ The build fails, with the file, line, and column, when:
|
|
|
403
583
|
- Blocks in PHP and other templates aren't supported, because Vite doesn't output them.
|
|
404
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.
|
|
405
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.
|
|
406
|
-
- Positions in `scss` blocks point to the start of the block, since Sass's output doesn't match the original lines.
|
|
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.
|
|
407
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).
|
|
408
591
|
|
|
409
592
|
## Packages
|
|
410
593
|
|
|
411
594
|
| Package | Description |
|
|
412
595
|
| ----------------------- | ------------------------------------------------------ |
|
|
413
|
-
| `@pre.style/core` | `css`, `scss`, and `cn`,
|
|
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. |
|
|
414
597
|
| `@pre.style/vite` | The Vite plugin. |
|
|
415
598
|
|
|
416
599
|
## Development
|
|
@@ -435,7 +618,7 @@ npm run format # Oxfmt
|
|
|
435
618
|
npm run build
|
|
436
619
|
```
|
|
437
620
|
|
|
438
|
-
`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 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.
|
|
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.
|
|
439
622
|
|
|
440
623
|
```sh
|
|
441
624
|
npm run demo:react-router # dev server
|
package/dist/compiler/block.d.ts
CHANGED
|
@@ -31,9 +31,22 @@ export interface GlobalRule {
|
|
|
31
31
|
overrideKey: string | null;
|
|
32
32
|
position: Position | null;
|
|
33
33
|
}
|
|
34
|
+
export interface Compose {
|
|
35
|
+
names: string[];
|
|
36
|
+
from: {
|
|
37
|
+
type: 'local';
|
|
38
|
+
} | {
|
|
39
|
+
type: 'global';
|
|
40
|
+
} | {
|
|
41
|
+
type: 'file';
|
|
42
|
+
path: string;
|
|
43
|
+
};
|
|
44
|
+
position: Position | null;
|
|
45
|
+
}
|
|
34
46
|
export interface ParsedBlock {
|
|
35
47
|
atoms: Atom[];
|
|
36
48
|
globals: GlobalRule[];
|
|
49
|
+
composes: Compose[];
|
|
37
50
|
}
|
|
38
51
|
export declare class PreStyleError extends Error {
|
|
39
52
|
readonly position: Position | null;
|
|
@@ -41,12 +54,14 @@ export declare class PreStyleError extends Error {
|
|
|
41
54
|
}
|
|
42
55
|
export interface SassCompiler {
|
|
43
56
|
compileString(source: string, options?: {
|
|
57
|
+
url?: URL;
|
|
44
58
|
logger?: {
|
|
45
59
|
warn?(message: string): void;
|
|
46
60
|
debug?(message: string): void;
|
|
47
61
|
};
|
|
48
62
|
}): {
|
|
49
63
|
css: string;
|
|
64
|
+
loadedUrls?: URL[];
|
|
50
65
|
};
|
|
51
66
|
}
|
|
52
67
|
export interface ParseOptions {
|
|
@@ -55,8 +70,21 @@ export interface ParseOptions {
|
|
|
55
70
|
sass: SassCompiler | null;
|
|
56
71
|
warn: (message: string) => void;
|
|
57
72
|
}
|
|
73
|
+
export declare const key: (value: unknown) => string;
|
|
58
74
|
export declare function parseBlock(code: string, lang: Lang, options: ParseOptions): ParsedBlock;
|
|
75
|
+
export declare function parseRules(css: string, lightningcss: LightningOptions, position: ToPosition, collect: (rules: Rule[]) => void): void;
|
|
76
|
+
export declare function dropOverridden(atoms: Atom[]): Atom[];
|
|
77
|
+
export type ToPosition = (loc: {
|
|
78
|
+
line: number;
|
|
79
|
+
column: number;
|
|
80
|
+
} | undefined) => Position | null;
|
|
59
81
|
export declare const valueOf: (rule: Rule) => Record<string, any>;
|
|
82
|
+
export declare function walk(rules: Rule[], atRules: AtRuleSegment[], variant: SelectorList | null, parsed: ParsedBlock, position: ToPosition): void;
|
|
83
|
+
export declare function addDeclarations(block: {
|
|
84
|
+
declarations?: Declaration[];
|
|
85
|
+
importantDeclarations?: Declaration[];
|
|
86
|
+
} | undefined, atRules: AtRuleSegment[], selectors: SelectorList, atoms: Atom[], composes: Compose[] | null, position: Position | null): void;
|
|
87
|
+
export declare function addGlobal(rule: Rule, atRules: AtRuleSegment[], globals: GlobalRule[], position: ToPosition): void;
|
|
60
88
|
export declare function propertyOf(declaration: Declaration): {
|
|
61
89
|
property: string;
|
|
62
90
|
shorthand: boolean;
|
package/dist/compiler/block.js
CHANGED
|
@@ -14,7 +14,7 @@ export class PreStyleError extends Error {
|
|
|
14
14
|
}
|
|
15
15
|
// JSON keys used to deduplicate. Locations are left out, so identical rules
|
|
16
16
|
// from different places share a key.
|
|
17
|
-
const key = (value) => JSON.stringify(value, (k, v) => (k === 'loc' ? undefined : v));
|
|
17
|
+
export const key = (value) => JSON.stringify(value, (k, v) => (k === 'loc' ? undefined : v));
|
|
18
18
|
const HOIST_ORDER = { charset: 0, layer: 1, import: 2, namespace: 3 };
|
|
19
19
|
const hoistOrder = (rule) => HOIST_ORDER[/^@([\w-]+)/.exec(rule)[1].toLowerCase()] ?? Infinity;
|
|
20
20
|
// Joins rules onto one line.
|
|
@@ -78,38 +78,46 @@ export function parseBlock(code, lang, options) {
|
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
-
const parsed = { atoms: [], globals: [] };
|
|
81
|
+
const parsed = { atoms: [], globals: [], composes: [] };
|
|
82
82
|
// Sass output positions don't match the block, so scss atoms have none.
|
|
83
83
|
const position = (loc) => lang === 'css' && loc ? toBlock(loc.line, loc.column) : null;
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
parseRules(css, options.lightningcss, position, (rules) => walk(rules, [], null, parsed, position));
|
|
85
|
+
return {
|
|
86
|
+
atoms: dropOverridden(parsed.atoms),
|
|
87
|
+
globals: parsed.globals,
|
|
88
|
+
composes: parsed.composes,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// Parses CSS with Lightning CSS, and passes its rules to `collect` once the
|
|
92
|
+
// user's visitor has changed them. `position` converts error locations.
|
|
93
|
+
export function parseRules(css, lightningcss, position, collect) {
|
|
94
|
+
const exit = {
|
|
86
95
|
StyleSheetExit(sheet) {
|
|
87
|
-
|
|
96
|
+
collect(sheet.rules);
|
|
88
97
|
},
|
|
89
98
|
};
|
|
90
|
-
const { visitor, ...rest } =
|
|
99
|
+
const { visitor, ...rest } = lightningcss;
|
|
91
100
|
try {
|
|
92
101
|
transform({
|
|
93
102
|
...rest,
|
|
94
103
|
filename: 'block.css',
|
|
95
104
|
code: Buffer.from(css),
|
|
96
|
-
visitor: visitor ? composeVisitors([visitor,
|
|
105
|
+
visitor: visitor ? composeVisitors([visitor, exit]) : exit,
|
|
97
106
|
});
|
|
98
107
|
}
|
|
99
108
|
catch (error) {
|
|
109
|
+
if (error instanceof PreStyleError)
|
|
110
|
+
throw error;
|
|
100
111
|
const loc = error.loc;
|
|
101
112
|
// Lightning CSS reports error lines from 1.
|
|
102
|
-
throw new PreStyleError(`Invalid CSS: ${error.message}`,
|
|
103
|
-
cause: error,
|
|
104
|
-
});
|
|
113
|
+
throw new PreStyleError(`Invalid CSS: ${error.message}`, loc ? position({ line: loc.line - 1, column: loc.column }) : null, { cause: error });
|
|
105
114
|
}
|
|
106
|
-
return { atoms: dropOverridden(parsed.atoms), globals: parsed.globals };
|
|
107
115
|
}
|
|
108
116
|
// Within a block, a later declaration overrides an earlier one that sets the
|
|
109
117
|
// same property under the same at-rules and selector variant, as in CSS.
|
|
110
118
|
// `!important` declarations are collected after normal ones in each rule,
|
|
111
119
|
// so they win. Identical atoms are kept once.
|
|
112
|
-
function dropOverridden(atoms) {
|
|
120
|
+
export function dropOverridden(atoms) {
|
|
113
121
|
const seen = new Set();
|
|
114
122
|
const kept = [];
|
|
115
123
|
for (let i = atoms.length - 1; i >= 0; i--) {
|
|
@@ -129,14 +137,15 @@ function dropOverridden(atoms) {
|
|
|
129
137
|
export const valueOf = (rule) => 'value' in rule && rule.value !== null ? rule.value : {};
|
|
130
138
|
const ruleLoc = (rule) => valueOf(rule).loc;
|
|
131
139
|
// Walks the rules. `variant` is the element's selector variant when inside a
|
|
132
|
-
// rule that targets the element, and null outside one.
|
|
133
|
-
|
|
140
|
+
// rule that targets the element, and null outside one. Without the
|
|
141
|
+
// placeholder, every rule is global.
|
|
142
|
+
export function walk(rules, atRules, variant, parsed, position) {
|
|
134
143
|
for (const rule of rules) {
|
|
135
144
|
if (rule.type === 'style') {
|
|
136
145
|
const { selectors, declarations, rules: children = [], loc } = rule.value;
|
|
137
146
|
if (variant !== null) {
|
|
138
147
|
const composed = compose(variant, selectors);
|
|
139
|
-
addDeclarations(declarations, atRules, composed, parsed, position(loc));
|
|
148
|
+
addDeclarations(declarations, atRules, composed, parsed.atoms, null, position(loc));
|
|
140
149
|
walk(children, atRules, composed, parsed, position);
|
|
141
150
|
continue;
|
|
142
151
|
}
|
|
@@ -151,15 +160,17 @@ function walk(rules, atRules, variant, parsed, position) {
|
|
|
151
160
|
other.push(selector);
|
|
152
161
|
}
|
|
153
162
|
if (other.length > 0)
|
|
154
|
-
addGlobal({ ...rule, value: { ...rule.value, selectors: other } }, atRules, parsed, position);
|
|
163
|
+
addGlobal({ ...rule, value: { ...rule.value, selectors: other } }, atRules, parsed.globals, position);
|
|
155
164
|
if (element.length > 0) {
|
|
156
|
-
|
|
165
|
+
// `composes` only works directly in the block, outside at-rules.
|
|
166
|
+
const composes = atRules.length === 0 && isElement(element) ? parsed.composes : null;
|
|
167
|
+
addDeclarations(declarations, atRules, element, parsed.atoms, composes, position(loc));
|
|
157
168
|
walk(children, atRules, element, parsed, position);
|
|
158
169
|
}
|
|
159
170
|
continue;
|
|
160
171
|
}
|
|
161
172
|
if (rule.type === 'nested-declarations' && variant !== null) {
|
|
162
|
-
addDeclarations(rule.value.declarations, atRules, variant, parsed, position(rule.value.loc));
|
|
173
|
+
addDeclarations(rule.value.declarations, atRules, variant, parsed.atoms, null, position(rule.value.loc));
|
|
163
174
|
continue;
|
|
164
175
|
}
|
|
165
176
|
if (atRuleInfo(rule.type).atomic) {
|
|
@@ -171,20 +182,40 @@ function walk(rules, atRules, variant, parsed, position) {
|
|
|
171
182
|
walk(children, [...atRules, segment], variant, parsed, position);
|
|
172
183
|
continue;
|
|
173
184
|
}
|
|
174
|
-
addGlobal(rule, atRules, parsed, position);
|
|
185
|
+
addGlobal(rule, atRules, parsed.globals, position);
|
|
175
186
|
}
|
|
176
187
|
}
|
|
177
|
-
|
|
188
|
+
const COMPOSES_PLACEMENT = "`composes` must come before the rule's other declarations, and can't be used inside nested selectors or at-rules.";
|
|
189
|
+
const isComposes = (declaration) => declaration.property === 'unparsed' && declaration.value.propertyId.property === 'composes';
|
|
190
|
+
// Adds a rule's declarations as atoms. `composes` declarations are added to
|
|
191
|
+
// `composes` when it's given, and are an error where it isn't (inside
|
|
192
|
+
// nested selectors and at-rules).
|
|
193
|
+
export function addDeclarations(block, atRules, selectors, atoms, composes, position) {
|
|
178
194
|
let variant = null;
|
|
179
195
|
if (!isElement(selectors)) {
|
|
180
196
|
const repaired = repair(selectors);
|
|
181
197
|
variant = { key: key(repaired), selectors: repaired };
|
|
182
198
|
}
|
|
199
|
+
if (block?.importantDeclarations?.some(isComposes))
|
|
200
|
+
throw new PreStyleError("`composes` can't be `!important`.", position);
|
|
201
|
+
let normal = block?.declarations ?? [];
|
|
202
|
+
// As in CSS Modules, `composes` comes before the rule's other declarations.
|
|
203
|
+
const leading = normal.findIndex((d) => !isComposes(d));
|
|
204
|
+
const own = leading === -1 ? normal.length : leading;
|
|
205
|
+
if (normal.slice(own).some(isComposes))
|
|
206
|
+
throw new PreStyleError(COMPOSES_PLACEMENT, position);
|
|
207
|
+
if (own > 0) {
|
|
208
|
+
if (composes === null)
|
|
209
|
+
throw new PreStyleError(COMPOSES_PLACEMENT, position);
|
|
210
|
+
for (const d of normal.slice(0, own))
|
|
211
|
+
composes.push(parseComposes(d, position));
|
|
212
|
+
normal = normal.slice(own);
|
|
213
|
+
}
|
|
183
214
|
const add = (declarations = [], important) => {
|
|
184
215
|
for (const declaration of declarations) {
|
|
185
216
|
for (const value of expand(declaration)) {
|
|
186
217
|
const { property, shorthand } = propertyOf(value);
|
|
187
|
-
|
|
218
|
+
atoms.push({
|
|
188
219
|
atRules,
|
|
189
220
|
variant,
|
|
190
221
|
declaration: { key: key(value) + (important ? '!' : ''), value, important },
|
|
@@ -195,11 +226,38 @@ function addDeclarations(block, atRules, selectors, parsed, position) {
|
|
|
195
226
|
}
|
|
196
227
|
}
|
|
197
228
|
};
|
|
198
|
-
add(
|
|
229
|
+
add(normal, false);
|
|
199
230
|
add(block?.importantDeclarations, true);
|
|
200
231
|
}
|
|
232
|
+
const ident = (t) => t?.type === 'token' && t.value.type === 'ident' ? t.value.value : null;
|
|
233
|
+
// Reads `composes: a b`, `composes: a from './file.css'`, or
|
|
234
|
+
// `composes: a from global`.
|
|
235
|
+
function parseComposes(declaration, position) {
|
|
236
|
+
const tokens = declaration.value.value.filter((t) => !(t.type === 'token' && t.value.type === 'white-space'));
|
|
237
|
+
const invalid = () => new PreStyleError('Invalid `composes`. Use `composes: a b;`, `composes: a from "./file.css";`, or `composes: a from global;`.', position);
|
|
238
|
+
const names = [];
|
|
239
|
+
let i = 0;
|
|
240
|
+
for (; i < tokens.length; i++) {
|
|
241
|
+
const name = ident(tokens[i]);
|
|
242
|
+
if (name === null || name === 'from')
|
|
243
|
+
break;
|
|
244
|
+
names.push(name);
|
|
245
|
+
}
|
|
246
|
+
if (names.length === 0)
|
|
247
|
+
throw invalid();
|
|
248
|
+
if (i === tokens.length)
|
|
249
|
+
return { names, from: { type: 'local' }, position };
|
|
250
|
+
if (ident(tokens[i]) !== 'from' || tokens.length !== i + 2)
|
|
251
|
+
throw invalid();
|
|
252
|
+
const source = tokens[i + 1];
|
|
253
|
+
if (ident(source) === 'global')
|
|
254
|
+
return { names, from: { type: 'global' }, position };
|
|
255
|
+
if (source.type === 'token' && source.value.type === 'string')
|
|
256
|
+
return { names, from: { type: 'file', path: source.value.value }, position };
|
|
257
|
+
throw invalid();
|
|
258
|
+
}
|
|
201
259
|
// Global rules inside at-rules keep their at-rules around them.
|
|
202
|
-
function addGlobal(rule, atRules,
|
|
260
|
+
export function addGlobal(rule, atRules, globals, position) {
|
|
203
261
|
let wrapped = rule;
|
|
204
262
|
for (const segment of atRules.toReversed()) {
|
|
205
263
|
wrapped = {
|
|
@@ -209,7 +267,7 @@ function addGlobal(rule, atRules, parsed, position) {
|
|
|
209
267
|
}
|
|
210
268
|
const info = atRules.length === 0 ? atRuleInfo(rule.type) : atRuleInfo('');
|
|
211
269
|
wrapped = repair(wrapped);
|
|
212
|
-
|
|
270
|
+
globals.push({
|
|
213
271
|
key: key(wrapped),
|
|
214
272
|
rule: wrapped,
|
|
215
273
|
hoist: info.hoist,
|
|
@@ -25,14 +25,27 @@ export interface Block {
|
|
|
25
25
|
line?: number;
|
|
26
26
|
column?: number;
|
|
27
27
|
}
|
|
28
|
+
export interface Stylesheet {
|
|
29
|
+
code: string;
|
|
30
|
+
lang: Lang;
|
|
31
|
+
file: string;
|
|
32
|
+
}
|
|
33
|
+
export interface CompiledStylesheet {
|
|
34
|
+
exports: [name: string, classString: string][];
|
|
35
|
+
dependencies: string[];
|
|
36
|
+
}
|
|
28
37
|
export declare function createCompiler(options?: CompilerOptions): Promise<Compiler>;
|
|
29
38
|
export declare class Compiler {
|
|
30
39
|
#private;
|
|
31
40
|
constructor(options: CompilerOptions, sass: SassCompiler | null);
|
|
32
41
|
get revision(): number;
|
|
33
42
|
scan(block: Block): void;
|
|
43
|
+
scanStylesheet(sheet: Stylesheet): void;
|
|
34
44
|
assignNames(): void;
|
|
35
45
|
classString(block: Block): string;
|
|
46
|
+
dependencies(block: Block): string[];
|
|
47
|
+
classNames(sheet: Stylesheet): string[];
|
|
48
|
+
stylesheet(sheet: Stylesheet): CompiledStylesheet;
|
|
36
49
|
css(): Printed;
|
|
37
50
|
cnTable(): CnTable;
|
|
38
51
|
}
|