From ce99e76a2b648e6a04082205e0cd56aa91b2d734 Mon Sep 17 00:00:00 2001 From: murashit Date: Wed, 24 Jun 2026 14:22:24 +0900 Subject: [PATCH] Audit TypeScript lint migration to Biome --- biome.jsonc | 17 +- docs/development.md | 2 +- eslint.config.mjs | 33 +- package-lock.json | 498 ---------------------- package.json | 1 - scripts/check.mjs | 1 - scripts/lint/check-import-cycles.mjs | 213 --------- tests/scripts/development-scripts.test.ts | 60 --- 8 files changed, 37 insertions(+), 788 deletions(-) delete mode 100644 scripts/lint/check-import-cycles.mjs diff --git a/biome.jsonc b/biome.jsonc index d67495d0..13cb00ea 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -25,7 +25,22 @@ "linter": { "rules": { "preset": "recommended", - "a11y": "info" + "a11y": "info", + "correctness": { + "useExhaustiveDependencies": { + "level": "error", + "options": { + "reportUnnecessaryDependencies": false + } + }, + "useHookAtTopLevel": "error" + }, + "nursery": { + "noFloatingPromises": "error" + }, + "suspicious": { + "noImportCycles": "error" + } } }, "css": { diff --git a/docs/development.md b/docs/development.md index cd259a66..18ee8cbb 100644 --- a/docs/development.md +++ b/docs/development.md @@ -12,7 +12,7 @@ npm run check Run `npm run format` after edits and before `npm run check` so Biome formatting issues are fixed upfront. `npm run check` runs TypeScript type checking, unit tests, lint checks, Biome format check, CSS verification, and the production esbuild bundle. `npm run check:ci` uses the same checks with CI cache settings for TypeScript, tests, and ESLint. Use `npm run build` when you need to refresh Obsidian-loaded assets without running the full check. -Formatting and general JavaScript/TypeScript/JSON linting and JavaScript/TypeScript/CSS/JSON formatting are handled by Biome. The Biome lint configuration explicitly uses the recommended preset, with accessibility diagnostics downgraded to info while the Obsidian-specific UI semantics are reviewed rule by rule. CSS is formatted by Biome but excluded from Biome linting so CSS lint policy stays centralized in Stylelint and the project CSS usage script. ESLint remains for rules that need TypeScript parser services, React Hooks analysis, Obsidian text checks, or Codex Panel responsibility-boundary checks. +Formatting and general JavaScript/TypeScript/JSON linting and JavaScript/TypeScript/CSS/JSON formatting are handled by Biome. The Biome lint configuration explicitly uses the recommended preset, with accessibility diagnostics downgraded to info while the Obsidian-specific UI semantics are reviewed rule by rule. Biome also owns floating Promise, import cycle, and Preact hook dependency/order checks. CSS is formatted by Biome but excluded from Biome linting so CSS lint policy stays centralized in Stylelint and the project CSS usage script. ESLint remains for unsafe-any TypeScript parser-service checks, Obsidian plugin API/text policy checks, and Codex Panel responsibility-boundary checks. ## Generated and Loaded Files diff --git a/eslint.config.mjs b/eslint.config.mjs index b5474513..4152ea75 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,5 @@ import { defineConfig } from "eslint/config"; import obsidianmd from "eslint-plugin-obsidianmd"; -import reactHooks from "eslint-plugin-react-hooks"; import codexPanelEslintPlugin from "./scripts/lint/eslint-plugin-codex-panel.mjs"; import tseslint from "typescript-eslint"; @@ -8,18 +7,12 @@ const typeScriptFiles = ["src/**/*.{ts,tsx}", "tests/**/*.{ts,tsx}"]; const nodeJavaScriptFiles = ["*.mjs", "scripts/**/*.mjs"]; const typeScriptConfigFiles = ["*.config.ts"]; const lintedTypeScriptFiles = [...typeScriptFiles, ...typeScriptConfigFiles]; -const unsafeTypeScriptRules = { +const unsafeAnyTypeScriptRules = { "@typescript-eslint/no-unsafe-argument": "error", "@typescript-eslint/no-unsafe-assignment": "error", "@typescript-eslint/no-unsafe-call": "error", "@typescript-eslint/no-unsafe-member-access": "error", }; -const projectTypeScriptRules = { - "@typescript-eslint/no-floating-promises": "error", - ...unsafeTypeScriptRules, - "react-hooks/exhaustive-deps": "error", - "react-hooks/rules-of-hooks": "error", -}; const testTypeScriptRelaxations = { "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-call": "off", @@ -236,19 +229,33 @@ function restrictedSyntaxRule(restrictions) { }; } +function obsidianRecommendedConfig(config) { + const rules = Object.fromEntries(Object.entries(config.rules ?? {}).filter(([ruleName]) => ruleName.startsWith("obsidianmd/"))); + if (Object.keys(rules).length === 0) return null; + const obsidianConfig = { + basePath: "src", + rules, + }; + if (config.files) obsidianConfig.files = config.files; + if (config.ignores) obsidianConfig.ignores = config.ignores; + return obsidianConfig; +} + export default defineConfig([ { ignores: ["main.js", "node_modules/**", "src/generated/**"], }, - ...obsidianmd.configs.recommended.map((config) => ({ - ...config, + { basePath: "src", - })), + plugins: { + obsidianmd, + }, + }, + ...obsidianmd.configs.recommended.map(obsidianRecommendedConfig).filter(Boolean), { files: lintedTypeScriptFiles, plugins: { "@typescript-eslint": tseslint.plugin, - "react-hooks": reactHooks, }, languageOptions: { parser: tseslint.parser, @@ -272,7 +279,7 @@ export default defineConfig([ }, }, rules: { - ...projectTypeScriptRules, + ...unsafeAnyTypeScriptRules, }, }, { diff --git a/package-lock.json b/package-lock.json index 1c717157..1ff5954c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "esbuild": "^0.28.0", "eslint": "^10.4.1", "eslint-plugin-obsidianmd": "^0.3.0", - "eslint-plugin-react-hooks": "^7.1.1", "jsdom": "^29.1.1", "knip": "^6.16.1", "obsidian": "~1.12.3", @@ -98,176 +97,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-validator-identifier": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", @@ -278,94 +107,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@biomejs/biome": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.1.tgz", @@ -1653,38 +1394,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -1692,17 +1401,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@keyv/serialize": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", @@ -3489,19 +3187,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.32", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", - "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -3538,40 +3223,6 @@ "node": ">=8" } }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, "node_modules/cacheable": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz", @@ -3656,27 +3307,6 @@ "node": ">=6" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -4141,13 +3771,6 @@ "node": ">= 0.4" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.362", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.362.tgz", - "integrity": "sha512-PUY2DrLvkjkUuWqq+KPL2iWshrJsZOcIojzRQ7eXFacc9dWga7MGMJAa15VbiejSZB1PAXaRLAiKgruHP8LB1w==", - "dev": true, - "license": "ISC" - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -5229,26 +4852,6 @@ "dev": true, "license": "MIT" }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", - "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" - } - }, "node_modules/eslint-plugin-security": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-2.1.1.tgz", @@ -5689,16 +5292,6 @@ "node": ">= 0.4" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -6031,23 +5624,6 @@ "node": ">= 0.4" } }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, "node_modules/hookified": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", @@ -6693,19 +6269,6 @@ } } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -7853,16 +7416,6 @@ "semver": "bin/semver.js" } }, - "node_modules/node-releases": { - "version": "2.0.46", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", - "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -9855,37 +9408,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "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", @@ -10365,13 +9887,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", @@ -10480,19 +9995,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } } } } diff --git a/package.json b/package.json index 5261f9c6..16b56b48 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "esbuild": "^0.28.0", "eslint": "^10.4.1", "eslint-plugin-obsidianmd": "^0.3.0", - "eslint-plugin-react-hooks": "^7.1.1", "jsdom": "^29.1.1", "knip": "^6.16.1", "obsidian": "~1.12.3", diff --git a/scripts/check.mjs b/scripts/check.mjs index 62579678..def2528d 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -54,7 +54,6 @@ function lintCommands({ ciMode, namePrefix = "" }) { }, { name: `${namePrefix}css`, command: 'stylelint "src/**/*.css" --max-warnings=0' }, { name: `${namePrefix}css-usage`, command: "node scripts/lint/check-css-usage.mjs" }, - { name: `${namePrefix}deps`, command: "node scripts/lint/check-import-cycles.mjs" }, { name: `${namePrefix}unused`, command: "knip --no-progress" }, ]; } diff --git a/scripts/lint/check-import-cycles.mjs b/scripts/lint/check-import-cycles.mjs deleted file mode 100644 index e826ab41..00000000 --- a/scripts/lint/check-import-cycles.mjs +++ /dev/null @@ -1,213 +0,0 @@ -import path from "node:path"; -import ts from "typescript"; - -if (process.argv.length > 2) { - console.error("Usage: node scripts/lint/check-import-cycles.mjs"); - process.exit(1); -} - -const cwd = process.cwd(); -const configPath = ts.findConfigFile(cwd, ts.sys.fileExists, "tsconfig.json"); -if (!configPath) { - console.error("Could not find tsconfig.json."); - process.exit(1); -} - -const configFile = ts.readConfigFile(configPath, ts.sys.readFile); -if (configFile.error) { - reportDiagnostic(configFile.error); - process.exit(1); -} - -const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(configPath)); -if (parsedConfig.errors.length > 0) { - for (const diagnostic of parsedConfig.errors) reportDiagnostic(diagnostic); - process.exit(1); -} - -const sourceFiles = parsedConfig.fileNames.map((file) => path.resolve(file)).filter((file) => isCheckedSourceFile(relativePath(file))); -const sourceFileSet = new Set(sourceFiles); -const compilerHost = ts.createCompilerHost(parsedConfig.options, true); -const moduleResolutionCache = ts.createModuleResolutionCache(cwd, (file) => file, parsedConfig.options); -const graph = new Map(sourceFiles.map((file) => [relativePath(file), new Set()])); - -for (const sourceFile of sourceFiles) { - const sourceText = ts.sys.readFile(sourceFile); - if (sourceText === undefined) continue; - - const parsedSource = ts.createSourceFile(sourceFile, sourceText, parsedConfig.options.target ?? ts.ScriptTarget.ES2022, true); - const from = relativePath(sourceFile); - - for (const edge of importEdges(parsedSource)) { - const resolved = ts.resolveModuleName( - edge.specifier, - sourceFile, - parsedConfig.options, - compilerHost, - moduleResolutionCache, - ).resolvedModule; - if (!resolved) continue; - - const resolvedFile = path.resolve(resolved.resolvedFileName); - if (!sourceFileSet.has(resolvedFile)) continue; - - graph.get(from)?.add(relativePath(resolvedFile)); - } -} - -const cycleComponents = stronglyConnectedComponents(graph) - .filter((component) => component.length > 1 || hasSelfDependency(graph, component[0])) - .sort((left, right) => left[0].localeCompare(right[0])); - -if (cycleComponents.length === 0) { - process.exit(0); -} - -console.error(`Import cycle check failed: found ${cycleComponents.length} cycle${cycleComponents.length === 1 ? "" : "s"}.`); -for (const [index, component] of cycleComponents.entries()) { - const cycle = sampleCycle(graph, component); - console.error(`${index + 1}. ${cycle.join(" -> ")}`); -} -process.exit(1); - -function importEdges(sourceFile) { - const edges = []; - - for (const statement of sourceFile.statements) { - if (ts.isImportDeclaration(statement) && hasRuntimeImportEdge(statement) && stringLiteralText(statement.moduleSpecifier)) { - edges.push({ - specifier: stringLiteralText(statement.moduleSpecifier), - }); - } else if ( - ts.isExportDeclaration(statement) && - hasRuntimeExportEdge(statement) && - statement.moduleSpecifier && - stringLiteralText(statement.moduleSpecifier) - ) { - edges.push({ - specifier: stringLiteralText(statement.moduleSpecifier), - }); - } - } - - return edges; -} - -function hasRuntimeImportEdge(statement) { - const importClause = statement.importClause; - if (!importClause) return true; - if (importClause.isTypeOnly) return false; - if (importClause.name) return true; - - const namedBindings = importClause.namedBindings; - if (!namedBindings) return false; - if (ts.isNamespaceImport(namedBindings)) return true; - return namedBindings.elements.some((element) => !element.isTypeOnly); -} - -function hasRuntimeExportEdge(statement) { - if (statement.isTypeOnly) return false; - - const exportClause = statement.exportClause; - if (!exportClause) return true; - if (ts.isNamespaceExport(exportClause)) return true; - return exportClause.elements.some((element) => !element.isTypeOnly); -} - -function stringLiteralText(node) { - return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : null; -} - -function stronglyConnectedComponents(graph) { - let nextIndex = 0; - const stack = []; - const onStack = new Set(); - const indices = new Map(); - const lowLinks = new Map(); - const components = []; - - for (const node of graph.keys()) { - if (!indices.has(node)) visit(node); - } - - return components.map((component) => component.sort()); - - function visit(node) { - indices.set(node, nextIndex); - lowLinks.set(node, nextIndex); - nextIndex += 1; - stack.push(node); - onStack.add(node); - - for (const dependency of graph.get(node) ?? []) { - if (!indices.has(dependency)) { - visit(dependency); - lowLinks.set(node, Math.min(lowLinks.get(node), lowLinks.get(dependency))); - } else if (onStack.has(dependency)) { - lowLinks.set(node, Math.min(lowLinks.get(node), indices.get(dependency))); - } - } - - if (lowLinks.get(node) !== indices.get(node)) return; - - const component = []; - let item; - do { - item = stack.pop(); - onStack.delete(item); - component.push(item); - } while (item !== node); - components.push(component); - } -} - -function sampleCycle(graph, component) { - const componentSet = new Set(component); - - for (const start of component) { - if (graph.get(start)?.has(start)) return [start, start]; - - for (const dependency of graph.get(start) ?? []) { - if (!componentSet.has(dependency)) continue; - const pathToStart = findPath(graph, componentSet, dependency, start, new Set([start])); - if (pathToStart) return [start, ...pathToStart]; - } - } - - return component; -} - -function findPath(graph, allowedNodes, current, target, seen) { - if (current === target) return [current]; - if (seen.has(current)) return null; - seen.add(current); - - for (const dependency of graph.get(current) ?? []) { - if (!allowedNodes.has(dependency)) continue; - const path = findPath(graph, allowedNodes, dependency, target, seen); - if (path) return [current, ...path]; - } - - return null; -} - -function hasSelfDependency(graph, node) { - return Boolean(node && graph.get(node)?.has(node)); -} - -function isCheckedSourceFile(file) { - return ( - file.startsWith("src/") && - !file.startsWith("src/generated/") && - (file.endsWith(".ts") || file.endsWith(".tsx")) && - !file.endsWith(".d.ts") - ); -} - -function relativePath(file) { - return path.relative(cwd, file).split(path.sep).join("/"); -} - -function reportDiagnostic(diagnostic) { - console.error(ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")); -} diff --git a/tests/scripts/development-scripts.test.ts b/tests/scripts/development-scripts.test.ts index 785ac2c5..bf374adf 100644 --- a/tests/scripts/development-scripts.test.ts +++ b/tests/scripts/development-scripts.test.ts @@ -116,34 +116,6 @@ describe("development scripts", () => { expect(report.failures).toEqual([]); }); - it("detects runtime import cycles", async () => { - const cwd = await tempWorkspace(); - await writeImportCycleFixture(cwd, { - "src/a.ts": 'import { b } from "./b";\nexport const a = b;\n', - "src/b.ts": 'import { a } from "./a";\nexport const b = a;\n', - }); - - const result = runNodeScript("scripts/lint/check-import-cycles.mjs", [], cwd); - - expect(result.status).toBe(1); - expect(result.stderr).toContain("found 1 cycle"); - expect(result.stderr).toContain("src/a.ts -> src/b.ts -> src/a.ts"); - }); - - it("ignores type-only import cycles", async () => { - const cwd = await tempWorkspace(); - await writeImportCycleFixture(cwd, { - "src/a.ts": 'import type { B } from "./b";\nexport interface A { b?: B }\n', - "src/b.ts": 'import type { A } from "./a";\nexport interface B { a?: A }\n', - }); - - const result = runNodeScript("scripts/lint/check-import-cycles.mjs", [], cwd); - - expect(result.status).toBe(0); - expect(result.stdout).toBe(""); - expect(result.stderr).toBe(""); - }); - it("keeps CSS usage checks quiet when every class is used", async () => { const cwd = await cssUsageFixture({ "src/styles/10-component.css": ".codex-panel__used { display: block; }\n", @@ -177,20 +149,6 @@ describe("development scripts", () => { expect(result.stderr).toContain("codex-panel__unused"); }); - it("detects import cycles when mixed imports include runtime values", async () => { - const cwd = await tempWorkspace(); - await writeImportCycleFixture(cwd, { - "src/a.ts": 'import { type B, b } from "./b";\nexport interface A { b?: B }\nexport const a = b;\n', - "src/b.ts": 'import { a, type A } from "./a";\nexport interface B { a?: A }\nexport const b = a;\n', - }); - - const result = runNodeScript("scripts/lint/check-import-cycles.mjs", [], cwd); - - expect(result.status).toBe(1); - expect(result.stderr).toContain("found 1 cycle"); - expect(result.stderr).toContain("src/a.ts -> src/b.ts -> src/a.ts"); - }); - it("fails release prepare before changing version files when release notes already exist", async () => { const cwd = await tempWorkspace(); await mkdir(path.join(cwd, ".github", "release-notes"), { recursive: true }); @@ -244,24 +202,6 @@ async function cssUsageFixture(files: Record): Promise { return cwd; } -async function writeImportCycleFixture(cwd: string, files: Record): Promise { - await writeJson(path.join(cwd, "tsconfig.json"), { - compilerOptions: { - target: "ES2022", - module: "ESNext", - moduleResolution: "Bundler", - verbatimModuleSyntax: true, - strict: true, - }, - include: ["src/**/*.ts"], - }); - - for (const [file, source] of Object.entries(files)) { - await mkdir(path.dirname(path.join(cwd, file)), { recursive: true }); - await writeFile(path.join(cwd, file), source); - } -} - function runNodeScript(script: string, args: string[] = [], cwd = repoRoot, env: NodeJS.ProcessEnv = {}) { return spawnSync(process.execPath, [path.join(repoRoot, script), ...args], { cwd,