diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8be60cd6..47b8c5be 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -69,6 +69,16 @@ jobs: tag: ${{ env.NPM_TAG }} token: ${{ secrets.NPM_TOKEN }} + - name: Build eslint-plugin-ui + run: npm run build -w @studiometa/eslint-plugin-ui + + - uses: JS-DevTools/npm-publish@v3 + with: + provenance: true + package: packages/eslint-plugin-ui/ + tag: ${{ env.NPM_TAG }} + token: ${{ secrets.NPM_TOKEN }} + - uses: ncipollo/release-action@v1 with: tag: ${{ github.ref }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fa991795..8c681e76 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,6 +44,43 @@ jobs: fail_ci_if_error: false verbose: true + unit_eslint_plugin: + runs-on: macos-latest + env: + NODE_VERSION: 24 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: 'npm' + - name: Get npm cache directory + id: npm-cache-dir + shell: bash + run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT} + - uses: actions/cache@v4 + id: npm-cache + with: + path: | + ${{ steps.npm-cache-dir.outputs.dir }} + **/node_modules + .eslintcache + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: ${{ runner.os }}-node- + - name: Install modules + run: npm install --no-audit --no-fund + - name: Run tests + run: npm run test -w @studiometa/eslint-plugin-ui -- --coverage.enabled + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + env_vars: NODE_VERSION + directory: ./packages/eslint-plugin-ui/coverage + flags: unittests + fail_ci_if_error: false + verbose: true + unit_pest: runs-on: ubuntu-latest strategy: diff --git a/.oxlintrc.json b/.oxlintrc.json index 7678e9d0..da72c4e4 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,6 +1,6 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", - "plugins": ["vitest", "import", "promise", "regex", "jsdoc"], + "plugins": ["vitest", "import", "promise", "jsdoc"], "rules": { "func-style": ["warn", "declaration"], "no-floating-promises": "allow", diff --git a/CHANGELOG.md b/CHANGELOG.md index 66649aaa..4a31963d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- **ESLintPluginUi:** add a new `@studiometa/eslint-plugin-ui` package with ESLint/Oxlint rules to help developers discover and use components from `@studiometa/ui` ([#503](https://github.com/studiometa/ui/pull/503)) + +### Fixed + +- **Config:** fix deprecated `moduleResolution: node` value in root `tsconfig.json` ([#503](https://github.com/studiometa/ui/pull/503)) + ## [v1.8.0](https://github.com/studiometa/ui/compare/1.7.0..1.8.0) (2026-03-25) ### Added diff --git a/package-lock.json b/package-lock.json index 35a183c3..88f27e27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1998,6 +1998,253 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@floating-ui/core": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", @@ -2038,6 +2285,72 @@ "node": ">=20.0.0" } }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@iconify-json/octicon": { "version": "1.2.22", "resolved": "https://registry.npmjs.org/@iconify-json/octicon/-/octicon-1.2.22.tgz", @@ -3139,6 +3452,18 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@oxlint/plugins": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/plugins/-/plugins-1.63.0.tgz", + "integrity": "sha512-vZAzaUQkwgdN62RHgPFzfCsiBI6SDJMUdUlBGpJK0V++UHCMUk7UeJseygOhE/wOUSgV3ccE4ORkgab3C3MC6g==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -3979,6 +4304,10 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@studiometa/eslint-plugin-ui": { + "resolved": "packages/eslint-plugin-ui", + "link": true + }, "node_modules/@studiometa/js-toolkit": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/@studiometa/js-toolkit/-/js-toolkit-3.4.3.tgz", @@ -5260,48 +5589,206 @@ "vue": "^3.2.25" } }, - "node_modules/@volar/language-core": { - "version": "2.4.28", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", - "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "node_modules/@vitest/coverage-v8": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.2.tgz", + "integrity": "sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.28" + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.2", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.2", + "vitest": "4.1.2" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } } }, - "node_modules/@volar/source-map": { - "version": "2.4.28", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", - "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "node_modules/@vitest/coverage-v8/node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, - "node_modules/@vue/compiler-core": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", - "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "node_modules/@vitest/expect": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/shared": "3.5.30", - "entities": "^7.0.1", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@vue/compiler-core/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "node_modules/@vitest/mocker": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", "dev": true, - "license": "MIT" - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", - "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", + "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.30", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", "dev": true, "license": "MIT", "dependencies": { @@ -5788,6 +6275,16 @@ "acorn": "^8.14.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.3.5", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", @@ -6165,6 +6662,23 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "license": "MIT" + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -7622,6 +8136,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -8196,6 +8717,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -8765,6 +9292,67 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint": { + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.34.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -8778,6 +9366,266 @@ "node": ">=8.0.0" } }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -8791,6 +9639,29 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -8830,6 +9701,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -8918,6 +9799,20 @@ "node": ">=8.6.0" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -8957,12 +9852,25 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "license": "MIT" - }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/filemanager-webpack-plugin": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/filemanager-webpack-plugin/-/filemanager-webpack-plugin-8.0.0.tgz", @@ -9065,6 +9973,27 @@ "flat": "cli.js" } }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/floating-vue": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/floating-vue/-/floating-vue-5.2.2.tgz", @@ -9363,6 +10292,19 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "license": "BSD-2-Clause" }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -9915,6 +10857,16 @@ "url": "https://github.com/sponsors/sxzz" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -10458,6 +11410,13 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -10489,6 +11448,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json-stable-stringify/node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -10528,6 +11494,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -10597,6 +11573,20 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lightningcss": { "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", @@ -11096,6 +12086,17 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -12184,6 +13185,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -12377,6 +13385,24 @@ "node": ">=4" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/oxlint": { "version": "1.57.0", "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.57.0.tgz", @@ -14096,6 +15122,16 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", @@ -14205,6 +15241,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", @@ -16792,6 +17838,19 @@ "node": ">=0.10.0" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/stylehacks": { "version": "7.0.8", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.8.tgz", @@ -17300,9 +18359,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -17498,6 +18557,19 @@ "typescript": "^5.5.0" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -18004,6 +19076,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -18792,6 +19874,108 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vitest": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/vue": { "version": "3.5.30", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", @@ -19154,12 +20338,6 @@ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" }, - "node_modules/webpack/node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT" - }, "node_modules/webpack/node_modules/webpack-sources": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", @@ -19251,6 +20429,16 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "license": "MIT" }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -19517,6 +20705,35 @@ "vitepress-plugin-llms": "1.12.0" } }, + "packages/eslint-plugin-ui": { + "name": "@studiometa/eslint-plugin-ui", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@oxlint/plugins": "1.63.0" + }, + "devDependencies": { + "@vitest/coverage-v8": "4.1.2", + "esbuild": "0.27.4", + "eslint": "9.34.0", + "typescript": "6.0.2", + "vitest": "4.1.2" + } + }, + "packages/eslint-plugin-ui/node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "packages/playground": { "name": "@studiometa/ui-playground", "version": "1.8.0", @@ -19836,46 +21053,12 @@ "url": "https://opencollective.com/vitest" } }, - "packages/tests/node_modules/ast-v8-to-istanbul": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", - "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, "packages/tests/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, - "packages/tests/node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT" - }, - "packages/tests/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "license": "MIT" - }, - "packages/tests/node_modules/magicast": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", - "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "source-map-js": "^1.2.1" - } - }, "packages/tests/node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", diff --git a/packages/docs/.vitepress/config.ts b/packages/docs/.vitepress/config.ts index 76b74a5a..64cf80dd 100644 --- a/packages/docs/.vitepress/config.ts +++ b/packages/docs/.vitepress/config.ts @@ -107,6 +107,7 @@ function getGuideSidebar() { { text: 'Installation', link: '/guide/installation/' }, { text: 'Usage', link: '/guide/usage/' }, { text: 'Contributing', link: '/guide/contributing/' }, + { text: 'ESLint Plugin', link: '/guide/eslint-plugin/' }, ], }, { diff --git a/packages/docs/guide/eslint-plugin/index.md b/packages/docs/guide/eslint-plugin/index.md new file mode 100644 index 00000000..589d5c3f --- /dev/null +++ b/packages/docs/guide/eslint-plugin/index.md @@ -0,0 +1,156 @@ +# ESLint Plugin + +`@studiometa/eslint-plugin-ui` is an ESLint plugin that helps developers discover and use components from `@studiometa/ui` rather than reimplementing them from scratch. + +## Installation + +```bash +npm install --save-dev @studiometa/eslint-plugin-ui +``` + +## Configuration + +### ESLint + +Add the recommended config to your `eslint.config.js` (ESLint v9 flat config): + +```js +import { ui } from '@studiometa/eslint-plugin-ui'; + +export default [ + ui.configs.recommended, +]; +``` + +All rules ship at `warn` severity in the recommended config. To override individual rules: + +```js +import { ui } from '@studiometa/eslint-plugin-ui'; + +export default [ + ui.configs.recommended, + { + rules: { + 'ui/prefer-ui-component': 'error', + }, + }, +]; +``` + +### Oxlint + +Add the plugin to your `.oxlintrc.json` using the `"ui"` name to get the `ui/` rule prefix: + +```json +{ + "jsPlugins": [{ "name": "ui", "specifier": "@studiometa/eslint-plugin-ui" }], + "rules": { + "ui/prefer-ui-component": "warn", + "ui/prefer-transition": "warn", + "ui/no-manual-fetch": "warn", + "ui/prefer-data-model": "warn", + "ui/prefer-action": "warn" + } +} +``` + +## Rules + +| Rule | Description | Recommended | +| ---- | ----------- | ----------- | +| [`ui/prefer-ui-component`](#prefer-ui-component) | Warn when a class named after a `@studiometa/ui` component extends `Base` directly | warn | +| [`ui/prefer-transition`](#prefer-transition) | Warn when a `Base` subclass manually implements `open()` and `close()` | warn | +| [`ui/no-manual-fetch`](#no-manual-fetch) | Warn when a `Base` subclass combines `fetch()` with DOM injection | warn | +| [`ui/prefer-data-model`](#prefer-data-model) | Warn when a `Base` subclass manually syncs input values to the DOM | warn | +| [`ui/prefer-action`](#prefer-action) | Warn when a `Base` subclass only defines a single simple event handler | warn | + +### `ui/prefer-ui-component` + +Detects classes whose name matches a component exported by `@studiometa/ui` (such as `Menu`, `Accordion`, or `Modal`) that extend `Base` directly. Suggests importing and extending the existing component instead. + +```js +// ❌ Incorrect +import { Base } from '@studiometa/js-toolkit'; +class Menu extends Base { } + +// ✅ Correct +import { Menu } from '@studiometa/ui'; +class MyMenu extends Menu { } +``` + +### `ui/prefer-transition` + +Detects `Base` subclasses that manually implement both `open()` and `close()` methods. This pattern is already handled by the [`Transition`](/components/Transition/) component, which manages CSS class transitions declaratively. + +```js +// ❌ Incorrect +import { Base } from '@studiometa/js-toolkit'; +class Drawer extends Base { + open() { this.$el.classList.add('is-open'); } + close() { this.$el.classList.remove('is-open'); } +} + +// ✅ Correct +import { Transition } from '@studiometa/ui'; +class Drawer extends Transition { } +``` + +### `ui/no-manual-fetch` + +Detects `Base` subclasses that combine a native `fetch()` call with DOM injection via `innerHTML` or `insertAdjacentHTML`. The [`Fetch`](/components/Fetch/) component handles this declaratively. + +```js +// ❌ Incorrect +import { Base } from '@studiometa/js-toolkit'; +class ProductList extends Base { + async loadMore() { + const res = await fetch('/products?page=2'); + this.$el.innerHTML = await res.text(); + } +} + +// ✅ Correct +import { Fetch } from '@studiometa/ui'; +class ProductList extends Fetch { } +``` + +### `ui/prefer-data-model` + +Detects `Base` subclasses with an `onInput*` or `onChange*` method that writes to `this.$refs.*` DOM properties (`textContent`, `innerHTML`, or `value`). The [`DataModel`](/components/DataModel/) and [`DataEffect`](/components/DataEffect/) components handle reactive bindings declaratively without manual wiring. + +```js +// ❌ Incorrect +import { Base } from '@studiometa/js-toolkit'; +class LiveSearch extends Base { + onQueryInput() { + this.$refs.results.innerHTML = ''; + } +} + +// ✅ Correct +import { DataModel, DataEffect } from '@studiometa/ui'; +``` + +### `ui/prefer-action` + +Detects `Base` subclasses whose only logic is a single simple event handler (`onClick`, `onMouseenter`, `onMouseleave`, etc.) with no other methods. The [`Action`](/components/Action/) component handles this use case declaratively via data attributes, without needing a JavaScript class. + +```js +// ❌ Incorrect +import { Base } from '@studiometa/js-toolkit'; +class Toggle extends Base { + onClick() { + this.$el.classList.toggle('is-active'); + } +} +``` + +```html + +
+``` diff --git a/packages/eslint-plugin-ui/README.md b/packages/eslint-plugin-ui/README.md new file mode 100644 index 00000000..e63f4756 --- /dev/null +++ b/packages/eslint-plugin-ui/README.md @@ -0,0 +1,158 @@ +# @studiometa/eslint-plugin-ui + +[![NPM Version](https://img.shields.io/npm/v/@studiometa/eslint-plugin-ui.svg?style=flat&colorB=3e63dd&colorA=414853)](https://www.npmjs.com/package/@studiometa/eslint-plugin-ui/) +[![Downloads](https://img.shields.io/npm/dm/@studiometa/eslint-plugin-ui?style=flat&colorB=3e63dd&colorA=414853)](https://www.npmjs.com/package/@studiometa/eslint-plugin-ui/) +[![Size](https://img.shields.io/bundlephobia/minzip/@studiometa/eslint-plugin-ui?style=flat&colorB=3e63dd&colorA=414853&label=size)](https://bundlephobia.com/package/@studiometa/eslint-plugin-ui) +![Codecov](https://img.shields.io/codecov/c/github/studiometa/ui?style=flat&colorB=3e63dd&colorA=414853) + +ESLint plugin to help developers discover and use components from [@studiometa/ui](https://ui.studiometa.dev). + +## Installation + +```bash +npm install --save-dev @studiometa/eslint-plugin-ui +``` + +## Configuration + +### ESLint + +Add the recommended config to your `eslint.config.js` (ESLint v9 flat config): + +```js +import { ui } from '@studiometa/eslint-plugin-ui'; + +export default [ + ui.configs.recommended, + // ...your other config +]; +``` + +To customise individual rule severities, add an override entry after the recommended config: + +```js +import { ui } from '@studiometa/eslint-plugin-ui'; + +export default [ + ui.configs.recommended, + { + rules: { + 'ui/prefer-ui-component': 'error', + }, + }, +]; +``` + +### Oxlint + +Add the plugin to your `.oxlintrc.json` using the `"ui"` name to get the `ui/` rule prefix: + +```json +{ + "jsPlugins": [{ "name": "ui", "specifier": "@studiometa/eslint-plugin-ui" }], + "rules": { + "ui/prefer-ui-component": "warn", + "ui/prefer-transition": "warn", + "ui/no-manual-fetch": "warn", + "ui/prefer-data-model": "warn", + "ui/prefer-action": "warn" + } +} +``` + +## Rules + +| Rule | Description | Recommended | +| ---- | ----------- | ----------- | +| `ui/prefer-ui-component` | Warn when a class named after a `@studiometa/ui` component extends `Base` directly instead of importing from the library | warn | +| `ui/prefer-transition` | Warn when a `Base` subclass manually implements `open()` and `close()` — suggest `Transition` | warn | +| `ui/no-manual-fetch` | Warn when a `Base` subclass combines `fetch()` with DOM injection — suggest the `Fetch` component | warn | +| `ui/prefer-data-model` | Warn when a `Base` subclass manually syncs input values to the DOM — suggest `DataModel`/`DataEffect` | warn | +| `ui/prefer-action` | Warn when a `Base` subclass only defines a single simple event handler — suggest `Action` | warn | + +### `ui/prefer-ui-component` + +Detects classes whose name matches a component exported by `@studiometa/ui` (e.g. `Menu`, `Accordion`, `Modal`) that extend `Base` directly. Suggests importing and extending the existing component instead. + +```js +// ❌ Incorrect +import { Base } from '@studiometa/js-toolkit'; +class Menu extends Base { } + +// ✅ Correct +import { Menu } from '@studiometa/ui'; +class MyMenu extends Menu { } +``` + +### `ui/prefer-transition` + +Detects `Base` subclasses that manually implement both `open()` and `close()` methods. This pattern is already handled by the `Transition` component. + +```js +// ❌ Incorrect +import { Base } from '@studiometa/js-toolkit'; +class Drawer extends Base { + open() { this.$el.classList.add('is-open'); } + close() { this.$el.classList.remove('is-open'); } +} + +// ✅ Correct +import { Transition } from '@studiometa/ui'; +class Drawer extends Transition { } +``` + +### `ui/no-manual-fetch` + +Detects `Base` subclasses that combine a `fetch()` call with DOM injection via `innerHTML` or `insertAdjacentHTML`. The `Fetch` component handles this declaratively. + +```js +// ❌ Incorrect +import { Base } from '@studiometa/js-toolkit'; +class ProductList extends Base { + async loadMore() { + const res = await fetch('/products?page=2'); + this.$el.innerHTML = await res.text(); + } +} + +// ✅ Correct +import { Fetch } from '@studiometa/ui'; +class ProductList extends Fetch { } +``` + +### `ui/prefer-data-model` + +Detects `Base` subclasses with an `onInput*` or `onChange*` method that writes to `this.$refs.*` DOM properties. The `DataModel` and `DataEffect` components handle reactive bindings declaratively. + +```js +// ❌ Incorrect +import { Base } from '@studiometa/js-toolkit'; +class LiveSearch extends Base { + onQueryInput() { + this.$refs.results.innerHTML = ''; + } +} + +// ✅ Correct +import { DataModel, DataEffect } from '@studiometa/ui'; +``` + +### `ui/prefer-action` + +Detects `Base` subclasses whose only logic is a single simple event handler (`onClick`, `onMouseenter`, etc.) with no other methods. The `Action` component handles this use case declaratively via data attributes. + +```js +// ❌ Incorrect +import { Base } from '@studiometa/js-toolkit'; +class Toggle extends Base { + onClick() { + this.$el.classList.toggle('is-active'); + } +} + +// ✅ Correct — use the Action component via data attributes +//
+``` diff --git a/packages/eslint-plugin-ui/package.json b/packages/eslint-plugin-ui/package.json new file mode 100644 index 00000000..b89c8f68 --- /dev/null +++ b/packages/eslint-plugin-ui/package.json @@ -0,0 +1,42 @@ +{ + "name": "@studiometa/eslint-plugin-ui", + "version": "1.0.0", + "description": "ESLint plugin to help developers discover and use @studiometa/ui components", + "publishConfig": { + "access": "public" + }, + "author": "Studio Meta (https://www.studiometa.fr)", + "license": "MIT", + "type": "module", + "sideEffects": false, + "files": [ + "dist", + "README.md" + ], + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "scripts": { + "build": "node scripts/build.js", + "test": "vitest run", + "test:watch": "vitest", + "lint:types": "tsgo --build tsconfig.json" + }, + "devDependencies": { + "@vitest/coverage-v8": "4.1.2", + "esbuild": "0.27.4", + "eslint": "9.34.0", + "typescript": "6.0.2", + "vitest": "4.1.2" + }, + "dependencies": { + "@oxlint/plugins": "1.63.0" + } +} diff --git a/packages/eslint-plugin-ui/scripts/build.js b/packages/eslint-plugin-ui/scripts/build.js new file mode 100644 index 00000000..1569b155 --- /dev/null +++ b/packages/eslint-plugin-ui/scripts/build.js @@ -0,0 +1,28 @@ +import { resolve, dirname } from 'node:path'; +import { execSync } from 'node:child_process'; +import { rmSync } from 'node:fs'; +import esbuild from 'esbuild'; + +const root = resolve(dirname(new URL(import.meta.url).pathname), '..'); + +rmSync(resolve(root, 'dist'), { recursive: true, force: true }); +console.log('Building @studiometa/eslint-plugin-ui...'); + +const { errors, warnings } = await esbuild.build({ + entryPoints: [resolve(root, 'src/index.ts')], + bundle: true, + write: true, + outdir: resolve(root, 'dist'), + target: 'esnext', + format: 'esm', + sourcemap: true, + platform: 'node', +}); + +errors.forEach(console.error); +warnings.forEach(console.warn); + +console.log('Emitting types...'); +execSync('tsc --build tsconfig.json', { cwd: root, stdio: 'inherit' }); + +console.log('Done!'); diff --git a/packages/eslint-plugin-ui/src/index.ts b/packages/eslint-plugin-ui/src/index.ts new file mode 100644 index 00000000..0750509d --- /dev/null +++ b/packages/eslint-plugin-ui/src/index.ts @@ -0,0 +1,43 @@ +import { eslintCompatPlugin } from '@oxlint/plugins'; +import { + preferUiComponent, + preferTransition, + noManualFetch, + preferDataModel, + preferAction, +} from './rules/index.ts'; + +const PLUGIN_NAME = 'ui'; + +const rules = { + 'prefer-ui-component': preferUiComponent, + 'prefer-transition': preferTransition, + 'no-manual-fetch': noManualFetch, + 'prefer-data-model': preferDataModel, + 'prefer-action': preferAction, +}; + +const recommendedRules: Record = { + [`${PLUGIN_NAME}/prefer-ui-component`]: 'warn', + [`${PLUGIN_NAME}/prefer-transition`]: 'warn', + [`${PLUGIN_NAME}/no-manual-fetch`]: 'warn', + [`${PLUGIN_NAME}/prefer-data-model`]: 'warn', + [`${PLUGIN_NAME}/prefer-action`]: 'warn', +}; + +const base = eslintCompatPlugin({ + meta: { + name: '@studiometa/eslint-plugin-ui', + }, + rules, +}); + +const plugin = Object.assign(base, { configs: {} as Record }); + +plugin.configs['recommended'] = { + plugins: { [PLUGIN_NAME]: plugin }, + rules: recommendedRules, +}; + +export default plugin; +export { plugin as ui }; diff --git a/packages/eslint-plugin-ui/src/rules/index.ts b/packages/eslint-plugin-ui/src/rules/index.ts new file mode 100644 index 00000000..f406a35e --- /dev/null +++ b/packages/eslint-plugin-ui/src/rules/index.ts @@ -0,0 +1,5 @@ +export { preferUiComponent } from './prefer-ui-component.ts'; +export { preferTransition } from './prefer-transition.ts'; +export { noManualFetch } from './no-manual-fetch.ts'; +export { preferDataModel } from './prefer-data-model.ts'; +export { preferAction } from './prefer-action.ts'; diff --git a/packages/eslint-plugin-ui/src/rules/no-manual-fetch.test.ts b/packages/eslint-plugin-ui/src/rules/no-manual-fetch.test.ts new file mode 100644 index 00000000..3bea372d --- /dev/null +++ b/packages/eslint-plugin-ui/src/rules/no-manual-fetch.test.ts @@ -0,0 +1,64 @@ +import { describe, it } from 'vitest'; +import { tester } from '../utils/rule-tester.ts'; +import { noManualFetch } from './no-manual-fetch.ts'; + +describe('no-manual-fetch', () => { + it('passes and fails correctly', () => { + tester.run('no-manual-fetch', noManualFetch as any, { + valid: [ + // fetch without DOM write — just data fetching + `import { Base } from '@studiometa/js-toolkit'; + class Foo extends Base { + async mounted() { + const data = await fetch('/api').then(r => r.json()); + this.data = data; + } + }`, + + // Already using Fetch from @studiometa/ui + `import { Fetch } from '@studiometa/ui'; + import { Base } from '@studiometa/js-toolkit'; + class Foo extends Base { + async mounted() { + const res = await fetch('/api'); + this.$el.innerHTML = await res.text(); + } + }`, + + // innerHTML write without fetch — might be something else + `import { Base } from '@studiometa/js-toolkit'; + class Foo extends Base { + mounted() { this.$el.innerHTML = '

hello

'; } + }`, + + // Outside a Base subclass + `async function loadContent(el) { + const res = await fetch('/fragment'); + el.innerHTML = await res.text(); + }`, + ], + invalid: [ + { + code: `import { Base } from '@studiometa/js-toolkit'; +class ProductList extends Base { + async loadMore() { + const res = await fetch('/products?page=2'); + this.$el.innerHTML = await res.text(); + } +}`, + errors: [{ messageId: 'preferFetch' }], + }, + { + code: `import { Base } from '@studiometa/js-toolkit'; +class Facets extends Base { + async update() { + const res = await fetch(this.url); + this.$refs.container.insertAdjacentHTML('beforeend', await res.text()); + } +}`, + errors: [{ messageId: 'preferFetch' }], + }, + ], + }); + }); +}); diff --git a/packages/eslint-plugin-ui/src/rules/no-manual-fetch.ts b/packages/eslint-plugin-ui/src/rules/no-manual-fetch.ts new file mode 100644 index 00000000..4c00cc15 --- /dev/null +++ b/packages/eslint-plugin-ui/src/rules/no-manual-fetch.ts @@ -0,0 +1,91 @@ +import { + findEnclosingClass, + isBaseSubclass, + isImportedFromUI, + getAncestors, + type Node, + type RuleContext, + createRule, +} from '../utils/ast.ts'; + +const DOM_WRITE_METHODS = new Set(['insertAdjacentHTML', 'insertAdjacentElement']); + +/** + * Detect manual fetch() calls inside Base subclasses that also write to innerHTML + * or use insertAdjacentHTML — suggest using the Fetch component from @studiometa/ui. + */ +export const noManualFetch = createRule({ + meta: { + type: 'suggestion', + docs: { + description: + 'Prefer the Fetch component from @studiometa/ui over manually fetching and injecting HTML', + }, + messages: { + preferFetch: + 'Manual fetch() with DOM injection detected. ' + + "Consider using the Fetch component from @studiometa/ui instead: import { Fetch } from '@studiometa/ui'.", + }, + }, + createOnce(context: RuleContext) { + const classesWithFetch = new WeakSet(); + const classesWithDomWrite = new WeakSet(); + const reported = new WeakSet(); + + function getEnclosingBaseClass(node: Node): Node | null { + const ancestors = getAncestors(context, node); + const cls = findEnclosingClass(ancestors); + if (!cls || !isBaseSubclass(cls, context)) return null; + if (isImportedFromUI('Fetch', context)) return null; + return cls; + } + + function maybeReport(cls: Node) { + if (reported.has(cls)) return; + if (classesWithFetch.has(cls) && classesWithDomWrite.has(cls)) { + reported.add(cls); + context.report({ node: cls.id ?? cls, messageId: 'preferFetch' }); + } + } + + return { + CallExpression(node: Node) { + const callee = node.callee; + + // Detect fetch(url) + const isFetch = + (callee.type === 'Identifier' && callee.name === 'fetch') || + (callee.type === 'MemberExpression' && + callee.property?.name === 'fetch' && + callee.object?.type === 'ThisExpression'); + + // Detect el.insertAdjacentHTML / el.insertAdjacentElement + const isDomWrite = + callee.type === 'MemberExpression' && + DOM_WRITE_METHODS.has(callee.property?.name); + + if (!isFetch && !isDomWrite) return; + + const cls = getEnclosingBaseClass(node); + if (!cls) return; + + if (isFetch) classesWithFetch.add(cls); + if (isDomWrite) classesWithDomWrite.add(cls); + + maybeReport(cls); + }, + + AssignmentExpression(node: Node) { + const left = node.left; + if (left.type !== 'MemberExpression') return; + if (left.property?.name !== 'innerHTML') return; + + const cls = getEnclosingBaseClass(node); + if (!cls) return; + + classesWithDomWrite.add(cls); + maybeReport(cls); + }, + }; + }, +}); diff --git a/packages/eslint-plugin-ui/src/rules/prefer-action.test.ts b/packages/eslint-plugin-ui/src/rules/prefer-action.test.ts new file mode 100644 index 00000000..22e54f87 --- /dev/null +++ b/packages/eslint-plugin-ui/src/rules/prefer-action.test.ts @@ -0,0 +1,71 @@ +import { describe, it } from 'vitest'; +import { tester } from '../utils/rule-tester.ts'; +import { preferAction } from './prefer-action.ts'; + +describe('prefer-action', () => { + it('passes and fails correctly', () => { + tester.run('prefer-action', preferAction as any, { + valid: [ + // Already imports Action + `import { Action } from '@studiometa/ui'; + import { Base } from '@studiometa/js-toolkit'; + class Toggle extends Base { + onClick() { this.$el.classList.toggle('is-active'); } + }`, + + // Multiple action methods — more complex than what Action handles + `import { Base } from '@studiometa/js-toolkit'; + class Toggle extends Base { + onClick() {} + onMouseenter() {} + }`, + + // Has additional logic methods + `import { Base } from '@studiometa/js-toolkit'; + class Toggle extends Base { + onClick() {} + updateState() {} + }`, + + // Accesses $children — orchestrates other components + `import { Base } from '@studiometa/js-toolkit'; + class Toggle extends Base { + onClick() { this.$children.Panel[0].open(); } + }`, + + // Not a Base subclass + `class Toggle { + onClick() {} + }`, + ], + invalid: [ + { + code: `import { Base } from '@studiometa/js-toolkit'; +class Toggle extends Base { + onClick() { + this.$el.classList.toggle('is-active'); + } +}`, + errors: [{ messageId: 'preferAction' }], + }, + { + code: `import { Base } from '@studiometa/js-toolkit'; +class HoverEffect extends Base { + onMouseenter() { + this.$el.classList.add('is-hovered'); + } +}`, + errors: [{ messageId: 'preferAction' }], + }, + { + code: `class SubmitBtn extends Base { + onClick() { + this.$refs.form.submit(); + } +}`, + errors: [{ messageId: 'preferAction' }], + }, + ], + }); + }); +}); diff --git a/packages/eslint-plugin-ui/src/rules/prefer-action.ts b/packages/eslint-plugin-ui/src/rules/prefer-action.ts new file mode 100644 index 00000000..c730babb --- /dev/null +++ b/packages/eslint-plugin-ui/src/rules/prefer-action.ts @@ -0,0 +1,124 @@ +import { + isBaseSubclass, + getClassMethods, + isImportedFromUI, + type Node, + type RuleContext, + createRule, +} from '../utils/ast.ts'; + +/** + * Simple event handler method names that the Action component already handles. + * These are on-handler names on this.$el (root element) — if the class body + * contains ONLY these kinds of methods (plus config), it's a good Action candidate. + */ +const ACTION_EVENT_METHODS = new Set([ + 'onClick', + 'onMouseenter', + 'onMouseleave', + 'onMouseover', + 'onMouseout', + 'onFocus', + 'onBlur', + 'onKeydown', + 'onKeyup', + 'onKeypress', + 'onPointerenter', + 'onPointerleave', + 'onPointerdown', + 'onPointerup', +]); + +const NON_LOGIC_METHODS = new Set(['mounted', 'destroyed', 'updated']); + +/** + * Detect Base subclasses whose only logic is a single simple event handler + * (onClick, onMouseenter, etc.) — these can be replaced by the Action component. + */ +export const preferAction = createRule({ + meta: { + type: 'suggestion', + docs: { + description: + 'Prefer the Action component from @studiometa/ui for simple event-triggered effects', + }, + messages: { + preferAction: + 'This component only handles "{{method}}" on the root element. ' + + 'Consider using the Action component from @studiometa/ui instead, ' + + "which declaratively wires events to effects via data attributes: import { Action } from '@studiometa/ui'.", + }, + }, + createOnce(context: RuleContext) { + return { + ClassDeclaration(node: Node) { + check(node, context); + }, + ClassExpression(node: Node) { + check(node, context); + }, + }; + }, +}); + +function check(node: Node, context: RuleContext) { + if (!isBaseSubclass(node, context)) return; + if (isImportedFromUI('Action', context)) return; + + const methods = getClassMethods(node); + + // Find action-handler methods defined in this class + const actionMethods: string[] = []; + const otherMethods: string[] = []; + + for (const [name] of methods) { + if (ACTION_EVENT_METHODS.has(name)) { + actionMethods.push(name); + } else if (name !== 'config' && !NON_LOGIC_METHODS.has(name)) { + otherMethods.push(name); + } + } + + // Only flag when: exactly one action method, no other logic methods + if (actionMethods.length !== 1 || otherMethods.length > 0) return; + + // Check the method body is simple — does not access this.$children or this.$refs + // to avoid false positives on components that orchestrate children + const method = methods.get(actionMethods[0])!; + if (hasComplexBody(method)) return; + + context.report({ + node: node.id ?? node, + messageId: 'preferAction', + data: { method: actionMethods[0] }, + }); +} + +function hasComplexBody(methodNode: Node): boolean { + let complex = false; + + function walk(n: Node) { + if (complex) return; + if (!n || typeof n !== 'object') return; + + // Accessing $children or using await suggests complex logic + if ( + n.type === 'MemberExpression' && + n.object?.type === 'ThisExpression' && + (n.property?.name === '$children' || n.property?.name === '$root') + ) { + complex = true; + return; + } + + for (const key of Object.keys(n)) { + if (key === 'parent') continue; + const child = n[key]; + if (Array.isArray(child)) child.forEach(walk); + else if (child && typeof child === 'object' && child.type) walk(child); + } + } + + walk(methodNode.value ?? methodNode); + return complex; +} diff --git a/packages/eslint-plugin-ui/src/rules/prefer-data-model.test.ts b/packages/eslint-plugin-ui/src/rules/prefer-data-model.test.ts new file mode 100644 index 00000000..1d761664 --- /dev/null +++ b/packages/eslint-plugin-ui/src/rules/prefer-data-model.test.ts @@ -0,0 +1,57 @@ +import { describe, it } from 'vitest'; +import { tester } from '../utils/rule-tester.ts'; +import { preferDataModel } from './prefer-data-model.ts'; + +describe('prefer-data-model', () => { + it('passes and fails correctly', () => { + tester.run('prefer-data-model', preferDataModel as any, { + valid: [ + // Already uses DataModel + `import { DataModel } from '@studiometa/ui'; + import { Base } from '@studiometa/js-toolkit'; + class Foo extends Base { + onInputChange() { this.$refs.output.textContent = this.$refs.input.value; } + }`, + + // Input handler but no DOM write on this.$refs + `import { Base } from '@studiometa/js-toolkit'; + class Foo extends Base { + onInputChange() { console.log('changed'); } + }`, + + // DOM write but no input handler + `import { Base } from '@studiometa/js-toolkit'; + class Foo extends Base { + mounted() { this.$refs.label.textContent = 'hello'; } + }`, + + // Not a Base subclass + `class Foo { + onInputChange() { this.$refs.output.textContent = 'x'; } + }`, + ], + invalid: [ + { + code: `import { Base } from '@studiometa/js-toolkit'; +class QuantityInput extends Base { + onPlusClick() {} + onMinusClick() {} + onInputChange() { + this.$refs.display.textContent = this.$refs.input.value; + } +}`, + errors: [{ messageId: 'preferDataModel' }], + }, + { + code: `import { Base } from '@studiometa/js-toolkit'; +class LiveSearch extends Base { + onQueryInput() { + this.$refs.results.innerHTML = ''; + } +}`, + errors: [{ messageId: 'preferDataModel' }], + }, + ], + }); + }); +}); diff --git a/packages/eslint-plugin-ui/src/rules/prefer-data-model.ts b/packages/eslint-plugin-ui/src/rules/prefer-data-model.ts new file mode 100644 index 00000000..908a1668 --- /dev/null +++ b/packages/eslint-plugin-ui/src/rules/prefer-data-model.ts @@ -0,0 +1,105 @@ +import { + findEnclosingClass, + isBaseSubclass, + isImportedFromUI, + getAncestors, + type Node, + type RuleContext, + createRule, +} from '../utils/ast.ts'; + +/** + * Detect manual input/change event listeners that update DOM — suggest DataModel/DataEffect. + * + * Signal: addEventListener('input', ...) or an onInput* method that writes to + * textContent / innerHTML / value on another element inside a Base subclass. + */ +/** Walk up a member expression chain and check if the root is this.$refs / this.$el / this.$children */ +function isThisComponentAccess(node: Node): boolean { + let current = node; + while (current?.type === 'MemberExpression') { + if ( + current.object?.type === 'ThisExpression' && + (current.property?.name === '$refs' || + current.property?.name === '$el' || + current.property?.name === '$children') + ) { + return true; + } + current = current.object; + } + return false; +} + +export const preferDataModel = createRule({ + meta: { + type: 'suggestion', + docs: { + description: + 'Prefer DataModel/DataEffect from @studiometa/ui over manually syncing input values to the DOM', + }, + messages: { + preferDataModel: + 'Manual input→DOM synchronization detected. ' + + 'Consider using DataModel and DataEffect from @studiometa/ui for reactive bindings: ' + + "import { DataModel, DataEffect } from '@studiometa/ui'.", + }, + }, + createOnce(context: RuleContext) { + const classesWithInputListener = new WeakSet(); + const classesWithDomWrite = new WeakSet(); + const reported = new WeakSet(); + + function getEnclosingBaseClass(node: Node): Node | null { + const ancestors = getAncestors(context, node); + const cls = findEnclosingClass(ancestors); + if (!cls || !isBaseSubclass(cls, context)) return null; + if (isImportedFromUI('DataModel', context) || isImportedFromUI('DataEffect', context)) + return null; + return cls; + } + + function maybeReport(cls: Node) { + if (reported.has(cls)) return; + if (classesWithInputListener.has(cls) && classesWithDomWrite.has(cls)) { + reported.add(cls); + context.report({ node: cls.id ?? cls, messageId: 'preferDataModel' }); + } + } + + return { + // Detect on-handler methods named onInput*, onChange*, onModelInput, etc. + MethodDefinition(node: Node) { + const name: string = node.key?.name ?? ''; + if (!/^on.*(Input|Change|Model)/i.test(name)) return; + + const ancestors = getAncestors(context, node); + const cls = findEnclosingClass(ancestors); + if (!cls || !isBaseSubclass(cls, context)) return; + if (isImportedFromUI('DataModel', context)) return; + + classesWithInputListener.add(cls); + maybeReport(cls); + }, + + AssignmentExpression(node: Node) { + const left = node.left; + if (left.type !== 'MemberExpression') return; + + const prop = left.property?.name; + const isDomWrite = + prop === 'textContent' || prop === 'innerHTML' || prop === 'value'; + if (!isDomWrite) return; + + // Must ultimately access this.$refs, this.$el, or this.$children + if (!isThisComponentAccess(left.object)) return; + + const cls = getEnclosingBaseClass(node); + if (!cls) return; + + classesWithDomWrite.add(cls); + maybeReport(cls); + }, + }; + }, +}); diff --git a/packages/eslint-plugin-ui/src/rules/prefer-transition.test.ts b/packages/eslint-plugin-ui/src/rules/prefer-transition.test.ts new file mode 100644 index 00000000..c667355f --- /dev/null +++ b/packages/eslint-plugin-ui/src/rules/prefer-transition.test.ts @@ -0,0 +1,54 @@ +import { describe, it } from 'vitest'; +import { tester } from '../utils/rule-tester.ts'; +import { preferTransition } from './prefer-transition.ts'; + +describe('prefer-transition', () => { + it('passes and fails correctly', () => { + tester.run('prefer-transition', preferTransition as any, { + valid: [ + // Already extends Transition from @studiometa/ui + `import { Transition } from '@studiometa/ui'; + class Cart extends Transition { + open() {} + close() {} + }`, + + // Has open/close but extends a UI component — fine + `import { Modal } from '@studiometa/ui'; + class Search extends Modal { + open() {} + close() {} + }`, + + // Only one of the two methods + `import { Base } from '@studiometa/js-toolkit'; + class Foo extends Base { + open() {} + }`, + + // No superclass + `class Foo { + open() {} + close() {} + }`, + ], + invalid: [ + { + code: `import { Base } from '@studiometa/js-toolkit'; +class Cart extends Base { + open() { this.$el.classList.add('is-open'); } + close() { this.$el.classList.remove('is-open'); } +}`, + errors: [{ messageId: 'preferTransition' }], + }, + { + code: `class Drawer extends Base { + open() {} + close() {} +}`, + errors: [{ messageId: 'preferTransition' }], + }, + ], + }); + }); +}); diff --git a/packages/eslint-plugin-ui/src/rules/prefer-transition.ts b/packages/eslint-plugin-ui/src/rules/prefer-transition.ts new file mode 100644 index 00000000..b75f3383 --- /dev/null +++ b/packages/eslint-plugin-ui/src/rules/prefer-transition.ts @@ -0,0 +1,49 @@ +import { + isBaseSubclass, + isImportedFromUI, + getClassMethods, + type Node, + type RuleContext, + createRule, +} from '../utils/ast.ts'; + +/** + * Detect classes that extend Base and define both open() and close() methods — + * a pattern that duplicates what Transition (or Modal) already provides. + */ +export const preferTransition = createRule({ + meta: { + type: 'suggestion', + docs: { + description: + 'Prefer @studiometa/ui Transition over manually implementing open/close logic in a Base subclass', + }, + messages: { + preferTransition: + 'This class manually implements open() and close() methods. ' + + 'Consider extending Transition from @studiometa/ui instead, which handles show/hide with CSS class transitions: ' + + "import { Transition } from '@studiometa/ui'.", + }, + }, + createOnce(context: RuleContext) { + return { + ClassDeclaration(node: Node) { + check(node, context); + }, + ClassExpression(node: Node) { + check(node, context); + }, + }; + }, +}); + +function check(node: Node, context: RuleContext) { + if (!isBaseSubclass(node, context)) return; + // Already using a @studiometa/ui component as the base + if (isImportedFromUI(node.superClass?.name, context)) return; + + const methods = getClassMethods(node); + if (!methods.has('open') || !methods.has('close')) return; + + context.report({ node: node.id ?? node, messageId: 'preferTransition' }); +} diff --git a/packages/eslint-plugin-ui/src/rules/prefer-ui-component.test.ts b/packages/eslint-plugin-ui/src/rules/prefer-ui-component.test.ts new file mode 100644 index 00000000..dd6916a7 --- /dev/null +++ b/packages/eslint-plugin-ui/src/rules/prefer-ui-component.test.ts @@ -0,0 +1,48 @@ +import { describe, it } from 'vitest'; +import { tester } from '../utils/rule-tester.ts'; +import { preferUiComponent } from './prefer-ui-component.ts'; + +describe('prefer-ui-component', () => { + it('passes and fails correctly', () => { + tester.run('prefer-ui-component', preferUiComponent as any, { + valid: [ + // Unknown component name — not in @studiometa/ui + `import { Base } from '@studiometa/js-toolkit'; + class Carousel extends Base {}`, + + // Properly extending from @studiometa/ui + `import { Menu as MenuCore } from '@studiometa/ui'; + class Menu extends MenuCore {}`, + + // Extending a UI component with a different local name + `import { Accordion } from '@studiometa/ui'; + class MyAccordion extends Accordion {}`, + + // No superclass + `class Accordion {}`, + ], + invalid: [ + { + code: `import { Base } from '@studiometa/js-toolkit'; +class Menu extends Base {}`, + errors: [{ messageId: 'preferImport' }], + }, + { + code: `import { Base } from '@studiometa/js-toolkit'; +class Accordion extends Base {}`, + errors: [{ messageId: 'preferImport' }], + }, + { + code: `import { Base } from '@studiometa/js-toolkit'; +class Modal extends Base {}`, + errors: [{ messageId: 'preferImport' }], + }, + { + // Bare Base (no import) also triggers + code: `class Sticky extends Base {}`, + errors: [{ messageId: 'preferImport' }], + }, + ], + }); + }); +}); diff --git a/packages/eslint-plugin-ui/src/rules/prefer-ui-component.ts b/packages/eslint-plugin-ui/src/rules/prefer-ui-component.ts new file mode 100644 index 00000000..e5addee9 --- /dev/null +++ b/packages/eslint-plugin-ui/src/rules/prefer-ui-component.ts @@ -0,0 +1,43 @@ +import { + UI_COMPONENT_NAMES, + isBaseSubclass, + isImportedFromUI, + type Node, + type RuleContext, + createRule, +} from '../utils/ast.ts'; + +export const preferUiComponent = createRule({ + meta: { + type: 'suggestion', + docs: { + description: + 'Prefer importing components from @studiometa/ui over reimplementing them from scratch', + }, + messages: { + preferImport: + '"{{name}}" is available in @studiometa/ui. ' + + 'Extend it instead of reimplementing from Base: import { {{name}} } from \'@studiometa/ui\'.', + }, + }, + createOnce(context: RuleContext) { + return { + ClassDeclaration(node: Node) { + check(node, context); + }, + ClassExpression(node: Node) { + check(node, context); + }, + }; + }, +}); + +function check(node: Node, context: RuleContext) { + const name: string = node.id?.name ?? ''; + if (!name || !UI_COMPONENT_NAMES.has(name)) return; + if (!isBaseSubclass(node, context)) return; + // Already importing it from @studiometa/ui — they're extending it properly + if (isImportedFromUI(name, context)) return; + + context.report({ node: node.id ?? node, messageId: 'preferImport', data: { name } }); +} diff --git a/packages/eslint-plugin-ui/src/utils/ast.ts b/packages/eslint-plugin-ui/src/utils/ast.ts new file mode 100644 index 00000000..dcf83c1d --- /dev/null +++ b/packages/eslint-plugin-ui/src/utils/ast.ts @@ -0,0 +1,172 @@ +export type Node = Record; +export type RuleContext = Record; + +export type RuleMeta = { + type?: 'problem' | 'suggestion' | 'layout'; + fixable?: 'code' | 'whitespace'; + hasSuggestions?: boolean; + docs?: { description?: string }; + messages?: Record; +}; + +export function createRule unknown>>(rule: { + meta?: RuleMeta; + createOnce(context: RuleContext): V; +}): { meta?: RuleMeta; createOnce(context: RuleContext): V } { + return rule; +} + +export const UI_PACKAGE = '@studiometa/ui'; +export const TOOLKIT_PACKAGE = '@studiometa/js-toolkit'; + +/** + * All component names exported by @studiometa/ui. + * Keep in sync with packages/ui/index.ts exports. + */ +export const UI_COMPONENT_NAMES = new Set([ + 'Accordion', + 'AccordionItem', + 'Action', + 'AnchorNav', + 'AnchorNavLink', + 'AnchorNavTarget', + 'CircularMarquee', + 'Cursor', + 'DataBind', + 'DataComputed', + 'DataEffect', + 'DataModel', + 'Draggable', + 'Figure', + 'FigureVideo', + 'Frame', + 'FrameAnchor', + 'FrameForm', + 'FrameTarget', + 'FrameLoader', + 'Hoverable', + 'LargeText', + 'LazyInclude', + 'Menu', + 'MenuBtn', + 'MenuList', + 'Modal', + 'Panel', + 'AbstractPrefetch', + 'PrefetchWhenOver', + 'PrefetchWhenVisible', + 'AbstractScrollAnimation', + 'ScrollAnimationTimeline', + 'ScrollAnimationTarget', + 'ScrollReveal', + 'Sentinel', + 'Slider', + 'SliderItem', + 'SliderDrag', + 'Sticky', + 'Tabs', + 'Transition', +]); + +/** + * Returns the local name of the superclass for a class declaration/expression. + */ +export function getSuperClassName(node: Node): string | null { + if (node.type !== 'ClassDeclaration' && node.type !== 'ClassExpression') return null; + if (!node.superClass) return null; + if (node.superClass.type === 'Identifier') return node.superClass.name; + return null; +} + +/** + * Returns true when a class extends Base (directly or via a toolkit import). + */ +export function isBaseSubclass(node: Node, context: RuleContext): boolean { + const superName = getSuperClassName(node); + if (!superName) return false; + if (superName === 'Base') return true; + + const sourceCode = context.sourceCode ?? context.getSourceCode?.(); + const ast = sourceCode?.ast; + if (!ast) return false; + + for (const n of ast.body) { + if (n.type !== 'ImportDeclaration') continue; + if (n.source.value !== TOOLKIT_PACKAGE) continue; + for (const specifier of n.specifiers) { + if (specifier.type === 'ImportSpecifier' && specifier.local.name === superName) return true; + } + } + + return false; +} + +/** + * Returns true when a class extends a component imported from @studiometa/ui. + */ +export function isUISubclass(node: Node, context: RuleContext): boolean { + const superName = getSuperClassName(node); + if (!superName) return false; + + const sourceCode = context.sourceCode ?? context.getSourceCode?.(); + const ast = sourceCode?.ast; + if (!ast) return false; + + for (const n of ast.body) { + if (n.type !== 'ImportDeclaration') continue; + if (n.source.value !== UI_PACKAGE) continue; + for (const specifier of n.specifiers) { + if (specifier.type === 'ImportSpecifier' && specifier.local.name === superName) return true; + } + } + + return false; +} + +/** + * Returns true when className is already imported from @studiometa/ui. + */ +export function isImportedFromUI(className: string, context: RuleContext): boolean { + const sourceCode = context.sourceCode ?? context.getSourceCode?.(); + const ast = sourceCode?.ast; + if (!ast) return false; + + for (const n of ast.body) { + if (n.type !== 'ImportDeclaration') continue; + if (n.source.value !== UI_PACKAGE) continue; + for (const specifier of n.specifiers) { + if (specifier.type === 'ImportSpecifier' && specifier.imported?.name === className) + return true; + } + } + + return false; +} + +/** + * Walks up ancestor nodes to find the nearest class declaration/expression. + */ +export function findEnclosingClass(ancestors: Node[]): Node | null { + for (let i = ancestors.length - 1; i >= 0; i--) { + const node = ancestors[i]; + if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') return node; + } + return null; +} + +export function getAncestors(context: RuleContext, node: Node): Node[] { + return context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? []; +} + +/** + * Returns all method definitions in a class body by name. + */ +export function getClassMethods(classNode: Node): Map { + const methods = new Map(); + for (const member of classNode.body?.body ?? []) { + if (member.type === 'MethodDefinition' && member.key?.type === 'Identifier') { + methods.set(member.key.name, member); + } + } + return methods; +} diff --git a/packages/eslint-plugin-ui/src/utils/rule-tester.ts b/packages/eslint-plugin-ui/src/utils/rule-tester.ts new file mode 100644 index 00000000..a9f6bf55 --- /dev/null +++ b/packages/eslint-plugin-ui/src/utils/rule-tester.ts @@ -0,0 +1,22 @@ +import { RuleTester } from 'eslint'; +import { eslintCompatPlugin } from '@oxlint/plugins'; + +export { RuleTester }; + +const _tester = new RuleTester({ + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, +}); + +export const tester = { + run( + name: string, + rule: Parameters[0]['rules'][string], + tests: Parameters['run']>[2], + ) { + const wrapped = eslintCompatPlugin({ rules: { [name]: rule } }); + _tester.run(name, wrapped.rules[name] as any, tests); + }, +}; diff --git a/packages/eslint-plugin-ui/tsconfig.json b/packages/eslint-plugin-ui/tsconfig.json new file mode 100644 index 00000000..b99b8425 --- /dev/null +++ b/packages/eslint-plugin-ui/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "noEmit": false, + "emitDeclarationOnly": true, + "declaration": true, + "allowImportingTsExtensions": true + }, + "include": ["./src/**/*.ts"], + "exclude": ["dist", "node_modules", "**/*.test.ts"] +} diff --git a/packages/eslint-plugin-ui/vitest.config.ts b/packages/eslint-plugin-ui/vitest.config.ts new file mode 100644 index 00000000..5c5ff27b --- /dev/null +++ b/packages/eslint-plugin-ui/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['src/**/*.test.ts'], + coverage: { + provider: 'v8', + include: ['src/**/*.ts'], + exclude: ['src/**/*.test.ts', 'src/utils/rule-tester.ts'], + }, + }, +}); diff --git a/tsconfig.json b/tsconfig.json index 1d1efc9b..48d83714 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "noEmit": true, "noImplicitThis": true, "esModuleInterop": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "skipLibCheck": true, "paths": { "#private/*": ["./packages/ui/*"],