diff --git a/README.md b/README.md index 03b76b0..cc6a126 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,41 @@ const sheet = css.make({ }); ``` +### css.extend + +```tsx +import { css } from "@swan-io/css"; + +const input = css.extend({ + colors: { + red: "#fa2c37", + blue: "#2c7fff", + green: "#00c950", + }, +}); + +type CustomInput = typeof input; + +declare module "@swan-io/css" { + export interface Input extends CustomInput {} +} +``` + +```tsx +import "./theme"; + +import { createRoot } from "react-dom/client"; +// … +``` + +```tsx +const sheet = css.make(({ colors }) => ({ + box: { + backgroundColor: colors.blue, + }, +})); +``` + ### cx Concatenate the generated classes from left to right, with subsequent styles overwriting the property values of earlier ones. @@ -119,6 +154,18 @@ const Component = ({ inline }: { inline: boolean }) => ( ); ``` +## CSS extraction + +```tsx +import swanCss from "@swan-io/css/vite-plugin"; +import react from "@vitejs/plugin-react-swc"; +import { defineConfig } from "vite"; + +export default defineConfig(({ command }) => ({ + plugins: [react(), command === "build" && swanCss()], +})); +``` + ## Links - ⚖️ [**License**](./LICENSE) diff --git a/example/.gitignore b/example/.gitignore index 3209677..b219d5d 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -1,5 +1,6 @@ *.local .DS_Store +.vite-inspect dist dist-ssr node_modules diff --git a/example/package.json b/example/package.json index bc6182e..cc1d2a4 100644 --- a/example/package.json +++ b/example/package.json @@ -17,6 +17,7 @@ "@types/react-dom": "19.1.7", "@vitejs/plugin-react-swc": "4.0.0", "typescript": "5.9.2", - "vite": "7.1.2" + "vite": "7.1.2", + "vite-plugin-inspect": "11.3.2" } } diff --git a/example/src/App.tsx b/example/src/App.tsx index a72c3ba..09a7a73 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,13 +1,36 @@ -import { css } from "@swan-io/css"; +import { css, cx } from "@swan-io/css"; +import { useState } from "react"; import { useSheetLogger } from "./useSheetLogger"; -const sheet = css.make({ +const sheet = css.make(({ colors }) => ({ title: { - color: "red", + color: colors.red, + ":hover": { + color: colors.blue, + }, }, -}); + extra: { + color: colors.green, + }, +})); export const App = () => { + const [checked, setChecked] = useState(false); + useSheetLogger(); // log sheet on "l" keypress - return

Hello world

