--- description: scripts/project-lint.ts is the repo-local plugin "project" holding the repo's rule list (typed DummyRuleMap) and repo-only rules; never published, no tests --- Every repo has `scripts/project-lint.ts`, copied from `cunarist/eslint-plugin-explicit-types` and extended. It is a repo-local Oxlint plugin with `meta.name` `"project"`, loaded through `jsPlugins` in `vite.config.ts`. It holds: - **`configs.project.rules`**: the repo's whole rule list beyond the published plugin configs: Deno parity rules, extras, per-rule options, and opt-outs, built-in Oxlint ids included (`curly`, `id-match`, `import/no-cycle`, ...). - **Custom rules**, enabled as `project/`. Every repo has `no-disable-comments` and `no-asset-imports`. Since 2026-10-19 every repo also has `prefer-ascii`, `no-nan` (a "no number" value is `null`, never `NaN`), and the hugoalh `recommended` ports from eslint-plugin-import-structure's own lint: `no-duplicate-typeofs`, `no-empty-yield`, `no-unsafe-number`, `no-useless-continue`, `no-duplicate-interfaces`, `no-duplicate-types`, `no-useless-type`, and `no-modifier-private`. `no-duplicate-awaits` and `no-duplicate-voids` are covered by `unicorn/no-unnecessary-await` and `typescript/no-meaningless-void-operator`, and `no-enum` by explicit-types. `scripts/` stays flat; don't add `scripts/rules/`. All rules live in the one `scripts/project-lint.ts` (no file splitting), next to the plugin object and the rule list. ```ts import type { DummyRuleMap } from "vite-plus/lint"; const rules: DummyRuleMap = { "id-match": ["error", "^_*[^_]*_*$|^[^a-z]*$", {/* ... */}], curly: "error", "project/no-disable-comments": "error", }; const plugin = { meta: { name: "project" }, rules: { "no-disable-comments": noDisableCommentsRule /* ... */ }, configs: { project: { rules } }, }; export default plugin; ``` ## Why `DummyRuleMap` and an unannotated plugin (decided 2026-10-15) - `DummyRuleMap` (re-exported by `vite-plus/lint`) is Oxlint's type for `OxlintConfig.rules`. Despite the name it lists every built-in rule with its options, so a typo in a rule id or option is a type error, not just a CI failure. Without it, TypeScript widens `"error"` to `string`; `as const` doesn't help because Oxlint wants mutable tuples. - `@eslint/core`'s `Plugin` is too loose to read back: `configs` is optional, any key may be missing, and a config may be an array, so `projectLint.configs.project.rules` fails in `vite.config.ts` (and `!` is banned). Optional chaining still leaves the array case. - Oxlint's own `Plugin` type (`vite-plus/lint/plugins`) has no `configs`. - Two hand-written interfaces worked but were dropped as noise; a plain config object was rejected because custom rules need the plugin form anyway and every repo should look the same. `scripts/` is tooling, not production code: never packed or published, and no tests. Its offs sit in the Node code entry of `projectOverrides`; see [[lint-config]].