@pre.style/core 0.0.0-stage → 3.0.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 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,456 @@
1
- # Temporary Holding Version
1
+ # pre.style
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://badge.fury.io/js/pre.style.svg)](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
+ ### Frameworks without `index.html`
92
+
93
+ 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:
94
+
95
+ ```ts
96
+ import 'virtual:pre.style.css';
97
+ ```
98
+
99
+ 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.
100
+
101
+ ### Astro
102
+
103
+ Add the plugin to Astro's Vite config, and import the CSS in your layout's frontmatter:
104
+
105
+ ```js
106
+ // astro.config.mjs
107
+ import { defineConfig } from 'astro/config';
108
+ import preStyle from '@pre.style/vite';
109
+
110
+ export default defineConfig({
111
+ vite: { plugins: [preStyle()] },
112
+ });
113
+ ```
114
+
115
+ ```astro
116
+ ---
117
+ import { css } from '@pre.style/core';
118
+ import 'virtual:pre.style.css';
119
+
120
+ const title = css`
121
+ font-size: 2rem;
122
+ `;
123
+ ---
124
+
125
+ <h1 class={title}>Hello</h1>
126
+ ```
127
+
128
+ 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.
129
+
130
+ ## Writing blocks
131
+
132
+ A block styles one element. Each block is treated as its own small stylesheet, where declarations at the top level apply to the element.
133
+
134
+ ### `css` and `scss`
135
+
136
+ `css` blocks are plain CSS. `scss` blocks are compiled with Sass first, so you can use variables, mixins, functions, and everything else Sass supports:
137
+
138
+ ```js
139
+ import { scss } from '@pre.style/core';
140
+
141
+ const title = scss`
142
+ $size: 2rem;
143
+ font-size: $size;
144
+ margin-bottom: $size / 2;
145
+ `;
146
+ ```
147
+
148
+ 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.
149
+
150
+ ### Selectors
151
+
152
+ 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:
153
+
154
+ ```js
155
+ css`
156
+ color: gray;
157
+
158
+ &:hover { color: black; }
159
+ &::before { content: '→'; }
160
+ & > * + * { margin-top: 1rem; }
161
+ .dark & { color: white; }
162
+ a { text-decoration: underline; } /* the same as `& a` */
163
+ `;
164
+ ```
165
+
166
+ ### At-rules
167
+
168
+ 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.
169
+
170
+ 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.
171
+
172
+ ```js
173
+ css`
174
+ @keyframes fade-in {
175
+ from { opacity: 0; }
176
+ }
177
+
178
+ animation: fade-in 200ms ease-out;
179
+ `;
180
+ ```
181
+
182
+ ### In HTML
183
+
184
+ HTML can't import anything, so blocks in HTML files that Vite processes, such as `index.html`, are found by matching `` css` `` and `` scss` ``:
185
+
186
+ ```html
187
+ <main class="css`padding: 1rem; max-width: 60rem;`">…</main>
188
+ ```
189
+
190
+ ## Combining class strings with `cn`
191
+
192
+ 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.
193
+
194
+ ```js
195
+ import { css, cn } from '@pre.style/core';
196
+
197
+ const button = css`
198
+ background-color: #0071ba;
199
+ color: white;
200
+ `;
201
+ const danger = css`
202
+ background-color: #c62828;
203
+ `;
204
+
205
+ const className = cn(button, isDanger && danger);
206
+ ```
207
+
208
+ `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.
209
+
210
+ 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.
211
+
212
+ 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.
213
+
214
+ > **Note:** `cn` removes `!important` declarations too. A later class that sets the same property wins, even when the earlier one is `!important`.
215
+
216
+ ## Sharing values with prepended files
217
+
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.
219
+
220
+ ```ts
221
+ preStyle({
222
+ prependFiles: {
223
+ css: ['src/styles/tokens.css'],
224
+ scss: ['src/styles/_variables.scss', 'src/styles/_mixins.scss'],
225
+ },
226
+ });
227
+ ```
228
+
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.
230
+
231
+ 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
+
233
+ ## Component libraries
234
+
235
+ 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.
236
+
237
+ ### Using a library
238
+
239
+ 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
+
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.
242
+
243
+ ```ts
244
+ preStyle({
245
+ libraries: {
246
+ 'legacy-ui': { include: 'dist/**/*.js' },
247
+ 'some-ui': false,
248
+ },
249
+ });
250
+ ```
251
+
252
+ A library's class names depend on the whole app, so they differ between apps. Don't depend on them.
253
+
254
+ ### Writing a library
255
+
256
+ - Ship ESM JavaScript (or TypeScript) with the blocks left as they are. Don't run pre.style on the library itself.
257
+ - List `@pre.style/core` as a peer dependency, so the library and the app share one copy.
258
+ - Add a `pre.style` field to `package.json`:
259
+
260
+ ```json
261
+ {
262
+ "name": "@acme/ui",
263
+ "peerDependencies": {
264
+ "@pre.style/core": "^3.0.0"
265
+ },
266
+ "pre.style": {
267
+ "include": "dist/**/*.js",
268
+ "prependFiles": {
269
+ "css": ["dist/styles/tokens.css"],
270
+ "scss": ["dist/styles/_tokens.scss"]
271
+ }
272
+ }
273
+ }
274
+ ```
275
+
276
+ | Field | Default | Description |
277
+ | -------------- | ---------------------------- | ----------- |
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. |
280
+
281
+ `"pre.style": true` uses the defaults.
282
+
283
+ 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
+
285
+ 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.
286
+
287
+ ## How the CSS is written
288
+
289
+ ### Class names
290
+
291
+ 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.
292
+
293
+ 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 (…) { … } }`.
294
+
295
+ 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.**
296
+
297
+ ### Shorthands
298
+
299
+ Shorthand properties are expanded into their longhands, so `cn` can resolve conflicts between them:
300
+
301
+ ```js
302
+ css`margin: auto;`;
303
+ ```
304
+
305
+ ```css
306
+ .D{margin-top:auto}
307
+ .C{margin-right:auto}
308
+ .A{margin-bottom:auto}
309
+ .B{margin-left:auto}
310
+ ```
311
+
312
+ `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:
313
+
314
+ ```js
315
+ css`padding: var(--space); padding-top: 0;`;
316
+ ```
317
+
318
+ 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.
319
+
320
+ ### Order
321
+
322
+ 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:
323
+
324
+ 1. Hoisted at-rules (`@layer` statements, `@import`, `@namespace`), then other copied at-rules.
325
+ 2. Classes on the element.
326
+ 3. Selector variants, with pseudo-classes in LVHFA order (`:link`, `:visited`, `:hover`, `:focus`, `:active`).
327
+ 4. At-rule groups. `@media` queries with only a `min-width` come last, smallest first (mobile-first).
328
+
329
+ ## Compared to utility-first frameworks
330
+
331
+ 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:
332
+
333
+ - **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.
334
+ - **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)).
335
+ - **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.
336
+ - **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.
337
+ - **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.
338
+
339
+ ## Configuration
340
+
341
+ All options are optional.
342
+
343
+ ```ts
344
+ preStyle({
345
+ include: ['src/**/*.{ts,tsx}', 'index.html'],
346
+ prependFiles: ['src/styles/tokens.css'],
347
+ lightningcss: { minify: true, targets: { safari: 16 << 16 } },
348
+ sourceMap: true,
349
+ quotes: 'single-quote',
350
+ output: {
351
+ file: 'css/pre.style.css',
352
+ banner: '/* Generated by pre.style */',
353
+ },
354
+ });
355
+ ```
356
+
357
+ | Option | Default | Description |
358
+ | -------------- | ------------------------------------------------- | ----------- |
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
+ | `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
+ | `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
+ | `quotes` | `'double-quote'` | Quotes around the class string that replaces each block in JS: `'single-quote'`, `'double-quote'`, or `'backtick'`. |
367
+ | `output.file` | `'pre.style.css'` | Path and name of the CSS file, relative to Vite's `build.outDir`. |
368
+ | `output.banner`| | Text added to the start of the CSS file. |
369
+ | `output.footer`| | Text added to the end of the CSS file. |
370
+
371
+ The CSS isn't minified unless you set `lightningcss: { minify: true }`.
372
+
373
+ ### Quirks mode
374
+
375
+ > [!WARNING]
376
+ > **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.
377
+
378
+ 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
+
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.
381
+
382
+ ## TypeScript
383
+
384
+ `css`, `scss`, and `cn` are typed. `css` and `scss` return `string`.
385
+
386
+ `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
+
388
+ ## Errors
389
+
390
+ The build fails, with the file, line, and column, when:
391
+
392
+ - a block has invalid CSS syntax;
393
+ - Sass fails to compile an `scss` block;
394
+ - a block contains `${…}` interpolation;
395
+ - `css` or `scss` is used other than as a tagged template;
396
+ - a project has an `scss` block but Sass isn't installed.
397
+
398
+ ## Known limitations
399
+
400
+ - `@charset` is dropped. Lightning CSS always writes UTF-8.
401
+ - At-rules Lightning CSS doesn't parse, such as `@when`, `@function`, and `@custom-selector`, are copied to the output as they are.
402
+ - Nested pseudo-elements, such as `::before::marker`, aren't supported yet.
403
+ - Blocks in PHP and other templates aren't supported, because Vite doesn't output them.
404
+ - 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
+ - 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.
407
+ - 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
+
409
+ ## Packages
410
+
411
+ | Package | Description |
412
+ | ----------------------- | ------------------------------------------------------ |
413
+ | `@pre.style/core` | `css`, `scss`, and `cn`, plus the compiler at `@pre.style/core/compiler` for bundler plugins. |
414
+ | `@pre.style/vite` | The Vite plugin. |
415
+
416
+ ## Development
417
+
418
+ The repo is an npm workspace with both packages under `packages/`.
419
+
420
+ ### Priorities
421
+
422
+ pre.style aims to be very fast, with the smallest and most performant output possible. When weighing a change, the priorities are, in order:
423
+
424
+ 1. **Output:** the size of what ships (the CSS file, class strings, and the `cn` lookup table) and how quickly the browser renders it.
425
+ 2. **Build time:** how long the compiler and plugin take, in production builds and in dev.
426
+
427
+ 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.
428
+
429
+ ```sh
430
+ npm install
431
+ npm test # node --test, on the TypeScript source
432
+ npm run typecheck
433
+ npm run lint # Oxlint
434
+ npm run format # Oxfmt
435
+ npm run build
436
+ ```
437
+
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.
439
+
440
+ ```sh
441
+ npm run demo:react-router # dev server
442
+ npm run demo:react-router:build # production build
443
+ npm run demo:react-router:start # serve the production build
444
+
445
+ npm run demo:tanstack-start # likewise for TanStack Start
446
+ npm run demo:tanstack-start:build
447
+ npm run demo:tanstack-start:start
448
+
449
+ npm run demo:astro # likewise for Astro
450
+ npm run demo:astro:build
451
+ npm run demo:astro:start
452
+ ```
453
+
454
+ ## License
455
+
456
+ 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;
package/dist/cn.js ADDED
@@ -0,0 +1,59 @@
1
+ import { SHORTHANDS } from './shorthands.js';
2
+ // clsx-style arguments, flattened into classes.
3
+ function collect(value, classes) {
4
+ if (!value)
5
+ return;
6
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'bigint') {
7
+ for (const c of String(value).split(/\s+/))
8
+ if (c)
9
+ classes.push(c);
10
+ }
11
+ else if (Array.isArray(value)) {
12
+ for (const v of value)
13
+ collect(v, classes);
14
+ }
15
+ else if (typeof value === 'object') {
16
+ for (const [key, on] of Object.entries(value))
17
+ if (on)
18
+ collect(key, classes);
19
+ }
20
+ }
21
+ // Removes earlier classes that later ones override: two classes conflict
22
+ // when they have the same prefix and set the same property. A later
23
+ // shorthand also removes earlier longhands it covers. Classes not in the
24
+ // table are kept.
25
+ function resolve(classes, lookup) {
26
+ const seen = new Set();
27
+ const kept = [];
28
+ for (let i = classes.length - 1; i >= 0; i--) {
29
+ const c = classes[i];
30
+ if (seen.has(c))
31
+ continue;
32
+ seen.add(c);
33
+ const entry = lookup.get(c);
34
+ if (entry !== undefined) {
35
+ const [prefix, property] = entry;
36
+ const key = `${prefix} ${property}`;
37
+ if (seen.has(key))
38
+ continue;
39
+ seen.add(key);
40
+ if (Object.hasOwn(SHORTHANDS, property))
41
+ for (const longhand of SHORTHANDS[property])
42
+ seen.add(`${prefix} ${longhand}`);
43
+ }
44
+ kept.push(c);
45
+ }
46
+ return kept.toReversed();
47
+ }
48
+ export function createCn(table) {
49
+ const lookup = new Map();
50
+ for (const [prefix, properties] of Object.entries(table))
51
+ for (const [property, classes] of Object.entries(properties))
52
+ for (const c of classes.split(' '))
53
+ lookup.set(c, [prefix, property]);
54
+ return (...inputs) => {
55
+ const classes = [];
56
+ collect(inputs, classes);
57
+ return resolve(classes, lookup).join(' ');
58
+ };
59
+ }
@@ -0,0 +1,8 @@
1
+ export interface AtRuleInfo {
2
+ hoist: number | false;
3
+ atomic: boolean;
4
+ overridable: boolean;
5
+ }
6
+ export declare const AT_RULES: Record<string, AtRuleInfo>;
7
+ export declare function atRuleInfo(type: string): AtRuleInfo;
8
+ export declare const NESTABLE_AT_RULES: Set<string>;
@@ -0,0 +1,41 @@
1
+ // Keyed by Lightning CSS rule type. Rules Lightning CSS doesn't parse
2
+ // (`@when`, `@function`, `@mixin`, `@apply`, `@custom-selector`,
3
+ // `@color-profile`, `@nest`) arrive as `unknown` rules and are kept as-is.
4
+ export const AT_RULES = {
5
+ import: { hoist: 2, atomic: false, overridable: false },
6
+ namespace: { hoist: 3, atomic: false, overridable: true },
7
+ media: { hoist: false, atomic: true, overridable: false },
8
+ supports: { hoist: false, atomic: true, overridable: false },
9
+ container: { hoist: false, atomic: true, overridable: false },
10
+ 'custom-media': { hoist: false, atomic: false, overridable: true },
11
+ 'layer-statement': { hoist: 1, atomic: false, overridable: false },
12
+ 'layer-block': { hoist: false, atomic: true, overridable: false },
13
+ scope: { hoist: false, atomic: true, overridable: false },
14
+ 'font-face': { hoist: false, atomic: false, overridable: true },
15
+ 'font-feature-values': { hoist: false, atomic: false, overridable: false },
16
+ 'font-palette-values': { hoist: false, atomic: false, overridable: true },
17
+ keyframes: { hoist: false, atomic: false, overridable: true },
18
+ 'starting-style': { hoist: false, atomic: true, overridable: false },
19
+ 'view-transition': { hoist: false, atomic: false, overridable: true },
20
+ property: { hoist: false, atomic: false, overridable: true },
21
+ page: { hoist: false, atomic: false, overridable: false },
22
+ 'position-try': { hoist: false, atomic: false, overridable: true },
23
+ 'counter-style': { hoist: false, atomic: false, overridable: true },
24
+ 'moz-document': { hoist: false, atomic: true, overridable: false },
25
+ viewport: { hoist: false, atomic: false, overridable: false },
26
+ };
27
+ const NOT_AT_RULE = { hoist: false, atomic: false, overridable: false };
28
+ export function atRuleInfo(type) {
29
+ return AT_RULES[type] ?? NOT_AT_RULE;
30
+ }
31
+ // At-rules that CSS nesting allows inside a style rule. Any other at-rule
32
+ // in a `css` block is moved out of the placeholder rule before parsing.
33
+ export const NESTABLE_AT_RULES = new Set([
34
+ 'media',
35
+ 'supports',
36
+ 'container',
37
+ 'layer',
38
+ 'scope',
39
+ 'starting-style',
40
+ '-moz-document',
41
+ ]);
@@ -0,0 +1,63 @@
1
+ import type { Declaration, Rule, SelectorList, TransformOptions, CustomAtRules } from 'lightningcss';
2
+ export type Lang = 'css' | 'scss';
3
+ export type LightningOptions = Omit<TransformOptions<CustomAtRules>, 'filename' | 'code' | 'sourceMap' | 'inputSourceMap' | 'projectRoot' | 'cssModules'>;
4
+ export interface Position {
5
+ line: number;
6
+ column: number;
7
+ }
8
+ export interface AtRuleSegment {
9
+ key: string;
10
+ rule: Rule;
11
+ }
12
+ export interface Atom {
13
+ atRules: AtRuleSegment[];
14
+ variant: {
15
+ key: string;
16
+ selectors: SelectorList;
17
+ } | null;
18
+ declaration: {
19
+ key: string;
20
+ value: Declaration;
21
+ important: boolean;
22
+ };
23
+ property: string;
24
+ shorthand: boolean;
25
+ position: Position | null;
26
+ }
27
+ export interface GlobalRule {
28
+ key: string;
29
+ rule: Rule;
30
+ hoist: number | false;
31
+ overrideKey: string | null;
32
+ position: Position | null;
33
+ }
34
+ export interface ParsedBlock {
35
+ atoms: Atom[];
36
+ globals: GlobalRule[];
37
+ }
38
+ export declare class PreStyleError extends Error {
39
+ readonly position: Position | null;
40
+ constructor(message: string, position: Position | null, options?: ErrorOptions);
41
+ }
42
+ export interface SassCompiler {
43
+ compileString(source: string, options?: {
44
+ logger?: {
45
+ warn?(message: string): void;
46
+ debug?(message: string): void;
47
+ };
48
+ }): {
49
+ css: string;
50
+ };
51
+ }
52
+ export interface ParseOptions {
53
+ lightningcss: LightningOptions;
54
+ prepend: Record<Lang, string>;
55
+ sass: SassCompiler | null;
56
+ warn: (message: string) => void;
57
+ }
58
+ export declare function parseBlock(code: string, lang: Lang, options: ParseOptions): ParsedBlock;
59
+ export declare const valueOf: (rule: Rule) => Record<string, any>;
60
+ export declare function propertyOf(declaration: Declaration): {
61
+ property: string;
62
+ shorthand: boolean;
63
+ };