; + + const handleOnChange = (event: React.ChangeEvent) => { + setChecked(event.target.checked); + }; + + return ( + <> +

Hello world

+ + + + Add extra className + + + ); }; diff --git a/example/src/index.tsx b/example/src/index.tsx index 46d7811..666df1c 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -1,3 +1,5 @@ +import "./theme"; + import { createRoot } from "react-dom/client"; import { App } from "./App"; diff --git a/example/src/theme.ts b/example/src/theme.ts new file mode 100644 index 0000000..af024bd --- /dev/null +++ b/example/src/theme.ts @@ -0,0 +1,15 @@ +import { css } from "@swan-io/css"; + +const input = css.extend({ + colors: { + red: "#fa2c37", + blue: "#2c7fff", + green: "#00c950", + }, +}); + +type CustomInput = typeof input; + +declare module "@swan-io/css" { + export interface Input extends CustomInput {} +} diff --git a/example/src/useSheetLogger.ts b/example/src/useSheetLogger.ts index c7d868a..a9e1eba 100644 --- a/example/src/useSheetLogger.ts +++ b/example/src/useSheetLogger.ts @@ -6,8 +6,9 @@ export const useSheetLogger = () => { if (event.code === "KeyL") { const id = "swan-stylesheet"; - const sheet = document.querySelector( - `style[id="${id}"]`, + const sheet = ( + document.querySelector(`link[id="${id}"]`) ?? + document.querySelector(`style[id="${id}"]`) )?.sheet; if (sheet != null) { diff --git a/example/vite.config.ts b/example/vite.config.ts index 794d7c6..5947907 100644 --- a/example/vite.config.ts +++ b/example/vite.config.ts @@ -1,7 +1,13 @@ +import swanCss from "@swan-io/css/vite-plugin"; import react from "@vitejs/plugin-react-swc"; import { defineConfig } from "vite"; +import inspect from "vite-plugin-inspect"; export default defineConfig(({ command }) => ({ build: { sourcemap: true }, - plugins: [react()], + plugins: [ + react(), + command === "build" && inspect({ build: true }), + command === "build" && swanCss(), + ], })); diff --git a/package.json b/package.json index ade2b19..392e749 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,21 @@ "url": "git+https://github.com/swan-io/css.git" }, "packageManager": "pnpm@10.14.0", - "source": "src/index.ts", - "main": "dist/index.js", - "module": "dist/index.mjs", - "types": "dist/index.d.ts", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "default": "./dist/index.js" + }, + "./vite-plugin": { + "types": "./dist/vite-plugin.d.ts", + "import": "./dist/vite-plugin.mjs", + "default": "./dist/vite-plugin.js" + } + }, "files": [ "dist" ], @@ -43,7 +54,8 @@ "pnpm": { "onlyBuiltDependencies": [ "@swc/core", - "esbuild" + "esbuild", + "oxc-resolver" ] }, "prettier": { @@ -51,10 +63,23 @@ "prettier-plugin-organize-imports" ] }, + "peerDependencies": { + "vite": ">=5" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + }, "dependencies": { "@emotion/hash": "^0.9.2", "@react-native/normalize-colors": "^0.81.0", "csstype": "^3.1.3", + "magic-string": "^0.30.17", + "node-html-parser": "^7.0.1", + "oxc-parser": "^0.82.1", + "oxc-resolver": "^11.6.1", + "oxc-walker": "^0.4.0", "postcss-value-parser": "^4.2.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1044794..366e9ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,9 +17,27 @@ importers: csstype: specifier: ^3.1.3 version: 3.1.3 + magic-string: + specifier: ^0.30.17 + version: 0.30.17 + node-html-parser: + specifier: ^7.0.1 + version: 7.0.1 + oxc-parser: + specifier: ^0.82.1 + version: 0.82.1 + oxc-resolver: + specifier: ^11.6.1 + version: 11.6.1 + oxc-walker: + specifier: ^0.4.0 + version: 0.4.0(oxc-parser@0.82.1) postcss-value-parser: specifier: ^4.2.0 version: 4.2.0 + vite: + specifier: '>=5' + version: 7.1.2(@types/node@24.3.0) devDependencies: '@types/node': specifier: ^24.3.0 @@ -88,6 +106,9 @@ importers: vite: specifier: 7.1.2 version: 7.1.2(@types/node@24.3.0) + vite-plugin-inspect: + specifier: 11.3.2 + version: 11.3.2(vite@7.1.2(@types/node@24.3.0)) packages: @@ -103,6 +124,15 @@ packages: resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} engines: {node: '>=6.9.0'} + '@emnapi/core@1.4.5': + resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} + + '@emnapi/runtime@1.4.5': + resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} + + '@emnapi/wasi-threads@1.0.4': + resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} + '@emotion/hash@0.9.2': resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} @@ -269,6 +299,9 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -279,6 +312,196 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@napi-rs/wasm-runtime@1.0.3': + resolution: {integrity: sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==} + + '@oxc-parser/binding-android-arm64@0.82.1': + resolution: {integrity: sha512-Vph9abEKcjDm1qypjgvvHzrMcjIC5Nhi5kVO/GQ9WTRIbVEq5yS7vWp3VYh6TQ405DxAX2z8g2o67Ovdh3r1hA==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.82.1': + resolution: {integrity: sha512-0biUTb+VBpbNG3begg5e2FW9DlOW/7wLLr/sF9JLXLKiCQnYMTTQ/FTkHMqxkDJBON+FTiHpnvp4aS2eUv3lkA==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.82.1': + resolution: {integrity: sha512-VGIJSzPsWAK501FOOW44TspbZ8eWIOhY1FfBNIsn0JTN3Ve9uk/waSVN/lysQzMJOX3S3mIhtrVGdKQ3fum8GQ==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.82.1': + resolution: {integrity: sha512-5oLRbNxkNQz8bRuvr07xOTJIzsmaRg6pLHxH2HqlRvhhpo9sXJN4yROSGKgoQSFCSWZyDylg18Aw5cxvSxfJgw==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.82.1': + resolution: {integrity: sha512-lhbZaoDoxbxNp83GOvcXw02R+qJk3ckUPmHmbxkTCjSD/BttrjTLsZ30HJdH2rDB9kmBtfsbg3/Mnz/bU2D6ow==} + engines: {node: '>=20.0.0'} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.82.1': + resolution: {integrity: sha512-0dLHzsC22O+9/diTCjDrTDSEaTUsnvbvq2jTGpHNaVg7903Jieb3Eftqut3MpBea9764a/IZkGoKP3btCdQnYQ==} + engines: {node: '>=20.0.0'} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.82.1': + resolution: {integrity: sha512-ftux8M4nPbYj/6lEO9PbfZ8knacHx/o4JfwueDbcrWOf53otJ9jHNITbZooIgl7zwGCW+JSZgJis2Ts4u9feQw==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-arm64-musl@0.82.1': + resolution: {integrity: sha512-dvtBGwTsW2bUHB0c6lVj0KIm7NT00xcsATWTCXiwGoDIGl/FPJctudpj+nMwXyjdPk3rlKRDjJ3pHcd7pYR1DQ==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-gnu@0.82.1': + resolution: {integrity: sha512-y2A2lXUyppruU5AE6MforoqAvptwRGSRV8rO32Xv+vdflJH3rKxqKhWxW2bfRc14YhKRbQUcwPaW4nEqPPvwag==} + engines: {node: '>=20.0.0'} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-s390x-gnu@0.82.1': + resolution: {integrity: sha512-2oajEj8l0TGyWawVl+cuFjn7mcVBCR2fTO2EFsCf9WH7KEG/gyU86G5XDLN6tnl1E3Gqe88A09s0J8UUj+qUKA==} + engines: {node: '>=20.0.0'} + cpu: [s390x] + os: [linux] + + '@oxc-parser/binding-linux-x64-gnu@0.82.1': + resolution: {integrity: sha512-r0XCtdH36uXlwH504O2zRXqjhD8NjBQaPgInnwGMFskgPKPQBJIAYqWqrBzTEOJEPQEuhksfjsGNn7TAOQKdNQ==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-linux-x64-musl@0.82.1': + resolution: {integrity: sha512-CW6Rw6RME+cp1AmS2GFT7wdg/7nCxL/pHGbwhq7RumO6ITBKzicd6YH2rkQrMYy0x8ZTzSdS4YjbPBHT78E4jA==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-wasm32-wasi@0.82.1': + resolution: {integrity: sha512-0Vy/d8iBwFxrXldnc5GfXFmF8Pxgrqv14d/htz5u2kb02bhCCWIk+GjI2gKEcQfOgl2Bn4oOK3tL5rUrFNPRPg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.82.1': + resolution: {integrity: sha512-qfWIjAPt7ljozHkIH8sa155yH4rLrG8w36GRRhSZ1ltQT6HFgNZO56HSyZShSw3++3zBb5AkHVVnBWvBTo5zjQ==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.82.1': + resolution: {integrity: sha512-xiMVlP38bsq/7FHR6e+pZQ8XJetPhNToPy5mNh227pIybSWWjcdPTHo0LAJmIrsqrx5+/msIkZ+Wm/E+SXBkww==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.82.1': + resolution: {integrity: sha512-MCPtxtmHRmCqMI+DZyADBtW7QrFQ6OtQvHVAu576LWu6Y5zshLNabDc6RDJE/+uKVdypd9ZU1r05J/547VooPQ==} + + '@oxc-resolver/binding-android-arm-eabi@11.6.1': + resolution: {integrity: sha512-Ma/kg29QJX1Jzelv0Q/j2iFuUad1WnjgPjpThvjqPjpOyLjCUaiFCCnshhmWjyS51Ki1Iol3fjf1qAzObf8GIA==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.6.1': + resolution: {integrity: sha512-xjL/FKKc5p8JkFWiH7pJWSzsewif3fRf1rw2qiRxRvq1uIa6l7Zoa14Zq2TNWEsqDjdeOrlJtfWiPNRnevK0oQ==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.6.1': + resolution: {integrity: sha512-u0yrJ3NHE0zyCjiYpIyz4Vmov21MA0yFKbhHgixDU/G6R6nvC8ZpuSFql3+7C8ttAK9p8WpqOGweepfcilH5Bw==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.6.1': + resolution: {integrity: sha512-2lox165h1EhzxcC8edUy0znXC/hnAbUPaMpYKVlzLpB2AoYmgU4/pmofFApj+axm2FXpNamjcppld8EoHo06rw==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.6.1': + resolution: {integrity: sha512-F45MhEQ7QbHfsvZtVNuA/9obu3il7QhpXYmCMfxn7Zt9nfAOw4pQ8hlS5DroHVp3rW35u9F7x0sixk/QEAi3qQ==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.6.1': + resolution: {integrity: sha512-r+3+MTTl0tD4NoWbfTIItAxJvuyIU7V0fwPDXrv7Uj64vZ3OYaiyV+lVaeU89Bk/FUUQxeUpWBwdKNKHjyRNQw==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.6.1': + resolution: {integrity: sha512-TBTZ63otsWZ72Z8ZNK2JVS0HW1w9zgOixJTFDNrYPUUW1pXGa28KAjQ1yGawj242WLAdu3lwdNIWtkxeO2BLxQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.6.1': + resolution: {integrity: sha512-SjwhNynjSG2yMdyA0f7wz7Yvo3ppejO+ET7n2oiI7ApCXrwxMzeRWjBzQt+oVWr2HzVOfaEcDS9rMtnR83ulig==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-musl@11.6.1': + resolution: {integrity: sha512-f4EMidK6rosInBzPMnJ0Ri4RttFCvvLNUNDFUBtELW/MFkBwPTDlvbsmW0u0Mk/ruBQ2WmRfOZ6tT62kWMcX2Q==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.6.1': + resolution: {integrity: sha512-1umENVKeUsrWnf5IlF/6SM7DCv8G6CoKI2LnYR6qhZuLYDPS4PBZ0Jow3UDV9Rtbv5KRPcA3/uXjI88ntWIcOQ==} + cpu: [ppc64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.6.1': + resolution: {integrity: sha512-Hjyp1FRdJhsEpIxsZq5VcDuFc8abC0Bgy8DWEa31trCKoTz7JqA7x3E2dkFbrAKsEFmZZ0NvuG5Ip3oIRARhow==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-musl@11.6.1': + resolution: {integrity: sha512-ODJOJng6f3QxpAXhLel3kyWs8rPsJeo9XIZHzA7p//e+5kLMDU7bTVk4eZnUHuxsqsB8MEvPCicJkKCEuur5Ag==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-s390x-gnu@11.6.1': + resolution: {integrity: sha512-hCzRiLhqe1ZOpHTsTGKp7gnMJRORlbCthawBueer2u22RVAka74pV/+4pP1tqM07mSlQn7VATuWaDw9gCl+cVg==} + cpu: [s390x] + os: [linux] + + '@oxc-resolver/binding-linux-x64-gnu@11.6.1': + resolution: {integrity: sha512-JansPD8ftOzMYIC3NfXJ68tt63LEcIAx44Blx6BAd7eY880KX7A0KN3hluCrelCz5aQkPaD95g8HBiJmKaEi2w==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-linux-x64-musl@11.6.1': + resolution: {integrity: sha512-R78ES1rd4z2x5NrFPtSWb/ViR1B8wdl+QN2X8DdtoYcqZE/4tvWtn9ZTCXMEzUp23tchJ2wUB+p6hXoonkyLpA==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-wasm32-wasi@11.6.1': + resolution: {integrity: sha512-qAR3tYIf3afkij/XYunZtlz3OH2Y4ni10etmCFIJB5VRGsqJyI6Hl+2dXHHGJNwbwjXjSEH/KWJBpVroF3TxBw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.6.1': + resolution: {integrity: sha512-QqygWygIuemGkaBA48POOTeinbVvlamqh6ucm8arGDGz/mB5O00gXWxed12/uVrYEjeqbMkla/CuL3fjL3EKvw==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-ia32-msvc@11.6.1': + resolution: {integrity: sha512-N2+kkWwt/bk0JTCxhPuK8t8JMp3nd0n2OhwOkU8KO4a7roAJEa4K1SZVjMv5CqUIr5sx2CxtXRBoFDiORX5oBg==} + cpu: [ia32] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.6.1': + resolution: {integrity: sha512-DfMg3cU9bJUbN62Prbp4fGCtLgexuwyEaQGtZAp8xmi1Ii26uflOGx0FJkFTF6lVMSFoIRFvIL8gsw5/ZdHrMw==} + cpu: [x64] + os: [win32] + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -477,6 +700,9 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tybys/wasm-util@0.10.0': + resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -575,6 +801,10 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + ansis@4.1.0: + resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} + engines: {node: '>=14'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -588,9 +818,19 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + birpc@2.5.0: + resolution: {integrity: sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -635,6 +875,13 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -651,6 +898,18 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + default-browser-id@5.0.0: + resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + engines: {node: '>=18'} + + default-browser@5.2.1: + resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -658,6 +917,19 @@ packages: dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -667,6 +939,13 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -712,10 +991,28 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -756,6 +1053,9 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true + magic-regexp@0.10.0: + resolution: {integrity: sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -785,10 +1085,40 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + napi-postinstall@0.3.3: + resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + node-html-parser@7.0.1: + resolution: {integrity: sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + oxc-parser@0.82.1: + resolution: {integrity: sha512-2bBrazc/0wpA/+XECTwLA6dvp2Swp9vm/psgvwDz4CcxwfvYyQN0/ghw9km0LFpPIDSrxhltJSsfajhb2NZq0A==} + engines: {node: '>=20.0.0'} + + oxc-resolver@11.6.1: + resolution: {integrity: sha512-WQgmxevT4cM5MZ9ioQnEwJiHpPzbvntV5nInGAKo9NQZzegcOonHvcVcnkYqld7bTG35UFHEKeF7VwwsmA3cZg==} + + oxc-walker@0.4.0: + resolution: {integrity: sha512-x5TJAZQD3kRnRBGZ+8uryMZUwkTYddwzBftkqyJIcmpBOXmoK/fwriRKATjZroR2d+aS7+2w1B0oz189bBTwfw==} + peerDependencies: + oxc-parser: '>=0.72.0' + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -807,6 +1137,9 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -895,6 +1228,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -904,6 +1241,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-applescript@7.0.0: + resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} + engines: {node: '>=18'} + scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} @@ -1008,6 +1349,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsup@8.5.0: resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==} engines: {node: '>=18'} @@ -1027,6 +1371,9 @@ packages: typescript: optional: true + type-level-regexp@0.1.17: + resolution: {integrity: sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==} + typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} @@ -1038,11 +1385,39 @@ packages: undici-types@7.10.0: resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} + unplugin-utils@0.2.5: + resolution: {integrity: sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==} + engines: {node: '>=18.12.0'} + + unplugin@2.3.6: + resolution: {integrity: sha512-+/MdXl8bLTXI2lJF22gUBeCFqZruEpL/oM9f8wxCuKh9+Mw9qeul3gTqgbKpMeOFlusCzc0s7x2Kax2xKW+FQg==} + engines: {node: '>=18.12.0'} + + vite-dev-rpc@1.1.0: + resolution: {integrity: sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 + + vite-hot-client@2.1.0: + resolution: {integrity: sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite-plugin-inspect@11.3.2: + resolution: {integrity: sha512-nzwvyFQg58XSMAmKVLr2uekAxNYvAbz1lyPmCAFVIBncCgN9S/HPM+2UM9Q9cvc4JEbC5ZBgwLAdaE2onmQuKg==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + vite@7.1.2: resolution: {integrity: sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1130,6 +1505,9 @@ packages: webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -1163,6 +1541,10 @@ packages: utf-8-validate: optional: true + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + snapshots: '@babel/code-frame@7.27.1': @@ -1175,6 +1557,22 @@ snapshots: '@babel/runtime@7.28.3': {} + '@emnapi/core@1.4.5': + dependencies: + '@emnapi/wasi-threads': 1.0.4 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.5': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.4': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/hash@0.9.2': {} '@esbuild/aix-ppc64@0.25.9': @@ -1269,6 +1667,11 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -1278,6 +1681,121 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@napi-rs/wasm-runtime@1.0.3': + dependencies: + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 + '@tybys/wasm-util': 0.10.0 + optional: true + + '@oxc-parser/binding-android-arm64@0.82.1': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.82.1': + optional: true + + '@oxc-parser/binding-darwin-x64@0.82.1': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.82.1': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.82.1': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.82.1': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.82.1': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.82.1': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.82.1': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.82.1': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.82.1': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.82.1': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.82.1': + dependencies: + '@napi-rs/wasm-runtime': 1.0.3 + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.82.1': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.82.1': + optional: true + + '@oxc-project/types@0.82.1': {} + + '@oxc-resolver/binding-android-arm-eabi@11.6.1': + optional: true + + '@oxc-resolver/binding-android-arm64@11.6.1': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.6.1': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.6.1': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.6.1': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.6.1': + dependencies: + '@napi-rs/wasm-runtime': 1.0.3 + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.6.1': + optional: true + + '@oxc-resolver/binding-win32-ia32-msvc@11.6.1': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.6.1': + optional: true + '@pkgjs/parseargs@0.11.0': optional: true @@ -1414,6 +1932,11 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 + '@tybys/wasm-util@0.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@types/aria-query@5.0.4': {} '@types/chai@5.2.2': @@ -1519,6 +2042,8 @@ snapshots: ansi-styles@6.2.1: {} + ansis@4.1.0: {} + any-promise@1.3.0: {} aria-query@5.3.0: @@ -1529,10 +2054,18 @@ snapshots: balanced-match@1.0.2: {} + birpc@2.5.0: {} + + boolbase@1.0.0: {} + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 + bundle-name@4.1.0: + dependencies: + run-applescript: 7.0.0 + bundle-require@5.1.0(esbuild@0.25.9): dependencies: esbuild: 0.25.9 @@ -1572,6 +2105,16 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@6.2.2: {} + csstype@3.1.3: {} debug@4.4.1: @@ -1580,16 +2123,47 @@ snapshots: deep-eql@5.0.2: {} + default-browser-id@5.0.0: {} + + default-browser@5.2.1: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.0 + + define-lazy-prop@3.0.0: {} + dequal@2.0.3: {} dom-accessibility-api@0.5.16: {} + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + eastasianwidth@0.2.0: {} emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + entities@4.5.0: {} + + error-stack-parser-es@1.0.5: {} + es-module-lexer@1.7.0: {} esbuild@0.25.9: @@ -1657,8 +2231,20 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + he@1.2.0: {} + + is-docker@3.0.0: {} + is-fullwidth-code-point@3.0.0: {} + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + isexe@2.0.0: {} jackspeak@3.4.3: @@ -1687,6 +2273,16 @@ snapshots: lz-string@1.5.0: {} + magic-regexp@0.10.0: + dependencies: + estree-walker: 3.0.3 + magic-string: 0.30.17 + mlly: 1.7.4 + regexp-tree: 0.1.27 + type-level-regexp: 0.1.17 + ufo: 1.6.1 + unplugin: 2.3.6 + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1716,8 +2312,78 @@ snapshots: nanoid@3.3.11: {} + napi-postinstall@0.3.3: {} + + node-html-parser@7.0.1: + dependencies: + css-select: 5.2.2 + he: 1.2.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + object-assign@4.1.1: {} + ohash@2.0.11: {} + + open@10.2.0: + dependencies: + default-browser: 5.2.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + oxc-parser@0.82.1: + dependencies: + '@oxc-project/types': 0.82.1 + optionalDependencies: + '@oxc-parser/binding-android-arm64': 0.82.1 + '@oxc-parser/binding-darwin-arm64': 0.82.1 + '@oxc-parser/binding-darwin-x64': 0.82.1 + '@oxc-parser/binding-freebsd-x64': 0.82.1 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.82.1 + '@oxc-parser/binding-linux-arm-musleabihf': 0.82.1 + '@oxc-parser/binding-linux-arm64-gnu': 0.82.1 + '@oxc-parser/binding-linux-arm64-musl': 0.82.1 + '@oxc-parser/binding-linux-riscv64-gnu': 0.82.1 + '@oxc-parser/binding-linux-s390x-gnu': 0.82.1 + '@oxc-parser/binding-linux-x64-gnu': 0.82.1 + '@oxc-parser/binding-linux-x64-musl': 0.82.1 + '@oxc-parser/binding-wasm32-wasi': 0.82.1 + '@oxc-parser/binding-win32-arm64-msvc': 0.82.1 + '@oxc-parser/binding-win32-x64-msvc': 0.82.1 + + oxc-resolver@11.6.1: + dependencies: + napi-postinstall: 0.3.3 + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.6.1 + '@oxc-resolver/binding-android-arm64': 11.6.1 + '@oxc-resolver/binding-darwin-arm64': 11.6.1 + '@oxc-resolver/binding-darwin-x64': 11.6.1 + '@oxc-resolver/binding-freebsd-x64': 11.6.1 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.6.1 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.6.1 + '@oxc-resolver/binding-linux-arm64-gnu': 11.6.1 + '@oxc-resolver/binding-linux-arm64-musl': 11.6.1 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.6.1 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.6.1 + '@oxc-resolver/binding-linux-riscv64-musl': 11.6.1 + '@oxc-resolver/binding-linux-s390x-gnu': 11.6.1 + '@oxc-resolver/binding-linux-x64-gnu': 11.6.1 + '@oxc-resolver/binding-linux-x64-musl': 11.6.1 + '@oxc-resolver/binding-wasm32-wasi': 11.6.1 + '@oxc-resolver/binding-win32-arm64-msvc': 11.6.1 + '@oxc-resolver/binding-win32-ia32-msvc': 11.6.1 + '@oxc-resolver/binding-win32-x64-msvc': 11.6.1 + + oxc-walker@0.4.0(oxc-parser@0.82.1): + dependencies: + estree-walker: 3.0.3 + magic-regexp: 0.10.0 + oxc-parser: 0.82.1 + package-json-from-dist@1.0.1: {} path-key@3.1.1: {} @@ -1731,6 +2397,8 @@ snapshots: pathval@2.0.1: {} + perfect-debounce@1.0.0: {} + picocolors@1.1.1: {} picomatch@4.0.3: {} @@ -1791,6 +2459,8 @@ snapshots: readdirp@4.1.2: {} + regexp-tree@0.1.27: {} + resolve-from@5.0.0: {} rollup@4.46.2: @@ -1819,6 +2489,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.46.2 fsevents: 2.3.3 + run-applescript@7.0.0: {} + scheduler@0.26.0: {} shebang-command@2.0.0: @@ -1914,6 +2586,9 @@ snapshots: ts-interface-checker@0.1.13: {} + tslib@2.8.1: + optional: true + tsup@8.5.0(@swc/core@1.13.3)(postcss@8.5.6)(typescript@5.9.2): dependencies: bundle-require: 5.1.0(esbuild@0.25.9) @@ -1943,12 +2618,36 @@ snapshots: - tsx - yaml + type-level-regexp@0.1.17: {} + typescript@5.9.2: {} ufo@1.6.1: {} undici-types@7.10.0: {} + unplugin-utils@0.2.5: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + unplugin@2.3.6: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.15.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + + vite-dev-rpc@1.1.0(vite@7.1.2(@types/node@24.3.0)): + dependencies: + birpc: 2.5.0 + vite: 7.1.2(@types/node@24.3.0) + vite-hot-client: 2.1.0(vite@7.1.2(@types/node@24.3.0)) + + vite-hot-client@2.1.0(vite@7.1.2(@types/node@24.3.0)): + dependencies: + vite: 7.1.2(@types/node@24.3.0) + vite-node@3.2.4(@types/node@24.3.0): dependencies: cac: 6.7.14 @@ -1970,6 +2669,21 @@ snapshots: - tsx - yaml + vite-plugin-inspect@11.3.2(vite@7.1.2(@types/node@24.3.0)): + dependencies: + ansis: 4.1.0 + debug: 4.4.1 + error-stack-parser-es: 1.0.5 + ohash: 2.0.11 + open: 10.2.0 + perfect-debounce: 1.0.0 + sirv: 3.0.1 + unplugin-utils: 0.2.5 + vite: 7.1.2(@types/node@24.3.0) + vite-dev-rpc: 1.1.0(vite@7.1.2(@types/node@24.3.0)) + transitivePeerDependencies: + - supports-color + vite@7.1.2(@types/node@24.3.0): dependencies: esbuild: 0.25.9 @@ -2036,6 +2750,8 @@ snapshots: webidl-conversions@4.0.2: {} + webpack-virtual-modules@0.6.2: {} + whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 @@ -2064,3 +2780,7 @@ snapshots: strip-ansi: 7.1.0 ws@8.18.3: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.0 diff --git a/src/css.ts b/src/css.ts new file mode 100644 index 0000000..3b51178 --- /dev/null +++ b/src/css.ts @@ -0,0 +1,318 @@ +import { caches } from "./cx"; +import { hash } from "./hash"; +import { hyphenateName } from "./hyphenateName"; +import { normalizeValue } from "./normalizeValue"; +import { + preprocessAtomicStyle, + preprocessKeyframes, + preprocessResetStyle, +} from "./preprocess"; +import type { FlatStyle, Input, Keyframes, Style } from "./types"; +import { appendString, forEach } from "./utils"; + +const getSheet = (id: string): CSSStyleSheet | null => { + if (typeof document === "undefined") { + return null; + } + + const current = + document.querySelector(`link[id="${id}"]`) ?? + document.querySelector(`style[id="${id}"]`); + + if (current != null) { + return current.sheet; + } + + const element = document.createElement("style"); + element.setAttribute("id", id); + document.head.appendChild(element); + + return element.sheet; +}; + +const getMediaRule = ( + sheet: CSSStyleSheet | null, + index: number, + media: string, +): { + cssRules: CSSRuleList | []; + toString: () => string; + insertRule: (rule: string) => void; +} => { + const cssRules = new Set(); + + const toString = () => + cssRules.size > 0 + ? `@media ${media}{${[...cssRules].join("")}}` + : `@media ${media}{}`; // Keep an empty media sheet to preserve the index (hydratation) + + if (sheet == null) { + return { + cssRules: [], + toString, + insertRule: (rule) => { + cssRules.add(rule); + }, + }; + } + + if (sheet.cssRules[index] == null) { + try { + sheet.insertRule(`@media ${media}{}`, index); + } catch (error) { + if (process.env.NODE_ENV === "development") { + console.error(error); + } + + return { + cssRules: [], + toString, + insertRule: (rule) => { + cssRules.add(rule); + }, + }; + } + } + + const mediaRule = sheet.cssRules[index] as CSSMediaRule; + + return { + cssRules: mediaRule.cssRules, + toString, + insertRule: (rule) => { + try { + mediaRule.insertRule(rule, mediaRule.cssRules.length); + cssRules.add(rule); + } catch (error) { + if (process.env.NODE_ENV === "development") { + console.error(error); + } + } + }, + }; +}; + +const stringifyRule = (key: string, value: string | number): string => { + if (key === "appearance") { + return `-webkit-appearance:${value};appearance:${value}`; + } + if (key === "lineClamp") { + return `-webkit-line-clamp:${value};line-clamp:${value}`; + } + + return `${hyphenateName(key)}:${normalizeValue(key, value)}`; +}; + +const getClassName = (rule: CSSStyleRule): string => { + const selector = rule.selectorText; + const end = selector.indexOf(":"); + return end > -1 ? selector.substring(1, end) : selector.substring(1); +}; + +const sheet = getSheet("swan-stylesheet"); + +const keyframesSheet = getMediaRule(sheet, 0, "all"); +const resetSheet = getMediaRule(sheet, 1, "all"); +const atomicSheet = getMediaRule(sheet, 2, "all"); +const hoverSheet = getMediaRule(sheet, 3, "(hover:hover)"); +const focusSheet = getMediaRule(sheet, 4, "all"); +const activeSheet = getMediaRule(sheet, 5, "all"); + +const keyframesNames = new Set(); + +// Rehydrate keyframes sheet +for (const rule of keyframesSheet.cssRules) { + if (rule instanceof CSSKeyframesRule) { + keyframesNames.add(rule.name); + } +} + +// Rehydrate reset sheet +for (const rule of resetSheet.cssRules) { + if (rule instanceof CSSStyleRule) { + caches.reset.add(getClassName(rule)); + } +} + +// Rehydrate atomic sheet +for (const rule of atomicSheet.cssRules) { + if (rule instanceof CSSStyleRule) { + caches.atomic.set(getClassName(rule), rule.style[0]); + } +} + +// Rehydrate hover sheet +for (const rule of hoverSheet.cssRules) { + if (rule instanceof CSSStyleRule) { + caches.hover.set(getClassName(rule), rule.style[0]); + } +} + +// Rehydrate focus sheet +for (const rule of focusSheet.cssRules) { + if (rule instanceof CSSStyleRule) { + caches.focus.set(getClassName(rule), rule.style[0]); + } +} + +// Rehydrate active sheet +for (const rule of activeSheet.cssRules) { + if (rule instanceof CSSStyleRule) { + caches.active.set(getClassName(rule), rule.style[0]); + } +} + +const insertKeyframes = (keyframes: Keyframes): string | undefined => { + let body = ""; + + forEach(keyframes, (key, value) => { + const rules: string[] = []; + + forEach(value, (key, value) => { + rules.push(stringifyRule(key, value)); + }); + + body += `${key}{${rules.join(";")}}`; + }); + + const name = "k-" + hash(body); + + if (!keyframesNames.has(name)) { + keyframesSheet.insertRule(`@keyframes ${name}{${body}}`); + keyframesNames.add(name); + } + + return name; +}; + +const insertResetRule = (style: FlatStyle): string => { + const rules: string[] = []; + + forEach(style, (key, value) => { + rules.push(stringifyRule(key, value)); + }); + + const body = rules.join(";"); + const className = "r-" + hash(body); + + if (!caches.reset.has(className)) { + resetSheet.insertRule(`.${className}{${body}}`); + caches.reset.add(className); + } + + return className; +}; + +const insertAtomicRules = (style: Style): string => { + let classNames = ""; + + forEach(style, (key, value) => { + switch (key) { + case ":hover": { + forEach(value as FlatStyle, (key, value) => { + const rule = stringifyRule(key, value); + const className = "h-" + hash(rule); + + if (!caches.hover.has(className)) { + hoverSheet.insertRule(`.${className}:hover{${rule}}`); + caches.hover.set(className, key); + } + + classNames = appendString(classNames, className); + }); + + break; + } + + case ":focus": { + forEach(value as FlatStyle, (key, value) => { + const rule = stringifyRule(key, value); + const className = "f-" + hash(rule); + + if (!caches.focus.has(className)) { + focusSheet.insertRule(`.${className}:focus-visible{${rule}}`); + caches.focus.set(className, key); + } + + classNames = appendString(classNames, className); + }); + + break; + } + + case ":active": { + forEach(value as FlatStyle, (key, value) => { + const rule = stringifyRule(key, value); + const className = "a-" + hash(rule); + + if (!caches.active.has(className)) { + activeSheet.insertRule(`.${className}:active{${rule}}`); + caches.active.set(className, key); + } + + classNames = appendString(classNames, className); + }); + + break; + } + + default: { + const rule = stringifyRule(key, value as string | number); + const className = "x-" + hash(rule); + + if (!caches.atomic.has(className)) { + atomicSheet.insertRule(`.${className}{${rule}}`); + caches.atomic.set(className, key); + } + + classNames = appendString(classNames, className); + } + } + }); + + return classNames; +}; + +const _input: Input = { + keyframes: (keyframes) => insertKeyframes(preprocessKeyframes(keyframes)), +}; + +export const css = { + extend: >(input: T) => { + forEach(input, (key, value) => { + // @ts-expect-error keep initial object instance reference + _input[key] = value; + }); + + return input; + }, + make: ( + styles: Record | ((input: Input) => Record), + ): Record => { + const output = {} as Record; + + forEach( + typeof styles === "function" ? styles(_input) : styles, + (key, value) => { + output[key] = + key[0] === "$" + ? insertResetRule(preprocessResetStyle(value)) + : insertAtomicRules(preprocessAtomicStyle(value)); + }, + ); + + return output; + }, +}; + +export const getCssMakeInput = () => _input; + +export const getCssFileContent = () => + [ + keyframesSheet.toString(), + resetSheet.toString(), + atomicSheet.toString(), + hoverSheet.toString(), + focusSheet.toString(), + activeSheet.toString(), + ].join("\n"); diff --git a/src/cx.ts b/src/cx.ts new file mode 100644 index 0000000..d068625 --- /dev/null +++ b/src/cx.ts @@ -0,0 +1,106 @@ +import type { ClassNames } from "./types"; +import { appendString, forEach } from "./utils"; + +export const caches = { + reset: new Set(), + atomic: new Map(), + hover: new Map(), + focus: new Map(), + active: new Map(), +}; + +const extractClassNames = (items: ClassNames, acc: string[]): string[] => { + for (const item of items) { + if (typeof item === "string") { + for (const part of item.split(" ")) { + if (part !== "") { + acc.push(part); + } + } + } else if (Array.isArray(item)) { + extractClassNames(item, acc); + } + } + + return acc; +}; + +const appendClassNames = ( + acc: string, + classNames: Record, +): string => { + let output = acc; + + forEach(classNames, (_, value) => { + output = appendString(output, value); + }); + + return output; +}; + +export const cx = (...items: ClassNames): string => { + const classNames = extractClassNames(items, []); + + let output = ""; + + let cacheKey: string | undefined = undefined; + let reset: string | undefined = undefined; + + const atomic: Record = {}; + const hover: Record = {}; + const focus: Record = {}; + const active: Record = {}; + + for (const className of classNames) { + cacheKey = caches.atomic.get(className); + + if (cacheKey != null) { + atomic[cacheKey] = className; + continue; + } + + cacheKey = caches.hover.get(className); + + if (cacheKey != null) { + hover[cacheKey] = className; + continue; + } + + cacheKey = caches.focus.get(className); + + if (cacheKey != null) { + focus[cacheKey] = className; + continue; + } + + cacheKey = caches.active.get(className); + + if (cacheKey != null) { + active[cacheKey] = className; + continue; + } + + if (caches.reset.has(className)) { + if (reset == null) { + reset = className; + } else if (reset !== className) { + if (process.env.NODE_ENV === "development") { + console.warn("`cx` only accepts one reset style."); + } + } + } else { + output = appendString(output, className); + } + } + + if (reset != null) { + output = appendString(output, reset); + } + + output = appendClassNames(output, atomic); + output = appendClassNames(output, hover); + output = appendClassNames(output, focus); + output = appendClassNames(output, active); + + return output; +}; diff --git a/src/index.ts b/src/index.ts index 0ac147d..4118fd5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ -import { createSheet } from "./sheet"; - -export const { css, cx } = createSheet(); +export { css } from "./css"; +export { cx } from "./cx"; +export type { Input } from "./types"; diff --git a/src/normalizeValue.ts b/src/normalizeValue.ts index df252e7..5742f29 100644 --- a/src/normalizeValue.ts +++ b/src/normalizeValue.ts @@ -64,25 +64,15 @@ const isWebColor = (color: string): boolean => color === "inherit" || color.indexOf("var(") === 0; -const hexLookupTable = "0123456789abcdef"; - const rgbToHex = (r: number, g: number, b: number) => { - const r1 = r >> 4; - const r2 = r & 0xf; - const g1 = g >> 4; - const g2 = g & 0xf; - const b1 = b >> 4; - const b2 = b & 0xf; - - return r1 === r2 && g1 === g2 && b1 === b2 - ? "#" + hexLookupTable[r1] + hexLookupTable[g1] + hexLookupTable[b1] - : "#" + - hexLookupTable[r1] + - hexLookupTable[r2] + - hexLookupTable[g1] + - hexLookupTable[g2] + - hexLookupTable[b1] + - hexLookupTable[b2]; + const hex = + r.toString(16).padStart(2, "0") + + g.toString(16).padStart(2, "0") + + b.toString(16).padStart(2, "0"); + + return hex[0] === hex[1] && hex[2] === hex[3] && hex[4] === hex[5] + ? "#" + hex[0] + hex[2] + hex[4] + : "#" + hex; }; export const normalizeValue = (key: string, value: string | number): string => { diff --git a/src/preprocess.ts b/src/preprocess.ts index ca0c0d6..4df07ca 100644 --- a/src/preprocess.ts +++ b/src/preprocess.ts @@ -163,14 +163,25 @@ export const preprocessAtomicStyle = (style: Style): Style => { let active: FlatStyle | undefined = undefined; forEach(style, (key, value) => { - if (key === ":hover") { - hover = preprocessStyle(value as FlatStyle); - } else if (key === ":focus") { - focus = preprocessStyle(value as FlatStyle); - } else if (key === ":active") { - active = preprocessStyle(value as FlatStyle); - } else { - preprocessRule(output, key, value as ValueOf); + switch (key) { + case ":hover": { + hover = preprocessStyle(value as FlatStyle); + break; + } + + case ":focus": { + focus = preprocessStyle(value as FlatStyle); + break; + } + + case ":active": { + active = preprocessStyle(value as FlatStyle); + break; + } + + default: { + preprocessRule(output, key, value as ValueOf); + } } }); diff --git a/src/sheet.ts b/src/sheet.ts deleted file mode 100644 index 1e94e53..0000000 --- a/src/sheet.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { hash } from "./hash"; -import { hyphenateName } from "./hyphenateName"; -import { normalizeValue } from "./normalizeValue"; -import { - preprocessAtomicStyle, - preprocessKeyframes, - preprocessResetStyle, -} from "./preprocess"; -import type { ClassNames, FlatStyle, Input, Keyframes, Style } from "./types"; -import { forEach } from "./utils"; - -const getSheet = (id: string): CSSStyleSheet | null => { - if (typeof document === "undefined") { - return null; - } - - const current = document.querySelector(`style[id="${id}"]`); - - if (current != null) { - return current.sheet; - } - - const element = document.createElement("style"); - element.setAttribute("id", id); - document.head.appendChild(element); - - return element.sheet; -}; - -const getMediaRule = ( - sheet: CSSStyleSheet | null, - index: number, - media: string, -): { - cssRules: CSSRuleList | []; - insertRule: (rule: string) => void; -} => { - if (sheet == null) { - return { - cssRules: [], - insertRule: () => {}, - }; - } - - if (sheet.cssRules[index] == null) { - try { - sheet.insertRule(`@media ${media}{}`, index); - } catch (error) { - if (process.env.NODE_ENV === "development") { - console.error(error); - } - - return { - cssRules: [], - insertRule: () => {}, - }; - } - } - - const mediaRule = sheet.cssRules[index] as CSSMediaRule; - - return { - cssRules: mediaRule.cssRules, - insertRule: (rule) => { - try { - mediaRule.insertRule(rule, mediaRule.cssRules.length); - } catch (error) { - if (process.env.NODE_ENV === "development") { - console.error(error); - } - } - }, - }; -}; - -const stringifyRule = (key: string, value: string | number): string => { - if (key === "appearance") { - return `-webkit-appearance:${value};appearance:${value}`; - } - if (key === "lineClamp") { - return `-webkit-line-clamp:${value};line-clamp:${value}`; - } - - return `${hyphenateName(key)}:${normalizeValue(key, value)}`; -}; - -const extractClassNames = (items: ClassNames, acc: string[]): string[] => { - for (const item of items) { - if (typeof item === "string") { - for (const part of item.split(" ")) { - if (part !== "") { - acc.push(part); - } - } - } else if (Array.isArray(item)) { - extractClassNames(item, acc); - } - } - - return acc; -}; - -const appendString = (acc: string, value: string): string => - acc + (acc ? " " + value : value); - -const appendClassNames = ( - acc: string, - classNames: Record, -): string => { - let output = acc; - - forEach(classNames, (_, value) => { - output = appendString(output, value); - }); - - return output; -}; - -const getClassName = (rule: CSSStyleRule): string => { - const selector = rule.selectorText; - const end = selector.indexOf(":"); - return end > -1 ? selector.substring(1, end) : selector.substring(1); -}; - -export const createSheet = () => { - const sheet = getSheet("swan-stylesheet"); - - const keyframesSheet = getMediaRule(sheet, 0, "all"); - const resetSheet = getMediaRule(sheet, 1, "all"); - const atomicSheet = getMediaRule(sheet, 2, "all"); - const hoverSheet = getMediaRule(sheet, 3, "(hover:hover)"); - const focusSheet = getMediaRule(sheet, 4, "all"); - const activeSheet = getMediaRule(sheet, 5, "all"); - - const keyframesNames = new Set(); - const resetClassNames = new Set(); - const atomicClassNames = new Map(); - const hoverClassNames = new Map(); - const focusClassNames = new Map(); - const activeClassNames = new Map(); - - // Rehydrate keyframes sheet - for (const rule of keyframesSheet.cssRules) { - if (rule instanceof CSSKeyframesRule) { - keyframesNames.add(rule.name); - } - } - - // Rehydrate reset sheet - for (const rule of resetSheet.cssRules) { - if (rule instanceof CSSStyleRule) { - resetClassNames.add(getClassName(rule)); - } - } - - // Rehydrate atomic sheet - for (const rule of atomicSheet.cssRules) { - if (rule instanceof CSSStyleRule) { - atomicClassNames.set(getClassName(rule), rule.style[0]); - } - } - - // Rehydrate hover sheet - for (const rule of hoverSheet.cssRules) { - if (rule instanceof CSSStyleRule) { - hoverClassNames.set(getClassName(rule), rule.style[0]); - } - } - - // Rehydrate focus sheet - for (const rule of focusSheet.cssRules) { - if (rule instanceof CSSStyleRule) { - focusClassNames.set(getClassName(rule), rule.style[0]); - } - } - - // Rehydrate active sheet - for (const rule of activeSheet.cssRules) { - if (rule instanceof CSSStyleRule) { - activeClassNames.set(getClassName(rule), rule.style[0]); - } - } - - const insertKeyframes = (keyframes: Keyframes): string | undefined => { - let body = ""; - - forEach(keyframes, (key, value) => { - const rules: string[] = []; - - forEach(value, (key, value) => { - rules.push(stringifyRule(key, value)); - }); - - body += `${key}{${rules.join(";")}}`; - }); - - const name = "k-" + hash(body); - - if (!keyframesNames.has(name)) { - keyframesSheet.insertRule(`@keyframes ${name}{${body}}`); - keyframesNames.add(name); - } - - return name; - }; - - const insertResetRule = (style: FlatStyle): string => { - const rules: string[] = []; - - forEach(style, (key, value) => { - rules.push(stringifyRule(key, value)); - }); - - const body = rules.join(";"); - const className = "r-" + hash(body); - - if (!resetClassNames.has(className)) { - resetSheet.insertRule(`.${className}{${body}}`); - resetClassNames.add(className); - } - - return className; - }; - - const insertAtomicRules = (style: Style): string => { - let classNames = ""; - - forEach(style, (key, value) => { - if (key === ":hover") { - forEach(value as FlatStyle, (key, value) => { - const rule = stringifyRule(key, value); - const className = "h-" + hash(rule); - - if (!hoverClassNames.has(className)) { - hoverSheet.insertRule(`.${className}:hover{${rule}}`); - hoverClassNames.set(className, key); - } - - classNames = appendString(classNames, className); - }); - } else if (key === ":focus") { - forEach(value as FlatStyle, (key, value) => { - const rule = stringifyRule(key, value); - const className = "f-" + hash(rule); - - if (!focusClassNames.has(className)) { - focusSheet.insertRule(`.${className}:focus-visible{${rule}}`); - focusClassNames.set(className, key); - } - - classNames = appendString(classNames, className); - }); - } else if (key === ":active") { - forEach(value as FlatStyle, (key, value) => { - const rule = stringifyRule(key, value); - const className = "a-" + hash(rule); - - if (!activeClassNames.has(className)) { - activeSheet.insertRule(`.${className}:active{${rule}}`); - activeClassNames.set(className, key); - } - - classNames = appendString(classNames, className); - }); - } else { - const rule = stringifyRule(key, value as string | number); - const className = "x-" + hash(rule); - - if (!atomicClassNames.has(className)) { - atomicSheet.insertRule(`.${className}{${rule}}`); - atomicClassNames.set(className, key); - } - - classNames = appendString(classNames, className); - } - }); - - return classNames; - }; - - const input: Input = { - keyframes: (keyframes) => insertKeyframes(preprocessKeyframes(keyframes)), - }; - - return { - css: { - make: ( - styles: Record | ((input: Input) => Record), - ): Record => { - const output = {} as Record; - - forEach( - typeof styles === "function" ? styles(input) : styles, - (key, value) => { - output[key] = - key[0] === "$" - ? insertResetRule(preprocessResetStyle(value)) - : insertAtomicRules(preprocessAtomicStyle(value)); - }, - ); - - return output; - }, - }, - cx: (...items: ClassNames): string => { - const classNames = extractClassNames(items, []); - - let output = ""; - - let cacheKey: string | undefined = undefined; - let reset: string | undefined = undefined; - - const atomic: Record = {}; - const hover: Record = {}; - const focus: Record = {}; - const active: Record = {}; - - for (const className of classNames) { - cacheKey = atomicClassNames.get(className); - - if (cacheKey != null) { - atomic[cacheKey] = className; - continue; - } - - cacheKey = hoverClassNames.get(className); - - if (cacheKey != null) { - hover[cacheKey] = className; - continue; - } - - cacheKey = focusClassNames.get(className); - - if (cacheKey != null) { - focus[cacheKey] = className; - continue; - } - - cacheKey = activeClassNames.get(className); - - if (cacheKey != null) { - active[cacheKey] = className; - continue; - } - - if (resetClassNames.has(className)) { - if (reset == null) { - reset = className; - } else if (reset !== className) { - if (process.env.NODE_ENV === "development") { - console.warn("`cx` only accepts one reset style."); - } - } - } else { - output = appendString(output, className); - } - } - - if (reset != null) { - output = appendString(output, reset); - } - - output = appendClassNames(output, atomic); - output = appendClassNames(output, hover); - output = appendClassNames(output, focus); - output = appendClassNames(output, active); - - return output; - }, - }; -}; diff --git a/src/utils.ts b/src/utils.ts index afa6819..590a943 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,8 @@ import type { ValueOf } from "./types"; +export const appendString = (acc: string, value: string): string => + acc + (acc ? " " + value : value); + export const forEach = >( object: T, callback: (key: keyof T, value: NonNullable>) => void, diff --git a/src/vite-plugin.ts b/src/vite-plugin.ts new file mode 100644 index 0000000..85134c9 --- /dev/null +++ b/src/vite-plugin.ts @@ -0,0 +1,430 @@ +import MagicString from "magic-string"; +import HTMLParser from "node-html-parser"; +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import type { CallExpression, ImportDeclaration } from "oxc-parser"; +import { ResolverFactory } from "oxc-resolver"; +import { parseAndWalk } from "oxc-walker"; +import type { Plugin, ResolvedConfig } from "vite"; + +type PluginOptions = { + fileName?: string; +}; + +const findSpecifier = ( + { specifiers }: ImportDeclaration, + specifierName: "css" | "cx", +) => + specifiers.find( + (specifier) => + specifier.type === "ImportSpecifier" && + specifier.imported.type === "Identifier" && + specifier.imported.name === specifierName, + ); + +const isCssMethod = ( + { callee }: CallExpression, + importName: string, + methodName: "extend" | "make", +) => + importName !== "" && + callee.type === "MemberExpression" && + callee.object.type === "Identifier" && + callee.object.name === importName && + callee.property.type === "Identifier" && + callee.property.name === methodName; + +const normalizeConfig = (config: ResolvedConfig) => { + const { build, configFile: file, resolve } = config; + const input = build.rollupOptions.input ?? "index.html"; // fallback to vite's default + + // https://vite.dev/config/shared-options.html#resolve-extensions + const supportedExts = new Set([".mjs", ".js", ".mts", ".ts", ".jsx", ".tsx"]); + + const root = path.isAbsolute(config.root) + ? config.root + : path.resolve( + file != null ? path.dirname(file) : process.cwd(), + config.root, + ); + + return { + assetsDir: build.assetsDir, + root, + extensions: resolve.extensions.filter((ext) => supportedExts.has(ext)), + + alias: resolve.alias.reduce>( + (acc, { find, replacement }) => + find === "string" + ? { ...acc, [find]: [...(acc[find] ?? []), replacement] } + : acc, + {}, + ), + + inputs: (typeof input === "string" + ? [input] + : Array.isArray(input) + ? input + : Object.values(input) + ).map((input) => path.resolve(root, input)), + }; +}; + +const plugin = async (options: PluginOptions = {}): Promise => { + const { css, getCssMakeInput, getCssFileContent } = await import("./css"); + const { caches } = await import("./cx"); + + const packageName = "@swan-io/css"; + const packageAliases = new Set([packageName]); + + let assetsDir = "assets"; + let cxVirtualModuleCode = ""; + let emittedFileName = ""; + + const cxVirtualModuleId = "virtual:@swan-io/cx"; + const cxResolvedVirtualModuleId = "\0" + cxVirtualModuleId; + + return { + name: packageName, + + configResolved: (resolvedConfig) => { + const config = normalizeConfig(resolvedConfig); + const { alias, extensions, inputs, root } = config; + + assetsDir = config.assetsDir; + + if (alias[packageName] != null) { + alias[packageName].forEach((item) => packageAliases.add(item)); + } + + const resolve = new ResolverFactory({ alias, extensions }); + const resolvedFiles = new Set(); + + const visit = (file: string) => { + if (resolvedFiles.has(file)) { + return; + } + + resolvedFiles.add(file); + + const code = fs.readFileSync(file, "utf-8"); + const dir = path.dirname(file); + const ext = path.extname(file); + + if (ext === ".html" || ext === ".htm") { + const html = HTMLParser.parse(code); + + const imports = [...html.querySelectorAll(`script[type="module"]`)] + .map((item) => item.getAttribute("src")) + .filter((src) => src != null) + .filter((src) => extensions.includes(path.extname(src))) + .map((src) => + path.resolve( + root, + path.isAbsolute(src) ? path.join(dir, src) : src, + ), + ); + + // Depth-first: visit each import before continuing + for (const item of imports) { + visit(item); + } + } else if (extensions.includes(ext)) { + const imports = new Set(); + + let cssImportName = ""; + + parseAndWalk(code, file, (node) => { + switch (node.type) { + case "ExportNamedDeclaration": { + if ( + typeof node.source != null && + typeof node.source?.value === "string" + ) { + imports.add(node.source.value); + } + + break; + } + + case "ExportAllDeclaration": { + imports.add(node.source.value); + break; + } + + case "ImportExpression": { + if ( + node.source.type === "Literal" && + typeof node.source.value === "string" + ) { + imports.add(node.source.value); + } + + break; + } + + case "ImportDeclaration": { + imports.add(node.source.value); + + if (packageAliases.has(node.source.value)) { + const specifier = findSpecifier(node, "css"); + + if (specifier != null) { + cssImportName = specifier.local.name; + } + } + + break; + } + + case "CallExpression": { + if (isCssMethod(node, cssImportName, "extend")) { + const arg = node.arguments[0]; + + if (arg?.type === "ObjectExpression") { + css.extend( + new Function( + `return ${code.slice(arg.start, arg.end)};`, + )(), + ); + } + + break; + } + + if (isCssMethod(node, cssImportName, "make")) { + const arg = node.arguments[0]; + + if (arg?.type === "ObjectExpression") { + css.make( + new Function( + `return ${code.slice(arg.start, arg.end)};`, + )(), + ); + } else if ( + arg?.type === "ArrowFunctionExpression" || + arg?.type === "FunctionExpression" + ) { + css.make( + new Function( + "input", + `return (${code.slice(arg.start, arg.end)})(input);`, + )(getCssMakeInput()), + ); + } + } + } + } + }); + + for (const specifier of imports) { + try { + const resolved = resolve.sync(dir, specifier).path; + + if (resolved != null) { + visit(resolved); + } + } catch { + // ignore unresolved + } + } + } + }; + + for (const input of inputs) { + visit(input); + } + + const cxId = path.join(__dirname, "./cx.mjs"); + const cxCode = fs.readFileSync(cxId, "utf-8"); + + const magicString = new MagicString(cxCode); + + const toStringMap = (map: Map): string => + `new Map([${[...map.entries()] + .reduce((acc, [key, value]) => { + return value != null ? [...acc, `["${key}", "${value}"]`] : acc; + }, []) + .join(",")}])`; + + parseAndWalk(cxCode, cxId, (node) => { + if (node.type === "VariableDeclaration") { + const declaration = node.declarations[0]; + + if ( + declaration != null && + declaration.id.type === "Identifier" && + declaration.id.name === "caches" + ) { + magicString.overwrite( + node.start, + node.end, + ` +var caches = { + reset: new Set([${[...caches.reset].map((item) => `"${item}"`).join(",")}]), + atomic: ${toStringMap(caches.atomic)}, + hover: ${toStringMap(caches.hover)}, + focus: ${toStringMap(caches.focus)}, + active: ${toStringMap(caches.active)}, +}; +`.trim(), + ); + } + } + }); + + cxVirtualModuleCode = magicString.toString(); + }, + + resolveId(id) { + if (id === cxVirtualModuleId) { + return cxResolvedVirtualModuleId; + } + }, + + load(id) { + if (id === cxResolvedVirtualModuleId) { + return cxVirtualModuleCode; + } + }, + + transform(code, id) { + let cssImportName = ""; + let cxImportName = ""; + + const magicString = new MagicString(code); + + parseAndWalk(code, id, (node) => { + switch (node.type) { + case "ImportDeclaration": { + if (packageAliases.has(node.source.value)) { + const cssLocalName = findSpecifier(node, "css")?.local.name; + const cxLocalName = findSpecifier(node, "cx")?.local.name; + + if (cssLocalName != null) { + cssImportName = cssLocalName; + magicString.overwrite(node.start, node.end, ""); + } + + if (cxLocalName != null && cxLocalName !== "") { + cxImportName = cxLocalName; + + magicString.overwrite( + node.start, + node.end, + `import { ${cxLocalName === "cx" ? "cx" : `cx as ${cxLocalName}`} } from "${cxVirtualModuleId}";`, + ); + } + } + + break; + } + + case "CallExpression": { + if (isCssMethod(node, cssImportName, "extend")) { + const arg = node.arguments[0]; + + if (arg?.type === "ObjectExpression") { + const result = css.extend( + new Function( + `return ${magicString.slice(arg.start, arg.end)};`, + )(), + ); + + magicString.overwrite( + node.start, + node.end, + JSON.stringify(result, null, 2), + ); + } else { + magicString.overwrite(node.start, node.end, "{}"); + } + + break; + } + + if (isCssMethod(node, cssImportName, "make")) { + const arg = node.arguments[0]; + + if (arg?.type === "ObjectExpression") { + const result = css.make( + new Function( + `return ${magicString.slice(arg.start, arg.end)};`, + )(), + ); + + magicString.overwrite( + node.start, + node.end, + JSON.stringify(result, null, 2), + ); + } else if ( + arg?.type === "ArrowFunctionExpression" || + arg?.type === "FunctionExpression" + ) { + const result = css.make( + new Function( + "input", + `return (${magicString.slice(arg.start, arg.end)})(input);`, + )(getCssMakeInput()), + ); + + magicString.overwrite( + node.start, + node.end, + JSON.stringify(result, null, 2), + ); + } else { + magicString.overwrite(node.start, node.end, "{}"); + } + } + } + } + }); + + if (cssImportName !== "" || cxImportName !== "") { + return { + code: magicString.toString(), + map: magicString.generateMap({ hires: true }), + }; + } + }, + + generateBundle() { + const source = getCssFileContent(); + + const hash = createHash("sha256") + .update(source) + .digest("hex") + .slice(0, 8); + + const fileName = options.fileName ?? "styles"; + emittedFileName = path.join(assetsDir, `${fileName}-${hash}.css`); + + this.emitFile({ + type: "asset", + source, + fileName: emittedFileName, + }); + }, + + transformIndexHtml(html) { + if (emittedFileName !== "") { + const attrs = { + rel: "stylesheet", + id: "swan-stylesheet", + crossorigin: true, + href: "/" + emittedFileName, + }; + + return { + html, + tags: [{ tag: "link", injectTo: "head", attrs }], + }; + } + }, + }; +}; + +export default plugin; diff --git a/tsup.config.ts b/tsup.config.ts index eafcf7a..408807d 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,16 +1,18 @@ import { defineConfig } from "tsup"; const config = { - entry: ["src/index.ts"], + entry: ["src/index.ts", "src/vite-plugin.ts"], target: "es2019", tsconfig: "./tsconfig.build.json", bundle: true, clean: false, + dts: false, sourcemap: false, splitting: false, }; export default defineConfig([ { ...config, format: "cjs", dts: true }, - { ...config, format: "esm", dts: false }, + { ...config, format: "esm" }, + { ...config, entry: ["src/cx.ts"], format: "esm" }, ]);