ReviewOS

also looking at this

stacks/ts-css

Merge @stacksjs/ts-css into this repo as packages/toolkit

#27
Merged glennmichael123 wants to merge merge/ts-css-toolkit into main
128 files +9797 -201
.config/buddy-bot.tsmodified+2-2
Changes to .config/buddy-bot.ts
@@ -2,8 +2,8 @@ import type { BuddyBotConfig } from 'buddy-bot'
22
33const config: BuddyBotConfig = {
44 repository: {
5 owner: 'stacksjs',
6 name: 'ts-css',
5 owner: 'cwcss',
6 name: 'crosswind',
77 provider: 'github',
88 // Uses GITHUB_TOKEN by default
99 },
.env.examplemodified+1-1
Changes to .env.example
@@ -12,7 +12,7 @@
1212# The release workflow uses this file only when both it and that secret exist,
1313# and otherwise falls back to a plain NPM_TOKEN secret.
1414
15# npm token used to publish `ts-css`. Publishing a package that does not exist
15# npm token used to publish `@ts-css/core`. Publishing a package that does not exist
1616# yet needs a token that can CREATE packages — an Automation token, or a
1717# granular token with read/write across all packages. A granular token scoped
1818# to existing packages returns 404 on the first publish.
.github/workflows/release.ymlmodified+1-1
Changes to .github/workflows/release.yml
@@ -28,7 +28,7 @@ jobs:
2828 run: bun install
2929
3030 - name: Build
31 run: cd packages/ts-css && bun run build
31 run: bun run build
3232
3333 - name: Cross-compile Binaries
3434 run: cd packages/ts-css && bun run compile:all
.gitignoremodified+3-0
Changes to .gitignore
@@ -29,4 +29,7 @@ temp
2929packages/ts-css/bin/cssx
3030packages/ts-css/bin/cssx-*
3131packages/ts-css/bin/*.zip
32packages/toolkit/bin/ts-css
33packages/toolkit/bin/ts-css-*
34packages/toolkit/bin/*.zip
3235pantry
CLAUDE.mdmodified+1-1
Changes to CLAUDE.md
@@ -2,7 +2,7 @@
22
33## About
44
5`ts-css` (published unscoped on npm; CLI binary `cssx`, config file `css.config.ts`, class prefix `tc`) is an on-demand CSS engine built with Bun, with two front ends over one atomic output:
5`ts-css` (published on npm as `@ts-css/core`; CLI binary `cssx`, config file `css.config.ts`, class prefix `tc`) is an on-demand CSS engine built with Bun, with two front ends over one atomic output:
66
77- **Utility classes** — Tailwind v4-compatible, with full variant support (responsive, state, dark mode, pseudo-elements), arbitrary values, shortcut aliases, and compile-class HTML optimization.
88- **Style objects** — a StyleX-shaped typed API (`css.create`, `css.props`, `css.defineVars`, `css.createTheme`, `css.keyframes`) in `src/style/`, collected at build time by evaluating the modules that declare them.
README.mdmodified+6-6
Changes to README.md
@@ -11,7 +11,7 @@ A CSS engine with two front ends and one atomic output.
1111Write **utility classes** when you are laying out markup, and Tailwind v4 semantics apply unchanged. Write **typed style objects** when a style has to be computed, tokenised, or merged across a component boundary, and you get StyleX's authoring model. Both compile through the same pipeline into the same deduplicated atomic CSS, in one stylesheet, with nothing left at runtime.
1212
1313```bash
14bun add ts-css
14bun add @ts-css/core
1515```
1616
1717## Why two APIs
@@ -43,7 +43,7 @@ bunx cssx watch # rebuilds on change
4343
4444```ts
4545// css.config.ts
46import type { TsCssOptions } from 'ts-css'
46import type { TsCssOptions } from '@ts-css/core'
4747
4848export default {
4949 content: ['./src/**/*.{html,ts,tsx,stx}'],
@@ -57,7 +57,7 @@ export default {
5757## Style objects
5858
5959```ts
60import { css } from 'ts-css'
60import { css } from '@ts-css/core'
6161
6262const theme = css.defineVars({
6363 accent: '#0b7',
@@ -223,7 +223,7 @@ cssx clean # remove the output file
223223## Configuration
224224
225225```ts
226import type { TsCssOptions } from 'ts-css'
226import type { TsCssOptions } from '@ts-css/core'
227227
228228export default {
229229 content: ['./src/**/*.{html,ts,tsx}'],
@@ -296,7 +296,7 @@ The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information.
296296Made with ��
297297
298298<!-- Badges -->
299[npm-version-src]: https://img.shields.io/npm/v/ts-css?style=flat-square
300[npm-version-href]: https://npmjs.com/package/ts-css
299[npm-version-src]: https://img.shields.io/npm/v/@ts-css/core?style=flat-square
300[npm-version-href]: https://npmjs.com/package/@ts-css/core
301301[github-actions-src]: https://img.shields.io/github/actions/workflow/status/cwcss/crosswind/ci.yml?style=flat-square&branch=main
302302[github-actions-href]: https://github.com/cwcss/crosswind/actions?query=workflow%3Aci
benchmark/style-api.bench.tsmodified+1-1
Changes to benchmark/style-api.bench.ts
@@ -157,7 +157,7 @@ const transpiler = new Bun.Transpiler({ loader: 'ts' })
157157
158158const tsCssSources = new Map(WORKLOADS.map(w => [
159159 w.name,
160 `import { css } from 'ts-css'\nexport const styles = css.create(${w.literal})\n`,
160 `import { css } from '@ts-css/core'\nexport const styles = css.create(${w.literal})\n`,
161161]))
162162
163163/**
bun.lockmodified+76-16
Changes to bun.lock
@@ -3,24 +3,46 @@
33 "configVersion": 1,
44 "workspaces": {
55 "": {
6 "name": "ts-css",
6 "name": "@ts-css/core",
77 "devDependencies": {
88 "better-dx": "^0.2.15",
99 },
1010 },
11 "packages/toolkit": {
12 "name": "@stacksjs/ts-css",
13 "version": "0.1.4",
14 "bin": {
15 "ts-css": "./dist/bin/cli.js",
16 },
17 "dependencies": {
18 "bunfig": "^0.15.6",
19 },
20 "devDependencies": {
21 "@types/bun": "latest",
22 "@types/css-tree": "^2.3.11",
23 "@types/csso": "^5.0.4",
24 "better-dx": "^0.2.15",
25 "css-select": "^7.0.0",
26 "css-tree": "^3.2.1",
27 "css-what": "^8.0.0",
28 "csso": "^5.0.5",
29 "mitata": "^1.0.34",
30 },
31 },
1132 "packages/ts-css": {
12 "name": "ts-css",
33 "name": "@ts-css/core",
1334 "version": "0.3.0",
1435 "bin": {
1536 "cssx": "./dist/cli.js",
1637 },
1738 "dependencies": {
1839 "@stacksjs/clapp": "^0.2.0",
40 "@stacksjs/ts-css": "workspace:*",
1941 "bunfig": "^0.15.17",
2042 },
2143 },
2244 "packages/vscode": {
23 "name": "ts-css-vscode",
45 "name": "@ts-css/vscode",
2446 "version": "0.3.0",
2547 },
2648 },
@@ -35,11 +57,11 @@
3557
3658 "@stacksjs/bunpress": ["@stacksjs/bunpress@0.2.14", "", { "dependencies": { "@cwcss/crosswind": "^0.2.16", "@stacksjs/clapp": "^0.2.0", "@stacksjs/stx": "^0.2.171", "@stacksjs/ts-i18n": "^0.1.11", "bunfig": "^0.15.17", "ts-syntax-highlighter": "^0.2.14" }, "peerDependencies": { "@stacksjs/ts-cloud": "^0.7.51" }, "optionalPeers": ["@stacksjs/ts-cloud"], "bin": { "bunpress": "./dist/bin/cli.js" } }, "sha512-o0Bf8b7kXKSyH5xXrVrdA46gPzMVssLBnbP82t8WzF17gIveFLNW+CzEZe3n9o+Ui2wiy2+6IF5SwGfrdTtWcQ=="],
3759
38 "@stacksjs/clapp": ["@stacksjs/clapp@0.2.13", "", {}, "sha512-wh6Ybgga1S/3P+GRDb2kBiOK97+Ay7TKTiaLKfhpgd+V04+BAHg23xDlZUNkpllhoRdFimeHpWahuwghoNYbUg=="],
60 "@stacksjs/clapp": ["@stacksjs/clapp@0.2.15", "", {}, "sha512-EM2fWrFZaU+paHZ4X0RGNZTnBYVgwdkbTnYdLD72yoc8JXUH5Wl88IZcn/JZoFPrViNgXlhYgI3xEK4Fryd3Xw=="],
3961
4062 "@stacksjs/clarity": ["@stacksjs/clarity@0.3.33", "", { "dependencies": { "bunfig": "^0.15.6" }, "bin": { "clarity": "./dist/bin/cli.js" } }, "sha512-+5GLKnAI/M/z64UfNLLfQqx/kp+frmRY+NZXYwwGwbrkud/xlsEGDWEExdhqOhqEDMWdb1oafw/ubGAPg1PaFw=="],
4163
42 "@stacksjs/desktop": ["@stacksjs/desktop@0.2.237", "", { "dependencies": { "craft-native": ">=0.0.76" } }, "sha512-hpC2xTRn6uxgNPMomJBURQG7TYJPxWFoLzpzJePzKVpi0o2lEt4vZRxAGdZRQgc0ZTm9QCP3pTqRR+k3t0bEbw=="],
64 "@stacksjs/desktop": ["@stacksjs/desktop@0.2.249", "", { "dependencies": { "craft-native": ">=0.0.76" } }, "sha512-mQfxzJW2wGzTFffa1NIYVA0qSbJH7QSHocYJbKZxBSQR5qD8u3SXfpxDNg3YcgoHCj6CfH/BsuSg5jqaiyRTpw=="],
4365
4466 "@stacksjs/dtsx": ["@stacksjs/dtsx@0.11.10", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0" }, "bin": { "dtsx": "./dist/bin/cli.js" } }, "sha512-NOSSGv7mdxzmijmKHxTZDmgZp7vCYrRKWpDwNqwTc97MTv5csjo3uukf7A/c+s7pSzJd+HxqsTCZ7IznjfEtmw=="],
4567
@@ -47,15 +69,15 @@
4769
4870 "@stacksjs/logsmith": ["@stacksjs/logsmith@0.2.11", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "bunfig": "^0.15.6", "pickier": "^0.1.9" }, "bin": { "@stacksjs/logsmith": "./dist/bin/cli.js", "logsmith": "./dist/bin/cli.js" } }, "sha512-wqqAxewpsHVDzpC+IRy+yYyGXGlv1j5GCqYTh8fL7d3LLhnRPN72PEQywM357burtORvdkFHfOzk3Ih1SMvuiw=="],
4971
50 "@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.237", "", {}, "sha512-hx51XqFdkoeVEKGXj083SGyY57BnWFRu8yWTrPk/FZlVqwup8XQGyiF0FTcf/u2z4pyRzpQ/pDCgsTQsh+d6tQ=="],
72 "@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.249", "", {}, "sha512-u7xHcDDeyj+IfDsqAEGpuFoZeRWuKfpnpexfyjPCOvBhJlFWIYKvFVY9QjuKN/kFQxDv5hYK9vkROpZAIVSzNQ=="],
5173
52 "@stacksjs/stx": ["@stacksjs/stx@0.2.237", "", { "dependencies": { "@cwcss/crosswind": "^0.2.19", "@stacksjs/clapp": "^0.2.0", "@stacksjs/desktop": "0.2.237", "@stacksjs/sanitizer": "0.2.237", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.237", "bunfig": "^0.15.17", "stx-router": "0.2.237", "ts-broadcasting": "^0.0.9", "ts-images": "^0.2.11", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-trsm4du5m4doOJwZztrVgYlClUxwi5wzfb79V/o5Vu5iw9WIdFbfwiOz/zcxDeoqBp6+eAVqD0bY1Jvq7/wABQ=="],
74 "@stacksjs/stx": ["@stacksjs/stx@0.2.249", "", { "dependencies": { "@cwcss/crosswind": "^0.2.19", "@stacksjs/clapp": "^0.2.14", "@stacksjs/desktop": "0.2.249", "@stacksjs/sanitizer": "0.2.249", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.249", "bunfig": "^0.15.17", "stx-router": "0.2.249", "ts-broadcasting": "^0.0.9", "ts-images": "^0.2.11", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-3r2RgvcrnJZ4FKwsjFMyOPwpyKtBV8RWnTVGby5C9XCKe0d/DsF125+A/8LAXTjB4z32NUYRDVnSpvhd1vRxPA=="],
5375
5476 "@stacksjs/ts-avif": ["@stacksjs/ts-avif@0.1.4", "", {}, "sha512-+ri7xxHLxaFGRJPihI7irDZpdLEIrEFpR7Em+ojfDbTjXQmXtQBqN/9WKNhBdQ1BZ2M1ckjdgnxmfege86QQ5Q=="],
5577
5678 "@stacksjs/ts-bmp": ["@stacksjs/ts-bmp@0.1.1", "", {}, "sha512-TIgm4rbCoYOijlg2gQuAyJErXeeoVzra4snxNvZ2fe6QBGIA0gm5147oo3t/cIeTMUzbmnyJ1g0QbT4kP4Ck9Q=="],
5779
58 "@stacksjs/ts-css": ["@stacksjs/ts-css@0.1.4", "", { "dependencies": { "bunfig": "^0.15.6" }, "bin": { "ts-css": "./dist/bin/cli.js" } }, "sha512-QvHvgagJ6kQY3+zY37andUEJFKq/v4v2JpDCrvVdbX3TYCLuRQExZl8Qf3ai+Fl9BN2oWikH+DfKSs5HuEZrGQ=="],
80 "@stacksjs/ts-css": ["@stacksjs/ts-css@workspace:packages/toolkit"],
5981
6082 "@stacksjs/ts-i18n": ["@stacksjs/ts-i18n@0.1.11", "", { "dependencies": { "@stacksjs/clapp": "^0.2.9", "bunfig": "^0.15.6" }, "bin": { "i18n": "./dist/cli.js" } }, "sha512-33FM0gKdxBy6EUsUZLGAMe0D7QUZ4f/BQXRsQIyzkZgs2+juruUTevPuJGCjWSZsgJuiaBfEJn8Oq8ctuo1OWQ=="],
6183
@@ -67,8 +89,16 @@
6789
6890 "@stacksjs/ts-webp": ["@stacksjs/ts-webp@0.1.3", "", {}, "sha512-0PFmmBLKCxIc+8pNEwfvoXfS5uH47y7rXpVaGEfj6VQGPoQCAWUmheppCeJ66YCdSVR1dezR/4MGTZCnT+uZow=="],
6991
92 "@ts-css/core": ["@ts-css/core@workspace:packages/ts-css"],
93
94 "@ts-css/vscode": ["@ts-css/vscode@workspace:packages/vscode"],
95
7096 "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="],
7197
98 "@types/css-tree": ["@types/css-tree@2.3.11", "", {}, "sha512-aEokibJOI77uIlqoBOkVbaQGC9zII0A+JH1kcTNKW2CwyYWD8KM6qdo+4c77wD3wZOQfJuNWAr9M4hdk+YhDIg=="],
99
100 "@types/csso": ["@types/csso@5.0.5", "", { "dependencies": { "@types/css-tree": "^2" } }, "sha512-Et/B3W1/1XMjG9SxwitiokWDabA9N8otVoTBJF2RmZ8itU0c5h7qmuZcTpp17BqfAnsdfkGQ3hwizff3nJvWSQ=="],
101
72102 "@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="],
73103
74104 "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
@@ -113,37 +143,61 @@
113143
114144 "better-dx": ["better-dx@0.2.25", "", { "dependencies": { "@stacksjs/bumpx": "^0.2.14", "@stacksjs/bunpress": "^0.2.8", "@stacksjs/clapp": "^0.2.12", "@stacksjs/clarity": "^0.3.31", "@stacksjs/gitlint": "^0.1.7", "@stacksjs/logsmith": "^0.2.6", "@types/bun": "^1.3.14", "buddy-bot": "^0.10.5", "bun-git-hooks": "^0.3.2", "bun-plugin-dtsx": "^0.11.1", "bunfig": "^0.15.15", "pickier": "^0.1.40", "ts-error-handling": "^0.1.3", "typescript": "^7.0.2" } }, "sha512-7GQ3EDXDwdZbpzwqWrOC2kzKkfw2Ptd30Bl1VlLS1SV82MvU5VIYXtGsRcJ5+6M17n9hQwGuT2TGEZEaWgZQaw=="],
115145
146 "boolbase": ["boolbase@2.0.0", "", {}, "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA=="],
147
116148 "buddy-bot": ["buddy-bot@0.10.5", "", { "dependencies": { "prompts": "^2.4.2", "ts-pantry": "^0.11.32" }, "peerDependencies": { "@anthropic-ai/sdk": "^0.117.1" }, "optionalPeers": ["@anthropic-ai/sdk"], "bin": { "buddy-bot": "./dist/bin/cli.js" } }, "sha512-pGyjHSi91Nix76u1VIVLEYEV6vkTVKvKS9NvRYbyGR6X6CdZQrvHcmJLfGqjN/4xuUWc4h6rlVpkiwG6MOo/6Q=="],
117149
118150 "bun-git-hooks": ["bun-git-hooks@0.3.3", "", { "bin": { "git-hooks": "./dist/bin/cli.js", "bun-git-hooks": "./dist/bin/cli.js" } }, "sha512-kcg8teRh/cMs8uZyt/+A/bBuJnmc6EvjnBJ12pWEJ61L9z3uSuqm0mvjrYX4Iu7SZPAwpvAK5fFKmqevS4WBkA=="],
119151
120152 "bun-plugin-dtsx": ["bun-plugin-dtsx@0.11.10", "", { "dependencies": { "@stacksjs/dtsx": "0.11.10" } }, "sha512-qQrEBIV1pzhDmrxmhTHhgVK5gqE5CfpO8C9NCp7zUaN48c4IJVrpqXbeNWch7sAT/Hf4GhVMWau7T65xySm59Q=="],
121153
122 "bun-plugin-stx": ["bun-plugin-stx@0.2.237", "", { "dependencies": { "@stacksjs/stx": "^0.2.227", "stx-router": "^0.2.227" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-LYBUiSNKtdkTHVeiUAYFPrrNGJWu4o/jLjOi2K1MiWDuOCqaQ6VHZItuw52nR4suVHOL5hbVJWRS93sQ+7dXxA=="],
154 "bun-plugin-stx": ["bun-plugin-stx@0.2.249", "", { "dependencies": { "@stacksjs/stx": "^0.2.227", "stx-router": "^0.2.227" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-TQ6UEPX54pATWLq27VnkQCAiOBbiM1roOPye6nbSjgmRj54mqz9we0HPfepfj+ahYvkniEiwIVw9Ym3xCXNjzQ=="],
123155
124156 "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="],
125157
126158 "bunfig": ["bunfig@0.15.17", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./bin/cli.js" } }, "sha512-nJAYGUq29lMmKindrIQIU5MD0JFFRoVFt+PBoNEvvVO9gKS3WgsYQVQLzN0gzHNnZtOZSR9p7jTBiRCwjQpr1g=="],
127159
128 "craft-native": ["craft-native@0.0.77", "", { "bin": { "craft-sdk": "./dist/cli.js" } }, "sha512-hBqFxZC22GVt8wv3srd/rsDTKykSzM/aFJDDM8+W/Mk8CuZDFjlOIkRSnyJqZwyVf4bvkRzdegd9fiOfnwUNOg=="],
160 "craft-native": ["craft-native@0.0.83", "", { "bin": { "craft-sdk": "./dist/cli.js" } }, "sha512-6FWJmaTG8TYpxKvj7gCYhkylq8jeY6anE75zNcxGbZDRt5AeaKpLBIdCo2AlCHkw1amuFUdahrJ6vPZdInGbQQ=="],
161
162 "css-select": ["css-select@7.0.0", "", { "dependencies": { "boolbase": "^2.0.0", "css-what": "^8.0.0", "domhandler": "^6.0.1", "domutils": "^4.0.2", "nth-check": "^3.0.1" } }, "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g=="],
163
164 "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
165
166 "css-what": ["css-what@8.0.0", "", {}, "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw=="],
167
168 "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
129169
130170 "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
131171
172 "dom-serializer": ["dom-serializer@3.1.1", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "entities": "^8.0.0" } }, "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw=="],
173
174 "domelementtype": ["domelementtype@3.0.0", "", {}, "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg=="],
175
176 "domhandler": ["domhandler@6.0.1", "", { "dependencies": { "domelementtype": "^3.0.0" } }, "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg=="],
177
178 "domutils": ["domutils@4.0.2", "", { "dependencies": { "dom-serializer": "^3.0.0", "domelementtype": "^3.0.0", "domhandler": "^6.0.0" } }, "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA=="],
179
180 "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
181
132182 "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
133183
184 "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
185
186 "mitata": ["mitata@1.0.34", "", {}, "sha512-Mc3zrtNBKIMeHSCQ0XqRLo1vbdIx1wvFV9c8NJAiyho6AjNfMY8bVhbS12bwciUdd1t4rj8099CH3N3NFahaUA=="],
187
188 "nth-check": ["nth-check@3.0.1", "", { "dependencies": { "boolbase": "^2.0.0" } }, "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ=="],
189
134190 "pickier": ["pickier@0.1.65", "", { "dependencies": { "@stacksjs/clapp": "^0.2.3", "@stacksjs/clarity": "^0.3.24" }, "optionalDependencies": { "@stacksjs/ts-spell-check": "^0.1.0" }, "bin": { "pickier": "./dist/bin/cli.js" } }, "sha512-hpGKvrWwzzHeV3hMErtnZoGRD/5Wmvy9sKxxHAWvMMdmdzr2DMtuk2T8Vkr8ZoUCNeTKRRPgHTMHg0oCNxDnDA=="],
135191
136192 "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
137193
138194 "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
139195
140 "stx-router": ["stx-router@0.2.237", "", {}, "sha512-sEuFmBW4YIgaF79AkBrqhBmkN6t6sdFISCHlalxoLb+QK4xyesc7CXnk6UOcLTkfEP89ojHyvM/8yvK2bqYdFg=="],
141
142 "ts-broadcasting": ["ts-broadcasting@0.0.9", "", { "bin": { "broadcast": "./dist/bin/cli.js" } }, "sha512-OA95bHPe3M/24Gx8FpbUV0+NzGwvemG2LP64s1p/zATagX8xJckeziru03ccjW0ExWaD+W5tC1GRIqS6lfbB4A=="],
196 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
143197
144 "ts-css": ["ts-css@workspace:packages/ts-css"],
198 "stx-router": ["stx-router@0.2.249", "", {}, "sha512-zKCQH2S/qj2FbxYhreS9UdCw0vKGiD1lx//ZohPaXqmIIKdha4gL219DyV7gfdQKrZbjOGEf6N7dN+tvu42gbQ=="],
145199
146 "ts-css-vscode": ["ts-css-vscode@workspace:packages/vscode"],
200 "ts-broadcasting": ["ts-broadcasting@0.0.9", "", { "bin": { "broadcast": "./dist/bin/cli.js" } }, "sha512-OA95bHPe3M/24Gx8FpbUV0+NzGwvemG2LP64s1p/zATagX8xJckeziru03ccjW0ExWaD+W5tC1GRIqS6lfbB4A=="],
147201
148202 "ts-error-handling": ["ts-error-handling@0.1.3", "", {}, "sha512-Agm8wbQL1YVvnffVDSUKMgImZBoR4UVFjzPaIwPxa0z7YwsowKV8R57pCDIjLP/dTHRBetg7ZfdTeuNVb21OfA=="],
149203
@@ -153,12 +207,18 @@
153207
154208 "ts-jpeg": ["ts-jpeg@0.3.5", "", {}, "sha512-etdIfMDRDYDF44kDu++z0M5GbR7ph9IKmubSXQop1O8NTUc+xjxE8dPt/niosHRUhMiQ4gaLtuFh0DH6QvfopA=="],
155209
156 "ts-pantry": ["ts-pantry@0.11.44", "", { "bin": { "ts-pantry": "./dist/bin/cli.js" } }, "sha512-xnvzhbhwdRk75aUyyXiSkSICRJNcUYR+xXtLUmRRufZiEBf4T1zrIPiEdgPWDhN0PYLCeA7GNi098yHxMcVvqg=="],
210 "ts-pantry": ["ts-pantry@0.11.45", "", { "bin": { "ts-pantry": "./dist/bin/cli.js" } }, "sha512-wh8pGg7EYrK0bR2rz71SFMniJSq9OFNMZTVkxvk04ywedSP8ECjYLpEg/MK1VRGJjW2/FfIZskkxVQq211jjSw=="],
157211
158212 "ts-syntax-highlighter": ["ts-syntax-highlighter@0.2.17", "", { "bin": { "ts-syntax-highlighter": "./dist/bin/cli.js" } }, "sha512-cHxkTRvqYabO9axIrw6KUkMweIHzXmVmcVmO+Yi2NEa9nxQa15zoISvvlKYPzK4OLsUS5I4uVaLz0Oivc3oOSw=="],
159213
160214 "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
161215
162216 "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
217
218 "@stacksjs/ts-svg/@stacksjs/ts-css": ["@stacksjs/ts-css@0.1.4", "", { "dependencies": { "bunfig": "^0.15.6" }, "bin": { "ts-css": "./dist/bin/cli.js" } }, "sha512-QvHvgagJ6kQY3+zY37andUEJFKq/v4v2JpDCrvVdbX3TYCLuRQExZl8Qf3ai+Fl9BN2oWikH+DfKSs5HuEZrGQ=="],
219
220 "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
221
222 "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
163223 }
164224}
docs/advanced/custom-rules.mdmodified+2-2
Changes to docs/advanced/custom-rules.md
@@ -16,7 +16,7 @@ While ts-css includes comprehensive built-in utilities, you can add custom rules
1616Define custom rules in your configuration:
1717
1818```typescript
19import type { TsCssOptions } from 'ts-css'
19import type { TsCssOptions } from '@ts-css/core'
2020
2121const config = {
2222 rules: [
@@ -234,7 +234,7 @@ const config = {
234234```### Using Theme Values
235235
236236Access theme values in custom rules:```typescript
237import type { TsCssOptions } from 'ts-css'
237import type { TsCssOptions } from '@ts-css/core'
238238
239239const config = {
240240 theme: {
docs/advanced/frameworks.mdmodified+13-13
Changes to docs/advanced/frameworks.md
@@ -15,7 +15,7 @@ cd my-app
1515
1616# Install ts-css
1717
18bun add ts-css
18bun add @ts-css/core
1919
2020# Initialize ts-css
2121
@@ -23,7 +23,7 @@ bunx cssx init
2323```**Configuration:**```typescript
2424
2525// css.config.ts
26import type { TsCssOptions } from 'ts-css'
26import type { TsCssOptions } from '@ts-css/core'
2727
2828const config = {
2929 content: ['./src/**/*.{js,jsx,ts,tsx}'],
@@ -90,7 +90,7 @@ bun install
9090
9191# Install ts-css
9292
93bun add ts-css
93bun add @ts-css/core
9494bunx cssx init
9595
9696```**Configuration:**```typescript
@@ -118,7 +118,7 @@ cd my-app
118118
119119# Install ts-css
120120
121bun add ts-css
121bun add @ts-css/core
122122bunx cssx init
123123```**Configuration:**```typescript
124124
@@ -191,7 +191,7 @@ bun install
191191
192192# Install ts-css
193193
194bun add ts-css
194bun add @ts-css/core
195195bunx cssx init
196196
197197```**Configuration:**```typescript
@@ -263,7 +263,7 @@ bun install
263263
264264# Install ts-css
265265
266bun add ts-css
266bun add @ts-css/core
267267bunx cssx init
268268```**Configuration:**```typescript
269269
@@ -307,7 +307,7 @@ bun install
307307
308308# Install ts-css
309309
310bun add ts-css
310bun add @ts-css/core
311311bunx cssx init
312312```**Configuration:**```typescript
313313
@@ -358,7 +358,7 @@ const config = {
358358bun create vite my-app --template svelte-ts
359359cd my-app
360360bun install
361bun add ts-css
361bun add @ts-css/core
362362bunx cssx init
363363
364364```**Configuration:**```typescript
@@ -377,7 +377,7 @@ bun install
377377
378378# Install ts-css
379379
380bun add ts-css
380bun add @ts-css/core
381381bunx cssx init
382382
383383```**Configuration:**```typescript
@@ -444,7 +444,7 @@ bun install
444444
445445# Install ts-css
446446
447bun add ts-css
447bun add @ts-css/core
448448bunx cssx init
449449```**Configuration:**```typescript
450450
@@ -471,7 +471,7 @@ bun install
471471
472472# Install ts-css
473473
474bun add ts-css
474bun add @ts-css/core
475475bunx cssx init
476476
477477```**Configuration:**```typescript
@@ -491,7 +491,7 @@ import './styles.css'
491491
492492# Install ts-css
493493
494bun add ts-css
494bun add @ts-css/core
495495bunx cssx init
496496```**Configuration:**```typescript
497497
@@ -525,7 +525,7 @@ const config = {
525525
526526# In your Hugo project
527527
528bun add ts-css
528bun add @ts-css/core
529529bunx cssx init
530530```**Configuration:**```typescript
531531
docs/advanced/presets.mdmodified+12-12
Changes to docs/advanced/presets.md
@@ -31,7 +31,7 @@ const config = {
3131### Basic Preset Structure
3232
3333A preset is an object that implements the`Preset`interface:```typescript
34import type { Preset } from 'ts-css'
34import type { Preset } from '@ts-css/core'
3535
3636export const myPreset: Preset = {
3737 name: 'my-preset',
@@ -54,7 +54,7 @@ export const myPreset: Preset = {
5454
5555```### Simple Example```typescript
5656// presets/minimal.ts
57import type { Preset } from 'ts-css'
57import type { Preset } from '@ts-css/core'
5858
5959export const minimalPreset: Preset = {
6060 name: 'minimal',
@@ -82,7 +82,7 @@ export const minimalPreset: Preset = {
8282### Single Preset```typescript
8383
8484// css.config.ts
85import type { TsCssOptions } from 'ts-css'
85import type { TsCssOptions } from '@ts-css/core'
8686import { minimalPreset } from './presets/minimal'
8787
8888const config = {
@@ -133,7 +133,7 @@ const config = {
133133### Design System Preset```typescript
134134
135135// presets/design-system.ts
136import type { Preset } from 'ts-css'
136import type { Preset } from '@ts-css/core'
137137
138138export const designSystemPreset: Preset = {
139139 name: 'design-system',
@@ -212,7 +212,7 @@ export const designSystemPreset: Preset = {
212212```### Component Library Preset```typescript
213213
214214// presets/components.ts
215import type { Preset } from 'ts-css'
215import type { Preset } from '@ts-css/core'
216216
217217export const componentsPreset: Preset = {
218218 name: 'components',
@@ -254,7 +254,7 @@ export const componentsPreset: Preset = {
254254
255255```### Utility Preset```typescript
256256// presets/utilities.ts
257import type { Preset } from 'ts-css'
257import type { Preset } from '@ts-css/core'
258258
259259export const utilitiesPreset: Preset = {
260260 name: 'utilities',
@@ -307,7 +307,7 @@ export const utilitiesPreset: Preset = {
307307```### Framework-Specific Preset```typescript
308308
309309// presets/react.ts
310import type { Preset } from 'ts-css'
310import type { Preset } from '@ts-css/core'
311311
312312export const reactPreset: Preset = {
313313 name: 'react',
@@ -360,7 +360,7 @@ my-ts-css-preset/
360360
361361```typescript
362362// src/index.ts
363import type { Preset } from 'ts-css'
363import type { Preset } from '@ts-css/core'
364364
365365export const myPreset: Preset = {
366366 name: 'my-preset',
@@ -389,10 +389,10 @@ export default myPreset
389389 "prepublishOnly": "bun run build"
390390 },
391391 "peerDependencies": {
392 "ts-css": ">=1.0.0"
392 "@ts-css/core": ">=1.0.0"
393393 },
394394 "devDependencies": {
395 "ts-css": "latest",
395 "@ts-css/core": "latest",
396396 "typescript": "latest"
397397 }
398398}
@@ -459,7 +459,7 @@ const config = {
459459
460460### Preset with Custom Preflight```typescript
461461
462import type { Preflight, Preset } from 'ts-css'
462import type { Preflight, Preset } from '@ts-css/core'
463463
464464const customPreflight: Preflight = {
465465 getCSS: () => `/*Custom reset*/*{
@@ -593,7 +593,7 @@ const config = {
593593}
594594```### Type Errors**Problem:**TypeScript errors when creating preset**Solution:**Import and use the`Preset`type:```typescript
595595
596import type { Preset } from 'ts-css'
596import type { Preset } from '@ts-css/core'
597597
598598export const myPreset: Preset = {
599599 name: 'my-preset',
docs/advanced/theme-customization.mdmodified+3-3
Changes to docs/advanced/theme-customization.md
@@ -8,7 +8,7 @@ The theme configuration defines the design tokens that utilities are generated f
88
99```typescript
1010// css.config.ts
11import type { TsCssOptions } from 'ts-css'
11import type { TsCssOptions } from '@ts-css/core'
1212
1313const config = {
1414 theme: {
@@ -339,7 +339,7 @@ const config = {
339339</div>
340340```## Complete Theme Example```typescript
341341
342import type { TsCssOptions } from 'ts-css'
342import type { TsCssOptions } from '@ts-css/core'
343343
344344const config = {
345345 theme: {
@@ -530,7 +530,7 @@ const config = {
530530```### Extending Default Theme
531531
532532Merge with the default theme:```typescript
533import { defaultConfig } from 'ts-css'
533import { defaultConfig } from '@ts-css/core'
534534
535535const config = {
536536 theme: {
docs/api-reference.mdmodified+21-21
Changes to docs/api-reference.md
@@ -5,12 +5,12 @@ This document provides a complete reference for ts-css's programmatic API. Use t
55## Installation
66
77```bash
8bun add ts-css
8bun add @ts-css/core
99```## Core Functions
1010
1111### `build(config: TsCssConfig): Promise<BuildResult>`Build CSS from content files and return the result.**Parameters:**-`config` - Complete ts-css configuration object**Returns:**`Promise<BuildResult>`**Example:**```typescript
1212
13import { build } from 'ts-css'
13import { build } from '@ts-css/core'
1414
1515const result = await build({
1616 content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
@@ -39,7 +39,7 @@ console.log(`Built ${result.classes.size} classes in ${result.duration}ms`)
3939console.log(`Generated CSS:\n${result.css}`)
4040
4141```###`buildAndWrite(config: TsCssConfig): Promise<BuildResult>`Build CSS and write it to the output file.**Parameters:**-`config` - Complete ts-css configuration object**Returns:**`Promise<BuildResult>`**Example:**```typescript
42import { buildAndWrite } from 'ts-css'
42import { buildAndWrite } from '@ts-css/core'
4343
4444const result = await buildAndWrite({
4545 content: ['./src/**/*.tsx'],
@@ -53,13 +53,13 @@ console.log(`File size: ${result.css.length} bytes`)
5353```###`writeCSS(css: string, outputPath: string): Promise<void>`Write CSS string to a file.**Parameters:**-`css`- CSS content to write
5454
5555-`outputPath` - Path to output file**Returns:**`Promise<void>`**Example:**```typescript
56import { build, writeCSS } from 'ts-css'
56import { build, writeCSS } from '@ts-css/core'
5757
5858const result = await build(config)
5959await writeCSS(result.css, './custom/path/output.css')
6060
6161```###`writeTransformedFiles(transformedFiles: Map<string, string>): Promise<void>`Write transformed files to disk (used with compile class transformer).**Parameters:**-`transformedFiles` - Map of file paths to transformed content**Returns:**`Promise<void>`**Example:**```typescript
62import { build, writeTransformedFiles } from 'ts-css'
62import { build, writeTransformedFiles } from '@ts-css/core'
6363
6464const result = await build({
6565 // ... config with compileClass enabled
@@ -79,7 +79,7 @@ Class responsible for generating CSS from utility classes.**Constructor:**```typ
7979new CSSGenerator(config: TsCssConfig)
8080
8181```**Methods:**#### `generate(className: string): void`Generate CSS for a single utility class.```typescript
82import { CSSGenerator } from 'ts-css'
82import { CSSGenerator } from '@ts-css/core'
8383
8484const generator = new CSSGenerator(config)
8585generator.generate('bg-blue-500')
@@ -96,7 +96,7 @@ const prettyCSS = generator.toCSS(false) // Formatted
9696generator.reset()
9797```**Complete example:**```typescript
9898
99import { CSSGenerator, defaultConfig } from 'ts-css'
99import { CSSGenerator, defaultConfig } from '@ts-css/core'
100100
101101const config = {
102102 ...defaultConfig,
@@ -133,7 +133,7 @@ new Scanner(
133133
134134-`classes`- Set of found utility classes
135135-`transformedFiles` - Map of transformed files (if transformer provided)**Example:**```typescript
136import { Scanner } from 'ts-css'
136import { Scanner } from '@ts-css/core'
137137
138138const scanner = new Scanner(['./src/**/*.tsx'])
139139const { classes, transformedFiles } = await scanner.scan()
@@ -147,7 +147,7 @@ for (const className of classes) {
147147
148148### `parseClass(className: string): ParsedClass`Parse a utility class string into its components.**Parameters:**-`className` - Class name to parse**Returns:**`ParsedClass` object**Example:**```typescript
149149
150import { parseClass } from 'ts-css'
150import { parseClass } from '@ts-css/core'
151151
152152const parsed = parseClass('md:hover:bg-blue-500')
153153
@@ -172,7 +172,7 @@ console.log(arbitrary.value) // '500px'
172172### `defaultConfig`
173173
174174The default ts-css configuration object.**Example:**```typescript
175import { defaultConfig } from 'ts-css'
175import { defaultConfig } from '@ts-css/core'
176176
177177// Extend default config
178178const config = {
@@ -188,7 +188,7 @@ const config = {
188188
189189```###`loadConfig(options): Promise<TsCssConfig>`Load configuration from file (used internally by CLI).**Parameters:**-`options.name`- Config name (default: 'css')
190190-`options.defaultConfig` - Default configuration**Returns:**`Promise<TsCssConfig>`**Example:**```typescript
191import { defaultConfig, loadConfig } from 'ts-css'
191import { defaultConfig, loadConfig } from '@ts-css/core'
192192
193193const config = await loadConfig({
194194 name: 'css',
@@ -208,7 +208,7 @@ new CompileClassTransformer(config?: {
208208```**Methods:**#### `getCompiledClasses(): Map<string, { className: string, utilities: string[] }>`
209209
210210Get all compiled classes.**Example:**```typescript
211import { CompileClassTransformer } from 'ts-css'
211import { CompileClassTransformer } from '@ts-css/core'
212212
213213const transformer = new CompileClassTransformer({
214214 trigger: ':tc:',
@@ -324,7 +324,7 @@ interface VariantConfig {
324324
325325### Custom Build Pipeline```typescript
326326
327import { CSSGenerator, Scanner, writeCSS } from 'ts-css'
327import { CSSGenerator, Scanner, writeCSS } from '@ts-css/core'
328328
329329async function customBuild() {
330330 // 1. Configure
@@ -361,7 +361,7 @@ async function customBuild() {
361361await customBuild()
362362
363363```### Analyze Utility Usage```typescript
364import { parseClass, Scanner } from 'ts-css'
364import { parseClass, Scanner } from '@ts-css/core'
365365
366366async function analyzeUtilities() {
367367 const scanner = new Scanner(['./src/**/*.tsx'])
@@ -389,7 +389,7 @@ await analyzeUtilities()
389389```### Watch Mode Implementation```typescript
390390
391391import { watch } from 'node:fs'
392import { buildAndWrite } from 'ts-css'
392import { buildAndWrite } from '@ts-css/core'
393393
394394async function watchMode(config: TsCssConfig) {
395395 console.log('Watching for changes...')
@@ -419,7 +419,7 @@ async function watchMode(config: TsCssConfig) {
419419await watchMode(config)
420420
421421```### Framework Plugin```typescript
422import { buildAndWrite } from 'ts-css'
422import { buildAndWrite } from '@ts-css/core'
423423
424424// Usage in vite.config.ts
425425import { defineConfig } from 'vite'
@@ -457,7 +457,7 @@ export default defineConfig({
457457})
458458```### Generate Multiple Themes```typescript
459459
460import { build, writeCSS } from 'ts-css'
460import { build, writeCSS } from '@ts-css/core'
461461
462462const baseConfig = {
463463 content: ['./src/**/*.tsx'],
@@ -494,7 +494,7 @@ for (const [name, theme] of Object.entries(themes)) {
494494
495495```### Testing Utilities```typescript
496496import { expect, test } from 'bun:test'
497import { CSSGenerator, defaultConfig, parseClass } from 'ts-css'
497import { CSSGenerator, defaultConfig, parseClass } from '@ts-css/core'
498498
499499test('parses utility class', () => {
500500 const parsed = parseClass('md:hover:bg-blue-500')
@@ -519,7 +519,7 @@ test('handles important modifier', () => {
519519})
520520```## Error Handling```typescript
521521
522import { buildAndWrite } from 'ts-css'
522import { buildAndWrite } from '@ts-css/core'
523523
524524try {
525525 const result = await buildAndWrite(config)
@@ -543,7 +543,7 @@ catch (error) {
543543
544544### Caching Results```typescript
545545
546import { build } from 'ts-css'
546import { build } from '@ts-css/core'
547547
548548const cache = new Map<string, BuildResult>()
549549
@@ -562,7 +562,7 @@ async function buildWithCache(config: TsCssConfig) {
562562}
563563```### Parallel Builds```typescript
564564
565import { build, writeCSS } from 'ts-css'
565import { build, writeCSS } from '@ts-css/core'
566566
567567const configs = [
568568 { output: './dist/app.css', content: ['./src/app/**/*.tsx'] },
docs/config.mdmodified+5-5
Changes to docs/config.md
@@ -10,7 +10,7 @@ Create a configuration file using the init command:
1010cssx init
1111```This creates a basic`css.config.ts`file:```typescript
1212
13import type { TsCssOptions } from 'ts-css'
13import type { TsCssOptions } from '@ts-css/core'
1414
1515const config = {
1616 content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
@@ -238,7 +238,7 @@ const config = {
238238
239239```**Creating a Preset:**```typescript
240240// presets/custom.ts
241import type { TsCssOptions } from 'ts-css'
241import type { TsCssOptions } from '@ts-css/core'
242242
243243export const myCustomPreset: Partial<TsCssConfig> = {
244244 theme: {
@@ -252,7 +252,7 @@ export const myCustomPreset: Partial<TsCssConfig> = {
252252}
253253```## Complete Configuration Example```typescript
254254
255import type { TsCssOptions } from 'ts-css'
255import type { TsCssOptions } from '@ts-css/core'
256256
257257const config = {
258258 // Required
@@ -306,7 +306,7 @@ export default config
306306```## Environment-Specific Configuration
307307
308308You can create different configurations for different environments:```typescript
309import type { TsCssOptions } from 'ts-css'
309import type { TsCssOptions } from '@ts-css/core'
310310
311311const isDev = process.env.NODE_ENV === 'development'
312312const isProd = process.env.NODE_ENV === 'production'
@@ -327,7 +327,7 @@ export default config
327327```## TypeScript Support
328328
329329ts-css provides full TypeScript support with type checking and autocomplete:```typescript
330import type { TsCssOptions } from 'ts-css'
330import type { TsCssOptions } from '@ts-css/core'
331331
332332// Use satisfies for type checking while preserving literal types
333333const config = {
docs/features/cli.mdmodified+6-6
Changes to docs/features/cli.md
@@ -10,11 +10,11 @@ Install ts-css globally or locally:
1010
1111# Global (use anywhere)
1212
13bun add --global ts-css
13bun add --global @ts-css/core
1414
1515# Local (project-specific)
1616
17bun add ts-css
17bun add @ts-css/core
1818```## Commands
1919
2020### `build`Build CSS from your content files.```bash
@@ -125,7 +125,7 @@ Next steps:
125125
126126```**Generated file:**```typescript
127127
128import type { TsCssOptions } from 'ts-css'
128import type { TsCssOptions } from '@ts-css/core'
129129
130130const config = {
131131 content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
@@ -403,7 +403,7 @@ cssx build --config ./css.test.config.ts
403403```### Programmatic Usage
404404
405405While the CLI is convenient, you can also use ts-css programmatically:```typescript
406import { build, buildAndWrite } from 'ts-css'
406import { build, buildAndWrite } from '@ts-css/core'
407407
408408// Build only (get result)
409409const result = await build({
@@ -459,7 +459,7 @@ const config = {
459459### Command Not Found**Problem:**`command not found: cssx`**Solutions:**1. Install globally
460460
461461 ```bash
462 bun add --global ts-css
462 bun add --global @ts-css/core
463463 ```2. Or use with package runner:```bash
464464 bunx cssx build
465465
@@ -494,7 +494,7 @@ const config = {
494494 ls -la ./src/**/*.tsx
495495 ```3. Validate config:```typescript
496496 // Use type checking
497 import type { TsCssOptions } from 'ts-css'
497 import type { TsCssOptions } from '@ts-css/core'
498498
499499 const config = {
500500 content: ['./src/**/*.tsx'],
docs/features/compile-class.mdmodified+1-1
Changes to docs/features/compile-class.md
@@ -30,7 +30,7 @@ Add the`:tc:`trigger to mark utility groups for compilation:```html
3030
3131Configure the transformer in your config:```typescript
3232// css.config.ts
33import type { TsCssOptions } from 'ts-css'
33import type { TsCssOptions } from '@ts-css/core'
3434
3535const config = {
3636 content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
docs/features/shortcuts.mdmodified+1-1
Changes to docs/features/shortcuts.md
@@ -37,7 +37,7 @@ Define shortcuts in your`css.config.ts`:
3737
3838```typescript
3939
40import type { TsCssOptions } from 'ts-css'
40import type { TsCssOptions } from '@ts-css/core'
4141
4242const config = {
4343 shortcuts: {
docs/features/typescript.mdmodified+25-25
Changes to docs/features/typescript.md
@@ -8,7 +8,7 @@ Use TypeScript for your configuration file to get autocompletion and type checki
88
99```typescript
1010// css.config.ts
11import type { TsCssOptions } from 'ts-css'
11import type { TsCssOptions } from '@ts-css/core'
1212
1313const config = {
1414 content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
@@ -31,7 +31,7 @@ export default config
3131```## Type Checking
3232
3333The`satisfies`keyword provides type checking while preserving literal types:```typescript
34import type { TsCssOptions } from 'ts-css'
34import type { TsCssOptions } from '@ts-css/core'
3535
3636// ✅ Type-safe with autocompletion
3737const config = {
@@ -107,7 +107,7 @@ Get IntelliSense and autocompletion in your IDE:
1071071. Install the TypeScript extension (comes pre-installed)
1081082. Create`css.config.ts`with the type import:```typescript
109109
110import type { TsCssOptions } from 'ts-css'
110import type { TsCssOptions } from '@ts-css/core'
111111
112112const config = {
113113 // Press Ctrl+Space here for autocompletion
@@ -161,8 +161,8 @@ const config = {
161161
162162### Build Function```typescript
163163
164import type { BuildResult, TsCssConfig } from 'ts-css'
165import { build } from 'ts-css'
164import type { BuildResult, TsCssConfig } from '@ts-css/core'
165import { build } from '@ts-css/core'
166166
167167const result: BuildResult = await build({
168168 content: ['./src/**/*.tsx'],
@@ -185,7 +185,7 @@ interface BuildResult {
185185}
186186
187187```### Using with async/await```typescript
188import { build, buildAndWrite } from 'ts-css'
188import { build, buildAndWrite } from '@ts-css/core'
189189
190190async function buildStyles() {
191191 // With build
@@ -207,7 +207,7 @@ async function buildStyles() {
207207```## Custom Rules with Types
208208
209209Define type-safe custom rules:```typescript
210import type { CustomRule, TsCssConfig } from 'ts-css'
210import type { CustomRule, TsCssConfig } from '@ts-css/core'
211211
212212const customRules: CustomRule[] = [
213213 // Pattern and handler with types
@@ -229,7 +229,7 @@ const config = {
229229```## Preset Types
230230
231231Create type-safe presets:```typescript
232import type { Preset, Theme } from 'ts-css'
232import type { Preset, Theme } from '@ts-css/core'
233233
234234const myPreset: Preset = {
235235 name: 'my-design-system',
@@ -264,7 +264,7 @@ const config = {
264264### Custom Theme Values
265265
266266Extend the theme with custom properties:```typescript
267import type { TsCssOptions, Theme } from 'ts-css'
267import type { TsCssOptions, Theme } from '@ts-css/core'
268268
269269interface CustomTheme extends Theme {
270270 customSpacing: Record<string, string>
@@ -286,7 +286,7 @@ const config = {
286286```### Custom Config
287287
288288Create a custom config interface:```typescript
289import type { TsCssOptions } from 'ts-css'
289import type { TsCssOptions } from '@ts-css/core'
290290
291291interface MyConfig extends Partial<TsCssConfig> {
292292 // Add custom properties
@@ -310,7 +310,7 @@ const config: MyConfig = {
310310```## Type Guards
311311
312312Use type guards for runtime type checking:```typescript
313import type { TsCssOptions } from 'ts-css'
313import type { TsCssOptions } from '@ts-css/core'
314314
315315function isValidConfig(config: unknown): config is TsCssConfig {
316316 return (
@@ -333,7 +333,7 @@ if (isValidConfig(config)) {
333333```## Generics
334334
335335Use generics for flexible, type-safe functions:```typescript
336import type { TsCssOptions } from 'ts-css'
336import type { TsCssOptions } from '@ts-css/core'
337337
338338function createConfig<T extends Partial<TsCssConfig>>(config: T): T {
339339 return {
@@ -356,12 +356,12 @@ const config = createConfig({
356356
357357Extend ts-css types globally:```typescript
358358// Now use in config
359import type { TsCssOptions } from 'ts-css'
359import type { TsCssOptions } from '@ts-css/core'
360360
361361// types/ts-css.d.ts
362import 'ts-css'
362import '@ts-css/core'
363363
364declare module 'ts-css' {
364declare module '@ts-css/core' {
365365 interface TsCssConfig {
366366 // Add your custom properties
367367 customFeature?: {
@@ -392,8 +392,8 @@ const config = {
392392
393393### Vite```typescript
394394
395import type { TsCssOptions } from 'ts-css'
396import { build } from 'ts-css'
395import type { TsCssOptions } from '@ts-css/core'
396import { build } from '@ts-css/core'
397397// vite.config.ts
398398import { defineConfig } from 'vite'
399399
@@ -415,10 +415,10 @@ export default defineConfig({
415415})
416416```### Next.js```typescript
417417
418import type { TsCssOptions } from 'ts-css'
418import type { TsCssOptions } from '@ts-css/core'
419419// next.config.ts
420420import type { NextConfig } from 'next'
421import { buildAndWrite } from 'ts-css'
421import { buildAndWrite } from '@ts-css/core'
422422
423423const tsCssConfig: TsCssConfig = {
424424 content: ['./src/**/*.tsx', './app/**/*.tsx'],
@@ -446,7 +446,7 @@ import type {
446446 ParsedClass,
447447 Preset,
448448 Theme,
449} from 'ts-css'
449} from '@ts-css/core'
450450
451451// Extract specific types
452452type ConfigColors = Theme['colors']
@@ -460,10 +460,10 @@ type PartialTheme = Partial<Theme>
460460### 1. Always Use Type Imports```typescript
461461
462462// ✅ Type-only import
463import type { TsCssOptions } from 'ts-css'
463import type { TsCssOptions } from '@ts-css/core'
464464
465465// ❌ Value import (unnecessary)
466import { TsCssConfig } from 'ts-css'
466import { TsCssConfig } from '@ts-css/core'
467467
468468```### 2. Use`satisfies`for Validation```typescript
469469// ✅ Validates types while preserving literals
@@ -478,7 +478,7 @@ const config: Partial<TsCssConfig> = {
478478```### 3. Separate Type Definitions```typescript
479479
480480// types/ts-css.ts
481import type { TsCssOptions, Preset } from 'ts-css'
481import type { TsCssOptions, Preset } from '@ts-css/core'
482482
483483// css.config.ts
484484import type { MyConfig } from './types/ts-css'
@@ -528,14 +528,14 @@ bun add --dev typescript
528528
529529# Ensure types are imported
530530
531import type { TsCssOptions } from 'ts-css'
531import type { TsCssOptions } from '@ts-css/core'
532532
533533```### Autocomplete Not Working**Problem:**No autocompletion in IDE**Solutions:**1. Restart TypeScript server
534534
5355352. Check tsconfig.json includes the config file
5365363. Ensure proper imports:```typescript
537537
538 import type { TsCssOptions } from 'ts-css'
538 import type { TsCssOptions } from '@ts-css/core'
539539 ```
540540
541541## Related*[Configuration](../config.md) - Full configuration guide*[Custom Rules](../advanced/custom-rules.md) - Creating custom utilities*[Presets](../advanced/presets.md) - Reusable configurations*[API Reference](../api-reference.md) - Programmatic API
docs/features/watch-mode.mdmodified+1-1
Changes to docs/features/watch-mode.md
@@ -33,7 +33,7 @@ When you start watch mode, ts-css:
3333
3434Watch mode uses the content patterns from your configuration file:```typescript
3535// css.config.ts
36import type { TsCssOptions } from 'ts-css'
36import type { TsCssOptions } from '@ts-css/core'
3737
3838const config = {
3939 content: [
docs/install.mdmodified+26-26
Changes to docs/install.md
@@ -9,31 +9,31 @@ Install ts-css as a development dependency in your project:
99::: code-group
1010
1111```sh [bun]
12bun add ts-css
12bun add @ts-css/core
1313
1414# or
1515
16bun install --dev ts-css
16bun install --dev @ts-css/core
1717```
1818
1919```sh [npm]
20npm install ts-css
20npm install @ts-css/core
2121
2222# or
2323
24npm i -D ts-css
24npm i -D @ts-css/core
2525```
2626
2727```sh [pnpm]
28pnpm add ts-css
28pnpm add @ts-css/core
2929
3030# or
3131
32pnpm add -D ts-css
32pnpm add -D @ts-css/core
3333```
3434
3535```sh [yarn]
36yarn add ts-css
36yarn add @ts-css/core
3737```:::
3838
3939### Global Installation
@@ -41,29 +41,29 @@ yarn add ts-css
4141For global installation (to use the CLI anywhere):
4242
4343::: code-group```sh [bun]
44bun add --global ts-css
44bun add --global @ts-css/core
4545
4646```
4747
4848```sh [npm]
4949
50npm install --global ts-css
50npm install --global @ts-css/core
5151
5252# or
5353
54npm i -g ts-css
54npm i -g @ts-css/core
5555
5656```
5757
5858```sh [pnpm]
5959
60pnpm add --global ts-css
60pnpm add --global @ts-css/core
6161
6262```
6363
6464```sh [yarn]
6565
66yarn global add ts-css
66yarn global add @ts-css/core
6767
6868```:::
6969
@@ -85,7 +85,7 @@ cssx watch
8585```## Configuration
8686
8787The`cssx init`command creates a basic`css.config.ts`file:```typescript
88import type { TsCssOptions } from 'ts-css'
88import type { TsCssOptions } from '@ts-css/core'
8989
9090const config = {
9191 content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
@@ -102,7 +102,7 @@ export default config
102102
103103# Install ts-css
104104
105bun add ts-css
105bun add @ts-css/core
106106
107107# Create config
108108
@@ -110,7 +110,7 @@ cssx init
110110```Update your`css.config.ts`:
111111
112112```typescript
113import type { TsCssOptions } from 'ts-css'
113import type { TsCssOptions } from '@ts-css/core'
114114
115115const config = {
116116 content: [
@@ -143,7 +143,7 @@ import './styles/styles.css'
143143
144144# Install ts-css
145145
146bun add ts-css
146bun add @ts-css/core
147147
148148# Create config
149149
@@ -151,7 +151,7 @@ cssx init
151151```Update your`css.config.ts`:
152152
153153```typescript
154import type { TsCssOptions } from 'ts-css'
154import type { TsCssOptions } from '@ts-css/core'
155155
156156const config = {
157157 content: [
@@ -174,7 +174,7 @@ export default config
174174
175175# Install ts-css
176176
177bun add ts-css
177bun add @ts-css/core
178178
179179# Create config
180180
@@ -182,7 +182,7 @@ cssx init
182182```Update your`css.config.ts`:
183183
184184```typescript
185import type { TsCssOptions } from 'ts-css'
185import type { TsCssOptions } from '@ts-css/core'
186186
187187const config = {
188188 content: [
@@ -203,7 +203,7 @@ export default config
203203
204204# Install ts-css
205205
206bun add ts-css
206bun add @ts-css/core
207207
208208# Create config
209209
@@ -211,7 +211,7 @@ cssx init
211211```Update your`css.config.ts`:
212212
213213```typescript
214import type { TsCssOptions } from 'ts-css'
214import type { TsCssOptions } from '@ts-css/core'
215215
216216const config = {
217217 content: [
@@ -235,7 +235,7 @@ import '/styles.css'
235235
236236# Install ts-css globally
237237
238bun add --global ts-css
238bun add --global @ts-css/core
239239
240240# Create config
241241
@@ -243,7 +243,7 @@ cssx init
243243```Update your`css.config.ts`:
244244
245245```typescript
246import type { TsCssOptions } from 'ts-css'
246import type { TsCssOptions } from '@ts-css/core'
247247
248248const config = {
249249 content: ['./src/**/*.html'],
@@ -397,11 +397,11 @@ If you get permission errors when installing globally:```bash
397397
398398# Use sudo on macOS/Linux
399399
400sudo bun add --global ts-css
400sudo bun add --global @ts-css/core
401401
402402# Or install locally and use npx
403403
404bun add ts-css
404bun add @ts-css/core
405405bunx cssx build
406406
407407```### TypeScript Errors
@@ -412,7 +412,7 @@ If you encounter TypeScript errors in your config file:
412412
413413 bun add --dev typescript
414414 ```2. Use the`satisfies`keyword for type checking:```typescript
415 import type { TsCssOptions } from 'ts-css'
415 import type { TsCssOptions } from '@ts-css/core'
416416
417417 const config = {
418418 content: ['./src/**/*.tsx'],
docs/intro.mdmodified+4-4
Changes to docs/intro.md
@@ -130,15 +130,15 @@ Install ts-css via your package manager:```bash
130130
131131# Using Bun (recommended)
132132
133bun add ts-css
133bun add @ts-css/core
134134
135135# Using npm
136136
137npm install ts-css
137npm install @ts-css/core
138138
139139# Using pnpm
140140
141pnpm add ts-css
141pnpm add @ts-css/core
142142
143143```### Configuration
144144
@@ -146,7 +146,7 @@ Create a configuration file:```bash
146146cssx init
147147```This creates a`css.config.ts`file:```typescript
148148
149import type { TsCssOptions } from 'ts-css'
149import type { TsCssOptions } from '@ts-css/core'
150150
151151const config = {
152152 content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
package.jsonmodified+5-3
Changes to package.json
@@ -1,5 +1,5 @@
11{
2 "name": "ts-css",
2 "name": "@ts-css/core",
33 "type": "module",
44 "sideEffects": false,
55 "version": "0.3.0",
@@ -29,7 +29,7 @@
2929 "zero-runtime"
3030 ],
3131 "scripts": {
32 "build": "cd packages/ts-css && bun run build",
32 "build": "bun run build:toolkit && cd packages/ts-css && bun run build",
3333 "fresh": "bunx rimraf node_modules/ bun.lock && bun i",
3434 "test": "bun test",
3535 "lint": "bunx --bun pickier .",
@@ -48,7 +48,9 @@
4848 "typecheck": "bun --bun tsc --noEmit",
4949 "benchmark": "bun run benchmark/framework-comparison.bench.ts",
5050 "benchmark:binary": "bun run benchmark/binary-vs-api.ts",
51 "benchmark:style": "bun run benchmark/style-api.bench.ts"
51 "benchmark:style": "bun run benchmark/style-api.bench.ts",
52 "build:toolkit": "cd packages/toolkit && bun run build",
53 "prepare": "bun run build:toolkit"
5254 },
5355 "devDependencies": {
5456 "better-dx": "^0.2.15"
.config/buddy-bot.ts packages/toolkit/.config/buddy-bot.tscopied+2-2
Changes to packages/toolkit/.config/buddy-bot.ts
@@ -3,9 +3,9 @@ import type { BuddyBotConfig } from 'buddy-bot'
33const config: BuddyBotConfig = {
44 repository: {
55 owner: 'stacksjs',
6 name: 'ts-css',
6 name: 'ts-starter',
77 provider: 'github',
8 // Uses GITHUB_TOKEN by default
8 // token: process.env.BUDDY_BOT_TOKEN,
99 },
1010 dashboard: {
1111 enabled: true,
packages/toolkit/.config/pickier.tsadded+62-0
Changes to packages/toolkit/.config/pickier.ts
@@ -0,0 +1,62 @@
1import type { PickierConfig } from 'pickier'
2
3const config: PickierConfig = {
4 verbose: false,
5 ignores: [
6 '**/node_modules/**',
7 '**/dist/**',
8 '**/build/**',
9 '**/bin/**',
10 '**/.git/**',
11 '**/coverage/**',
12 '**/*.min.js',
13 '**/bun.lock',
14 '**/benchmarks/**',
15 '**/.claude/**',
16 '**/.zed/**',
17 ],
18
19 lint: {
20 extensions: ['ts', 'js'],
21 reporter: 'stylish',
22 cache: false,
23 maxWarnings: -1,
24 },
25
26 format: {
27 extensions: ['ts', 'js', 'json', 'md', 'yaml', 'yml'],
28 trimTrailingWhitespace: true,
29 maxConsecutiveBlankLines: 1,
30 finalNewline: 'one',
31 indent: 2,
32 quotes: 'single',
33 semi: false,
34 },
35
36 rules: {
37 noDebugger: 'error',
38 noConsole: 'off',
39 },
40
41 pluginRules: {
42 'ts/no-explicit-any': 'off',
43 'ts/no-unused-vars': 'warn',
44 'ts/no-top-level-await': 'off',
45 'regexp/no-unused-capturing-group': 'off',
46 'regexp/no-super-linear-backtracking': 'off',
47 'style/brace-style': 'off',
48 'style/max-statements-per-line': 'off',
49 'markdown/heading-increment': 'error',
50 'markdown/no-trailing-spaces': 'error',
51 'markdown/fenced-code-language': 'warn',
52 'markdown/no-inline-html': 'off',
53 'markdown/reference-links-images': 'off',
54 'markdown/single-title': 'off',
55 'markdown/blanks-around-fences': 'off',
56 'markdown/no-duplicate-heading': 'off',
57 'markdown/single-trailing-newline': 'off',
58 'markdown/link-image-style': 'off',
59 },
60}
61
62export default config
packages/toolkit/.editorconfigadded+9-0
Changes to packages/toolkit/.editorconfig
@@ -0,0 +1,9 @@
1root = true
2
3[*]
4charset = utf-8
5indent_style = space
6indent_size = 2
7end_of_line = lf
8insert_final_newline = true
9trim_trailing_whitespace = true
packages/toolkit/.gitattributesadded+1-0
Changes to packages/toolkit/.gitattributes
@@ -0,0 +1,1 @@
1* text=auto
packages/toolkit/.gitignoreadded+15-0
Changes to packages/toolkit/.gitignore
@@ -0,0 +1,15 @@
1.cache
2.DS_Store
3.idea
4*.log
5*.tgz
6coverage
7dist
8lib-cov
9logs
10node_modules
11temp
12docs/.vitepress/cache
13pantry
14.zed
15.cursor
packages/toolkit/.vscode/dictionary.txtadded+47-0
Changes to packages/toolkit/.vscode/dictionary.txt
@@ -0,0 +1,47 @@
1antfu
2biomejs
3booleanish
4bumpp
5bunfig
6bunx
7changelogen
8changelogithub
9chrisbbreuer
10clapp
11codecov
12commitlint
13commitlintrc
14composables
15davidanson
16degit
17deps
18destructurable
19dtsx
20entrypoints
21heroicons
22iconify
23lockb
24openweb
25outdir
26pausable
27Postcardware
28prefetch
29preinstall
30quickfix
31shikijs
32socio
33Solana
34Spatie
35stacksjs
36twoslash
37typecheck
38unconfig
39unocss
40unplugin
41unref
42upath
43vite
44vitebook
45vitejs
46vue-demi
47vueus
packages/toolkit/.vscode/extensions.jsonadded+7-0
Changes to packages/toolkit/.vscode/extensions.json
@@ -0,0 +1,7 @@
1{
2 "recommendations": [
3 "dbaeumer.vscode-eslint",
4 "davidanson.vscode-markdownlint",
5 "streetsidesoftware.code-spell-checker"
6 ]
7}
packages/toolkit/.vscode/settings.jsonadded+153-0
Changes to packages/toolkit/.vscode/settings.json
@@ -0,0 +1,153 @@
1{
2 // Disable the default formatter, use eslint instead
3 "prettier.enable": false,
4 "biome.enabled": false,
5 "editor.formatOnSave": false,
6 // Auto fix
7 "editor.codeActionsOnSave": {
8 "source.fixAll.eslint": "explicit",
9 "source.organizeImports": "never"
10 },
11 // Silent the stylistic rules in you IDE, but still auto fix them
12 "eslint.rules.customizations": [
13 {
14 "rule": "style/*",
15 "severity": "off",
16 "fixable": true
17 },
18 {
19 "rule": "format/*",
20 "severity": "off",
21 "fixable": true
22 },
23 {
24 "rule": "*-indent",
25 "severity": "off",
26 "fixable": true
27 },
28 {
29 "rule": "*-spacing",
30 "severity": "off",
31 "fixable": true
32 },
33 {
34 "rule": "*-spaces",
35 "severity": "off",
36 "fixable": true
37 },
38 {
39 "rule": "*-order",
40 "severity": "off",
41 "fixable": true
42 },
43 {
44 "rule": "*-dangle",
45 "severity": "off",
46 "fixable": true
47 },
48 {
49 "rule": "*-newline",
50 "severity": "off",
51 "fixable": true
52 },
53 {
54 "rule": "*quotes",
55 "severity": "off",
56 "fixable": true
57 },
58 {
59 "rule": "*semi",
60 "severity": "off",
61 "fixable": true
62 }
63 ],
64 // Enable eslint for all supported languages
65 "eslint.validate": [
66 "javascript",
67 "javascriptreact",
68 "typescript",
69 "typescriptreact",
70 "vue",
71 "html",
72 "markdown",
73 "json",
74 "jsonc",
75 "yaml",
76 "toml",
77 "xml",
78 "gql",
79 "graphql",
80 "astro",
81 "svelte",
82 "css",
83 "less",
84 "scss",
85 "pcss",
86 "postcss"
87 ],
88 "[stx]": {
89 "editor.defaultFormatter": "dbaeumer.vscode-eslint"
90 },
91 // Shell
92 "[shellscript]": {
93 "editor.defaultFormatter": "foxundermoon.shell-format"
94 },
95 // Markdown
96 "[markdown]": {
97 "editor.defaultFormatter": "DavidAnson.vscode-markdownlint",
98 "editor.formatOnSave": true
99 },
100 "[dockerfile]": {
101 "editor.defaultFormatter": "foxundermoon.shell-format"
102 },
103 "markdownlint.config": {
104 "default": true,
105 "MD033": {
106 "allowed_elements": [
107 "details",
108 "summary",
109 "VPTeamPage",
110 "VPTeamPageTitle",
111 "VPTeamMembers",
112 "script"
113 ]
114 },
115 "MD041": false
116 },
117 "typescript.preferGoToSourceDefinition": true,
118 "editor.quickSuggestions": {
119 "strings": true
120 },
121 "git.enableSmartCommit": true,
122 "npm.enableRunFromFolder": true,
123 "npm.packageManager": "bun",
124 "editor.gotoLocation.multipleDefinitions": "goto",
125 "search.exclude": {
126 "**/node_modules": true,
127 "**/cdk.out": true,
128 "**/dist": true,
129 "CHANGELOG.md": true
130 },
131
132 "cSpell.ignorePaths": ["node_modules"],
133 "cSpell.dictionaries": ["custom-dictionary"],
134 "cSpell.diagnosticLevel": "Hint",
135 "cSpell.customDictionaries": {
136 "bun-plugin-env": {
137 "name": "custom-dictionary",
138 "path": "./.vscode/dictionary.txt",
139 "scope": "user",
140 "addWords": true
141 },
142 "custom": true // enable the `custom` dictionary
143 },
144 "terminal.integrated.scrollback": 10000,
145 "grammarly.files.include": [
146 "**/README.md",
147 "**/readme.md",
148 "**/*.txt"
149 ],
150 "grammarly.files.exclude": [
151 "**/dictionary.txt"
152 ]
153}
packages/toolkit/CHANGELOG.mdadded+252-0
Changes to packages/toolkit/CHANGELOG.md
@@ -0,0 +1,252 @@
1[Compare changes](https://github.com/stacksjs/ts-css/compare/v0.1.3...v0.1.4)
2
3## 🐛 Bug Fixes
4
5- **build**: build the CLI the bin field points at ([61d4584](https://github.com/stacksjs/ts-css/commit/61d4584)) _(by Chris <chrisbreuer93@gmail.com>)_
6
7## 🧹 Chores
8
9- release v0.1.4 ([5c4ca3d](https://github.com/stacksjs/ts-css/commit/5c4ca3d)) _(by Chris <chrisbreuer93@gmail.com>)_
10
11## Contributors
12
13- _Chris <chrisbreuer93@gmail.com>_
14
15[Compare changes](https://github.com/stacksjs/ts-css/compare/v0.1.2...v0.1.3)
16
17## ⚡ Performance Improvements
18
19- **build**: cut the waste out of the published output ([222ad06](https://github.com/stacksjs/ts-css/commit/222ad06)) _(by Chris <chrisbreuer93@gmail.com>)_
20
21## 🧹 Chores
22
23- release v0.1.3 ([1b59c44](https://github.com/stacksjs/ts-css/commit/1b59c44)) _(by Chris <chrisbreuer93@gmail.com>)_
24
25## Contributors
26
27- _Chris <chrisbreuer93@gmail.com>_
28
29[Compare changes](https://github.com/stacksjs/ts-css/compare/v0.1.1...v0.1.2)
30
31## ⚡ Performance Improvements
32
33- **build**: minify the published bundles ([ab79be3](https://github.com/stacksjs/ts-css/commit/ab79be3)) _(by Chris <chrisbreuer93@gmail.com>)_
34
35## 🧹 Chores
36
37- release v0.1.2 ([8e3ce9a](https://github.com/stacksjs/ts-css/commit/8e3ce9a)) _(by Chris <chrisbreuer93@gmail.com>)_
38
39## Contributors
40
41- _Chris <chrisbreuer93@gmail.com>_
42
43[Compare changes](https://github.com/stacksjs/ts-css/compare/v0.1.0...v0.1.1)
44
45## 🐛 Bug Fixes
46
47- remove consumer postinstall hook ([44d1ee3](https://github.com/stacksjs/ts-css/commit/44d1ee3)) _(by Chris <chrisbreuer93@gmail.com>)_
48- **scripts**: stop double-generating CHANGELOG on release ([7fe2506](https://github.com/stacksjs/ts-css/commit/7fe2506)) _(by Glenn Michael Torregosa <gtorregosa@gmail.com>)_
49
50## 🤖 Continuous Integration
51
52- drop redundant setup-bun (pantry installs bun via deps.yaml) ([7fafadf](https://github.com/stacksjs/ts-css/commit/7fafadf)) _(by glennmichael123 <gtorregosa@gmail.com>)_
53
54## 🧹 Chores
55
56- release v0.1.1 ([5639aff](https://github.com/stacksjs/ts-css/commit/5639aff)) _(by Chris <chrisbreuer93@gmail.com>)_
57- **deps**: declare bun ^1.3.14 in deps.yaml ([83f0002](https://github.com/stacksjs/ts-css/commit/83f0002)) _(by Chris <chrisbreuer93@gmail.com>)_
58- **deps**: refresh bun.lock to pick up pickier 0.1.37 ([6b8d86c](https://github.com/stacksjs/ts-css/commit/6b8d86c)) _(by glennmichael123 <gtorregosa@gmail.com>)_
59- **deps**: refresh bun.lock to pick up pickier 0.1.35 ([2dc313e](https://github.com/stacksjs/ts-css/commit/2dc313e)) _(by glennmichael123 <gtorregosa@gmail.com>)_
60- **deps**: refresh bun.lock to pick up pickier 0.1.33 ([a89a742](https://github.com/stacksjs/ts-css/commit/a89a742)) _(by glennmichael123 <gtorregosa@gmail.com>)_
61- **deps**: refresh bun.lock to pick up @stacksjs/logsmith 0.2.3 ([0b2e754](https://github.com/stacksjs/ts-css/commit/0b2e754)) _(by glennmichael123 <gtorregosa@gmail.com>)_
62- **deps**: refresh bun.lock to pick up buddy-bot 0.9.20 ([66d94f2](https://github.com/stacksjs/ts-css/commit/66d94f2)) _(by glennmichael123 <gtorregosa@gmail.com>)_
63- **deps**: bump better-dx to ^0.2.15 ([cbe57f9](https://github.com/stacksjs/ts-css/commit/cbe57f9)) _(by glennmichael123 <gtorregosa@gmail.com>)_
64- **ci**: bump actions/checkout to v6, actions/cache to v5 ([7ed4b33](https://github.com/stacksjs/ts-css/commit/7ed4b33)) _(by glennmichael123 <gtorregosa@gmail.com>)_
65- refresh bun.lock to pick up bun-plugin-dtsx@0.9.18 ([12aa00f](https://github.com/stacksjs/ts-css/commit/12aa00f)) _(by glennmichael123 <gtorregosa@gmail.com>)_
66- refresh bun.lock and apply pickier --fix ([ceb799c](https://github.com/stacksjs/ts-css/commit/ceb799c)) _(by glennmichael123 <gtorregosa@gmail.com>)_
67- refresh bun.lock ([9a27cc0](https://github.com/stacksjs/ts-css/commit/9a27cc0)) _(by glennmichael123 <gtorregosa@gmail.com>)_
68- refresh bun.lock to pick up latest pickier ([5c717ee](https://github.com/stacksjs/ts-css/commit/5c717ee)) _(by glennmichael123 <gtorregosa@gmail.com>)_
69
70## ⏪ Reverts
71
72- keep staged-lint kebab + bunx gitlint shorthand ([cb5bcc0](https://github.com/stacksjs/ts-css/commit/cb5bcc0)) _(by glennmichael123 <gtorregosa@gmail.com>)_
73
74## Contributors
75
76- _Chris <chrisbreuer93@gmail.com>_
77- _Glenn Michael Torregosa <gtorregosa@gmail.com>_
78- _glennmichael123 <gtorregosa@gmail.com>_
79
80### 🐛 Bug Fixes
81
82- add setup-bun to publish-commit job ([ba0a364](https://github.com/stacksjs/ts-css/commit/ba0a364)) _(by glennmichael123 <gtorregosa@gmail.com>)_
83
84### 🧹 Chores
85
86- add --bun flag to changelog scripts ([fd558f1](https://github.com/stacksjs/ts-css/commit/fd558f1)) _(by glennmichael123 <gtorregosa@gmail.com>)_
87- normalize CI action versions to match other repos ([f2cfd71](https://github.com/stacksjs/ts-css/commit/f2cfd71)) _(by glennmichael123 <gtorregosa@gmail.com>)_
88- fix lint and typecheck errors ([9b61350](https://github.com/stacksjs/ts-css/commit/9b61350)) _(by glennmichael123 <gtorregosa@gmail.com>)_
89- wip ([7e8a9b5](https://github.com/stacksjs/ts-css/commit/7e8a9b5)) _(by Chris <chrisbreuer93@gmail.com>)_
90- wip ([5624fd4](https://github.com/stacksjs/ts-css/commit/5624fd4)) _(by Chris <chrisbreuer93@gmail.com>)_
91- wip ([e793c83](https://github.com/stacksjs/ts-css/commit/e793c83)) _(by Chris <chrisbreuer93@gmail.com>)_
92- wip ([57b551c](https://github.com/stacksjs/ts-css/commit/57b551c)) _(by Chris <chrisbreuer93@gmail.com>)_
93- wip ([10a6e56](https://github.com/stacksjs/ts-css/commit/10a6e56)) _(by Chris <chrisbreuer93@gmail.com>)_
94- wip ([19a0620](https://github.com/stacksjs/ts-css/commit/19a0620)) _(by Chris <chrisbreuer93@gmail.com>)_
95- wip ([8649287](https://github.com/stacksjs/ts-css/commit/8649287)) _(by Chris <chrisbreuer93@gmail.com>)_
96- initial commit ([ee6a5e0](https://github.com/stacksjs/ts-css/commit/ee6a5e0)) _(by Chris <chrisbreuer93@gmail.com>)_
97
98### Contributors
99
100- _Chris <chrisbreuer93@gmail.com>_
101- _glennmichael123 <gtorregosa@gmail.com>_
102
103## v0.3.1...main
104
105[compare changes](https://github.com/stacksjs/ts-starter/compare/v0.3.1...main)
106
107### 🚀 Enhancements
108
109- Add `bun-plugin-dts-auto` ([c0c487b](https://github.com/stacksjs/ts-starter/commit/c0c487b))
110
111### ❤️ Contributors
112
113- Chris <chrisbreuer93@gmail.com>
114
115## v0.3.0...main
116
117[compare changes](https://github.com/stacksjs/ts-starter/compare/v0.3.0...main)
118
119### 🏡 Chore
120
121- Fix isolatedDeclarations setting ([b87b6b1](https://github.com/stacksjs/ts-starter/commit/b87b6b1))
122- Adjust urls ([0a40b72](https://github.com/stacksjs/ts-starter/commit/0a40b72))
123
124### ❤️ Contributors
125
126- Chris <chrisbreuer93@gmail.com>
127
128## v0.2.1...main
129
130[compare changes](https://github.com/stacksjs/ts-starter/compare/v0.2.1...main)
131
132### 🚀 Enhancements
133
134- Add `noFallthroughCasesInSwitch` ([b9cfa30](https://github.com/stacksjs/ts-starter/commit/b9cfa30))
135- Add `verbatimModuleSyntax` ([c495d17](https://github.com/stacksjs/ts-starter/commit/c495d17))
136- Several updates ([f703179](https://github.com/stacksjs/ts-starter/commit/f703179))
137
138### 🩹 Fixes
139
140- Properly use bun types ([7144221](https://github.com/stacksjs/ts-starter/commit/7144221))
141
142### 🏡 Chore
143
144- Adjust badge links ([432aff7](https://github.com/stacksjs/ts-starter/commit/432aff7))
145- Add `runs-on` options ([9a5b97f](https://github.com/stacksjs/ts-starter/commit/9a5b97f))
146- Cache node_modules ([ba2f6ce](https://github.com/stacksjs/ts-starter/commit/ba2f6ce))
147- Use `ubuntu-latest` for now ([1add684](https://github.com/stacksjs/ts-starter/commit/1add684))
148- Minor updates ([1007cff](https://github.com/stacksjs/ts-starter/commit/1007cff))
149- Lint ([d531bdc](https://github.com/stacksjs/ts-starter/commit/d531bdc))
150- Remove bunx usage ([e1a5575](https://github.com/stacksjs/ts-starter/commit/e1a5575))
151- Pass bun flag ([960976f](https://github.com/stacksjs/ts-starter/commit/960976f))
152- Use defaults ([157455b](https://github.com/stacksjs/ts-starter/commit/157455b))
153- Run typecheck using bun flag ([f22f3b1](https://github.com/stacksjs/ts-starter/commit/f22f3b1))
154- Test ([0b3c3a1](https://github.com/stacksjs/ts-starter/commit/0b3c3a1))
155- Use modern js for commitlint ([4bd6978](https://github.com/stacksjs/ts-starter/commit/4bd6978))
156- Update worklows readme ([f54aae9](https://github.com/stacksjs/ts-starter/commit/f54aae9))
157- Adjust readme ([92d7ff1](https://github.com/stacksjs/ts-starter/commit/92d7ff1))
158- More updates ([0225587](https://github.com/stacksjs/ts-starter/commit/0225587))
159- Add .zed settings for biome ([1688024](https://github.com/stacksjs/ts-starter/commit/1688024))
160- Extend via alias ([b108d30](https://github.com/stacksjs/ts-starter/commit/b108d30))
161- Lint ([d961b2a](https://github.com/stacksjs/ts-starter/commit/d961b2a))
162- Minor updates ([e66d44a](https://github.com/stacksjs/ts-starter/commit/e66d44a))
163
164### ❤️ Contributors
165
166- Chris <chrisbreuer93@gmail.com>
167
168## v0.2.0...main
169
170[compare changes](https://github.com/stacksjs/ts-starter/compare/v0.2.0...main)
171
172### 🏡 Chore
173
174- Remove unused action ([066f85a](https://github.com/stacksjs/ts-starter/commit/066f85a))
175- Housekeeping ([fc4e24d](https://github.com/stacksjs/ts-starter/commit/fc4e24d))
176
177### ❤️ Contributors
178
179- Chris <chrisbreuer93@gmail.com>
180
181## v0.1.1...main
182
183[compare changes](https://github.com/stacksjs/ts-starter/compare/v0.1.1...main)
184
185### 🏡 Chore
186
187- Adjust eslint config name ([53c2aa6](https://github.com/stacksjs/ts-starter/commit/53c2aa6))
188- Set type module ([22dde14](https://github.com/stacksjs/ts-starter/commit/22dde14))
189
190### ❤️ Contributors
191
192- Chris <chrisbreuer93@gmail.com>
193
194## v0.1.0...main
195
196[compare changes](https://github.com/stacksjs/ts-starter/compare/v0.1.0...main)
197
198### 🏡 Chore
199
200- Use correct cover image ([75bd3ae](https://github.com/stacksjs/ts-starter/commit/75bd3ae))
201
202### ❤️ Contributors
203
204- Chris <chrisbreuer93@gmail.com>
205
206## v0.0.5...main
207
208[compare changes](https://github.com/stacksjs/ts-starter/compare/v0.0.5...main)
209
210### 🚀 Enhancements
211
212- Add pkgx deps ([319c066](https://github.com/stacksjs/ts-starter/commit/319c066))
213- Use flat eslint config ([cdb0093](https://github.com/stacksjs/ts-starter/commit/cdb0093))
214
215### 🏡 Chore
216
217- Fix badge ([bc3b000](https://github.com/stacksjs/ts-starter/commit/bc3b000))
218- Minor updates ([78dc522](https://github.com/stacksjs/ts-starter/commit/78dc522))
219- Housekeeping ([e1cba3b](https://github.com/stacksjs/ts-starter/commit/e1cba3b))
220- Additional housekeeping ([f5dc625](https://github.com/stacksjs/ts-starter/commit/f5dc625))
221- Add `.gitattributes` ([7080f8c](https://github.com/stacksjs/ts-starter/commit/7080f8c))
222- Adjust deps ([cc71b42](https://github.com/stacksjs/ts-starter/commit/cc71b42))
223- Adjust wording ([3bc54b3](https://github.com/stacksjs/ts-starter/commit/3bc54b3))
224- Adjust readme cover ([e6acbb2](https://github.com/stacksjs/ts-starter/commit/e6acbb2))
225
226### ❤️ Contributors
227
228- Chris <chrisbreuer93@gmail.com>
229
230## v0.0.5...main
231
232[compare changes](https://github.com/stacksjs/ts-starter/compare/v0.0.5...main)
233
234### 🚀 Enhancements
235
236- Add pkgx deps ([319c066](https://github.com/stacksjs/ts-starter/commit/319c066))
237- Use flat eslint config ([cdb0093](https://github.com/stacksjs/ts-starter/commit/cdb0093))
238
239### 🏡 Chore
240
241- Fix badge ([bc3b000](https://github.com/stacksjs/ts-starter/commit/bc3b000))
242- Minor updates ([78dc522](https://github.com/stacksjs/ts-starter/commit/78dc522))
243- Housekeeping ([e1cba3b](https://github.com/stacksjs/ts-starter/commit/e1cba3b))
244- Additional housekeeping ([f5dc625](https://github.com/stacksjs/ts-starter/commit/f5dc625))
245- Add `.gitattributes` ([7080f8c](https://github.com/stacksjs/ts-starter/commit/7080f8c))
246- Adjust deps ([cc71b42](https://github.com/stacksjs/ts-starter/commit/cc71b42))
247- Adjust wording ([3bc54b3](https://github.com/stacksjs/ts-starter/commit/3bc54b3))
248- Adjust readme cover ([e6acbb2](https://github.com/stacksjs/ts-starter/commit/e6acbb2))
249
250### ❤️ Contributors
251
252- Chris <chrisbreuer93@gmail.com>
packages/toolkit/CLAUDE.mdadded+23-0
Changes to packages/toolkit/CLAUDE.md
@@ -0,0 +1,23 @@
1# Claude Code Guidelines
2
3## Linting
4
5- Use **pickier** for linting — never use eslint directly
6- Run `bunx --bun pickier .` to lint, `bunx --bun pickier . --fix` to auto-fix
7- When fixing unused variable warnings, prefer `// eslint-disable-next-line` comments over prefixing with `_`
8
9## Frontend
10
11- Use **stx** for templating — never write vanilla JS (`var`, `document.*`, `window.*`) in stx templates
12- Use **crosswind** as the default CSS framework which enables standard Tailwind-like utility classes
13- stx `<script>` tags should only contain stx-compatible code (signals, composables, directives)
14
15## Dependencies
16
17- **buddy-bot** handles dependency updates — not renovatebot
18- **better-dx** provides shared dev tooling as peer dependencies — do not install its peers (e.g., `typescript`, `pickier`, `bun-plugin-dtsx`) separately if `better-dx` is already in `package.json`
19- If `better-dx` is in `package.json`, ensure `bunfig.toml` includes `linker = "hoisted"`
20
21## Commits
22
23- Use conventional commit messages (e.g., `fix:`, `feat:`, `chore:`)
packages/toolkit/LICENSE.mdadded+21-0
Changes to packages/toolkit/LICENSE.md
@@ -0,0 +1,21 @@
1# MIT License
2
3Copyright (c) 2024 Open Web Foundation
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in all
13copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21SOFTWARE.
packages/toolkit/README.mdadded+213-0
Changes to packages/toolkit/README.md
@@ -0,0 +1,213 @@
1# @stacksjs/ts-css
2
3> Pure-TypeScript CSS toolkit for Bun & Node. Zero runtime dependencies.
4
5`ts-css` is a CSS toolkit for build tools, linters, design-system pipelines,
6SSR templating, runtime style transforms — anything that needs to read,
7walk, query, transform, or minify CSS in a JS/TS environment. Written from
8scratch in TypeScript with strict types and the CSS Syntax Module Level 3
9tokenizer.
10
11It bundles every common CSS pipeline primitive — tokenizer, AST parser,
12visitor walker, generator, selector parser, selector matcher, and minifier —
13into a single zero-dep package. The shape of each sub-API mirrors the
14de-facto community standard for that capability (`css-tree`-style AST and
15walker, `css-what`-style selector tokens, the `css-select` adapter contract,
16`csso`-style minify result), so adopting `ts-css` rarely requires call-site
17changes — but the project stands on its own and isn't pitched as "the
18replacement for X."
19
20## Highlights
21
22- **Zero runtime deps.** Only ts-css's own code. Ships ~4.5k lines of TS.
23- **One install for the whole CSS pipeline.** Tokenizer, parser, walker,
24 generator, selector parser/matcher, and minifier — all behind sub-module
25 imports at `@stacksjs/ts-css/parse`, `/what`, `/select`, `/optimize`.
26- **Familiar API shapes.** Visitor/walker callbacks, AST node types, and
27 the `css-select` adapter contract follow the conventions established by
28 the popular community libraries, so most existing call sites work
29 unchanged.
30- **Bun-first.** Built and tested on Bun, works on Node ≥18.
31- **Strict TypeScript.** Strict mode, isolatedDeclarations, verbatimModuleSyntax.
32
33## Install
34
35```bash
36bun add @stacksjs/ts-css
37# or
38npm i @stacksjs/ts-css
39```
40
41## Quick start
42
43### Parse, walk, generate
44
45```ts
46import { generate, parse, walk } from '@stacksjs/ts-css'
47
48const ast = parse(`
49 .foo, .bar > .baz {
50 color: red;
51 background: url("/x.png") no-repeat;
52 }
53`)
54
55walk(ast, {
56 visit: 'Declaration',
57 enter(decl) {
58 console.log(decl.property, '→', generate(decl.value))
59 },
60})
61// color → red
62// background → url("/x.png") no-repeat
63
64console.log(generate(ast))
65// .foo,.bar>.baz{color:red;background:url("/x.png") no-repeat}
66```
67
68### Match selectors against any tree
69
70```ts
71import { selectAll } from '@stacksjs/ts-css'
72
73const adapter = {
74 isTag: (n) => n?.type === 'el',
75 getName: (e) => e.name,
76 getAttributeValue: (e, n) => e.attrs[n],
77 // …other adapter methods (see docs/api/select.md)
78}
79
80selectAll('p > span.foo:not(.disabled)', root, { adapter, xmlMode: true })
81```
82
83The matcher is decoupled from any DOM implementation: pass an `Adapter`
84describing your tree (HTML, XML, an SVG AST, your design-system component
85graph, anything with `parent`/`children`/`name`/`attrs`-like access) and
86ts-css will match standard CSS selectors against it.
87
88### Minify CSS
89
90```ts
91import { minify } from '@stacksjs/ts-css'
92
93minify(`
94 .a {
95 margin: 0px 0.5em 10.000px;
96 color: #aabbcc;
97 color: blue; /* later wins */
98 }
99`).css
100// → ".a{margin:0 .5em 10px;color:blue}"
101```
102
103### Compute selector specificity
104
105```ts
106import { parse, syntax } from '@stacksjs/ts-css'
107
108const sel = parse('#a.b div', { context: 'selector' })
109syntax.specificity(sel) // [1, 1, 1]
110```
111
112## API surface
113
114`ts-css` exposes a complete CSS pipeline. The named exports below cover the
115endpoints most build/lint/transform tools reach for:
116
117| Capability | Export |
118| -------------------------------- | ---------------------------- |
119| Parse a stylesheet to an AST (`stylesheet`/`rule`/`declaration`/`selectorList`/etc. contexts) | `parse(source, opts?)` |
120| Walk the AST with `enter`/`leave` callbacks, `visit` filtering, and a `walk.skip` sentinel | `walk(ast, fn \| visitor)` |
121| Stringify any AST node back to CSS source | `generate(node)` |
122| Deep-clone an AST subtree | `clone(node)` |
123| Doubly-linked children container with cursor-safe `forEach(data, item, list)` mid-walk mutation | `List` / `ListItem` |
124| Tokenize a selector string into segments | `cssWhat.parse(selector)` |
125| Detect combinator/traversal segments | `cssWhat.isTraversal(seg)` |
126| Match a selector against an arbitrary tree via the `Adapter` contract | `selectAll`, `selectOne`, `is`, `compile` |
127| Minify with declaration/value compression and whitespace collapse | `minify`, `minifyBlock` |
128| Compute selector specificity tuple | `syntax.specificity(node)` |
129
130A namespace export (`csstree`, `cssWhat`, `cssSelect`, `csso`) is also
131available so existing code that imports from those packages can typically
132switch by just changing the import path.
133
134## CLI
135
136```bash
137ts-css minify input.css > output.css
138ts-css parse input.css # AST as JSON on stdout
139ts-css format input.css # round-trip through parser/generator
140```
141
142## What's not (yet) in scope
143
144This is a pragmatic toolkit — full surface area where it's load-bearing,
145trimmed where edge cases would force in spec data or rarely-used passes:
146
147- ❌ **No CSS-spec-aware value lexer.** Validating that `red` is a valid
148 `<color>`, or that `1px` is the right syntax for a given property, is
149 thousands of lines of spec grammar that most pipelines don't need. The
150 AST preserves enough information to layer that on top if you do.
151- ❌ **No cross-rule restructuring pass.** Reordering rules across the
152 whole stylesheet to fold compatible declarations is complex and gives
153 marginal gains beyond gzip. Declaration-level minification (numbers,
154 colors, dedup, longhand collapse) ships fully.
155
156Everything else round-trips byte-equivalent.
157
158## Project layout
159
160```
161src/
162 parse/ # tokenizer, parser, AST types, walker, generator, clone, List
163 what/ # selector parser/stringifier
164 select/ # selectAll/selectOne/is engine + pseudo-class compilers
165 optimize/ # minifier, value compressors, specificity
166```
167
168Each sub-module is exported as a separate entry point so you can import
169just what you need:
170
171```ts
172import { parse, walk } from '@stacksjs/ts-css/parse'
173import { selectAll } from '@stacksjs/ts-css/select'
174import { minify } from '@stacksjs/ts-css/optimize'
175```
176
177## Performance
178
179End-to-end (parse → minify → generate) on a 6 KB representative fixture
180(Apple M3 Pro, Bun 1.3):
181
182| Pipeline | Time/iter |
183| --------------------------- | --------- |
184| `ts-css.minify(source)` | **~270 µs** |
185| `csso.minify(source)` | ~890 µs |
186
187→ **~3.3× faster end-to-end** with zero runtime deps. Per-API breakdown
188in [`docs/benchmarks.md`](./docs/benchmarks.md). Run them yourself with
189`bun run bench`.
190
191## Documentation
192
193Full docs: see [`./docs`](./docs) or run `bun run dev:docs` to start the
194local docs site.
195
196- [Getting started](./docs/intro.md)
197- [Parser API](./docs/api/parse.md)
198- [Selector parser API](./docs/api/what.md)
199- [Selector matcher API](./docs/api/select.md)
200- [Optimizer API](./docs/api/optimize.md)
201- [Migration guide](./docs/migration.md)
202- [Benchmarks](./docs/benchmarks.md)
203
204## License
205
206MIT — see [`LICENSE.md`](./LICENSE.md).
207
208`ts-css` is an independent implementation. The visitor / linked-list shape
209in `src/parse`, the selector tokenizer model in `src/what`, the adapter
210pattern in `src/select`, and the minification plan in `src/optimize` all
211follow conventions established by the long-standing community libraries in
212each area, so call sites that already use those conventions tend to drop in
213without changes. All such projects are MIT-licensed.
packages/toolkit/bench/index.bench.tsadded+185-0
Changes to packages/toolkit/bench/index.bench.ts
@@ -0,0 +1,185 @@
1/**
2 * Mitata bench: ts-css vs the original css-tree / css-select / css-what / csso
3 * libraries. Run with:
4 *
5 * bun bench/index.bench.ts
6 *
7 * Prints per-operation timings and ratios. The fixtures under
8 * `test/fixtures/` are concatenated into one ~6 KB stylesheet (small but
9 * representative of the SVGO/optimize use case).
10 */
11
12import { readFileSync } from 'node:fs'
13import { join } from 'node:path'
14import * as cssTree from 'css-tree'
15import * as cssSelect from 'css-select'
16import * as cssWhat from 'css-what'
17import * as csso from 'csso'
18import { barplot, bench, group, run, summary } from 'mitata'
19
20import * as ourCss from '../src'
21import * as ourCsso from '../src/optimize'
22import * as ourSelect from '../src/select'
23import * as ourWhat from '../src/what'
24
25const FIX = join(import.meta.dir, '..', 'test', 'fixtures')
26const samples = ['reset.css', 'utilities.css', 'component.css'].map(f => readFileSync(join(FIX, f), 'utf8'))
27const sample = samples.join('\n')
28
29console.log(`Fixture: ${sample.length} bytes (concatenation of reset+utilities+component).`)
30console.log(`Bun ${Bun.version}`)
31console.log()
32
33// ---- Parse ----
34group('parse stylesheet', () => {
35 barplot(() => {
36 bench('ts-css', () => ourCss.parse(sample))
37 bench('css-tree', () => cssTree.parse(sample))
38 })
39})
40
41// ---- Generate ----
42{
43 const ourAst = ourCss.parse(sample)
44 const treeAst = cssTree.parse(sample)
45 group('generate stylesheet', () => {
46 barplot(() => {
47 bench('ts-css', () => ourCss.generate(ourAst))
48 bench('css-tree', () => cssTree.generate(treeAst))
49 })
50 })
51}
52
53// ---- Walk ----
54{
55 const ourAst = ourCss.parse(sample)
56 const treeAst = cssTree.parse(sample)
57 group('walk all nodes', () => {
58 barplot(() => {
59 bench('ts-css', () => {
60 let n = 0
61 ourCss.walk(ourAst, () => { n++ })
62 return n
63 })
64 bench('css-tree', () => {
65 let n = 0
66 cssTree.walk(treeAst, () => { n++ })
67 return n
68 })
69 })
70 })
71}
72
73// ---- Selector parse ----
74const SELECTORS = [
75 'div',
76 '.foo',
77 '#bar',
78 'p > span.foo',
79 'a:hover[data-x="y" i]',
80 'div + .x ~ p:nth-child(2n+1):not(.disabled)',
81 '*[data-foo],[data-bar]:where(.a, .b)',
82]
83group('parse 7 selectors', () => {
84 barplot(() => {
85 bench('ts-css', () => {
86 for (const s of SELECTORS) ourWhat.parse(s)
87 })
88 bench('css-what', () => {
89 for (const s of SELECTORS) cssWhat.parse(s)
90 })
91 })
92})
93
94// ---- selectAll on a synthetic tree ----
95{
96 interface El { type: 'el', name: string, attrs: Record<string, string>, children: El[], parent: El | null }
97 function el(name: string, attrs: Record<string, string> = {}, ...children: El[]): El {
98 const e: El = { type: 'el', name, attrs, children, parent: null }
99 for (const c of children) c.parent = e
100 return e
101 }
102 // 5-level deep tree, ~250 nodes
103 const root = el('html', {},
104 el('body', { class: 'app' },
105 ...Array.from({ length: 10 }, (_, i) =>
106 el('section', { id: `s${i}`, class: i % 2 ? 'even' : 'odd' },
107 ...Array.from({ length: 4 }, (_, j) =>
108 el('article', { 'data-idx': String(j) },
109 el('h2', { class: 'title' }, el('span', { class: 'inner' })),
110 el('p', { class: i === 5 ? 'highlight' : 'normal' }, el('a', { href: '#x' })),
111 el('p', { class: 'tail' }),
112 ),
113 ),
114 ),
115 ),
116 ),
117 )
118 const adapter = {
119 isTag: (n: any): n is El => n?.type === 'el',
120 existsOne: (test: any, xs: any[]): boolean => xs.some((x: any) => adapter.isTag(x) && (test(x) || adapter.existsOne(test, x.children))),
121 getAttributeValue: (e: El, n: string) => e.attrs[n],
122 getChildren: (e: any) => e.children ?? [],
123 getName: (e: El) => e.name,
124 getParent: (e: any) => e.parent ?? null,
125 getSiblings: (e: any) => e.parent ? e.parent.children : [e],
126 getText: () => '',
127 hasAttrib: (e: El, n: string) => n in e.attrs,
128 removeSubsets: (n: any[]) => n,
129 findAll: (test: any, xs: any[]): El[] => {
130 const out: El[] = []
131 for (const x of xs) {
132 if (adapter.isTag(x)) {
133 if (test(x)) out.push(x)
134 out.push(...adapter.findAll(test, x.children))
135 }
136 }
137 return out
138 },
139 findOne: (test: any, xs: any[]): El | null => {
140 for (const x of xs) {
141 if (adapter.isTag(x)) {
142 if (test(x)) return x
143 const r = adapter.findOne(test, x.children)
144 if (r) return r
145 }
146 }
147 return null
148 },
149 }
150 const opts = { adapter, xmlMode: true } as any
151
152 const targets = ['p', 'p.highlight', 'section.odd > article > h2 .inner', '[data-idx="2"]']
153 group('selectAll on a 250-node tree', () => {
154 barplot(() => {
155 bench('ts-css', () => {
156 for (const sel of targets) ourSelect.selectAll(sel, root, opts)
157 })
158 bench('css-select', () => {
159 for (const sel of targets) cssSelect.selectAll(sel, root, opts)
160 })
161 })
162 })
163}
164
165// ---- minify ----
166group('minify stylesheet', () => {
167 barplot(() => {
168 bench('ts-css', () => ourCsso.minify(sample))
169 bench('csso', () => csso.minify(sample))
170 })
171})
172
173// ---- summary ----
174summary(() => {
175 bench('ts-css combined parse→minify→generate', () => {
176 const { css } = ourCsso.minify(sample)
177 return css.length
178 })
179 bench('original combined parse→minify→generate', () => {
180 const { css } = csso.minify(sample)
181 return css.length
182 })
183})
184
185await run()
packages/toolkit/bin/cli.tsadded+58-0
Changes to packages/toolkit/bin/cli.ts
@@ -0,0 +1,58 @@
1#!/usr/bin/env bun
2/**
3 * ts-css CLI — minify, parse, or round-trip CSS files.
4 */
5
6import { CLI } from '@stacksjs/clapp'
7import { existsSync, readFileSync } from 'node:fs'
8import { minify, minifyBlock } from '../src/optimize'
9import { generate, parse } from '../src/parse'
10import { version } from '../package.json'
11
12function readSourceOrExit(file: string): string {
13 if (!existsSync(file)) {
14 process.stderr.write(`ts-css: file not found: ${file}\n`)
15 process.exit(1)
16 }
17 return readFileSync(file, 'utf8')
18}
19
20const cli = new CLI('ts-css')
21
22cli
23 .command('minify <file>', 'Minify a CSS file')
24 .option('--no-comments', 'Strip /*!*/ comments too')
25 .option('--block', 'Treat input as a declarationList (style="…" body)')
26 .action(async (file: string, opts: { comments: boolean, block: boolean }) => {
27 const css = readSourceOrExit(file)
28 const minOptions = { comments: opts.comments === false ? false : ('exclamation' as const) }
29 const result = opts.block
30 ? minifyBlock(css, minOptions)
31 : minify(css, minOptions)
32 process.stdout.write(result.css)
33 })
34
35cli
36 .command('parse <file>', 'Parse a CSS file and print its AST as JSON')
37 .option('--positions', 'Include source locations on every node')
38 .action(async (file: string, opts: { positions: boolean }) => {
39 const css = readSourceOrExit(file)
40 const ast = parse(css, { positions: opts.positions })
41 process.stdout.write(JSON.stringify(ast, null, 2))
42 })
43
44cli
45 .command('format <file>', 'Round-trip CSS through parser/generator (deterministic single-line output)')
46 .action(async (file: string) => {
47 const css = readSourceOrExit(file)
48 const ast = parse(css)
49 process.stdout.write(generate(ast))
50 })
51
52cli.command('version', 'Show the version of the CLI').action(() => {
53 console.log(version)
54})
55
56cli.version(version)
57cli.help()
58cli.parse()
packages/toolkit/build.tsadded+27-0
Changes to packages/toolkit/build.ts
@@ -0,0 +1,27 @@
1import { dts } from 'bun-plugin-dtsx'
2
3await Bun.build({
4 splitting: true,
5 minify: true,
6 entrypoints: [
7 'src/index.ts',
8 'src/parse/index.ts',
9 'src/what/index.ts',
10 'src/select/index.ts',
11 'src/optimize/index.ts',
12 ],
13 outdir: './dist',
14 target: 'bun',
15 plugins: [dts()],
16})
17
18// The CLI the `bin` field points at. Nothing built it, so `dist/bin/cli.js`
19// has never existed and every install logged a failed bin link — the command
20// was declared and unavailable. Built separately so it lands under dist/bin/,
21// which is where the manifest already looks for it.
22await Bun.build({
23 minify: true,
24 entrypoints: ['bin/cli.ts'],
25 outdir: './dist/bin',
26 target: 'bun',
27})
packages/toolkit/bun.lockadded+338-0
Changes to packages/toolkit/bun.lock
@@ -0,0 +1,338 @@
1{
2 "lockfileVersion": 1,
3 "configVersion": 1,
4 "workspaces": {
5 "": {
6 "name": "@stacksjs/ts-css",
7 "dependencies": {
8 "bunfig": "^0.15.6",
9 },
10 "devDependencies": {
11 "@types/bun": "latest",
12 "@types/css-tree": "^2.3.11",
13 "@types/csso": "^5.0.4",
14 "better-dx": "^0.2.15",
15 "css-select": "^7.0.0",
16 "css-tree": "^3.2.1",
17 "css-what": "^8.0.0",
18 "csso": "^5.0.5",
19 "mitata": "^1.0.34",
20 },
21 },
22 },
23 "packages": {
24 "@cwcss/crosswind": ["@cwcss/crosswind@0.2.6", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "bunfig": "^0.15.0" }, "bin": { "crosswind": "./dist/cli.js" } }, "sha512-8T1oJD8UMrpImgODg5tZO8soUySt5U8gEeHCIcC1yRNgyiEDIRIgQuKallaKs21/ab2pPOwCZdHyDXuWkq2ygg=="],
25
26 "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
27
28 "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
29
30 "@stacksjs/bumpx": ["@stacksjs/bumpx@0.2.6", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "@stacksjs/logsmith": "^0.2.1", "bunfig": "^0.15.6" }, "bin": { "bumpx": "./dist/bin/cli.js" } }, "sha512-y4Jz8WKTJtes6/o1vsIbOeuIoqKItfnbeel94qrJKNKsY2CBzU3FT3YwKjs8K52JFfyACCdQa1tGx+lQWtpyTA=="],
31
32 "@stacksjs/bunpress": ["@stacksjs/bunpress@0.1.11", "", { "dependencies": { "@stacksjs/stx": "^0.2.72", "@stacksjs/ts-cloud": "^0.5.17", "@stacksjs/ts-md": "^0.1.1", "bunfig": "^0.15.6" }, "bin": { "bunpress": "./dist/bin/cli.js" } }, "sha512-nXWQxlBcEU9RmjZC1Wn+DSiQyNyHWUPsxAl+xuf2F/Vqk/FCfpCBueZCO1sP87dYFguWDIig8TQVB1DEBk+CTA=="],
33
34 "@stacksjs/clapp": ["@stacksjs/clapp@0.2.10", "", {}, "sha512-l/2F0u5ihpae8R4kJ3Pdh6tV7aEpSxu6hCyafhw38Fl9+x2YKxZfUK+fF/G5AVwSzJar3dkKuMxbMcQ0PXKGRg=="],
35
36 "@stacksjs/clarity": ["@stacksjs/clarity@0.3.28", "", { "dependencies": { "bunfig": "^0.15.6" }, "bin": { "clarity": "./dist/bin/cli.js" } }, "sha512-SElzGeDewEbp0c4C8PTfdvi/Xtr1HGAjG6b0prHvkVxyYGM+2R0BFU0otUmy+DbR7wsfZ4rNU3YN1+JK/CzoTw=="],
37
38 "@stacksjs/desktop": ["@stacksjs/desktop@0.2.82", "", { "peerDependencies": { "craft": "*" }, "optionalPeers": ["craft"] }, "sha512-ymawT8XY6lT6hHdfOGgdOlKB/pUu2NB1eSG+zxmXs+vn/PuIO6rt1nibOuFyDdulkS0zTmeXo0uYiNHvY7mhoQ=="],
39
40 "@stacksjs/dtsx": ["@stacksjs/dtsx@0.9.26", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0" }, "peerDependencies": { "typescript": ">=5.9.3" }, "bin": { "dtsx": "./dist/bin/cli.js" } }, "sha512-MOUxZn8BhXpFYM+vh7cnQLUOLjZ4G6q76V+4OypkRiu/Gvpek9ywBQLbmv8iWLjT6gXlFMpEdw33o3GKKLpMCw=="],
41
42 "@stacksjs/gitlint": ["@stacksjs/gitlint@0.1.6", "", { "bin": { "gitlint": "./dist/bin/cli.js" } }, "sha512-A4MTsddeWHEhz0mLKq+P/CqHxmWv4SWhrmWotP1UMtVbGlpYs/q+bwqrFyThT6L9xMR8iWKGVTV908oS6QDeNg=="],
43
44 "@stacksjs/logsmith": ["@stacksjs/logsmith@0.2.3", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "bunfig": "^0.15.6", "pickier": "^0.1.9" }, "bin": { "@stacksjs/logsmith": "./dist/bin/cli.js", "logsmith": "./dist/bin/cli.js" } }, "sha512-Jy2AHVdEQwDtjlOVSG4xOmIMSy/D2iZcPK7KaoDOmKfZbMmfmbhveK16w1TWKCY9duhMZigdpeFx39ZzXGARVg=="],
45
46 "@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.82", "", {}, "sha512-NOqW3bcvqRutURpaIORdiLg/4RCZvkCzQEHhyfzlfuAWDwdoPVbGBKdNjyLL4yoKLj1h7ntHrah+Tc8OuDYn6w=="],
47
48 "@stacksjs/stx": ["@stacksjs/stx@0.2.82", "", { "dependencies": { "@cwcss/crosswind": "^0.2.6", "@stacksjs/clapp": "^0.2.0", "@stacksjs/desktop": "0.2.82", "@stacksjs/sanitizer": "0.2.82", "@stacksjs/ts-craft": "^0.0.2", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.82", "bunfig": "^0.15.6", "stx-router": "0.2.82", "ts-broadcasting": "^0.0.4", "ts-images": "^0.1.8", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-k8pgFx3uBW8+QwdBzMQWMgC5RtLpmEvrlLW6NMzkop34jZppz8L5L/1vfs73mVvnKvnh6IqAT0Hizgp1R8umSg=="],
49
50 "@stacksjs/ts-avif": ["@stacksjs/ts-avif@0.1.0", "", {}, "sha512-ytcta6ZhZeZwXJTcWlTraNLBsqL4imOfruSkJ2PwEp5RjHb4eSoqb21W4u7HLILRcILr7B1nOkIMeQct4Pozbw=="],
51
52 "@stacksjs/ts-bmp": ["@stacksjs/ts-bmp@0.1.0", "", {}, "sha512-O02r5q35OaoLRkTLOcFCylQzY3Oeji6QebdzLBfuWt77MoS/gY/4igY53N++pUjA4uS34cHHd/P651LlCKoBeA=="],
53
54 "@stacksjs/ts-cloud": ["@stacksjs/ts-cloud@0.5.35", "", { "dependencies": { "@stacksjs/ts-xml": "^0.1.0", "@ts-cloud/aws-types": "0.5.35", "@ts-cloud/core": "0.5.35" }, "bin": { "cloud": "./dist/bin/cli.js" } }, "sha512-yzUGTCCSDBr5AD5FEmQqcLz6RyYj68LLitE4HvmKFmom4nVVNn4L2ll9HI8Wdjs9CG6mu8NoFd3gTnryP0EXWQ=="],
55
56 "@stacksjs/ts-craft": ["@stacksjs/ts-craft@0.0.2", "", { "dependencies": { "archiver": "^7.0.1" }, "bin": { "craft": "./bin/cli.ts" } }, "sha512-09vAI4BJuXXNClFoOE6f1acrKqV77U7lObmOjnRteSqDoX7WZJU0HnMhrjaX96lF9EdPIozfIp59kPq7EZwLYQ=="],
57
58 "@stacksjs/ts-css": ["@stacksjs/ts-css@0.1.0", "", { "dependencies": { "bunfig": "^0.15.6" }, "bin": { "ts-css": "./dist/bin/cli.js" } }, "sha512-373WvzZeXdqwi6lRwyfr05r3KOtbo4QXWPhmoGFI6NSa5ZcUEiyYJGL2KsV9mfQNTsBp+0q3wPnj1EQ/1zuieg=="],
59
60 "@stacksjs/ts-i18n": ["@stacksjs/ts-i18n@0.1.11", "", { "dependencies": { "@stacksjs/clapp": "^0.2.9", "bunfig": "^0.15.6" }, "bin": { "i18n": "./dist/cli.js" } }, "sha512-33FM0gKdxBy6EUsUZLGAMe0D7QUZ4f/BQXRsQIyzkZgs2+juruUTevPuJGCjWSZsgJuiaBfEJn8Oq8ctuo1OWQ=="],
61
62 "@stacksjs/ts-md": ["@stacksjs/ts-md@0.1.1", "", { "bin": { "md": "./bin/cli.ts" } }, "sha512-kvsSg7S3vHq65fnHIRggTLudN4n1sjkos86xLB62V6PVRa8iosYAmFKVDTGaJk+0MdsIoRwQbJ2vLWVcNQsgGw=="],
63
64 "@stacksjs/ts-png": ["@stacksjs/ts-png@0.1.2", "", {}, "sha512-NT9AVCAlZiOMzcBH+OlAAUAbfTzakm/6Ibp5S31JInrpTDd+Qe3arEfatPLuMasFR9tyO+NMAFsF84MT6TK1vA=="],
65
66 "@stacksjs/ts-spell-check": ["@stacksjs/ts-spell-check@0.1.0", "", {}, "sha512-GnrxhHh606U6e0uPoUfbyh3sWI2VCtj6IwtSS4ik8Jz7vF11WFZ4qeCnXlUSZXLLlk0ukcdpilZr4girmx7PbA=="],
67
68 "@stacksjs/ts-svg": ["@stacksjs/ts-svg@0.1.1", "", { "dependencies": { "@stacksjs/ts-css": "^0.1.0", "@stacksjs/ts-png": "^0.1.2" }, "bin": { "svg": "dist/cli.js" } }, "sha512-jG/DC3JaWEvblsrl3X1mVFPZY+1+DEtt9ypWXuXHyWyud+CaD/MQdGRypJbRyLrT03HWx/x6TG9UOzh7GDaing=="],
69
70 "@stacksjs/ts-webp": ["@stacksjs/ts-webp@0.1.0", "", {}, "sha512-wWA+HEmRszZg4av8jzSa1l2eDPEu06Cdy/c9gjlP/7rqUb7K5ETzK5rZt9DgDe28treKGy/vGo/1YS4yiUvtUA=="],
71
72 "@stacksjs/ts-xml": ["@stacksjs/ts-xml@0.1.0", "", { "bin": { "bin-name": "./dist/bin/cli.js" } }, "sha512-YB7FSlFcPqopUOmjK1Qqr5E6QaarerIFJzWfUXOO/EuR1qX+IRRKxOKbWRmCvN72Mnchz8EfXhlvxEK+Zz/mQQ=="],
73
74 "@ts-cloud/aws-types": ["@ts-cloud/aws-types@0.5.35", "", {}, "sha512-yZP8SzM3T3Lpm6o6ohEe52vjq8He41Ml+p+keoJcTL+V2TAOxvlWu6QVw7A+XVNXl39eQrDulygIxfcORoeBAw=="],
75
76 "@ts-cloud/core": ["@ts-cloud/core@0.5.35", "", { "dependencies": { "@ts-cloud/aws-types": "0.5.35" } }, "sha512-3W5Bx2ql4ImF7fSw9lRjZxPchUbOKAFvnQGo5pirtUzCdehMVrzL+H5cU9YszDWUMFhdkyQsYIuRg6a0GeSFMw=="],
77
78 "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
79
80 "@types/css-tree": ["@types/css-tree@2.3.11", "", {}, "sha512-aEokibJOI77uIlqoBOkVbaQGC9zII0A+JH1kcTNKW2CwyYWD8KM6qdo+4c77wD3wZOQfJuNWAr9M4hdk+YhDIg=="],
81
82 "@types/csso": ["@types/csso@5.0.4", "", { "dependencies": { "@types/css-tree": "*" } }, "sha512-W/FsRkm/9c04x9ON+bj+HQ0cSgNkG1LvcfuBCpkP7cpikM7+RkrNFLGtiofb++xBG6KGMUycLoDbi9/K621ZCw=="],
83
84 "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
85
86 "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
87
88 "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
89
90 "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
91
92 "archiver": ["archiver@7.0.1", "", { "dependencies": { "archiver-utils": "^5.0.2", "async": "^3.2.4", "buffer-crc32": "^1.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", "tar-stream": "^3.0.0", "zip-stream": "^6.0.1" } }, "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ=="],
93
94 "archiver-utils": ["archiver-utils@5.0.2", "", { "dependencies": { "glob": "^10.0.0", "graceful-fs": "^4.2.0", "is-stream": "^2.0.1", "lazystream": "^1.0.0", "lodash": "^4.17.15", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA=="],
95
96 "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
97
98 "b4a": ["b4a@1.8.1", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw=="],
99
100 "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
101
102 "bare-events": ["bare-events@2.9.1", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg=="],
103
104 "bare-fs": ["bare-fs@4.7.4", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ=="],
105
106 "bare-path": ["bare-path@3.1.1", "", {}, "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ=="],
107
108 "bare-stream": ["bare-stream@2.13.3", "", { "dependencies": { "b4a": "^1.8.1", "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ=="],
109
110 "bare-url": ["bare-url@2.4.5", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ=="],
111
112 "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
113
114 "better-dx": ["better-dx@0.2.15", "", { "dependencies": { "@stacksjs/bumpx": "^0.2.5", "@stacksjs/bunpress": "^0.1.4", "@stacksjs/clapp": "^0.2.6", "@stacksjs/clarity": "^0.3.28", "@stacksjs/gitlint": "^0.1.5", "@stacksjs/logsmith": "^0.2.1", "@types/bun": "^1.3.13", "buddy-bot": "^0.9.17", "bun-git-hooks": "^0.3.2", "bun-plugin-dtsx": "^0.9.25", "bunfig": "^0.15.11", "pickier": "^0.1.28", "ts-error-handling": "^0.1.1", "typescript": "^6.0.3" } }, "sha512-MYTbINOS8ISmLO5zdjSotRlqiIt6PO7Ap9qOfXU1MD2dUB+V2WHyStAzgHG9QUnmC/vj+8AUfGWYjV3lBgXGXw=="],
115
116 "boolbase": ["boolbase@2.0.0", "", {}, "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA=="],
117
118 "brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="],
119
120 "buddy-bot": ["buddy-bot@0.9.20", "", { "dependencies": { "prompts": "^2.4.2", "ts-pantry": "^0.9.30" }, "bin": { "buddy-bot": "./dist/bin/cli.js" } }, "sha512-5OZxlumHdKBHHpiUhvcl5xC9WmL4F8vcEKVgbFBqrqpvwRR9ejyry4uQgq1NvAgZHLtKRhQgUkOK/ltARxoy2A=="],
121
122 "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
123
124 "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="],
125
126 "bun-git-hooks": ["bun-git-hooks@0.3.2", "", { "bin": { "git-hooks": "./dist/bin/cli.js", "bun-git-hooks": "./dist/bin/cli.js" } }, "sha512-cHkmB7sxmtPNekzG0nGXqbN8Q+dZf4muvV4eCB8dBCduW1DHR+4YulKinPUk/C6Rogy+JMyL1ysVVVWwZobwZw=="],
127
128 "bun-plugin-dtsx": ["bun-plugin-dtsx@0.9.26", "", { "dependencies": { "@stacksjs/dtsx": "0.9.26" } }, "sha512-dOlA+gx3mju+Be0uoZqsuQ3U+GPu08wv7QQwA1k569tw8Bv9GwVWJz0jjQw6tzJi9Rvwfl31MVXu9rHYQXSKow=="],
129
130 "bun-plugin-stx": ["bun-plugin-stx@0.2.82", "", { "dependencies": { "@stacksjs/stx": "^0.2.72", "stx-router": "^0.2.72" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-1H6J1C4hg0fFdw2Xs5qqyBSEsVBiCo5jjuASXqRPAS5WSCT7AY6D8MaUGHYCRSYQ7L+ZdA2/eTG0XwDgrN776A=="],
131
132 "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
133
134 "bunfig": ["bunfig@0.15.14", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./bin/cli.js" } }, "sha512-9yzw5/MkTBF3X8Dc96DJUPpZDl/jIAYVlY6XxGrP8+o5fEf8sGgoIc53GkSFucyMoqC5jF1qM++3bD1LL7/N1Q=="],
135
136 "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
137
138 "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
139
140 "compress-commons": ["compress-commons@6.0.2", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^6.0.0", "is-stream": "^2.0.1", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg=="],
141
142 "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
143
144 "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
145
146 "crc32-stream": ["crc32-stream@6.0.0", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^4.0.0" } }, "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g=="],
147
148 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
149
150 "css-select": ["css-select@7.0.0", "", { "dependencies": { "boolbase": "^2.0.0", "css-what": "^8.0.0", "domhandler": "^6.0.1", "domutils": "^4.0.2", "nth-check": "^3.0.1" } }, "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g=="],
151
152 "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
153
154 "css-what": ["css-what@8.0.0", "", {}, "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw=="],
155
156 "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
157
158 "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
159
160 "dom-serializer": ["dom-serializer@3.1.1", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "entities": "^8.0.0" } }, "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw=="],
161
162 "domelementtype": ["domelementtype@3.0.0", "", {}, "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg=="],
163
164 "domhandler": ["domhandler@6.0.1", "", { "dependencies": { "domelementtype": "^3.0.0" } }, "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg=="],
165
166 "domutils": ["domutils@4.0.2", "", { "dependencies": { "dom-serializer": "^3.0.0", "domelementtype": "^3.0.0", "domhandler": "^6.0.0" } }, "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA=="],
167
168 "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
169
170 "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
171
172 "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
173
174 "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
175
176 "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
177
178 "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
179
180 "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
181
182 "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
183
184 "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
185
186 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
187
188 "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
189
190 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
191
192 "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
193
194 "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
195
196 "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
197
198 "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
199
200 "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
201
202 "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
203
204 "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="],
205
206 "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
207
208 "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
209
210 "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
211
212 "minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
213
214 "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
215
216 "mitata": ["mitata@1.0.34", "", {}, "sha512-Mc3zrtNBKIMeHSCQ0XqRLo1vbdIx1wvFV9c8NJAiyho6AjNfMY8bVhbS12bwciUdd1t4rj8099CH3N3NFahaUA=="],
217
218 "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
219
220 "nth-check": ["nth-check@3.0.1", "", { "dependencies": { "boolbase": "^2.0.0" } }, "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ=="],
221
222 "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
223
224 "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
225
226 "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
227
228 "pickier": ["pickier@0.1.37", "", { "dependencies": { "@stacksjs/clapp": "^0.2.3", "@stacksjs/clarity": "^0.3.24" }, "optionalDependencies": { "@stacksjs/ts-spell-check": "^0.1.0" }, "bin": { "pickier": "./dist/bin/cli.js" } }, "sha512-wna4oJxKm59QuTCwGEMuxowaEeXmj0L1tzHtAkgJ593KM6eKGurp7fDclMdnnnCk1eHlCuxDBI20qvK3DUC4KQ=="],
229
230 "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
231
232 "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
233
234 "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
235
236 "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
237
238 "readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="],
239
240 "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
241
242 "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
243
244 "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
245
246 "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
247
248 "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
249
250 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
251
252 "streamx": ["streamx@2.28.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw=="],
253
254 "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
255
256 "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
257
258 "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
259
260 "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
261
262 "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
263
264 "stx-router": ["stx-router@0.2.82", "", {}, "sha512-IAEow8sBL0en/EBgNDCOvz5cUrJcauaFV+NflP/BQXDhJsmi1FIRTSxwAfv14q7NZ0AjzGu49zGvwcYBuPO9Hw=="],
265
266 "tar-stream": ["tar-stream@3.2.0", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="],
267
268 "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="],
269
270 "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="],
271
272 "ts-broadcasting": ["ts-broadcasting@0.0.4", "", { "bin": { "broadcast": "./dist/bin/cli.js" } }, "sha512-dnPtNM6mTCCnJcf8S9ffnWMOLCr5LRpxz87haYw7AtqyqCkM5B3kT6tXr1jJnxjDlWPfSfhpY+n6Y6N2l9CGPQ=="],
273
274 "ts-error-handling": ["ts-error-handling@0.1.1", "", {}, "sha512-d4VqdX0bJ+jvOYNsY58isJgrEr806Q9cjef5+1UO/kIn1M/+YYSwpR6fqdRhn9XXVASJLSzNmcnBSdl0fRCjDg=="],
275
276 "ts-gif": ["ts-gif@0.1.2", "", { "dependencies": { "data-uri-to-buffer": "^6.0.2" } }, "sha512-jbraplO1qqJtrtkFeepNPUGPad7w8FQHWIQFsn1OJbHeWKUbdqdkrX18XzNXI5Fw7+s6A3nbNTdj+Ukn4d3f8g=="],
277
278 "ts-images": ["ts-images@0.1.8", "", { "dependencies": { "@stacksjs/ts-avif": "^0.1.0", "@stacksjs/ts-bmp": "^0.1.0", "@stacksjs/ts-png": "^0.1.2", "@stacksjs/ts-svg": "^0.1.1", "@stacksjs/ts-webp": "^0.1.0", "ts-gif": "^0.1.2", "ts-jpeg": "^0.3.1" }, "bin": { "imgx": "./dist/bin/cli.js" } }, "sha512-z1S9gRanhORW6/N6QOtRwx2E/jTwIsJMh1KIRm31MEVulq5nsoNI3YFVWdkYnrLnRc3s2A83ptY4DeEPtY7Sqg=="],
279
280 "ts-jpeg": ["ts-jpeg@0.3.1", "", {}, "sha512-KIGTJj4UIlkGoCFEofx8jHHQEkJfSLFJY0MHz60Mu4Scjrcj7K/0UJGCbL9QvfIaFGAOGdf0xNFpYSuzUpimwA=="],
281
282 "ts-pantry": ["ts-pantry@0.9.37", "", { "dependencies": { "@stacksjs/ts-cloud": "^0.2.19", "bunfig": "^0.15.6", "ts-web-scraper": "^0.1.1" }, "bin": { "ts-pantry": "./dist/bin/cli.js" } }, "sha512-mEm/uxcxroKGZTRwXRpvFBs47wLR2djN3zO627Yy/42cmjtY74OuUewhgh1Th9qXhPPHS3zEkE+dMK6RK0rXfw=="],
283
284 "ts-syntax-highlighter": ["ts-syntax-highlighter@0.2.1", "", { "bin": { "ts-syntax-highlighter": "dist/bin/cli.js" } }, "sha512-HEfxaVKifwvnX0NaOE8nfs4VFdZo6WdREj1KRehelfYsO1N+m9vOIj5exhlOqFpxOPWd4BlalU8y0WWS7yPWjw=="],
285
286 "ts-web-scraper": ["ts-web-scraper@0.1.1", "", { "bin": { "scraper": "dist/bin/cli.js" } }, "sha512-js+gSRe95yDD9U3+GmjvIKBYLAN9WoZKV84x7HcSI7dqdWEIoZSeSGR/Jbr8f5hCeV9FD70KdTPIS2LQwviQXQ=="],
287
288 "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
289
290 "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
291
292 "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
293
294 "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
295
296 "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
297
298 "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
299
300 "zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="],
301
302 "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
303
304 "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
305
306 "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
307
308 "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
309
310 "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
311
312 "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
313
314 "ts-pantry/@stacksjs/ts-cloud": ["@stacksjs/ts-cloud@0.2.27", "", { "dependencies": { "@stacksjs/ts-xml": "^0.1.0", "@ts-cloud/aws-types": "0.2.27", "@ts-cloud/core": "0.2.27" }, "bin": { "cloud": "./dist/bin/cli.js" } }, "sha512-81+ORovneY4muI4CoM4bTrq3LORqvzbwVVEo5ifRzNglhc5SQSZWA3/S/nDnMZt7VNhFy0mn8VkqrSyZ9Emwcg=="],
315
316 "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
317
318 "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
319
320 "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
321
322 "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
323
324 "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
325
326 "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
327
328 "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
329
330 "ts-pantry/@stacksjs/ts-cloud/@ts-cloud/aws-types": ["@ts-cloud/aws-types@0.2.27", "", {}, "sha512-9SStgWrllUtIA9xedrMjq0AlzAXt5xjdcVl9Q4xX9UVTp3tFIvYbaoxpCq2WmoaNqYVXTQDdArsbQvkLdpJmiw=="],
331
332 "ts-pantry/@stacksjs/ts-cloud/@ts-cloud/core": ["@ts-cloud/core@0.2.27", "", { "dependencies": { "@ts-cloud/aws-types": "0.2.27" } }, "sha512-ePte26pVqgo/v5cAfinM9eEApENNr0x0uICnqjXeShDGCli1Fyh0MMxuMD60alIP7d/1qLyTESlUTrfhD9uKPQ=="],
333
334 "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
335
336 "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
337 }
338}
packages/toolkit/bunfig.tomladded+3-0
Changes to packages/toolkit/bunfig.toml
@@ -0,0 +1,3 @@
1[install]
2registry = { url = "https://registry.npmjs.org/", token = "$BUN_AUTH_TOKEN" }
3linker = "hoisted"
packages/toolkit/cssadded+2-0
Changes to packages/toolkit/css
@@ -0,0 +1,2 @@
1#!/usr/bin/env bun
2import('./bin/cli')
packages/toolkit/deps.yamladded+2-0
Changes to packages/toolkit/deps.yaml
@@ -0,0 +1,2 @@
1dependencies:
2 bun.sh: ^1.3.14
packages/toolkit/docs/_data/team.jsadded+102-0
Changes to packages/toolkit/docs/_data/team.js
@@ -0,0 +1,102 @@
1export const core = [
2 {
3 avatar: 'https://ca.slack-edge.com/TAFCQEYEP-UAFFN6YSE-fb28a6b5d278-512',
4 name: 'Chris Breuer',
5 title: 'Creator',
6 org: 'Stacks',
7 orgLink: 'https://stacksjs.org/',
8 desc: 'Independent open source developer, builder in the Stacks ecosystem.',
9 links: [
10 { icon: 'github', link: 'https://github.com/chrisbbreuer' },
11 { icon: 'bluesky', link: 'https://bsky.app/profile/chrisbreuer.dev' },
12 { icon: 'twitter', link: 'https://twitter.com/chrisbbreuer' },
13 ],
14 sponsor: 'https://github.com/sponsors/chrisbbreuer',
15 },
16 {
17 avatar: 'https://avatars.githubusercontent.com/u/72235211',
18 name: 'Blake Ayer',
19 title: 'Cloud Genius',
20 org: 'Stacks',
21 orgLink: 'https://stacksjs.org/',
22 desc: 'Core team member of Stacks.',
23 links: [{ icon: 'github', link: 'https://github.com/blakeayer' }],
24 sponsor: 'https://github.com/sponsors/blakeayer',
25 },
26 {
27 avatar: 'https://avatars.githubusercontent.com/u/19656966',
28 name: 'Zoltan',
29 title: 'Desktop Wizard',
30 org: 'Stacks',
31 orgLink: 'https://stacksjs.org/',
32 desc: 'Core team member of Stacks.',
33 links: [{ icon: 'github', link: 'https://github.com/konkonam' }],
34 sponsor: 'https://github.com/sponsors/konkonam',
35 },
36 {
37 avatar: 'https://www.averyahill.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Favatar.626e0c07.jpg&w=128&q=75',
38 name: 'Avery Hill',
39 title: 'Merchandise & Event Planning',
40 org: 'Stacks',
41 orgLink: 'https://stacksjs.org/',
42 desc: 'Core team member of Stacks.',
43 links: [{ icon: 'github', link: 'https://www.averyahill.com/' }],
44 },
45 {
46 avatar: 'https://avatars.githubusercontent.com/u/10015302',
47 name: 'Harlequin Doyon',
48 title: 'A collaborative being',
49 org: 'Stacks',
50 orgLink: 'https://stacksjs.org/',
51 desc: 'Core team member of Stacks.',
52 links: [{ icon: 'github', link: 'https://github.com/harlekoy' }],
53 sponsor: 'https://github.com/sponsors/harlekoy',
54 },
55 {
56 avatar: 'https://ca.slack-edge.com/TAFCQEYEP-UCX5LQ2NP-g16e383ecf66-512',
57 name: 'Germaine Abellanosa',
58 title: 'Social Tech Genius',
59 org: 'Stacks',
60 orgLink: 'https://stacksjs.org/',
61 desc: 'Core team member of Stacks.',
62 links: [{ icon: 'github', link: 'https://github.com/germikee' }],
63 sponsor: 'https://github.com/sponsors/germikee',
64 },
65 {
66 avatar: 'https://avatars.githubusercontent.com/u/58994540',
67 name: 'Frederik Bußmann',
68 title: 'A collaborative being',
69 org: 'Stacks',
70 orgLink: 'https://stacksjs.org/',
71 desc: 'Core team member of Stacks.',
72 links: [{ icon: 'github', link: 'https://github.com/freb97' }],
73 sponsor: 'https://github.com/sponsors/freb97',
74 },
75 {
76 avatar: 'https://avatars.githubusercontent.com/u/977413',
77 name: 'Dorell James',
78 title: 'A collaborative being',
79 org: 'Stacks',
80 orgLink: 'https://stacksjs.org/',
81 desc: 'Core team member of Stacks.',
82 links: [
83 { icon: 'github', link: 'https://github.com/dorelljames' },
84 { icon: 'twitter', link: 'https://twitter.com/dorelljames' },
85 ],
86 sponsor: 'https://github.com/sponsors/dorelljames',
87 },
88 {
89 avatar: 'https://avatars.githubusercontent.com/u/29087513',
90 name: 'Glenn Michael',
91 title: 'Desktop, Mobile, Web',
92 org: 'Stacks',
93 orgLink: 'https://stacksjs.org/',
94 desc: 'Core team member of Stacks.',
95 links: [{ icon: 'github', link: 'https://github.com/glennmichael123' }],
96 sponsor: 'https://github.com/sponsors/glennmichael123',
97 },
98]
99
100export const emeriti = [
101 //
102]
packages/toolkit/docs/api/optimize.mdadded+107-0
Changes to packages/toolkit/docs/api/optimize.md
@@ -0,0 +1,107 @@
1# Optimizer API
2
3Replaces `csso`. Compact a CSS string, remove duplicate declarations,
4shorten colors, and compute selector specificity.
5
6```ts
7import { minify, minifyBlock, specificity, syntax } from '@stacksjs/ts-css'
8// or:
9import { /* … */ } from '@stacksjs/ts-css/optimize'
10```
11
12## `minify(source, options?)`
13
14Parse, compress, and re-stringify a full stylesheet.
15
16```ts
17minify(source: string, options?: MinifyOptions): { css: string, ast: CssNode }
18```
19
20```ts
21minify('.a { margin: 0px 0.5em 10.000px; color: #aabbcc }').css
22// → ".a{margin:0 .5em 10px;color:#abc}"
23```
24
25## `minifyBlock(source, options?)`
26
27Same, but parse `source` as a declaration list (the body of a `style="…"`
28attribute).
29
30```ts
31minifyBlock('color: red; color: blue').css
32// → "color:blue"
33```
34
35## `MinifyOptions`
36
37| Option | Default | Description |
38| ------------------- | ---------------- | ---------------------------------------------------------- |
39| `comments` | `'exclamation'` | `false` to strip all, `true` / `'exclamation'` to keep `/*!*/` comments, `'first-exclamation'` to keep just the first. |
40| `restructure` | `true` | (Reserved — currently a no-op; declaration-level minification only.) |
41| `forceMediaMerge` | `false` | (Reserved.) |
42| `usage` | `null` | Usage hints (`{tags, ids, classes, force}`) — accepted for API parity. |
43
44## What gets compressed
45
46Inside any `Value`, ts-css compresses:
47
48| Input | Output |
49| ------------------ | ---------- |
50| `0.5em` | `.5em` |
51| `10.000px` | `10px` |
52| `0px` / `0em` / … | `0` |
53| `#aabbcc` | `#abc` |
54| `#aabbccdd` | `#abcd` |
55| `rgb(255,0,0)` | `#f00` |
56| `url("/x.png")` | `url(/x.png)` (when safe) |
57
58In each `Block`, duplicate declarations are folded with **last wins**
59semantics (respecting `!important`):
60
61```css
62.a { color: red; color: blue } → .a { color: blue }
63.a { color: red !important;
64 color: blue } → .a { color: red !important }
65```
66
67Comments matching `options.comments` rule are stripped.
68
69## `specificity(node)`
70
71Compute the specificity of a `Selector` AST.
72
73```ts
74import { parse, syntax } from '@stacksjs/ts-css'
75
76const sel = parse('#a.b div', { context: 'selector' })
77syntax.specificity(sel) // [1, 1, 1]
78```
79
80Returns a `[a, b, c]` tuple per
81[selectors level 4](https://www.w3.org/TR/selectors-4/#specificity-rules):
82
83- `a` — id selectors
84- `b` — class / attribute / pseudo-class selectors
85- `c` — type / pseudo-element selectors
86
87`:is()`, `:not()`, `:has()`, `:matches()`, `:-moz-any()`, `:-webkit-any()`
88take the **maximum** specificity of their arguments.
89`:where()` always contributes `[0,0,0]`.
90
91The `syntax.specificity` re-export mirrors `csso.syntax.specificity` so
92existing call sites work without changes.
93
94## Compose your own pipeline
95
96If you want finer control, all the building blocks are exported:
97
98```ts
99import { compressTree, dedupeDeclarations, removeComments } from '@stacksjs/ts-css/optimize'
100import { generate, parse } from '@stacksjs/ts-css/parse'
101
102const ast = parse(input)
103removeComments(ast, { exclamation: true })
104compressTree(ast)
105dedupeDeclarations(ast)
106const out = generate(ast)
107```
packages/toolkit/docs/api/parse.mdadded+166-0
Changes to packages/toolkit/docs/api/parse.md
@@ -0,0 +1,166 @@
1# Parser API
2
3Replaces `css-tree`. Tokenizer, recursive-descent parser, walker,
4generator, deep clone, and a doubly-linked `List`.
5
6```ts
7import { clone, generate, List, parse, walk } from '@stacksjs/ts-css'
8// or, when you only need the parser:
9import { /* … */ } from '@stacksjs/ts-css/parse'
10```
11
12## `parse(source, options?)`
13
14Parse a CSS source string into an AST.
15
16```ts
17parse(source: string, options?: ParseOptions): CssNode
18```
19
20### Options
21
22| Option | Default | Description |
23| ----------------------- | -------------- | ------------------------------------------------------------------------ |
24| `context` | `'stylesheet'` | What grammar to parse against (see below). |
25| `parseValue` | `true` | When `false`, declaration values are stored as `Raw` nodes. |
26| `parseAtrulePrelude` | `true` | When `false`, at-rule preludes are stored as `Raw`. |
27| `parseRulePrelude` | `true` | When `false`, rule preludes (selector lists) are stored as `Raw`. |
28| `parseCustomProperty` | `false` | When `true`, custom property values are parsed (otherwise `Raw`). |
29| `positions` | `false` | Track source `loc` info on every node. |
30| `filename` | — | Source filename, used in `loc.source`. |
31| `onParseError` | — | Called when the parser substitutes a `Raw` node for an error. |
32
33### Parse contexts
34
35| `context` | Returns | Use for |
36| ------------------- | ------------------------------------ | ---------------------------------------------------- |
37| `'stylesheet'` | `StyleSheet` | Top-level CSS files (default) |
38| `'atrule'` | `Atrule \| StyleSheet` | A single `@rule …;` or `@rule { … }` |
39| `'atrulePrelude'` | `AtrulePrelude` | The `(min-width: 800px)` part of a media query |
40| `'rule'` | `Rule` | A single `selector { … }` block |
41| `'block'` | `Block` | Just the `{ decl; decl; }` body |
42| `'declarationList'` | `DeclarationList` | A `style="…"` attribute body |
43| `'declaration'` | `Declaration` | A single `prop: value;` line |
44| `'value'` | `Value` | Just the value side of a declaration |
45| `'selector'` | `Selector` | A single selector segment (`a > b`) |
46| `'selectorList'` | `SelectorList` | A comma-separated list (`a, b > c`) |
47
48## `walk(ast, visitor)`
49
50Recursively traverse the AST.
51
52```ts
53walk(ast: CssNode, visitor: WalkVisitor): symbol | undefined
54```
55
56`visitor` is either:
57- A function: `(node, item, list) => void | symbol`
58- An object: `{ visit?, enter?, leave?, reverse? }`
59
60Inside callbacks, `this` carries traversal context: `this.atrule`,
61`this.rule`, `this.selector`, `this.declaration`, etc.
62
63```ts
64walk(ast, {
65 visit: 'Declaration',
66 enter(decl) {
67 if (decl.property === 'color')
68 decl.value = parse('blue', { context: 'value' })
69 },
70})
71```
72
73### Sentinels
74
75| Constant | What it does |
76| ------------- | --------------------------------------------------------- |
77| `walk.skip` | Return from `enter` to skip the node's children. |
78| `walk.stop` | Return to halt the entire traversal. |
79
80## `generate(node)`
81
82Serialize an AST node back to a CSS string. Output is compact (no
83unnecessary whitespace) — comparable to `csstree.generate(node)` with
84no formatting options.
85
86```ts
87generate(node: CssNode): string
88```
89
90## `clone(node)`
91
92Deep-clone an AST node, preserving the `List` structure of children.
93
94```ts
95clone<T extends CssNode>(node: T): T
96```
97
98## `List<T>`
99
100Doubly linked list — every container node uses one for its `children`.
101
102```ts
103import { List } from '@stacksjs/ts-css'
104
105const list = new List<number>()
106list.appendData(1)
107list.appendData(2)
108list.toArray() // [1, 2]
109```
110
111### Mutation methods
112
113| Method | Description |
114| --------------------------- | ------------------------------------------------- |
115| `appendData(data)` | Append a new item built from `data`. |
116| `prependData(data)` | Prepend a new item. |
117| `insertData(data, before?)` | Insert before `before` (a `ListItem`), or at tail. |
118| `insert(item, before?)` | Insert an existing `ListItem`. |
119| `remove(item)` | Detach an item from the list. |
120| `replace(old, newItem)` | Replace `old` with `newItem` (or another `List`). |
121| `clear()` | Detach all items. |
122
123### Iteration
124
125| Method | Callback signature |
126| ------------------------------- | ----------------------------------------------- |
127| `forEach(fn)` | `(data, item, list)` — matches `css-tree`. |
128| `forEachRight(fn)` | Same, in reverse. |
129| `reduce(fn, init)` | `(acc, data, index, list)` |
130| `some(fn)` | `(data, index, list) => boolean` |
131| `map(fn)` | Returns a new `List`. |
132| `filter(fn)` | Returns a new `List`. |
133
134The `forEach` / `forEachRight` callbacks honor a cursor protocol so
135callbacks can safely insert/remove items mid-walk without breaking the
136iterator.
137
138## AST node reference
139
140The full discriminated union is `CssNode`. The most common variants:
141
142```ts
143interface StyleSheet { type: 'StyleSheet', children: List<CssNode> }
144interface Rule { type: 'Rule', prelude: SelectorList | Raw, block: Block }
145interface Atrule { type: 'Atrule', name: string, prelude: AtrulePrelude | Raw | null, block: Block | null }
146interface Block { type: 'Block', children: List<CssNode> }
147interface Declaration { type: 'Declaration', property: string, value: Value | Raw, important: boolean | string }
148interface Value { type: 'Value', children: List<CssNode> }
149interface Selector { type: 'Selector', children: List<CssNode> }
150// + identifiers, numbers, dimensions, hashes, urls, functions, …
151```
152
153See `src/parse/types.ts` for the complete type definitions.
154
155## Tokenizer (advanced)
156
157If you need raw token output, import `Tokenizer` directly:
158
159```ts
160import { Tokenizer, TokenType } from '@stacksjs/ts-css/parse'
161
162const t = new Tokenizer('.foo { color: red }')
163for (const tok of t.tokens) {
164 console.log(TokenType[tok.type], tok.start, tok.end)
165}
166```
packages/toolkit/docs/api/select.mdadded+123-0
Changes to packages/toolkit/docs/api/select.md
@@ -0,0 +1,123 @@
1# Selector matcher API
2
3Replaces `css-select`. Run any CSS selector against any tree-like data
4structure via an `Adapter`.
5
6```ts
7import { compile, is, selectAll, selectOne } from '@stacksjs/ts-css'
8// or:
9import { /* … */ } from '@stacksjs/ts-css/select'
10```
11
12## `selectAll(selector, root, options)`
13
14Return every element under `root` that matches `selector`.
15
16```ts
17selectAll<Node, ElementNode extends Node>(
18 selector: string | Selector[][] | CompiledQuery<ElementNode>,
19 root: Node | Node[],
20 options: Options<Node, ElementNode>,
21): ElementNode[]
22```
23
24## `selectOne(selector, root, options)`
25
26Return the first match (in document order), or `null`.
27
28```ts
29selectOne(...): ElementNode | null
30```
31
32## `is(node, selector, options)`
33
34Test whether `node` itself matches `selector`.
35
36```ts
37is(node, selector, options): boolean
38```
39
40## `compile(selector, options)`
41
42Compile a selector once and reuse the predicate against many trees.
43
44```ts
45const test = compile('p > span.foo', options)
46adapter.findAll(test, [root])
47```
48
49## `Adapter<Node, ElementNode>`
50
51The adapter is the tree-access contract. Every method has the same
52signature as `css-select`'s — if you already have one, it works as-is.
53
54| Method | Returns | Purpose |
55| ------------------------------------------------- | -------------------------------- | ----------------------------------------------- |
56| `isTag(node)` | `node is ElementNode` | Type guard for element-vs-text/comment. |
57| `getName(elem)` | `string` | Element tag name. |
58| `getAttributeValue(elem, name)` | `string \| undefined` | Attribute value (case-sensitive on `name`). |
59| `hasAttrib(elem, name)` | `boolean` | Whether `elem[name]` is set. |
60| `getChildren(node)` | `Node[]` | Direct children of `node`. |
61| `getParent(elem)` | `ElementNode \| null` | Parent element (or null for the root). |
62| `getSiblings(elem)` | `Node[]` | All siblings, including `elem`. |
63| `getText(node)` | `string` | Text content (used by some pseudos). |
64| `findAll(test, elems)` | `ElementNode[]` | DFS traversal. |
65| `findOne(test, elems)` | `ElementNode \| null` | DFS, first match. |
66| `existsOne(test, elems)` | `boolean` | DFS, just a check. |
67| `removeSubsets(nodes)` | `Node[]` | Remove descendants when their ancestor is also in `nodes`. |
68
69Optional state-aware hooks (always called for `:hover` / `:active` /
70`:visited`):
71
72| Method | Default | When you'd implement it |
73| --------------------- | ---------------- | --------------------------------------------- |
74| `isActive(elem)` | always `false` | DOM-aware match for `:active` |
75| `isHovered(elem)` | always `false` | DOM-aware match for `:hover` |
76| `isVisited(elem)` | always `false` | DOM-aware match for `:visited` |
77| `equals(a, b)` | `===` | Custom node equality (rare). |
78
79If your tree has no notion of focus/hover, leave these undefined — the
80matcher treats stateful pseudos as never-matching.
81
82## `Options`
83
84| Option | Default | Description |
85| ---------------------------- | ------- | -------------------------------------------------------------------- |
86| `adapter` | — | **Required.** The `Adapter` for your tree. |
87| `xmlMode` | `false` | Disable HTML quirks (case-insensitive tags / attrs). |
88| `lowerCaseAttributeNames` | `true` | Lowercase attribute names before matching. |
89| `lowerCaseTags` | `true` | Lowercase tag names. |
90| `cacheResults` | `true` | Cache compiled selectors keyed by selector string. |
91| `context` | — | Reference node(s) for `:scope`. |
92| `pseudos` | — | Map of custom pseudo-class implementations. |
93| `relativeSelector` | — | Match `:has` lazily. |
94| `quirksMode` | `false` | Match HTML quirks-mode attribute case rules. |
95
96## Supported selectors
97
98| Category | Examples |
99| --------------------- | --------------------------------------------------- |
100| Type / universal | `div`, `*`, `svg\|circle` (namespaces in xmlMode) |
101| Class / id | `.foo`, `#bar` |
102| Attribute | `[a]`, `[a=x]`, `[a~=x]`, `[a\|=x]`, `[a^=x]`, `[a$=x]`, `[a*=x]` (with `i` / `s` case flags) |
103| Combinators | `a b`, `a > b`, `a + b`, `a ~ b`, `a \|\| b` |
104| Structural pseudos | `:first-child`, `:last-child`, `:only-child`, `:first-of-type`, `:last-of-type`, `:only-of-type`, `:nth-child(...)`, `:nth-last-child(...)`, `:nth-of-type(...)`, `:nth-last-of-type(...)` |
105| Logical pseudos | `:is(...)`, `:not(...)`, `:where(...)`, `:has(...)`, `:matches(...)` |
106| Other pseudos | `:empty`, `:root`, `:scope` |
107| Stateful pseudos | `:hover`, `:active`, `:visited`, `:focus`, etc. — gated on adapter `is*` hooks. |
108
109## Custom pseudo-classes
110
111Add your own via `options.pseudos`:
112
113```ts
114selectAll(':my-thing', root, {
115 adapter,
116 pseudos: {
117 'my-thing': elem => adapter.getName(elem).startsWith('x-'),
118 },
119})
120```
121
122The value is either a predicate `(elem, arg?) => boolean` or a string
123selector that the matcher will compile.
packages/toolkit/docs/api/what.mdadded+104-0
Changes to packages/toolkit/docs/api/what.md
@@ -0,0 +1,104 @@
1# Selector parser API
2
3Replaces `css-what`. Pure-string-to-selector-AST parsing — separate from
4the matcher, so you can analyse selectors (specificity, rewrites, lint
5rules) without running them.
6
7```ts
8import { isTraversal, parseSelector, stringifySelector } from '@stacksjs/ts-css'
9// or:
10import { parse, stringify, isTraversal } from '@stacksjs/ts-css/what'
11```
12
13## `parse(selector, options?)`
14
15Parse a CSS selector string into a list-of-lists of selector segments.
16
17```ts
18parse(selector: string, options?: ParseOptions): Selector[][]
19```
20
21The outer array is **comma-separated selectors**; the inner arrays are
22**simple selectors and combinators** in source order.
23
24```ts
25import { parse } from '@stacksjs/ts-css/what'
26
27parse('div > p, .foo')
28// [
29// [{type:'tag',name:'div'}, {type:'child'}, {type:'tag',name:'p'}],
30// [{type:'attribute',name:'class',action:'element',value:'foo'}],
31// ]
32```
33
34### Options
35
36| Option | Default | Description |
37| ---------------------------- | ------- | -------------------------------------------------------- |
38| `xmlMode` | `false` | Disable HTML quirks (case-insensitive tags / attrs). |
39| `lowerCaseAttributeNames` | `true` | Lowercase attribute names (HTML default). |
40| `lowerCaseTags` | `true` | Lowercase tag names. |
41
42## `stringify(ast)`
43
44Inverse of `parse` — turn a selector AST back into a string.
45
46```ts
47stringify(ast: Selector[][]): string
48```
49
50```ts
51import { parse, stringify } from '@stacksjs/ts-css/what'
52
53const ast = parse('div > p:not(.x)')
54stringify(ast) // 'div > p:not(.x)'
55```
56
57## `isTraversal(token)`
58
59Returns `true` if `token` is a combinator (descendant / child / sibling /
60adjacent / parent / column-combinator) and not a simple selector.
61
62```ts
63isTraversal(token: Selector): boolean
64```
65
66## Segment types
67
68The `Selector` union contains:
69
70```ts
71type Selector =
72 | { type: 'tag', name: string, namespace: string | null }
73 | { type: 'universal', namespace: string | null }
74 | { type: 'attribute', name: string, action: AttributeAction, value: string, namespace: string | null, ignoreCase: boolean | 'quirks' | null }
75 | { type: 'pseudo', name: string, data: string | Selector[][] | null }
76 | { type: 'pseudo-element', name: string, data: string | null }
77 | { type: 'descendant' }
78 | { type: 'child' }
79 | { type: 'parent' }
80 | { type: 'sibling' }
81 | { type: 'adjacent' }
82 | { type: 'column-combinator' }
83```
84
85`AttributeAction` is one of:
86`'any'`, `'element'`, `'end'`, `'equals'`, `'exists'`, `'hyphen'`, `'not'`, `'start'`.
87
88| `[a]` form | `action` |
89| -------------------- | ------------ |
90| `[a]` | `'exists'` |
91| `[a="x"]` | `'equals'` |
92| `[a~="x"]` | `'element'` |
93| `[a\|="x"]` | `'hyphen'` |
94| `[a^="x"]` | `'start'` |
95| `[a$="x"]` | `'end'` |
96| `[a*="x"]` | `'any'` |
97| `[a!="x"]` | `'not'` |
98
99## Pseudo-class shape
100
101For pseudo-classes that take a selector list (`:is`, `:not`, `:where`,
102`:has`, `:matches`, `:-moz-any`, `:-webkit-any`), the `data` field is
103**already parsed** as a `Selector[][]`. For other pseudo-classes that take
104a string (e.g. `:nth-child(2n+1)`), `data` is the raw inner string.
packages/toolkit/docs/benchmarks.mdadded+91-0
Changes to packages/toolkit/docs/benchmarks.md
@@ -0,0 +1,91 @@
1# Benchmarks
2
3Numbers below are from a representative `~6 KB` CSS fixture
4(reset + utilities + component CSS), measured with
5[`mitata`](https://github.com/evanwashere/mitata) on Bun.
6
7## Run them yourself
8
9```bash
10bun run bench
11```
12
13The bench script lives at `bench/index.bench.ts` and benchmarks every
14public API against the closest community equivalent:
15
16| ts-css module | compared with |
17|---|---|
18| `parse` | `css-tree.parse` |
19| `walk` | `css-tree.walk` |
20| `generate` | `css-tree.generate` |
21| `what.parse`| `css-what.parse` |
22| `selectAll` | `css-select.selectAll` |
23| `minify` | `csso.minify` |
24| **end-to-end**: `minify(source)` | `csso.minify(source)` |
25
26## Latest numbers (Apple M3 Pro, Bun 1.3)
27
28```
29Fixture: ~6 KB stylesheet
30
31parse stylesheet
32 ts-css 83 µs/iter (ts-css 1.10× faster)
33 css-tree 92 µs/iter
34
35generate stylesheet
36 ts-css 19 µs/iter (ts-css 4.0× faster)
37 css-tree 77 µs/iter
38
39walk all nodes
40 ts-css 32 µs/iter (ts-css 1.34× faster)
41 css-tree 43 µs/iter
42
43parse 7 selectors
44 ts-css 1.6 µs/iter (ts-css 1.65× faster)
45 css-what 2.7 µs/iter
46
47selectAll on a 250-node tree
48 ts-css 40 µs/iter (ts-css 1.13× faster)
49 css-select 45 µs/iter
50
51minify stylesheet
52 ts-css 277 µs/iter (ts-css 3.3× faster)
53 csso 907 µs/iter
54
55end-to-end (parse → minify → generate)
56 ts-css 271 µs/iter (ts-css 3.28× faster)
57 csso 890 µs/iter
58```
59
60## Why we win on `generate` and `minify`
61
62- **`generate`** is a tight switch over the AST that walks the doubly
63 linked list directly — no allocations beyond string concatenation, no
64 array materialization to support lookahead, no formatter options.
65- **`minify`** does a single AST walk that combines value compression and
66 whitespace compaction (no second tree traversal), short-circuits
67 declaration dedup on blocks with fewer than two declarations, and uses
68 manual scans where css-select / csso fall back to per-call regex.
69
70## Why we win on `parse` and `selectAll`
71
72- **`parse`** — declaration values and at-rule preludes are parsed
73 directly off the existing token stream by temporarily lowering an
74 in-state `end` index. Re-tokenizing each declaration value text (the
75 obvious approach) is what makes other parsers slower; we never do it.
76- **`selectAll`** — we ship our own iterative `findAll` so the consumer
77 adapter's `findAll` (often a recursive `[].push(...spread)` shape that
78 allocates per node) doesn't gate selector matching. The compile chain
79 uses fail-fast leaves (`ALWAYS_TRUE` short-circuits `&& next(e)` calls)
80 and procedure-cost sorting so the cheapest test in a compound runs
81 first. Compile results are cached two-level (adapter + flags →
82 selector) so per-call key allocation is gone.
83- **`what.parse`** — sticky regex (`/.../y`) lets us match identifier
84 tokens without slicing. `charCodeAt` dispatch instead of `charAt`
85 string allocation.
86
87## End-to-end is what matters
88
89For build-tool pipelines that run parse → walk → mutate → generate, the
90combined pipeline completes in **roughly a third of the time** of the
91csso default — and ships **zero runtime dependencies**.
packages/toolkit/docs/config.mdadded+67-0
Changes to packages/toolkit/docs/config.md
@@ -0,0 +1,67 @@
1# Configuration
2
3_This is just an example of the ts-starter docs._
4
5The Reverse Proxy can be configured using a `reverse-proxy.config.ts` _(or `reverse-proxy.config.js`)_ file and it will be automatically loaded when running the `reverse-proxy` command.
6
7```ts
8// reverse-proxy.config.{ts,js}
9import type { ReverseProxyOptions } from '@stacksjs/rpx'
10import os from 'node:os'
11import path from 'node:path'
12
13const config: ReverseProxyOptions = {
14 /**
15 * The from URL to proxy from.
16 * Default: localhost:5173
17 */
18 from: 'localhost:5173',
19
20 /**
21 * The to URL to proxy to.
22 * Default: stacks.localhost
23 */
24 to: 'stacks.localhost',
25
26 /**
27 * The HTTPS settings.
28 * Default: true
29 * If set to false, the proxy will use HTTP.
30 * If set to true, the proxy will use HTTPS.
31 * If set to an object, the proxy will use HTTPS with the provided settings.
32 */
33 https: {
34 domain: 'stacks.localhost',
35 hostCertCN: 'stacks.localhost',
36 caCertPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.ca.crt`),
37 certPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt`),
38 keyPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt.key`),
39 altNameIPs: ['127.0.0.1'],
40 altNameURIs: ['localhost'],
41 organizationName: 'stacksjs.org',
42 countryName: 'US',
43 stateName: 'California',
44 localityName: 'Playa Vista',
45 commonName: 'stacks.localhost',
46 validityDays: 180,
47 verbose: false,
48 },
49
50 /**
51 * The verbose setting.
52 * Default: false
53 * If set to true, the proxy will log more information.
54 */
55 verbose: false,
56}
57
58export default config
59```
60
61_Then run:_
62
63```bash
64./rpx start
65```
66
67To learn more, head over to the [documentation](https://reverse-proxy.sh/).
packages/toolkit/docs/index.mdadded+38-0
Changes to packages/toolkit/docs/index.md
@@ -0,0 +1,38 @@
1---
2layout: home
3
4hero:
5 name: "@stacksjs/ts-css"
6 text: "Pure-TypeScript CSS toolkit."
7 tagline: "Parser, walker, generator, selector engine, minifier — zero runtime deps."
8 actions:
9 - theme: brand
10 text: Get started
11 link: /intro
12 - theme: alt
13 text: Migration guide
14 link: /migration
15 - theme: alt
16 text: View on GitHub
17 link: https://github.com/stacksjs/ts-css
18
19features:
20 - title: "All-in-one CSS pipeline"
21 icon: "🧩"
22 details: "Replaces css-tree + css-select + css-what + csso. One install, no transitive deps."
23 - title: "Drop-in compatible"
24 icon: "🔁"
25 details: "Namespace re-exports keep existing call sites unchanged. Migrate by editing imports."
26 - title: "Selector engine"
27 icon: "🎯"
28 details: "Run any CSS selector against any tree. Adapter pattern preserved verbatim."
29 - title: "Minifier"
30 icon: "📦"
31 details: "Number / color / unit compression, declaration dedup, comment cleanup, specificity calc."
32 - title: "Bun-first"
33 icon: "🥟"
34 details: "Built on Bun, runs on Node ≥18. Strict TypeScript with isolatedDeclarations."
35 - title: "Small surface, full coverage"
36 icon: "✂️"
37 details: "Pragmatic — every API real consumers (CSSO, SVGO, lint tools) actually use."
38---
packages/toolkit/docs/install.mdadded+76-0
Changes to packages/toolkit/docs/install.md
@@ -0,0 +1,76 @@
1# Install
2
3`@stacksjs/ts-css` is a single package. There are no transitive dependencies.
4
5## Package managers
6
7::: code-group
8
9```sh [bun]
10bun add @stacksjs/ts-css
11# or, dev dependency
12bun add --dev @stacksjs/ts-css
13```
14
15```sh [npm]
16npm install @stacksjs/ts-css
17# or, dev dependency
18npm install --save-dev @stacksjs/ts-css
19```
20
21```sh [pnpm]
22pnpm add @stacksjs/ts-css
23# or, dev dependency
24pnpm add --save-dev @stacksjs/ts-css
25```
26
27```sh [yarn]
28yarn add @stacksjs/ts-css
29```
30
31:::
32
33## Local development with `bun link`
34
35If you're contributing to ts-css and want to test the changes against
36another project locally, register it once with `bun link`:
37
38```sh
39# inside the ts-css repo
40bun link
41
42# inside the consumer project
43bun link @stacksjs/ts-css
44```
45
46That's how [`ts-svg`](https://github.com/stacksjs/ts-svg) consumes ts-css
47during development — its `package.json` carries
48`"@stacksjs/ts-css": "link:@stacksjs/ts-css"`.
49
50## CLI binary (optional)
51
52The `ts-css` CLI ships with the package and is exposed via Bun's `bin`
53field. After installing, you can run:
54
55```sh
56bunx ts-css minify input.css
57```
58
59For a standalone binary, build from source:
60
61```sh
62bun run compile # builds bin/ts-css for your platform
63bun run compile:all # cross-compile for darwin/linux/windows
64```
65
66## Verifying the install
67
68```ts
69import { minify, parse } from '@stacksjs/ts-css'
70
71console.log(minify('.a { color: #aabbcc }').css)
72// → ".a{color:#abc}"
73```
74
75If that prints the minified output, you're set. Continue to
76[**Getting started**](./intro.md).
packages/toolkit/docs/intro.mdadded+99-0
Changes to packages/toolkit/docs/intro.md
@@ -0,0 +1,99 @@
1# Getting started
2
3`@stacksjs/ts-css` is a pure-TypeScript CSS toolkit — a parser, walker,
4generator, selector engine, and minifier — all in one **zero-dependency**
5package. It's a drop-in replacement for the four-library quartet most CSS
6pipelines pull in:
7
8| Replaces | Provides |
9| ------------- | ----------------------------------------------------- |
10| `css-tree` | Tokenizer, parser, walker, generator, clone, `List` |
11| `css-what` | Selector parser & stringifier |
12| `css-select` | `selectAll` / `selectOne` / `is` against any tree |
13| `csso` | Minifier + `syntax.specificity` |
14
15If you've worked with any of those four libraries before, the migration
16is one block of `import` lines (see the [migration guide](./migration.md)).
17
18## Install
19
20```bash
21bun add @stacksjs/ts-css
22# or
23npm i @stacksjs/ts-css
24```
25
26## Your first parse
27
28```ts
29import { generate, parse, walk } from '@stacksjs/ts-css'
30
31const ast = parse('.foo { color: red }')
32
33walk(ast, (node) => {
34 if (node.type === 'Declaration')
35 console.log(node.property)
36})
37// → "color"
38
39console.log(generate(ast))
40// → ".foo{color:red}"
41```
42
43That's the full pipeline: parse → walk/transform → generate.
44
45## Exploring the AST
46
47Pass a `context` to parse a fragment instead of a full stylesheet:
48
49```ts
50parse('color: red', { context: 'declaration' })
51parse('color: red; font-size: 12px', { context: 'declarationList' })
52parse('.a > .b', { context: 'selector' })
53parse('@media (min-width: 800px)', { context: 'atrulePrelude' })
54```
55
56The full set of contexts is in the [parser API reference](./api/parse.md).
57
58## Selecting nodes
59
60`ts-css` ships a tree-agnostic selector engine. Provide an `Adapter` for
61your tree and you can run any CSS selector against it:
62
63```ts
64import { selectAll } from '@stacksjs/ts-css'
65
66const matches = selectAll('p > span.foo:not(.disabled)', root, {
67 adapter: myAdapter,
68 xmlMode: true,
69})
70```
71
72[Full selector matcher API →](./api/select.md)
73
74## Minifying
75
76```ts
77import { minify } from '@stacksjs/ts-css'
78
79minify('.a { margin: 0px; color: #aabbcc }').css
80// → ".a{margin:0;color:#abc}"
81```
82
83[Full minifier API →](./api/optimize.md)
84
85## CLI
86
87```bash
88ts-css minify input.css > output.css
89ts-css parse input.css | jq
90ts-css format input.css
91```
92
93## Where to next
94
95- [**Parser** API](./api/parse.md) — `parse`, `walk`, `generate`, `clone`, `List`
96- [**Selector parser** API](./api/what.md) — `parse`, `stringify`, `isTraversal`
97- [**Selector matcher** API](./api/select.md) — `selectAll`, `selectOne`, `is`
98- [**Optimizer** API](./api/optimize.md) — `minify`, `minifyBlock`, `specificity`
99- [**Migration** guide](./migration.md) — moving off the four-library setup
packages/toolkit/docs/license.mdadded+21-0
Changes to packages/toolkit/docs/license.md
@@ -0,0 +1,21 @@
1# MIT License
2
3Copyright (c) 2025 Stacks.js
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in all
13copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21SOFTWARE.
packages/toolkit/docs/migration.mdadded+125-0
Changes to packages/toolkit/docs/migration.md
@@ -0,0 +1,125 @@
1# Migration guide
2
3Migrating from `css-tree` / `css-select` / `css-what` / `csso` to
4`@stacksjs/ts-css` is one block of `import` lines.
5
6## The diff
7
8```diff
9- import * as csstree from 'css-tree'
10- import * as csswhat from 'css-what'
11- import { is, selectAll, selectOne } from 'css-select'
12- import * as csso from 'csso'
13
14+ import {
15+ csstree, // namespace mirror of css-tree
16+ cssWhat as csswhat, // namespace mirror of css-what
17+ csso, // namespace mirror of csso
18+ is, selectAll, selectOne, // top-level css-select APIs
19+ } from '@stacksjs/ts-css'
20```
21
22That's it. Every method real consumers use is preserved.
23
24## Per-library compatibility
25
26### `css-tree`
27
28| Surface | Compat |
29| -------------------------------------------- | ------ |
30| `parse(source, opts)` — all `context` modes | ✅ |
31| `walk(ast, cb)` & `walk(ast, { visit, enter, leave })` | ✅ |
32| `walk.skip` / `walk.stop` | ✅ |
33| `this.atrule` / `this.rule` context inside callbacks | ✅ |
34| `generate(node)` | ✅ |
35| `clone(node)` | ✅ |
36| `List` class — `forEach(data, item, list)`, `insert`, `remove`, `replace`, `prepend`, `append`, `prependList`, `appendList`, `insertList`, `createItem`, `first`, `last`, `isEmpty` | ✅ |
37| Lexer (`syntax.lexer.match*`, `syntax.match*`) | ❌ — see below |
38
39### `css-what`
40
41| Surface | Compat |
42| -------------------- | ------ |
43| `parse(selector)` | ✅ |
44| `stringify(ast)` | ✅ |
45| `isTraversal(token)` | ✅ |
46| Selector segment shape (incl. `ignoreCase: 'quirks'`) | ✅ |
47
48### `css-select`
49
50| Surface | Compat |
51| ------------------------------------------- | ------ |
52| `selectAll(sel, root, opts)` | ✅ |
53| `selectOne(sel, root, opts)` | ✅ |
54| `is(node, sel, opts)` | ✅ |
55| `compile(sel, opts)` | ✅ |
56| `Adapter` interface | ✅ — same shape, including state hooks (`isActive`, etc.) |
57| `pseudos` extension | ✅ |
58| `:has`, `:is`, `:not`, `:where`, `:matches` | ✅ |
59| `:nth-child`, `:nth-last-child`, `:nth-of-type`, `:nth-last-of-type`, `:first-child`, `:last-child`, `:only-child`, `:first-of-type`, `:last-of-type`, `:only-of-type` | ✅ |
60| `:empty`, `:root`, `:scope` | ✅ |
61| Stateful pseudos via adapter hooks | ✅ |
62
63### `csso`
64
65| Surface | Compat |
66| -------------------------------------------- | ------ |
67| `minify(source, opts)` | ✅ |
68| `minifyBlock(source, opts)` | ✅ |
69| `syntax.specificity(node)` | ✅ |
70| Restructuring across rules / selectors | ❌ — declaration-level only |
71
72## What's intentionally not ported
73
74We dropped two large chunks of the original libs that real consumers
75rarely need:
76
77### `css-tree`'s lexer (`syntax.lexer`)
78
79`css-tree` ships ~3000 lines of CSS spec data that lets you ask "is the
80string `red 1px solid` a valid `<border>` shorthand?". This is great for
81linters and editors, but downstream consumers like CSSO, SVGO, and most
82formatters never touch it.
83
84The AST that ts-css produces is **rich enough** for you to add lexer
85behaviour back on top — every node carries its source slice and a typed
86discriminated union, so a value-type checker is a straightforward
87external library.
88
89### CSSO's restructuring pass
90
91`csso` does heroic work to merge declarations across rules:
92
93```css
94.a { color: red; padding: 4px }
95.b { color: red }
96.c { padding: 4px }
97```
98
99→ rewritten to share declarations between rules. Beyond gzip's reach
100this saves **single-digit percent**, costs ~1000 lines, and breaks subtly
101in edge cases (specificity, source-order dependencies, vendor-prefix
102collisions). ts-css does declaration-level minification (numbers,
103colors, dedup-within-rule) which catches the bulk of the bytes.
104
105If you have a use case where restructuring matters, file an issue —
106adding it back is straightforward, but we'd rather not ship dead code.
107
108## Real-world swap: `ts-svg`
109
110[`ts-svg`](https://github.com/stacksjs/ts-svg)'s SVGO port uses ts-css.
111The migration was 7 files and looked like:
112
113```diff
114- import * as csstree from 'css-tree'
115- import * as csswhat from 'css-what'
116- import { is, selectAll, selectOne } from 'css-select'
117- import { syntax } from 'csso'
118- import * as csso from 'csso'
119
120+ import { csstree, cssWhat as csswhat, csso, is, selectAll, selectOne } from '@stacksjs/ts-css'
121+ const { syntax } = csso
122```
123
124The full SVGO test suite (82 tests) passes after the swap with no
125behaviour changes, and removes 4 transitive dep trees from `node_modules`.
packages/toolkit/docs/partners.mdadded+8-0
Changes to packages/toolkit/docs/partners.md
@@ -0,0 +1,8 @@
1# Partners
2
3The following companies and organizations are supporting Stacks development through partnerships:
4
5- [JetBrains](https://www.jetbrains.com/)
6- [The Solana Foundation](https://solana.com/)
7
8If you are interested in becoming a partner, please reach out to us.
packages/toolkit/docs/postcardware.mdadded+11-0
Changes to packages/toolkit/docs/postcardware.md
@@ -0,0 +1,11 @@
1# Postcardware
2
3Two things are true: Stacks OSS will always stay open-source, and we do love to receive postcards from wherever Stacks is used! ��
4
5_We also publish them on our website._
6
7## Address
8
9Our address in the US is:
10
11Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States ��
packages/toolkit/docs/public/images/favicon-dark.svgadded+12-0
Changes to packages/toolkit/docs/public/images/favicon-dark.svg
@@ -0,0 +1,12 @@
1<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
2<g clip-path="url(#clip0_1142_2)">
3<path fill-rule="evenodd" clip-rule="evenodd" d="M-0.5 -0.5C132.833 -0.5 266.167 -0.5 399.5 -0.5C399.5 132.833 399.5 266.167 399.5 399.5C266.167 399.5 132.833 399.5 -0.5 399.5C-0.5 266.167 -0.5 132.833 -0.5 -0.5Z" fill="#F4F5F7"/>
4<path d="M279.057 114.88L314.118 79.96C320.024 74.0775 315.852 64 307.532 64H204.085C204.085 64 148.635 64 112.145 107.943C110.903 109.443 109.684 110.943 108.419 112.396C106.896 114.177 104.271 117.388 101.247 121.887C80.3893 152.823 76.5224 192.008 89.4591 226.998C95.3182 242.794 104.763 261.614 119.223 274.175L135.605 257.559C146.034 246.989 148.143 231.029 141.44 217.788C137.222 209.445 133.918 198.031 134.457 183.079C134.996 168.127 139.87 153.104 149.385 141.316C159.463 128.824 176.712 116.356 205.421 116.684L279.057 114.927V114.88Z" fill="#0A0ABC"/>
5<path d="M120.606 273.12L85.5453 308.039C79.6394 313.922 83.8112 324 92.131 324H195.578C195.578 324 251.028 324 287.518 280.057C288.76 278.557 289.979 277.057 291.244 275.604C292.768 273.823 295.393 270.612 298.416 266.112C319.274 235.177 323.141 195.991 310.204 161.001C304.345 145.205 294.901 126.386 280.44 113.824L264.059 130.44C253.63 141.01 251.52 156.97 258.223 170.212C262.442 178.555 265.746 189.968 265.207 204.921C264.668 219.873 259.793 234.895 250.278 246.684C240.201 259.175 222.951 271.643 194.242 271.315L120.606 273.073V273.12Z" fill="#0A0ABC"/>
6</g>
7<defs>
8<clipPath id="clip0_1142_2">
9<rect width="400" height="400" fill="white"/>
10</clipPath>
11</defs>
12</svg>
packages/toolkit/docs/public/images/favicon.svgadded+12-0
Changes to packages/toolkit/docs/public/images/favicon.svg
@@ -0,0 +1,12 @@
1<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
2<g clip-path="url(#clip0_1142_2)">
3<path fill-rule="evenodd" clip-rule="evenodd" d="M-0.5 -0.5C132.833 -0.5 266.167 -0.5 399.5 -0.5C399.5 132.833 399.5 266.167 399.5 399.5C266.167 399.5 132.833 399.5 -0.5 399.5C-0.5 266.167 -0.5 132.833 -0.5 -0.5Z" fill="#F4F5F7"/>
4<path d="M279.057 114.88L314.118 79.96C320.024 74.0775 315.852 64 307.532 64H204.085C204.085 64 148.635 64 112.145 107.943C110.903 109.443 109.684 110.943 108.419 112.396C106.896 114.177 104.271 117.388 101.247 121.887C80.3893 152.823 76.5224 192.008 89.4591 226.998C95.3182 242.794 104.763 261.614 119.223 274.175L135.605 257.559C146.034 246.989 148.143 231.029 141.44 217.788C137.222 209.445 133.918 198.031 134.457 183.079C134.996 168.127 139.87 153.104 149.385 141.316C159.463 128.824 176.712 116.356 205.421 116.684L279.057 114.927V114.88Z" fill="#0A0ABC"/>
5<path d="M120.606 273.12L85.5453 308.039C79.6394 313.922 83.8112 324 92.131 324H195.578C195.578 324 251.028 324 287.518 280.057C288.76 278.557 289.979 277.057 291.244 275.604C292.768 273.823 295.393 270.612 298.416 266.112C319.274 235.177 323.141 195.991 310.204 161.001C304.345 145.205 294.901 126.386 280.44 113.824L264.059 130.44C253.63 141.01 251.52 156.97 258.223 170.212C262.442 178.555 265.746 189.968 265.207 204.921C264.668 219.873 259.793 234.895 250.278 246.684C240.201 259.175 222.951 271.643 194.242 271.315L120.606 273.073V273.12Z" fill="#0A0ABC"/>
6</g>
7<defs>
8<clipPath id="clip0_1142_2">
9<rect width="400" height="400" fill="white"/>
10</clipPath>
11</defs>
12</svg>
packages/toolkit/docs/public/images/logo-mini.svgadded+4-0
Changes to packages/toolkit/docs/public/images/logo-mini.svg
@@ -0,0 +1,4 @@
1<svg width="236" height="260" viewBox="0 0 236 260" fill="none" xmlns="http://www.w3.org/2000/svg">
2<path d="M197.057 50.8799L232.118 15.96C238.024 10.0775 233.852 0 225.532 0H122.085C122.085 0 66.6354 0 30.1453 43.9428C28.9032 45.4427 27.6844 46.9426 26.4189 48.3956C24.8955 50.1768 22.2707 53.3876 19.2474 57.8873C-1.61074 88.823 -5.47763 128.008 7.45912 162.998C13.3182 178.794 22.763 197.614 37.2231 210.175L53.6049 193.559C64.034 182.989 66.1432 167.029 59.4405 153.788C55.222 145.445 51.9175 134.031 52.4566 119.079C52.9956 104.127 57.8703 89.1042 67.3854 77.3158C77.4629 64.8244 94.7119 52.3564 123.421 52.6845L197.057 50.9268V50.8799Z" fill="#0A0ABC"/>
3<path d="M38.6059 209.12L3.54534 244.039C-2.36057 249.922 1.8112 260 10.131 260H113.578C113.578 260 169.028 260 205.518 216.057C206.76 214.557 207.979 213.057 209.244 211.604C210.768 209.823 213.393 206.612 216.416 202.112C237.274 171.177 241.141 131.991 228.204 97.0012C222.345 81.2052 212.901 62.386 198.44 49.8242L182.059 66.4404C171.63 77.0101 169.52 92.9702 176.223 106.212C180.442 114.555 183.746 125.968 183.207 140.921C182.668 155.873 177.793 170.895 168.278 182.684C158.201 195.175 140.951 207.643 112.242 207.315L38.6059 209.073V209.12Z" fill="#0A0ABC"/>
4</svg>
packages/toolkit/docs/public/images/logo-transparent.svgadded+4-0
Changes to packages/toolkit/docs/public/images/logo-transparent.svg
@@ -0,0 +1,4 @@
1<svg width="236" height="260" viewBox="0 0 236 260" fill="none" xmlns="http://www.w3.org/2000/svg">
2<path d="M197.057 50.8799L232.118 15.96C238.024 10.0775 233.852 0 225.532 0H122.085C122.085 0 66.6354 0 30.1453 43.9428C28.9032 45.4427 27.6844 46.9426 26.4189 48.3956C24.8955 50.1768 22.2707 53.3876 19.2474 57.8873C-1.61074 88.823 -5.47763 128.008 7.45912 162.998C13.3182 178.794 22.763 197.614 37.2231 210.175L53.6049 193.559C64.034 182.989 66.1432 167.029 59.4405 153.788C55.222 145.445 51.9175 134.031 52.4566 119.079C52.9956 104.127 57.8703 89.1042 67.3854 77.3158C77.4629 64.8244 94.7119 52.3564 123.421 52.6845L197.057 50.9268V50.8799Z" fill="#0A0ABC"/>
3<path d="M38.6059 209.12L3.54534 244.039C-2.36057 249.922 1.8112 260 10.131 260H113.578C113.578 260 169.028 260 205.518 216.057C206.76 214.557 207.979 213.057 209.244 211.604C210.768 209.823 213.393 206.612 216.416 202.112C237.274 171.177 241.141 131.991 228.204 97.0012C222.345 81.2052 212.901 62.386 198.44 49.8242L182.059 66.4404C171.63 77.0101 169.52 92.9702 176.223 106.212C180.442 114.555 183.746 125.968 183.207 140.921C182.668 155.873 177.793 170.895 168.278 182.684C158.201 195.175 140.951 207.643 112.242 207.315L38.6059 209.073V209.12Z" fill="#0A0ABC"/>
4</svg>
packages/toolkit/docs/public/images/logo-white-transparent.svgadded+4-0
Changes to packages/toolkit/docs/public/images/logo-white-transparent.svg
@@ -0,0 +1,4 @@
1<svg width="236" height="260" viewBox="0 0 236 260" fill="none" xmlns="http://www.w3.org/2000/svg">
2<path d="M197.057 50.8799L232.118 15.96C238.024 10.0775 233.852 0 225.532 0H122.085C122.085 0 66.6354 0 30.1453 43.9428C28.9032 45.4427 27.6844 46.9426 26.4189 48.3956C24.8955 50.1768 22.2707 53.3876 19.2474 57.8873C-1.61074 88.823 -5.47763 128.008 7.45912 162.998C13.3182 178.794 22.763 197.614 37.2231 210.175L53.6049 193.559C64.034 182.989 66.1432 167.029 59.4405 153.788C55.222 145.445 51.9175 134.031 52.4566 119.079C52.9956 104.127 57.8703 89.1042 67.3854 77.3158C77.4629 64.8244 94.7119 52.3564 123.421 52.6845L197.057 50.9268V50.8799Z" fill="#fff"/>
3<path d="M38.6059 209.12L3.54534 244.039C-2.36057 249.922 1.8112 260 10.131 260H113.578C113.578 260 169.028 260 205.518 216.057C206.76 214.557 207.979 213.057 209.244 211.604C210.768 209.823 213.393 206.612 216.416 202.112C237.274 171.177 241.141 131.991 228.204 97.0012C222.345 81.2052 212.901 62.386 198.44 49.8242L182.059 66.4404C171.63 77.0101 169.52 92.9702 176.223 106.212C180.442 114.555 183.746 125.968 183.207 140.921C182.668 155.873 177.793 170.895 168.278 182.684C158.201 195.175 140.951 207.643 112.242 207.315L38.6059 209.073V209.12Z" fill="#fff"/>
4</svg>
packages/toolkit/docs/public/images/logo-white.pngadded+0-0

Binary file. Nothing to show line by line.

packages/toolkit/docs/public/images/logo.pngadded+0-0

Binary file. Nothing to show line by line.

packages/toolkit/docs/public/images/logo.svgadded+7-0
Changes to packages/toolkit/docs/public/images/logo.svg
@@ -0,0 +1,7 @@
1<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
2<g clip-path="url(#clip0_1142_2)">
3<path fill-rule="evenodd" clip-rule="evenodd" d="M-0.5 -0.5C132.833 -0.5 266.167 -0.5 399.5 -0.5C399.5 132.833 399.5 266.167 399.5 399.5C266.167 399.5 132.833 399.5 -0.5 399.5C-0.5 266.167 -0.5 132.833 -0.5 -0.5Z" fill="#F4F5F7"/>
4<path d="M279.057 114.88L314.118 79.96C320.024 74.0775 315.852 64 307.532 64H204.085C204.085 64 148.635 64 112.145 107.943C110.903 109.443 109.684 110.943 108.419 112.396C106.896 114.177 104.271 117.388 101.247 121.887C80.3893 152.823 76.5224 192.008 89.4591 226.998C95.3182 242.794 104.763 261.614 119.223 274.175L135.605 257.559C146.034 246.989 148.143 231.029 141.44 217.788C137.222 209.445 133.918 198.031 134.457 183.079C134.996 168.127 139.87 153.104 149.385 141.316C159.463 128.824 176.712 116.356 205.421 116.684L279.057 114.927V114.88Z" fill="#0A0ABC"/>
5<path d="M120.606 273.12L85.5453 308.039C79.6394 313.922 83.8112 324 92.131 324H195.578C195.578 324 251.028 324 287.518 280.057C288.76 278.557 289.979 277.057 291.244 275.604C292.768 273.823 295.393 270.612 298.416 266.112C319.274 235.177 323.141 195.991 310.204 161.001C304.345 145.205 294.901 126.386 280.44 113.824L264.059 130.44C253.63 141.01 251.52 156.97 258.223 170.212C262.442 178.555 265.746 189.968 265.207 204.921C264.668 219.873 259.793 234.895 250.278 246.684C240.201 259.175 222.951 271.643 194.242 271.315L120.606 273.073V273.12Z" fill="#0A0ABC"/>
6</g>
7</svg>
packages/toolkit/docs/public/images/og-image.pngadded+0-0

Binary file. Nothing to show line by line.

packages/toolkit/docs/showcase.mdadded+101-0
Changes to packages/toolkit/docs/showcase.md
@@ -0,0 +1,101 @@
1# Showcase
2
3Here are all the cool projects that people have built using `rpx`.
4
5## Community Projects
6
7- _Yours could be here!_
8
9If you authored a project you’d like to showcase, please share it with us in any way _(on Discord, Social Media, or via a PR, etc.)_, and we’ll add it here.
10
11## First-Party
12
13The whole Stacks ecosystem is built using `rpx`. Here are some of the projects utilizing it:
14
15- [`rpx`](https://github.com/stacksjs/rpx) _(A modern, fast reverse proxy)_
16- [`tlsx`](https://github.com/stacksjs/tlsx) _(A TLS library with automation. HTTPS by default through a light-weight library. Similar to mkcert.)_
17- [`dtsx`](https://github.com/stacksjs/dtsx) _(Extremely fast & configurable DTS emitter)_
18- [`bunfig`](https://github.com/stacksjs/bunfig) _(Lightweight & smart Bun configuration loader)_
19- [`bun-plugin-auto-imports`](https://github.com/stacksjs/bun-plugin-auto-imports) _(Auto Imports support for Bun)_
20- [`ts-collect`](https://github.com/stacksjs/ts-collect) _(Lightweight & powerful Laravel-like Collections written for TypeScript)_
21- [`ts-spreadsheets`](https://github.com/stacksjs/ts-spreadsheets) _(Easily generate spreadsheets, like CSVs and Excel files)_
22- [`localtunnels`](https://github.com/stacksjs/localtunnels) _(A simple and smart tunneling alternative. Without or with self-hosting)_
23- [`vite-plugin-local`](https://github.com/stacksjs/vite-plugin-local) _(A Vite plugin for pretty development URLs and HTTPS. Zero config, zero setup)_
24
25### Stacks Framework
26
27As mentioned before, the whole core of the Stacks Framework is built using `dtsx`. Here are some of those Stacks core packages:
28
29- [`@stacksjs/actions`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/actions)
30- [`@stacksjs/ai`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/ai)
31- [`@stacksjs/alias`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/alias)
32- [`@stacksjs/analytics`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/analytics)
33- [`@stacksjs/api`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/api)
34- [`@stacksjs/arrays`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/arrays)
35- [`@stacksjs/auth`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/auth)
36- [`@stacksjs/browser`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/browser)
37- [`@stacksjs/buddy`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/buddy)
38- [`@stacksjs/build`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/build)
39- [`@stacksjs/cache`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/cache)
40- [`@stacksjs/calendar`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/calendar)
41- [`@stacksjs/chat`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/chat)
42- [`@stacksjs/cli`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/cli)
43- [`@stacksjs/cloud`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/cloud)
44- [`@stacksjs/collections`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/collections)
45- [`@stacksjs/commerce`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/commerce)
46- [`@stacksjs/components`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/components)
47- [`@stacksjs/config`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/config)
48- [`@stacksjs/cron`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/cron)
49- [`@stacksjs/database`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/database)
50- [`@stacksjs/datetime`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/datetime)
51- [`@stacksjs/desktop`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/desktop)
52- [`@stacksjs/development`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/development)
53- [`@stacksjs/dns`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/dns)
54- [`@stacksjs/docs`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/docs)
55- [`@stacksjs/email`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/email)
56- [`@stacksjs/enums`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/enums)
57- [`@stacksjs/env`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/env)
58- [`@stacksjs/error-handling`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/error-handling)
59- [`@stacksjs/events`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/events)
60- [`@stacksjs/faker`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/faker)
61- [`@stacksjs/git`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/git)
62- [`@stacksjs/health`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/health)
63- [`@stacksjs/http`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/http)
64- [`@stacksjs/lint`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/lint)
65- [`@stacksjs/logging`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/logging)
66- [`@stacksjs/notifications`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/notifications)
67- [`@stacksjs/objects`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/objects)
68- [`@stacksjs/orm`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/orm)
69- [`@stacksjs/path`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/path)
70- [`@stacksjs/payments`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/payments)
71- [`@stacksjs/plugins`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/plugins)
72- [`@stacksjs/push`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/push)
73- [`@stacksjs/query-builder`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/query-builder)
74- [`@stacksjs/queue`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/queue)
75- [`@stacksjs/raycast`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/raycast)
76- [`@stacksjs/realtime`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/realtime)
77- [`@stacksjs/registry`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/registry)
78- [`@stacksjs/repl`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/repl)
79- [`@stacksjs/router`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/router)
80- [`@stacksjs/scheduler`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/scheduler)
81- [`@stacksjs/search-engine`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/search-engine)
82- [`@stacksjs/security`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/security)
83- [`@stacksjs/server`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/server)
84- [`@stacksjs/shell`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/shell)
85- [`@stacksjs/slug`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/slug)
86- [`@stacksjs/sms`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/sms)
87- [`@stacksjs/storage`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/storage)
88- [`@stacksjs/strings`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/strings)
89- [`@stacksjs/testing`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/testing)
90- [`@stacksjs/tinker`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/tinker)
91- [`@stacksjs/tunnel`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/tunnel)
92- [`@stacksjs/types`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/types)
93- [`@stacksjs/ui`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/ui)
94- [`@stacksjs/utils`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/utils)
95- [`@stacksjs/validation`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/validation)
96- [`@stacksjs/vite-config`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/vite-config)
97- [`@stacksjs/vite-plugin`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/vite-plugin)
98- [`@stacksjs/whois`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/whois)
99- [`@stacksjs/x-ray`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/x-ray)
100
101We try to keep the list up-to-date, but it's possible that some packages are missing. If you find any, please let us know!
packages/toolkit/docs/sponsors.mdadded+131-0
Changes to packages/toolkit/docs/sponsors.md
@@ -0,0 +1,131 @@
1# Become a Stacks.js Sponsor
2
3Stacks.js is an MIT licensed open source project and completely free to use. The tremendous amount of effort needed to maintain such a large ecosystem and develop new features for the project is only made sustainable thanks to the generous financial backing of our sponsors.
4
5## How to Sponsor
6
7Sponsorships can be done via [GitHub Sponsors](https://github.com/sponsors/chrisbbreuer) or [OpenCollective](https://opencollective.com/stacksjs). Invoices can be obtained via GitHub's payment system. Both monthly-recurring sponsorships and one-time donations are accepted. Recurring sponsorships are entitled to logo placements as specified in Sponsorship Tiers.
8
9If you have questions regarding tiers, payment logistics, or sponsor exposure data, please reach out to <sponsor@stacksjs.org>.
10
11The following companies and organizations are supporting Stacks development through sponsorships:
12
13## Sponsoring Stacks as a Business
14
15Sponsoring Stacks gives you developer exposure around the world through our website and GitHub project READMEs. This not only directly generates leads, but also improves your brand recognition as a business that cares about Open Source. This is an intangible but extremely important asset for companies building products for developers, as it improves your conversion rate.
16
17If you are using Stacks to build a revenue-generating product, it makes business sense to sponsor Stacks's development: it ensures the project that your product relies on stays healthy and actively maintained. The exposure and positive brand image in the Stacks community also makes it easier to attract and recruit Stacks developers.
18
19If you are building a product where your target customers are developers, you will gain high quality traffic through the sponsorship exposure, since all our visitors are developers. The sponsorship also builds brand recognition and improves conversion.
20
21## Sponsoring Stacks as an Individual
22
23If you are an individual user and have enjoyed the productivity of using Stacks, consider donating as a sign of appreciation - like buying us coffee once in a while. Many of our team members accept sponsorships and donations via GitHub Sponsors. Look for the "Sponsor" button on each team member's profile on our [team page](https://stacksjs-docs.netlify.app/team).
24
25You can also try to convince your employer to sponsor Stacks as a business. This may not be easy, but business sponsorships typically make a much larger impact on the sustainability of OSS projects than individual donations, so you will help us much more if you succeed.
26
27## Tier Benefits
28
29- ***Global Special Sponsor***
30 - Limited to one sponsor globally. Currently vacant. [Get in touch!](mailto:sponsors@stacksjs.org)
31 - *(Exclusive)* Above the fold logo placement on the front page of [stacksjs.org](https://stacksjs.org).
32 - *(Exclusive)* Special shoutout and regular retweets of major product launches via Stacks's official social accounts.
33 - Most prominent logo placement in all locations from tiers below.
34
35___
36
37- **Diamond (USD$2,500/mo):**
38 - Prominent logo placement on the front page of [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri).
39 - Prominent logo placement in sidebar of all content pages.
40 - Prominent logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri`
41
42___
43
44- **Platinum (USD$1,000/mo):**
45 - Large logo placement on the front page of [stacksjs.org](https://stacksjs.org), [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri).
46 - Large logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri`
47
48___
49
50- **Double Gold (USD$500/mo):**
51 - Large logo placement on the front page of [stacksjs.org](https://stacksjs.org), [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri).
52 - Large logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri`
53
54___
55
56- **Gold (USD$250/mo):**
57 - Large logo placement on the front page of [stacksjs.org](https://stacksjs.org), [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri).
58 - Large logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri`
59
60___
61
62- **Silver (USD$100/mo):**
63 - Medium logo placement on the front page of [stacksjs.org](https://stacksjs.org), [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri).
64 - Medium logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri`
65 - Medium logo placement in the `BACKERS.md` file of>70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri`
66
67___
68
69- **Bronze (50/mo):**
70 - Small logo placement on the front page of [stacksjs.org](https://stacksjs.org), [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri).
71 - Small logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri`
72 - Small logo placement in the `BACKERS.md` file of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri`
73
74___
75
76- **Generous Backer (USD$25/mo):**
77 - Private Discord channel invite
78 - Free Personal Account for Stacks Dashboard
79 - Social media follow
80 - Name listed in the `BACKERS.md` file of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri`
81
82___
83
84- **Sponsorware Tier (USD$10/mo):**
85 - Get access to all current sponsorware projects *(i.e. the Stacks DynamoDB ORM driver)*
86 - Name listed in the `BACKERS.md` file of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri`
87
88___
89
90- **Individual Backer (USD$5/mo):**
91 - Name listed in the `BACKERS.md` file of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri`
92
93## Current Sponsors
94
95### Global Special Sponsor
96
97- Vacant
98
99### Diamond Sponsors
100
101- Vacant
102
103### Platinum Sponsors
104
105- Vacant
106
107### Gold Sponsors
108
109- Vacant
110
111### Silver Sponsors
112
113- Vacant
114
115### Bronze Sponsors
116
117- Vacant
118
119### Generous Backers
120
121- Vacant
122
123### Individual Backers
124
125- Vacant
126
127___
128
129##### Thanks to Vue.js for the inspiration of this sponsorship page
130
131*You can find their sponsorship page [here](https://vuejs.org/sponsor/).*
packages/toolkit/docs/stargazers.mdadded+3-0
Changes to packages/toolkit/docs/stargazers.md
@@ -0,0 +1,3 @@
1## Stargazers
2
3[![Stargazers](https://starchart.cc/stacksjs/ts-starter.svg?variant=adaptive)](https://starchart.cc/stacksjs/ts-starter)
packages/toolkit/docs/team.mdadded+36-0
Changes to packages/toolkit/docs/team.md
@@ -0,0 +1,36 @@
1---
2layout: page
3title: Meet the Team
4description: A team of incredible people.
5sidebar: false
6---
7
8<script setup>
9import {
10 VPTeamPage,
11 VPTeamPageTitle,
12 VPTeamPageSection,
13 VPTeamMembers
14} from 'vitepress/theme'
15import { core, emeriti } from './_data/team'
16</script>
17
18<VPTeamPage>
19 <VPTeamPageTitle>
20 <template #title>Meet the Team</template>
21 <template #lead>
22 A team of incredible talent & ambition.
23 </template>
24 </VPTeamPageTitle>
25 <VPTeamMembers :members="core" />
26 <!-- <VPTeamPageSection>
27 <template #title>Team Emeriti</template>
28 <template #lead>
29 Here we honor some no-longer-active team members who have made valuable
30 contributions in the past.
31 </template>
32 <template #members>
33 <VPTeamMembers size="small" :members="emeriti" />
34 </template>
35 </VPTeamPageSection> -->
36</VPTeamPage>
packages/toolkit/docs/usage.mdadded+162-0
Changes to packages/toolkit/docs/usage.md
@@ -0,0 +1,162 @@
1# Usage
2
3`@stacksjs/ts-css` is a library first, with a small CLI on top. Most
4consumers import it as a library; the CLI is a convenience wrapper for
5ad-hoc minification and AST inspection.
6
7## Library
8
9### Full pipeline: parse → walk → generate
10
11```ts
12import { generate, parse, walk } from '@stacksjs/ts-css'
13
14const ast = parse(`
15 .foo {
16 color: red;
17 background: url("/assets/bg.png");
18 }
19`)
20
21walk(ast, {
22 visit: 'Url',
23 enter(url) {
24 console.log('asset:', url.value)
25 },
26})
27
28console.log(generate(ast))
29```
30
31### Selector matching against your tree
32
33```ts
34import { selectAll } from '@stacksjs/ts-css'
35
36interface MyEl {
37 type: 'el'
38 name: string
39 attrs: Record<string, string>
40 children: MyEl[]
41 parent: MyEl | null
42}
43
44const adapter = {
45 isTag: (n: any): n is MyEl => n?.type === 'el',
46 getName: (e: MyEl) => e.name,
47 getParent: (e: MyEl) => e.parent,
48 getChildren: (e: MyEl) => e.children,
49 getSiblings: (e: MyEl) => e.parent?.children ?? [e],
50 getAttributeValue: (e: MyEl, n: string) => e.attrs[n],
51 hasAttrib: (e: MyEl, n: string) => n in e.attrs,
52 getText: () => '',
53 removeSubsets: (xs: any[]) => xs,
54 existsOne: (test: (e: MyEl) => boolean, xs: any[]): boolean =>
55 xs.some(x => adapter.isTag(x) && (test(x) || adapter.existsOne(test, x.children))),
56 findAll: (test: (e: MyEl) => boolean, xs: any[]): MyEl[] => {
57 const out: MyEl[] = []
58 for (const x of xs) {
59 if (adapter.isTag(x)) {
60 if (test(x)) out.push(x)
61 out.push(...adapter.findAll(test, x.children))
62 }
63 }
64 return out
65 },
66 findOne: (test: (e: MyEl) => boolean, xs: any[]): MyEl | null => {
67 for (const x of xs) {
68 if (adapter.isTag(x)) {
69 if (test(x)) return x
70 const r = adapter.findOne(test, x.children)
71 if (r) return r
72 }
73 }
74 return null
75 },
76}
77
78const matches = selectAll('p > .foo:not(.disabled)', root, {
79 adapter,
80 xmlMode: true,
81})
82```
83
84### Minify a stylesheet
85
86```ts
87import { minify } from '@stacksjs/ts-css'
88
89const { css } = minify(input, {
90 comments: 'exclamation', // keep `/*!*/` comments
91})
92```
93
94### Compute selector specificity
95
96```ts
97import { parse, syntax } from '@stacksjs/ts-css'
98
99const sel = parse('#a.b div', { context: 'selector' })
100syntax.specificity(sel) // [1, 1, 1]
101```
102
103## CLI
104
105```sh
106ts-css <command> <file>
107```
108
109| Command | Output |
110| --------------- | ---------------------------------------------------- |
111| `minify <file>` | Minified CSS on stdout. |
112| `parse <file>` | The parsed AST as JSON on stdout. |
113| `format <file>` | Round-trip through parser/generator (deterministic). |
114| `version` | Print the installed CLI version. |
115
116### Examples
117
118```sh
119# Minify a stylesheet, write to disk
120ts-css minify src/app.css > dist/app.css
121
122# Inspect the AST of a snippet
123echo '.a { color: red }' > /tmp/snip.css
124ts-css parse /tmp/snip.css | jq '.children[0]'
125
126# Format-only round trip
127ts-css format src/app.css
128```
129
130### Flags
131
132`ts-css minify`:
133
134| Flag | Description |
135| ---------------- | ------------------------------------ |
136| `--no-comments` | Strip `/*!*/` comments too. |
137| `--block` | Treat input as a `style="…"` body. |
138
139`ts-css parse`:
140
141| Flag | Description |
142| -------------- | --------------------------------------------------- |
143| `--positions` | Track source-location info on every AST node. |
144
145## Configuration
146
147You can drop a `ts-css.config.ts` at the project root for defaults:
148
149```ts
150// ts-css.config.ts
151import type { CSSOptions } from '@stacksjs/ts-css'
152
153const config: CSSOptions = {
154 floatPrecision: 3,
155 verbose: false,
156}
157
158export default config
159```
160
161`ts-css` loads it via [`bunfig`](https://github.com/stacksjs/bunfig) — the
162same loader used across the Stacks toolchain.
packages/toolkit/package.jsonadded+123-0
Changes to packages/toolkit/package.json
@@ -0,0 +1,123 @@
1{
2 "name": "@stacksjs/ts-css",
3 "type": "module",
4 "version": "0.1.4",
5 "description": "Pure-TypeScript CSS toolkit for Bun/Node \u2014 parser, walker, generator, selector engine, and minifier. Zero runtime deps.",
6 "author": "Chris Breuer <chris@stacksjs.org>",
7 "license": "MIT",
8 "homepage": "https://github.com/cwcss/crosswind/tree/main/packages/toolkit#readme",
9 "repository": {
10 "type": "git",
11 "url": "git+https://github.com/cwcss/crosswind.git",
12 "directory": "packages/toolkit"
13 },
14 "bugs": {
15 "url": "https://github.com/cwcss/crosswind/issues"
16 },
17 "keywords": [
18 "typescript",
19 "bun",
20 "css",
21 "parser",
22 "minifier",
23 "selector",
24 "ast",
25 "walker",
26 "generator",
27 "csso",
28 "css-tree",
29 "css-select",
30 "css-what"
31 ],
32 "exports": {
33 ".": {
34 "types": "./dist/index.d.ts",
35 "import": "./dist/index.js"
36 },
37 "./parse": {
38 "types": "./dist/parse/index.d.ts",
39 "import": "./dist/parse/index.js"
40 },
41 "./what": {
42 "types": "./dist/what/index.d.ts",
43 "import": "./dist/what/index.js"
44 },
45 "./select": {
46 "types": "./dist/select/index.d.ts",
47 "import": "./dist/select/index.js"
48 },
49 "./optimize": {
50 "types": "./dist/optimize/index.d.ts",
51 "import": "./dist/optimize/index.js"
52 },
53 "./*": {
54 "import": "./dist/*"
55 }
56 },
57 "module": "./dist/index.js",
58 "types": "./dist/index.d.ts",
59 "bin": {
60 "ts-css": "./dist/bin/cli.js"
61 },
62 "files": [
63 "README.md",
64 "dist"
65 ],
66 "scripts": {
67 "build": "bun --bun build.ts && bun run compile",
68 "compile": "bun build ./bin/cli.ts --compile --minify --outfile bin/ts-css",
69 "compile:all": "bun run compile:linux-x64 && bun run compile:linux-arm64 && bun run compile:windows-x64 && bun run compile:darwin-x64 && bun run compile:darwin-arm64",
70 "compile:linux-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-linux-x64 --outfile bin/ts-css-linux-x64",
71 "compile:linux-arm64": "bun build ./bin/cli.ts --compile --minify --target=bun-linux-arm64 --outfile bin/ts-css-linux-arm64",
72 "compile:windows-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-windows-x64 --outfile bin/ts-css-windows-x64.exe",
73 "compile:darwin-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-x64 --outfile bin/ts-css-darwin-x64",
74 "compile:darwin-arm64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-arm64 --outfile bin/ts-css-darwin-arm64",
75 "zip": "bun run zip:all",
76 "zip:all": "bun run zip:linux-x64 && bun run zip:linux-arm64 && bun run zip:windows-x64 && bun run zip:darwin-x64 && bun run zip:darwin-arm64",
77 "zip:linux-x64": "zip -j bin/ts-css-linux-x64.zip bin/ts-css-linux-x64",
78 "zip:linux-arm64": "zip -j bin/ts-css-linux-arm64.zip bin/ts-css-linux-arm64",
79 "zip:windows-x64": "zip -j bin/ts-css-windows-x64.zip bin/ts-css-windows-x64.exe",
80 "zip:darwin-x64": "zip -j bin/ts-css-darwin-x64.zip bin/ts-css-darwin-x64",
81 "zip:darwin-arm64": "zip -j bin/ts-css-darwin-arm64.zip bin/ts-css-darwin-arm64",
82 "fresh": "bunx rimraf node_modules/ bun.lock && bun i",
83 "prepublishOnly": "bun --bun run build && bun run compile:all && bun run zip",
84 "test": "bun test",
85 "bench": "bun bench/index.bench.ts",
86 "lint": "bunx --bun pickier .",
87 "lint:fix": "bunx --bun pickier . --fix",
88 "changelog": "bunx --bun logsmith --verbose",
89 "changelog:generate": "bunx --bun logsmith --output CHANGELOG.md",
90 "release": "bunx --bun bumpx prompt --recursive",
91 "release:patch": "bunx --bun bumpx patch --yes --recursive",
92 "release:minor": "bunx --bun bumpx minor --yes --recursive",
93 "dev:docs": "bun --bun bunpress dev docs",
94 "build:docs": "bun --bun bunpress build docs",
95 "preview:docs": "bun --bun bunpress preview docs",
96 "typecheck": "bun --bun tsc --noEmit",
97 "format": "bunx --bun pickier . --format",
98 "format:fix": "bunx --bun pickier . --format --write"
99 },
100 "dependencies": {
101 "bunfig": "^0.15.6"
102 },
103 "devDependencies": {
104 "@types/bun": "latest",
105 "@types/css-tree": "^2.3.11",
106 "@types/csso": "^5.0.4",
107 "better-dx": "^0.2.15",
108 "css-select": "^7.0.0",
109 "css-tree": "^3.2.1",
110 "css-what": "^8.0.0",
111 "csso": "^5.0.5",
112 "mitata": "^1.0.34"
113 },
114 "git-hooks": {
115 "pre-commit": {
116 "staged-lint": {
117 "*.{js,ts,json,yaml,yml,md}": "bunx --bun pickier lint --fix"
118 },
119 "autoRestage": true
120 },
121 "commit-msg": "bunx gitlint --edit .git/COMMIT_EDITMSG"
122 }
123}
packages/toolkit/pantry.lockadded+15-0
Changes to packages/toolkit/pantry.lock
@@ -0,0 +1,15 @@
1{
2 "version": "1.0",
3 "packages": {
4 "bun.sh@0.0.0": {
5 "name": "bun.sh",
6 "version": "0.0.0",
7 "resolved": "registry:bun.sh@0.0.0"
8 },
9 "bun.sh@1.3.9": {
10 "name": "bun.sh",
11 "version": "1.3.9",
12 "resolved": "registry:bun.sh@1.3.9"
13 }
14 }
15}
packages/toolkit/src/config.tsadded+21-0
Changes to packages/toolkit/src/config.ts
@@ -0,0 +1,21 @@
1import type { CSSConfig } from './types'
2import { loadConfig } from 'bunfig'
3
4export const defaultConfig: CSSConfig = {
5 floatPrecision: 3,
6 verbose: false,
7}
8
9let _config: CSSConfig | null = null
10
11export async function getConfig(): Promise<CSSConfig> {
12 if (!_config) {
13 _config = await loadConfig({
14 name: 'ts-css',
15 defaultConfig,
16 })
17 }
18 return _config
19}
20
21export const config: CSSConfig = defaultConfig
packages/toolkit/src/index.tsadded+29-0
Changes to packages/toolkit/src/index.ts
@@ -0,0 +1,29 @@
1/**
2 * @stacksjs/ts-css — pure-TypeScript CSS toolkit.
3 *
4 * Top-level surface re-exports the four sub-modules:
5 * - parse — CSS parser/walker/generator (replaces css-tree)
6 * - what — selector parser (replaces css-what)
7 * - select — selector matching (replaces css-select)
8 * - optimize — CSS minifier + specificity (replaces csso)
9 */
10
11export * from './config'
12
13// Sub-module namespaces — `import * as csstree from '@stacksjs/ts-css'` etc.
14export * as csstree from './parse'
15export * as cssWhat from './what'
16export * as cssSelect from './select'
17export * as csso from './optimize'
18
19// Top-level convenience re-exports
20export { clone, generate, List, parse, walk } from './parse'
21export type { CssNode, CssNodeType, ListItem, ParseOptions } from './parse'
22export { is, selectAll, selectOne } from './select'
23export type { Adapter, CompiledQuery, Options } from './select'
24export { isTraversal, parse as parseSelector, stringify as stringifySelector } from './what'
25export type { Selector, SelectorType } from './what'
26export { minify, minifyBlock, specificity, syntax } from './optimize'
27export type { MinifyOptions, MinifyResult, Specificity } from './optimize'
28
29export * from './types'
packages/toolkit/src/optimize/clean/comment.tsadded+24-0
Changes to packages/toolkit/src/optimize/clean/comment.ts
@@ -0,0 +1,24 @@
1/**
2 * Strip CSS comments (except `/*!*\/` license-style comments by default).
3 */
4
5import type { CssNode } from '../../parse'
6import { walk } from '../../parse'
7
8export function removeComments(ast: CssNode, options: { exclamation?: boolean | 'first-exclamation' } = {}): void {
9 const exclamation = options.exclamation ?? false
10 let firstSeen = false
11 walk(ast, (node, item, list) => {
12 if (node.type !== 'Comment')
13 return
14 const isExclamation = node.value.startsWith('!')
15 if (exclamation === true && isExclamation)
16 return
17 if (exclamation === 'first-exclamation' && isExclamation && !firstSeen) {
18 firstSeen = true
19 return
20 }
21 if (item && list)
22 list.remove(item)
23 })
24}
packages/toolkit/src/optimize/clean/declaration.tsadded+61-0
Changes to packages/toolkit/src/optimize/clean/declaration.ts
@@ -0,0 +1,61 @@
1/**
2 * Within a declaration list, drop duplicates earlier in the list when the
3 * later occurrence has equal or higher importance. Preserves the LAST
4 * occurrence per property.
5 */
6
7import type { CssNode, Declaration } from '../../parse'
8import { walk } from '../../parse'
9
10export function dedupeDeclarations(ast: CssNode): void {
11 walk(ast, (node) => {
12 if (node.type !== 'Block' && node.type !== 'DeclarationList')
13 return
14 if (!('children' in node) || !node.children)
15 return
16 const list = node.children as any
17 // Quick check: at least one Declaration AND a possibly-duplicating
18 // sibling. Single-decl (or no-decl) blocks are the common case for
19 // nested rules and skipping the Map allocation here is measurable.
20 let head = list.head
21 if (head == null || head.next == null)
22 return
23 // Pessimistic-but-cheap pre-scan that also confirms ≥2 declarations.
24 let declCount = 0
25 for (let it = head; it != null; it = it.next) {
26 if ((it.data as CssNode).type === 'Declaration') {
27 declCount++
28 if (declCount >= 2)
29 break
30 }
31 }
32 if (declCount < 2)
33 return
34
35 const seen = new Map<string, { item: any, decl: Declaration }>()
36 const toRemove: any[] = []
37 for (let item = head; item != null; item = item.next) {
38 const data = item.data as CssNode
39 if (data.type !== 'Declaration')
40 continue
41 const key = data.property
42 const prev = seen.get(key)
43 if (prev) {
44 const prevImp = !!prev.decl.important
45 const curImp = !!data.important
46 if (curImp || prevImp === curImp) {
47 toRemove.push(prev.item)
48 seen.set(key, { item, decl: data })
49 }
50 else {
51 toRemove.push(item)
52 }
53 }
54 else {
55 seen.set(key, { item, decl: data })
56 }
57 }
58 for (const it of toRemove)
59 list.remove(it)
60 })
61}
packages/toolkit/src/optimize/clean/index.tsadded+2-0
Changes to packages/toolkit/src/optimize/clean/index.ts
@@ -0,0 +1,2 @@
1export { removeComments } from './comment'
2export { dedupeDeclarations } from './declaration'
packages/toolkit/src/optimize/compress/color.tsadded+94-0
Changes to packages/toolkit/src/optimize/compress/color.ts
@@ -0,0 +1,94 @@
1/**
2 * Color shorthand compressor.
3 * - `#aabbcc` → `#abc` when each pair is a doubled hex digit
4 * - `rgb(255, 0, 0)` → `red`
5 * - `rgb(0, 0, 0)` → `#000` (handled by named-name table later)
6 *
7 * Lookup tables here are conservative — only the substitutions that
8 * unambiguously shorten the bytes.
9 */
10
11const NAMED_TO_HEX: Record<string, string> = {
12 black: '#000',
13 fuchsia: '#f0f',
14 white: '#fff',
15 red: '#f00',
16 cyan: '#0ff',
17 blue: '#00f',
18 yellow: '#ff0',
19 magenta: '#f0f',
20 lime: '#0f0',
21 silver: '#c0c0c0',
22 gray: '#808080',
23 maroon: '#800000',
24 olive: '#808000',
25 green: '#008000',
26 purple: '#800080',
27 teal: '#008080',
28 navy: '#000080',
29}
30
31const HEX_TO_SHORT_NAME: Record<string, string> = {
32 '#f00': 'red',
33 '#ff0000': 'red',
34 '#000080': 'navy',
35 '#008080': 'teal',
36}
37
38export function shortenHex(hex: string): string {
39 if (!hex.startsWith('#'))
40 return hex
41 const body = hex.slice(1)
42 if (body.length === 6) {
43 if (body[0] === body[1] && body[2] === body[3] && body[4] === body[5])
44 return `#${body[0]}${body[2]}${body[4]}`
45 }
46 if (body.length === 8) {
47 if (body[0] === body[1] && body[2] === body[3] && body[4] === body[5] && body[6] === body[7])
48 return `#${body[0]}${body[2]}${body[4]}${body[6]}`
49 }
50 return hex
51}
52
53export function colorNameToHex(name: string): string | null {
54 const lower = name.toLowerCase()
55 return NAMED_TO_HEX[lower] ?? null
56}
57
58export function hexToShortName(hex: string): string | null {
59 const lower = hex.toLowerCase()
60 return HEX_TO_SHORT_NAME[lower] ?? null
61}
62
63export function rgbToHex(r: number, g: number, b: number): string {
64 const hex = ((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')
65 return shortenHex(`#${hex}`)
66}
67
68const RGB_RE = /^rgba?\(\s*([+-]?\d*\.?\d+%?)\s*[,\s]\s*([+-]?\d*\.?\d+%?)\s*[,\s]\s*([+-]?\d*\.?\d+%?)\s*(?:[,/]\s*([+-]?\d*\.?\d+%?)\s*)?\)$/
69
70export function compressRgbToHex(value: string): string {
71 const m = RGB_RE.exec(value.trim())
72 if (!m)
73 return value
74 const r = parseChannel(m[1]!)
75 const g = parseChannel(m[2]!)
76 const b = parseChannel(m[3]!)
77 if (m[4] !== undefined && !isAlphaOne(m[4])) {
78 // alpha != 1 — keep as rgba
79 return value
80 }
81 return rgbToHex(r, g, b)
82}
83
84function isAlphaOne(s: string): boolean {
85 if (s.endsWith('%'))
86 return Number.parseFloat(s) >= 100
87 return Number.parseFloat(s) >= 1
88}
89
90function parseChannel(s: string): number {
91 if (s.endsWith('%'))
92 return Math.max(0, Math.min(255, Math.round(Number.parseFloat(s) * 2.55)))
93 return Math.max(0, Math.min(255, Math.round(Number.parseFloat(s))))
94}
packages/toolkit/src/optimize/compress/index.tsadded+189-0
Changes to packages/toolkit/src/optimize/compress/index.ts
@@ -0,0 +1,189 @@
1/**
2 * Run all compressors over the AST in place.
3 *
4 * Identifier ↔ Hash conversions happen only when the surrounding
5 * declaration's property is known to accept a color (so `font-family: red`
6 * isn't treated as a color, and `color: red` is). The list+item args
7 * from the walker callback let us swap nodes in place without rebuilding
8 * the parent list.
9 */
10
11import type { CssNode, HashNode, Identifier } from '../../parse'
12import { generate, walk } from '../../parse'
13import { colorNameToHex, compressRgbToHex, hexToShortName, shortenHex } from './color'
14import { compressDimension, compressNumber, compressPercentage, roundNumberString } from './number'
15
16export interface CompressOptions {
17 /** Round numeric values to this many decimal places. `null` = no rounding. */
18 floatPrecision?: number | null
19}
20
21const COLOR_PROPERTIES: ReadonlySet<string> = new Set([
22 'color',
23 'background',
24 'background-color',
25 'border',
26 'border-color',
27 'border-top',
28 'border-top-color',
29 'border-right',
30 'border-right-color',
31 'border-bottom',
32 'border-bottom-color',
33 'border-left',
34 'border-left-color',
35 'border-block',
36 'border-block-color',
37 'border-block-start-color',
38 'border-block-end-color',
39 'border-inline',
40 'border-inline-color',
41 'border-inline-start-color',
42 'border-inline-end-color',
43 'outline',
44 'outline-color',
45 'caret-color',
46 'fill',
47 'stroke',
48 'flood-color',
49 'lighting-color',
50 'stop-color',
51 'column-rule',
52 'column-rule-color',
53 'text-decoration',
54 'text-decoration-color',
55 'text-emphasis',
56 'text-emphasis-color',
57 'text-shadow',
58 'box-shadow',
59 'accent-color',
60 'scrollbar-color',
61])
62
63/**
64 * Drop redundant WhiteSpace nodes from a `Parentheses` / `Function` /
65 * `Value` container — adjacent to `:` `,` `/` operators and at the
66 * boundaries. Run inline as a `leave` callback so the compressor only
67 * traverses the AST once instead of twice.
68 *
69 * Whitespace around `+` and `-` is preserved: in `calc()` (and friends)
70 * the spec requires whitespace on both sides of those operators, and a
71 * compactor that strips it produces unparseable output (e.g.
72 * `calc(100%-1rem)` parses as two adjacent dimensions, not a subtraction).
73 */
74function isCompactableOperator(node: any): boolean {
75 if (!node || node.type !== 'Operator')
76 return false
77 const v = node.value
78 return v === ':' || v === ',' || v === '/'
79}
80
81function compactContainerWhitespace(node: { children: any }): void {
82 const list = node.children as any
83 if (!list)
84 return
85 // 1. trim leading
86 while (list.head && list.head.data.type === 'WhiteSpace')
87 list.remove(list.head)
88 // 2. trim trailing
89 while (list.tail && list.tail.data.type === 'WhiteSpace')
90 list.remove(list.tail)
91 // 3. drop WhiteSpace adjacent to `: , /` operators only.
92 let cur = list.head
93 while (cur) {
94 const nxt = cur.next
95 if (cur.data.type === 'WhiteSpace') {
96 const prevIsOp = cur.prev && isCompactableOperator(cur.prev.data)
97 const nextIsOp = nxt && isCompactableOperator(nxt.data)
98 if (prevIsOp || nextIsOp)
99 list.remove(cur)
100 }
101 cur = nxt
102 }
103}
104
105export function compressTree(ast: CssNode, options: CompressOptions = {}): void {
106 const fp = options.floatPrecision ?? null
107 const round = fp === null ? null : (s: string) => roundNumberString(s, fp)
108 walk(ast, {
109 enter(node, item, list) {
110 switch (node.type) {
111 case 'Number': {
112 const v = round ? round(node.value) : node.value
113 node.value = compressNumber(v)
114 return
115 }
116 case 'Percentage': {
117 const v = round ? round(node.value) : node.value
118 node.value = compressPercentage(v)
119 return
120 }
121 case 'Dimension': {
122 const v = round ? round(node.value) : node.value
123 const c = compressDimension(v, node.unit)
124 node.value = c.value
125 node.unit = c.unit
126 return
127 }
128 case 'Hash': {
129 const newName = shortenHex(`#${node.name}`).slice(1)
130 node.name = newName
131 if (item && list && this.declaration && COLOR_PROPERTIES.has(this.declaration.property.toLowerCase())) {
132 const short = hexToShortName(`#${newName}`)
133 if (short && short.length < newName.length + 1) {
134 const ident: Identifier = { type: 'Identifier', name: short, loc: node.loc }
135 list.replace(item, list.createItem(ident))
136 }
137 }
138 return
139 }
140 case 'Identifier': {
141 if (!item || !list || !this.declaration)
142 return
143 if (!COLOR_PROPERTIES.has(this.declaration.property.toLowerCase()))
144 return
145 const hex = colorNameToHex(node.name)
146 if (hex && hex.length < node.name.length) {
147 const hashNode: HashNode = { type: 'Hash', name: hex.slice(1), loc: node.loc }
148 list.replace(item, list.createItem(hashNode))
149 }
150 return
151 }
152 case 'Url':
153 // Generator decides whether to quote based on the value's chars
154 // (`/[\s"'()\\]/`). Storing a normalized inner string here is a
155 // no-op pass — the previous double-trip through `compressUrl`
156 // followed by `.replace(/^"|"$/g, '')` was equivalent to leaving
157 // the value alone. Kept the case branch for future shrinkers
158 // (e.g. dropping a leading `./`).
159 return
160 case 'Function': {
161 // `rgb(...)` / `rgba(...)` → `#rrggbb` (or `#rgb`) when the alpha
162 // is opaque. We let the walker process the children first (so any
163 // contained Number/Percentage was already shortened) and then
164 // replace the whole Function with a Hash on `leave`. Mark the
165 // node so the leave handler knows to do the substitution.
166 const lname = node.name.toLowerCase()
167 if ((lname === 'rgb' || lname === 'rgba') && item && list)
168 (node as any).__rgbReplaceCandidate = true
169 }
170 }
171 },
172 leave(node, item, list) {
173 if (node.type === 'Parentheses' || node.type === 'Function' || node.type === 'Value')
174 compactContainerWhitespace(node as any)
175 if (node.type === 'Function' && (node as any).__rgbReplaceCandidate && item && list) {
176 // Re-stringify the function with its (now-compressed) children and
177 // run the rgb→hex compressor. If the result is a `#rrggbb` token,
178 // splice it in as a Hash so further passes can shorten it more
179 // and the property-aware Identifier swap can apply.
180 const text = generate(node as CssNode)
181 const replacement = compressRgbToHex(text)
182 if (replacement !== text && replacement.startsWith('#')) {
183 const hashNode: HashNode = { type: 'Hash', name: replacement.slice(1), loc: node.loc }
184 list.replace(item, list.createItem(hashNode))
185 }
186 }
187 },
188 })
189}
packages/toolkit/src/optimize/compress/number.tsadded+84-0
Changes to packages/toolkit/src/optimize/compress/number.ts
@@ -0,0 +1,84 @@
1/**
2 * Number / dimension / percentage compressors.
3 *
4 * - `0.5px` → `.5px`
5 * - `0.5em` → `.5em`
6 * - `0px` / `0%` / `0em` (etc.) → `0` in length context
7 * - trailing zeros removed: `2.5000` → `2.5`
8 * - exponential / scientific stays as-is — already shortest form
9 */
10
11// Length units only — angle/time/frequency/resolution units are NOT here:
12// `0deg`, `0s`, `0Hz`, `0dpi` are NOT equivalent to `0` in CSS, and stripping
13// the unit there breaks the value type. Modern viewport variants (`svh`,
14// `lvh`, `dvh`, `svw`, `lvw`, `dvw`, `svi`, `svb`, …) and container-query
15// units (`cqw`, `cqh`, `cqi`, `cqb`, `cqmin`, `cqmax`) are length units too.
16const ZERO_UNITS = new Set([
17 // absolute / classic
18 'px', 'pt', 'pc', 'in', 'cm', 'mm', 'q',
19 // font-relative
20 'em', 'rem', 'ex', 'ch', 'cap', 'ic', 'lh', 'rlh',
21 // viewport-relative
22 'vw', 'vh', 'vi', 'vb', 'vmin', 'vmax',
23 // small/large/dynamic viewport
24 'svw', 'svh', 'svi', 'svb', 'svmin', 'svmax',
25 'lvw', 'lvh', 'lvi', 'lvb', 'lvmin', 'lvmax',
26 'dvw', 'dvh', 'dvi', 'dvb', 'dvmin', 'dvmax',
27 // container-query units
28 'cqw', 'cqh', 'cqi', 'cqb', 'cqmin', 'cqmax',
29])
30
31export function compressNumber(value: string): string {
32 // Drop a leading `+` sign (CSS numbers can't be `+`-prefixed except
33 // immediately after an operator; the parser already handles that).
34 if (value.charCodeAt(0) === 43 /* + */)
35 value = value.slice(1)
36
37 // strip trailing zeros after decimal point: `2.5000` → `2.5`,
38 // `2.500e5` → `2.5e5`. The `($|e|E)` boundary keeps an exponent intact.
39 if (value.includes('.')) {
40 let v = value.replace(/(\.\d*?)0+($|[eE])/, '$1$2')
41 // bare `.` left over — strip it (`5.` → `5`, `5.e2` → `5e2`)
42 v = v.replace(/\.($|[eE])/, '$1')
43 value = v
44 }
45
46 // 0.5 → .5 / -0.5 → -.5
47 if (value.startsWith('0.') && value.length > 2)
48 value = value.slice(1)
49 else if (value.startsWith('-0.') && value.length > 3)
50 value = `-${value.slice(2)}`
51
52 // Negative zero `-0` (and `-0.0` which has already collapsed) is just 0.
53 if (value === '-0' || value === '-.0' || value === '-0.0')
54 value = '0'
55
56 return value
57}
58
59export function compressDimension(value: string, unit: string): { value: string, unit: string } {
60 const compressed = compressNumber(value)
61 if ((compressed === '0' || compressed === '-0') && ZERO_UNITS.has(unit.toLowerCase()))
62 return { value: '0', unit: '' }
63 return { value: compressed, unit }
64}
65
66export function compressPercentage(value: string): string {
67 const compressed = compressNumber(value)
68 return compressed
69}
70
71/**
72 * Round a numeric string to `precision` decimal places. Returns the input
73 * unchanged when it's not a finite number — calling sites should still
74 * follow up with `compressNumber` to strip leading zeros etc.
75 */
76export function roundNumberString(value: string, precision: number): string {
77 const n = Number.parseFloat(value)
78 if (!Number.isFinite(n))
79 return value
80 // toFixed with negative precision is invalid in older runtimes — clamp.
81 const p = precision < 0 ? 0 : precision
82 // toFixed rounds; trim trailing zeros and stray dot.
83 return n.toFixed(p)
84}
packages/toolkit/src/optimize/compress/string.tsadded+23-0
Changes to packages/toolkit/src/optimize/compress/string.ts
@@ -0,0 +1,23 @@
1/**
2 * String compressor — picks the quote style with fewer escapes.
3 *
4 * `String.split(q).length - 1` is the conventional "count occurrences"
5 * idiom but it allocates the intermediate array; for typical CSS strings
6 * the value is short, but a tight indexOf-based count is cheaper and
7 * doesn't churn the GC when the same string flows through repeatedly.
8 */
9
10// eslint-disable-next-line pickier/no-unused-vars
11export function compressString(value: string): string {
12 let dq = 0
13 let sq = 0
14 for (let i = 0; i < value.length; i++) {
15 const c = value.charCodeAt(i)
16 if (c === 34 /* " */) dq++
17 else if (c === 39 /* ' */) sq++
18 }
19 if (dq <= sq) {
20 return `"${value.replace(/"/g, '\\"')}"`
21 }
22 return `'${value.replace(/'/g, '\\\'')}'`
23}
packages/toolkit/src/optimize/compress/url.tsadded+20-0
Changes to packages/toolkit/src/optimize/compress/url.ts
@@ -0,0 +1,20 @@
1/**
2 * URL compressor: drop quotes when the inner string is "URL-safe" (no
3 * whitespace, no parens, no quotes, no backslash). Manual char scan is a
4 * smidge faster than `RegExp.test` and avoids the per-call regex
5 * lastIndex bookkeeping the runtime does for `g`-style patterns.
6 */
7
8// eslint-disable-next-line pickier/no-unused-vars
9export function compressUrl(value: string): string {
10 for (let i = 0; i < value.length; i++) {
11 const c = value.charCodeAt(i)
12 if (
13 c === 34 /* " */ || c === 39 /* ' */ || c === 40 /* ( */ || c === 41 /* ) */
14 || c === 92 /* \ */ || c === 32 || c === 9 || c === 10 || c === 12 || c === 13
15 ) {
16 return `"${value.replace(/"/g, '\\"')}"`
17 }
18 }
19 return value
20}
packages/toolkit/src/optimize/index.tsadded+18-0
Changes to packages/toolkit/src/optimize/index.ts
@@ -0,0 +1,18 @@
1/**
2 * CSS minifier — the API surface that csso users (notably SVGO's
3 * `minifyStyles` and `inlineStyles`) depend on.
4 */
5
6export { dedupeDeclarations, removeComments } from './clean'
7export { compressTree } from './compress'
8export { minify, minifyBlock } from './minify'
9export type { MinifyOptions, MinifyResult } from './minify'
10export { specificity, specificityToString } from './specificity'
11export type { Specificity } from './specificity'
12
13import { specificity as _specificity } from './specificity'
14
15/** Mirror csso's `syntax` namespace export. */
16export const syntax: { specificity: typeof _specificity } = {
17 specificity: _specificity,
18}
packages/toolkit/src/optimize/minify.tsadded+67-0
Changes to packages/toolkit/src/optimize/minify.ts
@@ -0,0 +1,67 @@
1/**
2 * Public minifier entry. Accepts a CSS string, returns
3 * `{ css, ast }` matching csso's `minify` shape (the `map` field for
4 * source maps is intentionally omitted — we don't generate source maps).
5 */
6
7import type { CssNode, ParseOptions } from '../parse'
8import { generate, parse } from '../parse'
9import { removeComments } from './clean/comment'
10import { dedupeDeclarations } from './clean/declaration'
11import { compressTree } from './compress'
12
13export interface MinifyOptions {
14 /** When false, comments starting with `!` are preserved. */
15 comments?: boolean | 'exclamation' | 'first-exclamation'
16 /**
17 * Maximum decimal places to keep on numeric values (rounded). Omit or
18 * set to `null` to keep input precision. Matches csso's option name.
19 */
20 floatPrecision?: number | null
21 /** Restructure rules across the document — currently a no-op (we
22 * keep declarations stable; csso's restructuring is the bulk of its
23 * code and gives marginal real-world benefit beyond gzip). */
24 restructure?: boolean
25 /** Merge `@media` rules with the same query. */
26 forceMediaMerge?: boolean
27 /** Usage hints for selector pruning (matches csso shape). */
28 usage?: { tags?: string[], ids?: string[], classes?: string[], force?: boolean } | null
29}
30
31export interface MinifyResult {
32 css: string
33 ast: CssNode
34}
35
36export function minify(source: string, options: MinifyOptions = {}): MinifyResult {
37 const ast = parse(source, { context: 'stylesheet' } as ParseOptions)
38 return runPipeline(ast, options)
39}
40
41export function minifyBlock(source: string, options: MinifyOptions = {}): MinifyResult {
42 const ast = parse(source, { context: 'declarationList' } as ParseOptions)
43 return runPipeline(ast, options)
44}
45
46function runPipeline(ast: CssNode, options: MinifyOptions): MinifyResult {
47 // 1. comment cleanup — default preserves `/*!*\/` comments (csso behaviour)
48 if (options.comments === false) {
49 removeComments(ast, { exclamation: false })
50 }
51 else if (options.comments === 'first-exclamation') {
52 removeComments(ast, { exclamation: 'first-exclamation' })
53 }
54 else {
55 // undefined | true | 'exclamation' → preserve `!` comments
56 removeComments(ast, { exclamation: true })
57 }
58
59 // 2. value-level compression (numbers, dimensions, colors, urls)
60 compressTree(ast, { floatPrecision: options.floatPrecision ?? null })
61
62 // 3. declaration dedup
63 dedupeDeclarations(ast)
64
65 // 4. stringify
66 return { css: generate(ast), ast }
67}
packages/toolkit/src/optimize/specificity.tsadded+181-0
Changes to packages/toolkit/src/optimize/specificity.ts
@@ -0,0 +1,181 @@
1/**
2 * Compute CSS selector specificity per https://www.w3.org/TR/selectors-4/#specificity-rules.
3 *
4 * Returns an `[a, b, c]` tuple where:
5 * a = id selectors
6 * b = class / attribute / pseudo-class selectors
7 * c = type / pseudo-element selectors
8 *
9 * Accepts either:
10 * - a css-tree-style `Selector` / `SelectorList` AST node (csso shape), OR
11 * - a `Selector[][]` returned by `cssWhat.parse(string)` — so callers
12 * can pipe `cssWhat.parse(...)` straight into `syntax.specificity(...)`.
13 */
14
15import type { CssNode } from '../parse'
16import type { Selector as WhatSelector } from '../what'
17import { parse as parseSelector } from '../what'
18
19export type Specificity = [number, number, number]
20
21export function specificity(input: CssNode | WhatSelector[][] | WhatSelector[] | string): Specificity {
22 const result: Specificity = [0, 0, 0]
23
24 if (typeof input === 'string') {
25 const ast = parseSelector(input)
26 visitWhat(ast, result)
27 return result
28 }
29
30 if (Array.isArray(input)) {
31 if (input.length === 0)
32 return result
33 // distinguish Selector[][] from Selector[]
34 if (Array.isArray(input[0]))
35 visitWhat(input as WhatSelector[][], result)
36 else
37 visitWhatGroup(input as WhatSelector[], result)
38 return result
39 }
40
41 visit(input, result)
42 return result
43}
44
45// ---- css-tree shape ----
46
47function visit(node: CssNode, result: Specificity): void {
48 switch (node.type) {
49 case 'IdSelector':
50 result[0]++
51 return
52 case 'ClassSelector':
53 case 'AttributeSelector':
54 result[1]++
55 return
56 case 'TypeSelector':
57 if (node.name !== '*')
58 result[2]++
59 return
60 case 'PseudoClassSelector': {
61 const name = node.name.toLowerCase()
62 if (name === 'is' || name === 'matches' || name === '-moz-any' || name === '-webkit-any' || name === 'not' || name === 'has') {
63 if (node.children) {
64 const selectors: CssNode[] = []
65 for (const inner of node.children as Iterable<CssNode>) {
66 if (inner.type === 'SelectorList' && 'children' in inner && inner.children) {
67 for (const s of inner.children as Iterable<CssNode>)
68 selectors.push(s)
69 }
70 else if (inner.type === 'Selector') {
71 selectors.push(inner)
72 }
73 }
74 let max: Specificity = [0, 0, 0]
75 for (const sel of selectors) {
76 const s = specificity(sel)
77 if (compareSpec(s, max) > 0)
78 max = s
79 }
80 result[0] += max[0]
81 result[1] += max[1]
82 result[2] += max[2]
83 }
84 return
85 }
86 if (name === 'where')
87 return
88 result[1]++
89 return
90 }
91 case 'PseudoElementSelector':
92 result[2]++
93 return
94 case 'Selector':
95 case 'SelectorList':
96 case 'AtrulePrelude':
97 if ('children' in node && node.children) {
98 for (const child of node.children as Iterable<CssNode>)
99 visit(child, result)
100 }
101 return
102 case 'NestingSelector':
103 return
104 }
105}
106
107// ---- css-what shape (Selector[][] or Selector[]) ----
108//
109// css-what's `Selector` is one of:
110// { type: 'tag' | 'universal' | 'attribute' | 'pseudo' | 'pseudo-element'
111// | 'descendant' | 'child' | 'parent' | 'sibling' | 'adjacent' | 'column-combinator',
112// … }
113//
114// For specificity, *only* attribute/pseudo/tag matter — combinators are
115// ignored. `:is`/`:not`/`:has`/`:matches` take the max of their args.
116
117function visitWhat(groups: WhatSelector[][], result: Specificity): void {
118 // Each top-level group is one comma-separated selector. css-tree-style
119 // specificity is computed per-selector; for SelectorList (`a, b`) we
120 // sum across all groups (matches csso behaviour).
121 for (const g of groups)
122 visitWhatGroup(g, result)
123}
124
125function visitWhatGroup(tokens: WhatSelector[], result: Specificity): void {
126 for (const t of tokens) {
127 switch (t.type) {
128 case 'tag':
129 if (t.name !== '*')
130 result[2]++
131 break
132 case 'attribute':
133 // `.foo` and `#bar` are emitted as attribute selectors with name
134 // 'class'/'id' — count them like ClassSelector / IdSelector.
135 if (t.name === 'id' && t.action === 'equals')
136 result[0]++
137 else
138 result[1]++
139 break
140 case 'pseudo': {
141 const name = t.name.toLowerCase()
142 if (name === 'is' || name === 'matches' || name === 'not' || name === 'has' || name === '-moz-any' || name === '-webkit-any') {
143 // data is Selector[][] when parsed by css-what's selector-list pseudos
144 if (Array.isArray(t.data)) {
145 let max: Specificity = [0, 0, 0]
146 for (const inner of t.data as WhatSelector[][]) {
147 const s: Specificity = [0, 0, 0]
148 visitWhatGroup(inner, s)
149 if (compareSpec(s, max) > 0)
150 max = s
151 }
152 result[0] += max[0]
153 result[1] += max[1]
154 result[2] += max[2]
155 }
156 break
157 }
158 if (name === 'where')
159 break
160 result[1]++
161 break
162 }
163 case 'pseudo-element':
164 result[2]++
165 break
166 // combinators don't contribute to specificity
167 }
168 }
169}
170
171function compareSpec(a: Specificity, b: Specificity): number {
172 for (let i = 0; i < 3; i++) {
173 if (a[i] !== b[i])
174 return a[i]! - b[i]!
175 }
176 return 0
177}
178
179export function specificityToString(s: Specificity): string {
180 return s.join(',')
181}
packages/toolkit/src/parse/clone.tsadded+43-0
Changes to packages/toolkit/src/parse/clone.ts
@@ -0,0 +1,43 @@
1/**
2 * Deep clone a CSS AST node. List children are cloned via list iteration
3 * so the result is structurally independent from the input.
4 */
5
6import type { CssNode } from './types'
7import { CssList } from './list'
8
9export function clone<T extends CssNode>(node: T): T {
10 return cloneAny(node) as T
11}
12
13function cloneAny(value: any): any {
14 if (value === null || typeof value !== 'object')
15 return value
16 if (value instanceof CssList) {
17 const out = new CssList<any>()
18 for (const child of value)
19 out.appendData(cloneAny(child))
20 return out
21 }
22 if (Array.isArray(value))
23 return value.map(cloneAny)
24 // CssLocation and other plain objects; defer detection of AST nodes to
25 // the `'type' in value` check below.
26 // typed AST node
27 if ('type' in value && typeof value.type === 'string') {
28 const out: Record<string, any> = { type: value.type }
29 for (const k of Object.keys(value)) {
30 if (k === 'type')
31 continue
32 out[k] = cloneAny(value[k])
33 }
34 return out
35 }
36 // plain object — including CssLocation which we want to preserve
37 // by-value rather than share by reference (matters when consumers
38 // mutate `loc` after cloning).
39 const out: Record<string, any> = {}
40 for (const k of Object.keys(value))
41 out[k] = cloneAny(value[k])
42 return out
43}
packages/toolkit/src/parse/generator.tsadded+0-0

Binary file. Nothing to show line by line.

packages/toolkit/src/parse/index.tsadded+59-0
Changes to packages/toolkit/src/parse/index.ts
@@ -0,0 +1,59 @@
1/**
2 * Public surface for the CSS parser/generator/walker. Drop-in compatible
3 * with css-tree v3 for the API surface used by csso/SVGO.
4 */
5
6export { clone } from './clone'
7export { generate } from './generator'
8export { CssList as List, makeList } from './list'
9export type { ListItem } from './list'
10export { parse, TokenType } from './parser'
11export type { Token } from './tokenizer'
12export { decodeName, Tokenizer } from './tokenizer'
13export type {
14 Atrule,
15 AtrulePrelude,
16 AttributeSelector,
17 Block,
18 ClassSelector,
19 Combinator,
20 CommentNode,
21 CssLocation,
22 CssNode,
23 CssNodeType,
24 Declaration,
25 DeclarationList,
26 DimensionNode,
27 FunctionNode,
28 HashNode,
29 Identifier,
30 IdSelector,
31 MediaFeatureNode,
32 MediaQueryListNode,
33 MediaQueryNode,
34 NestingSelector,
35 NthNode,
36 NumberNode,
37 OperatorNode,
38 ParenthesesNode,
39 ParseContext,
40 ParseOptions,
41 PercentageNode,
42 PseudoClassSelector,
43 PseudoElementSelector,
44 RatioNode,
45 Raw,
46 RawNode,
47 Rule,
48 Selector,
49 SelectorList,
50 StringNode,
51 StyleSheet,
52 TypeSelector,
53 UnicodeRangeNode,
54 UrlNode,
55 Value,
56 WhiteSpaceNode,
57} from './types'
58export { walk } from './walker'
59export type { WalkContext, WalkVisitor } from './walker'
packages/toolkit/src/parse/list.tsadded+375-0
Changes to packages/toolkit/src/parse/list.ts
@@ -0,0 +1,375 @@
1/**
2 * Doubly linked list used as the children container for every CSS AST node.
3 *
4 * Mirrors css-tree's `List` so `csstree.List` import sites can switch to
5 * `import { CssList as List } from '@stacksjs/ts-css/parse'` without changing
6 * call sites. The list owns its `ListItem<T>` cells; items can be moved
7 * between lists by detach/insert pairs.
8 */
9
10export interface ListItem<T> {
11 prev: ListItem<T> | null
12 next: ListItem<T> | null
13 data: T
14}
15
16function createItem<T>(data: T): ListItem<T> {
17 return { prev: null, next: null, data }
18}
19
20interface ListCursor<T> {
21 prev: ListItem<T> | null
22 next: ListItem<T> | null
23}
24
25export class CssList<T> {
26 head: ListItem<T> | null = null
27 tail: ListItem<T> | null = null
28
29 // ----- mutation cursors -----
30 // Each list owns its own cursor stack. Callbacks may insert/remove items
31 // mid-walk; the topmost active cursor records the in-progress prev/next
32 // pointers so a removal that lands on the cursor's `next` slot can be
33 // repointed onto the survivor. Per-list (not module-global) so nested
34 // walks of the same list don't corrupt each other.
35 private cursors: ListCursor<T>[] = []
36
37 private allocateCursor(prev: ListItem<T> | null, next: ListItem<T> | null): ListCursor<T> {
38 const cursor: ListCursor<T> = { prev, next }
39 this.cursors.push(cursor)
40 return cursor
41 }
42
43 private releaseCursor(): void {
44 this.cursors.pop()
45 }
46
47 private updateCursors(prevOld: ListItem<T> | null, prevNew: ListItem<T> | null, nextOld: ListItem<T> | null, nextNew: ListItem<T> | null): void {
48 for (const c of this.cursors) {
49 if (c.prev === prevOld)
50 c.prev = prevNew
51 if (c.next === nextOld)
52 c.next = nextNew
53 }
54 }
55
56 static createItem<U>(data: U): ListItem<U> {
57 return createItem(data)
58 }
59
60 createItem(data: T): ListItem<T> {
61 return createItem(data)
62 }
63
64 get isEmpty(): boolean {
65 return this.head === null
66 }
67
68 get first(): T | null {
69 return this.head?.data ?? null
70 }
71
72 get last(): T | null {
73 return this.tail?.data ?? null
74 }
75
76 *[Symbol.iterator](): Iterator<T> {
77 for (let cur = this.head; cur != null; cur = cur.next)
78 yield cur.data
79 }
80
81 fromArray(items: ReadonlyArray<T>): this {
82 let prev: ListItem<T> | null = null
83 this.head = null
84 for (const data of items) {
85 const item = createItem(data)
86 item.prev = prev
87 if (prev)
88 prev.next = item
89 else
90 this.head = item
91 prev = item
92 }
93 this.tail = prev
94 return this
95 }
96
97 toArray(): T[] {
98 const out: T[] = []
99 for (let cur = this.head; cur != null; cur = cur.next)
100 out.push(cur.data)
101 return out
102 }
103
104 toJSON(): T[] {
105 return this.toArray()
106 }
107
108 /**
109 * css-tree-style forEach: callback receives `(data, item, list)`.
110 * The `item` is the underlying `ListItem<T>`, not the index — call sites
111 * use it with `list.remove(item)` / `list.replace(item, ...)`.
112 */
113 // eslint-disable-next-line pickier/no-unused-vars
114 forEach(fn: (data: T, item: ListItem<T>, list: CssList<T>) => void, thisArg?: any): void {
115 const cursor = this.allocateCursor(null, this.head)
116 while (cursor.next !== null) {
117 const item = cursor.next as ListItem<T>
118 cursor.prev = item
119 cursor.next = item.next
120 fn.call(thisArg, item.data, item, this)
121 }
122 this.releaseCursor()
123 }
124
125 // eslint-disable-next-line pickier/no-unused-vars
126 forEachRight(fn: (data: T, item: ListItem<T>, list: CssList<T>) => void, thisArg?: any): void {
127 const cursor = this.allocateCursor(this.tail, null)
128 while (cursor.prev !== null) {
129 const item = cursor.prev as ListItem<T>
130 cursor.next = item
131 cursor.prev = item.prev
132 fn.call(thisArg, item.data, item, this)
133 }
134 this.releaseCursor()
135 }
136
137 // eslint-disable-next-line pickier/no-unused-vars
138 reduce<U>(fn: (acc: U, data: T, index: number, list: CssList<T>) => U, initial: U): U {
139 let acc = initial
140 let i = 0
141 for (let cur = this.head; cur != null; cur = cur.next)
142 acc = fn(acc, cur.data, i++, this)
143 return acc
144 }
145
146 // eslint-disable-next-line pickier/no-unused-vars
147 some(fn: (item: T, index: number, list: CssList<T>) => boolean): boolean {
148 let i = 0
149 for (let cur = this.head; cur != null; cur = cur.next) {
150 if (fn(cur.data, i++, this))
151 return true
152 }
153 return false
154 }
155
156 // eslint-disable-next-line pickier/no-unused-vars
157 map<U>(fn: (item: T, index: number, list: CssList<T>) => U): CssList<U> {
158 const result = new CssList<U>()
159 let prev: ListItem<U> | null = null
160 let i = 0
161 for (let cur = this.head; cur != null; cur = cur.next) {
162 const item = createItem(fn(cur.data, i++, this))
163 item.prev = prev
164 if (prev)
165 prev.next = item
166 else
167 result.head = item
168 prev = item
169 }
170 result.tail = prev
171 return result
172 }
173
174 // eslint-disable-next-line pickier/no-unused-vars
175 filter(fn: (item: T, index: number, list: CssList<T>) => boolean): CssList<T> {
176 const result = new CssList<T>()
177 let prev: ListItem<T> | null = null
178 let i = 0
179 for (let cur = this.head; cur != null; cur = cur.next) {
180 if (fn(cur.data, i++, this)) {
181 const item = createItem(cur.data)
182 item.prev = prev
183 if (prev)
184 prev.next = item
185 else
186 result.head = item
187 prev = item
188 }
189 }
190 result.tail = prev
191 return result
192 }
193
194 clear(): void {
195 // Unlink every item so consumers that retained a reference via
196 // `forEach`/`map` callbacks don't keep the rest of the chain alive
197 // through the prev/next pointers (which would defeat GC and let
198 // mutations on a "cleared" list surprise other code holding handles).
199 let cur = this.head
200 while (cur) {
201 const nxt = cur.next
202 cur.prev = null
203 cur.next = null
204 cur = nxt
205 }
206 this.head = null
207 this.tail = null
208 }
209
210 copy(): CssList<T> {
211 const result = new CssList<T>()
212 let prev: ListItem<T> | null = null
213 for (let cur = this.head; cur != null; cur = cur.next) {
214 const item = createItem(cur.data)
215 item.prev = prev
216 if (prev)
217 prev.next = item
218 else
219 result.head = item
220 prev = item
221 }
222 result.tail = prev
223 return result
224 }
225
226 prepend(item: ListItem<T>): this {
227 return this.insert(item, this.head)
228 }
229
230 prependData(data: T): this {
231 return this.insert(createItem(data), this.head)
232 }
233
234 append(item: ListItem<T>): this {
235 return this.insert(item, null)
236 }
237
238 appendData(data: T): this {
239 return this.insert(createItem(data), null)
240 }
241
242 /** Insert `item` before `before`, or at tail if `before` is null/undefined. */
243 insert(item: ListItem<T>, before: ListItem<T> | null | undefined = null): this {
244 if (before != null) {
245 this.updateCursors(before.prev, item, before, item)
246 if (before.prev === null) {
247 if (this.head !== before)
248 throw new Error('before doesn\'t belong to list')
249 this.head = item
250 before.prev = item
251 item.next = before
252 this.updateCursors(null, item, null, null)
253 }
254 else {
255 before.prev.next = item
256 item.prev = before.prev
257 before.prev = item
258 item.next = before
259 }
260 }
261 else {
262 this.updateCursors(this.tail, item, null, item)
263 if (this.tail !== null) {
264 this.tail.next = item
265 item.prev = this.tail
266 this.tail = item
267 }
268 else {
269 this.head = item
270 this.tail = item
271 }
272 }
273 return this
274 }
275
276 insertData(data: T, before: ListItem<T> | null = null): this {
277 return this.insert(createItem(data), before)
278 }
279
280 remove(item: ListItem<T>): ListItem<T> {
281 this.updateCursors(item, item.prev, item, item.next)
282 if (item.prev !== null)
283 item.prev.next = item.next
284 else if (this.head === item)
285 this.head = item.next
286 else
287 throw new Error('item doesn\'t belong to list')
288 if (item.next !== null)
289 item.next.prev = item.prev
290 else if (this.tail === item)
291 this.tail = item.prev
292 else
293 throw new Error('item doesn\'t belong to list')
294 item.prev = null
295 item.next = null
296 return item
297 }
298
299 push(data: T): void {
300 this.insert(createItem(data), null)
301 }
302
303 pop(): ListItem<T> | null {
304 if (this.tail === null)
305 return null
306 return this.remove(this.tail)
307 }
308
309 unshift(data: T): void {
310 this.prependData(data)
311 }
312
313 shift(): ListItem<T> | null {
314 if (this.head === null)
315 return null
316 return this.remove(this.head)
317 }
318
319 prependList(list: CssList<T>): this {
320 return this.insertList(list, this.head)
321 }
322
323 appendList(list: CssList<T>): this {
324 return this.insertList(list, null)
325 }
326
327 insertList(list: CssList<T>, before: ListItem<T> | null = null): this {
328 if (list.head === null)
329 return this
330 if (before !== null) {
331 this.updateCursors(before.prev, list.tail, before, list.head)
332 if (before.prev !== null) {
333 before.prev.next = list.head
334 list.head.prev = before.prev
335 }
336 else {
337 this.head = list.head
338 }
339 before.prev = list.tail
340 list.tail!.next = before
341 }
342 else {
343 this.updateCursors(this.tail, list.tail, null, list.head)
344 if (this.tail !== null) {
345 this.tail.next = list.head
346 list.head.prev = this.tail
347 }
348 else {
349 this.head = list.head
350 }
351 this.tail = list.tail
352 }
353 list.head = null
354 list.tail = null
355 return this
356 }
357
358 replace(oldItem: ListItem<T>, newItemOrList: ListItem<T> | CssList<T>): void {
359 if (newItemOrList instanceof CssList) {
360 this.insertList(newItemOrList, oldItem)
361 this.remove(oldItem)
362 }
363 else {
364 this.insert(newItemOrList, oldItem)
365 this.remove(oldItem)
366 }
367 }
368}
369
370/**
371 * Convenience constructor matching `csstree.List()` (callable without `new`).
372 */
373export function makeList<T>(): CssList<T> {
374 return new CssList<T>()
375}
packages/toolkit/src/parse/parser/index.tsadded+1340-0
Changes to packages/toolkit/src/parse/parser/index.ts
@@ -0,0 +1,1340 @@
1/**
2 * CSS parser. Recursive-descent over the tokenizer output. Produces an
3 * AST shape compatible with css-tree v3 (so consumer walker callbacks
4 * remain unchanged).
5 *
6 * Implemented contexts:
7 * stylesheet | declarationList | declaration | atrule | atrulePrelude
8 * selectorList | selector | rule | block | value | mediaQuery
9 * mediaQueryList | raw
10 *
11 * Options recognised (all match css-tree):
12 * parseValue, parseAtrulePrelude, parseCustomProperty, parseRulePrelude,
13 * filename, positions, context, onParseError
14 */
15
16import type {
17 Atrule,
18 AtrulePrelude,
19 AttributeSelector,
20 Block,
21 ClassSelector,
22 Combinator,
23 CommentNode,
24 CssLocation,
25 CssNode,
26 Declaration,
27 DeclarationList,
28 Identifier,
29 IdSelector,
30 ParenthesesNode,
31 ParseContext,
32 ParseOptions,
33 PseudoClassSelector,
34 PseudoElementSelector,
35 Raw,
36 Rule,
37 Selector,
38 SelectorList,
39 StringNode,
40 StyleSheet,
41 TypeSelector,
42 UrlNode,
43 Value,
44} from '../types'
45import { CssList } from '../list'
46import type { Token } from '../tokenizer'
47import { decodeName, decodeString, Tokenizer, TokenType } from '../tokenizer'
48
49interface ParserState {
50 source: string
51 // Parallel typed-array storage from the tokenizer. Direct reads avoid
52 // per-token Object allocation in the hot loop.
53 types: Uint8Array
54 starts: Uint32Array
55 ends: Uint32Array
56 count: number
57 /**
58 * Effective end of token stream visible to the parser. Equals `count` for
59 * normal parsing; gets temporarily lowered when we recurse into a
60 * sub-range (e.g. a declaration's value tokens) so the parser sees
61 * "virtual EOF" at the right boundary without re-tokenizing.
62 */
63 end: number
64 pos: number
65 positions: boolean
66 filename: string | undefined
67 parseValue: boolean
68 parseAtrulePrelude: boolean
69 parseCustomProperty: boolean
70 parseRulePrelude: boolean
71 // eslint-disable-next-line pickier/no-unused-vars
72 onParseError?: (error: SyntaxError, fallbackNode: CssNode) => void
73 tokenizer: Tokenizer
74}
75
76function makeState(source: string, options: ParseOptions): ParserState {
77 const tokenizer = new Tokenizer(source)
78 return {
79 source,
80 types: tokenizer.types,
81 starts: tokenizer.starts,
82 ends: tokenizer.ends,
83 count: tokenizer.count,
84 end: tokenizer.count,
85 pos: 0,
86 positions: options.positions ?? false,
87 filename: options.filename,
88 parseValue: options.parseValue ?? true,
89 parseAtrulePrelude: options.parseAtrulePrelude ?? true,
90 parseCustomProperty: options.parseCustomProperty ?? false,
91 parseRulePrelude: options.parseRulePrelude ?? true,
92 onParseError: options.onParseError,
93 tokenizer,
94 }
95}
96
97// ----- token-stream helpers -----
98// Read directly from the parallel typed arrays. `peek(s)` materialises a
99// `Token` view object lazily — most parser code only needs `.type` /
100// `.start` / `.end` which the helpers below expose without an alloc.
101
102function peekType(s: ParserState): TokenType {
103 return s.pos < s.end ? (s.types[s.pos]! as TokenType) : TokenType.EOF
104}
105
106function peek(s: ParserState): Token {
107 const i = s.pos
108 if (i >= s.end)
109 return { type: TokenType.EOF, start: s.starts[i]! ?? s.source.length, end: s.ends[i]! ?? s.source.length }
110 return { type: s.types[i]! as TokenType, start: s.starts[i]!, end: s.ends[i]! }
111}
112
113function consume(s: ParserState): Token {
114 const i = s.pos++
115 if (i >= s.end)
116 return { type: TokenType.EOF, start: s.starts[i]! ?? s.source.length, end: s.ends[i]! ?? s.source.length }
117 return { type: s.types[i]! as TokenType, start: s.starts[i]!, end: s.ends[i]! }
118}
119
120function tokenSlice(s: ParserState, t: Token): string {
121 return s.source.slice(t.start, t.end)
122}
123
124function skipWhitespace(s: ParserState): void {
125 const types = s.types
126 const end = s.end
127 while (s.pos < end) {
128 const t = types[s.pos]!
129 if (t === TokenType.WhiteSpace || t === TokenType.Comment)
130 s.pos++
131 else
132 break
133 }
134}
135
136function skipWhitespaceOnly(s: ParserState): void {
137 const types = s.types
138 const end = s.end
139 while (s.pos < end && types[s.pos]! === TokenType.WhiteSpace)
140 s.pos++
141}
142
143function loc(s: ParserState, startTok: Token, endTok: Token): CssLocation | null {
144 if (!s.positions)
145 return null
146 const start = s.tokenizer.locate(startTok.start)
147 const end = s.tokenizer.locate(endTok.end)
148 return {
149 source: s.filename ?? '<unknown>',
150 start: { offset: startTok.start, line: start.line, column: start.column },
151 end: { offset: endTok.end, line: end.line, column: end.column },
152 }
153}
154
155function emptyLoc(s: ParserState): CssLocation | null {
156 if (!s.positions)
157 return null
158 const t = peek(s)
159 const p = s.tokenizer.locate(t.start)
160 return {
161 source: s.filename ?? '<unknown>',
162 start: { offset: t.start, line: p.line, column: p.column },
163 end: { offset: t.start, line: p.line, column: p.column },
164 }
165}
166
167// ----- node constructors -----
168
169function rawNode(value: string, start: Token, end: Token, s: ParserState): Raw {
170 return { type: 'Raw', value, loc: loc(s, start, end) }
171}
172
173function newList<T>(): CssList<T> {
174 return new CssList<T>()
175}
176
177/**
178 * Report a parse problem to the consumer. Always returns the supplied
179 * fallback node so call sites can `return reportError(…, raw)`. The
180 * `onParseError` hook lets consumers (linters, IDEs) surface
181 * the parser's recovery substitutions.
182 */
183function reportError(s: ParserState, message: string, at: Token, fallback: CssNode): CssNode {
184 if (s.onParseError) {
185 const { line, column } = s.tokenizer.locate(at.start)
186 const err = new SyntaxError(`${s.filename ?? '<input>'}:${line}:${column}: ${message}`) as SyntaxError & { line: number, column: number, offset: number }
187 err.line = line
188 err.column = column
189 err.offset = at.start
190 s.onParseError(err, fallback)
191 }
192 return fallback
193}
194
195// ----- top-level dispatch -----
196
197export function parse(source: string, options: ParseOptions = {}): CssNode {
198 const s = makeState(source, options)
199 const ctx: ParseContext = options.context ?? 'stylesheet'
200
201 switch (ctx) {
202 case 'stylesheet': return parseStyleSheet(s)
203 case 'atrule': return parseAtrule(s) ?? makeEmptyStyleSheet(s)
204 case 'atrulePrelude': return parseAtrulePreludeContext(s)
205 case 'mediaQuery':
206 case 'mediaQueryList': return parseRawAsValue(s)
207 case 'rule': return parseRule(s)
208 case 'selectorList': return parseSelectorList(s)
209 case 'selector': return parseSelector(s)
210 case 'block': return parseBlock(s)
211 case 'declarationList': return parseDeclarationList(s)
212 case 'declaration': return parseDeclaration(s) ?? makeEmptyStyleSheet(s)
213 case 'value': return parseValue(s)
214 case 'raw': return parseRawAsValue(s)
215 default: return parseStyleSheet(s)
216 }
217}
218
219function makeEmptyStyleSheet(s: ParserState): StyleSheet {
220 return { type: 'StyleSheet', children: newList<CssNode>(), loc: emptyLoc(s) }
221}
222
223function parseRawAsValue(s: ParserState): Raw {
224 const start = peek(s)
225 let end = start
226 while (peekType(s) !== TokenType.EOF) {
227 end = consume(s)
228 }
229 return { type: 'Raw', value: s.source.slice(start.start, end.end), loc: loc(s, start, end) }
230}
231
232// ----- stylesheet -----
233
234function parseStyleSheet(s: ParserState): StyleSheet {
235 const startTok = peek(s)
236 const children = newList<CssNode>()
237 const types = s.types
238 const end = s.end
239 // Hot loop: read type from the typed array directly to avoid the
240 // per-iteration Token-object alloc that `peek()` would do.
241 while (s.pos < end) {
242 const t = types[s.pos]!
243 if (t === TokenType.WhiteSpace) {
244 s.pos++
245 continue
246 }
247 if (t === TokenType.Comment) {
248 const tok: Token = { type: TokenType.Comment, start: s.starts[s.pos]!, end: s.ends[s.pos]! }
249 const node = makeComment(s, tok)
250 s.pos++
251 children.appendData(node)
252 continue
253 }
254 if (t === TokenType.EOF)
255 break
256 if (t === TokenType.CDO || t === TokenType.CDC) {
257 s.pos++
258 continue
259 }
260 if (t === TokenType.AtKeyword) {
261 const at = parseAtrule(s)
262 if (at)
263 children.appendData(at)
264 continue
265 }
266 // Top-level safety: a stray `}` (mismatched braces in malformed input)
267 // would otherwise let parseRule return without advancing — guard against
268 // the infinite loop here rather than embedding the check inside parseRule.
269 if (t === TokenType.RightCurlyBracket) {
270 s.pos++
271 continue
272 }
273 const before = s.pos
274 const r = parseRule(s)
275 children.appendData(r)
276 if (s.pos === before)
277 s.pos++
278 }
279 const lastIdx = s.end - 1
280 const endTok: Token = lastIdx >= 0
281 ? { type: s.types[lastIdx]! as TokenType, start: s.starts[lastIdx]!, end: s.ends[lastIdx]! }
282 : { type: TokenType.EOF, start: 0, end: 0 }
283 return { type: 'StyleSheet', children, loc: loc(s, startTok, endTok) }
284}
285
286function makeComment(s: ParserState, t: Token): CommentNode {
287 return { type: 'Comment', value: s.source.slice(t.start + 2, t.end - 2), loc: loc(s, t, t) }
288}
289
290// ----- at-rule -----
291
292function parseAtrule(s: ParserState): Atrule | null {
293 const startTok = peek(s)
294 if (startTok.type !== TokenType.AtKeyword)
295 return null
296 s.pos++
297 const name = lowerIfNeeded(s.source, startTok.start + 1, startTok.end)
298
299 // Collect prelude tokens until ; or { — pre-scan for the boundary.
300 skipWhitespace(s)
301 const preludeStartPos = s.pos
302 const preludeStartTok = peek(s)
303 const types = s.types
304 const tend = s.end
305 let scanPos = preludeStartPos
306 let preludeEndPos = preludeStartPos
307 while (scanPos < tend) {
308 const t = types[scanPos]!
309 if (t === TokenType.EOF || t === TokenType.Semicolon || t === TokenType.LeftCurlyBracket)
310 break
311 if (t !== TokenType.WhiteSpace && t !== TokenType.Comment)
312 preludeEndPos = scanPos + 1
313 scanPos++
314 }
315
316 let prelude: AtrulePrelude | Raw | null = null
317 if (preludeEndPos > preludeStartPos) {
318 const preludeStartByte = s.starts[preludeStartPos]!
319 const preludeEndByte = s.ends[preludeEndPos - 1]!
320 const preludeEndTok: Token = { type: types[preludeEndPos - 1]! as TokenType, start: s.starts[preludeEndPos - 1]!, end: preludeEndByte }
321 const raw = s.source.slice(preludeStartByte, preludeEndByte).trim()
322 if (raw.length > 0) {
323 if (s.parseAtrulePrelude) {
324 try {
325 // Run the prelude parser on the existing token stream — temporarily
326 // restrict `s.end` to the prelude boundary so the loop sees EOF.
327 const savedEnd = s.end
328 s.end = preludeEndPos
329 s.pos = preludeStartPos
330 const children = newList<CssNode>()
331 while (peekType(s) !== TokenType.EOF) {
332 const node = parseValueChild(s)
333 if (node)
334 children.appendData(node)
335 }
336 promoteRatiosDeep(children)
337 s.end = savedEnd
338 // pos lands on the ; or { boundary so parseAtrule's caller can
339 // dispatch on the next token without re-walking.
340 s.pos = scanPos
341 if (children.isEmpty)
342 prelude = rawNode(raw, preludeStartTok, preludeEndTok, s)
343 else
344 prelude = { type: 'AtrulePrelude', children, loc: null }
345 }
346 catch (err) {
347 const fallback = rawNode(raw, preludeStartTok, preludeEndTok, s)
348 reportError(s, `Failed to parse @${name} prelude: ${(err as Error).message}`, preludeStartTok, fallback)
349 prelude = fallback
350 s.pos = scanPos
351 }
352 }
353 else {
354 prelude = rawNode(raw, preludeStartTok, preludeEndTok, s)
355 s.pos = scanPos
356 }
357 }
358 else {
359 s.pos = scanPos
360 }
361 }
362 else {
363 s.pos = scanPos
364 }
365
366 let block: Block | null = null
367 if (peekType(s) === TokenType.Semicolon) {
368 consume(s)
369 }
370 else if (peekType(s) === TokenType.LeftCurlyBracket) {
371 block = parseBlock(s, AT_RULES_WITH_NESTED_RULES.has(name))
372 }
373
374 return { type: 'Atrule', name, prelude, block, loc: loc(s, startTok, peek(s)) }
375}
376
377/**
378 * Pseudo-class names whose argument is a selector list and needs to be
379 * re-parsed by `parseSelectorList` (rather than collected as a value).
380 */
381const SELECTOR_LIST_PSEUDOS_PARSE: ReadonlySet<string> = new Set([
382 'is', 'not', 'where', 'has', 'matches', '-moz-any', '-webkit-any',
383])
384
385/**
386 * At-rules whose block contains nested *rules* (selector { … }), as
387 * opposed to direct declarations. Used to switch parser mode inside the
388 * block. Lives here (not on _collections) because it's a parser concern.
389 */
390// eslint-disable-next-line pickier/no-unused-vars
391const AT_RULES_WITH_NESTED_RULES: ReadonlySet<string> = new Set([
392 'media',
393 'supports',
394 'document',
395 'layer',
396 'container',
397 'scope',
398 'starting-style',
399 '-moz-document',
400 // keyframes — children are rules with `from` / `to` / `50%` preludes
401 'keyframes',
402 '-webkit-keyframes',
403 '-moz-keyframes',
404 '-o-keyframes',
405])
406
407function parseAtrulePreludeContext(s: ParserState): AtrulePrelude {
408 const startTok = peek(s)
409 const children = newList<CssNode>()
410 while (peekType(s) !== TokenType.EOF) {
411 const t = peek(s)
412 if (t.type === TokenType.WhiteSpace) {
413 s.pos++
414 continue
415 }
416 const node = parseValueChild(s)
417 if (node)
418 children.appendData(node)
419 }
420 return { type: 'AtrulePrelude', children, loc: loc(s, startTok, peek(s)) }
421}
422
423// ----- rule -----
424
425function parseRule(s: ParserState): Rule {
426 const startTok = peek(s)
427
428 let prelude: SelectorList | Raw
429 if (s.parseRulePrelude) {
430 // Parse the selector list straight from the existing token stream —
431 // `parseSelectorList` stops at `,`-terminated boundaries internally and
432 // at the leading `{` of the block, so no re-tokenization is needed.
433 prelude = parseSelectorList(s)
434 }
435 else {
436 // Raw fallback: collect tokens up to `{` and slice the text.
437 const preludeStart = peek(s)
438 let preludeEnd = preludeStart
439 while (peekType(s) !== TokenType.EOF && peekType(s) !== TokenType.LeftCurlyBracket)
440 preludeEnd = consume(s)
441 const preludeText = s.source.slice(preludeStart.start, preludeEnd.end).trim()
442 prelude = rawNode(preludeText, preludeStart, preludeEnd, s)
443 }
444
445 // Rules support CSS Nesting (`.foo { color: red; & .bar { color: blue } }`)
446 // — `allowNested` flips on the lookahead that distinguishes a nested
447 // rule from a declaration with the same opening tokens.
448 const block = parseBlock(s, true)
449 return { type: 'Rule', prelude, block, loc: loc(s, startTok, peek(s)) }
450}
451
452// ----- block -----
453
454function parseBlock(s: ParserState, allowNested: boolean = false): Block {
455 const startTok = peek(s)
456 if (peekType(s) !== TokenType.LeftCurlyBracket)
457 return { type: 'Block', children: newList<CssNode>(), loc: emptyLoc(s) }
458 s.pos++ // {
459 const children = newList<CssNode>()
460 const types = s.types
461 const end = s.end
462 while (s.pos < end) {
463 const t = types[s.pos]!
464 if (t === TokenType.RightCurlyBracket || t === TokenType.EOF)
465 break
466 if (t === TokenType.WhiteSpace) {
467 s.pos++
468 continue
469 }
470 if (t === TokenType.Comment) {
471 const tok: Token = { type: TokenType.Comment, start: s.starts[s.pos]!, end: s.ends[s.pos]! }
472 s.pos++
473 children.appendData(makeComment(s, tok))
474 continue
475 }
476 if (t === TokenType.AtKeyword) {
477 const at = parseAtrule(s)
478 if (at)
479 children.appendData(at)
480 continue
481 }
482 if (allowNested && lookahead(s, isCurlyAheadBeforeSemicolon)) {
483 const r = parseRule(s)
484 children.appendData(r)
485 continue
486 }
487 const decl = parseDeclaration(s)
488 if (decl)
489 children.appendData(decl)
490 if (s.pos < end && types[s.pos]! === TokenType.Semicolon)
491 s.pos++
492 }
493 if (peekType(s) === TokenType.RightCurlyBracket)
494 s.pos++
495 return { type: 'Block', children, loc: loc(s, startTok, peek(s)) }
496}
497
498function lookahead(s: ParserState, predicate: (s: ParserState) => boolean): boolean {
499 const saved = s.pos
500 const r = predicate(s)
501 s.pos = saved
502 return r
503}
504
505function isCurlyAheadBeforeSemicolon(s: ParserState): boolean {
506 const types = s.types
507 const end = s.end
508 while (s.pos < end) {
509 const t = types[s.pos]!
510 if (t === TokenType.LeftCurlyBracket)
511 return true
512 if (t === TokenType.Semicolon || t === TokenType.RightCurlyBracket || t === TokenType.EOF)
513 return false
514 s.pos++
515 }
516 return false
517}
518
519// ----- declarationList -----
520
521function parseDeclarationList(s: ParserState): DeclarationList {
522 const startTok = peek(s)
523 const children = newList<CssNode>()
524 while (peekType(s) !== TokenType.EOF && peekType(s) !== TokenType.RightCurlyBracket) {
525 const t = peek(s)
526 if (t.type === TokenType.WhiteSpace) {
527 s.pos++
528 continue
529 }
530 if (t.type === TokenType.Comment) {
531 children.appendData(makeComment(s, consume(s)))
532 continue
533 }
534 if (t.type === TokenType.Semicolon) {
535 s.pos++
536 continue
537 }
538 if (t.type === TokenType.AtKeyword) {
539 const at = parseAtrule(s)
540 if (at)
541 children.appendData(at)
542 continue
543 }
544 const decl = parseDeclaration(s)
545 if (decl)
546 children.appendData(decl)
547 if (peekType(s) === TokenType.Semicolon)
548 consume(s)
549 }
550 return { type: 'DeclarationList', children, loc: loc(s, startTok, peek(s)) }
551}
552
553// ----- declaration -----
554
555function parseDeclaration(s: ParserState): Declaration | null {
556 skipWhitespace(s)
557 const startTok = peek(s)
558 if (startTok.type !== TokenType.Ident && startTok.type !== TokenType.Hash && !(startTok.type === TokenType.Delim && s.source[startTok.start] === '*' && s.source[startTok.start + 1] === ' ')) {
559 if (startTok.type === TokenType.Delim && s.source[startTok.start] === '*') {
560 // ie *property
561 s.pos++
562 }
563 else {
564 reportError(s, `Expected property name`, startTok, { type: 'Raw', value: '', loc: null })
565 const types = s.types
566 const end = s.end
567 while (s.pos < end) {
568 const t = types[s.pos]!
569 if (t === TokenType.EOF || t === TokenType.Semicolon || t === TokenType.RightCurlyBracket)
570 break
571 s.pos++
572 }
573 return null
574 }
575 }
576 const propStart = s.starts[s.pos]!
577 const propEnd = s.ends[s.pos]!
578 s.pos++
579 const property = s.source.slice(propStart, propEnd)
580
581 skipWhitespaceOnly(s)
582 if (peekType(s) !== TokenType.Colon) {
583 reportError(s, `Expected ':' after property "${property}"`, peek(s), { type: 'Raw', value: '', loc: null })
584 const types = s.types
585 const end = s.end
586 while (s.pos < end) {
587 const t = types[s.pos]!
588 if (t === TokenType.EOF || t === TokenType.Semicolon || t === TokenType.RightCurlyBracket)
589 break
590 s.pos++
591 }
592 return null
593 }
594 s.pos++ // :
595
596 skipWhitespaceOnly(s)
597 // Pre-scan the value token range. We track:
598 // - lastNonWsPos: index after the last non-whitespace value token
599 // (where the parsable value ends — excludes trailing whitespace/!important)
600 // - importantPos: position of the `important` ident if !important detected
601 // - endPos: stop position (Semicolon/RightCurly/EOF)
602 // This avoids re-tokenizing the value text — the parser walks the
603 // already-tokenised stream directly.
604 const types = s.types
605 const starts = s.starts
606 const tend = s.end
607 const valueStartPos = s.pos
608 let scanPos = valueStartPos
609 let lastNonWsPos = valueStartPos
610 let importantIdentPos = -1
611 let bangPos = -1
612 while (scanPos < tend) {
613 const t = types[scanPos]!
614 if (t === TokenType.EOF || t === TokenType.Semicolon || t === TokenType.RightCurlyBracket)
615 break
616 if (t === TokenType.Delim && s.source.charCodeAt(starts[scanPos]!) === 33 /* ! */) {
617 // potential !important — peek next non-ws
618 let look = scanPos + 1
619 while (look < tend && types[look]! === TokenType.WhiteSpace) look++
620 if (look < tend && types[look]! === TokenType.Ident) {
621 const ts = starts[look]!
622 const te = s.ends[look]!
623 // Inline lowercase compare for "important" (9 chars)
624 if (te - ts === 9
625 && (s.source.charCodeAt(ts) | 32) === 105
626 && (s.source.charCodeAt(ts + 1) | 32) === 109
627 && (s.source.charCodeAt(ts + 2) | 32) === 112
628 && (s.source.charCodeAt(ts + 3) | 32) === 111
629 && (s.source.charCodeAt(ts + 4) | 32) === 114
630 && (s.source.charCodeAt(ts + 5) | 32) === 116
631 && (s.source.charCodeAt(ts + 6) | 32) === 97
632 && (s.source.charCodeAt(ts + 7) | 32) === 110
633 && (s.source.charCodeAt(ts + 8) | 32) === 116) {
634 bangPos = scanPos
635 importantIdentPos = look
636 scanPos = look + 1
637 continue
638 }
639 }
640 }
641 if (t !== TokenType.WhiteSpace && t !== TokenType.Comment)
642 lastNonWsPos = scanPos + 1
643 scanPos++
644 }
645
646 // The value range, parser-relative: [valueStartPos, valueEndPos).
647 // `lastNonWsPos` already excludes both the trailing whitespace AND the
648 // !important suffix because we don't update it inside the bang branch.
649 void bangPos
650 const valueEndPos = lastNonWsPos
651 const importantTok: Token | null = importantIdentPos >= 0
652 ? { type: TokenType.Ident, start: starts[importantIdentPos]!, end: s.ends[importantIdentPos]! }
653 : null
654
655 // `property` came directly from a single Ident token's source slice, so
656 // it can't have leading/trailing whitespace — the previous `.trim()`
657 // here was load-bearing only when the slice path included whitespace
658 // chars, which doesn't happen with the tokenizer.
659 const isCustom = property.startsWith('--')
660 let value: Value | Raw
661 const valueStartByte = starts[valueStartPos]!
662 const valueEndByte = valueEndPos > valueStartPos ? s.ends[valueEndPos - 1]! : valueStartByte
663 const valueStartTok: Token = { type: types[valueStartPos]! as TokenType, start: valueStartByte, end: s.ends[valueStartPos]! }
664 const valueEndTok: Token = valueEndPos > valueStartPos
665 ? { type: types[valueEndPos - 1]! as TokenType, start: starts[valueEndPos - 1]!, end: valueEndByte }
666 : valueStartTok
667
668 if ((isCustom && !s.parseCustomProperty) || !s.parseValue) {
669 const valueText = s.source.slice(valueStartByte, valueEndByte).trim()
670 value = { type: 'Raw', value: valueText, loc: loc(s, valueStartTok, valueEndTok) }
671 }
672 else if (valueEndPos === valueStartPos) {
673 value = { type: 'Value', children: newList<CssNode>(), loc: loc(s, valueStartTok, valueEndTok) }
674 }
675 else {
676 // Run parseValueChildren on the existing token stream by temporarily
677 // shrinking `s.end` to the value boundary so the parser sees a virtual
678 // EOF at the right place. No re-tokenization required.
679 const savedEnd = s.end
680 s.end = valueEndPos
681 s.pos = valueStartPos
682 value = parseValueChildren(s)
683 s.end = savedEnd
684 }
685
686 // Position the parser at the boundary token (Semicolon / RightCurly / EOF).
687 // `scanPos` already advanced past !important when it was detected.
688 s.pos = scanPos
689
690 return {
691 type: 'Declaration',
692 property,
693 important: importantTok ? true : false,
694 value,
695 loc: loc(s, startTok, peek(s)),
696 }
697}
698
699// ----- value -----
700
701function parseValue(s: ParserState): Value {
702 return parseValueChildren(s)
703}
704
705function parseValueChildren(s: ParserState): Value {
706 const startTok = peek(s)
707 const children = newList<CssNode>()
708 while (peekType(s) !== TokenType.EOF
709 && peekType(s) !== TokenType.Semicolon
710 && peekType(s) !== TokenType.RightCurlyBracket
711 && peekType(s) !== TokenType.RightParenthesis
712 && peekType(s) !== TokenType.RightSquareBracket) {
713 const node = parseValueChild(s)
714 if (node)
715 children.appendData(node)
716 }
717 promoteRatios(children)
718 return { type: 'Value', children, loc: loc(s, startTok, peek(s)) }
719}
720
721/**
722 * Recursive ratio promotion — walk every nested children list (Function,
723 * Parentheses, AtrulePrelude, etc.) and run `promoteRatios` on each.
724 */
725function promoteRatiosDeep(list: CssList<CssNode>): void {
726 for (const child of list) {
727 if ('children' in child && (child as any).children instanceof CssList)
728 promoteRatiosDeep((child as any).children)
729 }
730 promoteRatios(list)
731}
732
733/**
734 * Walk a children list and promote `Number / Number` sequences (with
735 * optional WhiteSpace around the slash) into a single `Ratio` node.
736 * Mirrors css-tree's handling of `aspect-ratio: 16/9`.
737 */
738function promoteRatios(list: CssList<CssNode>): void {
739 let cur = list.head
740 while (cur) {
741 if (cur.data.type === 'Number') {
742 // skip whitespace
743 let slashItem = cur.next
744 while (slashItem && slashItem.data.type === 'WhiteSpace')
745 slashItem = slashItem.next
746 if (slashItem && slashItem.data.type === 'Operator' && (slashItem.data as { value: string }).value === '/') {
747 let rightItem = slashItem.next
748 while (rightItem && rightItem.data.type === 'WhiteSpace')
749 rightItem = rightItem.next
750 if (rightItem && rightItem.data.type === 'Number') {
751 // Build Ratio and splice it in.
752 const ratio: CssNode = {
753 type: 'Ratio',
754 left: cur.data as any,
755 right: rightItem.data as any,
756 loc: null,
757 }
758 // Remove items between cur (exclusive end) and rightItem (inclusive)
759 let toRemove = cur.next
760 while (toRemove && toRemove !== rightItem.next) {
761 const nxt = toRemove.next
762 list.remove(toRemove)
763 toRemove = nxt
764 }
765 list.replace(cur, list.createItem(ratio))
766 cur = list.head // restart — list pointer changed
767 continue
768 }
769 }
770 }
771 cur = cur.next
772 }
773}
774
775function parseValueChild(s: ParserState): CssNode | null {
776 const i = s.pos
777 if (i >= s.end)
778 return null
779 const ttype = s.types[i]! as TokenType
780 const tstart = s.starts[i]!
781 const tend = s.ends[i]!
782 const src = s.source
783 switch (ttype) {
784 case TokenType.WhiteSpace: {
785 s.pos++
786 return s.positions
787 ? { type: 'WhiteSpace', value: ' ', loc: locRange(s, tstart, tend) }
788 : { type: 'WhiteSpace', value: ' ', loc: null }
789 }
790 case TokenType.Comment: {
791 s.pos++
792 return { type: 'Comment', value: src.slice(tstart + 2, tend - 2), loc: s.positions ? locRange(s, tstart, tend) : null }
793 }
794 case TokenType.Number: {
795 s.pos++
796 return { type: 'Number', value: src.slice(tstart, tend), loc: s.positions ? locRange(s, tstart, tend) : null }
797 }
798 case TokenType.Percentage: {
799 s.pos++
800 return { type: 'Percentage', value: src.slice(tstart, tend - 1), loc: s.positions ? locRange(s, tstart, tend) : null }
801 }
802 case TokenType.Dimension: {
803 s.pos++
804 // Manual scan: number portion ends where the unit (alpha/`-`/escape)
805 // begins. Faster than the previous regex.
806 let unitStart = tstart
807 // optional sign
808 const c0 = src.charCodeAt(unitStart)
809 if (c0 === 43 /* + */ || c0 === 45 /* - */)
810 unitStart++
811 // integer/fractional digits and a single dot
812 let sawDot = false
813 while (unitStart < tend) {
814 const c = src.charCodeAt(unitStart)
815 if (c >= 48 && c <= 57) {
816 unitStart++
817 continue
818 }
819 if (c === 46 && !sawDot) {
820 sawDot = true
821 unitStart++
822 continue
823 }
824 break
825 }
826 // optional scientific notation
827 const eC = src.charCodeAt(unitStart)
828 if ((eC === 69 || eC === 101) /* e/E */) {
829 let look = unitStart + 1
830 const sgn = src.charCodeAt(look)
831 if (sgn === 43 || sgn === 45) look++
832 let any = false
833 while (look < tend) {
834 const c = src.charCodeAt(look)
835 if (c >= 48 && c <= 57) { look++; any = true; continue }
836 break
837 }
838 if (any) unitStart = look
839 }
840 return {
841 type: 'Dimension',
842 value: src.slice(tstart, unitStart),
843 unit: src.slice(unitStart, tend),
844 loc: s.positions ? locRange(s, tstart, tend) : null,
845 }
846 }
847 case TokenType.Ident: {
848 s.pos++
849 return { type: 'Identifier', name: decodeName(src, tstart, tend), loc: s.positions ? locRange(s, tstart, tend) : null }
850 }
851 case TokenType.String: {
852 s.pos++
853 const innerStart = tstart + 1
854 const closesProperly = tend - tstart >= 2 && src.charCodeAt(tend - 1) === src.charCodeAt(tstart)
855 const innerEnd = closesProperly ? tend - 1 : tend
856 const value = decodeString(src, innerStart, innerEnd)
857 return { type: 'String', value, loc: s.positions ? locRange(s, tstart, tend) : null }
858 }
859 case TokenType.Url: {
860 s.pos++
861 // url(<inner>) — strip `url(` and the matching `)` (and any quotes
862 // on the inner if present), avoiding a regex+slice chain.
863 let a = tstart + 4 // past `url(`
864 while (a < tend && isWhitespaceCode(src.charCodeAt(a))) a++
865 let b = tend
866 if (b > a && src.charCodeAt(b - 1) === 41 /* ) */) b--
867 while (b > a && isWhitespaceCode(src.charCodeAt(b - 1))) b--
868 let value: string
869 if (b - a >= 2) {
870 const q0 = src.charCodeAt(a)
871 const q1 = src.charCodeAt(b - 1)
872 if ((q0 === 34 || q0 === 39) && q0 === q1)
873 value = src.slice(a + 1, b - 1)
874 else
875 value = src.slice(a, b)
876 }
877 else {
878 value = src.slice(a, b)
879 }
880 return { type: 'Url', value, loc: s.positions ? locRange(s, tstart, tend) : null }
881 }
882 case TokenType.Hash: {
883 s.pos++
884 return { type: 'Hash', name: src.slice(tstart + 1, tend), loc: s.positions ? locRange(s, tstart, tend) : null }
885 }
886 case TokenType.Function: {
887 return parseFunction(s)
888 }
889 case TokenType.LeftParenthesis: {
890 return parseParentheses(s)
891 }
892 case TokenType.Comma: {
893 s.pos++
894 return { type: 'Operator', value: ',', loc: s.positions ? locRange(s, tstart, tend) : null }
895 }
896 case TokenType.Colon: {
897 s.pos++
898 return { type: 'Operator', value: ':', loc: s.positions ? locRange(s, tstart, tend) : null }
899 }
900 case TokenType.Delim: {
901 s.pos++
902 const code = src.charCodeAt(tstart)
903 const ch = src[tstart]!
904 // ASCII bitmap: `/ + - * = > < ~ | $ ^ ! &` — Operator semantics in
905 // value contexts. Anything else falls through as an Identifier
906 // (e.g. `&` mid-value, `!` outside !important, etc.).
907 if (isValueOperatorChar(code))
908 return { type: 'Operator', value: ch, loc: s.positions ? locRange(s, tstart, tend) : null }
909 return { type: 'Identifier', name: ch, loc: s.positions ? locRange(s, tstart, tend) : null }
910 }
911 case TokenType.LeftSquareBracket: {
912 // `[…]` inside a value (e.g. `grid-template-columns: [start] 1fr`)
913 // becomes a `Brackets` node — matches css-tree's shape so consumers
914 // walking nested values don't see a Raw blob here.
915 s.pos++
916 const children = newList<CssNode>()
917 while (peekType(s) !== TokenType.EOF && peekType(s) !== TokenType.RightSquareBracket) {
918 const inner = parseValueChild(s)
919 if (inner)
920 children.appendData(inner)
921 }
922 const endByte = s.pos < s.end ? s.ends[s.pos]! : tend
923 if (peekType(s) === TokenType.RightSquareBracket)
924 s.pos++
925 return { type: 'Brackets', children, loc: s.positions ? locRange(s, tstart, endByte) : null }
926 }
927 case TokenType.AtKeyword:
928 case TokenType.LeftCurlyBracket:
929 case TokenType.RightCurlyBracket:
930 case TokenType.RightParenthesis:
931 case TokenType.RightSquareBracket:
932 case TokenType.Semicolon:
933 case TokenType.EOF:
934 case TokenType.CDO:
935 case TokenType.CDC:
936 case TokenType.BadString:
937 case TokenType.BadUrl: {
938 s.pos++
939 return { type: 'Raw', value: src.slice(tstart, tend), loc: s.positions ? locRange(s, tstart, tend) : null }
940 }
941 }
942 s.pos++
943 return null
944}
945
946function isWhitespaceCode(c: number): boolean {
947 return c === 32 || c === 9 || c === 10 || c === 12 || c === 13
948}
949
950function isValueOperatorChar(code: number): boolean {
951 // / + - * = > < ~ | $ ^ ! &
952 return code === 47 || code === 43 || code === 45 || code === 42
953 || code === 61 || code === 62 || code === 60 || code === 126
954 || code === 124 || code === 36 || code === 94 || code === 33
955 || code === 38
956}
957
958/**
959 * Slice `source[start:end]` and lowercase it, but only allocate a new
960 * string when the slice actually contains uppercase ASCII. Pure-ASCII
961 * lowercase identifiers (the common case) fast-path through `slice`.
962 */
963function lowerIfNeeded(source: string, start: number, end: number): string {
964 for (let i = start; i < end; i++) {
965 const c = source.charCodeAt(i)
966 if (c >= 65 && c <= 90)
967 return source.slice(start, end).toLowerCase()
968 }
969 return source.slice(start, end)
970}
971
972function locRange(s: ParserState, startByte: number, endByte: number): CssLocation {
973 const start = s.tokenizer.locate(startByte)
974 const end = s.tokenizer.locate(endByte)
975 return {
976 source: s.filename ?? '<unknown>',
977 start: { offset: startByte, line: start.line, column: start.column },
978 end: { offset: endByte, line: end.line, column: end.column },
979 }
980}
981
982function parseFunction(s: ParserState): CssNode {
983 const startTok = consume(s) // function token (including `(`)
984 // Fast path: scan for any uppercase ASCII before allocating a lower-case
985 // copy. Most function names in real stylesheets (`rgb`, `var`, `calc`,
986 // `linear-gradient`, …) are already lowercase, so the scan-with-no-upper
987 // path saves the `.toLowerCase()` allocation per Function token.
988 const name = lowerIfNeeded(s.source, startTok.start, startTok.end - 1)
989 const children = newList<CssNode>()
990 while (peekType(s) !== TokenType.EOF && peekType(s) !== TokenType.RightParenthesis) {
991 const node = parseValueChild(s)
992 if (node)
993 children.appendData(node)
994 }
995 if (peekType(s) === TokenType.RightParenthesis)
996 consume(s)
997
998 // Normalise url("…") / url('…') into a Url node (matches css-tree shape).
999 // Walk the linked list directly — avoids the `toArray()` allocation.
1000 if (name === 'url') {
1001 for (let cur: any = children.head; cur != null; cur = cur.next) {
1002 if (cur.data.type === 'String')
1003 return { type: 'Url', value: (cur.data as any).value, loc: loc(s, startTok, peek(s)) } as UrlNode
1004 }
1005 }
1006
1007 return { type: 'Function', name, children, loc: loc(s, startTok, peek(s)) }
1008}
1009
1010function parseParentheses(s: ParserState): ParenthesesNode {
1011 const startTok = consume(s) // (
1012 const children = newList<CssNode>()
1013 while (peekType(s) !== TokenType.EOF && peekType(s) !== TokenType.RightParenthesis) {
1014 const node = parseValueChild(s)
1015 if (node)
1016 children.appendData(node)
1017 }
1018 if (peekType(s) === TokenType.RightParenthesis)
1019 consume(s)
1020 return { type: 'Parentheses', children, loc: loc(s, startTok, peek(s)) }
1021}
1022
1023// ----- selectorList / selector -----
1024
1025function parseSelectorList(s: ParserState): SelectorList {
1026 const startTok = peek(s)
1027 const children = newList<CssNode>()
1028 while (peekType(s) !== TokenType.EOF) {
1029 skipWhitespace(s)
1030 if (peekType(s) === TokenType.EOF || peekType(s) === TokenType.LeftCurlyBracket)
1031 break
1032 const sel = parseSelector(s)
1033 // Skip empty selectors entirely — these arise from malformed input
1034 // like `,, .a {}` where parseSelector hits a `,` without finding any
1035 // segment. Adding them would surface as `Rule.prelude.children: [{},…]`
1036 // and break round-trips downstream.
1037 if ((sel.children as any).head !== null)
1038 children.appendData(sel)
1039 skipWhitespace(s)
1040 if (peekType(s) === TokenType.Comma) {
1041 consume(s)
1042 continue
1043 }
1044 break
1045 }
1046 return { type: 'SelectorList', children, loc: loc(s, startTok, peek(s)) }
1047}
1048
1049function parseSelector(s: ParserState): Selector {
1050 const startTok = peek(s)
1051 const children = newList<CssNode>()
1052 let lastWasCombinator = true
1053 while (peekType(s) !== TokenType.EOF
1054 && peekType(s) !== TokenType.Comma
1055 && peekType(s) !== TokenType.LeftCurlyBracket) {
1056 const t = peek(s)
1057 if (t.type === TokenType.WhiteSpace) {
1058 consume(s)
1059 // descendant combinator only between segments — and only when the
1060 // next token isn't itself a combinator (`>`/`+`/`~` or `||`).
1061 if (!lastWasCombinator
1062 && peekType(s) !== TokenType.EOF
1063 && peekType(s) !== TokenType.Comma
1064 && peekType(s) !== TokenType.LeftCurlyBracket
1065 && !isExplicitCombinator(s)
1066 && !isColumnCombinator(s)) {
1067 const c: Combinator = { type: 'Combinator', name: ' ', loc: loc(s, t, t) }
1068 children.appendData(c)
1069 lastWasCombinator = true
1070 }
1071 continue
1072 }
1073 // Column combinator `||` — two consecutive `|` Delim tokens (CSS
1074 // Selectors 4). Detect before the single-char combinator path.
1075 if (
1076 t.type === TokenType.Delim
1077 && s.source.charCodeAt(t.start) === 124 /* | */
1078 && s.types[s.pos + 1] === TokenType.Delim
1079 && s.source.charCodeAt(s.starts[s.pos + 1]!) === 124
1080 ) {
1081 const startCol = peek(s)
1082 consume(s)
1083 const endCol = consume(s)
1084 const c: Combinator = { type: 'Combinator', name: '||', loc: loc(s, startCol, endCol) }
1085 children.appendData(c)
1086 lastWasCombinator = true
1087 skipWhitespace(s)
1088 continue
1089 }
1090 if (isExplicitCombinator(s)) {
1091 const ct = consume(s)
1092 const ch = s.source[ct.start]!
1093 const c: Combinator = { type: 'Combinator', name: ch, loc: loc(s, ct, ct) }
1094 children.appendData(c)
1095 lastWasCombinator = true
1096 skipWhitespace(s)
1097 continue
1098 }
1099 const seg = parseSelectorSegment(s)
1100 if (!seg)
1101 break
1102 children.appendData(seg)
1103 lastWasCombinator = false
1104 }
1105 // A trailing combinator (descendant or otherwise) is meaningless — the
1106 // selector ended before another segment arrived. Strip it so the AST
1107 // doesn't carry a phantom `Combinator` that round-trips to extra
1108 // whitespace at the end of the rule prelude.
1109 while (children.tail && (children.tail.data as CssNode).type === 'Combinator')
1110 children.remove(children.tail)
1111 return { type: 'Selector', children, loc: loc(s, startTok, peek(s)) }
1112}
1113
1114function isExplicitCombinator(s: ParserState): boolean {
1115 const t = peek(s)
1116 if (t.type !== TokenType.Delim)
1117 return false
1118 const ch = s.source[t.start]!
1119 return ch === '>' || ch === '+' || ch === '~'
1120}
1121
1122function isColumnCombinator(s: ParserState): boolean {
1123 // `||` — two consecutive `|` Delim tokens.
1124 if (s.types[s.pos] !== TokenType.Delim)
1125 return false
1126 if (s.source.charCodeAt(s.starts[s.pos]!) !== 124 /* | */)
1127 return false
1128 if (s.types[s.pos + 1] !== TokenType.Delim)
1129 return false
1130 return s.source.charCodeAt(s.starts[s.pos + 1]!) === 124
1131}
1132
1133function parseSelectorSegment(s: ParserState): CssNode | null {
1134 const t = peek(s)
1135 switch (t.type) {
1136 case TokenType.Ident: {
1137 consume(s)
1138 // Namespace prefix? `svg|circle` → TypeSelector(name="svg|circle").
1139 // We keep the prefix as part of the name (matches css-tree shape).
1140 // Skip when the trailing `|` is part of `||` (column combinator) or
1141 // `|=` (attribute hyphen-match) — neither is a namespace separator.
1142 if (peekType(s) === TokenType.Delim && s.source[peek(s).start] === '|'
1143 && s.source[peek(s).start + 1] !== '='
1144 && !(s.types[s.pos + 1] === TokenType.Delim && s.source.charCodeAt(s.starts[s.pos + 1]!) === 124)) {
1145 consume(s)
1146 if (peekType(s) === TokenType.Ident) {
1147 const local = consume(s)
1148 const node: TypeSelector = {
1149 type: 'TypeSelector',
1150 name: `${tokenSlice(s, t)}|${tokenSlice(s, local)}`,
1151 loc: loc(s, t, local),
1152 }
1153 return node
1154 }
1155 if (peekType(s) === TokenType.Delim && s.source[peek(s).start] === '*') {
1156 const star = consume(s)
1157 const node: TypeSelector = {
1158 type: 'TypeSelector',
1159 name: `${tokenSlice(s, t)}|*`,
1160 loc: loc(s, t, star),
1161 }
1162 return node
1163 }
1164 }
1165 const node: TypeSelector = { type: 'TypeSelector', name: tokenSlice(s, t), loc: loc(s, t, t) }
1166 return node
1167 }
1168 // `@keyframes` rule preludes (`0%`, `100%`) parse as Percentage tokens.
1169 // Treat them as a TypeSelector so the round-trip preserves the literal.
1170 case TokenType.Percentage: {
1171 consume(s)
1172 const node: TypeSelector = { type: 'TypeSelector', name: tokenSlice(s, t), loc: loc(s, t, t) }
1173 return node
1174 }
1175 case TokenType.Number: {
1176 consume(s)
1177 const node: TypeSelector = { type: 'TypeSelector', name: tokenSlice(s, t), loc: loc(s, t, t) }
1178 return node
1179 }
1180 case TokenType.Delim: {
1181 const ch = s.source[t.start]!
1182 if (ch === '*') {
1183 consume(s)
1184 const node: TypeSelector = { type: 'TypeSelector', name: '*', loc: loc(s, t, t) }
1185 return node
1186 }
1187 if (ch === '.') {
1188 consume(s)
1189 if (peekType(s) === TokenType.Ident) {
1190 const id = consume(s)
1191 const node: ClassSelector = { type: 'ClassSelector', name: tokenSlice(s, id), loc: loc(s, t, id) }
1192 return node
1193 }
1194 return null
1195 }
1196 if (ch === '&') {
1197 consume(s)
1198 return { type: 'NestingSelector', loc: loc(s, t, t) }
1199 }
1200 consume(s)
1201 return null
1202 }
1203 case TokenType.Hash: {
1204 consume(s)
1205 const node: IdSelector = { type: 'IdSelector', name: s.source.slice(t.start + 1, t.end), loc: loc(s, t, t) }
1206 return node
1207 }
1208 case TokenType.Colon: {
1209 consume(s)
1210 const isElement = peekType(s) === TokenType.Colon
1211 if (isElement)
1212 consume(s)
1213 return parsePseudo(s, isElement, t)
1214 }
1215 case TokenType.LeftSquareBracket: {
1216 return parseAttribute(s)
1217 }
1218 }
1219 return null
1220}
1221
1222function parsePseudo(s: ParserState, isElement: boolean, startTok: Token): PseudoClassSelector | PseudoElementSelector | null {
1223 const t = peek(s)
1224 if (t.type === TokenType.Ident) {
1225 consume(s)
1226 const name = lowerIfNeeded(s.source, t.start, t.end)
1227 const node = isElement
1228 ? { type: 'PseudoElementSelector' as const, name, children: null, loc: loc(s, startTok, t) }
1229 : { type: 'PseudoClassSelector' as const, name, children: null, loc: loc(s, startTok, t) }
1230 return node
1231 }
1232 if (t.type === TokenType.Function) {
1233 consume(s)
1234 const name = lowerIfNeeded(s.source, t.start, t.end - 1)
1235 const children = newList<CssNode>()
1236
1237 // Pseudo-classes that take a selector list re-parse their argument.
1238 const isSelectorListPseudo = !isElement && SELECTOR_LIST_PSEUDOS_PARSE.has(name)
1239 if (isSelectorListPseudo) {
1240 // Slurp the argument tokens up to the matching `)` and re-parse them.
1241 const argStart = peek(s)
1242 let argEnd = argStart
1243 let depth = 1
1244 while (peekType(s) !== TokenType.EOF && depth > 0) {
1245 if (peekType(s) === TokenType.LeftParenthesis || peekType(s) === TokenType.Function)
1246 depth++
1247 else if (peekType(s) === TokenType.RightParenthesis) {
1248 depth--
1249 if (depth === 0)
1250 break
1251 }
1252 argEnd = consume(s)
1253 }
1254 if (peekType(s) === TokenType.RightParenthesis)
1255 consume(s)
1256 const inner = s.source.slice(argStart.start, argEnd.end).trim()
1257 if (inner.length > 0) {
1258 const sub = makeState(inner, { positions: false })
1259 const list = parseSelectorList(sub)
1260 children.appendData(list)
1261 }
1262 }
1263 else {
1264 while (peekType(s) !== TokenType.EOF && peekType(s) !== TokenType.RightParenthesis) {
1265 const inner = parseValueChild(s)
1266 if (inner)
1267 children.appendData(inner)
1268 }
1269 if (peekType(s) === TokenType.RightParenthesis)
1270 consume(s)
1271 }
1272
1273 if (isElement) {
1274 const node: PseudoElementSelector = { type: 'PseudoElementSelector', name, children, loc: loc(s, startTok, peek(s)) }
1275 return node
1276 }
1277 const node: PseudoClassSelector = { type: 'PseudoClassSelector', name, children, loc: loc(s, startTok, peek(s)) }
1278 return node
1279 }
1280 return null
1281}
1282
1283function parseAttribute(s: ParserState): AttributeSelector | null {
1284 const startTok = consume(s) // [
1285 skipWhitespace(s)
1286 const nameTok = peek(s)
1287 if (nameTok.type !== TokenType.Ident)
1288 return null
1289 consume(s)
1290 const name: Identifier = { type: 'Identifier', name: tokenSlice(s, nameTok), loc: loc(s, nameTok, nameTok) }
1291 skipWhitespace(s)
1292 let matcher: string | null = null
1293 let value: StringNode | Identifier | null = null
1294 let flags: string | null = null
1295 const next = peek(s)
1296 if (next.type === TokenType.Delim) {
1297 const ch = s.source[next.start]!
1298 const ch2 = s.source[next.start + 1]
1299 if (ch === '=') {
1300 matcher = '='
1301 consume(s)
1302 }
1303 else if ((ch === '~' || ch === '|' || ch === '^' || ch === '$' || ch === '*') && ch2 === '=') {
1304 // Two consecutive delim tokens
1305 consume(s)
1306 const eq = peek(s)
1307 if (eq.type === TokenType.Delim && s.source[eq.start] === '=') {
1308 consume(s)
1309 matcher = `${ch}=`
1310 }
1311 }
1312 skipWhitespace(s)
1313 const v = peek(s)
1314 if (v.type === TokenType.String) {
1315 consume(s)
1316 const closesProperly = v.end - v.start >= 2 && s.source.charCodeAt(v.end - 1) === s.source.charCodeAt(v.start)
1317 const text = decodeString(s.source, v.start + 1, closesProperly ? v.end - 1 : v.end)
1318 value = { type: 'String', value: text, loc: loc(s, v, v) }
1319 }
1320 else if (v.type === TokenType.Ident) {
1321 consume(s)
1322 value = { type: 'Identifier', name: tokenSlice(s, v), loc: loc(s, v, v) }
1323 }
1324 skipWhitespace(s)
1325 const flagTok = peek(s)
1326 if (flagTok.type === TokenType.Ident) {
1327 consume(s)
1328 flags = tokenSlice(s, flagTok)
1329 }
1330 }
1331 skipWhitespace(s)
1332 let endTok = peek(s)
1333 if (peekType(s) === TokenType.RightSquareBracket) {
1334 endTok = consume(s)
1335 }
1336 return { type: 'AttributeSelector', name, matcher, value, flags, loc: loc(s, startTok, endTok) }
1337}
1338
1339// expose some helpers for the public surface
1340export { TokenType }
packages/toolkit/src/parse/tokenizer.tsadded+708-0
Changes to packages/toolkit/src/parse/tokenizer.ts
@@ -0,0 +1,708 @@
1/**
2 * CSS Syntax Module Level 3 tokenizer.
3 *
4 * https://www.w3.org/TR/css-syntax-3/#tokenization
5 *
6 * Produces a flat sequence of token offsets that the parser walks. Tokens
7 * are recorded as parallel arrays (type + offset) rather than allocations
8 * — every other CSS lib does the same for performance.
9 */
10
11export const enum TokenType {
12 EOF = 0,
13 Ident = 1,
14 Function = 2,
15 AtKeyword = 3,
16 Hash = 4,
17 String = 5,
18 BadString = 6,
19 Url = 7,
20 BadUrl = 8,
21 Delim = 9,
22 Number = 10,
23 Percentage = 11,
24 Dimension = 12,
25 WhiteSpace = 13,
26 CDO = 14,
27 CDC = 15,
28 Colon = 16,
29 Semicolon = 17,
30 Comma = 18,
31 LeftSquareBracket = 19,
32 RightSquareBracket = 20,
33 LeftParenthesis = 21,
34 RightParenthesis = 22,
35 LeftCurlyBracket = 23,
36 RightCurlyBracket = 24,
37 Comment = 25,
38}
39
40export interface Token {
41 type: TokenType
42 start: number
43 end: number
44}
45
46const REPLACEMENT = 0xFFFD
47
48// ----- pre-built character-class lookup -----
49// One byte per char code (0-127). Each bit is a class:
50// bit 0 — whitespace (space/tab/CR/LF/FF)
51// bit 1 — digit (0-9)
52// bit 2 — hex digit (0-9 a-f A-F)
53// bit 3 — name-start (letter / underscore — non-ASCII handled separately)
54// bit 4 — newline (CR / LF / FF)
55const CHAR_CLASS = (() => {
56 const t = new Uint8Array(128)
57 for (let c = 0; c < 128; c++) {
58 let bits = 0
59 if (c === 32 || c === 9 || c === 10 || c === 12 || c === 13)
60 bits |= 1
61 if (c >= 48 && c <= 57)
62 bits |= 2
63 if ((c >= 48 && c <= 57) || (c >= 65 && c <= 70) || (c >= 97 && c <= 102))
64 bits |= 4
65 if ((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 95)
66 bits |= 8
67 if (c === 10 || c === 12 || c === 13)
68 bits |= 16
69 t[c] = bits
70 }
71 return t
72})()
73
74function isDigit(code: number): boolean {
75 return code < 128 && (CHAR_CLASS[code]! & 2) !== 0
76}
77
78function isHexDigit(code: number): boolean {
79 return code < 128 && (CHAR_CLASS[code]! & 4) !== 0
80}
81
82function isNameStart(code: number): boolean {
83 return code >= 0x80 || (code < 128 && (CHAR_CLASS[code]! & 8) !== 0)
84}
85
86function isName(code: number): boolean {
87 return code === 45 /* - */ || code >= 0x80 || (code < 128 && (CHAR_CLASS[code]! & (2 | 8)) !== 0)
88}
89
90function isNonPrintable(code: number): boolean {
91 return (code >= 0 && code <= 8) || code === 11 || (code >= 14 && code <= 31) || code === 127
92}
93
94function isNewline(code: number): boolean {
95 return code < 128 && (CHAR_CLASS[code]! & 16) !== 0
96}
97
98function isWhitespace(code: number): boolean {
99 return code < 128 && (CHAR_CLASS[code]! & 1) !== 0
100}
101
102function isValidEscape(c1: number, c2: number): boolean {
103 if (c1 !== 92 /* \ */)
104 return false
105 if (isNewline(c2))
106 return false
107 return true
108}
109
110function startsIdentifier(c1: number, c2: number, c3: number): boolean {
111 if (c1 === 45 /* - */) {
112 return isNameStart(c2) || c2 === 45 || isValidEscape(c2, c3)
113 }
114 if (isNameStart(c1))
115 return true
116 if (c1 === 92 /* \ */)
117 return isValidEscape(c1, c2)
118 return false
119}
120
121function startsNumber(c1: number, c2: number, c3: number): boolean {
122 if (c1 === 43 /* + */ || c1 === 45 /* - */) {
123 if (isDigit(c2))
124 return true
125 if (c2 === 46 /* . */ && isDigit(c3))
126 return true
127 return false
128 }
129 if (c1 === 46 /* . */)
130 return isDigit(c2)
131 return isDigit(c1)
132}
133
134export class Tokenizer {
135 source: string
136 offset = 0
137 /**
138 * Parallel typed-array storage. The parser walks `count` tokens by
139 * index; `types[i]` / `starts[i]` / `ends[i]` are the three fields of
140 * the i-th token. This avoids one `Object` allocation per token, which
141 * matters a lot on parse — for a 6 KB stylesheet there are ~1500 tokens.
142 */
143 types: Uint8Array
144 starts: Uint32Array
145 ends: Uint32Array
146 count = 0
147 /** Lazy-built `Token`-shaped array — only filled if a consumer
148 * reads `.tokens`. Internally the parser uses the typed arrays. */
149 private _tokens: Token[] | null = null
150 /**
151 * Offset where each line starts; used for line/col reporting. Built
152 * lazily by `locate()` — almost every parse runs with `positions: false`
153 * and never asks for source locations, so paying the per-newline
154 * `lineStarts.push()` during tokenization is pure overhead in the
155 * common case. The closures the original loops captured for that push
156 * are also gone.
157 */
158 private _lineStarts: number[] | null = null
159
160 constructor(source: string) {
161 this.source = source
162 // CSS averages ~3.5 chars/token across realistic stylesheets (selectors,
163 // declarations, values, whitespace). Allocating `len/3` covers nearly
164 // all inputs without needing `grow()`. Floor at 64 for tiny snippets.
165 const cap = Math.max(64, Math.ceil(source.length / 3))
166 this.types = new Uint8Array(cap)
167 this.starts = new Uint32Array(cap)
168 this.ends = new Uint32Array(cap)
169 this.tokenize()
170 }
171
172 /** Public alias kept for back-compat — built lazily on first read. */
173 get lineStarts(): number[] {
174 if (this._lineStarts == null)
175 this._lineStarts = buildLineStarts(this.source)
176 return this._lineStarts
177 }
178
179 /** Lazy `Token[]` view for backwards-compat. */
180 get tokens(): Token[] {
181 if (this._tokens != null)
182 return this._tokens
183 const out: Token[] = Array.from({ length: this.count })
184 for (let i = 0; i < this.count; i++)
185 out[i] = { type: this.types[i]! as TokenType, start: this.starts[i]!, end: this.ends[i]! }
186 this._tokens = out
187 return out
188 }
189
190 private addToken(type: TokenType, start: number, end: number): void {
191 if (this.count >= this.types.length)
192 this.grow()
193 this.types[this.count] = type
194 this.starts[this.count] = start
195 this.ends[this.count] = end
196 this.count++
197 }
198
199 private grow(): void {
200 const oldLen = this.types.length
201 const newLen = oldLen * 2
202 // eslint-disable-next-line pickier/no-unused-vars
203 const t = new Uint8Array(newLen); t.set(this.types); this.types = t
204 // eslint-disable-next-line pickier/no-unused-vars
205 const s = new Uint32Array(newLen); s.set(this.starts); this.starts = s
206 // eslint-disable-next-line pickier/no-unused-vars
207 const e = new Uint32Array(newLen); e.set(this.ends); this.ends = e
208 }
209
210 private tokenize(): void {
211 const src = this.source
212 let i = 0
213 const len = src.length
214
215 while (i < len) {
216 const c1 = src.charCodeAt(i)
217
218 // comment /* ... */
219 if (c1 === 47 /* / */ && src.charCodeAt(i + 1) === 42 /* * */) {
220 const start = i
221 i += 2
222 while (i < len && !(src.charCodeAt(i) === 42 && src.charCodeAt(i + 1) === 47))
223 i++
224 i = i < len ? i + 2 : len
225 this.addToken(TokenType.Comment, start, i)
226 continue
227 }
228
229 // whitespace
230 if (isWhitespace(c1)) {
231 const start = i
232 while (i < len && isWhitespace(src.charCodeAt(i)))
233 i++
234 this.addToken(TokenType.WhiteSpace, start, i)
235 continue
236 }
237
238 // string
239 if (c1 === 34 /* " */ || c1 === 39 /* ' */) {
240 i = this.consumeString(c1, i)
241 continue
242 }
243
244 // hash
245 if (c1 === 35 /* # */) {
246 if (i + 1 < len && (isName(src.charCodeAt(i + 1)) || isValidEscape(src.charCodeAt(i + 1), src.charCodeAt(i + 2)))) {
247 const start = i
248 i++
249 i = this.consumeName(i)
250 this.addToken(TokenType.Hash, start, i)
251 continue
252 }
253 this.addToken(TokenType.Delim, i, i + 1)
254 i++
255 continue
256 }
257
258 // ( ) [ ] { } , : ;
259 switch (c1) {
260 case 40: this.addToken(TokenType.LeftParenthesis, i, i + 1); i++; continue
261 case 41: this.addToken(TokenType.RightParenthesis, i, i + 1); i++; continue
262 case 91: this.addToken(TokenType.LeftSquareBracket, i, i + 1); i++; continue
263 case 93: this.addToken(TokenType.RightSquareBracket, i, i + 1); i++; continue
264 case 123: this.addToken(TokenType.LeftCurlyBracket, i, i + 1); i++; continue
265 case 125: this.addToken(TokenType.RightCurlyBracket, i, i + 1); i++; continue
266 case 44: this.addToken(TokenType.Comma, i, i + 1); i++; continue
267 case 58: this.addToken(TokenType.Colon, i, i + 1); i++; continue
268 case 59: this.addToken(TokenType.Semicolon, i, i + 1); i++; continue
269 }
270
271 // CDO/CDC
272 if (c1 === 60 /* < */ && src.startsWith('!--', i + 1)) {
273 this.addToken(TokenType.CDO, i, i + 4)
274 i += 4
275 continue
276 }
277 if (c1 === 45 /* - */ && src.startsWith('->', i + 1)) {
278 this.addToken(TokenType.CDC, i, i + 3)
279 i += 3
280 continue
281 }
282
283 // @ keyword
284 if (c1 === 64 /* @ */) {
285 const c2 = src.charCodeAt(i + 1)
286 const c3 = src.charCodeAt(i + 2)
287 const c4 = src.charCodeAt(i + 3)
288 if (startsIdentifier(c2, c3, c4)) {
289 const start = i
290 i++
291 i = this.consumeName(i)
292 this.addToken(TokenType.AtKeyword, start, i)
293 continue
294 }
295 this.addToken(TokenType.Delim, i, i + 1)
296 i++
297 continue
298 }
299
300 // number / percentage / dimension
301 const c2 = src.charCodeAt(i + 1)
302 const c3 = src.charCodeAt(i + 2)
303 if (startsNumber(c1, c2, c3)) {
304 i = this.consumeNumeric(i)
305 continue
306 }
307
308 // ident-like (function, url, ident)
309 if (startsIdentifier(c1, c2, c3)) {
310 i = this.consumeIdentLike(i)
311 continue
312 }
313
314 // \\ escape
315 if (c1 === 92 /* \ */) {
316 if (isValidEscape(c1, c2)) {
317 i = this.consumeIdentLike(i)
318 continue
319 }
320 this.addToken(TokenType.Delim, i, i + 1)
321 i++
322 continue
323 }
324
325 // anything else: delim
326 this.addToken(TokenType.Delim, i, i + 1)
327 i++
328 }
329 this.addToken(TokenType.EOF, len, len)
330 }
331
332 private consumeString(quote: number, start: number): number {
333 const src = this.source
334 const len = src.length
335 let i = start + 1
336 while (i < len) {
337 const c = src.charCodeAt(i)
338 if (c === quote) {
339 i++
340 this.addToken(TokenType.String, start, i)
341 return i
342 }
343 if (isNewline(c)) {
344 this.addToken(TokenType.BadString, start, i)
345 return i
346 }
347 if (c === 92 /* \ */) {
348 const c2 = src.charCodeAt(i + 1)
349 if (isNewline(c2)) {
350 i += 2
351 continue
352 }
353 if (i + 1 < len) {
354 i = this.consumeEscapeSkip(i + 1)
355 continue
356 }
357 }
358 i++
359 }
360 this.addToken(TokenType.String, start, i)
361 return i
362 }
363
364 private consumeEscapeSkip(i: number): number {
365 const src = this.source
366 if (i >= src.length)
367 return i
368 const c = src.charCodeAt(i)
369 if (isHexDigit(c)) {
370 let n = 1
371 i++
372 while (n < 6 && i < src.length && isHexDigit(src.charCodeAt(i))) {
373 i++
374 n++
375 }
376 if (i < src.length && isWhitespace(src.charCodeAt(i)))
377 i++
378 return i
379 }
380 return i + 1
381 }
382
383 private consumeName(start: number): number {
384 const src = this.source
385 const len = src.length
386 let i = start
387 // Fast path: pure ASCII name chars — no escape, no non-ASCII. Inline
388 // `isName` so the JIT keeps the loop body in a single branch path.
389 while (i < len) {
390 const c = src.charCodeAt(i)
391 if (c < 128 && (CHAR_CLASS[c]! & (2 | 8)) !== 0) {
392 i++
393 continue
394 }
395 if (c === 45 /* - */) {
396 i++
397 continue
398 }
399 // Slow path: non-ASCII or escape — fall back to the general check.
400 if (c >= 0x80) {
401 i++
402 continue
403 }
404 if (c === 92 /* \ */ && isValidEscape(c, src.charCodeAt(i + 1))) {
405 i = this.consumeEscapeSkip(i + 1)
406 continue
407 }
408 break
409 }
410 return i
411 }
412
413 private consumeNumber(start: number): number {
414 const src = this.source
415 let i = start
416 if (src.charCodeAt(i) === 43 || src.charCodeAt(i) === 45)
417 i++
418 while (i < src.length && isDigit(src.charCodeAt(i)))
419 i++
420 if (src.charCodeAt(i) === 46 && isDigit(src.charCodeAt(i + 1))) {
421 i += 2
422 while (i < src.length && isDigit(src.charCodeAt(i)))
423 i++
424 }
425 const eC = src.charCodeAt(i)
426 if (eC === 69 || eC === 101) {
427 const next = src.charCodeAt(i + 1)
428 const next2 = src.charCodeAt(i + 2)
429 if (isDigit(next)) {
430 i += 2
431 while (i < src.length && isDigit(src.charCodeAt(i)))
432 i++
433 }
434 else if ((next === 43 || next === 45) && isDigit(next2)) {
435 i += 3
436 while (i < src.length && isDigit(src.charCodeAt(i)))
437 i++
438 }
439 }
440 return i
441 }
442
443 private consumeNumeric(start: number): number {
444 const src = this.source
445 const numEnd = this.consumeNumber(start)
446 const c1 = src.charCodeAt(numEnd)
447 const c2 = src.charCodeAt(numEnd + 1)
448 const c3 = src.charCodeAt(numEnd + 2)
449
450 if (startsIdentifier(c1, c2, c3)) {
451 const end = this.consumeName(numEnd)
452 this.addToken(TokenType.Dimension, start, end)
453 return end
454 }
455 if (c1 === 37 /* % */) {
456 this.addToken(TokenType.Percentage, start, numEnd + 1)
457 return numEnd + 1
458 }
459 this.addToken(TokenType.Number, start, numEnd)
460 return numEnd
461 }
462
463 private consumeIdentLike(start: number): number {
464 const src = this.source
465 const nameEnd = this.consumeName(start)
466 const c = src.charCodeAt(nameEnd)
467 // Only `url` (3 chars) needs special handling; avoid the
468 // `slice(...).toLowerCase()` allocation per ident in the hot path.
469 if (c === 40 /* ( */ && nameEnd - start === 3) {
470 const c0 = src.charCodeAt(start) | 32
471 const c1 = src.charCodeAt(start + 1) | 32
472 const c2 = src.charCodeAt(start + 2) | 32
473 if (c0 === 117 && c1 === 114 && c2 === 108) {
474 let i = nameEnd + 1
475 while (i < src.length && isWhitespace(src.charCodeAt(i)))
476 i++
477 const next = src.charCodeAt(i)
478 if (next === 34 || next === 39) {
479 // function url("...")
480 this.addToken(TokenType.Function, start, nameEnd + 1)
481 return nameEnd + 1
482 }
483 return this.consumeUrl(start, nameEnd)
484 }
485 }
486 if (c === 40 /* ( */) {
487 this.addToken(TokenType.Function, start, nameEnd + 1)
488 return nameEnd + 1
489 }
490 this.addToken(TokenType.Ident, start, nameEnd)
491 return nameEnd
492 }
493
494 private consumeUrl(start: number, nameEnd: number): number {
495 const src = this.source
496 const len = src.length
497 let i = nameEnd + 1
498 while (i < len && isWhitespace(src.charCodeAt(i)))
499 i++
500 while (i < len) {
501 const c = src.charCodeAt(i)
502 if (c === 41 /* ) */) {
503 i++
504 this.addToken(TokenType.Url, start, i)
505 return i
506 }
507 if (isWhitespace(c)) {
508 const wsStart = i
509 while (i < len && isWhitespace(src.charCodeAt(i)))
510 i++
511 if (i >= len) {
512 this.addToken(TokenType.Url, start, i)
513 return i
514 }
515 if (src.charCodeAt(i) === 41) {
516 i++
517 this.addToken(TokenType.Url, start, i)
518 return i
519 }
520 // bad url — recover by consuming to next ) or eof
521 return this.consumeBadUrl(start, wsStart)
522 }
523 if (c === 34 || c === 39 || c === 40 || isNonPrintable(c)) {
524 return this.consumeBadUrl(start, i)
525 }
526 if (c === 92 /* \ */) {
527 if (isValidEscape(c, src.charCodeAt(i + 1))) {
528 i = this.consumeEscapeSkip(i + 1)
529 continue
530 }
531 return this.consumeBadUrl(start, i)
532 }
533 i++
534 }
535 this.addToken(TokenType.Url, start, i)
536 return i
537 }
538
539 private consumeBadUrl(start: number, from: number): number {
540 const src = this.source
541 let i = from
542 while (i < src.length) {
543 const c = src.charCodeAt(i)
544 if (c === 41) {
545 i++
546 break
547 }
548 if (c === 92 && isValidEscape(c, src.charCodeAt(i + 1))) {
549 i = this.consumeEscapeSkip(i + 1)
550 continue
551 }
552 i++
553 }
554 this.addToken(TokenType.BadUrl, start, i)
555 return i
556 }
557
558 /** Resolve `offset` to (line, column) for error messages. */
559 locate(offset: number): { line: number, column: number } {
560 const ls = this.lineStarts
561 let lo = 0
562 let hi = ls.length - 1
563 while (lo < hi) {
564 const mid = (lo + hi + 1) >> 1
565 if (ls[mid]! <= offset)
566 lo = mid
567 else
568 hi = mid - 1
569 }
570 const lineStart = ls[lo]!
571 return { line: lo + 1, column: offset - lineStart + 1 }
572 }
573}
574
575function buildLineStarts(source: string): number[] {
576 const out = [0]
577 const len = source.length
578 for (let i = 0; i < len; i++) {
579 const c = source.charCodeAt(i)
580 if (c === 10) {
581 out.push(i + 1)
582 }
583 else if (c === 13) {
584 // \r without \n, OR \r\n (treat the LF position consistently)
585 if (source.charCodeAt(i + 1) !== 10)
586 out.push(i + 1)
587 }
588 }
589 return out
590}
591
592/** Decode a CSS-escaped name slice (e.g. `\\26 B` → `&B`). */
593/**
594 * Decode a CSS string literal slice to its actual character value.
595 * `start` / `end` exclude the surrounding quote characters.
596 *
597 * Handles:
598 * - hex escapes (`\26 ` → `&`)
599 * - `\"` / `\'` / `\\`
600 * - escaped newlines (CSS: backslash followed by newline is removed)
601 * - any other `\<char>` → `<char>`
602 */
603export function decodeString(source: string, start: number, end: number): string {
604 // Fast path: no escapes → return the slice directly. The vast majority of
605 // CSS strings/identifiers have no backslash escapes, so a one-pass scan
606 // saves us the per-char concat allocation that the slow path does.
607 for (let k = start; k < end; k++) {
608 if (source.charCodeAt(k) === 92 /* \\ */)
609 return decodeStringSlow(source, start, end, k)
610 }
611 return source.slice(start, end)
612}
613
614function decodeStringSlow(source: string, start: number, end: number, firstEscape: number): string {
615 let out = source.slice(start, firstEscape)
616 let i = firstEscape
617 while (i < end) {
618 const c = source.charCodeAt(i)
619 if (c !== 92 /* \\ */) {
620 out += source[i]!
621 i++
622 continue
623 }
624 // backslash at end-of-input
625 if (i + 1 >= end) {
626 i++
627 continue
628 }
629 // backslash + newline = continuation (removed entirely)
630 const next = source.charCodeAt(i + 1)
631 if (next === 10) {
632 i += 2
633 continue
634 }
635 if (next === 13) {
636 i += source.charCodeAt(i + 2) === 10 ? 3 : 2
637 continue
638 }
639 // hex escape
640 if (isHexDigit(next)) {
641 let j = i + 1
642 let hex = ''
643 while (hex.length < 6 && j < end && isHexDigit(source.charCodeAt(j))) {
644 hex += source[j]
645 j++
646 }
647 const code = Number.parseInt(hex, 16)
648 if (j < end && isWhitespace(source.charCodeAt(j)))
649 j++
650 out += code === 0 || (code >= 0xD800 && code <= 0xDFFF) || code > 0x10FFFF
651 ? String.fromCodePoint(REPLACEMENT)
652 : String.fromCodePoint(code)
653 i = j
654 continue
655 }
656 // escape of any other char (including \" \' \\) — keep the char
657 out += source[i + 1]
658 i += 2
659 }
660 return out
661}
662
663export function decodeName(source: string, start: number, end: number): string {
664 // Fast path: no escapes → return the slice directly.
665 for (let k = start; k < end; k++) {
666 if (source.charCodeAt(k) === 92 /* \\ */)
667 return decodeNameSlow(source, start, end, k)
668 }
669 return source.slice(start, end)
670}
671
672function decodeNameSlow(source: string, start: number, end: number, firstEscape: number): string {
673 let out = source.slice(start, firstEscape)
674 let i = firstEscape
675 while (i < end) {
676 const c = source.charCodeAt(i)
677 if (c !== 92 /* \\ */) {
678 out += source[i]!
679 i++
680 continue
681 }
682 // hex escape
683 let j = i + 1
684 let hex = ''
685 while (hex.length < 6 && j < end && isHexDigit(source.charCodeAt(j))) {
686 hex += source[j]
687 j++
688 }
689 if (hex.length > 0) {
690 const code = Number.parseInt(hex, 16)
691 if (j < end && isWhitespace(source.charCodeAt(j)))
692 j++
693 out += code === 0 || (code >= 0xD800 && code <= 0xDFFF) || code > 0x10FFFF
694 ? String.fromCodePoint(REPLACEMENT)
695 : String.fromCodePoint(code)
696 i = j
697 continue
698 }
699 if (j < end) {
700 out += source[j]
701 i = j + 1
702 continue
703 }
704 out += String.fromCodePoint(REPLACEMENT)
705 i = j
706 }
707 return out
708}
packages/toolkit/src/parse/types.tsadded+344-0
Changes to packages/toolkit/src/parse/types.ts
@@ -0,0 +1,344 @@
1/**
2 * AST node types for the CSS parser.
3 *
4 * Mirrors the css-tree v3 node taxonomy so existing consumers (CSSO,
5 * SVGO-style optimisers, etc.) can swap to ts-css without touching their
6 * walker callbacks. Node shapes are intentionally narrow — we don't
7 * model spec-aware lexer types (no `<color>` / `<length>` validation),
8 * because the SVGO/csso pipelines that we target don't use them.
9 */
10
11import type { CssList } from './list'
12
13export type CssNodeType
14 = | 'AnPlusB'
15 | 'Atrule'
16 | 'AtrulePrelude'
17 | 'AttributeSelector'
18 | 'Block'
19 | 'Brackets'
20 | 'CDC'
21 | 'CDO'
22 | 'ClassSelector'
23 | 'Combinator'
24 | 'Comment'
25 | 'Declaration'
26 | 'DeclarationList'
27 | 'Dimension'
28 | 'Function'
29 | 'Hash'
30 | 'IdSelector'
31 | 'Identifier'
32 | 'MediaFeature'
33 | 'MediaQuery'
34 | 'MediaQueryList'
35 | 'NestingSelector'
36 | 'Nth'
37 | 'Number'
38 | 'Operator'
39 | 'Parentheses'
40 | 'Percentage'
41 | 'PseudoClassSelector'
42 | 'PseudoElementSelector'
43 | 'Ratio'
44 | 'Raw'
45 | 'Rule'
46 | 'Selector'
47 | 'SelectorList'
48 | 'StyleSheet'
49 | 'TypeSelector'
50 | 'UnicodeRange'
51 | 'Url'
52 | 'Value'
53 | 'WhiteSpace'
54 | 'String'
55
56export interface BaseNode {
57 type: CssNodeType
58 loc?: CssLocation | null
59}
60
61export interface CssLocation {
62 source: string
63 start: { offset: number, line: number, column: number }
64 end: { offset: number, line: number, column: number }
65}
66
67// ----- Containers -----
68
69export interface StyleSheet extends BaseNode {
70 type: 'StyleSheet'
71 children: CssList<CssNode>
72}
73
74export interface Rule extends BaseNode {
75 type: 'Rule'
76 prelude: SelectorList | Raw
77 block: Block
78}
79
80export interface Atrule extends BaseNode {
81 type: 'Atrule'
82 name: string
83 prelude: AtrulePrelude | Raw | null
84 block: Block | null
85}
86
87export interface AtrulePrelude extends BaseNode {
88 type: 'AtrulePrelude'
89 children: CssList<CssNode>
90}
91
92export interface Block extends BaseNode {
93 type: 'Block'
94 children: CssList<CssNode>
95}
96
97// ----- Declarations -----
98
99export interface Declaration extends BaseNode {
100 type: 'Declaration'
101 /**
102 * `true` when the declaration ends with `!important`. The string-form
103 * (e.g. `'!ie'` for IE-prefix hacks) is occasionally produced by other
104 * libraries, so the type stays permissive — but ts-css's parser always
105 * sets `boolean`.
106 */
107 important: boolean | string
108 property: string
109 value: Value | Raw
110}
111
112export interface DeclarationList extends BaseNode {
113 type: 'DeclarationList'
114 children: CssList<CssNode>
115}
116
117export interface Value extends BaseNode {
118 type: 'Value'
119 children: CssList<CssNode>
120}
121
122// ----- Selectors -----
123
124export interface SelectorList extends BaseNode {
125 type: 'SelectorList'
126 children: CssList<CssNode>
127}
128
129export interface Selector extends BaseNode {
130 type: 'Selector'
131 children: CssList<CssNode>
132}
133
134export interface TypeSelector extends BaseNode {
135 type: 'TypeSelector'
136 name: string
137}
138
139export interface IdSelector extends BaseNode {
140 type: 'IdSelector'
141 name: string
142}
143
144export interface ClassSelector extends BaseNode {
145 type: 'ClassSelector'
146 name: string
147}
148
149export interface AttributeSelector extends BaseNode {
150 type: 'AttributeSelector'
151 name: Identifier
152 matcher: string | null
153 value: StringNode | Identifier | null
154 flags: string | null
155}
156
157export interface PseudoClassSelector extends BaseNode {
158 type: 'PseudoClassSelector'
159 name: string
160 children: CssList<CssNode> | null
161}
162
163export interface PseudoElementSelector extends BaseNode {
164 type: 'PseudoElementSelector'
165 name: string
166 children: CssList<CssNode> | null
167}
168
169export interface Combinator extends BaseNode {
170 type: 'Combinator'
171 name: string
172}
173
174export interface NestingSelector extends BaseNode {
175 type: 'NestingSelector'
176}
177
178// ----- Atomic values -----
179
180export interface Identifier extends BaseNode {
181 type: 'Identifier'
182 name: string
183}
184
185export interface NumberNode extends BaseNode {
186 type: 'Number'
187 value: string
188}
189
190export interface PercentageNode extends BaseNode {
191 type: 'Percentage'
192 value: string
193}
194
195export interface DimensionNode extends BaseNode {
196 type: 'Dimension'
197 value: string
198 unit: string
199}
200
201export interface StringNode extends BaseNode {
202 type: 'String'
203 value: string
204}
205
206export interface UrlNode extends BaseNode {
207 type: 'Url'
208 value: string
209}
210
211export interface HashNode extends BaseNode {
212 type: 'Hash'
213 name: string
214}
215
216export interface OperatorNode extends BaseNode {
217 type: 'Operator'
218 value: string
219}
220
221export interface FunctionNode extends BaseNode {
222 type: 'Function'
223 name: string
224 children: CssList<CssNode>
225}
226
227export interface ParenthesesNode extends BaseNode {
228 type: 'Parentheses'
229 children: CssList<CssNode>
230}
231
232export interface BracketsNode extends BaseNode {
233 type: 'Brackets'
234 children: CssList<CssNode>
235}
236
237export interface RawNode extends BaseNode {
238 type: 'Raw'
239 value: string
240}
241
242export interface CommentNode extends BaseNode {
243 type: 'Comment'
244 value: string
245}
246
247export interface WhiteSpaceNode extends BaseNode {
248 type: 'WhiteSpace'
249 value: string
250}
251
252export interface CDONode extends BaseNode { type: 'CDO' }
253export interface CDCNode extends BaseNode { type: 'CDC' }
254
255export interface NthNode extends BaseNode {
256 type: 'Nth'
257 nth: AnPlusBNode | Identifier
258 selector: SelectorList | null
259}
260
261export interface AnPlusBNode extends BaseNode {
262 type: 'AnPlusB'
263 a: string | null
264 b: string | null
265}
266
267export interface RatioNode extends BaseNode {
268 type: 'Ratio'
269 left: NumberNode
270 right: NumberNode
271}
272
273export interface UnicodeRangeNode extends BaseNode {
274 type: 'UnicodeRange'
275 value: string
276}
277
278export interface MediaQueryListNode extends BaseNode {
279 type: 'MediaQueryList'
280 children: CssList<CssNode>
281}
282
283export interface MediaQueryNode extends BaseNode {
284 type: 'MediaQuery'
285 children: CssList<CssNode>
286}
287
288export interface MediaFeatureNode extends BaseNode {
289 type: 'MediaFeature'
290 name: string
291 value: NumberNode | DimensionNode | RatioNode | Identifier | null
292}
293
294// ----- Aliases -----
295
296export type Raw = RawNode
297
298export type CssNode
299 = | StyleSheet | Rule | Atrule | AtrulePrelude | Block
300 | Declaration | DeclarationList | Value
301 | SelectorList | Selector | TypeSelector | IdSelector | ClassSelector
302 | AttributeSelector | PseudoClassSelector | PseudoElementSelector
303 | Combinator | NestingSelector
304 | Identifier | NumberNode | PercentageNode | DimensionNode
305 | StringNode | UrlNode | HashNode | OperatorNode
306 | FunctionNode | ParenthesesNode | BracketsNode
307 | RawNode | CommentNode | WhiteSpaceNode | CDONode | CDCNode
308 | NthNode | AnPlusBNode | RatioNode | UnicodeRangeNode
309 | MediaQueryListNode | MediaQueryNode | MediaFeatureNode
310
311export type ParseContext
312 = | 'stylesheet'
313 | 'atrule'
314 | 'atrulePrelude'
315 | 'mediaQuery'
316 | 'mediaQueryList'
317 | 'rule'
318 | 'selectorList'
319 | 'selector'
320 | 'block'
321 | 'declarationList'
322 | 'declaration'
323 | 'value'
324 | 'raw'
325
326export interface ParseOptions {
327 /** Source filename used in error/loc reports. */
328 filename?: string
329 /** Track source locations on every node. */
330 positions?: boolean
331 /** Grammar context to parse `source` as (default `'stylesheet'`). */
332 context?: ParseContext
333 /** When false, declaration values are stored as `Raw` rather than parsed. */
334 parseValue?: boolean
335 /** When false, at-rule preludes are stored as `Raw` rather than parsed. */
336 parseAtrulePrelude?: boolean
337 /** When false, custom-property values are stored as `Raw`. */
338 parseCustomProperty?: boolean
339 /** When false, rule preludes (selector lists) are stored as `Raw`. */
340 parseRulePrelude?: boolean
341 /** Forwarded to consumer's error handler. */
342 // eslint-disable-next-line pickier/no-unused-vars
343 onParseError?: (error: SyntaxError, fallbackNode: CssNode) => void
344}
packages/toolkit/src/parse/walker.tsadded+252-0
Changes to packages/toolkit/src/parse/walker.ts
@@ -0,0 +1,252 @@
1/**
2 * Tree-walker for the CSS AST. Mirrors css-tree's `walk(ast, callback)` and
3 * `walk(ast, { visit, enter, leave })` shapes — including the `walk.skip`
4 * sentinel and the `this.atrule` / `this.rule` context exposed inside
5 * callbacks.
6 */
7
8import type { Atrule, CssNode, Declaration, Rule } from './types'
9import { CssList, type ListItem } from './list'
10
11const SKIP: symbol = Symbol('walkSkip')
12const STOP: symbol = Symbol('walkStop')
13
14export interface WalkContext {
15 root: CssNode
16 stylesheet: CssNode | null
17 atrule: Atrule | null
18 atrulePrelude: CssNode | null
19 rule: Rule | null
20 selector: CssNode | null
21 block: CssNode | null
22 declaration: Declaration | null
23 function: CssNode | null
24}
25
26export type WalkVisit = CssNode['type']
27
28// Narrow CssNode by its `type` discriminant — used to type the
29// `visit: 'Rule'` filter so consumer callbacks receive a `Rule`, not the
30// full union.
31export type CssNodeOfType<T extends WalkVisit> = Extract<CssNode, { type: T }>
32
33// eslint-disable-next-line pickier/no-unused-vars
34type VisitorCallback<N> = (this: WalkContext, node: N, item: ListItem<CssNode> | null, list: CssList<CssNode> | null) => void | symbol
35
36/**
37 * Object-form visitor. The `T extends WalkVisit` generic is inferred from
38 * the literal value of `visit`, so writing
39 *
40 * walk(ast, { visit: 'Rule', enter(node) { … } })
41 *
42 * narrows `node` to `Rule`. Without `visit`, callbacks receive the full
43 * `CssNode` union as `enter`/`leave` arguments.
44 */
45export interface WalkVisitorObject<T extends WalkVisit | undefined = undefined> {
46 visit?: T
47 reverse?: boolean
48 enter?: VisitorCallback<T extends WalkVisit ? CssNodeOfType<T> : CssNode>
49 leave?: VisitorCallback<T extends WalkVisit ? CssNodeOfType<T> : CssNode>
50}
51
52/** Plain-function visitor — runs against every node. */
53export type WalkVisitorFunction = VisitorCallback<CssNode>
54
55export type WalkVisitor = WalkVisitorFunction | WalkVisitorObject<WalkVisit | undefined>
56
57function newContext(root: CssNode): WalkContext {
58 return {
59 root,
60 stylesheet: null,
61 atrule: null,
62 atrulePrelude: null,
63 rule: null,
64 selector: null,
65 block: null,
66 declaration: null,
67 function: null,
68 }
69}
70
71/**
72 * Walk every node of `root`, calling `visitor` (or visitor.enter / .leave).
73 * Returns:
74 * - undefined when traversal completes,
75 * - the SKIP / STOP sentinel propagated from a visitor.
76 *
77 * `walk.skip` / `walk.stop` are exposed as static-symbol sentinels.
78 */
79export interface WalkFn {
80 // 1. visitor function — every node (no narrowing)
81 // eslint-disable-next-line pickier/no-unused-vars
82 (root: CssNode, visitor: WalkVisitorFunction): symbol | undefined
83 // 2. visitor object with `visit: '<NodeType>'` — narrowed callbacks
84 // eslint-disable-next-line pickier/no-unused-vars
85 <T extends WalkVisit>(root: CssNode, visitor: WalkVisitorObject<T>): symbol | undefined
86 skip: symbol
87 stop: symbol
88}
89
90function walkImpl(root: CssNode, visitor: WalkVisitor): symbol | undefined {
91 const ctx = newContext(root)
92 const enter = typeof visitor === 'function' ? visitor : visitor.enter
93 const leave = typeof visitor === 'function' ? null : (visitor.leave ?? null)
94 const filter = typeof visitor === 'function' ? null : (visitor.visit ?? null)
95 const reverse = typeof visitor === 'function' ? false : (visitor.reverse ?? false)
96
97 function visitNode(node: CssNode, item: ListItem<CssNode> | null, list: CssList<CssNode> | null): symbol | undefined {
98 let pushed: keyof WalkContext | null = null
99 let prev: any
100 switch (node.type) {
101 case 'StyleSheet':
102 pushed = 'stylesheet'
103 prev = ctx.stylesheet
104 ctx.stylesheet = node
105 break
106 case 'Atrule':
107 pushed = 'atrule'
108 prev = ctx.atrule
109 ctx.atrule = node
110 break
111 case 'AtrulePrelude':
112 pushed = 'atrulePrelude'
113 prev = ctx.atrulePrelude
114 ctx.atrulePrelude = node
115 break
116 case 'Rule':
117 pushed = 'rule'
118 prev = ctx.rule
119 ctx.rule = node
120 break
121 case 'Selector':
122 pushed = 'selector'
123 prev = ctx.selector
124 ctx.selector = node
125 break
126 case 'Block':
127 pushed = 'block'
128 prev = ctx.block
129 ctx.block = node
130 break
131 case 'Declaration':
132 pushed = 'declaration'
133 prev = ctx.declaration
134 ctx.declaration = node
135 break
136 case 'Function':
137 pushed = 'function'
138 prev = ctx.function
139 ctx.function = node
140 break
141 }
142
143 let result: symbol | undefined
144 if ((filter === null || filter === node.type) && enter) {
145 const r = enter.call(ctx, node, item, list)
146 if (r === SKIP) {
147 if (pushed) (ctx as any)[pushed] = prev
148 return undefined
149 }
150 if (r === STOP) {
151 if (pushed) (ctx as any)[pushed] = prev
152 return STOP
153 }
154 }
155
156 // descend
157 result = walkChildren(node, reverse, visitNode)
158 if (result === STOP) {
159 if (pushed) (ctx as any)[pushed] = prev
160 return STOP
161 }
162
163 if ((filter === null || filter === node.type) && leave) {
164 const r = leave.call(ctx, node, item, list)
165 if (r === STOP) {
166 if (pushed) (ctx as any)[pushed] = prev
167 return STOP
168 }
169 }
170 if (pushed) (ctx as any)[pushed] = prev
171 return undefined
172 }
173
174 return visitNode(root, null, null)
175}
176
177export const walk: WalkFn = Object.assign(walkImpl, { skip: SKIP, stop: STOP }) as WalkFn
178
179export const walkSkip: symbol = SKIP
180export const walkStop: symbol = STOP
181
182function walkChildren(
183 node: CssNode,
184 reverse: boolean,
185 visitNode: (n: CssNode, item: ListItem<CssNode> | null, list: CssList<CssNode> | null) => symbol | undefined,
186): symbol | undefined {
187 // Composite children
188 if ('children' in node && node.children instanceof CssList) {
189 return walkList(node.children, reverse, visitNode)
190 }
191 // Rule prelude/block
192 if (node.type === 'Rule') {
193 const r1 = visitNode(node.prelude as CssNode, null, null)
194 if (r1 === STOP) return STOP
195 const r2 = visitNode(node.block as CssNode, null, null)
196 if (r2 === STOP) return STOP
197 return undefined
198 }
199 if (node.type === 'Atrule') {
200 if (node.prelude) {
201 const r = visitNode(node.prelude, null, null)
202 if (r === STOP) return STOP
203 }
204 if (node.block) {
205 const r = visitNode(node.block, null, null)
206 if (r === STOP) return STOP
207 }
208 return undefined
209 }
210 if (node.type === 'Declaration') {
211 return visitNode(node.value as CssNode, null, null)
212 }
213 if (node.type === 'AttributeSelector') {
214 const r = visitNode(node.name, null, null)
215 if (r === STOP) return STOP
216 if (node.value)
217 return visitNode(node.value, null, null)
218 return undefined
219 }
220 return undefined
221}
222
223function walkList(
224 list: CssList<CssNode>,
225 reverse: boolean,
226 visitNode: (n: CssNode, item: ListItem<CssNode> | null, list: CssList<CssNode> | null) => symbol | undefined,
227): symbol | undefined {
228 // The list's `forEach` callback already receives the current `ListItem`
229 // as its second argument — passing it straight to `visitNode` avoids the
230 // O(N) `findItem` linear scan per child that the previous implementation
231 // performed (turning every walk into O(N²)).
232 let stopped: symbol | undefined
233 if (reverse) {
234 list.forEachRight((data, item, l) => {
235 if (stopped)
236 return
237 const r = visitNode(data, item, l)
238 if (r === STOP)
239 stopped = STOP
240 })
241 }
242 else {
243 list.forEach((data, item, l) => {
244 if (stopped)
245 return
246 const r = visitNode(data, item, l)
247 if (r === STOP)
248 stopped = STOP
249 })
250 }
251 return stopped
252}
packages/toolkit/src/select/attributes.tsadded+243-0
Changes to packages/toolkit/src/select/attributes.ts
@@ -0,0 +1,243 @@
1/**
2 * Attribute-selector matchers. One factory per `AttributeAction` returning
3 * a predicate over the consumer's element type (via the adapter).
4 */
5
6import type { AttributeSelectorNode } from '../what'
7import type { Adapter, CompiledQuery, Options } from './types'
8import { ALWAYS_TRUE } from './helpers/always-true'
9
10function isWsCode(c: number): boolean {
11 return c === 32 || c === 9 || c === 10 || c === 12 || c === 13
12}
13
14/**
15 * Whitespace-separated token containment — equivalent to the `~=` operator
16 * (and the `.class` shorthand), but without the regex compile + RegExp.test
17 * overhead of a per-element check.
18 */
19function containsWsToken(haystack: string, needle: string): boolean {
20 const nlen = needle.length
21 const hlen = haystack.length
22 if (nlen === 0 || nlen > hlen)
23 return false
24 let idx = haystack.indexOf(needle)
25 while (idx >= 0) {
26 const before = idx === 0 || isWsCode(haystack.charCodeAt(idx - 1))
27 const afterIdx = idx + nlen
28 const after = afterIdx === hlen || isWsCode(haystack.charCodeAt(afterIdx))
29 if (before && after)
30 return true
31 idx = haystack.indexOf(needle, idx + 1)
32 }
33 return false
34}
35
36// eslint-disable-next-line pickier/no-unused-vars
37function caseInsensitive(value: string, attr: AttributeSelectorNode, options: Options<unknown, any>): boolean {
38 if (attr.ignoreCase === true)
39 return true
40 if (attr.ignoreCase === false)
41 return false
42 if (attr.ignoreCase === 'quirks')
43 return !options.xmlMode
44 return false
45}
46
47export function compileAttribute<Node, ElementNode extends Node>(
48 selector: AttributeSelectorNode,
49 options: Options<Node, ElementNode>,
50 next: CompiledQuery<ElementNode>,
51): CompiledQuery<ElementNode> {
52 const adapter = options.adapter
53 const action = selector.action
54 const name = selector.name
55 let value = selector.value
56 const ci = caseInsensitive(value, selector, options as any)
57 if (ci)
58 value = value.toLowerCase()
59 return wrap(action, name, value, ci, adapter, next)
60}
61
62function wrap<Node, ElementNode extends Node>(
63 action: string,
64 name: string,
65 value: string,
66 ci: boolean,
67 adapter: Adapter<Node, ElementNode>,
68 next: CompiledQuery<ElementNode>,
69): CompiledQuery<ElementNode> {
70 const isLeaf = next === ALWAYS_TRUE
71 switch (action) {
72 case 'exists':
73 if (isLeaf)
74 return e => adapter.hasAttrib(e, name)
75 return e => adapter.hasAttrib(e, name) && next(e)
76
77 case 'equals':
78 if (!ci) {
79 // String equality with `null`/`undefined` already short-circuits to
80 // false, so we can skip the explicit null guard — matches
81 // css-select's equality fast path.
82 if (isLeaf)
83 return e => adapter.getAttributeValue(e, name) === value
84 return e => adapter.getAttributeValue(e, name) === value && next(e)
85 }
86 if (isLeaf) {
87 return (e) => {
88 const v = adapter.getAttributeValue(e, name)
89 if (v == null)
90 return false
91 return v.toLowerCase() === value
92 }
93 }
94 return (e) => {
95 const v = adapter.getAttributeValue(e, name)
96 if (v == null)
97 return false
98 return v.toLowerCase() === value && next(e)
99 }
100
101 case 'not':
102 return (e) => {
103 const v = adapter.getAttributeValue(e, name)
104 if (v == null)
105 return next(e)
106 return (ci ? v.toLowerCase() : v) !== value && next(e)
107 }
108
109 case 'start':
110 if (value === '')
111 return () => false
112 if (isLeaf) {
113 return (e) => {
114 const v = adapter.getAttributeValue(e, name)
115 if (v == null)
116 return false
117 return (ci ? v.toLowerCase() : v).startsWith(value)
118 }
119 }
120 return (e) => {
121 const v = adapter.getAttributeValue(e, name)
122 if (v == null)
123 return false
124 return (ci ? v.toLowerCase() : v).startsWith(value) && next(e)
125 }
126
127 case 'end':
128 if (value === '')
129 return () => false
130 if (isLeaf) {
131 return (e) => {
132 const v = adapter.getAttributeValue(e, name)
133 if (v == null)
134 return false
135 return (ci ? v.toLowerCase() : v).endsWith(value)
136 }
137 }
138 return (e) => {
139 const v = adapter.getAttributeValue(e, name)
140 if (v == null)
141 return false
142 return (ci ? v.toLowerCase() : v).endsWith(value) && next(e)
143 }
144
145 case 'any': {
146 if (value === '')
147 return () => false
148 if (ci) {
149 if (isLeaf) {
150 return (e) => {
151 const v = adapter.getAttributeValue(e, name)
152 if (v == null)
153 return false
154 return v.toLowerCase().indexOf(value) >= 0
155 }
156 }
157 return (e) => {
158 const v = adapter.getAttributeValue(e, name)
159 if (v == null)
160 return false
161 return v.toLowerCase().indexOf(value) >= 0 && next(e)
162 }
163 }
164 if (isLeaf) {
165 return (e) => {
166 const v = adapter.getAttributeValue(e, name)
167 if (v == null)
168 return false
169 return v.indexOf(value) >= 0
170 }
171 }
172 return (e) => {
173 const v = adapter.getAttributeValue(e, name)
174 if (v == null)
175 return false
176 return v.indexOf(value) >= 0 && next(e)
177 }
178 }
179
180 case 'element': {
181 if (value === '' || /\s/.test(value))
182 return () => false
183 if (ci) {
184 if (isLeaf) {
185 return (e) => {
186 const v = adapter.getAttributeValue(e, name)
187 if (v == null)
188 return false
189 return containsWsToken(v.toLowerCase(), value)
190 }
191 }
192 return (e) => {
193 const v = adapter.getAttributeValue(e, name)
194 if (v == null)
195 return false
196 return containsWsToken(v.toLowerCase(), value) && next(e)
197 }
198 }
199 if (isLeaf) {
200 return (e) => {
201 const v = adapter.getAttributeValue(e, name)
202 if (v == null)
203 return false
204 return containsWsToken(v, value)
205 }
206 }
207 return (e) => {
208 const v = adapter.getAttributeValue(e, name)
209 if (v == null)
210 return false
211 return containsWsToken(v, value) && next(e)
212 }
213 }
214
215 case 'hyphen':
216 if (isLeaf) {
217 return (e) => {
218 const v = adapter.getAttributeValue(e, name)
219 if (v == null)
220 return false
221 const lower = ci ? v.toLowerCase() : v
222 if (lower === value)
223 return true
224 if (lower.startsWith(value) && lower.charAt(value.length) === '-')
225 return true
226 return false
227 }
228 }
229 return (e) => {
230 const v = adapter.getAttributeValue(e, name)
231 if (v == null)
232 return false
233 const lower = ci ? v.toLowerCase() : v
234 if (lower === value)
235 return next(e)
236 if (lower.startsWith(value) && lower.charAt(value.length) === '-')
237 return next(e)
238 return false
239 }
240 }
241 return _ => { void _; return false }
242}
243
packages/toolkit/src/select/general.tsadded+55-0
Changes to packages/toolkit/src/select/general.ts
@@ -0,0 +1,55 @@
1/**
2 * Compile a *simple* selector token (tag/universal/id/class/attr/pseudo)
3 * into a predicate. Combinators (descendant/child/sibling/adjacent/parent)
4 * are handled at a higher level — see pseudo-selectors/selectors.ts.
5 *
6 * `next` is the predicate for the rest of the compound chain to the right
7 * of this token. When the caller has nothing further to test, it passes the
8 * shared `ALWAYS_TRUE` sentinel — each branch detects that and returns a
9 * leaf predicate without the trailing `&& next(e)` indirection. Skipping
10 * the no-op call shaves ~10 % off `selectAll` on tight loops.
11 */
12
13import type { Selector } from '../what'
14import type { CompiledQuery, Options } from './types'
15import { compileAttribute } from './attributes'
16import { ALWAYS_TRUE } from './helpers/always-true'
17import { compilePseudo } from './pseudo-selectors'
18
19type Compiled<E> = CompiledQuery<E>
20
21export { ALWAYS_TRUE }
22
23export function compileToken<Node, ElementNode extends Node>(
24 token: Selector,
25 options: Options<Node, ElementNode>,
26 next: Compiled<ElementNode>,
27): Compiled<ElementNode> {
28 const adapter = options.adapter
29 const isLeaf = next === ALWAYS_TRUE
30 switch (token.type) {
31 case 'tag': {
32 const name = options.lowerCaseTags !== false && !options.xmlMode ? token.name.toLowerCase() : token.name
33 if (isLeaf)
34 return e => adapter.getName(e) === name
35 return e => adapter.getName(e) === name && next(e)
36 }
37 case 'universal':
38 return next
39 case 'attribute':
40 return compileAttribute(token, options, next)
41 case 'pseudo':
42 case 'pseudo-element':
43 return compilePseudo(token as any, options, next) as any
44 case 'descendant':
45 case 'child':
46 case 'parent':
47 case 'sibling':
48 case 'adjacent':
49 case 'column-combinator':
50 // Should never reach here — combinators are handled by selectors.ts
51 // before we get a chance to dispatch on them.
52 return next
53 }
54 return _ => { void _; return false }
55}
packages/toolkit/src/select/helpers/always-true.tsadded+7-0
Changes to packages/toolkit/src/select/helpers/always-true.ts
@@ -0,0 +1,7 @@
1/**
2 * Singleton "always-matches" predicate. Used as the leaf sentinel in
3 * compound-selector chains so each compileX factory can detect the leaf
4 * case and skip the trailing `&& next(e)` indirection.
5 */
6
7export const ALWAYS_TRUE: (_: any) => boolean = (_: any): boolean => { void _; return true }
packages/toolkit/src/select/helpers/querying.tsadded+91-0
Changes to packages/toolkit/src/select/helpers/querying.ts
@@ -0,0 +1,91 @@
1/**
2 * Iterative tree search for selectAll / selectOne. Mirrors `css-select`'s
3 * approach: use a stack of `(children, index)` pairs to walk the tree
4 * depth-first without recursion, avoiding the per-recursion frame and the
5 * `out.push(...spread)` allocations a naive recursive findAll incurs.
6 *
7 * We deliberately ignore `adapter.findAll` even when the consumer supplies
8 * one, because forcing every selectAll through the adapter's recursion is
9 * the single biggest cost on tree walks. Adapters are still consulted for
10 * `isTag` / `getChildren` / `getName`.
11 */
12
13import type { Adapter, CompiledQuery } from '../types'
14
15export function findAll<Node, ElementNode extends Node>(
16 query: CompiledQuery<ElementNode>,
17 nodes: ReadonlyArray<Node>,
18 adapter: Adapter<Node, ElementNode>,
19 xmlMode: boolean,
20): ElementNode[] {
21 const result: ElementNode[] = []
22 const nodeStack: ReadonlyArray<Node>[] = [nodes]
23 const indexStack: number[] = [0]
24 for (; ;) {
25 if (indexStack[0]! >= nodeStack[0]!.length) {
26 if (nodeStack.length === 1)
27 return result
28 nodeStack.shift()
29 indexStack.shift()
30 continue
31 }
32 const element = nodeStack[0]![indexStack[0]!++]!
33 if (!adapter.isTag(element))
34 continue
35 if (query(element as unknown as ElementNode))
36 result.push(element as unknown as ElementNode)
37 if (xmlMode || adapter.getName(element as unknown as ElementNode) !== 'template') {
38 const children = adapter.getChildren(element as unknown as ElementNode) as Node[]
39 if (children.length > 0) {
40 nodeStack.unshift(children)
41 indexStack.unshift(0)
42 }
43 }
44 }
45}
46
47export function findOne<Node, ElementNode extends Node>(
48 query: CompiledQuery<ElementNode>,
49 nodes: ReadonlyArray<Node>,
50 adapter: Adapter<Node, ElementNode>,
51 xmlMode: boolean,
52): ElementNode | null {
53 const nodeStack: ReadonlyArray<Node>[] = [nodes]
54 const indexStack: number[] = [0]
55 for (; ;) {
56 if (indexStack[0]! >= nodeStack[0]!.length) {
57 if (nodeStack.length === 1)
58 return null
59 nodeStack.shift()
60 indexStack.shift()
61 continue
62 }
63 const element = nodeStack[0]![indexStack[0]!++]!
64 if (!adapter.isTag(element))
65 continue
66 if (query(element as unknown as ElementNode))
67 return element as unknown as ElementNode
68 if (xmlMode || adapter.getName(element as unknown as ElementNode) !== 'template') {
69 const children = adapter.getChildren(element as unknown as ElementNode) as Node[]
70 if (children.length > 0) {
71 nodeStack.unshift(children)
72 indexStack.unshift(0)
73 }
74 }
75 }
76}
77
78/**
79 * Convert the user-supplied root into a search context. Mirrors
80 * `css-select`'s `prepareContext` — a single root node is replaced by its
81 * children (so the root itself isn't a candidate); arrays go through
82 * `removeSubsets` to avoid testing nested selections twice.
83 */
84export function prepareContext<Node, ElementNode extends Node>(
85 elements: Node | Node[],
86 adapter: Adapter<Node, ElementNode>,
87): ReadonlyArray<Node> {
88 if (Array.isArray(elements))
89 return adapter.removeSubsets(elements)
90 return adapter.getChildren(elements as unknown as ElementNode) as Node[]
91}
packages/toolkit/src/select/index.tsadded+141-0
Changes to packages/toolkit/src/select/index.ts
@@ -0,0 +1,141 @@
1/**
2 * Public surface — drop-in compatible with css-select v5.
3 *
4 * selectAll(selector, root, options) → ElementNode[]
5 * selectOne(selector, root, options) → ElementNode | null
6 * is(node, selector, options) → boolean
7 * compile(selector, options) → CompiledQuery
8 *
9 * Compiled-selector cache: the same selector string compiled with the
10 * same adapter + options is reused. Disable with `cacheResults: false`.
11 */
12
13import type { Selector } from '../what'
14import type { Adapter, CompiledQuery, Options } from './types'
15import { parse } from '../what'
16import { findAll as ourFindAll, findOne as ourFindOne, prepareContext } from './helpers/querying'
17import { compileGeneric, parseAndCompile } from './pseudo-selectors/selectors'
18
19function getAdapter<Node, ElementNode extends Node>(options: Options<Node, ElementNode>): Adapter<Node, ElementNode> {
20 if (!options || !options.adapter)
21 throw new Error('css-select: adapter is required')
22 return options.adapter
23}
24
25// ----- compiled-selector cache -----
26// Two-level lookup: outer keyed by Adapter (object identity), inner keyed
27// by a 3-bit flag-signature integer encoding the options that change
28// compile output. The innermost map is keyed by selector text. This avoids
29// the per-call `${flags}|${selector}` string concat the previous
30// single-level cache did — meaningful when consumers call selectAll in a
31// hot loop.
32const adapterCaches = new WeakMap<object, (Map<string, CompiledQuery<any>> | undefined)[]>()
33
34function flagSig(options: Options<any, any>): number {
35 return ((options.xmlMode ? 1 : 0) << 0)
36 | ((options.lowerCaseAttributeNames === false ? 1 : 0) << 1)
37 | ((options.lowerCaseTags === false ? 1 : 0) << 2)
38}
39
40function getCache(adapter: object, sig: number): Map<string, CompiledQuery<any>> {
41 let bucket = adapterCaches.get(adapter)
42 if (!bucket) {
43 bucket = []
44 adapterCaches.set(adapter, bucket)
45 }
46 let m = bucket[sig]
47 if (!m) {
48 m = new Map()
49 bucket[sig] = m
50 }
51 return m
52}
53
54function compileSelectorString<Node, ElementNode extends Node>(
55 selector: string,
56 options: Options<Node, ElementNode>,
57): CompiledQuery<ElementNode> {
58 const ast = parse(selector, {
59 xmlMode: options.xmlMode,
60 lowerCaseAttributeNames: options.lowerCaseAttributeNames,
61 lowerCaseTags: options.lowerCaseTags,
62 })
63 return compileGeneric(ast, options)
64}
65
66export function compile<Node, ElementNode extends Node>(
67 selector: string | Selector[][],
68 options: Options<Node, ElementNode>,
69): CompiledQuery<ElementNode> {
70 if (typeof selector !== 'string')
71 return compileGeneric(selector, options)
72
73 // Fast path: use the per-adapter cache unless the consumer opted out.
74 if (options.cacheResults === false || !options.adapter)
75 return compileSelectorString(selector, options)
76
77 const cache = getCache(options.adapter as unknown as object, flagSig(options))
78 let compiled = cache.get(selector)
79 if (compiled !== undefined)
80 return compiled as CompiledQuery<ElementNode>
81 compiled = compileSelectorString(selector, options)
82 cache.set(selector, compiled)
83 return compiled as CompiledQuery<ElementNode>
84}
85
86/** Drop the cached compile of `selector` for `adapter`, or all entries
87 * if `selector` is omitted. */
88export function clearSelectorCache(adapter: object, selector?: string): void {
89 if (selector === undefined) {
90 adapterCaches.delete(adapter)
91 return
92 }
93 const bucket = adapterCaches.get(adapter)
94 if (!bucket)
95 return
96 for (const m of bucket) {
97 if (m)
98 m.delete(selector)
99 }
100}
101
102export function selectAll<Node, ElementNode extends Node>(
103 selector: string | Selector[][] | CompiledQuery<ElementNode>,
104 root: Node | Node[],
105 options: Options<Node, ElementNode>,
106): ElementNode[] {
107 const adapter = getAdapter(options)
108 const test = typeof selector === 'function' ? selector : compile(selector, options)
109 const ctx = prepareContext(root, adapter)
110 return ourFindAll(test, ctx, adapter, options.xmlMode === true)
111}
112
113export function selectOne<Node, ElementNode extends Node>(
114 selector: string | Selector[][] | CompiledQuery<ElementNode>,
115 root: Node | Node[],
116 options: Options<Node, ElementNode>,
117): ElementNode | null {
118 const adapter = getAdapter(options)
119 const test = typeof selector === 'function' ? selector : compile(selector, options)
120 const ctx = prepareContext(root, adapter)
121 return ourFindOne(test, ctx, adapter, options.xmlMode === true)
122}
123
124export function is<Node, ElementNode extends Node>(
125 node: ElementNode,
126 selector: string | Selector[][] | CompiledQuery<ElementNode>,
127 options: Options<Node, ElementNode>,
128): boolean {
129 const test = typeof selector === 'function' ? selector : compile(selector, options)
130 return test(node)
131}
132
133export type {
134 Adapter,
135 CompiledQuery,
136 Options,
137 Predicate,
138} from './types'
139
140// expose internals for advanced use
141export { parseAndCompile }
packages/toolkit/src/select/pseudo-selectors/index.tsadded+333-0
Changes to packages/toolkit/src/select/pseudo-selectors/index.ts
@@ -0,0 +1,333 @@
1/**
2 * Pseudo-selector dispatcher. Three families:
3 * - filters: take a string or selector list, modify selection (`:nth-child`, `:not`, `:is`, `:where`, `:has`)
4 * - pseudos: simple boolean predicates (`:empty`, `:root`, `:checked`, etc.)
5 * - aliases: rewrite to attribute / sub-selectors
6 */
7
8import type { Selector } from '../../what'
9import type { CompiledQuery, Options } from '../types'
10import { parse as parseSelectorString } from '../../what'
11import { compileGeneric, parseAndCompile } from './selectors'
12
13interface PseudoToken {
14 type: 'pseudo' | 'pseudo-element'
15 name: string
16 data: string | Selector[][] | null
17}
18
19// Pseudo-classes whose match depends on document state (focus, hover, form
20// validity, etc.) that ts-css doesn't model. They fail closed unless the
21// caller supplies an `options.pseudos[name]` predicate.
22const STATEFUL_PSEUDOS: ReadonlySet<string> = new Set([
23 'link', 'any-link', 'visited', 'hover', 'active', 'focus',
24 'focus-visible', 'focus-within', 'target', 'target-within', 'enabled',
25 'disabled', 'checked', 'required', 'optional', 'valid', 'invalid',
26 'selected', 'placeholder-shown', 'read-only', 'read-write', 'in-range',
27 'out-of-range', 'default', 'indeterminate',
28])
29
30const SELECTOR_LIST_PSEUDOS: ReadonlySet<string> = new Set([
31 'is', 'where', 'matches', '-moz-any', '-webkit-any',
32])
33
34const NTH_PSEUDOS: ReadonlySet<string> = new Set([
35 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type',
36])
37
38export function compilePseudo<Node, ElementNode extends Node>(
39 token: PseudoToken,
40 options: Options<Node, ElementNode>,
41 next: CompiledQuery<ElementNode>,
42): CompiledQuery<ElementNode> {
43 const adapter = options.adapter
44 const name = token.name
45 const data = token.data
46
47 // -------- combinatorial pseudos: take selector lists --------
48 if (token.type === 'pseudo' && SELECTOR_LIST_PSEUDOS.has(name)) {
49 if (!Array.isArray(data))
50 return next
51 const tests = (data as Selector[][]).map(seg => parseAndCompile(seg, options))
52 // For a single sub-selector, skip the array iteration cost.
53 if (tests.length === 1) {
54 const t0 = tests[0]!
55 return e => t0(e) && next(e)
56 }
57 return (e) => {
58 for (const t of tests) {
59 if (t(e))
60 return next(e)
61 }
62 return false
63 }
64 }
65 if (token.type === 'pseudo' && name === 'not') {
66 if (!Array.isArray(data))
67 return next
68 const tests = (data as Selector[][]).map(seg => parseAndCompile(seg, options))
69 if (tests.length === 1) {
70 const t0 = tests[0]!
71 return e => !t0(e) && next(e)
72 }
73 return (e) => {
74 for (const t of tests) {
75 if (t(e))
76 return false
77 }
78 return next(e)
79 }
80 }
81 if (token.type === 'pseudo' && name === 'has') {
82 if (!Array.isArray(data))
83 return next
84 const tests = (data as Selector[][]).map(seg => parseAndCompile(seg, options))
85 // Collapse the sub-test list into a single predicate so the inner
86 // descendant loop only invokes one function per node.
87 const subjectTest: CompiledQuery<ElementNode> = tests.length === 1
88 ? tests[0]!
89 : ((node: any) => {
90 for (const t of tests) {
91 if (t(node))
92 return true
93 }
94 return false
95 }) as any
96 return (e) => {
97 // Iterative DFS using `pop` (O(1)) instead of `shift` (O(n)). The
98 // iteration order is reversed but `:has()` doesn't care about order
99 // — it stops at the first descendant that satisfies the test.
100 const stack: any[] = adapter.getChildren(e).slice()
101 while (stack.length > 0) {
102 const cur = stack.pop()
103 if (!adapter.isTag(cur))
104 continue
105 if (subjectTest(cur as any))
106 return next(e)
107 const kids = adapter.getChildren(cur as any)
108 for (let i = 0; i < kids.length; i++)
109 stack.push(kids[i])
110 }
111 return false
112 }
113 }
114
115 // -------- nth-* pseudos --------
116 if (token.type === 'pseudo' && NTH_PSEUDOS.has(name)) {
117 const fn = compileNth(typeof data === 'string' ? data : '', name, options)
118 return e => fn(e) && next(e)
119 }
120
121 // -------- structural pseudos --------
122 // Each variant scans `getSiblings(e)` once without allocating the
123 // filtered intermediate array `[].filter()` would (the previous shape
124 // produced one allocation per element-test, which the matcher hits
125 // 250+ times per selector even for tiny trees).
126 if (token.type === 'pseudo' && name === 'first-child') {
127 return (e) => {
128 const sibs = adapter.getSiblings(e)
129 for (const s of sibs) {
130 if (!adapter.isTag(s))
131 continue
132 return s === e && next(e)
133 }
134 return false
135 }
136 }
137 if (token.type === 'pseudo' && name === 'last-child') {
138 return (e) => {
139 const sibs = adapter.getSiblings(e)
140 for (let i = sibs.length - 1; i >= 0; i--) {
141 const s = sibs[i]!
142 if (!adapter.isTag(s))
143 continue
144 return s === e && next(e)
145 }
146 return false
147 }
148 }
149 if (token.type === 'pseudo' && name === 'first-of-type') {
150 return (e) => {
151 const tag = adapter.getName(e)
152 const sibs = adapter.getSiblings(e)
153 for (const s of sibs) {
154 if (!adapter.isTag(s) || adapter.getName(s as any) !== tag)
155 continue
156 return s === e && next(e)
157 }
158 return false
159 }
160 }
161 if (token.type === 'pseudo' && name === 'last-of-type') {
162 return (e) => {
163 const tag = adapter.getName(e)
164 const sibs = adapter.getSiblings(e)
165 for (let i = sibs.length - 1; i >= 0; i--) {
166 const s = sibs[i]!
167 if (!adapter.isTag(s) || adapter.getName(s as any) !== tag)
168 continue
169 return s === e && next(e)
170 }
171 return false
172 }
173 }
174 if (token.type === 'pseudo' && name === 'only-child') {
175 return (e) => {
176 const sibs = adapter.getSiblings(e)
177 let count = 0
178 let found = false
179 for (const s of sibs) {
180 if (!adapter.isTag(s))
181 continue
182 count++
183 if (count > 1)
184 return false
185 if (s === e)
186 found = true
187 }
188 return count === 1 && found && next(e)
189 }
190 }
191 if (token.type === 'pseudo' && name === 'only-of-type') {
192 return (e) => {
193 const tag = adapter.getName(e)
194 const sibs = adapter.getSiblings(e)
195 let count = 0
196 let found = false
197 for (const s of sibs) {
198 if (!adapter.isTag(s) || adapter.getName(s as any) !== tag)
199 continue
200 count++
201 if (count > 1)
202 return false
203 if (s === e)
204 found = true
205 }
206 return count === 1 && found && next(e)
207 }
208 }
209
210 // -------- simple pseudos --------
211 if (token.type === 'pseudo' && name === 'empty') {
212 return e => adapter.getChildren(e).length === 0 && next(e)
213 }
214 if (token.type === 'pseudo' && name === 'root') {
215 return (e) => {
216 const p = adapter.getParent(e)
217 return (p == null || !adapter.isTag(p as any)) && next(e)
218 }
219 }
220 if (token.type === 'pseudo' && name === 'scope') {
221 if (options.context && Array.isArray(options.context)) {
222 // Materialize a Set once at compile time — `Array.includes` is O(n)
223 // per element check and `:scope` runs against every candidate.
224 const ctx = new Set<any>(options.context as any[])
225 return e => ctx.has(e) && next(e)
226 }
227 if (options.context) {
228 const ctx = options.context as any
229 return e => (e as any) === ctx && next(e)
230 }
231 return (e) => {
232 const p = adapter.getParent(e)
233 return (p == null || !adapter.isTag(p as any)) && next(e)
234 }
235 }
236 if (token.type === 'pseudo' && STATEFUL_PSEUDOS.has(name)) {
237 // Stateful pseudos — without a real DOM we treat them as always-false
238 // (matches css-select's `xmlMode` defaults). Consumers who need real
239 // matching can pass `options.pseudos[name]` callbacks.
240 if (options.pseudos && options.pseudos[name]) {
241 const ext = options.pseudos[name]
242 if (typeof ext === 'function')
243 return e => ext(e, undefined) && next(e)
244 }
245 return _ => { void _; return false }
246 }
247
248 // -------- pseudo-elements --------
249 if (token.type === 'pseudo-element') {
250 // We don't model pseudo-elements; treat as no-op pass-through filter
251 // (matches css-select's behaviour for ::before/::after/etc.).
252 return next
253 }
254
255 // -------- caller-provided extensions --------
256 if (options.pseudos && options.pseudos[name]) {
257 const ext = options.pseudos[name]
258 if (typeof ext === 'function') {
259 return e => ext(e, typeof data === 'string' ? data : undefined) && next(e)
260 }
261 if (typeof ext === 'string') {
262 // Compile the alias selector string with `compileGeneric` so commas
263 // (selector lists) are handled correctly. Previously this routed
264 // through a stubbed `parseRaw` that always returned `[]`, which
265 // silently caused string-aliased pseudos to never match.
266 const aliasGroups = parseSelectorString(ext)
267 const re = compileGeneric(aliasGroups, options)
268 return e => re(e) && next(e)
269 }
270 }
271
272 // unknown — fail closed
273 return _ => { void _; return false }
274}
275
276
277function parseAnPlusB(s: string): { a: number, b: number } | null {
278 const trimmed = s.trim()
279 if (trimmed === 'odd')
280 return { a: 2, b: 1 }
281 if (trimmed === 'even')
282 return { a: 2, b: 0 }
283 const m = /^([+-]?\d*)n([+-]\d+)?$/.exec(trimmed.replace(/\s+/g, ''))
284 if (m) {
285 const a = m[1] === '' || m[1] === '+' ? 1 : m[1] === '-' ? -1 : Number(m[1])
286 const b = m[2] ? Number(m[2]) : 0
287 return { a, b }
288 }
289 const num = Number(trimmed)
290 if (!Number.isNaN(num))
291 return { a: 0, b: num }
292 return null
293}
294
295function compileNth<Node, ElementNode extends Node>(
296 arg: string,
297 kind: string,
298 options: Options<Node, ElementNode>,
299): (e: ElementNode) => boolean {
300 const ab = parseAnPlusB(arg)
301 if (!ab)
302 return () => false
303 const { a, b } = ab
304 const adapter = options.adapter
305 const ofType = kind === 'nth-of-type' || kind === 'nth-last-of-type'
306 const reverse = kind === 'nth-last-child' || kind === 'nth-last-of-type'
307 return (e) => {
308 // Compute idx in one pass over siblings — no `filter()` allocs and no
309 // `reverse()` mutation. For `nth-last-*` we count total matching
310 // siblings then take `total - rawIdx + 1`.
311 const sibs = adapter.getSiblings(e)
312 const tag = ofType ? adapter.getName(e) : ''
313 let raw = 0 // 1-based index of `e` among matching siblings
314 let total = 0
315 for (const s of sibs) {
316 if (!adapter.isTag(s))
317 continue
318 if (ofType && adapter.getName(s as any) !== tag)
319 continue
320 total++
321 if (s === e)
322 raw = total
323 }
324 if (raw === 0)
325 return false
326 const idx = reverse ? total - raw + 1 : raw
327 if (a === 0)
328 return idx === b
329 return ((idx - b) / a) >= 0 && ((idx - b) % a) === 0
330 }
331}
332
333export { compileGeneric }
packages/toolkit/src/select/pseudo-selectors/selectors.tsadded+138-0
Changes to packages/toolkit/src/select/pseudo-selectors/selectors.ts
@@ -0,0 +1,138 @@
1/**
2 * Compose a token list into a test function.
3 *
4 * Tokens carry simple selectors (tag/class/attr/pseudo) interleaved with
5 * combinators (descendant/child/sibling/adjacent). Each token becomes one
6 * link in a chain — combinators wrap the running chain so it tests the
7 * left-hand chain on a new pivot. We iterate left-to-right (matching
8 * `css-select`'s compile model) but sort tokens *within each compound* by
9 * procedure cost so cheap tests (tag) wind up outermost and fail-fast.
10 */
11
12import type { AttributeSelectorNode, Selector } from '../../what'
13import type { Adapter, CompiledQuery, Options } from '../types'
14import { compileToken } from '../general'
15import { ALWAYS_TRUE } from '../helpers/always-true'
16
17// Lower = more expensive → wrapped innermost → runs last.
18// Higher = cheaper → wrapped outermost → runs first (lets us fail-fast).
19const PROCEDURE: Record<string, number> = {
20 universal: 50,
21 tag: 30,
22 attribute: 1,
23 pseudo: 0,
24 'pseudo-element': 0,
25}
26
27// Within `attribute`, equality is cheaper than substring/regex.
28const ATTRIBUTE_PROCEDURE: Record<string, number> = {
29 exists: 10,
30 equals: 8,
31 not: 7,
32 start: 6,
33 end: 6,
34 hyphen: 5,
35 any: 4,
36 element: 3,
37}
38
39function tokenProcedure(t: Selector): number {
40 const base = PROCEDURE[t.type] ?? 0
41 if (t.type === 'attribute')
42 return base + (ATTRIBUTE_PROCEDURE[(t as AttributeSelectorNode).action] ?? 0)
43 return base
44}
45
46export function compileGeneric<Node, ElementNode extends Node>(
47 segments: Selector[][],
48 options: Options<Node, ElementNode>,
49): CompiledQuery<ElementNode> {
50 if (segments.length === 1)
51 return parseAndCompile(segments[0]!, options)
52 const compiled = segments.map(seg => parseAndCompile(seg, options))
53 return e => compiled.some(c => c(e))
54}
55
56export function parseAndCompile<Node, ElementNode extends Node>(
57 tokens: Selector[],
58 options: Options<Node, ElementNode>,
59): CompiledQuery<ElementNode> {
60 const adapter = options.adapter
61 // Split into compound buckets; combinator markers stay in `combs`.
62 const compounds: Selector[][] = [[]]
63 const combs: Selector[] = []
64 for (const t of tokens) {
65 if (
66 t.type === 'descendant' || t.type === 'child' || t.type === 'sibling'
67 || t.type === 'adjacent' || t.type === 'parent' || t.type === 'column-combinator'
68 ) {
69 combs.push(t)
70 compounds.push([])
71 }
72 else {
73 compounds[compounds.length - 1]!.push(t)
74 }
75 }
76
77 // Build left-to-right. Within each compound, sort by procedure ascending
78 // so the cheapest test ends up outermost (runs first).
79 let func: CompiledQuery<ElementNode> = ALWAYS_TRUE
80 for (let ci = 0; ci < compounds.length; ci++) {
81 const compound = compounds[ci]!.slice().sort((a, b) => tokenProcedure(a) - tokenProcedure(b))
82 for (const t of compound)
83 func = compileToken(t, options, func)
84 if (ci < combs.length)
85 func = wrapCombinator(combs[ci]!, func, adapter)
86 }
87 return func
88}
89
90function wrapCombinator<Node, ElementNode extends Node>(
91 comb: Selector,
92 after: CompiledQuery<ElementNode>,
93 adapter: Adapter<Node, ElementNode>,
94): CompiledQuery<ElementNode> {
95 switch (comb.type) {
96 case 'descendant':
97 case 'column-combinator':
98 return (elem: any) => {
99 let p: any = adapter.getParent(elem)
100 while (p) {
101 if (adapter.isTag(p) && after(p))
102 return true
103 p = adapter.getParent(p)
104 }
105 return false
106 }
107 case 'child':
108 return (elem: any) => {
109 const p: any = adapter.getParent(elem)
110 return p != null && adapter.isTag(p) && after(p)
111 }
112 case 'sibling':
113 return (elem: any) => {
114 const sibs: any[] = adapter.getSiblings(elem) as any
115 for (const s of sibs) {
116 if (s === elem)
117 return false
118 if (adapter.isTag(s) && after(s))
119 return true
120 }
121 return false
122 }
123 case 'adjacent':
124 return (elem: any) => {
125 const sibs: any[] = adapter.getSiblings(elem) as any
126 let prev: any = null
127 for (const s of sibs) {
128 if (s === elem)
129 return prev != null && adapter.isTag(prev) && after(prev)
130 prev = s
131 }
132 return false
133 }
134 case 'parent':
135 return (elem: any) => adapter.existsOne(after, adapter.getChildren(elem))
136 }
137 return _ => { void _; return false }
138}
packages/toolkit/src/select/types.tsadded+77-0
Changes to packages/toolkit/src/select/types.ts
@@ -0,0 +1,77 @@
1/**
2 * css-select adapter / option types. Drop-in compatible with css-select v5.
3 *
4 * The library is generic over the consumer's tree (`Node`/`ElementNode`),
5 * communicated via the `Adapter` interface — exactly the shape SVGO etc.
6 * already implement, so callers don't need to change a thing.
7 */
8
9export interface Adapter<Node, ElementNode extends Node> {
10 // eslint-disable-next-line pickier/no-unused-vars
11 isTag: (node: Node) => node is ElementNode
12 // eslint-disable-next-line pickier/no-unused-vars
13 existsOne: (test: (e: ElementNode) => boolean, elems: Node[]) => boolean
14 // eslint-disable-next-line pickier/no-unused-vars
15 getAttributeValue: (elem: ElementNode, name: string) => string | undefined
16 // eslint-disable-next-line pickier/no-unused-vars
17 getChildren: (node: Node) => Node[]
18 // eslint-disable-next-line pickier/no-unused-vars
19 getName: (elem: ElementNode) => string
20 // eslint-disable-next-line pickier/no-unused-vars
21 getParent: (elem: ElementNode | Node) => ElementNode | null
22 // eslint-disable-next-line pickier/no-unused-vars
23 getSiblings: (elem: Node) => Node[]
24 // eslint-disable-next-line pickier/no-unused-vars
25 getText: (node: Node) => string
26 // eslint-disable-next-line pickier/no-unused-vars
27 hasAttrib: (elem: ElementNode, name: string) => boolean
28 // eslint-disable-next-line pickier/no-unused-vars
29 removeSubsets: (nodes: Node[]) => Node[]
30 /**
31 * `findAll` / `findOne` are accepted for adapter-shape compatibility
32 * with css-select, but ts-css implements its own iterative tree walker
33 * (`select/helpers/querying.ts`) and does NOT call these. They're
34 * declared optional so adapters that don't provide them still type-check.
35 */
36 // eslint-disable-next-line pickier/no-unused-vars
37 findAll?: (test: (e: ElementNode) => boolean, elems: Node[]) => ElementNode[]
38 // eslint-disable-next-line pickier/no-unused-vars
39 findOne?: (test: (e: ElementNode) => boolean, elems: Node[]) => ElementNode | null
40 // eslint-disable-next-line pickier/no-unused-vars
41 equals?: (a: Node, b: Node) => boolean
42 // eslint-disable-next-line pickier/no-unused-vars
43 isActive?: (elem: ElementNode) => boolean
44 // eslint-disable-next-line pickier/no-unused-vars
45 isVisited?: (elem: ElementNode) => boolean
46 // eslint-disable-next-line pickier/no-unused-vars
47 isHovered?: (elem: ElementNode) => boolean
48}
49
50export interface Options<Node, ElementNode extends Node> {
51 /** XML mode disables HTML quirks (case-insensitive tags etc.). */
52 xmlMode?: boolean
53 /** When false, attribute names are not lowercased before lookup. */
54 lowerCaseAttributeNames?: boolean
55 /** When false, tags are not lowercased. */
56 lowerCaseTags?: boolean
57 /** Cache compiled selectors when true. */
58 cacheResults?: boolean
59 /** Adapter providing tree access. */
60 adapter: Adapter<Node, ElementNode>
61 /** Optional context node for relative selectors (`:has`, `:not`). */
62 context?: Node | Node[]
63 /** Optional pseudo-class/element extension hooks. */
64 // eslint-disable-next-line pickier/no-unused-vars
65 pseudos?: Record<string, string | ((elem: ElementNode, value?: string) => boolean)>
66 /** Set, treated as already-visited siblings root. */
67 rootFunc?: (elem: ElementNode) => boolean
68 /** Whether to match `:has` lazily. */
69 relativeSelector?: boolean
70 /** Whether the parsed selector itself is the document. */
71 quirksMode?: boolean
72}
73
74// eslint-disable-next-line pickier/no-unused-vars
75export type CompiledQuery<ElementNode> = (node: ElementNode) => boolean
76// eslint-disable-next-line pickier/no-unused-vars
77export type Predicate<Value> = (v: Value) => boolean
packages/toolkit/src/types.tsadded+12-0
Changes to packages/toolkit/src/types.ts
@@ -0,0 +1,12 @@
1/**
2 * Public configuration shape for ts-css. Loaded by `bunfig` from
3 * `css.config.ts` (or `.json` / `.toml`) when present.
4 */
5export interface CSSConfig {
6 /** Default float precision used by the minifier when rounding numeric values. */
7 floatPrecision: number
8 /** Verbose logging from CLI commands. */
9 verbose: boolean
10}
11
12export type CSSOptions = Partial<CSSConfig>
packages/toolkit/src/what/index.tsadded+21-0
Changes to packages/toolkit/src/what/index.ts
@@ -0,0 +1,21 @@
1/**
2 * Public surface — drop-in compatible with css-what v6.
3 */
4
5export { parse } from './parse'
6export { stringify } from './stringify'
7export { isTraversal } from './traversal'
8export type {
9 AttributeAction,
10 AttributeSelectorNode,
11 IgnoreCase,
12 ParseOptions,
13 PseudoElementNode,
14 PseudoSelectorNode,
15 Selector,
16 SelectorType,
17 TagSelectorNode,
18 TraversalNode,
19 UniversalSelectorNode,
20} from './types'
21export { IgnoreCaseMode } from './types'
packages/toolkit/src/what/parse.tsadded+388-0
Changes to packages/toolkit/src/what/parse.ts
@@ -0,0 +1,388 @@
1/**
2 * Selector parser — string → list-of-lists of selector segments.
3 *
4 * Returns one inner list per top-level (comma-separated) selector, with
5 * traversal segments (descendant/child/sibling/adjacent/column) interleaved
6 * between simple selector segments. Mirrors `css-what` v6.
7 */
8
9import type { ParseOptions, Selector } from './types'
10
11// Sticky regexes: `y` lets us match from a specific index without slicing.
12// Using `lastIndex` directly keeps the parser allocation-light per name.
13const RE_NAME_STICKY = /(?:\\(?:[\dA-Fa-f]{1,6} ?|[^])|[\w\-°-￿])+/y
14const RE_ESCAPE = /\\([\dA-Fa-f]{1,6} ?|[^])/g
15
16function unescape(name: string): string {
17 return name.replace(RE_ESCAPE, (_m, escape: string) => {
18 if (escape.length > 1 && /^[\dA-Fa-f]/.test(escape)) {
19 const code = Number.parseInt(escape, 16)
20 if (code >= 0xD800 && code <= 0xDFFF)
21 return '�'
22 return String.fromCodePoint(code)
23 }
24 return escape
25 })
26}
27
28function unescapeIfNeeded(name: string): string {
29 // Vast majority of CSS selector identifiers have no `\\` escapes — skip
30 // the regex replace pipeline and return the string directly.
31 return name.indexOf('\\') < 0 ? name : unescape(name)
32}
33
34const ATTRIBUTES_QUIRKS = new Set([
35 'accept', 'accept-charset', 'align', 'alink', 'axis', 'bgcolor', 'charset',
36 'checked', 'clear', 'codetype', 'color', 'compact', 'declare', 'defer',
37 'dir', 'direction', 'disabled', 'enctype', 'face', 'frame', 'hreflang',
38 'http-equiv', 'lang', 'language', 'link', 'media', 'method', 'multiple',
39 'nohref', 'noresize', 'noshade', 'nowrap', 'readonly', 'rel', 'rev',
40 'rules', 'scope', 'scrolling', 'selected', 'shape', 'target', 'text',
41 'type', 'valign', 'valuetype', 'vlink',
42])
43
44// Action-string lookup as a plain function (no per-parse Map).
45function actionFromChar(ch: number): string | null {
46 switch (ch) {
47 case 126 /* ~ */: return 'element'
48 case 94 /* ^ */: return 'start'
49 case 36 /* $ */: return 'end'
50 case 42 /* * */: return 'any'
51 case 33 /* ! */: return 'not'
52 case 124 /* | */: return 'hyphen'
53 default: return null
54 }
55}
56
57function isWsCode(c: number): boolean {
58 return c === 32 || c === 9 || c === 10 || c === 13 || c === 12
59}
60
61export function parse(selector: string, options: ParseOptions = {}): Selector[][] {
62 const subselects: Selector[][] = []
63 const endIndex = parseSelectorImpl(subselects, selector, options, 0)
64 if (endIndex < selector.length)
65 throw new Error(`Unmatched selector: ${selector.slice(endIndex)}`)
66 return subselects
67}
68
69function readName(selector: string, from: number): { value: string, end: number } {
70 RE_NAME_STICKY.lastIndex = from
71 const m = RE_NAME_STICKY.exec(selector)
72 if (!m)
73 throw new Error(`Expected name, found ${selector.slice(from)}`)
74 return { value: unescapeIfNeeded(m[0]), end: from + m[0].length }
75}
76
77function stripWS(selector: string, from: number): number {
78 while (from < selector.length && isWsCode(selector.charCodeAt(from)))
79 from++
80 return from
81}
82
83function parseSelectorImpl(subselects: Selector[][], selector: string, options: ParseOptions, startIndex: number): number {
84 let tokens: Selector[] = []
85 let i = stripWS(selector, startIndex)
86 const len = selector.length
87 const xmlMode = options.xmlMode === true
88 const lowerCaseAttrs = options.lowerCaseAttributeNames !== false && !xmlMode
89 const lowerCaseTagsFlag = options.lowerCaseTags !== false
90
91 while (i < len) {
92 const code = selector.charCodeAt(i)
93
94 // whitespace → descendant combinator (or just trim if at start)
95 if (isWsCode(code)) {
96 let trimmed = i + 1
97 while (trimmed < len && isWsCode(selector.charCodeAt(trimmed)))
98 trimmed++
99 if (tokens.length === 0)
100 return trimmed
101 i = trimmed
102 addTraversal(tokens, 'descendant')
103 continue
104 }
105
106 // explicit combinators
107 if (code === 62 /* > */ || code === 60 /* < */ || code === 126 /* ~ */ || code === 43 /* + */ || code === 124 /* | */) {
108 let j = i + 1
109 while (j < len && isWsCode(selector.charCodeAt(j))) j++
110 i = j
111 switch (code) {
112 case 62: addTraversal(tokens, 'child'); break
113 case 60: addTraversal(tokens, 'parent'); break
114 case 126: addTraversal(tokens, 'sibling'); break
115 case 43: addTraversal(tokens, 'adjacent'); break
116 case 124:
117 if (i < len && selector.charCodeAt(i) === 124) {
118 i++
119 i = stripWS(selector, i)
120 addTraversal(tokens, 'column-combinator')
121 }
122 else {
123 tokens.push({ type: 'tag', name: '', namespace: '' })
124 }
125 break
126 }
127 continue
128 }
129
130 if (code === 44 /* , */) {
131 if (tokens.length === 0)
132 throw new Error('Empty sub-selector')
133 subselects.push(tokens)
134 tokens = []
135 i = stripWS(selector, i + 1)
136 continue
137 }
138
139 // comment /* ... */
140 if (code === 47 /* / */ && selector.charCodeAt(i + 1) === 42 /* * */) {
141 const end = selector.indexOf('*/', i + 2)
142 if (end < 0)
143 throw new Error('Unmatched comment')
144 i = stripWS(selector, end + 2)
145 continue
146 }
147
148 if (code === 42 /* * */) {
149 i++
150 tokens.push({ type: 'universal', namespace: null })
151 continue
152 }
153
154 if (code === 35 /* # */) {
155 const r = readName(selector, i + 1)
156 i = r.end
157 tokens.push({
158 type: 'attribute',
159 name: 'id',
160 action: 'equals',
161 value: r.value,
162 namespace: null,
163 ignoreCase: false,
164 })
165 continue
166 }
167
168 if (code === 46 /* . */) {
169 const r = readName(selector, i + 1)
170 i = r.end
171 tokens.push({
172 type: 'attribute',
173 name: 'class',
174 action: 'element',
175 value: r.value,
176 namespace: null,
177 ignoreCase: false,
178 })
179 continue
180 }
181
182 if (code === 91 /* [ */) {
183 i = parseAttribute(selector, i, tokens, options, xmlMode, lowerCaseAttrs)
184 continue
185 }
186
187 if (code === 58 /* : */) {
188 i = parsePseudo(selector, i, tokens, options)
189 continue
190 }
191
192 // tag selector — readName then optional `|tag`
193 if (code === 124 /* | */) {
194 i++
195 const r = readName(selector, i)
196 i = r.end
197 tokens.push({ type: 'tag', name: lowerCaseTagsFlag ? r.value.toLowerCase() : r.value, namespace: '' })
198 continue
199 }
200
201 {
202 const r1 = readName(selector, i)
203 i = r1.end
204 if (i < len && selector.charCodeAt(i) === 124 /* | */ && selector.charCodeAt(i + 1) !== 61 /* = */) {
205 i++
206 const r2 = readName(selector, i)
207 i = r2.end
208 tokens.push({ type: 'tag', name: lowerCaseTagsFlag ? r2.value.toLowerCase() : r2.value, namespace: r1.value })
209 }
210 else {
211 tokens.push({ type: 'tag', name: lowerCaseTagsFlag ? r1.value.toLowerCase() : r1.value, namespace: null })
212 }
213 }
214 }
215 if (tokens.length > 0)
216 subselects.push(tokens)
217 return i
218}
219
220// eslint-disable-next-line pickier/no-unused-vars
221function parseAttribute(selector: string, idx: number, tokens: Selector[], options: ParseOptions, xmlMode: boolean, lowerCaseAttrs: boolean): number {
222 let i = idx + 1
223 const len = selector.length
224 let attribute: string
225 if (selector.charCodeAt(i) === 124 /* | */)
226 throw new Error('Empty namespace not supported')
227 if (selector.charCodeAt(i) === 42 /* * */ && selector.charCodeAt(i + 1) === 124 /* | */) {
228 i += 2
229 const r = readName(selector, i)
230 i = r.end
231 attribute = r.value
232 }
233 else {
234 const r = readName(selector, i)
235 i = r.end
236 attribute = r.value
237 if (selector.charCodeAt(i) === 124 /* | */ && selector.charCodeAt(i + 1) !== 61 /* = */) {
238 i++
239 const r2 = readName(selector, i)
240 i = r2.end
241 attribute = r2.value
242 }
243 }
244 i = stripWS(selector, i)
245 let action: 'exists' | 'equals' | 'element' | 'start' | 'end' | 'any' | 'not' | 'hyphen' = 'exists'
246 let value = ''
247 let ignoreCase: boolean | null = null
248 const opCode = selector.charCodeAt(i)
249 if (opCode === 61 /* = */) {
250 action = 'equals'
251 i++
252 }
253 else if (opCode === 33 /* ! */ && selector.charCodeAt(i + 1) === 61) {
254 action = 'not'
255 i += 2
256 }
257 else {
258 const a = actionFromChar(opCode)
259 if (a !== null && selector.charCodeAt(i + 1) === 61) {
260 action = a as any
261 i += 2
262 }
263 }
264 if (action !== 'exists') {
265 i = stripWS(selector, i)
266 const q = selector.charCodeAt(i)
267 if (q === 34 /* " */ || q === 39 /* ' */) {
268 const end = findEndOfString(selector, i + 1, q)
269 value = unescapeIfNeeded(selector.slice(i + 1, end))
270 i = end + 1
271 }
272 else {
273 const r = readName(selector, i)
274 value = r.value
275 i = r.end
276 }
277 i = stripWS(selector, i)
278 const flag = selector.charCodeAt(i)
279 if (flag === 105 /* i */ || flag === 73 /* I */) {
280 ignoreCase = true
281 i++
282 }
283 else if (flag === 115 /* s */ || flag === 83 /* S */) {
284 ignoreCase = false
285 i++
286 }
287 }
288 if (selector.charCodeAt(i) !== 93 /* ] */)
289 throw new Error('Expected ]')
290 i++
291 if (ignoreCase === null && !xmlMode && ATTRIBUTES_QUIRKS.has(attribute.toLowerCase()))
292 ignoreCase = 'quirks' as any
293 void options
294 void len
295 tokens.push({
296 type: 'attribute',
297 name: lowerCaseAttrs ? attribute.toLowerCase() : attribute,
298 action,
299 value,
300 namespace: null,
301 ignoreCase,
302 })
303 return i
304}
305
306function parsePseudo(selector: string, idx: number, tokens: Selector[], options: ParseOptions): number {
307 if (selector.charCodeAt(idx + 1) === 58 /* : */) {
308 let i = idx + 2
309 const r = readName(selector, i)
310 i = r.end
311 const name = r.value.toLowerCase()
312 let data: string | null = null
313 if (selector.charCodeAt(i) === 40 /* ( */) {
314 const end = findClose(selector, i)
315 data = selector.slice(i + 1, end).trim()
316 i = end + 1
317 }
318 tokens.push({ type: 'pseudo-element', name, data })
319 return i
320 }
321 let i = idx + 1
322 const r = readName(selector, i)
323 i = r.end
324 const name = r.value.toLowerCase()
325 if (selector.charCodeAt(i) === 40 /* ( */) {
326 const end = findClose(selector, i)
327 const inner = selector.slice(i + 1, end)
328 i = end + 1
329 if (name === 'is' || name === 'not' || name === 'where' || name === 'has' || name === 'matches' || name === '-moz-any' || name === '-webkit-any') {
330 const sub: Selector[][] = []
331 parseSelectorImpl(sub, inner.trim(), options, 0)
332 tokens.push({ type: 'pseudo', name, data: sub })
333 }
334 else {
335 tokens.push({ type: 'pseudo', name, data: inner.trim() })
336 }
337 }
338 else {
339 tokens.push({ type: 'pseudo', name, data: null })
340 }
341 return i
342}
343
344function addTraversal(tokens: Selector[], type: 'adjacent' | 'child' | 'descendant' | 'parent' | 'sibling' | 'column-combinator'): void {
345 if (tokens.length > 0 && tokens[tokens.length - 1]!.type === 'descendant' && type !== 'descendant')
346 tokens.pop()
347 if (tokens.length > 0 && (tokens[tokens.length - 1]!.type === type))
348 return
349 tokens.push({ type } as any)
350}
351
352function findEndOfString(selector: string, start: number, qCode: number): number {
353 let i = start
354 const len = selector.length
355 while (i < len) {
356 const c = selector.charCodeAt(i)
357 if (c === 92 /* \ */) {
358 i += 2
359 continue
360 }
361 if (c === qCode)
362 return i
363 i++
364 }
365 throw new Error('Unterminated string')
366}
367
368// eslint-disable-next-line pickier/no-unused-vars
369function findClose(selector: string, openParen: number): number {
370 let depth = 1
371 let i = openParen + 1
372 const len = selector.length
373 while (i < len) {
374 const c = selector.charCodeAt(i)
375 if (c === 92 /* \ */) { i += 2; continue }
376 if (c === 34 /* " */ || c === 39 /* ' */) {
377 i = findEndOfString(selector, i + 1, c) + 1
378 continue
379 }
380 if (c === 40 /* ( */) depth++
381 else if (c === 41 /* ) */) {
382 depth--
383 if (depth === 0) return i
384 }
385 i++
386 }
387 throw new Error('Unterminated parenthesis')
388}
packages/toolkit/src/what/stringify.tsadded+94-0
Changes to packages/toolkit/src/what/stringify.ts
@@ -0,0 +1,94 @@
1/**
2 * Selector AST → string. Inverse of `parse`.
3 */
4
5import type { Selector } from './types'
6
7const COMBINATORS: Record<string, string> = {
8 child: ' > ',
9 parent: ' < ',
10 sibling: ' ~ ',
11 adjacent: ' + ',
12 descendant: ' ',
13 'column-combinator': ' || ',
14}
15
16export function stringify(selector: Selector[][]): string {
17 return selector.map(stringifySegments).join(', ')
18}
19
20function stringifySegments(tokens: Selector[]): string {
21 return tokens.map((t, i) => stringifyOne(t, tokens[i - 1])).join('')
22}
23
24// eslint-disable-next-line pickier/no-unused-vars
25function stringifyOne(token: Selector, _prev?: Selector): string {
26 switch (token.type) {
27 case 'tag': return `${nsPrefix(token.namespace)}${escapeIdent(token.name)}`
28 case 'universal': return `${nsPrefix(token.namespace)}*`
29 case 'attribute': {
30 if (token.name === 'id' && token.action === 'equals' && !token.ignoreCase && !token.namespace)
31 return `#${escapeIdent(token.value)}`
32 if (token.name === 'class' && token.action === 'element' && !token.ignoreCase && !token.namespace)
33 return `.${escapeIdent(token.value)}`
34 let out = `[${nsPrefix(token.namespace)}${escapeIdent(token.name)}`
35 if (token.action !== 'exists') {
36 const op = ACTION_OP[token.action] ?? '='
37 out += op
38 out += `"${token.value.replace(/"/g, '\\"')}"`
39 if (token.ignoreCase === true)
40 out += ' i'
41 else if (token.ignoreCase === false)
42 out += ' s'
43 }
44 out += ']'
45 return out
46 }
47 case 'pseudo':
48 if (token.data === null)
49 return `:${token.name}`
50 if (typeof token.data === 'string')
51 return `:${token.name}(${token.data})`
52 return `:${token.name}(${stringify(token.data as Selector[][])})`
53 case 'pseudo-element':
54 return token.data === null ? `::${token.name}` : `::${token.name}(${token.data})`
55 case 'descendant':
56 case 'child':
57 case 'parent':
58 case 'sibling':
59 case 'adjacent':
60 case 'column-combinator':
61 return COMBINATORS[token.type] ?? ' '
62 }
63 return ''
64}
65
66// eslint-disable-next-line pickier/no-unused-vars
67const ACTION_OP: Record<string, string> = {
68 equals: '=',
69 element: '~=',
70 start: '^=',
71 end: '$=',
72 any: '*=',
73 not: '!=',
74 hyphen: '|=',
75}
76
77function nsPrefix(ns: string | null): string {
78 if (ns === null)
79 return ''
80 if (ns === '')
81 return '|'
82 return `${escapeIdent(ns)}|`
83}
84
85// Valid CSS identifier chars: ASCII letters, digits, `-`, `_`, and any
86// non-ASCII (>= 0x80). Anything else needs an escape. `\W` rejects
87// non-ASCII, so we use an explicit positive set.
88const RE_INVALID_ID_CHAR = /[^\w°-￿-]/g
89
90function escapeIdent(name: string): string {
91 if (name === '')
92 return ''
93 return name.replace(RE_INVALID_ID_CHAR, m => `\\${m}`)
94}
packages/toolkit/src/what/traversal.tsadded+8-0
Changes to packages/toolkit/src/what/traversal.ts
@@ -0,0 +1,8 @@
1import type { Selector } from './types'
2
3const TRAVERSAL_TYPES = new Set(['adjacent', 'child', 'descendant', 'parent', 'sibling', 'column-combinator'])
4
5/** True when `token` is a traversal/combinator segment (vs. simple selector). */
6export function isTraversal(token: Selector): boolean {
7 return TRAVERSAL_TYPES.has(token.type)
8}
packages/toolkit/src/what/types.tsadded+93-0
Changes to packages/toolkit/src/what/types.ts
@@ -0,0 +1,93 @@
1/**
2 * Selector AST mirroring css-what v6 — the segment-list-of-lists shape
3 * `parse(selector)` returns. Compatible with css-select consumers.
4 */
5
6export type SelectorType
7 = | 'attribute'
8 | 'pseudo'
9 | 'pseudo-element'
10 | 'tag'
11 | 'universal'
12 | 'adjacent'
13 | 'child'
14 | 'descendant'
15 | 'parent'
16 | 'sibling'
17 | 'column-combinator'
18
19export type AttributeAction
20 = | 'any'
21 | 'element'
22 | 'end'
23 | 'equals'
24 | 'exists'
25 | 'hyphen'
26 | 'not'
27 | 'start'
28
29export const IgnoreCaseMode: Readonly<Record<string, true | false | 'quirks' | null>> = {
30 Unknown: null,
31 QuirksMode: 'quirks',
32 IgnoreCase: true,
33 CaseSensitive: false,
34} as const
35
36export type IgnoreCase = boolean | 'quirks' | null
37
38export interface AttributeSelectorNode {
39 type: 'attribute'
40 name: string
41 action: AttributeAction
42 value: string
43 namespace: string | null
44 ignoreCase: IgnoreCase
45}
46
47export interface TagSelectorNode {
48 type: 'tag'
49 name: string
50 namespace: string | null
51}
52
53export interface UniversalSelectorNode {
54 type: 'universal'
55 namespace: string | null
56}
57
58export interface PseudoSelectorNode {
59 type: 'pseudo'
60 name: string
61 data: string | Selector[][] | null
62}
63
64export interface PseudoElementNode {
65 type: 'pseudo-element'
66 name: string
67 data: string | null
68}
69
70export interface TraversalNode {
71 type: 'adjacent' | 'child' | 'descendant' | 'parent' | 'sibling' | 'column-combinator'
72}
73
74export type Selector
75 = | AttributeSelectorNode
76 | TagSelectorNode
77 | UniversalSelectorNode
78 | PseudoSelectorNode
79 | PseudoElementNode
80 | TraversalNode
81
82export interface ParseOptions {
83 /**
84 * When true, attribute names retain their original case.
85 * css-what default mirrors HTML quirks-mode where attributes are
86 * case-insensitive unless explicitly flagged.
87 */
88 xmlMode?: boolean
89 /** Lowercase attribute names. */
90 lowerCaseAttributeNames?: boolean
91 /** Lowercase tag names. */
92 lowerCaseTags?: boolean
93}
packages/toolkit/test/fixtures.test.tsadded+52-0
Changes to packages/toolkit/test/fixtures.test.ts
@@ -0,0 +1,52 @@
1/**
2 * Real-world CSS fixtures.
3 *
4 * For each fixture we verify three properties:
5 * 1. parse + generate round-trip is stable (re-parsing the output
6 * produces the same string).
7 * 2. minify produces output strictly smaller than input.
8 * 3. minify is itself a fixed point (re-minifying doesn't change anything).
9 */
10
11import { describe, expect, it } from 'bun:test'
12import { readFileSync } from 'node:fs'
13import { join } from 'node:path'
14import { minify } from '../src/optimize'
15import { generate, parse } from '../src/parse'
16
17const FIXTURES = ['reset.css', 'utilities.css', 'component.css']
18
19describe('fixtures', () => {
20 for (const name of FIXTURES) {
21 describe(name, () => {
22 const source = readFileSync(join(import.meta.dir, 'fixtures', name), 'utf8')
23
24 it('parse + generate round-trips stably', () => {
25 const out1 = generate(parse(source))
26 const out2 = generate(parse(out1))
27 expect(out1).toBe(out2)
28 })
29
30 it('minify shrinks the source meaningfully', () => {
31 const { css } = minify(source)
32 expect(css.length).toBeLessThan(source.length)
33 // sanity floor — the minified output should keep the basic structure
34 expect(css).toContain('{')
35 expect(css).toContain('}')
36 })
37
38 it('minify is idempotent', () => {
39 const a = minify(source).css
40 const b = minify(a).css
41 expect(b).toBe(a)
42 })
43
44 it('minify keeps `/*!*/` comments by default', () => {
45 if (source.includes('/*!')) {
46 const { css } = minify(source)
47 expect(css).toContain('/*!')
48 }
49 })
50 })
51 }
52})
packages/toolkit/test/fixtures/component.cssadded+88-0
Changes to packages/toolkit/test/fixtures/component.css
@@ -0,0 +1,88 @@
1/*! design-system-style component fixture */
2
3:root {
4 --color-primary: #3b82f6;
5 --color-primary-hover: #2563eb;
6 --color-text: #111827;
7 --color-text-muted: #6b7280;
8 --color-bg: #ffffff;
9 --color-border: #e5e7eb;
10 --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
11 --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
12 --radius-sm: 0.25rem;
13 --radius-md: 0.5rem;
14 --space-1: 0.25rem;
15 --space-2: 0.5rem;
16 --space-4: 1rem;
17}
18
19@media (prefers-color-scheme: dark) {
20 :root {
21 --color-text: #f9fafb;
22 --color-text-muted: #9ca3af;
23 --color-bg: #111827;
24 --color-border: #374151;
25 }
26}
27
28.btn {
29 display: inline-flex;
30 align-items: center;
31 justify-content: center;
32 padding: 0.5rem 1rem;
33 border: 1px solid var(--color-border);
34 border-radius: var(--radius-md);
35 background-color: var(--color-bg);
36 color: var(--color-text);
37 font-weight: 500;
38 font-size: 0.875rem;
39 line-height: 1.25rem;
40 cursor: pointer;
41 transition: background-color 150ms ease-out, border-color 150ms ease-out;
42}
43.btn:hover { background-color: var(--color-primary); border-color: var(--color-primary); color: #fff; }
44.btn:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
45.btn:disabled { opacity: 0.5; cursor: not-allowed; }
46
47.btn--primary { background-color: var(--color-primary); color: #fff; border-color: transparent; }
48.btn--primary:hover { background-color: var(--color-primary-hover); }
49.btn--small { padding: 0.25rem 0.5rem; font-size: 0.75rem; }
50.btn--icon-only { padding: 0.5rem; aspect-ratio: 1 / 1; }
51
52.card {
53 background: var(--color-bg);
54 border: 1px solid var(--color-border);
55 border-radius: var(--radius-md);
56 box-shadow: var(--shadow-sm);
57 overflow: hidden;
58}
59.card > .card__header { padding: var(--space-4); border-bottom: 1px solid var(--color-border); font-weight: 600; }
60.card > .card__body { padding: var(--space-4); }
61.card > .card__footer { padding: var(--space-4); border-top: 1px solid var(--color-border); display: flex; justify-content: flex-end; gap: var(--space-2); }
62
63@keyframes fade-in {
64 from { opacity: 0; transform: translateY(8px); }
65 to { opacity: 1; transform: translateY(0); }
66}
67.fade-in { animation: fade-in 200ms ease-out; }
68
69@supports (backdrop-filter: blur(8px)) {
70 .glass { backdrop-filter: blur(8px); background-color: rgba(255, 255, 255, 0.7); }
71}
72
73@container (min-width: 400px) {
74 .card { padding: var(--space-4); }
75}
76
77.icon { width: 1em; height: 1em; flex-shrink: 0; fill: currentColor; }
78.icon--sm { width: 0.75em; height: 0.75em; }
79.icon--lg { width: 1.5em; height: 1.5em; }
80
81.list { list-style: none; margin: 0; padding: 0; }
82.list > li + li { margin-top: var(--space-2); }
83.list a:hover { text-decoration: underline; text-decoration-thickness: 2px; }
84
85@media (max-width: 600px) {
86 .card { border-radius: 0; box-shadow: none; }
87 .btn { width: 100%; }
88}
packages/toolkit/test/fixtures/reset.cssadded+50-0
Changes to packages/toolkit/test/fixtures/reset.css
@@ -0,0 +1,50 @@
1/*! normalize-style reset (excerpt). */
2*, *::before, *::after { box-sizing: border-box; }
3html {
4 -webkit-text-size-adjust: 100%;
5 -moz-tab-size: 4;
6 tab-size: 4;
7 line-height: 1.15;
8}
9body {
10 margin: 0;
11 font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
12}
13hr { box-sizing: content-box; height: 0; overflow: visible; }
14abbr[title] { text-decoration: underline dotted; }
15b, strong { font-weight: bolder; }
16code, kbd, samp, pre {
17 font-family: ui-monospace, SFMono-Regular, "SF Mono", monospace;
18 font-size: 1em;
19}
20small { font-size: 80%; }
21sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
22sub { bottom: -0.25em; }
23sup { top: -0.5em; }
24table { border-color: inherit; text-indent: 0; }
25button, input, optgroup, select, textarea {
26 font-family: inherit;
27 font-size: 100%;
28 line-height: 1.15;
29 margin: 0;
30}
31button, [type='button'], [type='reset'], [type='submit'] {
32 -webkit-appearance: button;
33}
34::-moz-focus-inner { border-style: none; padding: 0; }
35:-moz-focusring { outline: 1px dotted ButtonText; }
36:-moz-ui-invalid { box-shadow: none; }
37legend {
38 box-sizing: border-box;
39 color: inherit;
40 display: table;
41 max-width: 100%;
42 padding: 0;
43 white-space: normal;
44}
45progress { vertical-align: baseline; }
46::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; }
47[type='search'] { -webkit-appearance: textfield; outline-offset: -2px; }
48::-webkit-search-decoration { -webkit-appearance: none; }
49::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; }
50summary { display: list-item; }
packages/toolkit/test/fixtures/utilities.cssadded+64-0
Changes to packages/toolkit/test/fixtures/utilities.css
@@ -0,0 +1,64 @@
1/*! tailwind-style utility excerpt */
2.flex { display: flex; }
3.inline-flex { display: inline-flex; }
4.grid { display: grid; }
5.hidden { display: none; }
6
7.flex-col { flex-direction: column; }
8.flex-row { flex-direction: row; }
9.flex-wrap { flex-wrap: wrap; }
10.items-center { align-items: center; }
11.justify-center { justify-content: center; }
12.justify-between { justify-content: space-between; }
13
14.gap-1 { gap: 0.25rem; }
15.gap-2 { gap: 0.5rem; }
16.gap-4 { gap: 1rem; }
17.gap-8 { gap: 2rem; }
18
19.p-0 { padding: 0; }
20.p-1 { padding: 0.25rem; }
21.p-2 { padding: 0.5rem; }
22.p-4 { padding: 1rem; }
23.p-8 { padding: 2rem; }
24
25.text-xs { font-size: 0.75rem; line-height: 1rem; }
26.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
27.text-base { font-size: 1rem; line-height: 1.5rem; }
28.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
29.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
30
31.bg-white { background-color: #ffffff; }
32.bg-black { background-color: #000000; }
33.bg-red-500 { background-color: #ef4444; }
34.bg-blue-500 { background-color: #3b82f6; }
35.bg-green-500 { background-color: #22c55e; }
36
37.text-white { color: #ffffff; }
38.text-black { color: #000000; }
39.text-gray-500 { color: #6b7280; }
40
41.rounded { border-radius: 0.25rem; }
42.rounded-md { border-radius: 0.375rem; }
43.rounded-lg { border-radius: 0.5rem; }
44.rounded-full { border-radius: 9999px; }
45
46.shadow { box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); }
47.shadow-md { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); }
48.shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); }
49
50.transition { transition-property: all; transition-duration: 150ms; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
51.duration-300 { transition-duration: 300ms; }
52
53@media (min-width: 640px) {
54 .sm\:flex { display: flex; }
55 .sm\:hidden { display: none; }
56 .sm\:p-4 { padding: 1rem; }
57}
58@media (min-width: 768px) {
59 .md\:grid { display: grid; }
60 .md\:hidden { display: none; }
61}
62@media (min-width: 1024px) {
63 .lg\:flex { display: flex; }
64}
packages/toolkit/test/optimize.test.tsadded+92-0
Changes to packages/toolkit/test/optimize.test.ts
@@ -0,0 +1,92 @@
1import { describe, expect, it } from 'bun:test'
2import { minify, minifyBlock, syntax } from '../src/optimize'
3import { parse } from '../src/parse'
4
5describe('minify', () => {
6 it('strips comments', () => {
7 expect(minify('/* hi */ .a{color:red}').css).toBe('.a{color:red}')
8 })
9 it('preserves /*!*/ comments by default', () => {
10 expect(minify('/*! keep */.a{color:red}').css).toContain('keep')
11 })
12 it('drops `/*!*/` when comments:false', () => {
13 expect(minify('/*! keep */.a{color:red}', { comments: false }).css).toBe('.a{color:red}')
14 })
15 it('shortens decimals', () => {
16 expect(minify('.a{margin:0.5em 10.000px}').css).toBe('.a{margin:.5em 10px}')
17 })
18 it('drops zero units', () => {
19 expect(minify('.a{margin:0px 0em}').css).toBe('.a{margin:0 0}')
20 })
21 it('dedupes declarations keeping the later one', () => {
22 expect(minify('.a{color:red;color:blue}').css).toBe('.a{color:blue}')
23 })
24 it('respects !important when deduping', () => {
25 expect(minify('.a{color:red!important;color:blue}').css).toBe('.a{color:red!important}')
26 })
27 it('shortens hex colors', () => {
28 expect(minify('.a{color:#aabbcc}').css).toBe('.a{color:#abc}')
29 })
30
31 it('shortens named colors when hex is strictly shorter', () => {
32 // black (5) → #000 (4): replace
33 expect(minify('.a{color:black}').css).toBe('.a{color:#000}')
34 // white (5) → #fff (4): replace
35 expect(minify('.a{color:white}').css).toBe('.a{color:#fff}')
36 })
37
38 it('keeps named colors that tie or beat hex', () => {
39 // blue (4) ties #00f (4) — keep keyword
40 expect(minify('.a{color:blue}').css).toBe('.a{color:blue}')
41 // red (3) beats #f00 (4) — keep keyword
42 expect(minify('.a{color:red}').css).toBe('.a{color:red}')
43 })
44
45 it('only swaps colors inside color properties', () => {
46 // font-family is not a color property — `red` stays
47 expect(minify('.a{font-family:red}').css).toBe('.a{font-family:red}')
48 })
49
50 it('reverses hex → name when name is strictly shorter', () => {
51 // #ff0000 (7) → red (3): replace
52 expect(minify('.a{color:#ff0000}').css).toBe('.a{color:red}')
53 })
54})
55
56describe('minifyBlock', () => {
57 it('minifies declaration lists in isolation', () => {
58 expect(minifyBlock('color:red;color:blue').css).toBe('color:blue')
59 })
60})
61
62describe('syntax.specificity', () => {
63 it('counts ids', () => {
64 const sel = parse('#a', { context: 'selector' })
65 expect(syntax.specificity(sel)).toEqual([1, 0, 0])
66 })
67 it('counts classes and attributes', () => {
68 const sel = parse('.a[type=button]', { context: 'selector' })
69 expect(syntax.specificity(sel)).toEqual([0, 2, 0])
70 })
71 it('counts type selectors', () => {
72 const sel = parse('div p', { context: 'selector' })
73 expect(syntax.specificity(sel)).toEqual([0, 0, 2])
74 })
75 it(':where() contributes nothing', () => {
76 const sel = parse(':where(#a) p', { context: 'selector' })
77 expect(syntax.specificity(sel)).toEqual([0, 0, 1])
78 })
79 it(':is() takes max of args', () => {
80 const sel = parse(':is(#a, .b)', { context: 'selector' })
81 expect(syntax.specificity(sel)).toEqual([1, 0, 0])
82 })
83 it('accepts a raw selector string', () => {
84 expect(syntax.specificity('#a.b div')).toEqual([1, 1, 1])
85 expect(syntax.specificity('p > span:not(#x)')).toEqual([1, 0, 2])
86 })
87 it('accepts cssWhat-style Selector[][] arrays', async () => {
88 const { parse: parseWhat } = await import('../src/what')
89 const ast = parseWhat('#a.b div')
90 expect(syntax.specificity(ast)).toEqual([1, 1, 1])
91 })
92})
packages/toolkit/test/parse.test.tsadded+203-0
Changes to packages/toolkit/test/parse.test.ts
@@ -0,0 +1,203 @@
1import { describe, expect, it } from 'bun:test'
2import { generate, List, parse, walk } from '../src/parse'
3
4describe('parse + generate round-trip', () => {
5 it('parses a basic stylesheet', () => {
6 const ast = parse('.foo { color: red }') as any
7 expect(ast.type).toBe('StyleSheet')
8 expect(ast.children.first.type).toBe('Rule')
9 })
10
11 it('keeps `/*!*/` comments at generate time but strips others', () => {
12 // matches css-tree behaviour: regular comments are parsed (so plugins
13 // can see them) but NOT regenerated by default. `!`-comments survive.
14 expect(generate(parse('/* hi */.a{color:red}'))).toBe('.a{color:red}')
15 expect(generate(parse('/*! keep */.a{color:red}'))).toBe('/*! keep */.a{color:red}')
16 })
17
18 it('preserves !important', () => {
19 const css = '.a{color:red!important}'
20 expect(generate(parse(css))).toBe(css)
21 })
22
23 it('parses comma-separated selectors', () => {
24 const ast = parse('.a, .b, .c {}') as any
25 const list = ast.children.first.prelude
26 expect(list.type).toBe('SelectorList')
27 expect(list.children.toArray().length).toBe(3)
28 })
29
30 it('parses combinators', () => {
31 const ast = parse('.a > .b + .c ~ .d {}') as any
32 const sel = ast.children.first.prelude.children.first
33 const types = sel.children.toArray().map((n: any) => n.type)
34 expect(types).toContain('Combinator')
35 })
36
37 it('parses an at-rule with block', () => {
38 const ast = parse('@media (min-width: 800px) { .a { color: red } }') as any
39 expect(ast.children.first.type).toBe('Atrule')
40 expect(ast.children.first.name).toBe('media')
41 })
42
43 it('parses CSS Nesting (& mid-selector)', () => {
44 const ast = parse('.foo { color: red; &.bar { color: blue } }') as any
45 const blockChildren = ast.children.first.block.children.toArray().map((n: any) => n.type)
46 expect(blockChildren).toEqual(['Declaration', 'Rule'])
47 })
48
49 it('parses namespace selectors svg|circle', () => {
50 const ast = parse('svg|circle{}') as any
51 const tag = ast.children.first.prelude.children.first.children.first
52 expect(tag.type).toBe('TypeSelector')
53 expect(tag.name).toBe('svg|circle')
54 })
55
56 it('promotes Number/Number to Ratio in media queries', () => {
57 const ast = parse('@media (aspect-ratio: 16/9) {}') as any
58 let foundRatio: any = null
59 walk(ast, (n) => {
60 if (n.type === 'Ratio') foundRatio = n
61 })
62 expect(foundRatio).not.toBeNull()
63 expect(foundRatio.left.value).toBe('16')
64 expect(foundRatio.right.value).toBe('9')
65 })
66
67 it('parses unknown at-rule preludes (no hardcoded whitelist)', () => {
68 const ast = parse('@my-custom rule (foo) { .a { color: red } }') as any
69 const at = ast.children.first
70 expect(at.type).toBe('Atrule')
71 expect(at.name).toBe('my-custom')
72 // Prelude should be parsed structurally (AtrulePrelude with children),
73 // not stored as a Raw blob.
74 expect(at.prelude.type).toBe('AtrulePrelude')
75 expect(at.prelude.children.toArray().length).toBeGreaterThan(0)
76 })
77
78 it('parses @scope, @starting-style, and @container with nested rules', () => {
79 for (const name of ['scope', 'starting-style', 'container']) {
80 const ast = parse(`@${name} (foo) { .x { color: blue } }`) as any
81 const at = ast.children.first
82 expect(at.type).toBe('Atrule')
83 expect(at.block.children.first.type).toBe('Rule')
84 }
85 })
86
87 it('calls onParseError for malformed declarations', () => {
88 const errors: string[] = []
89 parse('.a { invalid-no-colon; color: red; }', {
90 onParseError: (err) => { errors.push(err.message) },
91 })
92 expect(errors.length).toBeGreaterThan(0)
93 })
94
95 it('parses url() function', () => {
96 const ast = parse('.a{background:url("x.png")}') as any
97 let foundUrl = false
98 walk(ast, (n) => {
99 if (n.type === 'Url') foundUrl = true
100 })
101 expect(foundUrl).toBe(true)
102 })
103
104 it('walk.skip stops traversal into a node', () => {
105 const ast = parse('.a{color:red} .b{color:blue}')
106 const seen: string[] = []
107 walk(ast, function (n) {
108 if (n.type === 'Rule')
109 seen.push('rule')
110 if (n.type === 'Block')
111 return walk.skip
112 })
113 expect(seen.length).toBe(2)
114 })
115
116 it('parses declarationList context', () => {
117 const ast = parse('color:red;font-size:12px', { context: 'declarationList' }) as any
118 expect(ast.type).toBe('DeclarationList')
119 expect(ast.children.toArray().length).toBe(2)
120 })
121
122 it('narrows visitor.enter via visit filter', () => {
123 const ast = parse('.a{color:red} .b{color:blue}')
124 const propsByRule: string[][] = []
125 walk(ast, {
126 visit: 'Rule',
127 enter(rule) {
128 // `rule` is narrowed to Rule here — TS should NOT see the wider union.
129 const props: string[] = []
130 walk(rule.block, {
131 visit: 'Declaration',
132 enter(decl) {
133 // `decl` narrowed to Declaration
134 props.push(decl.property)
135 },
136 })
137 propsByRule.push(props)
138 },
139 })
140 expect(propsByRule).toEqual([['color'], ['color']])
141 })
142
143 it('exposes a doubly linked List', () => {
144 const list = new List<number>()
145 list.appendData(1)
146 list.appendData(2)
147 list.appendData(3)
148 expect(list.toArray()).toEqual([1, 2, 3])
149 list.shift()
150 expect(list.toArray()).toEqual([2, 3])
151 })
152
153 it('round-trips strings with escape sequences', () => {
154 const cases = [
155 '.a{content:"with \\"quote\\" inside"}',
156 '.a{content:"line1\\a line2"}',
157 'a[data-x="\\"x\\""]{color:red}',
158 ]
159 for (const c of cases) {
160 const out1 = generate(parse(c))
161 const out2 = generate(parse(out1))
162 expect(out1).toBe(out2)
163 }
164 })
165
166 it('decodes hex escapes in string values', () => {
167 const ast = parse('.a{content:"\\26 B"}') as any
168 const decl = ast.children.first.block.children.first
169 const string = decl.value.children.first
170 expect(string.type).toBe('String')
171 expect(string.value).toBe('&B')
172 })
173
174 it('round-trips selector identifier escapes', () => {
175 const cases = [
176 '.\\26 B{color:red}',
177 '.foo\\30 bar{color:red}',
178 '.foo\\!bar{color:red}',
179 ]
180 for (const c of cases) {
181 const out1 = generate(parse(c))
182 const out2 = generate(parse(out1))
183 expect(out1).toBe(out2)
184 }
185 })
186
187 it('survives nested forEach with mid-walk removal', () => {
188 const list = new List<number>()
189 list.fromArray([1, 2, 3, 4, 5])
190 const seen: number[] = []
191 list.forEach((data, item) => {
192 seen.push(data)
193 // a nested walk over the SAME list — must not corrupt the outer cursor.
194 list.forEach((d) => { void d })
195 if (data === 2) {
196 // remove the current node mid-iteration
197 list.remove(item)
198 }
199 })
200 expect(seen).toEqual([1, 2, 3, 4, 5])
201 expect(list.toArray()).toEqual([1, 3, 4, 5])
202 })
203})
packages/toolkit/test/select.test.tsadded+131-0
Changes to packages/toolkit/test/select.test.ts
@@ -0,0 +1,131 @@
1import type { Adapter, Options } from '../src/select'
2import { describe, expect, it } from 'bun:test'
3import { is, selectAll, selectOne } from '../src/select'
4
5interface El { type: 'el', name: string, attrs: Record<string, string>, children: El[], parent: El | null }
6
7function el(name: string, attrs: Record<string, string> = {}, ...children: El[]): El {
8 const e: El = { type: 'el', name, attrs, children, parent: null }
9 for (const c of children) c.parent = e
10 return e
11}
12
13const adapter: Adapter<El, El> = {
14 isTag: (n: any): n is El => n && n.type === 'el',
15 existsOne: (test, elems) => elems.some(e => adapter.isTag(e) && (test(e) || adapter.existsOne(test, e.children))),
16 getAttributeValue: (e, n) => e.attrs[n],
17 getChildren: (e: any) => e.children ?? [],
18 getName: e => e.name,
19 getParent: (e: any) => e.parent ?? null,
20 getSiblings: (e: any) => e.parent ? e.parent.children : [e],
21 getText: () => '',
22 hasAttrib: (e, n) => n in e.attrs,
23 removeSubsets: n => n,
24 findAll: (test, elems) => {
25 const out: El[] = []
26 for (const e of elems) {
27 if (adapter.isTag(e)) {
28 if (test(e)) out.push(e)
29 out.push(...adapter.findAll!(test, e.children))
30 }
31 }
32 return out
33 },
34 findOne: (test, elems) => {
35 for (const e of elems) {
36 if (adapter.isTag(e)) {
37 if (test(e)) return e
38 const r = adapter.findOne!(test, e.children)
39 if (r) return r
40 }
41 }
42 return null
43 },
44}
45
46const opts: Options<El, El> = { adapter, xmlMode: true }
47
48const tree = el('div', { class: 'root' },
49 el('p', { id: 'a' }, el('span', { class: 'foo bar' })),
50 el('p', { id: 'b' },
51 el('span', { class: 'foo' }),
52 el('span', { class: 'bar' }),
53 ),
54)
55
56describe('selectAll', () => {
57 it('matches by tag name', () => {
58 expect(selectAll('span', tree, opts).length).toBe(3)
59 })
60 it('matches by class', () => {
61 expect(selectAll('.foo', tree, opts).length).toBe(2)
62 })
63 it('matches by id', () => {
64 expect(selectAll('#a', tree, opts).length).toBe(1)
65 })
66 it('matches child combinator', () => {
67 expect(selectAll('p > span.foo', tree, opts).length).toBe(2)
68 })
69 it('matches descendant combinator', () => {
70 expect(selectAll('p#a span', tree, opts).length).toBe(1)
71 })
72 it('matches attribute ~=', () => {
73 expect(selectAll('[class~="bar"]', tree, opts).length).toBe(2)
74 })
75 it('matches :not()', () => {
76 expect(selectAll('p:not(#a)', tree, opts).length).toBe(1)
77 })
78 it('matches :has()', () => {
79 // querySelector-style: passing a single root excludes the root itself
80 // (matches css-select). Wrap in `[tree]` to opt into root-as-candidate.
81 expect(selectAll('div:has(span.bar)', [tree], opts).length).toBe(1)
82 })
83 it('matches :nth-child', () => {
84 expect(selectAll('p:nth-child(1)', tree, opts).length).toBe(1)
85 })
86 it('matches :first-child', () => {
87 expect(selectAll('span:first-child', tree, opts).length).toBe(2)
88 })
89 it('matches adjacent sibling', () => {
90 expect(selectAll('p + p', tree, opts).length).toBe(1)
91 })
92 it('matches general sibling', () => {
93 expect(selectAll('p ~ p', tree, opts).length).toBe(1)
94 })
95})
96
97describe('selectOne / is', () => {
98 it('selectOne returns the first match', () => {
99 expect(selectOne('.bar', tree, opts)).not.toBeNull()
100 })
101 it('is() tests a node directly', () => {
102 const span = tree.children[0]!.children[0]!
103 expect(is(span, '.foo', opts)).toBe(true)
104 expect(is(span, '.missing', opts)).toBe(false)
105 })
106})
107
108describe('compile cache', () => {
109 it('reuses compiled selectors with default options', async () => {
110 const { compile, clearSelectorCache } = await import('../src/select')
111 clearSelectorCache(adapter as any)
112 const a = compile('p > span.foo', opts)
113 const b = compile('p > span.foo', opts)
114 expect(a).toBe(b)
115 })
116
117 it('skips the cache when cacheResults is false', async () => {
118 const { compile } = await import('../src/select')
119 const noCache: typeof opts = { ...opts, cacheResults: false }
120 const a = compile('p > span', noCache)
121 const b = compile('p > span', noCache)
122 expect(a).not.toBe(b)
123 })
124
125 it('keys cache by xmlMode flag', async () => {
126 const { compile } = await import('../src/select')
127 const a = compile('p', opts)
128 const b = compile('p', { ...opts, xmlMode: false })
129 expect(a).not.toBe(b)
130 })
131})
packages/toolkit/test/what.test.tsadded+48-0
Changes to packages/toolkit/test/what.test.ts
@@ -0,0 +1,48 @@
1import { describe, expect, it } from 'bun:test'
2import { isTraversal, parse, stringify } from '../src/what'
3
4describe('what.parse', () => {
5 it('parses a tag selector', () => {
6 expect(parse('div')).toEqual([[{ type: 'tag', name: 'div', namespace: null }]])
7 })
8 it('parses a class selector as attribute selector', () => {
9 const ast = parse('.foo')
10 expect(ast[0]![0]).toMatchObject({ type: 'attribute', name: 'class', action: 'element', value: 'foo' })
11 })
12 it('parses combinators', () => {
13 const ast = parse('a > b + c ~ d')
14 const types = ast[0]!.map(x => x.type)
15 expect(types).toEqual(['tag', 'child', 'tag', 'adjacent', 'tag', 'sibling', 'tag'])
16 })
17 it('parses descendant combinator', () => {
18 const ast = parse('a b')
19 const types = ast[0]!.map(x => x.type)
20 expect(types).toEqual(['tag', 'descendant', 'tag'])
21 })
22 it('parses :not(...) with sub-selectors', () => {
23 const ast = parse(':not(.x, .y)')
24 expect(ast[0]![0]).toMatchObject({ type: 'pseudo', name: 'not' })
25 expect(Array.isArray((ast[0]![0] as any).data)).toBe(true)
26 })
27 it('parses pseudo-elements', () => {
28 expect(parse('::before')[0]![0]).toMatchObject({ type: 'pseudo-element', name: 'before' })
29 })
30})
31
32describe('what.stringify', () => {
33 it('round-trips simple selectors', () => {
34 const cases = ['div', '.a', '#x', 'a > b', '[type="button"]', ':hover', '::before']
35 for (const c of cases) {
36 const re = stringify(parse(c))
37 // Round-trip should produce something that re-parses to the same shape
38 expect(parse(re)).toEqual(parse(c))
39 }
40 })
41})
42
43describe('what.isTraversal', () => {
44 it('detects combinator types', () => {
45 expect(isTraversal({ type: 'descendant' } as any)).toBe(true)
46 expect(isTraversal({ type: 'tag', name: 'a', namespace: null })).toBe(false)
47 })
48})
packages/toolkit/ts-css.config.tsadded+8-0
Changes to packages/toolkit/ts-css.config.ts
@@ -0,0 +1,8 @@
1import type { CSSOptions } from './src/types'
2
3const config: CSSOptions = {
4 floatPrecision: 3,
5 verbose: false,
6}
7
8export default config
tsconfig.json packages/toolkit/tsconfig.jsoncopied+1-9
Changes to packages/toolkit/tsconfig.json
@@ -5,9 +5,6 @@
55 "moduleDetection": "force",
66 "module": "esnext",
77 "moduleResolution": "bundler",
8 "paths": {
9 "ts-css": ["./packages/ts-css/src/index.ts"]
10 },
118 "resolveJsonModule": true,
129 "types": ["bun"],
1310 "allowImportingTsExtensions": true,
@@ -23,10 +20,5 @@
2320 "verbatimModuleSyntax": true,
2421 "skipDefaultLibCheck": true,
2522 "skipLibCheck": true
26 },
27 "exclude": [
28 "pantry",
29 "packages/ts-css/examples/**/*",
30 "benchmark"
31 ]
23 }
3224}
packages/ts-css/PLUGIN.mdmodified+7-7
Changes to packages/ts-css/PLUGIN.md
@@ -5,7 +5,7 @@ A Bun plugin that automatically generates and injects Crosswind CSS into your HT
55## Installation
66
77```bash
8bun add ts-css
8bun add @ts-css/core
99```
1010
1111## Quick Start**1. Create your HTML file**(`src/template.html`)
@@ -32,7 +32,7 @@ document.body.innerHTML = template
3232```**3. Build with the plugin**:
3333
3434```typescript
35import { plugin } from 'ts-css'
35import { plugin } from '@ts-css/core'
3636
3737await Bun.build({
3838 entrypoints: ['./src/index.ts'],
@@ -49,7 +49,7 @@ await Bun.build({
4949
5050### Basic Configuration```typescript
5151
52import { plugin } from 'ts-css'
52import { plugin } from '@ts-css/core'
5353
5454await Bun.build({
5555 entrypoints: ['./src/index.ts'],
@@ -62,7 +62,7 @@ await Bun.build({
6262})
6363
6464```### Custom Theme```typescript
65import { plugin } from 'ts-css'
65import { plugin } from '@ts-css/core'
6666
6767await Bun.build({
6868 entrypoints: ['./src/index.ts'],
@@ -92,7 +92,7 @@ await Bun.build({
9292})
9393```### Advanced Configuration```typescript
9494
95import { plugin } from 'ts-css'
95import { plugin } from '@ts-css/core'
9696
9797await Bun.build({
9898 entrypoints: ['./src/index.ts'],
@@ -186,7 +186,7 @@ The plugin is highly performant:
186186## TypeScript Support
187187
188188The plugin is fully typed. Import the types:```typescript
189import type { TsCssPluginOptions } from 'ts-css'
189import type { TsCssPluginOptions } from '@ts-css/core'
190190
191191const options: TsCssPluginOptions = {
192192 config: {
@@ -197,7 +197,7 @@ const options: TsCssPluginOptions = {
197197
198198The plugin also respects`crosswind.config.ts`in your project root:```typescript
199199// crosswind.config.ts
200import type { TsCssOptions } from 'ts-css'
200import type { TsCssOptions } from '@ts-css/core'
201201
202202export default {
203203 minify: true,
packages/ts-css/README.mdmodified+6-6
Changes to packages/ts-css/README.md
@@ -5,7 +5,7 @@ A CSS engine with two front ends and one atomic output: Tailwind-compatible **ut
55## Installation
66
77```bash
8bun add ts-css
8bun add @ts-css/core
99```
1010
1111## Quick Start
@@ -13,7 +13,7 @@ bun add ts-css
1313### Programmatic API
1414
1515```typescript
16import { CSSGenerator, defaultConfig } from 'ts-css'
16import { CSSGenerator, defaultConfig } from '@ts-css/core'
1717
1818const gen = new CSSGenerator(defaultConfig)
1919gen.generate('flex')
@@ -32,7 +32,7 @@ const css = gen.toCSS(true) // true = include preflight
3232### Build API
3333
3434```typescript
35import { build } from 'ts-css'
35import { build } from '@ts-css/core'
3636
3737const result = await build({
3838 content: ['./src/**/*.html', './src/**/*.tsx'],
@@ -44,7 +44,7 @@ const result = await build({
4444### Style API
4545
4646```typescript
47import { css } from 'ts-css'
47import { css } from '@ts-css/core'
4848
4949const theme = css.defineVars({
5050 accent: '#0b7',
@@ -101,7 +101,7 @@ cssx build --minify
101101Create a `css.config.ts` in your project root:
102102
103103```typescript
104import type { TsCssConfig } from 'ts-css'
104import type { TsCssConfig } from '@ts-css/core'
105105
106106export default {
107107 content: ['./src/**/*.{html,tsx,stx}'],
@@ -588,7 +588,7 @@ Prefix with `!` to apply `!important`:
588588## Presets
589589
590590```typescript
591import type { Preset } from 'ts-css'
591import type { Preset } from '@ts-css/core'
592592
593593const myPreset: Preset = {
594594 name: 'my-preset',
packages/ts-css/bin/cli.tsmodified+1-1
Changes to packages/ts-css/bin/cli.ts
@@ -309,7 +309,7 @@ cli
309309 process.exit(1)
310310 }
311311
312 const defaultConfig = `import type { TsCssOptions } from 'ts-css'
312 const defaultConfig = `import type { TsCssOptions } from '@ts-css/core'
313313
314314const config = {
315315 content: ['./src/**/*.{html,js,ts,jsx,tsx,stx}'],
packages/ts-css/examples/plugin/README.mdmodified+2-2
Changes to packages/ts-css/examples/plugin/README.md
@@ -20,7 +20,7 @@ The plugin works by intercepting HTML imports in your TypeScript/JavaScript code
2020
2121### Basic Setup```typescript
2222
23import { plugin } from 'ts-css'
23import { plugin } from '@ts-css/core'
2424
2525await Bun.build({
2626 entrypoints: ['./src/index.ts'],
@@ -36,7 +36,7 @@ import html from './index.html'
3636document.body.innerHTML = html
3737```### With Custom Configuration```typescript
3838
39import { plugin } from 'ts-css'
39import { plugin } from '@ts-css/core'
4040
4141await Bun.build({
4242 entrypoints: ['./src/index.ts'],
packages/ts-css/examples/plugin/build.tsmodified+1-1
Changes to packages/ts-css/examples/plugin/build.ts
@@ -1,4 +1,4 @@
1import { plugin } from 'ts-css'
1import { plugin } from '@ts-css/core'
22
33// Build with the ts-css plugin
44// The plugin will automatically process HTML files that are imported in your TypeScript/JavaScript code
packages/ts-css/package.jsonmodified+2-1
Changes to packages/ts-css/package.json
@@ -1,5 +1,5 @@
11{
2 "name": "ts-css",
2 "name": "@ts-css/core",
33 "type": "module",
44 "version": "0.3.0",
55 "description": "A blazingly fast utility-first CSS engine with a StyleX-style typed API. Tailwind-compatible utilities, zero runtime.",
@@ -73,6 +73,7 @@
7373 },
7474 "dependencies": {
7575 "@stacksjs/clapp": "^0.2.0",
76 "@stacksjs/ts-css": "workspace:*",
7677 "bunfig": "^0.15.17"
7778 },
7879 "lint-staged": {
packages/ts-css/src/generator.tsmodified+7-10
Changes to packages/ts-css/src/generator.ts
@@ -1,5 +1,6 @@
11import type { CSSRule, TsCssConfig, ParsedClass } from './types'
22import type { UtilityRule } from './rules'
3import { minifyBlock } from '@stacksjs/ts-css/optimize'
34import { parseClass } from './parser'
45import { defaultConfig } from './config'
56import { builtInRules } from './rules'
@@ -1436,17 +1437,13 @@ const NOT_VARIANT_SELECTORS: Record<string, string> = {
14361437// around syntax characters. Used for preflight and keyframes, which were
14371438// previously only whitespace-collapsed (comments and per-declaration
14381439// spacing survived into "minified" output).
1440/**
1441 * Minification is delegated to the toolkit's AST-based optimiser rather than
1442 * squeezed with regexes. The old inline version could not tell a space inside
1443 * a string or `url()` from a space between tokens.
1444 */
14391445function minifyCSSBlock(css: string): string {
1440 return css
1441 .replace(/\/\*[\s\S]*?\*\//g, '')
1442 .replace(/\s+/g, ' ')
1443 .replace(/\s*\{\s*/g, '{')
1444 .replace(/\s*\}\s*/g, '}')
1445 .replace(/;\s*/g, ';')
1446 .replace(/:\s+/g, ':')
1447 .replace(/,\s+/g, ',')
1448 .replace(/;\}/g, '}')
1449 .trim()
1446 return minifyBlock(css).css
14501447}
14511448
14521449// Separator between stacked at-rule wrappers in a rule-group key
packages/ts-css/src/plugin.tsmodified+1-1
Changes to packages/ts-css/src/plugin.ts
@@ -22,7 +22,7 @@ export interface TsCssPluginOptions {
2222 *
2323 * @example
2424 * ```typescript
25 * import { plugin } from 'ts-css'
25 * import { plugin } from '@ts-css/core'
2626 *
2727 * await Bun.build({
2828 * entrypoints: ['./src/index.ts'],
packages/ts-css/src/style/index.tsmodified+1-1
Changes to packages/ts-css/src/style/index.ts
@@ -51,7 +51,7 @@ import { renderStyles, resetStyles } from './registry'
5151 *
5252 * @example
5353 * ```ts
54 * import { css } from 'ts-css'
54 * import { css } from '@ts-css/core'
5555 *
5656 * const styles = css.create({ card: { padding: 16 } })
5757 * <div {...css.props(styles.card)} />
packages/vscode/package.jsonmodified+1-1
Changes to packages/vscode/package.json
@@ -1,5 +1,5 @@
11{
2 "name": "ts-css-vscode",
2 "name": "@ts-css/vscode",
33 "version": "0.3.0",
44 "description": "VS Code extension for ts-css (placeholder package)",
55 "license": "MIT",
scripts/setup-publishing.shmodified+7-7
Changes to scripts/setup-publishing.sh
@@ -28,11 +28,11 @@ TAG="${1:-$(git tag --list 'v*' --sort=-v:refname | head -1)}"
2828VERSION="${TAG#v}"
2929
3030echo "Repository : $REPO"
31echo "Release tag: $TAG (ts-css@$VERSION)"
31echo "Release tag: $TAG (@ts-css/core@$VERSION)"
3232echo
3333echo "Paste the npm token, then press Enter. It will not be displayed."
3434echo "It needs permission to CREATE packages (Automation, or granular with"
35echo "read/write on all packages) — 'ts-css' does not exist on npm yet."
35echo "read/write on all packages) — '@ts-css/core' does not exist on npm yet."
3636printf 'npm token: '
3737read -rs NPM_TOKEN_VALUE
3838echo
@@ -62,7 +62,7 @@ if [[ "$WHOAMI_STATUS" != "200" ]]; then
6262 echo "npm rejected the token (HTTP $WHOAMI_STATUS)." >&2
6363 echo "Create a fresh one at https://www.npmjs.com/settings/~/tokens" >&2
6464 echo "It must be an Automation token, or granular with read/write on ALL" >&2
65 echo "packages — 'ts-css' does not exist yet, so a token scoped to existing" >&2
65 echo "packages — '@ts-css/core' does not exist yet, so a token scoped to existing" >&2
6666 echo "packages cannot create it." >&2
6767 exit 1
6868fi
@@ -129,11 +129,11 @@ fi
129129# publish step has previously reported errors while the run itself succeeded,
130130# and the registry is the only authority on whether the version exists.
131131echo
132echo "Confirming ts-css@$VERSION on the registry ..."
132echo "Confirming @ts-css/core@$VERSION on the registry ..."
133133for attempt in 1 2 3 4 5 6; do
134 STATUS="$(curl -s -o /dev/null -w '%{http_code}' "https://registry.npmjs.org/ts-css/$VERSION" || echo 000)"
134 STATUS="$(curl -s -o /dev/null -w '%{http_code}' "https://registry.npmjs.org/@ts-css%2Fcore/$VERSION" || echo 000)"
135135 if [[ "$STATUS" == "200" ]]; then
136 echo " ts-css@$VERSION is live: https://www.npmjs.com/package/ts-css/v/$VERSION"
136 echo " @ts-css/core@$VERSION is live: https://www.npmjs.com/package/@ts-css/core/v/$VERSION"
137137 PUBLISHED=1
138138 break
139139 fi
@@ -143,7 +143,7 @@ done
143143
144144if [[ "${PUBLISHED:-0}" != "1" ]]; then
145145 echo >&2
146 echo "The workflow finished but ts-css@$VERSION is not on the registry." >&2
146 echo "The workflow finished but @ts-css/core@$VERSION is not on the registry." >&2
147147 echo "Inspect the publish step with:" >&2
148148 echo " gh run view $RUN_ID --log --repo $REPO | grep -A5 'Publish to npm'" >&2
149149 exit 1
tsconfig.jsonmodified+1-1
Changes to tsconfig.json
@@ -6,7 +6,7 @@
66 "module": "esnext",
77 "moduleResolution": "bundler",
88 "paths": {
9 "ts-css": ["./packages/ts-css/src/index.ts"]
9 "@ts-css/core": ["./packages/ts-css/src/index.ts"]
1010 },
1111 "resolveJsonModule": true,
1212 "types": ["bun"],