@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 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
- ## Sharing values with prepended files
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
- Use `prependFiles` to add files to the start of every block, for custom properties, SCSS variables, mixins, and so on. Each file is read once per build.
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
- preStyle({
222
- prependFiles: {
223
- css: ['src/styles/tokens.css'],
224
- scss: ['src/styles/_variables.scss', 'src/styles/_mixins.scss'],
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` option. `false` leaves a package out.
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
- ```ts
244
- preStyle({
245
- libraries: {
246
- 'legacy-ui': { include: 'dist/**/*.js' },
247
- 'some-ui': false,
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
- | `prependFiles` | | Files added to the start of the library's blocks, relative to the package. They apply only to the library's blocks, not the app's, so the library's tokens and mixins are always available to it and never leak into the app. |
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
- All options are optional.
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`. 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.
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`, plus the compiler at `@pre.style/core/compiler` for bundler plugins. |
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
@@ -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;
@@ -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
- // Collected on exit, after the user's visitor has changed the rules.
85
- const collect = {
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
- walk(sheet.rules, [], null, parsed, position);
96
+ collect(sheet.rules);
88
97
  },
89
98
  };
90
- const { visitor, ...rest } = options.lightningcss;
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, collect]) : collect,
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}`, lang === 'css' && loc ? toBlock(loc.line - 1, loc.column) : null, {
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
- function walk(rules, atRules, variant, parsed, position) {
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
- addDeclarations(declarations, atRules, element, parsed, position(loc));
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
- function addDeclarations(block, atRules, selectors, parsed, position) {
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
- parsed.atoms.push({
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(block?.declarations, false);
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, parsed, position) {
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
- parsed.globals.push({
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
  }