mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Tighten Biome lint policy
This commit is contained in:
parent
79a2c7fcbf
commit
ca26a53e1b
15 changed files with 86 additions and 35 deletions
|
|
@ -44,9 +44,13 @@
|
|||
"useHookAtTopLevel": "error"
|
||||
},
|
||||
"nursery": {
|
||||
"noFloatingPromises": "error"
|
||||
"noFloatingPromises": "error",
|
||||
"noMisusedPromises": "error",
|
||||
"noUnsafePlusOperands": "error",
|
||||
"useExhaustiveSwitchCases": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noArrayIndexKey": "error",
|
||||
"noImportCycles": "error"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@ npm run format
|
|||
npm run check
|
||||
```
|
||||
|
||||
Run `npm run format` after edits and before `npm run check` so Biome formatting and import organization issues are fixed upfront. `npm run check` runs TypeScript type checking, unit tests, lint checks, Biome format and assist checks, 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.
|
||||
Use this as the normal edit loop. `npm run format` applies Biome formatting and import organization; `npm run check` is the local preflight for type checking, tests, lint, format/assist checks, CSS checks, and the production bundle.
|
||||
|
||||
Formatting, import organization, 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.
|
||||
Use focused scripts for tight loops: `npm run typecheck`, `npm run test`, `npm run lint`, or `npm run build`. The `*:ci` variants match CI cache behavior.
|
||||
|
||||
Biome owns formatting, import organization, and general JavaScript/TypeScript/JSON linting. Biome warnings fail `npm run lint` and `npm run check`; info diagnostics stay advisory, including accessibility while Obsidian-specific UI semantics are reviewed rule by rule. CSS lint policy stays in Stylelint and the CSS usage script. ESLint remains for typed unsafe-any checks, Obsidian plugin policy, and Codex Panel responsibility-boundary rules.
|
||||
|
||||
## Generated and Loaded Files
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ function readMarkdownTableValues(markdown) {
|
|||
const header = splitMarkdownTableRow(lines[index]);
|
||||
if (!header) continue;
|
||||
const separator = splitMarkdownTableRow(lines[index + 1]);
|
||||
if (!separator || !separator.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
|
||||
if (!separator?.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
|
||||
|
||||
const keyColumn = header.findIndex((cell) => normalizeTableHeader(cell) === "key");
|
||||
const versionColumn = header.findIndex((cell) => normalizeTableHeader(cell) === "version");
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ function lintCommands({ ciMode, namePrefix = "" }) {
|
|||
const eslintCacheArgs = ciMode ? "" : " --cache --cache-strategy content --cache-location node_modules/.cache/eslint/.eslintcache";
|
||||
|
||||
return [
|
||||
{ name: `${namePrefix}biome`, command: "biome lint --no-errors-on-unmatched --diagnostic-level=error" },
|
||||
{ name: `${namePrefix}biome`, command: "biome lint --no-errors-on-unmatched --diagnostic-level=warn --error-on-warnings" },
|
||||
{
|
||||
name: `${namePrefix}eslint`,
|
||||
command: `eslint src tests scripts "*.config.ts" "*.config.mjs" --max-warnings=0${eslintCacheArgs}`,
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ async function readTexts(files) {
|
|||
function locationsInTexts(texts, needle) {
|
||||
const locations = [];
|
||||
for (const { file, text } of texts) {
|
||||
let offset = text.indexOf(needle);
|
||||
const offset = text.indexOf(needle);
|
||||
if (offset === -1) continue;
|
||||
const line = text.slice(0, offset).split("\n").length;
|
||||
locations.push(`${relative(file)}:${line}`);
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ function finiteTimestamp(value: number): number {
|
|||
}
|
||||
|
||||
function recencyAtPatch(thread: ThreadRecord): { recencyAt: number | null } | Record<string, never> {
|
||||
if (!Object.prototype.hasOwnProperty.call(thread, "recencyAt")) return {};
|
||||
if (!Object.hasOwn(thread, "recencyAt")) return {};
|
||||
const value = thread.recencyAt;
|
||||
return { recencyAt: typeof value === "number" && Number.isFinite(value) ? value : null };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ const STREAM_UPDATE_PLANNERS = {
|
|||
"item/reasoning/summaryPartAdded": (state, notification) =>
|
||||
appendToolTextPlan(state, notification.params.itemId, notification.params.turnId, "reasoning", "", "reasoning"),
|
||||
"item/started": (_state, notification) => startedItemPlan(notification.params.item, notification.params.turnId),
|
||||
"item/completed": (state, notification) => completedItemPlan(state, notification.params.item, notification.params.turnId),
|
||||
"item/completed": (_state, notification) => completedItemPlan(notification.params.item, notification.params.turnId),
|
||||
"item/commandExecution/outputDelta": (_state, notification) =>
|
||||
actionPlan({
|
||||
type: "message-stream/item-output-appended",
|
||||
|
|
@ -433,7 +433,7 @@ function startedItemPlan(item: TurnItem, turnId: string): ChatNotificationPlan {
|
|||
return streamItem ? actionPlan({ type: "message-stream/item-upserted", item: streamItem }) : EMPTY_PLAN;
|
||||
}
|
||||
|
||||
function completedItemPlan(state: ChatState, item: TurnItem, turnId: string): ChatNotificationPlan {
|
||||
function completedItemPlan(item: TurnItem, turnId: string): ChatNotificationPlan {
|
||||
if (item.type === "userMessage") return EMPTY_PLAN;
|
||||
const streamItem = messageStreamItemFromTurnItem(item, turnId);
|
||||
if (!streamItem) return EMPTY_PLAN;
|
||||
|
|
|
|||
|
|
@ -312,11 +312,11 @@ function serverRequestScope(request: ServerRequest): MessageScope {
|
|||
}
|
||||
|
||||
function isServerRequest(message: ServerNotification | ServerRequest): message is ServerRequest {
|
||||
return Object.prototype.hasOwnProperty.call(SERVER_REQUEST_SCOPE_EXTRACTORS, message.method);
|
||||
return Object.hasOwn(SERVER_REQUEST_SCOPE_EXTRACTORS, message.method);
|
||||
}
|
||||
|
||||
function isServerNotification(message: ServerNotification | ServerRequest): message is ServerNotification {
|
||||
return Object.prototype.hasOwnProperty.call(SERVER_NOTIFICATION_SCOPE_EXTRACTORS, message.method);
|
||||
return Object.hasOwn(SERVER_NOTIFICATION_SCOPE_EXTRACTORS, message.method);
|
||||
}
|
||||
|
||||
function threadTurnRequestScope(request: { params: { threadId: string; turnId: string | null } }): MessageScope {
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ function threadSettingsValueEqual(left: unknown, right: unknown): boolean {
|
|||
const rightKeys = Object.keys(right);
|
||||
return (
|
||||
leftKeys.length === rightKeys.length &&
|
||||
leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && threadSettingsValueEqual(left[key], right[key]))
|
||||
leftKeys.every((key) => Object.hasOwn(right, key) && threadSettingsValueEqual(left[key], right[key]))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ interface ComposerContextMeterViewModel {
|
|||
percent: string;
|
||||
}
|
||||
|
||||
const COMPOSER_CONTEXT_METER_CELL_IDS = ["context-0", "context-1", "context-2", "context-3"] as const;
|
||||
|
||||
export interface ComposerCallbacks {
|
||||
onInput: (value: string) => void;
|
||||
onUpdateSuggestions: () => void;
|
||||
|
|
@ -341,14 +343,18 @@ function ComposerContextMeter({ context }: { context: ComposerMetaViewModel["con
|
|||
return (
|
||||
<span className="codex-panel__composer-meta-context">
|
||||
<span className="codex-panel__composer-meta-context-dots">
|
||||
{context.cells.map((cell, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className={["codex-panel__composer-meta-context-dot", cell.placeholder ? "is-placeholder" : ""].filter(Boolean).join(" ")}
|
||||
>
|
||||
{cell.text}
|
||||
</span>
|
||||
))}
|
||||
{COMPOSER_CONTEXT_METER_CELL_IDS.map((id, index) => {
|
||||
const cell = context.cells[index];
|
||||
if (!cell) return null;
|
||||
return (
|
||||
<span
|
||||
key={id}
|
||||
className={["codex-panel__composer-meta-context-dot", cell.placeholder ? "is-placeholder" : ""].filter(Boolean).join(" ")}
|
||||
>
|
||||
{cell.text}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
<span className="codex-panel__composer-meta-context-percent">{context.percent}</span>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { ComponentChild as UiNode } from "preact";
|
|||
import { useLayoutEffect, useRef } from "preact/hooks";
|
||||
|
||||
import { renderRawDiffLines } from "../../../../shared/diff/render";
|
||||
import { type DetailSection, type DetailView } from "../../presentation/message-stream/detail-view";
|
||||
import type { DetailSection, DetailView } from "../../presentation/message-stream/detail-view";
|
||||
import type { MessageStreamDisclosureState } from "./context";
|
||||
|
||||
export interface DetailRenderContext {
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
|||
|
||||
import { hasPendingRequests, pendingRequestCountsFromQueues } from "../../../../domain/pending-requests/aggregate";
|
||||
import { approvalDetailsDisclosureId } from "../../domain/pending-requests/disclosure-ids";
|
||||
import {
|
||||
type PendingApprovalViewModel,
|
||||
type PendingMcpElicitationFieldViewModel,
|
||||
type PendingMcpElicitationViewModel,
|
||||
type PendingUserInputQuestionViewModel,
|
||||
type PendingUserInputViewModel,
|
||||
import type {
|
||||
PendingApprovalViewModel,
|
||||
PendingMcpElicitationFieldViewModel,
|
||||
PendingMcpElicitationViewModel,
|
||||
PendingUserInputQuestionViewModel,
|
||||
PendingUserInputViewModel,
|
||||
} from "../../presentation/pending-requests/view-model";
|
||||
import type { PendingRequestBlockActions } from "./context";
|
||||
import { createStatusMessageClassName } from "./status";
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { Fragment, type ComponentChild as UiNode } from "preact";
|
||||
|
||||
import {
|
||||
type MessageStreamActivityItemView,
|
||||
type MessageStreamRenderedItemView,
|
||||
type MessageStreamViewBlock,
|
||||
import type {
|
||||
MessageStreamActivityItemView,
|
||||
MessageStreamRenderedItemView,
|
||||
MessageStreamViewBlock,
|
||||
} from "../../presentation/message-stream/view-model";
|
||||
import type { MessageStreamContext, PendingRequestBlockContext } from "./context";
|
||||
import { detailNode } from "./detail";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type Ref, type ComponentChild as UiNode } from "preact";
|
||||
import type { Ref, ComponentChild as UiNode } from "preact";
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import type { MessageStreamTextView } from "../../presentation/message-stream/text-view";
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ describe("development scripts", () => {
|
|||
[
|
||||
"#!/usr/bin/env node",
|
||||
"import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs';",
|
||||
"appendFileSync('codex-args.txt', `${process.argv.slice(2).join('\\n')}\\n`);",
|
||||
"appendFileSync('codex-args.txt', process.argv.slice(2).join('\\n') + '\\n');",
|
||||
"mkdirSync('src/generated/app-server/v2', { recursive: true });",
|
||||
"writeFileSync('src/generated/app-server/v2/Example.ts', '// GENERATED CODE! DO NOT MODIFY BY HAND!\\nexport type Example = string | null | null;\\n');",
|
||||
].join("\n"),
|
||||
|
|
@ -116,6 +116,28 @@ describe("development scripts", () => {
|
|||
expect(report.failures).toEqual([]);
|
||||
});
|
||||
|
||||
it("runs Biome lint with warning-level diagnostics", async () => {
|
||||
const cwd = await tempWorkspace();
|
||||
const binDir = path.join(cwd, "bin");
|
||||
await mkdir(binDir, { recursive: true });
|
||||
await mkdir(path.join(cwd, "scripts", "lint"), { recursive: true });
|
||||
await writeFile(path.join(cwd, "scripts", "lint", "check-css-usage.mjs"), "");
|
||||
|
||||
await writeCaptureBin(path.join(binDir, "biome"), "biome-args.txt");
|
||||
await writeExitZeroBin(path.join(binDir, "eslint"));
|
||||
await writeExitZeroBin(path.join(binDir, "stylelint"));
|
||||
await writeExitZeroBin(path.join(binDir, "knip"));
|
||||
|
||||
const result = runNodeScript("scripts/check.mjs", ["--lint"], cwd, {
|
||||
PATH: `${binDir}${path.delimiter}${process.env["PATH"] ?? ""}`,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
await expect(readFile(path.join(cwd, "biome-args.txt"), "utf8")).resolves.toBe(
|
||||
"lint\n--no-errors-on-unmatched\n--diagnostic-level=warn\n--error-on-warnings\n",
|
||||
);
|
||||
});
|
||||
|
||||
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",
|
||||
|
|
@ -154,7 +176,7 @@ describe("development scripts", () => {
|
|||
"src/styles/10-component.css": ".codex-panel__used { display: block; }\n",
|
||||
"src/component.ts": [
|
||||
'export const used = "codex-panel__used";',
|
||||
"export const dynamicClassName = `codex-panel__task-step--${status}`;",
|
||||
"export const dynamicClassName = `codex-panel__task-step--$" + "{status}`;",
|
||||
].join("\n"),
|
||||
});
|
||||
|
||||
|
|
@ -170,7 +192,7 @@ describe("development scripts", () => {
|
|||
it("does not let dynamic CSS prefixes hide unconfigured class candidates", async () => {
|
||||
const cwd = await cssUsageFixture({
|
||||
"src/styles/10-component.css": ".codex-panel__task-step--stale { display: block; }\n",
|
||||
"src/component.ts": "export const className = `codex-panel__task-step--${status}`;\n",
|
||||
"src/component.ts": "export const className = `codex-panel__task-step--$" + "{status}`;\n",
|
||||
});
|
||||
|
||||
const result = runNodeScript("scripts/lint/check-css-usage.mjs", [], cwd);
|
||||
|
|
@ -250,3 +272,20 @@ async function writeJson(file: string, value: unknown): Promise<void> {
|
|||
async function readJson(file: string): Promise<unknown> {
|
||||
return JSON.parse(await readFile(file, "utf8"));
|
||||
}
|
||||
|
||||
async function writeCaptureBin(file: string, outputFile: string): Promise<void> {
|
||||
await writeFile(
|
||||
file,
|
||||
[
|
||||
"#!/usr/bin/env node",
|
||||
"const { appendFileSync } = require('node:fs');",
|
||||
`appendFileSync(${JSON.stringify(outputFile)}, \`\${process.argv.slice(2).join("\\n")}\\n\`);`,
|
||||
].join("\n"),
|
||||
);
|
||||
await chmod(file, 0o755);
|
||||
}
|
||||
|
||||
async function writeExitZeroBin(file: string): Promise<void> {
|
||||
await writeFile(file, "#!/usr/bin/env node\n");
|
||||
await chmod(file, 0o755);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue