diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d6c69c..f7499b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,88 +5,96 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.6.0] - 2025-04-06 + +- add new rule `no-empty-style-blocks` that detects and disallows empty style objects in vanilla-extract style functions + - Identifies empty objects in style, styleVariants, recipe, globalStyle and other API functions + - Handles nested empty selectors, media queries, and conditional styles + - Provides auto-fix capability to remove unnecessary empty blocks + - Special handling for recipe objects with empty base and variants + ## [1.5.3] - 2025-03-12 -- Add bug and feature request templates () +- Add bug and feature request templates ## [1.5.2] - 2025-03-12 -- Add CODEOWNERS file to enforce code review requirements (cd4314d) +- Add CODEOWNERS file to enforce code review requirements ## [1.5.1] - 2025-03-12 -- Update project dependencies to latest versions (d7b0ca8) +- Update project dependencies to latest versions ## [1.5.0] - 2025-03-12 -- Fix handling of missing groupOrder configuration (46751da) +- Fix handling of missing groupOrder configuration - Refactor negative conditions to positive ones with optional chaining - Add comprehensive tests to achieve total coverage ## [1.4.7] - 2025-03-10 -- Exclude test directories from published package (5557409) +- Exclude test directories from published package ## [1.4.6] - 2025-03-10 -- Add demo gif to README (fb77b52) +- Add demo gif to README ## [1.4.5] - 2025-03-10 -- Add GitHub Actions workflow for linting and testing (58249ba) +- Add GitHub Actions workflow for linting and testing ## [1.4.4] - 2025-03-10 -- Improve GitHub Actions workflow for release creation (d2b62d3) +- Improve GitHub Actions workflow for release creation ## [1.4.3] - 2025-03-10 -- Add coverage for shared utility functions (1092b47) +- Add coverage for shared utility functions ## [1.4.2] - 2025-03-09 -- Add GitHub Action to create releases from tags (7c19c9d) +- Add GitHub Action to create releases from tags ## [1.4.1] - 2025-03-09 -- Add comprehensive test suite for CSS ordering rules (5f1e602) +- Add comprehensive test suite for CSS ordering rules ## [1.4.0] - 2025-03-08 -- Implement special ordering for fontFace APIs (3e9bad1) +- Implement special ordering for fontFace APIs ## [1.3.1] - 2025-03-07 -- Update milestones (8916be7) +- Update milestones ## [1.3.0] - 2025-03-06 -- Add script for versioning updates (f2ad87c) +- Add script for versioning updates ## [1.2.0] - 2025-03-05 -- Add support for linting keyframes and globalKeyframes (dea0a32) +- Add support for linting keyframes and globalKeyframes ## [1.1.2] - 2025-03-05 -- add .npmignore to exclude development files from npm package (223a81d) +- add .npmignore to exclude development files from npm package ## [1.1.1] - 2025-03-05 -- Improve packaging and TypeScript configuration (c616fb0) +- Improve packaging and TypeScript configuration ## [1.1.0] - 2025-03-04 -- Lower minimum Node.js version to 18.18.0 (44aba94) +- Lower minimum Node.js version to 18.18.0 ## [1.0.2] - 2025-03-04 -- Add npm version badge and link to vanilla-extract (87acd61) +- Add npm version badge and link to vanilla-extract ## [1.0.1] - 2025-03-04 -- Add sample CSS file for linting demo during development (88a9d43) +- Add sample CSS file for linting demo during development ## [1.0.0] - 2025-03-04 -- Initialize project with complete codebase (d569dea) +- Initialize project with complete codebase diff --git a/README.md b/README.md index d60e9e2..a688d04 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # @antebudimir/eslint-plugin-vanilla-extract -[![npm version](https://img.shields.io/npm/v/@antebudimir/eslint-plugin-vanilla-extract.svg)](https://www.npmjs.com/package/@antebudimir/eslint-plugin-vanilla-extract) [![CI](https://github.com/antebudimir/eslint-plugin-vanilla-extract/actions/workflows/ci.yml/badge.svg)](https://github.com/antebudimir/eslint-plugin-vanilla-extract/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/antebudimir/eslint-plugin-vanilla-extract/badge.svg?branch=main)](https://coveralls.io/github/antebudimir/eslint-plugin-vanilla-extract?branch=main) +[![CI](https://github.com/antebudimir/eslint-plugin-vanilla-extract/actions/workflows/ci.yml/badge.svg)](https://github.com/antebudimir/eslint-plugin-vanilla-extract/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/antebudimir/eslint-plugin-vanilla-extract/badge.svg?branch=main)](https://coveralls.io/github/antebudimir/eslint-plugin-vanilla-extract?branch=main) [![npm version](https://img.shields.io/npm/v/@antebudimir/eslint-plugin-vanilla-extract.svg)](https://www.npmjs.com/package/@antebudimir/eslint-plugin-vanilla-extract) ![NPM Downloads](https://img.shields.io/npm/d18m/%40antebudimir%2Feslint-plugin-vanilla-extract) An ESLint plugin for enforcing best practices in [vanilla-extract](https://github.com/vanilla-extract-css/vanilla-extract) CSS styles, including CSS property ordering and additional linting rules. Available presets are for alphabetical and [concentric](https://rhodesmill.org/brandon/2011/concentric-css/) CSS ordering. The plugin also supports a custom group ordering option based on groups available in [concentric CSS](src/css-rules/concentric-order/concentric-groups.ts). @@ -193,6 +193,42 @@ export const myStyle = style({ }); ``` +### vanilla-extract/no-empty-style-blocks + +This rule detects and prevents empty style blocks in vanilla-extract stylesheets. It helps maintain cleaner codebases by eliminating empty style definitions that often result from incomplete refactoring or forgotten implementations. + +```typescript +// ❌ Incorrect +import { style } from '@vanilla-extract/css'; + +export const emptyStyle = style({}); + +export const nestedEmpty = style({ + color: 'blue', + + ':hover': {}, + '@media': { + '(min-width: 768px)': {}, + }, +}); + +export const recipeWithEmptyVariants = recipe({ + base: { color: 'black' }, + variants: {}, +}); + +// ✅ Correct +import { style } from '@vanilla-extract/css'; + +export const nestedEmpty = style({ + color: 'blue', +}); + +export const recipeWithEmptyVariants = recipe({ + base: { color: 'black' }, +}); +``` + ## Font Face Declarations For `fontFace` and `globalFontFace` API calls, all three ordering rules (alphabetical, concentric, and custom) enforce the same special ordering: @@ -266,11 +302,12 @@ The roadmap outlines the project's current status and future plans: - Initial release with support for alphabetical, concentric, and custom group CSS ordering. - Auto-fix capability integrated into ESLint. - Support for multiple vanilla-extract APIs (e.g., `style`, `styleVariants`, `recipe`, `globalStyle`, `fontFace`, etc.). -- Rules tested. +- `no-empty-style-blocks` rule to disallow empty blocks. +- Comprehensive rule testing. ### Current Work -- `no-empty-blocks` rule to disallow empty blocks. +- Setting up recommended ESLint configuration for the plugin. ### Upcoming Features diff --git a/eslint.config.mjs b/eslint.config.mjs index e44a25a..97244ce 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,7 +4,6 @@ import { FlatCompat } from '@eslint/eslintrc'; import eslintPluginESLintPlugin from 'eslint-plugin-eslint-plugin'; import importPlugin from 'eslint-plugin-import'; import * as tseslint from 'typescript-eslint'; -import vanillaExtract from '@antebudimir/eslint-plugin-vanilla-extract'; // mimic CommonJS variables const __filename = fileURLToPath(import.meta.url); @@ -77,25 +76,6 @@ export default [ }, ...tseslint.configs.recommended, - { - files: ['**/*.css.ts'], - plugins: { - 'vanilla-extract': vanillaExtract, - }, - rules: { - // 'vanilla-extract/alphabetical-order': 'warn', - // 'vanilla-extract/concentric-order': 'error', - 'vanilla-extract/custom-order': [ - 'error', - { - groupOrder: ['dimensions', 'margin', 'font', 'border', 'boxShadow'], - // Optional - sortRemainingProperties: 'concentric', // or 'alphabetical' (default) - }, - ], - }, - }, - { files: ['**/*.{js,ts}'], languageOptions: { diff --git a/package.json b/package.json index 4dc86f8..a123924 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@antebudimir/eslint-plugin-vanilla-extract", - "version": "1.5.3", + "version": "1.6.0", "description": "ESLint plugin for enforcing best practices in vanilla-extract CSS styles, including CSS property ordering and additional linting rules.", "author": "Ante Budimir", "license": "MIT", @@ -84,6 +84,6 @@ "prettier": "^3.5.3", "typescript": "^5.8.2", "typescript-eslint": "^8.26.1", - "vitest": "^3.0.8" + "vitest": "3.0.8" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 110db30..5bd0433 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,13 +43,13 @@ importers: version: 10.1.1(eslint@9.22.0) eslint-import-resolver-typescript: specifier: ^3.8.5 - version: 3.8.5(eslint-plugin-import@2.31.0)(eslint@9.22.0) + version: 3.9.1(eslint-plugin-import@2.31.0)(eslint@9.22.0) eslint-plugin-eslint-plugin: specifier: ^6.4.0 version: 6.4.0(eslint@9.22.0) eslint-plugin-import: specifier: ^2.31.0 - version: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.5)(eslint@9.22.0) + version: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.1)(eslint@9.22.0) eslint-vitest-rule-tester: specifier: ^1.1.0 version: 1.1.0(eslint@9.22.0)(typescript@5.8.2)(vitest@3.0.8(@types/node@20.17.24)(tsx@4.19.3)) @@ -63,7 +63,7 @@ importers: specifier: ^8.26.1 version: 8.26.1(eslint@9.22.0)(typescript@5.8.2) vitest: - specifier: ^3.0.8 + specifier: 3.0.8 version: 3.0.8(@types/node@20.17.24)(tsx@4.19.3) packages: @@ -83,328 +83,187 @@ packages: resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - '@babel/parser@7.26.9': - resolution: {integrity: sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==} + '@babel/parser@7.26.10': + resolution: {integrity: sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/runtime@7.26.9': - resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==} + '@babel/runtime@7.26.10': + resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==} engines: {node: '>=6.9.0'} - '@babel/types@7.26.9': - resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} + '@babel/types@7.26.10': + resolution: {integrity: sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@emnapi/core@1.3.1': + resolution: {integrity: sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==} + + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + + '@emnapi/wasi-threads@1.0.1': + resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==} + '@emotion/hash@0.9.2': resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} - '@esbuild/aix-ppc64@0.25.0': - resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.25.1': resolution: {integrity: sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.0': - resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.25.1': resolution: {integrity: sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.0': - resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.25.1': resolution: {integrity: sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.0': - resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.25.1': resolution: {integrity: sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.0': - resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.25.1': resolution: {integrity: sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.0': - resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.25.1': resolution: {integrity: sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.0': - resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.25.1': resolution: {integrity: sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.0': - resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.25.1': resolution: {integrity: sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.0': - resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.25.1': resolution: {integrity: sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.0': - resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.25.1': resolution: {integrity: sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.0': - resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.25.1': resolution: {integrity: sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.0': - resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.25.1': resolution: {integrity: sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.0': - resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.25.1': resolution: {integrity: sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.0': - resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.25.1': resolution: {integrity: sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.0': - resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.25.1': resolution: {integrity: sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.0': - resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.25.1': resolution: {integrity: sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.0': - resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.25.1': resolution: {integrity: sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.0': - resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-arm64@0.25.1': resolution: {integrity: sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.0': - resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.25.1': resolution: {integrity: sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.0': - resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-arm64@0.25.1': resolution: {integrity: sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.0': - resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.25.1': resolution: {integrity: sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.25.0': - resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.25.1': resolution: {integrity: sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.0': - resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.25.1': resolution: {integrity: sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.0': - resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.25.1': resolution: {integrity: sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.0': - resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.25.1': resolution: {integrity: sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.4.1': - resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + '@eslint-community/eslint-utils@4.5.1': + resolution: {integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -487,6 +346,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@napi-rs/wasm-runtime@0.2.7': + resolution: {integrity: sha512-5yximcFK5FNompXfJFoWanu5l8v1hNGqNHh9du1xETp9HWk/B/PzvchX55WYOPaIeNglG8++68AAiauBAtbnzw==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -507,104 +369,107 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@rollup/rollup-android-arm-eabi@4.35.0': - resolution: {integrity: sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==} + '@rollup/rollup-android-arm-eabi@4.36.0': + resolution: {integrity: sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.35.0': - resolution: {integrity: sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA==} + '@rollup/rollup-android-arm64@4.36.0': + resolution: {integrity: sha512-NyfuLvdPdNUfUNeYKUwPwKsE5SXa2J6bCt2LdB/N+AxShnkpiczi3tcLJrm5mA+eqpy0HmaIY9F6XCa32N5yzg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.35.0': - resolution: {integrity: sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q==} + '@rollup/rollup-darwin-arm64@4.36.0': + resolution: {integrity: sha512-JQ1Jk5G4bGrD4pWJQzWsD8I1n1mgPXq33+/vP4sk8j/z/C2siRuxZtaUA7yMTf71TCZTZl/4e1bfzwUmFb3+rw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.35.0': - resolution: {integrity: sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q==} + '@rollup/rollup-darwin-x64@4.36.0': + resolution: {integrity: sha512-6c6wMZa1lrtiRsbDziCmjE53YbTkxMYhhnWnSW8R/yqsM7a6mSJ3uAVT0t8Y/DGt7gxUWYuFM4bwWk9XCJrFKA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.35.0': - resolution: {integrity: sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ==} + '@rollup/rollup-freebsd-arm64@4.36.0': + resolution: {integrity: sha512-KXVsijKeJXOl8QzXTsA+sHVDsFOmMCdBRgFmBb+mfEb/7geR7+C8ypAml4fquUt14ZyVXaw2o1FWhqAfOvA4sg==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.35.0': - resolution: {integrity: sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw==} + '@rollup/rollup-freebsd-x64@4.36.0': + resolution: {integrity: sha512-dVeWq1ebbvByI+ndz4IJcD4a09RJgRYmLccwlQ8bPd4olz3Y213uf1iwvc7ZaxNn2ab7bjc08PrtBgMu6nb4pQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.35.0': - resolution: {integrity: sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg==} + '@rollup/rollup-linux-arm-gnueabihf@4.36.0': + resolution: {integrity: sha512-bvXVU42mOVcF4le6XSjscdXjqx8okv4n5vmwgzcmtvFdifQ5U4dXFYaCB87namDRKlUL9ybVtLQ9ztnawaSzvg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.35.0': - resolution: {integrity: sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A==} + '@rollup/rollup-linux-arm-musleabihf@4.36.0': + resolution: {integrity: sha512-JFIQrDJYrxOnyDQGYkqnNBtjDwTgbasdbUiQvcU8JmGDfValfH1lNpng+4FWlhaVIR4KPkeddYjsVVbmJYvDcg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.35.0': - resolution: {integrity: sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A==} + '@rollup/rollup-linux-arm64-gnu@4.36.0': + resolution: {integrity: sha512-KqjYVh3oM1bj//5X7k79PSCZ6CvaVzb7Qs7VMWS+SlWB5M8p3FqufLP9VNp4CazJ0CsPDLwVD9r3vX7Ci4J56A==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.35.0': - resolution: {integrity: sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg==} + '@rollup/rollup-linux-arm64-musl@4.36.0': + resolution: {integrity: sha512-QiGnhScND+mAAtfHqeT+cB1S9yFnNQ/EwCg5yE3MzoaZZnIV0RV9O5alJAoJKX/sBONVKeZdMfO8QSaWEygMhw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.35.0': - resolution: {integrity: sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g==} + '@rollup/rollup-linux-loongarch64-gnu@4.36.0': + resolution: {integrity: sha512-1ZPyEDWF8phd4FQtTzMh8FQwqzvIjLsl6/84gzUxnMNFBtExBtpL51H67mV9xipuxl1AEAerRBgBwFNpkw8+Lg==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.35.0': - resolution: {integrity: sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA==} + '@rollup/rollup-linux-powerpc64le-gnu@4.36.0': + resolution: {integrity: sha512-VMPMEIUpPFKpPI9GZMhJrtu8rxnp6mJR3ZzQPykq4xc2GmdHj3Q4cA+7avMyegXy4n1v+Qynr9fR88BmyO74tg==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.35.0': - resolution: {integrity: sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g==} + '@rollup/rollup-linux-riscv64-gnu@4.36.0': + resolution: {integrity: sha512-ttE6ayb/kHwNRJGYLpuAvB7SMtOeQnVXEIpMtAvx3kepFQeowVED0n1K9nAdraHUPJ5hydEMxBpIR7o4nrm8uA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.35.0': - resolution: {integrity: sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw==} + '@rollup/rollup-linux-s390x-gnu@4.36.0': + resolution: {integrity: sha512-4a5gf2jpS0AIe7uBjxDeUMNcFmaRTbNv7NxI5xOCs4lhzsVyGR/0qBXduPnoWf6dGC365saTiwag8hP1imTgag==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.35.0': - resolution: {integrity: sha512-Pim1T8rXOri+0HmV4CdKSGrqcBWX0d1HoPnQ0uw0bdp1aP5SdQVNBy8LjYncvnLgu3fnnCt17xjWGd4cqh8/hA==} + '@rollup/rollup-linux-x64-gnu@4.36.0': + resolution: {integrity: sha512-5KtoW8UWmwFKQ96aQL3LlRXX16IMwyzMq/jSSVIIyAANiE1doaQsx/KRyhAvpHlPjPiSU/AYX/8m+lQ9VToxFQ==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.35.0': - resolution: {integrity: sha512-QysqXzYiDvQWfUiTm8XmJNO2zm9yC9P/2Gkrwg2dH9cxotQzunBHYr6jk4SujCTqnfGxduOmQcI7c2ryuW8XVg==} + '@rollup/rollup-linux-x64-musl@4.36.0': + resolution: {integrity: sha512-sycrYZPrv2ag4OCvaN5js+f01eoZ2U+RmT5as8vhxiFz+kxwlHrsxOwKPSA8WyS+Wc6Epid9QeI/IkQ9NkgYyQ==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.35.0': - resolution: {integrity: sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg==} + '@rollup/rollup-win32-arm64-msvc@4.36.0': + resolution: {integrity: sha512-qbqt4N7tokFwwSVlWDsjfoHgviS3n/vZ8LK0h1uLG9TYIRuUTJC88E1xb3LM2iqZ/WTqNQjYrtmtGmrmmawB6A==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.35.0': - resolution: {integrity: sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw==} + '@rollup/rollup-win32-ia32-msvc@4.36.0': + resolution: {integrity: sha512-t+RY0JuRamIocMuQcfwYSOkmdX9dtkr1PbhKW42AMvaDQa+jOdpUYysroTF/nuPpAaQMWp7ye+ndlmmthieJrQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.35.0': - resolution: {integrity: sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw==} + '@rollup/rollup-win32-x64-msvc@4.36.0': + resolution: {integrity: sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw==} cpu: [x64] os: [win32] '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@types/eslint@9.6.1': resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} @@ -673,6 +538,61 @@ packages: resolution: {integrity: sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@unrs/rspack-resolver-binding-darwin-arm64@1.1.2': + resolution: {integrity: sha512-bQx2L40UF5XxsXwkD26PzuspqUbUswWVbmclmUC+c83Cv/EFrFJ1JaZj5Q5jyYglKGOtyIWY/hXTCdWRN9vT0Q==} + cpu: [arm64] + os: [darwin] + + '@unrs/rspack-resolver-binding-darwin-x64@1.1.2': + resolution: {integrity: sha512-dMi9a7//BsuPTnhWEDxmdKZ6wxQlPnAob8VSjefGbKX/a+pHfTaX1pm/jv2VPdarP96IIjCKPatJS/TtLQeGQA==} + cpu: [x64] + os: [darwin] + + '@unrs/rspack-resolver-binding-freebsd-x64@1.1.2': + resolution: {integrity: sha512-RiBZQ+LSORQObfhV1yH7jGz+4sN3SDYtV53jgc8tUVvqdqVDaUm1KA3zHLffmoiYNGrYkE3sSreGC+FVpsB4Vg==} + cpu: [x64] + os: [freebsd] + + '@unrs/rspack-resolver-binding-linux-arm-gnueabihf@1.1.2': + resolution: {integrity: sha512-IyKIFBtOvuPCJt1WPx9e9ovTGhZzrIbW11vWzw4aPmx3VShE+YcMpAldqQubdCep0UVKZyFt+2hQDQZwFiJ4jg==} + cpu: [arm] + os: [linux] + + '@unrs/rspack-resolver-binding-linux-arm64-gnu@1.1.2': + resolution: {integrity: sha512-RfYtlCtJrv5i6TO4dSlpbyOJX9Zbhmkqrr9hjDfr6YyE5KD0ywLRzw8UjXsohxG1XWgRpb2tvPuRYtURJwbqWg==} + cpu: [arm64] + os: [linux] + + '@unrs/rspack-resolver-binding-linux-arm64-musl@1.1.2': + resolution: {integrity: sha512-MaITzkoqsn1Rm3+YnplubgAQEfOt+2jHfFvuFhXseUfcfbxe8Zyc3TM7LKwgv7mRVjIl+/yYN5JqL0cjbnhAnQ==} + cpu: [arm64] + os: [linux] + + '@unrs/rspack-resolver-binding-linux-x64-gnu@1.1.2': + resolution: {integrity: sha512-Nu981XmzQqis/uB3j4Gi3p5BYCd/zReU5zbJmjMrEH7IIRH0dxZpdOmS/+KwEk6ao7Xd8P2D2gDHpHD/QTp0aQ==} + cpu: [x64] + os: [linux] + + '@unrs/rspack-resolver-binding-linux-x64-musl@1.1.2': + resolution: {integrity: sha512-xJupeDvaRpV0ADMuG1dY9jkOjhUzTqtykvchiU2NldSD+nafSUcMWnoqzNUx7HGiqbTMOw9d9xT8ZiFs+6ZFyQ==} + cpu: [x64] + os: [linux] + + '@unrs/rspack-resolver-binding-wasm32-wasi@1.1.2': + resolution: {integrity: sha512-un6X/xInks+KEgGpIHFV8BdoODHRohaDRvOwtjq+FXuoI4Ga0P6sLRvf4rPSZDvoMnqUhZtVNG0jG9oxOnrrLQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/rspack-resolver-binding-win32-arm64-msvc@1.1.2': + resolution: {integrity: sha512-2lCFkeT1HYUb/OOStBS1m67aZOf9BQxRA+Wf/xs94CGgzmoQt7H4V/BrkB/GSGKsudXjkiwt2oHNkHiowAS90A==} + cpu: [arm64] + os: [win32] + + '@unrs/rspack-resolver-binding-win32-x64-msvc@1.1.2': + resolution: {integrity: sha512-EYfya5HCQ/8Yfy7rvAAX2rGytu81+d/CIhNCbZfNKLQ690/qFsdEeTXRsMQW1afHoluMM50PsjPYu8ndy8fSQg==} + cpu: [x64] + os: [win32] + '@vanilla-extract/css@1.17.1': resolution: {integrity: sha512-tOHQXHm10FrJeXKFeWE09JfDGN/tvV6mbjwoNB9k03u930Vg021vTnbrCwVLkECj9Zvh/SHLBHJ4r2flGqfovw==} @@ -710,6 +630,9 @@ packages: '@vitest/pretty-format@3.0.8': resolution: {integrity: sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg==} + '@vitest/pretty-format@3.0.9': + resolution: {integrity: sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==} + '@vitest/runner@3.0.8': resolution: {integrity: sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w==} @@ -727,8 +650,8 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} engines: {node: '>=0.4.0'} hasBin: true @@ -762,8 +685,8 @@ packages: resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} engines: {node: '>= 0.4'} - array.prototype.findlastindex@1.2.5: - resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} engines: {node: '>= 0.4'} array.prototype.flat@1.3.3: @@ -815,8 +738,8 @@ packages: resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} engines: {node: '>= 0.4'} - call-bound@1.0.3: - resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} callsites@3.1.0: @@ -937,10 +860,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - enhanced-resolve@5.18.1: - resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} - engines: {node: '>=10.13.0'} - es-abstract@1.23.9: resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} engines: {node: '>= 0.4'} @@ -972,11 +891,6 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.25.0: - resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==} - engines: {node: '>=18'} - hasBin: true - esbuild@0.25.1: resolution: {integrity: sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==} engines: {node: '>=18'} @@ -995,8 +909,8 @@ packages: eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - eslint-import-resolver-typescript@3.8.5: - resolution: {integrity: sha512-0ZRnzOqKc7TRm85w6REOUkVLHevN6nWd/xZsmKhSD/dcDktoxQaQAg59e5EK/QEsGFf7o5JSpE6qTwCEz0WjTw==} + eslint-import-resolver-typescript@3.9.1: + resolution: {integrity: sha512-euxa5rTGqHeqVxmOHT25hpk58PxkQ4mNoX6Yun4ooGaCHAxOCojJYNvjmyeOQxj/LyW+3fulH0+xtk+p2kPPTw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: eslint: '*' @@ -1205,9 +1119,6 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -1467,8 +1378,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.9: - resolution: {integrity: sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==} + nanoid@3.3.10: + resolution: {integrity: sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -1606,11 +1517,14 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.35.0: - resolution: {integrity: sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==} + rollup@4.36.0: + resolution: {integrity: sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rspack-resolver@1.1.2: + resolution: {integrity: sha512-eHhz+9JWHFdbl/CVVqEP6kviLFZqw1s0MWxLdsGMtUKUspSO3SERptPohmrUIC9jT1bGV9Bd3+r8AmWbdfNAzQ==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -1682,8 +1596,8 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - stable-hash@0.0.4: - resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -1735,10 +1649,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} - engines: {node: '>=6'} - test-exclude@7.0.1: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} @@ -1778,6 +1688,9 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.19.3: resolution: {integrity: sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==} engines: {node: '>=18.0.0'} @@ -1830,8 +1743,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite@6.2.1: - resolution: {integrity: sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==} + vite@6.2.2: + resolution: {integrity: sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -1910,8 +1823,8 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-typed-array@1.1.18: - resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} which@2.0.2: @@ -1953,174 +1866,115 @@ snapshots: '@babel/helper-validator-identifier@7.25.9': {} - '@babel/parser@7.26.9': + '@babel/parser@7.26.10': dependencies: - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 - '@babel/runtime@7.26.9': + '@babel/runtime@7.26.10': dependencies: regenerator-runtime: 0.14.1 - '@babel/types@7.26.9': + '@babel/types@7.26.10': dependencies: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 '@bcoe/v8-coverage@1.0.2': {} + '@emnapi/core@1.3.1': + dependencies: + '@emnapi/wasi-threads': 1.0.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/hash@0.9.2': {} - '@esbuild/aix-ppc64@0.25.0': - optional: true - '@esbuild/aix-ppc64@0.25.1': optional: true - '@esbuild/android-arm64@0.25.0': - optional: true - '@esbuild/android-arm64@0.25.1': optional: true - '@esbuild/android-arm@0.25.0': - optional: true - '@esbuild/android-arm@0.25.1': optional: true - '@esbuild/android-x64@0.25.0': - optional: true - '@esbuild/android-x64@0.25.1': optional: true - '@esbuild/darwin-arm64@0.25.0': - optional: true - '@esbuild/darwin-arm64@0.25.1': optional: true - '@esbuild/darwin-x64@0.25.0': - optional: true - '@esbuild/darwin-x64@0.25.1': optional: true - '@esbuild/freebsd-arm64@0.25.0': - optional: true - '@esbuild/freebsd-arm64@0.25.1': optional: true - '@esbuild/freebsd-x64@0.25.0': - optional: true - '@esbuild/freebsd-x64@0.25.1': optional: true - '@esbuild/linux-arm64@0.25.0': - optional: true - '@esbuild/linux-arm64@0.25.1': optional: true - '@esbuild/linux-arm@0.25.0': - optional: true - '@esbuild/linux-arm@0.25.1': optional: true - '@esbuild/linux-ia32@0.25.0': - optional: true - '@esbuild/linux-ia32@0.25.1': optional: true - '@esbuild/linux-loong64@0.25.0': - optional: true - '@esbuild/linux-loong64@0.25.1': optional: true - '@esbuild/linux-mips64el@0.25.0': - optional: true - '@esbuild/linux-mips64el@0.25.1': optional: true - '@esbuild/linux-ppc64@0.25.0': - optional: true - '@esbuild/linux-ppc64@0.25.1': optional: true - '@esbuild/linux-riscv64@0.25.0': - optional: true - '@esbuild/linux-riscv64@0.25.1': optional: true - '@esbuild/linux-s390x@0.25.0': - optional: true - '@esbuild/linux-s390x@0.25.1': optional: true - '@esbuild/linux-x64@0.25.0': - optional: true - '@esbuild/linux-x64@0.25.1': optional: true - '@esbuild/netbsd-arm64@0.25.0': - optional: true - '@esbuild/netbsd-arm64@0.25.1': optional: true - '@esbuild/netbsd-x64@0.25.0': - optional: true - '@esbuild/netbsd-x64@0.25.1': optional: true - '@esbuild/openbsd-arm64@0.25.0': - optional: true - '@esbuild/openbsd-arm64@0.25.1': optional: true - '@esbuild/openbsd-x64@0.25.0': - optional: true - '@esbuild/openbsd-x64@0.25.1': optional: true - '@esbuild/sunos-x64@0.25.0': - optional: true - '@esbuild/sunos-x64@0.25.1': optional: true - '@esbuild/win32-arm64@0.25.0': - optional: true - '@esbuild/win32-arm64@0.25.1': optional: true - '@esbuild/win32-ia32@0.25.0': - optional: true - '@esbuild/win32-ia32@0.25.1': optional: true - '@esbuild/win32-x64@0.25.0': - optional: true - '@esbuild/win32-x64@0.25.1': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.22.0)': + '@eslint-community/eslint-utils@4.5.1(eslint@9.22.0)': dependencies: eslint: 9.22.0 eslint-visitor-keys: 3.4.3 @@ -2205,6 +2059,13 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@napi-rs/wasm-runtime@0.2.7': + dependencies: + '@emnapi/core': 1.3.1 + '@emnapi/runtime': 1.3.1 + '@tybys/wasm-util': 0.9.0 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2222,65 +2083,70 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@rollup/rollup-android-arm-eabi@4.35.0': + '@rollup/rollup-android-arm-eabi@4.36.0': optional: true - '@rollup/rollup-android-arm64@4.35.0': + '@rollup/rollup-android-arm64@4.36.0': optional: true - '@rollup/rollup-darwin-arm64@4.35.0': + '@rollup/rollup-darwin-arm64@4.36.0': optional: true - '@rollup/rollup-darwin-x64@4.35.0': + '@rollup/rollup-darwin-x64@4.36.0': optional: true - '@rollup/rollup-freebsd-arm64@4.35.0': + '@rollup/rollup-freebsd-arm64@4.36.0': optional: true - '@rollup/rollup-freebsd-x64@4.35.0': + '@rollup/rollup-freebsd-x64@4.36.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.35.0': + '@rollup/rollup-linux-arm-gnueabihf@4.36.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.35.0': + '@rollup/rollup-linux-arm-musleabihf@4.36.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.35.0': + '@rollup/rollup-linux-arm64-gnu@4.36.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.35.0': + '@rollup/rollup-linux-arm64-musl@4.36.0': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.35.0': + '@rollup/rollup-linux-loongarch64-gnu@4.36.0': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.35.0': + '@rollup/rollup-linux-powerpc64le-gnu@4.36.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.35.0': + '@rollup/rollup-linux-riscv64-gnu@4.36.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.35.0': + '@rollup/rollup-linux-s390x-gnu@4.36.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.35.0': + '@rollup/rollup-linux-x64-gnu@4.36.0': optional: true - '@rollup/rollup-linux-x64-musl@4.35.0': + '@rollup/rollup-linux-x64-musl@4.36.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.35.0': + '@rollup/rollup-win32-arm64-msvc@4.36.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.35.0': + '@rollup/rollup-win32-ia32-msvc@4.36.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.35.0': + '@rollup/rollup-win32-x64-msvc@4.36.0': optional: true '@rtsao/scc@1.1.0': {} + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + optional: true + '@types/eslint@9.6.1': dependencies: '@types/estree': 1.0.6 @@ -2373,7 +2239,7 @@ snapshots: '@typescript-eslint/utils@8.26.1(eslint@9.22.0)(typescript@5.8.2)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.22.0) + '@eslint-community/eslint-utils': 4.5.1(eslint@9.22.0) '@typescript-eslint/scope-manager': 8.26.1 '@typescript-eslint/types': 8.26.1 '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.8.2) @@ -2387,6 +2253,41 @@ snapshots: '@typescript-eslint/types': 8.26.1 eslint-visitor-keys: 4.2.0 + '@unrs/rspack-resolver-binding-darwin-arm64@1.1.2': + optional: true + + '@unrs/rspack-resolver-binding-darwin-x64@1.1.2': + optional: true + + '@unrs/rspack-resolver-binding-freebsd-x64@1.1.2': + optional: true + + '@unrs/rspack-resolver-binding-linux-arm-gnueabihf@1.1.2': + optional: true + + '@unrs/rspack-resolver-binding-linux-arm64-gnu@1.1.2': + optional: true + + '@unrs/rspack-resolver-binding-linux-arm64-musl@1.1.2': + optional: true + + '@unrs/rspack-resolver-binding-linux-x64-gnu@1.1.2': + optional: true + + '@unrs/rspack-resolver-binding-linux-x64-musl@1.1.2': + optional: true + + '@unrs/rspack-resolver-binding-wasm32-wasi@1.1.2': + dependencies: + '@napi-rs/wasm-runtime': 0.2.7 + optional: true + + '@unrs/rspack-resolver-binding-win32-arm64-msvc@1.1.2': + optional: true + + '@unrs/rspack-resolver-binding-win32-x64-msvc@1.1.2': + optional: true + '@vanilla-extract/css@1.17.1': dependencies: '@emotion/hash': 0.9.2 @@ -2435,18 +2336,22 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.8(vite@6.2.1(@types/node@20.17.24)(tsx@4.19.3))': + '@vitest/mocker@3.0.8(vite@6.2.2(@types/node@20.17.24)(tsx@4.19.3))': dependencies: '@vitest/spy': 3.0.8 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.2.1(@types/node@20.17.24)(tsx@4.19.3) + vite: 6.2.2(@types/node@20.17.24)(tsx@4.19.3) '@vitest/pretty-format@3.0.8': dependencies: tinyrainbow: 2.0.0 + '@vitest/pretty-format@3.0.9': + dependencies: + tinyrainbow: 2.0.0 + '@vitest/runner@3.0.8': dependencies: '@vitest/utils': 3.0.8 @@ -2468,11 +2373,11 @@ snapshots: loupe: 3.1.3 tinyrainbow: 2.0.0 - acorn-jsx@5.3.2(acorn@8.14.0): + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: - acorn: 8.14.0 + acorn: 8.14.1 - acorn@8.14.0: {} + acorn@8.14.1: {} ajv@6.12.6: dependencies: @@ -2495,7 +2400,7 @@ snapshots: array-buffer-byte-length@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-array-buffer: 3.0.5 array-includes@3.1.8: @@ -2507,9 +2412,10 @@ snapshots: get-intrinsic: 1.3.0 is-string: 1.1.1 - array.prototype.findlastindex@1.2.5: + array.prototype.findlastindex@1.2.6: dependencies: call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 es-abstract: 1.23.9 es-errors: 1.3.0 @@ -2577,7 +2483,7 @@ snapshots: get-intrinsic: 1.3.0 set-function-length: 1.2.2 - call-bound@1.0.3: + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 @@ -2621,19 +2527,19 @@ snapshots: data-view-buffer@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 data-view-byte-length@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 data-view-byte-offset@1.0.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 @@ -2683,18 +2589,13 @@ snapshots: emoji-regex@9.2.2: {} - enhanced-resolve@5.18.1: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.2.1 - es-abstract@1.23.9: dependencies: array-buffer-byte-length: 1.0.2 arraybuffer.prototype.slice: 1.0.4 available-typed-arrays: 1.0.7 call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 data-view-buffer: 1.0.2 data-view-byte-length: 1.0.2 data-view-byte-offset: 1.0.1 @@ -2740,7 +2641,7 @@ snapshots: typed-array-byte-offset: 1.0.4 typed-array-length: 1.0.7 unbox-primitive: 1.1.0 - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 es-define-property@1.0.1: {} @@ -2769,34 +2670,6 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.25.0: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.0 - '@esbuild/android-arm': 0.25.0 - '@esbuild/android-arm64': 0.25.0 - '@esbuild/android-x64': 0.25.0 - '@esbuild/darwin-arm64': 0.25.0 - '@esbuild/darwin-x64': 0.25.0 - '@esbuild/freebsd-arm64': 0.25.0 - '@esbuild/freebsd-x64': 0.25.0 - '@esbuild/linux-arm': 0.25.0 - '@esbuild/linux-arm64': 0.25.0 - '@esbuild/linux-ia32': 0.25.0 - '@esbuild/linux-loong64': 0.25.0 - '@esbuild/linux-mips64el': 0.25.0 - '@esbuild/linux-ppc64': 0.25.0 - '@esbuild/linux-riscv64': 0.25.0 - '@esbuild/linux-s390x': 0.25.0 - '@esbuild/linux-x64': 0.25.0 - '@esbuild/netbsd-arm64': 0.25.0 - '@esbuild/netbsd-x64': 0.25.0 - '@esbuild/openbsd-arm64': 0.25.0 - '@esbuild/openbsd-x64': 0.25.0 - '@esbuild/sunos-x64': 0.25.0 - '@esbuild/win32-arm64': 0.25.0 - '@esbuild/win32-ia32': 0.25.0 - '@esbuild/win32-x64': 0.25.0 - esbuild@0.25.1: optionalDependencies: '@esbuild/aix-ppc64': 0.25.1 @@ -2824,7 +2697,6 @@ snapshots: '@esbuild/win32-arm64': 0.25.1 '@esbuild/win32-ia32': 0.25.1 '@esbuild/win32-x64': 0.25.1 - optional: true escape-string-regexp@4.0.0: {} @@ -2840,50 +2712,50 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.8.5(eslint-plugin-import@2.31.0)(eslint@9.22.0): + eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.31.0)(eslint@9.22.0): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0 - enhanced-resolve: 5.18.1 eslint: 9.22.0 get-tsconfig: 4.10.0 is-bun-module: 1.3.0 - stable-hash: 0.0.4 + rspack-resolver: 1.1.2 + stable-hash: 0.0.5 tinyglobby: 0.2.12 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.5)(eslint@9.22.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.1)(eslint@9.22.0) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.5)(eslint@9.22.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.1)(eslint@9.22.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.26.1(eslint@9.22.0)(typescript@5.8.2) eslint: 9.22.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.5(eslint-plugin-import@2.31.0)(eslint@9.22.0) + eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.31.0)(eslint@9.22.0) transitivePeerDependencies: - supports-color eslint-plugin-eslint-plugin@6.4.0(eslint@9.22.0): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.22.0) + '@eslint-community/eslint-utils': 4.5.1(eslint@9.22.0) eslint: 9.22.0 estraverse: 5.3.0 - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.5)(eslint@9.22.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.1)(eslint@9.22.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.5 + array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 eslint: 9.22.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.5)(eslint@9.22.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.1)(eslint@9.22.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -2923,7 +2795,7 @@ snapshots: eslint@9.22.0: dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.22.0) + '@eslint-community/eslint-utils': 4.5.1(eslint@9.22.0) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.19.2 '@eslint/config-helpers': 0.1.0 @@ -2963,8 +2835,8 @@ snapshots: espree@10.3.0: dependencies: - acorn: 8.14.0 - acorn-jsx: 5.3.2(acorn@8.14.0) + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) eslint-visitor-keys: 4.2.0 esquery@1.6.0: @@ -3044,7 +2916,7 @@ snapshots: function.prototype.name@1.1.8: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 functions-have-names: 1.2.3 hasown: 2.0.2 @@ -3072,7 +2944,7 @@ snapshots: get-symbol-description@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 get-intrinsic: 1.3.0 @@ -3106,8 +2978,6 @@ snapshots: gopd@1.2.0: {} - graceful-fs@4.2.11: {} - graphemer@1.4.0: {} has-bigints@1.1.0: {} @@ -3152,13 +3022,13 @@ snapshots: is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 get-intrinsic: 1.3.0 is-async-function@2.1.1: dependencies: async-function: 1.0.0 - call-bound: 1.0.3 + call-bound: 1.0.4 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -3169,7 +3039,7 @@ snapshots: is-boolean-object@1.2.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-bun-module@1.3.0: @@ -3184,26 +3054,26 @@ snapshots: is-data-view@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 get-intrinsic: 1.3.0 is-typed-array: 1.1.15 is-date-object@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-fullwidth-code-point@3.0.0: {} is-generator-function@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -3216,14 +3086,14 @@ snapshots: is-number-object@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-number@7.0.0: {} is-regex@1.2.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 gopd: 1.2.0 has-tostringtag: 1.0.2 hasown: 2.0.2 @@ -3232,32 +3102,32 @@ snapshots: is-shared-array-buffer@1.0.4: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-string@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-symbol@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-symbols: 1.1.0 safe-regex-test: 1.1.0 is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 is-weakmap@2.0.2: {} is-weakref@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-weakset@2.0.4: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 get-intrinsic: 1.3.0 isarray@2.0.5: {} @@ -3330,8 +3200,8 @@ snapshots: magicast@0.3.5: dependencies: - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 + '@babel/parser': 7.26.10 + '@babel/types': 7.26.10 source-map-js: 1.2.1 make-dir@4.0.0: @@ -3342,7 +3212,7 @@ snapshots: media-query-parser@2.0.2: dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.26.10 merge2@1.4.1: {} @@ -3367,7 +3237,7 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.9: {} + nanoid@3.3.10: {} natural-compare@1.4.0: {} @@ -3378,7 +3248,7 @@ snapshots: object.assign@4.1.7: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 has-symbols: 1.1.0 @@ -3400,7 +3270,7 @@ snapshots: object.values@1.2.1: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 @@ -3458,7 +3328,7 @@ snapshots: postcss@8.5.3: dependencies: - nanoid: 3.3.9 + nanoid: 3.3.10 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -3504,31 +3374,45 @@ snapshots: reusify@1.1.0: {} - rollup@4.35.0: + rollup@4.36.0: dependencies: '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.35.0 - '@rollup/rollup-android-arm64': 4.35.0 - '@rollup/rollup-darwin-arm64': 4.35.0 - '@rollup/rollup-darwin-x64': 4.35.0 - '@rollup/rollup-freebsd-arm64': 4.35.0 - '@rollup/rollup-freebsd-x64': 4.35.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.35.0 - '@rollup/rollup-linux-arm-musleabihf': 4.35.0 - '@rollup/rollup-linux-arm64-gnu': 4.35.0 - '@rollup/rollup-linux-arm64-musl': 4.35.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.35.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.35.0 - '@rollup/rollup-linux-riscv64-gnu': 4.35.0 - '@rollup/rollup-linux-s390x-gnu': 4.35.0 - '@rollup/rollup-linux-x64-gnu': 4.35.0 - '@rollup/rollup-linux-x64-musl': 4.35.0 - '@rollup/rollup-win32-arm64-msvc': 4.35.0 - '@rollup/rollup-win32-ia32-msvc': 4.35.0 - '@rollup/rollup-win32-x64-msvc': 4.35.0 + '@rollup/rollup-android-arm-eabi': 4.36.0 + '@rollup/rollup-android-arm64': 4.36.0 + '@rollup/rollup-darwin-arm64': 4.36.0 + '@rollup/rollup-darwin-x64': 4.36.0 + '@rollup/rollup-freebsd-arm64': 4.36.0 + '@rollup/rollup-freebsd-x64': 4.36.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.36.0 + '@rollup/rollup-linux-arm-musleabihf': 4.36.0 + '@rollup/rollup-linux-arm64-gnu': 4.36.0 + '@rollup/rollup-linux-arm64-musl': 4.36.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.36.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.36.0 + '@rollup/rollup-linux-riscv64-gnu': 4.36.0 + '@rollup/rollup-linux-s390x-gnu': 4.36.0 + '@rollup/rollup-linux-x64-gnu': 4.36.0 + '@rollup/rollup-linux-x64-musl': 4.36.0 + '@rollup/rollup-win32-arm64-msvc': 4.36.0 + '@rollup/rollup-win32-ia32-msvc': 4.36.0 + '@rollup/rollup-win32-x64-msvc': 4.36.0 fsevents: 2.3.3 + rspack-resolver@1.1.2: + optionalDependencies: + '@unrs/rspack-resolver-binding-darwin-arm64': 1.1.2 + '@unrs/rspack-resolver-binding-darwin-x64': 1.1.2 + '@unrs/rspack-resolver-binding-freebsd-x64': 1.1.2 + '@unrs/rspack-resolver-binding-linux-arm-gnueabihf': 1.1.2 + '@unrs/rspack-resolver-binding-linux-arm64-gnu': 1.1.2 + '@unrs/rspack-resolver-binding-linux-arm64-musl': 1.1.2 + '@unrs/rspack-resolver-binding-linux-x64-gnu': 1.1.2 + '@unrs/rspack-resolver-binding-linux-x64-musl': 1.1.2 + '@unrs/rspack-resolver-binding-wasm32-wasi': 1.1.2 + '@unrs/rspack-resolver-binding-win32-arm64-msvc': 1.1.2 + '@unrs/rspack-resolver-binding-win32-x64-msvc': 1.1.2 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -3536,7 +3420,7 @@ snapshots: safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 get-intrinsic: 1.3.0 has-symbols: 1.1.0 isarray: 2.0.5 @@ -3548,7 +3432,7 @@ snapshots: safe-regex-test@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-regex: 1.2.1 @@ -3591,14 +3475,14 @@ snapshots: side-channel-map@1.0.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 get-intrinsic: 1.3.0 object-inspect: 1.13.4 side-channel-weakmap@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 get-intrinsic: 1.3.0 object-inspect: 1.13.4 @@ -3618,7 +3502,7 @@ snapshots: source-map-js@1.2.1: {} - stable-hash@0.0.4: {} + stable-hash@0.0.5: {} stackback@0.0.2: {} @@ -3639,7 +3523,7 @@ snapshots: string.prototype.trim@1.2.10: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 es-abstract: 1.23.9 @@ -3649,7 +3533,7 @@ snapshots: string.prototype.trimend@1.0.9: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 @@ -3677,8 +3561,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - tapable@2.2.1: {} - test-exclude@7.0.1: dependencies: '@istanbuljs/schema': 0.1.3 @@ -3715,6 +3597,9 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tslib@2.8.1: + optional: true + tsx@4.19.3: dependencies: esbuild: 0.25.1 @@ -3729,7 +3614,7 @@ snapshots: typed-array-buffer@1.0.3: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-typed-array: 1.1.15 @@ -3774,7 +3659,7 @@ snapshots: unbox-primitive@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-bigints: 1.1.0 has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 @@ -3791,7 +3676,7 @@ snapshots: debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.3 - vite: 6.2.1(@types/node@20.17.24)(tsx@4.19.3) + vite: 6.2.2(@types/node@20.17.24)(tsx@4.19.3) transitivePeerDependencies: - '@types/node' - jiti @@ -3806,11 +3691,11 @@ snapshots: - tsx - yaml - vite@6.2.1(@types/node@20.17.24)(tsx@4.19.3): + vite@6.2.2(@types/node@20.17.24)(tsx@4.19.3): dependencies: - esbuild: 0.25.0 + esbuild: 0.25.1 postcss: 8.5.3 - rollup: 4.35.0 + rollup: 4.36.0 optionalDependencies: '@types/node': 20.17.24 fsevents: 2.3.3 @@ -3819,8 +3704,8 @@ snapshots: vitest@3.0.8(@types/node@20.17.24)(tsx@4.19.3): dependencies: '@vitest/expect': 3.0.8 - '@vitest/mocker': 3.0.8(vite@6.2.1(@types/node@20.17.24)(tsx@4.19.3)) - '@vitest/pretty-format': 3.0.8 + '@vitest/mocker': 3.0.8(vite@6.2.2(@types/node@20.17.24)(tsx@4.19.3)) + '@vitest/pretty-format': 3.0.9 '@vitest/runner': 3.0.8 '@vitest/snapshot': 3.0.8 '@vitest/spy': 3.0.8 @@ -3835,7 +3720,7 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.2.1(@types/node@20.17.24)(tsx@4.19.3) + vite: 6.2.2(@types/node@20.17.24)(tsx@4.19.3) vite-node: 3.0.8(@types/node@20.17.24)(tsx@4.19.3) why-is-node-running: 2.3.0 optionalDependencies: @@ -3864,7 +3749,7 @@ snapshots: which-builtin-type@1.2.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 function.prototype.name: 1.1.8 has-tostringtag: 1.0.2 is-async-function: 2.1.1 @@ -3876,7 +3761,7 @@ snapshots: isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 which-collection@1.0.2: dependencies: @@ -3885,12 +3770,13 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-typed-array@1.1.18: + which-typed-array@1.1.19: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 for-each: 0.3.5 + get-proto: 1.0.1 gopd: 1.2.0 has-tostringtag: 1.0.2 diff --git a/scripts/update-version.mjs b/scripts/update-version.mjs index 0c726cc..8b6526d 100644 --- a/scripts/update-version.mjs +++ b/scripts/update-version.mjs @@ -22,12 +22,12 @@ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8')); const newVersion = packageJson.version; // Read src/index.ts -let indexTsContent = await fs.readFile(indexTsPath, 'utf-8'); +const indexTsContent = await fs.readFile(indexTsPath, 'utf-8'); // Replace the version string in src/index.ts -indexTsContent = indexTsContent.replace(/version: '(\d+\.\d+\.\d+)'/, `version: '${newVersion}'`); +const updatedIndexTsContent = indexTsContent.replace(/version: '(\d+\.\d+\.\d+)'/, `version: '${newVersion}'`); // Write the updated content back to src/index.ts -await fs.writeFile(indexTsPath, indexTsContent); +await fs.writeFile(indexTsPath, updatedIndexTsContent); console.log(`Updated package.json and src/index.ts to version ${newVersion}`); diff --git a/src/css-rules/alphabetical-order/property-order-enforcer.ts b/src/css-rules/alphabetical-order/property-order-enforcer.ts index 92d3dcd..41eccef 100644 --- a/src/css-rules/alphabetical-order/property-order-enforcer.ts +++ b/src/css-rules/alphabetical-order/property-order-enforcer.ts @@ -2,7 +2,7 @@ import type { Rule } from 'eslint'; import { TSESTree } from '@typescript-eslint/utils'; import { comparePropertiesAlphabetically } from '../shared-utils/alphabetical-property-comparator.js'; import { generateFixesForCSSOrder } from '../shared-utils/css-order-fixer.js'; -import { getPropertyName } from '../shared-utils/property-separator.js'; +import { getPropertyNameForSorting } from '../shared-utils/property-separator.js'; /** * Reports an ordering issue to ESLint and generates fixes. @@ -21,8 +21,8 @@ const reportOrderingIssue = ( node: nextProperty as Rule.Node, messageId: 'alphabeticalOrder', data: { - nextProperty: getPropertyName(nextProperty), - currentProperty: getPropertyName(currentProperty), + nextProperty: getPropertyNameForSorting(nextProperty), + currentProperty: getPropertyNameForSorting(currentProperty), }, fix: (fixer) => generateFixesForCSSOrder( diff --git a/src/css-rules/alphabetical-order/rule-definition.ts b/src/css-rules/alphabetical-order/rule-definition.ts index ad2fe3f..0ebf831 100644 --- a/src/css-rules/alphabetical-order/rule-definition.ts +++ b/src/css-rules/alphabetical-order/rule-definition.ts @@ -1,5 +1,6 @@ import type { Rule } from 'eslint'; import { createNodeVisitors } from '../shared-utils/order-strategy-visitor-creator.js'; +import type { OrderingStrategy } from '../types.js'; const alphabeticalOrderRule: Rule.RuleModule = { meta: { @@ -18,7 +19,7 @@ const alphabeticalOrderRule: Rule.RuleModule = { }, }, create(context) { - return createNodeVisitors(context, 'alphabetical'); + return createNodeVisitors(context, 'alphabetical' as OrderingStrategy); }, }; diff --git a/src/css-rules/concentric-order/style-object-processor.ts b/src/css-rules/concentric-order/style-object-processor.ts index e99654c..aa0ae06 100644 --- a/src/css-rules/concentric-order/style-object-processor.ts +++ b/src/css-rules/concentric-order/style-object-processor.ts @@ -2,7 +2,7 @@ import type { Rule } from 'eslint'; import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; import { createCSSPropertyPriorityMap } from '../shared-utils/css-property-priority-map.js'; import { isSelectorsObject, processNestedSelectors } from '../shared-utils/nested-selectors-processor.js'; -import { getPropertyName, separateProperties } from '../shared-utils/property-separator.js'; +import { getPropertyNameForSorting, separateProperties } from '../shared-utils/property-separator.js'; import { enforceConcentricCSSOrder } from './property-order-enforcer.js'; import type { CSSPropertyInfo } from './types.js'; @@ -16,7 +16,7 @@ const cssPropertyPriorityMap = createCSSPropertyPriorityMap(); */ const buildCSSPropertyInfoList = (regularStyleProperties: TSESTree.Property[]): CSSPropertyInfo[] => { return regularStyleProperties.map((styleProperty) => { - const propertyName = getPropertyName(styleProperty); + const propertyName = getPropertyNameForSorting(styleProperty); const propertyInfo = cssPropertyPriorityMap.get(propertyName); return { diff --git a/src/css-rules/concentric-order/types.ts b/src/css-rules/concentric-order/types.ts index 2424f9a..53517ba 100644 --- a/src/css-rules/concentric-order/types.ts +++ b/src/css-rules/concentric-order/types.ts @@ -7,3 +7,5 @@ export interface CSSPropertyInfo { positionInGroup: number; group?: string; } + +export type SortRemainingProperties = 'alphabetical' | 'concentric'; diff --git a/src/css-rules/custom-order/property-order-enforcer.ts b/src/css-rules/custom-order/property-order-enforcer.ts index a5c6788..f350721 100644 --- a/src/css-rules/custom-order/property-order-enforcer.ts +++ b/src/css-rules/custom-order/property-order-enforcer.ts @@ -1,7 +1,7 @@ import type { Rule } from 'eslint'; import { generateFixesForCSSOrder } from '../shared-utils/css-order-fixer.js'; import { createCSSPropertyPriorityMap } from '../shared-utils/css-property-priority-map.js'; -import type { CSSPropertyInfo } from '../concentric-order/types.js'; +import type { CSSPropertyInfo, SortRemainingProperties } from '../concentric-order/types.js'; /** * Enforces a custom ordering of CSS properties based on user-defined groups. @@ -21,7 +21,7 @@ export const enforceCustomGroupOrder = ( ruleContext: Rule.RuleContext, cssPropertyInfoList: CSSPropertyInfo[], userDefinedGroups: string[] = [], - sortRemainingProperties?: 'alphabetical' | 'concentric', + sortRemainingProperties?: SortRemainingProperties, ): void => { if (cssPropertyInfoList.length > 1) { const cssPropertyPriorityMap = createCSSPropertyPriorityMap(userDefinedGroups); @@ -50,7 +50,7 @@ export const enforceCustomGroupOrder = ( } // For properties not in user-defined groups - if (sortRemainingProperties === 'alphabetical') { + if (sortRemainingProperties === ('alphabetical' as SortRemainingProperties)) { return firstProperty.name.localeCompare(secondProperty.name); } else { return ( @@ -70,9 +70,8 @@ export const enforceCustomGroupOrder = ( if (violatingProperty) { const indexInSorted = cssPropertyInfoList.indexOf(violatingProperty); const sortedProperty = sortedPropertyList[indexInSorted]; - // Defensive programming - sortedProperty will always exist and have a name because sortedPropertyList is derived from cssPropertyInfoList and cssPropertyInfoList exists and is non-empty - // Therefore, we can exclude the next line from coverage because it's unreachable in practice - /* v8 ignore next */ + // Defensive programming - sortedProperty will always exist and have a name because sortedPropertyList is derived from cssPropertyInfoList and cssPropertyInfoList exists and is non-empty. + // This fallback is theoretically unreachable in practice but included for type safety. const nextPropertyName = sortedProperty?.name ?? ''; ruleContext.report({ diff --git a/src/css-rules/custom-order/recipe-order-enforcer.ts b/src/css-rules/custom-order/recipe-order-enforcer.ts index c627aa0..a167e54 100644 --- a/src/css-rules/custom-order/recipe-order-enforcer.ts +++ b/src/css-rules/custom-order/recipe-order-enforcer.ts @@ -3,6 +3,7 @@ import { TSESTree } from '@typescript-eslint/utils'; import { processRecipeProperties } from '../shared-utils/recipe-property-processor.js'; import { processStyleNode } from '../shared-utils/style-node-processor.js'; import { enforceUserDefinedGroupOrderInStyleObject } from './style-object-processor.js'; +import type { SortRemainingProperties } from '../concentric-order/types.js'; /** * Enforces custom group ordering of CSS properties within a recipe function call. @@ -21,7 +22,7 @@ export const enforceUserDefinedGroupOrderInRecipe = ( ruleContext: Rule.RuleContext, callExpression: TSESTree.CallExpression, userDefinedGroups: string[], - sortRemainingPropertiesMethod?: 'alphabetical' | 'concentric', + sortRemainingProperties?: SortRemainingProperties, ): void => { if (callExpression.arguments[0]?.type === 'ObjectExpression') { const recipeObjectExpression = callExpression.arguments[0]; @@ -32,7 +33,7 @@ export const enforceUserDefinedGroupOrderInRecipe = ( styleContext, styleObjectNode, userDefinedGroups, - sortRemainingPropertiesMethod, + sortRemainingProperties, ), ), ); diff --git a/src/css-rules/custom-order/rule-definition.ts b/src/css-rules/custom-order/rule-definition.ts index 91e5bc8..550e143 100644 --- a/src/css-rules/custom-order/rule-definition.ts +++ b/src/css-rules/custom-order/rule-definition.ts @@ -1,10 +1,11 @@ import type { Rule } from 'eslint'; import { availableGroups } from '../concentric-order/concentric-groups.js'; import { createNodeVisitors } from '../shared-utils/order-strategy-visitor-creator.js'; +import type { SortRemainingProperties } from '../concentric-order/types.js'; interface CustomGroupRuleConfiguration { groupOrder?: string[]; - sortRemainingProperties: 'alphabetical' | 'concentric'; + sortRemainingProperties: SortRemainingProperties; } const customGroupOrderRule: Rule.RuleModule = { @@ -46,7 +47,8 @@ const customGroupOrderRule: Rule.RuleModule = { create(ruleContext: Rule.RuleContext) { const ruleConfiguration = ruleContext.options[0] as CustomGroupRuleConfiguration; const userDefinedGroupOrder = ruleConfiguration?.groupOrder ?? []; - const sortRemainingPropertiesMethod = ruleConfiguration?.sortRemainingProperties ?? 'alphabetical'; + const sortRemainingPropertiesMethod = + ruleConfiguration?.sortRemainingProperties ?? ('alphabetical' as SortRemainingProperties); return createNodeVisitors( ruleContext, diff --git a/src/css-rules/custom-order/style-object-processor.ts b/src/css-rules/custom-order/style-object-processor.ts index be3998d..21327e5 100644 --- a/src/css-rules/custom-order/style-object-processor.ts +++ b/src/css-rules/custom-order/style-object-processor.ts @@ -3,9 +3,9 @@ import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; import { concentricGroups } from '../concentric-order/concentric-groups.js'; import { createCSSPropertyPriorityMap } from '../shared-utils/css-property-priority-map.js'; import { isSelectorsObject, processNestedSelectors } from '../shared-utils/nested-selectors-processor.js'; -import { getPropertyName, separateProperties } from '../shared-utils/property-separator.js'; +import { getPropertyNameForSorting, separateProperties } from '../shared-utils/property-separator.js'; import { enforceCustomGroupOrder } from './property-order-enforcer.js'; -import type { CSSPropertyInfo } from '../concentric-order/types.js'; +import type { CSSPropertyInfo, SortRemainingProperties } from '../concentric-order/types.js'; /** * Enforces a custom ordering of CSS properties based on user-defined groups in a given style object. @@ -13,7 +13,7 @@ import type { CSSPropertyInfo } from '../concentric-order/types.js'; * @param context The ESLint rule context for reporting and fixing issues. * @param styleObject The ObjectExpression node representing the style object to be processed. * @param userDefinedGroups An array of property groups in the desired order. - * @param sortRemainingPropertiesMethod Strategy for sorting properties not in user-defined groups ('alphabetical' or 'concentric'). Defaults to 'concentric'. + * @param sortRemainingProperties Strategy for sorting properties not in user-defined groups ('alphabetical' or 'concentric'). Defaults to 'concentric'. * * This function: * 1. Validates the input styleObject. @@ -27,7 +27,7 @@ export const enforceUserDefinedGroupOrderInStyleObject = ( ruleContext: Rule.RuleContext, styleObject: TSESTree.ObjectExpression, userDefinedGroups: string[], - sortRemainingPropertiesMethod: 'alphabetical' | 'concentric' = 'concentric', + sortRemainingProperties: SortRemainingProperties = 'concentric', ): void => { if (styleObject?.type === AST_NODE_TYPES.ObjectExpression) { if (isSelectorsObject(styleObject)) { @@ -37,7 +37,7 @@ export const enforceUserDefinedGroupOrderInStyleObject = ( ruleContext, property.value, userDefinedGroups, - sortRemainingPropertiesMethod, + sortRemainingProperties, ); } }); @@ -48,7 +48,7 @@ export const enforceUserDefinedGroupOrderInStyleObject = ( const { regularProperties } = separateProperties(styleObject.properties); const cssPropertyInfoList: CSSPropertyInfo[] = regularProperties.map((property) => { - const propertyName = getPropertyName(property); + const propertyName = getPropertyNameForSorting(property); const propertyInfo = cssPropertyPriorityMap.get(propertyName); const group = userDefinedGroups.find((groupName) => concentricGroups[groupName]?.includes(propertyName)) || 'remaining'; @@ -63,15 +63,10 @@ export const enforceUserDefinedGroupOrderInStyleObject = ( }; }); - enforceCustomGroupOrder(ruleContext, cssPropertyInfoList, userDefinedGroups, sortRemainingPropertiesMethod); + enforceCustomGroupOrder(ruleContext, cssPropertyInfoList, userDefinedGroups, sortRemainingProperties); processNestedSelectors(ruleContext, styleObject, (nestedContext, nestedNode) => - enforceUserDefinedGroupOrderInStyleObject( - nestedContext, - nestedNode, - userDefinedGroups, - sortRemainingPropertiesMethod, - ), + enforceUserDefinedGroupOrderInStyleObject(nestedContext, nestedNode, userDefinedGroups, sortRemainingProperties), ); } }; diff --git a/src/css-rules/no-empty-blocks/__tests__/are-all-children-empty.test.ts b/src/css-rules/no-empty-blocks/__tests__/are-all-children-empty.test.ts new file mode 100644 index 0000000..e2efde0 --- /dev/null +++ b/src/css-rules/no-empty-blocks/__tests__/are-all-children-empty.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect } from 'vitest'; +import { areAllChildrenEmpty } from '../property-utils.js'; +import type { TSESTree } from '@typescript-eslint/utils'; + +describe('areAllChildrenEmpty', () => { + it('should return true for an empty object with no properties', () => { + const emptyObject = { + type: 'ObjectExpression', + properties: [], + }; + + const result = areAllChildrenEmpty(emptyObject as unknown as TSESTree.ObjectExpression); + expect(result).toBe(true); + }); + + it('should return false when a property is not of type Property', () => { + const objectWithNonPropertyType = { + type: 'ObjectExpression', + properties: [ + { + type: 'SpreadElement', // Not a Property type + argument: { + type: 'Identifier', + name: 'spread', + }, + }, + ], + }; + + const result = areAllChildrenEmpty(objectWithNonPropertyType as unknown as TSESTree.ObjectExpression); + expect(result).toBe(false); + }); + + it('should return false when a property value is not an ObjectExpression', () => { + const objectWithNonObjectValue = { + type: 'ObjectExpression', + properties: [ + { + type: 'Property', + key: { + type: 'Identifier', + name: 'prop', + }, + value: { + type: 'Literal', // Not an ObjectExpression + value: 'string value', + }, + }, + ], + }; + + const result = areAllChildrenEmpty(objectWithNonObjectValue as unknown as TSESTree.ObjectExpression); + expect(result).toBe(false); + }); +}); diff --git a/src/css-rules/no-empty-blocks/__tests__/conditionals.test.ts b/src/css-rules/no-empty-blocks/__tests__/conditionals.test.ts new file mode 100644 index 0000000..9dae169 --- /dev/null +++ b/src/css-rules/no-empty-blocks/__tests__/conditionals.test.ts @@ -0,0 +1,58 @@ +import tsParser from '@typescript-eslint/parser'; +import { run } from 'eslint-vitest-rule-tester'; +import noEmptyStyleBlocksRule from '../rule-definition.js'; + +run({ + name: 'vanilla-extract/no-empty-style-blocks/conditional', + rule: noEmptyStyleBlocksRule, + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + }, + valid: [ + // Valid cases with non-empty objects + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style(condition ? { color: 'red' } : { background: 'blue' }); + `, + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style(condition ? { /* comment */ color: 'red' } : { background: 'blue' }); + `, + }, + ], + invalid: [ + // Test empty consequent (first branch) + { + code: `import { style } from '@vanilla-extract/css'; +const myStyle = style(condition ? {} : { background: 'blue' });`, + errors: [{ messageId: 'emptyConditionalStyle' }], + }, + // Test empty alternate (second branch) + { + code: `import { style } from '@vanilla-extract/css'; +const myStyle = style(condition ? { color: 'red' } : {});`, + errors: [{ messageId: 'emptyConditionalStyle' }], + }, + // Test both branches empty - should report the entire declaration + { + code: `import { style } from '@vanilla-extract/css'; +const myStyle = style(condition ? {} : {}); + +`, + errors: [{ messageId: 'emptyStyleDeclaration' }], + }, + // Test nested conditional expressions with empty objects + { + code: `import { style } from '@vanilla-extract/css'; +const myStyle = style(outerCondition ? (innerCondition ? {} : { color: 'blue' }) : {});`, + errors: [{ messageId: 'emptyConditionalStyle' }], + }, + ], +}); diff --git a/src/css-rules/no-empty-blocks/__tests__/globals.test.ts b/src/css-rules/no-empty-blocks/__tests__/globals.test.ts new file mode 100644 index 0000000..d3a7d01 --- /dev/null +++ b/src/css-rules/no-empty-blocks/__tests__/globals.test.ts @@ -0,0 +1,132 @@ +import tsParser from '@typescript-eslint/parser'; +import { run } from 'eslint-vitest-rule-tester'; +import noEmptyStyleBlocksRule from '../rule-definition.js'; + +run({ + name: 'vanilla-extract/no-empty-style-blocks/style', + rule: noEmptyStyleBlocksRule, + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + }, + valid: [ + // Basic non-empty style + ` + import { style } from '@vanilla-extract/css'; + + const myStyle = style({ + color: 'blue', + margin: '10px' + }); + `, + + // Style with comments (not empty) + ` + import { style } from '@vanilla-extract/css'; + + const myStyle = style({ + /* This is a comment */ + color: 'blue' + }); + `, + + // Valid globalStyle with non-empty style object + ` + import { globalStyle } from '@vanilla-extract/css'; + + globalStyle('a', { + color: 'blue' + }); +`, + + // Valid globalFontFace with non-empty style object + ` + import { globalFontFace } from '@vanilla-extract/css'; + + globalFontFace('MyFont', { + src: 'url("/fonts/my-font.woff2")' + }); +`, + + // Valid globalKeyframes with non-empty style object + ` + import { globalKeyframes } from '@vanilla-extract/css'; + + globalKeyframes('fadeIn', { + '0%': { opacity: 0 }, + '100%': { opacity: 1 } + }); +`, + + // Add these to the valid array + // Test for global functions without enough arguments + ` + import { globalStyle } from '@vanilla-extract/css'; + + // Missing second argument (style object) + globalStyle('.selector'); +`, + + // Test for globalFontFace without enough arguments + ` + import { globalFontFace } from '@vanilla-extract/css'; + + // Missing second argument (style object) + globalFontFace('MyFont'); +`, + + // Test for globalKeyframes without enough arguments + ` + import { globalKeyframes } from '@vanilla-extract/css'; + + // Missing second argument (style object) + globalKeyframes('fadeIn'); +`, + ], + invalid: [ + // Empty globalStyle object + { + code: ` + import { globalStyle } from '@vanilla-extract/css'; + + globalStyle('ul', {}); + `, + errors: [{ messageId: 'emptyStyleDeclaration' }], + output: ` + import { globalStyle } from '@vanilla-extract/css'; + + `, + }, + + // Empty globalFontFace object + { + code: ` + import { globalFontFace } from '@vanilla-extract/css'; + + globalFontFace('MyFont', {}); + `, + errors: [{ messageId: 'emptyStyleDeclaration' }], + output: ` + import { globalFontFace } from '@vanilla-extract/css'; + + `, + }, + + // Empty globalKeyframes object + { + code: ` + import { globalKeyframes } from '@vanilla-extract/css'; + + globalKeyframes('fadeIn', {}); + `, + errors: [{ messageId: 'emptyStyleDeclaration' }], + output: ` + import { globalKeyframes } from '@vanilla-extract/css'; + + `, + }, + ], +}); diff --git a/src/css-rules/no-empty-blocks/__tests__/is-effectively-empty-styles-object.test.ts b/src/css-rules/no-empty-blocks/__tests__/is-effectively-empty-styles-object.test.ts new file mode 100644 index 0000000..cc3f07d --- /dev/null +++ b/src/css-rules/no-empty-blocks/__tests__/is-effectively-empty-styles-object.test.ts @@ -0,0 +1,103 @@ +import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils'; +import { describe, expect, it } from 'vitest'; +import { isEffectivelyEmptyStylesObject } from '../empty-style-visitor-creator.js'; + +describe('isEffectivelyEmptyStylesObject', () => { + const createObjectExpression = (properties: TSESTree.Property[]): TSESTree.ObjectExpression => ({ + type: AST_NODE_TYPES.ObjectExpression, + properties, + range: [0, 0], + loc: { + start: { line: 0, column: 0 }, + end: { line: 0, column: 0 }, + }, + parent: null as unknown as TSESTree.Node, + }); + + const createProperty = (key: string, value: TSESTree.Expression): TSESTree.Property => ({ + type: AST_NODE_TYPES.Property, + key: { + type: AST_NODE_TYPES.Identifier, + name: key, + range: [0, 0], + loc: { + start: { line: 0, column: 0 }, + end: { line: 0, column: 0 }, + }, + parent: null as unknown as TSESTree.Property, + decorators: [], + optional: false, + typeAnnotation: undefined, + }, + value, + computed: false, + method: false, + shorthand: false, + kind: 'init', + range: [0, 0], + loc: { + start: { line: 0, column: 0 }, + end: { line: 0, column: 0 }, + }, + parent: null as unknown as TSESTree.ObjectExpression, + optional: false, + }); + + const createLiteral = (value: string): TSESTree.Literal => ({ + type: AST_NODE_TYPES.Literal, + value, + raw: `'${value}'`, + range: [0, 0], + loc: { + start: { line: 0, column: 0 }, + end: { line: 0, column: 0 }, + }, + parent: null as unknown as TSESTree.Node, + }); + + it('should return true for an object with empty selectors, media, or supports objects', () => { + const object = createObjectExpression([ + createProperty('color', createLiteral('blue')), + createProperty('selectors', createObjectExpression([])), + createProperty('@media', createObjectExpression([])), + createProperty('@supports', createObjectExpression([])), + ]); + expect(isEffectivelyEmptyStylesObject(object)).toBe(true); + }); + + it('should return true for an empty object', () => { + const emptyObject = createObjectExpression([]); + expect(isEffectivelyEmptyStylesObject(emptyObject)).toBe(true); + }); + + it('should return false for an object with non-empty properties', () => { + const object = createObjectExpression([createProperty('color', createLiteral('blue'))]); + expect(isEffectivelyEmptyStylesObject(object)).toBe(false); + }); + + it('should return true for an object with only empty nested selectors', () => { + const object = createObjectExpression([ + createProperty( + 'selectors', + createObjectExpression([ + createProperty('&:hover', createObjectExpression([])), + createProperty('&:focus', createObjectExpression([])), + ]), + ), + ]); + expect(isEffectivelyEmptyStylesObject(object)).toBe(true); + }); + + it('should return true for an object with only empty nested media queries', () => { + const object = createObjectExpression([ + createProperty( + '@media', + createObjectExpression([ + createProperty('(min-width: 768px)', createObjectExpression([])), + createProperty('(max-width: 1024px)', createObjectExpression([])), + ]), + ), + ]); + expect(isEffectivelyEmptyStylesObject(object)).toBe(true); + }); +}); diff --git a/src/css-rules/no-empty-blocks/__tests__/nested.test.ts b/src/css-rules/no-empty-blocks/__tests__/nested.test.ts new file mode 100644 index 0000000..502775b --- /dev/null +++ b/src/css-rules/no-empty-blocks/__tests__/nested.test.ts @@ -0,0 +1,266 @@ +import tsParser from '@typescript-eslint/parser'; +import { run } from 'eslint-vitest-rule-tester'; +import noEmptyStyleBlocksRule from '../rule-definition.js'; + +run({ + name: 'vanilla-extract/no-empty-style-blocks/nested', + rule: noEmptyStyleBlocksRule, + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + }, + valid: [ + // Style with non-empty nested selectors + ` + import { style } from '@vanilla-extract/css'; + + const myStyle = style({ + color: 'blue', + selectors: { + '&:hover': { + color: 'red' + } + } + }); + `, + + // Style with non-empty media queries + ` + import { style } from '@vanilla-extract/css'; + + const myStyle = style({ + color: 'blue', + '@media': { + '(min-width: 768px)': { + color: 'red' + } + } + }); + `, + + // Style with computed property name + ` + import { style } from '@vanilla-extract/css'; + + const styleWithComputedProperty = style({ + color: 'blue', + // Using a computed property name + [Symbol('test')]: { + color: 'red' + } + }); + `, + ], + invalid: [ + // Style with empty nested selectors + { + code: ` + import { style } from '@vanilla-extract/css'; + + const styleWithEmptySelector = style({ + color: 'blue', + selectors: { + '&:hover': {} + } + }); + `, + errors: [{ messageId: 'emptySelectors' }], + output: ` + import { style } from '@vanilla-extract/css'; + + const styleWithEmptySelector = style({ + color: 'blue', + + }); + `, + }, + + // Style with empty media queries + { + code: ` + import { style } from '@vanilla-extract/css'; + + const styleWithEmptyMedia = style({ + color: 'blue', + '@media': { + '(min-width: 768px)': {} + } + }); + `, + errors: [{ messageId: 'emptyMedia' }], + output: ` + import { style } from '@vanilla-extract/css'; + + const styleWithEmptyMedia = style({ + color: 'blue', + + }); + `, + }, + + // Style with empty @supports + { + code: ` + import { style } from '@vanilla-extract/css'; + + const styleWithEmptySupports = style({ + display: 'block', + '@supports': { + '(display: grid)': {} + } + }); + `, + errors: [{ messageId: 'emptySupports' }], + output: ` + import { style } from '@vanilla-extract/css'; + + const styleWithEmptySupports = style({ + display: 'block', + + }); + `, + }, + + // Nested empty style with multiple levels + { + code: ` + import { style } from '@vanilla-extract/css'; + + export const nestedEmptyStyle = style({ + selectors: { + '&:hover': {}, + '&:focus': {} + } + }); + `, + errors: [{ messageId: 'emptySelectors' }], + output: ` + import { style } from '@vanilla-extract/css'; + + `, + }, + + // Multiple empty nested styles (individual reporting) + { + code: ` + import { style } from '@vanilla-extract/css'; + + const styleWithMultipleEmptySelectors = style({ + '@media': { + '(min-width: 768px)': {}, + '(max-width: 1024px)': {}, + '(prefers-color-scheme: dark)': { color: 'white' } + } + }); + `, + errors: [{ messageId: 'emptyNestedStyle' }, { messageId: 'emptyNestedStyle' }], + output: ` + import { style } from '@vanilla-extract/css'; + + const styleWithMultipleEmptySelectors = style({ + '@media': { + + + '(prefers-color-scheme: dark)': { color: 'white' } + } + }); + `, + }, + + // Style with custom empty conditional style (not selectors, media, or supports) + { + code: ` + import { style } from '@vanilla-extract/css'; + + const styleWithCustomConditional = style({ + color: 'blue', + '@customCondition': {} + }); + `, + errors: [{ messageId: 'emptyConditionalStyle' }], + output: ` + import { style } from '@vanilla-extract/css'; + + const styleWithCustomConditional = style({ + color: 'blue', + + }); + `, + }, + + // Style with nested empty selectors, media queries, and supports + { + code: ` + import { style } from '@vanilla-extract/css'; + + const styleWithNestedEmpty = style({ + color: 'blue', + selectors: { + '&:hover': {}, + '&:focus': {} + }, + '@media': { + '(min-width: 768px)': {}, + '(max-width: 1024px)': {} + }, + '@supports': { + '(display: grid)': {} + } + }); + `, + errors: [{ messageId: 'emptySelectors' }, { messageId: 'emptyMedia' }, { messageId: 'emptySupports' }], + output: ` + import { style } from '@vanilla-extract/css'; + + const styleWithNestedEmpty = style({ + color: 'blue', + + + + }); + `, + }, + + // Style with mixed empty and non-empty nested objects + { + code: ` + import { style } from '@vanilla-extract/css'; + + const styleWithMixedNested = style({ + color: 'blue', + selectors: { + '&:hover': { color: 'red' }, + '&:focus': {} + }, + '@media': { + '(min-width: 768px)': {}, + '(max-width: 1024px)': { fontSize: '16px' } + }, + '@supports': { + '(display: grid)': {} + } + }); + `, + errors: [{ messageId: 'emptyNestedStyle' }, { messageId: 'emptyNestedStyle' }, { messageId: 'emptySupports' }], + output: ` + import { style } from '@vanilla-extract/css'; + + const styleWithMixedNested = style({ + color: 'blue', + selectors: { + '&:hover': { color: 'red' }, + + }, + '@media': { + + '(max-width: 1024px)': { fontSize: '16px' } + }, + + }); + `, + }, + ], +}); diff --git a/src/css-rules/no-empty-blocks/__tests__/recipe.test.ts b/src/css-rules/no-empty-blocks/__tests__/recipe.test.ts new file mode 100644 index 0000000..3b5020d --- /dev/null +++ b/src/css-rules/no-empty-blocks/__tests__/recipe.test.ts @@ -0,0 +1,284 @@ +import tsParser from '@typescript-eslint/parser'; +import { run } from 'eslint-vitest-rule-tester'; +import noEmptyStyleBlocksRule from '../rule-definition.js'; + +run({ + name: 'vanilla-extract/no-empty-style-blocks/recipe', + rule: noEmptyStyleBlocksRule, + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + }, + valid: [ + // Recipe with non-empty variants + ` + import { recipe } from '@vanilla-extract/recipes'; + + const myRecipe = recipe({ + base: { + color: 'black' + }, + variants: { + color: { + blue: { color: 'blue' }, + red: { color: 'red' } + } + } + }); + `, + + // Tests the early return when property.type !== 'Property' + // Valid because the spread operator is skipped by the rule (early return) + // This covers the code path: if (property.type !== 'Property') { return; } + ` + import { recipe } from '@vanilla-extract/recipes'; + + const recipeWithSpreadProperty = recipe({ + ...{ someProperty: true }, + base: { color: 'black' } + }); + `, + + // Tests the early return when property name cannot be determined + // Valid because computed properties are skipped by the rule (early return) + // This covers the code path: if (!propertyName) { return; } + ` + import { recipe } from '@vanilla-extract/recipes'; + + function computedKey() { return 'dynamicKey'; } + + const recipeWithComputedProperty = recipe({ + [computedKey()]: { color: 'black' }, + base: { color: 'black' } + }); + `, + + // Tests the early return when variantCategoryProperty is not a Property or its value is not an ObjectExpression + // Valid because non-Property variant categories or non-ObjectExpression values are skipped by the rule + // This covers the code path: if (variantCategoryProperty.type !== 'Property' || variantCategoryProperty.value.type !== 'ObjectExpression') { return; } + ` + import { recipe } from '@vanilla-extract/recipes'; + + const recipeWithNonPropertyVariantCategory = recipe({ + base: { color: 'black' }, + variants: { + ...{ size: { small: { fontSize: '12px' } } }, + color: { + blue: { color: 'blue' } + } + } + }); +`, + + // Tests the early return when variantValueProperty is not a Property + // Valid because non-Property variant values are skipped by the rule + // This covers the code path: if (variantValueProperty.type !== 'Property') { return; } + ` + import { recipe } from '@vanilla-extract/recipes'; + + const recipeWithNonPropertyVariantValue = recipe({ + base: { color: 'black' }, + variants: { + color: { + ...{ blue: { color: 'blue' } }, + red: { color: 'red' } + } + } + }); +`, + ], + invalid: [ + // Empty recipe + { + code: ` + import { recipe } from '@vanilla-extract/recipes'; + + export const emptyRecipe = recipe({}); + `, + errors: [{ messageId: 'emptyStyleDeclaration' }], + output: ` + import { recipe } from '@vanilla-extract/recipes'; + + `, + }, + + // Recipe with empty base + { + code: ` + import { recipe } from '@vanilla-extract/recipes'; + + const recipeWithEmptyBase = recipe({ + base: {}, + variants: { + color: { + blue: { color: 'blue' } + } + } + }); + `, + errors: [{ messageId: 'emptyRecipeProperty' }], + output: ` + import { recipe } from '@vanilla-extract/recipes'; + + const recipeWithEmptyBase = recipe({ + + variants: { + color: { + blue: { color: 'blue' } + } + } + }); + `, + }, + + // Recipe with empty variant values + { + code: ` + import { recipe } from '@vanilla-extract/recipes'; + + const recipeWithEmptyVariantValues = recipe({ + base: { color: 'black' }, + variants: { + color: { + blue: {}, + red: {} + } + } + }); + `, + errors: [{ messageId: 'emptyVariantCategory' }], + output: ` + import { recipe } from '@vanilla-extract/recipes'; + + const recipeWithEmptyVariantValues = recipe({ + base: { color: 'black' }, + + }); + `, + }, + + // Recipe with both empty base and variants + { + code: ` + import { recipe } from '@vanilla-extract/recipes'; + + export const recipeWithBothEmpty = recipe({ + base: {}, + variants: {} + }); + `, + errors: [{ messageId: 'emptyStyleDeclaration' }], + output: ` + import { recipe } from '@vanilla-extract/recipes'; + + `, + }, + + // Test for non-object property values in recipe + // + { + code: ` + import { recipe } from '@vanilla-extract/recipes'; + + const recipeWithNonObjectValue = recipe({ + base: { color: 'black' }, + variants: { + color: { + blue: "blue", // String instead of object + red: { color: 'red' } + } + } + }); + `, + errors: [{ messageId: 'invalidPropertyType' }], + }, + + // Tests the Identifier type handling in variant values + // This covers the code path where friendlyType === 'Identifier' + { + code: ` + import { recipe } from '@vanilla-extract/recipes'; + + const colorValue = { color: 'blue' }; + + const recipeWithIdentifierValue = recipe({ + base: { color: 'black' }, + variants: { + color: { + blue: colorValue // Using a variable (Identifier) instead of an object literal + } + } + }); + `, + errors: [{ messageId: 'invalidPropertyType', data: { type: 'variable' } }], + }, + + // Test for handling null literal values in variant properties + { + code: ` + import { recipe } from '@vanilla-extract/recipes'; + + const recipeWithNullVariantValue = recipe({ + base: { color: 'black' }, + variants: { + color: { + blue: null, + red: { color: 'red' } + } + } + }); + `, + errors: [{ messageId: 'invalidPropertyType', data: { type: 'null' } }], + }, + + // This test ensures the rule correctly identifies and reports empty object values in variants + { + code: ` + import { recipe } from '@vanilla-extract/recipes'; + + const recipeWithEmptyVariantValue = recipe({ + base: { color: 'black' }, + variants: { + color: { + blue: {}, // Empty object should be reported + red: { color: 'red' } // Non-empty to prevent category-level reporting + } + } + }); + + const anotherRecipe = recipe({ + base: { color: 'black' }, + variants: { + color: { + blue: {}, // Another empty object that should be reported + red: { color: 'red' } + } + } + }); + `, + errors: [{ messageId: 'emptyVariantValue' }, { messageId: 'emptyVariantValue' }], + }, + + // Tests the default case for node type handling in variant values + // This covers the code path where neither Literal nor Identifier conditions are met + { + code: ` + import { recipe } from '@vanilla-extract/recipes'; + + const recipeWithArrowFunctionValue = recipe({ + base: { color: 'black' }, + variants: { + color: { + blue: () => ({ color: 'blue' }), // Arrow function instead of an object literal + red: { color: 'red' } + } + } + }); +`, + errors: [{ messageId: 'invalidPropertyType', data: { type: 'ArrowFunctionExpression' } }], + }, + ], +}); diff --git a/src/css-rules/no-empty-blocks/__tests__/spread.test.ts b/src/css-rules/no-empty-blocks/__tests__/spread.test.ts new file mode 100644 index 0000000..bc08568 --- /dev/null +++ b/src/css-rules/no-empty-blocks/__tests__/spread.test.ts @@ -0,0 +1,48 @@ +import tsParser from '@typescript-eslint/parser'; +import { run } from 'eslint-vitest-rule-tester'; +import noEmptyStyleBlocksRule from '../rule-definition.js'; + +run({ + name: 'vanilla-extract/no-empty-style-blocks/spread', + rule: noEmptyStyleBlocksRule, + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + }, + valid: [], + invalid: [ + // Style with empty spread object + { + code: ` + import { style } from '@vanilla-extract/css'; + + const baseStyles = { + color: 'blue', + margin: '10px' + }; + + const spreadStyle = style({ + ...baseStyles, + ...{} + }); + `, + errors: [{ messageId: 'emptySpreadObject' }], + output: ` + import { style } from '@vanilla-extract/css'; + + const baseStyles = { + color: 'blue', + margin: '10px' + }; + + const spreadStyle = style({ + ...baseStyles, + + }); + `, + }, + ], +}); diff --git a/src/css-rules/no-empty-blocks/__tests__/style-variants.test.ts b/src/css-rules/no-empty-blocks/__tests__/style-variants.test.ts new file mode 100644 index 0000000..95f4e30 --- /dev/null +++ b/src/css-rules/no-empty-blocks/__tests__/style-variants.test.ts @@ -0,0 +1,220 @@ +import tsParser from '@typescript-eslint/parser'; +import { run } from 'eslint-vitest-rule-tester'; +import { expect } from 'vitest'; +import noEmptyStyleBlocksRule from '../rule-definition.js'; + +// The output function approach with assertions is used instead of exact string comparison because ESLint's autofix functionality modifies whitespace in ways that are difficult to predict exactly, and ESLint v9 has stricter RuleTester checks that require output to match character-for-character. This approach allows for more flexible assertions about the important parts of the output without requiring exact whitespace matching, making tests more resilient to small changes in whitespace handling. + +run({ + name: 'vanilla-extract/no-empty-style-blocks/style-variants', + rule: noEmptyStyleBlocksRule, + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + }, + valid: [ + // In the valid array: + { + code: ` + import { styleVariants } from '@vanilla-extract/css'; + + const styles = { color: 'blue' }; + export const variantsWithSpread = styleVariants({ + ...styles, + primary: { color: 'red' } + }); + `, + }, + + // Valid style variants with non-empty objects + { + code: ` + import { styleVariants } from '@vanilla-extract/css'; + + export const validVariants = styleVariants({ + primary: { color: 'blue' }, + secondary: { color: 'green' } + }); + `, + }, + // Valid style variants with arrays containing values + { + code: ` + import { styleVariants } from '@vanilla-extract/css'; + + export const validArrayVariants = styleVariants({ + primary: [{ color: 'blue' }, { fontWeight: 'bold' }], + secondary: [{ color: 'green' }] + }); + `, + }, + // Mixed valid variants + { + code: ` + import { styleVariants } from '@vanilla-extract/css'; + + export const mixedVariants = styleVariants({ + primary: { color: 'blue' }, + secondary: [{ color: 'green' }, { fontWeight: 'bold' }] + }); + `, + }, + ], + invalid: [ + // Empty style variants object + { + code: ` + import { styleVariants } from '@vanilla-extract/css'; + + export const emptyVariants = styleVariants({}); + `, + errors: [{ messageId: 'emptyStyleDeclaration' }], + output(output) { + expect(output).toContain('import { styleVariants } from'); + expect(output).not.toContain('export const emptyVariants'); + }, + }, + // Style variants with empty object property + { + code: ` + import { styleVariants } from '@vanilla-extract/css'; + + export const variantsWithEmptyObject = styleVariants({ + primary: {}, + secondary: { color: 'green' } + }); + `, + errors: [{ messageId: 'emptyVariantValue' }], + output(output) { + expect(output).toContain("secondary: { color: 'green' }"); + expect(output).not.toContain('primary: {}'); + }, + }, + // Style variants with empty array property + { + code: ` + import { styleVariants } from '@vanilla-extract/css'; + + export const variantsWithEmptyArray = styleVariants({ + primary: [], + secondary: { color: 'green' } + }); + `, + errors: [{ messageId: 'emptyVariantValue' }], + output(output) { + expect(output).toContain("secondary: { color: 'green' }"); + expect(output).not.toContain('primary: []'); + }, + }, + // Style variants with multiple empty properties + { + code: ` + import { styleVariants } from '@vanilla-extract/css'; + + export const variantsWithMultipleEmptyProps = styleVariants({ + primary: {}, + secondary: [], + tertiary: { color: 'green' } + }); + `, + errors: [{ messageId: 'emptyVariantValue' }, { messageId: 'emptyVariantValue' }], + output(output) { + expect(output).toContain("tertiary: { color: 'green' }"); + expect(output).not.toContain('primary: {}'); + expect(output).not.toContain('secondary: []'); + }, + }, + // Style variants with all empty properties + { + code: ` + import { styleVariants } from '@vanilla-extract/css'; + + export const allEmptyVariants = styleVariants({ + primary: {}, + secondary: [] + }); + `, + errors: [{ messageId: 'emptyVariantValue' }, { messageId: 'emptyVariantValue' }], + output(output) { + expect(output).toContain('import { styleVariants } from'); + expect(output).not.toContain('export const allEmptyVariants'); + expect(output).not.toContain('primary: {}'); + expect(output).not.toContain('secondary: []'); + }, + }, + + // Style variants with trailing comma + { + code: ` + import { styleVariants } from '@vanilla-extract/css'; + + export const variantsWithTrailingComma = styleVariants({ + primary: {}, + secondary: { color: 'green' }, + }); + `, + errors: [{ messageId: 'emptyVariantValue' }], + output(output) { + expect(output).toContain("secondary: { color: 'green' },"); + expect(output).not.toContain('primary: {}'); + }, + }, + // First empty property + { + code: ` + import { styleVariants } from '@vanilla-extract/css'; + + export const firstEmptyProperty = styleVariants({ + primary: {}, + secondary: { color: 'green' }, + tertiary: { color: 'blue' } + }); + `, + errors: [{ messageId: 'emptyVariantValue' }], + output(output) { + expect(output).toContain("secondary: { color: 'green' }"); + expect(output).toContain("tertiary: { color: 'blue' }"); + expect(output).not.toContain('primary: {}'); + }, + }, + // Middle empty property + { + code: ` + import { styleVariants } from '@vanilla-extract/css'; + + export const middleEmptyProperty = styleVariants({ + primary: { color: 'red' }, + secondary: {}, + tertiary: { color: 'blue' } + }); + `, + errors: [{ messageId: 'emptyVariantValue' }], + output(output) { + expect(output).toContain("primary: { color: 'red' }"); + expect(output).toContain("tertiary: { color: 'blue' }"); + expect(output).not.toContain('secondary: {}'); + }, + }, + // Last empty property + { + code: ` + import { styleVariants } from '@vanilla-extract/css'; + + export const lastEmptyProperty = styleVariants({ + primary: { color: 'red' }, + secondary: { color: 'green' }, + tertiary: {} + }); + `, + errors: [{ messageId: 'emptyVariantValue' }], + output(output) { + expect(output).toContain("primary: { color: 'red' }"); + expect(output).toContain("secondary: { color: 'green' }"); + expect(output).not.toContain('tertiary: {}'); + }, + }, + ], +}); diff --git a/src/css-rules/no-empty-blocks/__tests__/style.test.ts b/src/css-rules/no-empty-blocks/__tests__/style.test.ts new file mode 100644 index 0000000..d88c019 --- /dev/null +++ b/src/css-rules/no-empty-blocks/__tests__/style.test.ts @@ -0,0 +1,138 @@ +import tsParser from '@typescript-eslint/parser'; +import { run } from 'eslint-vitest-rule-tester'; +import noEmptyStyleBlocksRule from '../rule-definition.js'; + +run({ + name: 'vanilla-extract/no-empty-style-blocks/style', + rule: noEmptyStyleBlocksRule, + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + }, + valid: [ + // Basic non-empty style + ` + import { style } from '@vanilla-extract/css'; + + const myStyle = style({ + color: 'blue', + margin: '10px' + }); + `, + + // Style with comments (not empty) + ` + import { style } from '@vanilla-extract/css'; + + const myStyle = style({ + /* This is a comment */ + color: 'blue' + }); + `, + ], + invalid: [ + // Empty style object + { + code: ` + import { style } from '@vanilla-extract/css'; + + const emptyStyle = style({}); + `, + errors: [{ messageId: 'emptyStyleDeclaration' }], + output: ` + import { style } from '@vanilla-extract/css'; + + `, + }, + + // Empty exported style object + { + code: ` + import { style } from '@vanilla-extract/css'; + + export const emptyStyle = style({}); + `, + errors: [{ messageId: 'emptyStyleDeclaration' }], + output: ` + import { style } from '@vanilla-extract/css'; + + `, + }, + + // Style with comments in empty object + { + code: ` + import { style } from '@vanilla-extract/css'; + + const styleWithComments = style({ + /* This is an empty style */ + }); + `, + errors: [{ messageId: 'emptyStyleDeclaration' }], + output: ` + import { style } from '@vanilla-extract/css'; + + `, + }, + + // Multiple empty styles + { + code: ` + import { style } from '@vanilla-extract/css'; + + export const emptyStyle1 = style({}); + export const emptyStyle2 = style({}); + `, + errors: [{ messageId: 'emptyStyleDeclaration' }, { messageId: 'emptyStyleDeclaration' }], + output: ` + import { style } from '@vanilla-extract/css'; + + `, + }, + + // Variable declaration with empty style + { + code: ` + import { style } from '@vanilla-extract/css'; + + const { className } = style({}); + `, + errors: [{ messageId: 'emptyStyleDeclaration' }], + output: ` + import { style } from '@vanilla-extract/css'; + + `, + }, + + // Export of variable with empty style + { + code: ` + import { style } from '@vanilla-extract/css'; + + const myEmptyStyle = style({}); + export { myEmptyStyle }; + `, + errors: [{ messageId: 'emptyStyleDeclaration' }], + output: ` + import { style } from '@vanilla-extract/css'; + + export { myEmptyStyle }; + `, + }, + + // Style in a callback or nested function + { + code: ` + import { style } from '@vanilla-extract/css'; + + [1, 2, 3].forEach(() => { + style({}); + }); + `, + errors: [{ messageId: 'emptyStyleDeclaration' }], + }, + ], +}); diff --git a/src/css-rules/no-empty-blocks/conditional-processor.ts b/src/css-rules/no-empty-blocks/conditional-processor.ts new file mode 100644 index 0000000..3996813 --- /dev/null +++ b/src/css-rules/no-empty-blocks/conditional-processor.ts @@ -0,0 +1,36 @@ +import type { Rule } from 'eslint'; +import { TSESTree } from '@typescript-eslint/utils'; +import { isEmptyObject } from '../shared-utils/empty-object-processor.js'; +import { reportEmptyDeclaration } from './fix-utils.js'; + +/** + * Handles conditional expressions with empty objects. + */ +export function processConditionalExpression( + context: Rule.RuleContext, + node: TSESTree.ConditionalExpression, + reportedNodes: Set, + callNode: TSESTree.CallExpression, +): void { + const isConsequentEmpty = node.consequent.type === 'ObjectExpression' && isEmptyObject(node.consequent); + const isAlternateEmpty = node.alternate.type === 'ObjectExpression' && isEmptyObject(node.alternate); + + // If both branches are empty, report the entire declaration for removal + if (isConsequentEmpty && isAlternateEmpty) { + reportedNodes.add(node); + reportEmptyDeclaration(context, node, callNode); + return; + } + + // Otherwise, handle individual empty branches + if (isConsequentEmpty || isAlternateEmpty) { + const emptyNode = isConsequentEmpty ? node.consequent : node.alternate; + reportedNodes.add(emptyNode); + + // No fix provided, just flagging the issue + context.report({ + node: emptyNode as Rule.Node, + messageId: 'emptyConditionalStyle', + }); + } +} diff --git a/src/css-rules/no-empty-blocks/empty-nested-style-processor.ts b/src/css-rules/no-empty-blocks/empty-nested-style-processor.ts new file mode 100644 index 0000000..9536e7f --- /dev/null +++ b/src/css-rules/no-empty-blocks/empty-nested-style-processor.ts @@ -0,0 +1,75 @@ +import type { Rule } from 'eslint'; +import { TSESTree } from '@typescript-eslint/utils'; +import { isEmptyObject } from '../shared-utils/empty-object-processor.js'; +import { removeNodeWithComma } from './node-remover.js'; +import { areAllChildrenEmpty, getStyleKeyName } from './property-utils.js'; + +/** + * Processes nested style objects like selectors and media queries. + */ +export function processEmptyNestedStyles( + ruleContext: Rule.RuleContext, + node: TSESTree.ObjectExpression, + reportedNodes: Set, +): void { + node.properties.forEach((property) => { + if (property.type !== 'Property') { + return; + } + + const propertyName = getStyleKeyName(property.key); + if (!propertyName) { + return; + } + + // Handle selectors, media queries, and supports + if ((propertyName === 'selectors' || propertyName.startsWith('@')) && property.value.type === 'ObjectExpression') { + // If the container is empty or all its children are empty, remove the entire property + if (isEmptyObject(property.value) || areAllChildrenEmpty(property.value)) { + if (!reportedNodes.has(property)) { + reportedNodes.add(property); + const messageId = + propertyName === 'selectors' + ? 'emptySelectors' + : propertyName === '@media' + ? 'emptyMedia' + : propertyName === '@supports' + ? 'emptySupports' + : 'emptyConditionalStyle'; + + ruleContext.report({ + node: property as Rule.Node, + messageId, + fix(fixer) { + return removeNodeWithComma(ruleContext, property, fixer); + }, + }); + } + return; + } + + // Process individual selectors or media queries if we're not removing the entire container + property.value.properties.forEach((nestedProperty) => { + if (nestedProperty.type === 'Property') { + if (nestedProperty.value.type === 'ObjectExpression') { + if (isEmptyObject(nestedProperty.value)) { + if (!reportedNodes.has(nestedProperty)) { + reportedNodes.add(nestedProperty); + ruleContext.report({ + node: nestedProperty as Rule.Node, + messageId: 'emptyNestedStyle', + fix(fixer) { + return removeNodeWithComma(ruleContext, nestedProperty, fixer); + }, + }); + } + } else { + // Recursively process nested styles (for deeply nested selectors/media) + processEmptyNestedStyles(ruleContext, nestedProperty.value, reportedNodes); + } + } + } + }); + } + }); +} diff --git a/src/css-rules/no-empty-blocks/empty-style-visitor-creator.ts b/src/css-rules/no-empty-blocks/empty-style-visitor-creator.ts new file mode 100644 index 0000000..ff9820f --- /dev/null +++ b/src/css-rules/no-empty-blocks/empty-style-visitor-creator.ts @@ -0,0 +1,228 @@ +import type { Rule } from 'eslint'; +import { TSESTree } from '@typescript-eslint/utils'; +import { isEmptyObject } from '../shared-utils/empty-object-processor.js'; +import { processConditionalExpression } from './conditional-processor.js'; +import { processEmptyNestedStyles } from './empty-nested-style-processor.js'; +import { reportEmptyDeclaration } from './fix-utils.js'; +import { removeNodeWithComma } from './node-remover.js'; +import { getStyleKeyName } from './property-utils.js'; +import { processRecipeProperties } from './recipe-processor.js'; +import { processStyleVariants } from './style-variants-processor.js'; + +/** + * Creates ESLint rule visitors for detecting empty style blocks in vanilla-extract. + * @param ruleContext The ESLint rule rule context. + * @returns An object with visitor functions for the ESLint rule. + */ +export const createEmptyStyleVisitors = (ruleContext: Rule.RuleContext): Rule.RuleListener => { + // Track reported nodes to prevent duplicate reports + const reportedNodes = new Set(); + + return { + CallExpression(node) { + if (node.callee.type !== 'Identifier') { + return; + } + + // Target vanilla-extract style functions + const styleApiFunctions = [ + 'style', + 'styleVariants', + 'recipe', + 'globalStyle', + 'fontFace', + 'globalFontFace', + 'keyframes', + 'globalKeyframes', + ]; + + if (!styleApiFunctions.includes(node.callee.name) || node.arguments.length === 0) { + return; + } + + // Handle styleVariants specifically + if (node.callee.name === 'styleVariants' && node.arguments[0]?.type === 'ObjectExpression') { + processStyleVariants(ruleContext, node.arguments[0] as TSESTree.ObjectExpression, reportedNodes); + + // If the entire styleVariants object is empty after processing, remove the declaration + if (isEmptyObject(node.arguments[0] as TSESTree.ObjectExpression)) { + reportEmptyDeclaration(ruleContext, node.arguments[0] as TSESTree.Node, node as TSESTree.CallExpression); + } + return; + } + + const defaultStyleArgumentIndex = 0; + const globalFunctionNames = ['globalStyle', 'globalFontFace', 'globalKeyframes']; + // Determine the style argument index based on the function name + const styleArgumentIndex = globalFunctionNames.includes(node.callee.name) ? 1 : defaultStyleArgumentIndex; + + // For global functions, check if we have enough arguments + if (styleArgumentIndex === 1 && node.arguments.length <= styleArgumentIndex) { + return; + } + + const styleArgument = node.arguments[styleArgumentIndex]; + + // This defensive check prevents duplicate processing of nodes. + // This code path's difficult to test because the ESLint visitor pattern + // typically ensures each node is only visited once per rule execution. + if (reportedNodes.has(styleArgument as TSESTree.Node)) { + return; + } + + // Handle conditional expressions + if (styleArgument?.type === 'ConditionalExpression') { + processConditionalExpression( + ruleContext, + styleArgument as TSESTree.ConditionalExpression, + reportedNodes, + node as TSESTree.CallExpression, + ); + return; + } + + // Direct empty object case - remove the entire declaration + if (styleArgument?.type === 'ObjectExpression' && isEmptyObject(styleArgument as TSESTree.ObjectExpression)) { + reportedNodes.add(styleArgument as TSESTree.ObjectExpression); + reportEmptyDeclaration(ruleContext, styleArgument as TSESTree.Node, node as TSESTree.CallExpression); + return; + } + + // For recipe - check if entire recipe is effectively empty + if (node.callee.name === 'recipe' && styleArgument?.type === 'ObjectExpression') { + if (isEffectivelyEmptyStylesObject(styleArgument as TSESTree.ObjectExpression)) { + reportedNodes.add(styleArgument as TSESTree.ObjectExpression); + reportEmptyDeclaration(ruleContext, styleArgument as TSESTree.Node, node as TSESTree.CallExpression); + return; + } + + // Process individual properties in recipe + processRecipeProperties(ruleContext, styleArgument as TSESTree.ObjectExpression, reportedNodes); + } + + // For style objects with nested empty objects + if (styleArgument?.type === 'ObjectExpression') { + // Check for spread elements + styleArgument.properties.forEach((property) => { + if ( + property.type === 'SpreadElement' && + property.argument.type === 'ObjectExpression' && + isEmptyObject(property.argument as TSESTree.ObjectExpression) + ) { + reportedNodes.add(property.argument as TSESTree.Node); + ruleContext.report({ + node: property.argument as Rule.Node, + messageId: 'emptySpreadObject', + fix(fixer) { + return removeNodeWithComma(ruleContext, property as TSESTree.Node, fixer); + }, + }); + } + }); + + // Process nested selectors and media queries + processEmptyNestedStyles(ruleContext, styleArgument as TSESTree.ObjectExpression, reportedNodes); + } + }, + }; +}; + +/** + * Checks if a style object is effectively empty (contains only empty objects). + */ +export function isEffectivelyEmptyStylesObject(stylesObject: TSESTree.ObjectExpression): boolean { + // Empty object itself + if (stylesObject.properties.length === 0) { + return true; + } + + // For recipe objects, we need special handling + let hasBaseProperty = false; + let isBaseEmpty = true; + let hasVariantsProperty = false; + let areAllVariantsEmpty = true; + + // First pass: identify recipe properties + for (const property of stylesObject.properties) { + if (property.type !== 'Property') { + continue; + } + + const propertyName = getStyleKeyName(property.key); + if (!propertyName) { + continue; + } + + if (propertyName === 'base') { + hasBaseProperty = true; + if (property.value.type === 'ObjectExpression' && !isEmptyObject(property.value)) { + isBaseEmpty = false; + } + } else if (propertyName === 'variants') { + hasVariantsProperty = true; + if (property.value.type === 'ObjectExpression' && !isEmptyObject(property.value)) { + areAllVariantsEmpty = false; + } + } + } + + // If this looks like a recipe object (has base or variants) + if (hasBaseProperty || hasVariantsProperty) { + // A recipe is effectively empty if both base and variants are empty + return isBaseEmpty && areAllVariantsEmpty; + } + + // / For non-recipe objects, check if all special properties (selectors, media queries, variants) are effectively empty + function isSpecialProperty(propertyName: string | null): boolean { + return ( + propertyName === 'selectors' || (propertyName && propertyName.startsWith('@')) || propertyName === 'variants' + ); + } + + const specialProperties = stylesObject.properties.filter( + (prop): prop is TSESTree.Property => prop.type === 'Property' && isSpecialProperty(getStyleKeyName(prop.key)), + ); + + const allSpecialPropertiesEmpty = specialProperties.every((property) => { + if (property.value.type === 'ObjectExpression' && isEmptyObject(property.value)) { + return true; + } + + const propertyName = getStyleKeyName(property.key); + // This defensive check handles malformed AST nodes that lack valid property names. + // This is difficult to test because it's challenging to construct a valid AST + // where getStyleKeyName would return a falsy value. + if (!propertyName) { + return false; + } + + // For selectors, media queries and supports, check if all nested objects are empty + if ( + (propertyName === 'selectors' || (propertyName && propertyName.startsWith('@'))) && + property.value.type === 'ObjectExpression' + ) { + // This handles the edge case of an empty properties array. + // This code path is difficult to test in isolation because it requires + // constructing a specific AST structure that bypasses earlier conditions. + if (property.value.properties.length === 0) { + return true; + } + + return property.value.properties.every((nestedProperty) => { + return ( + nestedProperty.type === 'Property' && + nestedProperty.value.type === 'ObjectExpression' && + isEmptyObject(nestedProperty.value) + ); + }); + } + + // Default fallback for cases not handled by the conditions above. + // This is difficult to test because it requires creating an AST structure + // that doesn't trigger any of the preceding return statements. + return false; + }); + + // If we have special properties and they're all empty, the style is effectively empty + return specialProperties.length > 0 && allSpecialPropertiesEmpty; +} diff --git a/src/css-rules/no-empty-blocks/fix-utils.ts b/src/css-rules/no-empty-blocks/fix-utils.ts new file mode 100644 index 0000000..c8330b2 --- /dev/null +++ b/src/css-rules/no-empty-blocks/fix-utils.ts @@ -0,0 +1,94 @@ +import type { Rule } from 'eslint'; +import { TSESTree } from '@typescript-eslint/utils'; + +/** + * Finds the target declaration node for a given call expression. + * + * Traverses the AST upwards from the `callNode` to find the appropriate + * declaration node. For global APIs, it targets the expression statement. + * For non-global APIs, it looks for variable declarations or export + * named declarations. + * + * @param callNode - The call expression node to start from. + * @param isGlobalApi - A flag indicating whether the API is global. + * @returns The target declaration node or null if not found. + */ + +export const findTargetDeclarationNode = ( + callNode: TSESTree.CallExpression, + isGlobalApi: boolean, +): TSESTree.Node | null => { + let current: TSESTree.Node = callNode; + + while (current.parent) { + current = current.parent; + + // For global APIs, we only need to check for ExpressionStatement + if (isGlobalApi && current.type === 'ExpressionStatement') { + return current; + } + + // For non-global APIs, check for variable declarations and exports + if (!isGlobalApi) { + if (current.type === 'VariableDeclarator' && current.parent && current.parent.type === 'VariableDeclaration') { + // If this is part of an export, get the export declaration + if (current.parent.parent && current.parent.parent.type === 'ExportNamedDeclaration') { + return current.parent.parent; + } + return current.parent; + } + } + } + + return null; +}; + +/** + * Reports an issue for an empty style declaration, and provides a fix to remove the declaration. + * @param ruleContext The ESLint rule context. + * @param node The node to report the issue on. + * @param callNode The CallExpression node of the style() function. + */ +export const reportEmptyDeclaration = ( + ruleContext: Rule.RuleContext, + node: TSESTree.Node, + callNode: TSESTree.CallExpression, +): void => { + // Check if this is a global API function + const isGlobalApi = + callNode.callee.type === 'Identifier' && + ['globalStyle', 'globalFontFace', 'globalKeyframes'].includes(callNode.callee.name); + + // Find the parent declaration node + const targetDeclarationNode = findTargetDeclarationNode(callNode, isGlobalApi); + + if (targetDeclarationNode) { + ruleContext.report({ + node: node as Rule.Node, + messageId: 'emptyStyleDeclaration', + fix(fixer) { + const sourceCode = ruleContext.sourceCode; + const startLine = sourceCode.getLocFromIndex(targetDeclarationNode.range[0]).line; + const lineStart = sourceCode.getIndexFromLoc({ + line: startLine, + column: 0, + }); + + // Find next line after the declaration + const endLine = sourceCode.getLocFromIndex(targetDeclarationNode.range[1]).line; + const nextLineStart = sourceCode.getIndexFromLoc({ + line: endLine + 1, + column: 0, + }); + + return fixer.removeRange([lineStart, nextLineStart]); + }, + }); + } else { + // Report the issue without providing a fix + ruleContext.report({ + node: node as Rule.Node, + messageId: 'emptyStyleDeclaration', + }); + } +}; diff --git a/src/css-rules/no-empty-blocks/index.ts b/src/css-rules/no-empty-blocks/index.ts new file mode 100644 index 0000000..3cda9b9 --- /dev/null +++ b/src/css-rules/no-empty-blocks/index.ts @@ -0,0 +1,3 @@ +import noEmptyStyleBlocksRule from './rule-definition.js'; + +export default noEmptyStyleBlocksRule; diff --git a/src/css-rules/no-empty-blocks/node-remover.ts b/src/css-rules/no-empty-blocks/node-remover.ts new file mode 100644 index 0000000..28971e2 --- /dev/null +++ b/src/css-rules/no-empty-blocks/node-remover.ts @@ -0,0 +1,18 @@ +import type { Rule } from 'eslint'; +import type { TSESTree } from '@typescript-eslint/utils'; + +/** + * Removes the given node and also removes a trailing comma if it exists. + * @param ruleContext The ESLint rule context. + * @param node The node to remove. + * @param fixer The ESLint fixer. + * @returns The fix object. + */ +export function removeNodeWithComma(ruleContext: Rule.RuleContext, node: TSESTree.Node, fixer: Rule.RuleFixer) { + const sourceCode = ruleContext.sourceCode; + const tokenAfter = sourceCode.getTokenAfter(node as Rule.Node); + if (tokenAfter && tokenAfter.value === ',' && node.range && tokenAfter.range) { + return fixer.removeRange([node.range[0], tokenAfter.range[1]]); + } + return fixer.remove(node as Rule.Node); +} diff --git a/src/css-rules/no-empty-blocks/property-utils.ts b/src/css-rules/no-empty-blocks/property-utils.ts new file mode 100644 index 0000000..720a1bf --- /dev/null +++ b/src/css-rules/no-empty-blocks/property-utils.ts @@ -0,0 +1,31 @@ +import { TSESTree } from '@typescript-eslint/utils'; +import { isEmptyObject } from '../shared-utils/empty-object-processor.js'; + +/** + * Gets the property name regardless of whether it's an identifier or a literal. + */ +export function getStyleKeyName(key: TSESTree.Expression | TSESTree.PrivateIdentifier): string | null { + if (key.type === 'Identifier') { + return key.name; + } + if (key.type === 'Literal' && typeof key.value === 'string') { + return key.value; + } + return null; +} + +/** + * Checks if all properties in a style object are empty objects. + */ +export const areAllChildrenEmpty = (stylesObject: TSESTree.ObjectExpression): boolean => { + if (stylesObject.properties.length === 0) { + return true; + } + + return stylesObject.properties.every((property) => { + if (property.type !== 'Property' || property.value.type !== 'ObjectExpression') { + return false; + } + return isEmptyObject(property.value); + }); +}; diff --git a/src/css-rules/no-empty-blocks/recipe-processor.ts b/src/css-rules/no-empty-blocks/recipe-processor.ts new file mode 100644 index 0000000..e593da6 --- /dev/null +++ b/src/css-rules/no-empty-blocks/recipe-processor.ts @@ -0,0 +1,142 @@ +import type { Rule } from 'eslint'; +import { TSESTree } from '@typescript-eslint/utils'; +import { isEmptyObject } from '../shared-utils/empty-object-processor.js'; +import { processEmptyNestedStyles } from './empty-nested-style-processor.js'; +import { removeNodeWithComma } from './node-remover.js'; +import { areAllChildrenEmpty, getStyleKeyName } from './property-utils.js'; + +/** + * Processes a recipe object, removing empty `base` and `variants` properties, as well as empty + * variant categories and values. + * + * @param ruleContext The ESLint rule context. + * @param recipeNode The recipe object node to process. + * @param reportedNodes A set of nodes that have already been reported by other processors. + */ +export function processRecipeProperties( + ruleContext: Rule.RuleContext, + recipeNode: TSESTree.ObjectExpression, + reportedNodes: Set, +): void { + recipeNode.properties.forEach((property) => { + if (property.type !== 'Property') { + return; + } + + const propertyName = getStyleKeyName(property.key); + if (!propertyName) { + return; + } + + // Handle empty base or variants properties + if ( + (propertyName === 'base' || propertyName === 'variants') && + property.value.type === 'ObjectExpression' && + isEmptyObject(property.value) + ) { + if (!reportedNodes.has(property)) { + reportedNodes.add(property); + ruleContext.report({ + node: property as Rule.Node, + messageId: 'emptyRecipeProperty', + data: { + propertyName, + }, + fix(fixer) { + return removeNodeWithComma(ruleContext, property, fixer); + }, + }); + } + } + + // Process base property nested objects + if (propertyName === 'base' && property.value.type === 'ObjectExpression') { + processEmptyNestedStyles(ruleContext, property.value, reportedNodes); + } + + // Process variant values + if (propertyName === 'variants' && property.value.type === 'ObjectExpression') { + // If variants is empty, it will be handled by the check above + if (!isEmptyObject(property.value)) { + // Process variant categories + property.value.properties.forEach((variantCategoryProperty) => { + if ( + variantCategoryProperty.type !== 'Property' || + variantCategoryProperty.value.type !== 'ObjectExpression' + ) { + return; + } + + // Check if all values in this category are empty + if (isEmptyObject(variantCategoryProperty.value) || areAllChildrenEmpty(variantCategoryProperty.value)) { + if (!reportedNodes.has(variantCategoryProperty)) { + reportedNodes.add(variantCategoryProperty); + ruleContext.report({ + node: variantCategoryProperty as Rule.Node, + messageId: 'emptyVariantCategory', + fix(fixer) { + return removeNodeWithComma(ruleContext, variantCategoryProperty, fixer); + }, + }); + } + return; + } + + // Process individual variant values + variantCategoryProperty.value.properties.forEach((variantValueProperty) => { + if (variantValueProperty.type !== 'Property') { + return; + } + + // Check for non-object variant values + if (variantValueProperty.value.type !== 'ObjectExpression') { + if (!reportedNodes.has(variantValueProperty)) { + reportedNodes.add(variantValueProperty); + + // Get a user-friendly type description as a string + const friendlyType = (() => { + const nodeType = variantValueProperty.value.type; + + if (nodeType === 'Literal') { + const literalValue = variantValueProperty.value as TSESTree.Literal; + return literalValue.value === null ? 'null' : typeof literalValue.value; + } else if (nodeType === 'Identifier') { + return 'variable'; + } + + return nodeType; + })(); + + ruleContext.report({ + node: variantValueProperty as Rule.Node, + messageId: 'invalidPropertyType', + data: { + type: friendlyType, + }, + }); + } + return; + } + + // Check for empty objects in variant properties + if (isEmptyObject(variantValueProperty.value)) { + if (!reportedNodes.has(variantValueProperty)) { + reportedNodes.add(variantValueProperty); + ruleContext.report({ + node: variantValueProperty as Rule.Node, + messageId: 'emptyVariantValue', + fix(fixer) { + return removeNodeWithComma(ruleContext, variantValueProperty, fixer); + }, + }); + } + } else { + // Process nested styles within variant values + processEmptyNestedStyles(ruleContext, variantValueProperty.value, reportedNodes); + } + }); + }); + } + } + }); +} diff --git a/src/css-rules/no-empty-blocks/rule-definition.ts b/src/css-rules/no-empty-blocks/rule-definition.ts new file mode 100644 index 0000000..1778c56 --- /dev/null +++ b/src/css-rules/no-empty-blocks/rule-definition.ts @@ -0,0 +1,33 @@ +import type { Rule } from 'eslint'; +import { createEmptyStyleVisitors } from './empty-style-visitor-creator.js'; + +const noEmptyStyleBlocksRule: Rule.RuleModule = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow empty style blocks in vanilla-extract stylesheets', + category: 'Best Practices', + recommended: true, + }, + fixable: 'code', + schema: [], + messages: { + emptyConditionalStyle: 'Empty conditional style object should be removed.', + emptyMedia: 'Empty @media object should be removed.', + emptyNestedStyle: 'Empty nested style object should be removed.', + emptyRecipeProperty: 'Empty {{propertyName}} object in recipe should be removed.', + emptySelectors: 'Empty selectors object should be removed.', + emptySpreadObject: 'Empty spread object should be removed.', + emptyStyleDeclaration: 'Declarations with only empty style blocks should be removed.', + emptySupports: 'Empty @supports object should be removed.', + emptyVariantCategory: 'Empty variant category should be removed.', + emptyVariantValue: 'Empty variant value should be removed.', + invalidPropertyType: 'Variant values must be objects, found {{type}} instead.', + }, + }, + create(ruleContext) { + return createEmptyStyleVisitors(ruleContext); + }, +}; + +export default noEmptyStyleBlocksRule; diff --git a/src/css-rules/no-empty-blocks/style-variants-processor.ts b/src/css-rules/no-empty-blocks/style-variants-processor.ts new file mode 100644 index 0000000..8992a2b --- /dev/null +++ b/src/css-rules/no-empty-blocks/style-variants-processor.ts @@ -0,0 +1,53 @@ +import type { Rule } from 'eslint'; +import { TSESTree } from '@typescript-eslint/utils'; +import { isEmptyObject } from '../shared-utils/empty-object-processor.js'; +import { removeNodeWithComma } from './node-remover.js'; + +/** + * Processes styleVariants function calls to detect and remove empty style variants. + * + * @param ruleContext The ESLint rule context. + * @param node The styleVariants call argument (object expression). + * @param reportedNodes A set of nodes that have already been reported. + */ +export function processStyleVariants( + ruleContext: Rule.RuleContext, + node: TSESTree.ObjectExpression, + reportedNodes: Set, +): void { + node.properties.forEach((property) => { + if (property.type !== 'Property') { + return; + } + + // Check for empty arrays + if (property.value.type === 'ArrayExpression' && property.value.elements.length === 0) { + if (!reportedNodes.has(property)) { + reportedNodes.add(property); + ruleContext.report({ + node: property as Rule.Node, + messageId: 'emptyVariantValue', + fix(fixer) { + return removeNodeWithComma(ruleContext, property, fixer); + }, + }); + } + return; + } + + // Check for empty objects + if (property.value.type === 'ObjectExpression' && isEmptyObject(property.value)) { + if (!reportedNodes.has(property)) { + reportedNodes.add(property); + ruleContext.report({ + node: property as Rule.Node, + messageId: 'emptyVariantValue', + fix(fixer) { + return removeNodeWithComma(ruleContext, property, fixer); + }, + }); + } + return; + } + }); +} diff --git a/src/css-rules/shared-utils/__tests__/font-face-property-order-enforcer.test.ts b/src/css-rules/shared-utils/__tests__/font-face-property-order-enforcer.test.ts index c4002dc..4d3a77c 100644 --- a/src/css-rules/shared-utils/__tests__/font-face-property-order-enforcer.test.ts +++ b/src/css-rules/shared-utils/__tests__/font-face-property-order-enforcer.test.ts @@ -14,15 +14,13 @@ const fontFaceEdgeCasesRule = { CallExpression(node: TSESTree.CallExpression) { if (node.callee.type === 'Identifier' && node.callee.name === 'testNullCase') { // This will trigger the first early return - // eslint-disable-next-line @typescript-eslint/no-explicit-any - enforceFontFaceOrder(context, null as any); + enforceFontFaceOrder(context, null as unknown as TSESTree.ObjectExpression); } // Test case for non-ObjectExpression node if (node.callee.type === 'Identifier' && node.callee.name === 'testNonObjectCase') { // This will trigger the first early return (wrong type) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - enforceFontFaceOrder(context, { type: 'Literal' } as any); + enforceFontFaceOrder(context, { type: 'Literal' } as unknown as TSESTree.ObjectExpression); } // Test case for empty or single property object @@ -41,8 +39,7 @@ const fontFaceEdgeCasesRule = { shorthand: false, }, ], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); + } as TSESTree.ObjectExpression); } }, }; diff --git a/src/css-rules/shared-utils/__tests__/order-strategy-visitor-creator.test.ts b/src/css-rules/shared-utils/__tests__/order-strategy-visitor-creator.test.ts index 00a110b..cead417 100644 --- a/src/css-rules/shared-utils/__tests__/order-strategy-visitor-creator.test.ts +++ b/src/css-rules/shared-utils/__tests__/order-strategy-visitor-creator.test.ts @@ -4,6 +4,7 @@ import { run } from 'eslint-vitest-rule-tester'; import alphabeticalOrderRule from '../../alphabetical-order/rule-definition.js'; import customGroupOrderRule from '../../custom-order/rule-definition.js'; import { createNodeVisitors } from '../order-strategy-visitor-creator.js'; +import type { OrderingStrategy } from '../../types.js'; import type { TSESTree } from '@typescript-eslint/utils'; // Modified version of the custom order rule with empty group order @@ -29,8 +30,8 @@ const defaultCaseRule = { ...alphabeticalOrderRule, create(context: Rule.RuleContext) { // Force the default case by passing an invalid ordering strategy - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const visitors = createNodeVisitors(context, 'invalid-strategy' as any); + + const visitors = createNodeVisitors(context, 'invalid-strategy' as OrderingStrategy); return visitors; }, }; @@ -44,8 +45,7 @@ const nonIdentifierCalleeRule = { // Original rule's visitor will be called first const visitors = alphabeticalOrderRule.create(context); if (visitors.CallExpression) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - visitors.CallExpression(node as any); + visitors.CallExpression(node as unknown as Parameters[0]); } }, }; diff --git a/src/css-rules/shared-utils/__tests__/test-property-name-rule.ts b/src/css-rules/shared-utils/__tests__/test-property-name-rule.ts index be24ccb..58f9398 100644 --- a/src/css-rules/shared-utils/__tests__/test-property-name-rule.ts +++ b/src/css-rules/shared-utils/__tests__/test-property-name-rule.ts @@ -1,5 +1,5 @@ import type { Rule } from 'eslint'; -import { getPropertyName } from '../property-separator.js'; +import { getPropertyNameForSorting } from '../property-separator.js'; import type { TSESTree } from '@typescript-eslint/utils'; const testRuleForPropertyNameExtractor: Rule.RuleModule = { @@ -22,8 +22,8 @@ const testRuleForPropertyNameExtractor: Rule.RuleModule = { // Extract property names without enforcing any order node.properties.forEach((property) => { if (property.type === 'Property') { - // Test the getPropertyName function - getPropertyName(property as TSESTree.Property); + // Test the getPropertyNameForSorting function + getPropertyNameForSorting(property as TSESTree.Property); } }); }, diff --git a/src/css-rules/shared-utils/alphabetical-property-comparator.ts b/src/css-rules/shared-utils/alphabetical-property-comparator.ts index 223af86..311bd37 100644 --- a/src/css-rules/shared-utils/alphabetical-property-comparator.ts +++ b/src/css-rules/shared-utils/alphabetical-property-comparator.ts @@ -1,4 +1,4 @@ -import { getPropertyName } from './property-separator.js'; +import { getPropertyNameForSorting } from './property-separator.js'; import type { TSESTree } from '@typescript-eslint/utils'; /** @@ -11,8 +11,8 @@ export const comparePropertiesAlphabetically = ( firstProperty: TSESTree.Property, secondProperty: TSESTree.Property, ): number => { - const firstName = getPropertyName(firstProperty); - const secondName = getPropertyName(secondProperty); + const firstName = getPropertyNameForSorting(firstProperty); + const secondName = getPropertyNameForSorting(secondProperty); // Special handling for 'src' property - it should always come first (relates to font face APIs only) if (firstName === 'src') { diff --git a/src/css-rules/shared-utils/empty-object-processor.ts b/src/css-rules/shared-utils/empty-object-processor.ts new file mode 100644 index 0000000..fbee6e5 --- /dev/null +++ b/src/css-rules/shared-utils/empty-object-processor.ts @@ -0,0 +1,17 @@ +import { TSESTree } from '@typescript-eslint/utils'; + +/** + * Type guard to check if a node is an ObjectExpression. + */ +export const isObjectExpression = (node: TSESTree.Node): node is TSESTree.ObjectExpression => { + return node.type === 'ObjectExpression'; +}; + +/** + * Checks if an object expression is empty (has no properties). + * @param node The node to check. + * @returns True if the node is an ObjectExpression with no properties. + */ +export const isEmptyObject = (node: TSESTree.Node): boolean => { + return isObjectExpression(node) && node.properties.length === 0; +}; diff --git a/src/css-rules/shared-utils/font-face-property-order-enforcer.ts b/src/css-rules/shared-utils/font-face-property-order-enforcer.ts index b2ac590..a8937bf 100644 --- a/src/css-rules/shared-utils/font-face-property-order-enforcer.ts +++ b/src/css-rules/shared-utils/font-face-property-order-enforcer.ts @@ -1,7 +1,7 @@ import type { Rule } from 'eslint'; import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; import { generateFixesForCSSOrder } from '../shared-utils/css-order-fixer.js'; -import { getPropertyName, separateProperties } from '../shared-utils/property-separator.js'; +import { getPropertyNameForSorting, separateProperties } from '../shared-utils/property-separator.js'; import { comparePropertiesAlphabetically } from './alphabetical-property-comparator.js'; /** @@ -36,8 +36,8 @@ export const enforceFontFaceOrder = ( ); if (violatingPair) { - const nextPropertyName = getPropertyName(violatingPair.nextProperty); - const currentPropertyName = getPropertyName(violatingPair.currentProperty); + const nextPropertyName = getPropertyNameForSorting(violatingPair.nextProperty); + const currentPropertyName = getPropertyNameForSorting(violatingPair.currentProperty); ruleContext.report({ node: violatingPair.nextProperty as Rule.Node, diff --git a/src/css-rules/shared-utils/order-strategy-visitor-creator.ts b/src/css-rules/shared-utils/order-strategy-visitor-creator.ts index 55b172c..a754aca 100644 --- a/src/css-rules/shared-utils/order-strategy-visitor-creator.ts +++ b/src/css-rules/shared-utils/order-strategy-visitor-creator.ts @@ -8,6 +8,8 @@ import { enforceUserDefinedGroupOrderInRecipe } from '../custom-order/recipe-ord import { enforceUserDefinedGroupOrderInStyleObject } from '../custom-order/style-object-processor.js'; import { enforceFontFaceOrder } from './font-face-property-order-enforcer.js'; import { processStyleNode } from './style-node-processor.js'; +import type { SortRemainingProperties } from '../concentric-order/types.js'; +import type { OrderingStrategy } from '../types.js'; /** * Creates an ESLint rule listener with visitors for style-related function calls. @@ -27,9 +29,9 @@ import { processStyleNode } from './style-node-processor.js'; */ export const createNodeVisitors = ( ruleContext: Rule.RuleContext, - orderingStrategy: 'alphabetical' | 'concentric' | 'userDefinedGroupOrder', + orderingStrategy: OrderingStrategy, userDefinedGroupOrder?: string[], - sortRemainingProperties?: 'alphabetical' | 'concentric', + sortRemainingProperties?: SortRemainingProperties, ): Rule.RuleListener => { // Select the appropriate property processing function based on the ordering strategy const processProperty = (() => { diff --git a/src/css-rules/shared-utils/property-separator.ts b/src/css-rules/shared-utils/property-separator.ts index 8259080..1aa9131 100644 --- a/src/css-rules/shared-utils/property-separator.ts +++ b/src/css-rules/shared-utils/property-separator.ts @@ -10,7 +10,7 @@ import { TSESTree } from '@typescript-eslint/utils'; * - Literal (string): Returns the string value. * For any other type of key, it returns an empty string. */ -export const getPropertyName = (property: TSESTree.Property): string => { +export const getPropertyNameForSorting = (property: TSESTree.Property): string => { if (property.key.type === 'Identifier') { return property.key.name; } else if (property.key.type === 'Literal' && typeof property.key.value === 'string') { @@ -43,7 +43,7 @@ export const separateProperties = ( // Separate regular CSS properties from special ones (pseudo selectors, etc.) properties.forEach((property) => { if (property.type === 'Property') { - const propName = getPropertyName(property); + const propName = getPropertyNameForSorting(property); if (propName.startsWith(':') || propName.startsWith('@') || propName === 'selectors') { specialProperties.push(property); diff --git a/src/css-rules/types.ts b/src/css-rules/types.ts new file mode 100644 index 0000000..fff9f86 --- /dev/null +++ b/src/css-rules/types.ts @@ -0,0 +1 @@ +export type OrderingStrategy = 'alphabetical' | 'concentric' | 'userDefinedGroupOrder'; diff --git a/src/css-sample/sample.css.ts b/src/css-sample/sample.css.ts index 3368c8d..f752015 100644 --- a/src/css-sample/sample.css.ts +++ b/src/css-sample/sample.css.ts @@ -432,3 +432,158 @@ export const selectButtonVariants = styleVariants({ color: 'red', }, }); + +// Test cases for noEmptyStyleBlocksRule + +// export const myRecipe = recipe({ +// base: { +// color: 'blue', +// selectors: {}, +// '@media': {}, +// '@supports': {}, +// }, +// variants: { +// size: { +// small: { +// selectors: { +// '&:hover': {}, +// }, +// '@media': { +// '(min-width: 768px)': {}, +// }, +// '@supports': { +// '(display: grid)': {}, +// }, +// }, +// }, +// }, +// }); + +// const base = style({ padding: 12 }); +// export const variant = styleVariants({ +// primary: [], +// secondary: [], +// bordered: {}, +// borderless: {}, +// }); + +// const baseStyles = { +// color: 'blue', +// margin: '10px', +// }; + +// const isDarkMode = false; + +// export const spreadStyle = style({ +// ...baseStyles, +// ...{}, +// }); + +// export const recipeWithNonObjectValue = recipe({ +// base: { color: 'black' }, +// variants: { +// color: { +// red: { color: 'red' }, +// // string instead of object +// string: 'string', +// // variable instead of object +// variable: baseStyles, +// }, +// }, +// }); + +// export const conditionalStyle = style(isDarkMode ? {} : {}); + +// export const recipeWithEmptyVariantValues = recipe({ +// base: { color: 'black' }, +// variants: { +// color: { +// blue: {}, +// red: {}, +// }, +// }, +// }); + +// export const nestedEmptyStyle = style({ +// selectors: { +// '&:hover': {}, +// '&:focus': {}, +// }, +// }); + +// const myEmptyStyle = style({}); +// export { myEmptyStyle }; + +// export const emptyStyle1 = style({}); +// export const emptyStyle2 = style({}); +// export const emptyVariants = styleVariants({}); +// export const emptyRecipe = recipe({}); + +// export const styleWithComments = style({ +// /* This is an empty style */ +// }); + +// export const styleWithEmptyMedia = style({ +// color: 'blue', +// '@media': { +// '(min-width: 768px)': {}, +// }, +// }); + +// export const styleWithEmptySelector = style({ +// color: 'blue', + +// selectors: { +// '&:hover': {}, +// }, +// }); + +// export const recipeWithBothEmpty = recipe({ +// base: {}, +// variants: {}, +// }); + +// export const recipeWithEmptyVariants = recipe({ +// base: { color: 'black' }, +// variants: {}, +// }); + +// export const recipeWithEmptyBase = recipe({ +// base: {}, +// variants: { +// color: { +// blue: { color: 'blue' }, +// }, +// }, +// }); + +// export const recipe = recipe({ +// base: {}, +// variants: { +// color: { +// red: {}, +// blue: {}, +// }, +// }, +// }); + +// export const recipeWithNonObjectVariants = recipe({ +// base: { color: 'blue' }, +// variants: { +// color: { +// size: 'string instead of object', // This is a string, not an object +// red: {}, +// }, +// }, +// }); + +// Using the same empty object reference in both branches +// export const myStyle = style(true ? {} : {}); + +// export const emptyFontFace = fontFace({}); +// globalFontFace('GlobalFont', {}); +// globalKeyframes('a', {}); +// export const emptyKeyframes = keyframes({}); +// globalStyle('ul', {}); +// export const emptyStyleVariants = styleVariants({}); +// export const emptyStyle = style({}); diff --git a/src/index.ts b/src/index.ts index da8fd0d..993269a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,26 +1,34 @@ import alphabeticalOrderRule from './css-rules/alphabetical-order/index.js'; import concentricOrderRule from './css-rules/concentric-order/index.js'; import customOrderRule from './css-rules/custom-order/rule-definition.js'; +import noEmptyStyleBlocksRule from './css-rules/no-empty-blocks/rule-definition.js'; export const vanillaExtract = { meta: { name: '@antebudimir/eslint-plugin-vanilla-extract', - version: '1.5.3', + version: '1.6.0', }, rules: { 'alphabetical-order': alphabeticalOrderRule, 'concentric-order': concentricOrderRule, 'custom-order': customOrderRule, + 'no-empty-style-blocks': noEmptyStyleBlocksRule, }, configs: { recommended: [ { plugins: { 'vanilla-extract': { - rules: { 'concentric-order': concentricOrderRule }, + rules: { + 'concentric-order': concentricOrderRule, + 'no-empty-style-blocks': noEmptyStyleBlocksRule, + }, }, }, - rules: { 'vanilla-extract/concentric-order': 'warn' }, + rules: { + 'vanilla-extract/concentric-order': 'warn', + 'vanilla-extract/no-empty-style-blocks': 'warn', + }, }, ], alphabetical: [ diff --git a/vitest.config.mjs b/vitest.config.mjs index 9cdea77..7cd7f71 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -6,7 +6,7 @@ export default defineConfig({ provider: 'v8', reporter: ['html', 'json', 'lcov', 'text'], reportsDirectory: './coverage/vitest-reports', - include: ['src/css-rules/**/*.ts', 'src/shared-utils/**/*.ts'], + include: ['src/css-rules/**/*.ts'], exclude: ['src/**/*.css.ts', 'src/**/*index.ts', 'src/**/*types.ts'], }, reporters: [