Audit TypeScript lint migration to Biome

This commit is contained in:
murashit 2026-06-24 14:22:24 +09:00
parent 321323532b
commit ce99e76a2b
8 changed files with 37 additions and 788 deletions

View file

@ -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": {

View file

@ -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

View file

@ -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,
},
},
{

498
package-lock.json generated
View file

@ -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"
}
}
}
}

View file

@ -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",

View file

@ -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" },
];
}

View file

@ -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"));
}

View file

@ -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<string, string>): Promise<string> {
return cwd;
}
async function writeImportCycleFixture(cwd: string, files: Record<string, string>): Promise<void> {
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,