Skip to main content

oxlint-typescript-conventions

WHAT: Write TypeScript under `lib/node/`, `tests/`, and `config/pi/` that passes the repo's strict `oxlint --type-aware` configuration on the first try, so the husky pre-commit hook does not reject the commit. WHEN: User asks to add or edit a `.ts` / `.spec.ts` file anywhere outside `config/pi/extensions/` (the extensions tree has a relaxed override). DO-NOT: Bypass the rules with `// oxlint-disable` instead of fixing the code.

インストールへ移動

ソース情報

リポジトリ
weikinhuang/dotfiles
ソースの最終更新活動
2026年6月23日 23:21
検出された SKILL.md の言語
英語
スター
21
フォーク
3

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
oxlint-typescript-conventions
description
WHAT: Write TypeScript under `lib/node/`, `tests/`, and `config/pi/` that passes the repo's strict `oxlint --type-aware` configuration on the first try, so the husky pre-commit hook does not reject the commit. WHEN: User asks to add or edit a `.ts` / `.spec.ts` file anywhere outside `config/pi/extensions/` (the extensions tree has a relaxed override). DO-NOT: Bypass the rules with `// oxlint-disable` instead of fixing the code.
# oxlint TypeScript conventions This repo runs `oxlint --type-aware` with `correctness: error` and `perf: error` plus the full `typescript` plugin (see [`oxlint.config.ts`](../../../oxlint.config.ts)). The husky pre-commit hook runs `lint-staged`, which invokes `oxlint` against every staged `.ts` and rejects the commit on any error. The rules are non-trivially strict; new code that passes `tsc` will routinely fail oxlint unless it is written with these patterns from the start. The `config/pi/extensions/**` tree - plus the shared pi-importing helpers under `lib/node/pi/ext/**` and their specs under `tests/lib/node/pi/ext/**` - has a relaxed override (most `no-unsafe-*` and `require-await` rules turned off) because those files import from `@earendil-works/*` (pi runtime / TUI / AI APIs). **This skill applies to everything else** — `lib/node/**` (except `lib/node/pi/ext/`), `tests/**` (except `tests/lib/node/pi/ext/`), `config/pi/*.ts` (e.g. `session-usage.ts`), and any future top-level `*.ts`. ## When to use this skill Apply whenever the user asks to: - Add or edit a `.ts` file under `lib/node/`. - Add or edit a `.spec.ts` under `tests/lib/`, `tests/config/`, or anywhere else vitest picks up. - Add a TypeScript helper that other extensions consume. Skip this skill for `.ts` files inside `config/pi/extensions/` (different rule set), `*.d.ts` type shims, and generated files. The `commit-message` and `lint-and-test-gate` skills are still the final word on subject lines and verification — this one only governs the source style. ## The pre-commit gate ```sh $ git commit ... ✖ Task killed: oxlint-tsgolint ✖ Task killed: oxfmt --no-error-on-unmatched-pattern husky - pre-commit script failed (code 1) ``` Run `npx oxlint --type-aware <file-or-dir>` against your changes BEFORE `git commit`. The pre-commit hook does the same thing; running it manually saves the round trip. For format issues separately: `npx oxfmt --check <files>` (and `npx oxfmt <files>` to fix in place). ## Patterns that trip the gate ### `typescript/require-await` — async stubs without `await` Tests routinely install async callbacks for type compatibility, then never `await`. oxlint flags this; rewrite as a non-async `() => Promise.resolve(...)`. ```ts // ✗ Fails: async with no await. installSandboxWrapper(async (cmd) => ({ command: `srt -- ${cmd}`, wrapped: true })); // ✓ Use Promise.resolve so the function stays non-async. installSandboxWrapper((cmd) => Promise.resolve({ command: `srt -- ${cmd}`, wrapped: true })); ``` The rule is keyed off the literal `async` keyword; an async function with NO `await` is the trigger. A real async helper that genuinely uses `await` passes. ### `typescript/prefer-includes` — `.test(literal)` on a substring `x.includes('foo')` is preferred over `/foo/.test(x)` whenever the regex is just a literal substring with no metacharacters. oxlint flags the `.test()` form even inside test assertions. ```ts // ✗ Fails: regex carries no metacharacters. expect(warnings.find((w) => /silently drop/.test(w.reason))).toBeDefined(); // ✓ Substring match. expect(warnings.find((w) => w.reason.includes('silently drop'))).toBeDefined(); ``` A real regex (anchors, character classes, alternation) is fine — keep `/^foo$/.test(s)` as-is. ### `eslint/no-use-before-define` — declare before first use Function and variable declarations must precede their first reference in source order. ESLint's `no-use-before-define` is on with the default config, which means the standard "helpers at the bottom" Python style does not work. ```ts // ✗ Fails: uniqueSorted used before defined. export function compileLinuxRules(...) { return { paths: uniqueSorted([...paths]), ... }; } function uniqueSorted(input: string[]): string[] { return [...new Set(input)].sort(); } // ✓ Define helpers above their first call. function uniqueSorted(input: string[]): string[] { return [...new Set(input)].sort(); } export function compileLinuxRules(...) { return { paths: uniqueSorted([...paths]), ... }; } ``` ### `typescript/explicit-function-return-type` — annotate test helpers `allowExpressions: true` is set, so arrow functions passed inline (`map((x) => x + 1)`) are fine. Top-level declarations and named factory helpers in tests are NOT, even when the return type is inferable. ```ts // ✗ Fails: top-level helper. function makeUI(over = {}) { return { hasUI: true, ... }; } // ✓ Annotate the return type. function makeUI(over: Partial<UIBridge> = {}): UIBridge { return { hasUI: true, ... }; } // ✗ Fails: test-local factory. const noop = () => undefined; // ✓ Annotate. const noop = (): undefined => undefined; ``` ### `typescript/no-unsafe-assignment` / `no-unsafe-member-access` — narrow `unknown` properly `Array.isArray(value)` narrows to `any[]`, and `JSON.parse(...)` returns `any`. Either widens to a typed shape with an explicit `as unknown[]` / `as Record<string, unknown>` step before iterating, or parses through a typed schema. ```ts // ✗ Fails: `value` after Array.isArray() is `any[]`, item is `any`. if (!Array.isArray(value)) return; for (let i = 0; i < value.length; i++) { const item = value[i]; if (typeof item !== 'string') continue; out.push(item); } // ✓ Re-cast through unknown[] so item is `unknown`. if (!Array.isArray(value)) return; const arr = value as unknown[]; for (let i = 0; i < arr.length; i++) { const item = arr[i]; if (typeof item !== 'string') continue; out.push(item); } // ✗ Fails: JSON.parse returns any, .command access is unsafe. expect(JSON.parse(line).command).toBe('first'); // ✓ Type the parse target. const rec = JSON.parse(line) as { command: string }; expect(rec.command).toBe('first'); ``` ### `typescript/consistent-indexed-object-style` — prefer `Record<K, V>` `{ [k: symbol]: unknown }` index signatures are flagged in favor of `Record<symbol, unknown>`. ```ts // ✗ Fails. const g = globalThis as unknown as { [k: symbol]: unknown }; // ✓ const g = globalThis as unknown as Record<symbol, unknown>; ``` ### `eslint/no-empty-function` — sentinel for `let cb = () => {}` A bare `() => {}` placeholder for a callback that's about to be reassigned trips the rule. Either use a named sentinel function (with a comment explaining the unreachability) or restructure to avoid the placeholder. ```ts // ✗ Fails. let resolveFn: () => void = () => {}; const next = new Promise<void>((resolve) => { resolveFn = resolve; }); return resolveFn; // ✓ Named sentinel. function noopResolve(): void { /* unreachable - reassigned by Promise ctor */ } let resolveFn: () => void = noopResolve; const next = new Promise<void>((resolve) => { resolveFn = resolve; }); return resolveFn; ``` ### `eslint/no-unused-vars` — drop unused imports Particularly easy to hit when refactoring: an `import { foo, bar }` line where one of the names is no longer referenced. The rule respects the `^_` ignore prefix for arguments and destructured elements but NOT for imports — drop the unused name from the import list. ## Workflow 1. Write the code naturally; run `npm run tsc` first to confirm types are sound. 2. Run `npx oxlint --type-aware <changed-paths>` (paths or directories — both work). 3. Fix every error reported. The patterns above cover ~95% of what trips on a fresh helper. 4. Run `npx oxfmt --check <changed-paths>`; run without `--check` to format if it complained. 5. Stage and commit. The pre-commit hook re-runs both — your local pass guarantees a clean commit. ## Anti-patterns - **Disabling rules with `// oxlint-disable-next-line ...`.** Almost never the right answer in this repo; the rules above all have a clean-code form. Reach for the disable comment only when a third- party type forces the unsafe pattern, and document why in the disable comment. - **Casting through `Record<string, unknown>` reflexively.** The `as unknown[]` / explicit-shape approach narrows correctly without adding a load-bearing assertion. Casts that hide a real bug are a real maintenance hazard — when in doubt, validate the shape with a guard. - **Skipping `oxlint` locally** because "the pre-commit hook will catch it." It will, by aborting your commit mid-message-typing. Run it yourself first. - **Matching the relaxed `config/pi/extensions/` posture in `lib/`** because "extensions get away with it." Lib helpers are the typed surface that extensions consume; tightening lib while extensions stay loose is the deliberate boundary. ## References - [`oxlint.config.ts`](../../../oxlint.config.ts) — every rule in force, plus the `config/pi/extensions/**` override. - [oxlint TypeScript rules index](https://oxc.rs/docs/guide/usage/linter/rules.html) — the upstream rule docs for the messages above. - [`lint-and-test-gate`](../lint-and-test-gate/SKILL.md) — when to run lint vs tests vs both. - [`commit-message`](../commit-message/SKILL.md) — what to put in the commit subject after the gate is green.
GitHubで見る