Organize Biome Grit policy configuration

This commit is contained in:
murashit 2026-06-27 22:31:54 +09:00
parent f068607509
commit b316481acd
35 changed files with 74 additions and 37 deletions

View file

@ -8,7 +8,7 @@
"plugins": [
// Shared source-shape boundaries.
{
"path": "./scripts/lint/no-responsibility-root-module-files.grit",
"path": "./scripts/grit/source-shape/no-responsibility-root-module-files.grit",
"includes": [
"**/src/app-server/*.ts",
"**/src/app-server/*.tsx",
@ -24,93 +24,93 @@
},
// App-server protocol and generated binding boundaries.
{
"path": "./scripts/lint/no-app-server-root-module-imports.grit",
"path": "./scripts/grit/architecture/no-app-server-root-module-imports.grit",
"includes": ["**/src/**/*.ts", "**/src/**/*.tsx"]
},
{
"path": "./scripts/lint/no-app-server-subfolder-root-imports.grit",
"path": "./scripts/grit/architecture/no-app-server-subfolder-root-imports.grit",
"includes": ["**/src/app-server/**/*.ts"]
},
{
"path": "./scripts/lint/no-app-server-projection-rpcs.grit",
"path": "./scripts/grit/architecture/no-app-server-projection-rpcs.grit",
"includes": ["**/src/features/chat/application/**/*.ts"]
},
{
"path": "./scripts/lint/no-app-server-protocol-boundary-imports.grit",
"path": "./scripts/grit/architecture/no-app-server-protocol-boundary-imports.grit",
"includes": ["**/src/**/*.ts", "**/src/**/*.tsx", "!**/src/app-server/**"]
},
{
"path": "./scripts/lint/no-generated-app-server-boundary-imports.grit",
"path": "./scripts/grit/architecture/no-generated-app-server-boundary-imports.grit",
"includes": ["**/src/**/*.ts", "**/src/**/*.tsx", "!**/src/app-server/connection/**"]
},
{
"path": "./scripts/lint/no-settings-app-server-boundary-imports.grit",
"path": "./scripts/grit/architecture/no-settings-app-server-boundary-imports.grit",
"includes": ["**/src/settings/**/*.ts", "**/src/settings/**/*.tsx", "!**/src/settings/app-server/**"]
},
// Shared source layering boundaries.
{
"path": "./scripts/lint/no-domain-outer-layer-imports.grit",
"path": "./scripts/grit/architecture/no-domain-outer-layer-imports.grit",
"includes": ["**/src/domain/**/*.ts", "**/src/features/chat/domain/**/*.ts"]
},
{
"path": "./scripts/lint/no-lower-level-feature-imports.grit",
"path": "./scripts/grit/architecture/no-lower-level-feature-imports.grit",
"includes": ["**/src/app-server/**/*.ts", "**/src/shared/**/*.ts", "**/src/shared/**/*.tsx"]
},
{
"path": "./scripts/lint/no-app-server-connection-boundary-imports.grit",
"path": "./scripts/grit/architecture/no-app-server-connection-boundary-imports.grit",
"includes": ["**/src/app-server/protocol/**/*.ts", "**/src/domain/**/*.ts", "**/src/shared/**/*.ts", "**/src/shared/**/*.tsx"]
},
// Chat feature architecture.
{
"path": "./scripts/lint/no-preact-signal-imports.grit",
"path": "./scripts/grit/architecture/no-preact-signal-imports.grit",
"includes": ["**/src/**/*.ts", "**/src/**/*.tsx", "!**/src/features/chat/panel/shell-state.tsx"]
},
{
"path": "./scripts/lint/no-state-module-side-effects.grit",
"path": "./scripts/grit/architecture/no-state-module-side-effects.grit",
"includes": ["**/src/features/chat/application/state/**/*.ts"]
},
{
"path": "./scripts/lint/no-chat-application-outer-layer-imports.grit",
"path": "./scripts/grit/architecture/no-chat-application-outer-layer-imports.grit",
"includes": ["**/src/features/chat/application/**/*.ts"]
},
{
"path": "./scripts/lint/no-chat-app-server-outer-layer-imports.grit",
"path": "./scripts/grit/architecture/no-chat-app-server-outer-layer-imports.grit",
"includes": ["**/src/features/chat/app-server/**/*.ts"]
},
{
"path": "./scripts/lint/no-chat-workspace-boundary-imports.grit",
"path": "./scripts/grit/architecture/no-chat-workspace-boundary-imports.grit",
"includes": ["**/src/features/chat/**/*.ts", "**/src/features/chat/**/*.tsx"]
},
{
"path": "./scripts/lint/no-feature-workspace-boundary-imports.grit",
"path": "./scripts/grit/architecture/no-feature-workspace-boundary-imports.grit",
"includes": ["**/src/features/**/*.ts", "**/src/features/**/*.tsx", "!**/src/features/chat/**"]
},
{
"path": "./scripts/lint/no-workspace-chat-internal-imports.grit",
"path": "./scripts/grit/architecture/no-workspace-chat-internal-imports.grit",
"includes": ["**/src/workspace/**/*.ts"]
},
{
"path": "./scripts/lint/no-chat-host-rendering-layer-imports.grit",
"path": "./scripts/grit/architecture/no-chat-host-rendering-layer-imports.grit",
"includes": ["**/src/features/chat/host/**/*.ts"]
},
{
"path": "./scripts/lint/no-chat-panel-runtime-boundary-imports.grit",
"path": "./scripts/grit/architecture/no-chat-panel-runtime-boundary-imports.grit",
"includes": ["**/src/features/chat/panel/**/*.ts", "**/src/features/chat/panel/**/*.tsx"]
},
{
"path": "./scripts/lint/no-chat-presentation-outer-layer-imports.grit",
"path": "./scripts/grit/architecture/no-chat-presentation-outer-layer-imports.grit",
"includes": ["**/src/features/chat/presentation/**/*.ts"]
},
{
"path": "./scripts/lint/no-chat-ui-outer-layer-imports.grit",
"path": "./scripts/grit/architecture/no-chat-ui-outer-layer-imports.grit",
"includes": ["**/src/features/chat/ui/**/*.ts", "**/src/features/chat/ui/**/*.tsx"]
},
// UI and DOM source boundaries.
{
"path": "./scripts/lint/no-implicit-dom-bridges.grit",
"path": "./scripts/grit/dom/no-implicit-dom-bridges.grit",
"includes": [
"**/src/**/*.ts",
"**/src/**/*.tsx",
@ -123,7 +123,7 @@
]
},
{
"path": "./scripts/lint/no-dom-events-imports.grit",
"path": "./scripts/grit/dom/no-dom-events-imports.grit",
"includes": [
"**/src/**/*.ts",
"**/src/**/*.tsx",
@ -136,7 +136,7 @@
]
},
{
"path": "./scripts/lint/no-ui-root-imports.grit",
"path": "./scripts/grit/dom/no-ui-root-imports.grit",
"includes": [
"**/src/**/*.ts",
"**/src/**/*.tsx",
@ -147,13 +147,13 @@
]
},
{
"path": "./scripts/lint/no-uncontrolled-preact-form-state.grit",
"path": "./scripts/grit/source-shape/no-uncontrolled-preact-form-state.grit",
"includes": ["**/src/**/*.tsx"]
},
// Project-wide source shape.
{
"path": "./scripts/lint/no-misplaced-tsx.grit",
"path": "./scripts/grit/source-shape/no-misplaced-tsx.grit",
"includes": [
"**/src/**/*.tsx",
"!**/src/features/chat/panel/**",
@ -166,21 +166,21 @@
]
},
{
"path": "./scripts/lint/no-handwritten-reexports.grit",
"path": "./scripts/grit/source-shape/no-handwritten-reexports.grit",
"includes": ["**/*.ts", "**/*.tsx", "**/*.mjs"]
},
{
"path": "./scripts/lint/no-self-referential-initializer-callback.grit",
"path": "./scripts/grit/source-shape/no-self-referential-initializer-callback.grit",
"includes": ["**/*.ts", "**/*.tsx", "**/*.mjs"]
},
{
"path": "./scripts/lint/no-unsafe-iterator-value.grit",
"path": "./scripts/grit/source-shape/no-unsafe-iterator-value.grit",
"includes": ["**/*.ts", "**/*.tsx", "**/*.mjs"]
},
// CSS source policy.
{
"path": "./scripts/lint/no-restricted-css-policy.grit",
"path": "./scripts/grit/no-restricted-css-policy.grit",
"includes": ["**/src/styles/**/*.css"]
}
],

View file

@ -19,7 +19,7 @@
"build:styles": "node scripts/build-styles.mjs",
"check": "concurrently --group --pad-prefix 'node:check:*' && node --run build",
"check:biome": "biome check --diagnostic-level=warn --error-on-warnings",
"check:css-usage": "node scripts/lint/check-css-usage.mjs",
"check:css-usage": "node scripts/check-css-usage.mjs",
"check:eslint": "eslint src manifest.json LICENSE --max-warnings=0",
"check:test": "node --run test",
"check:typecheck": "node --run typecheck",

View file

@ -4,7 +4,7 @@ import path from "node:path";
const root = process.cwd();
const stylesDir = path.join(root, "src", "styles");
if (process.argv.length > 2) {
console.error("Usage: node scripts/lint/check-css-usage.mjs");
console.error("Usage: node scripts/check-css-usage.mjs");
process.exit(1);
}

View file

@ -123,7 +123,7 @@ describe("development scripts", () => {
"tests/component.test.ts": 'expect("codex-panel__test-only").toBeTruthy();\n',
});
const result = runNodeScript("scripts/lint/check-css-usage.mjs", [], cwd);
const result = runNodeScript("scripts/check-css-usage.mjs", [], cwd);
expect(result.status).toBe(1);
expect(result.stdout).toBe("");

View file

@ -1,15 +1,17 @@
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, readdir, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
const repoRoot = process.cwd();
const biomeBin = path.join(repoRoot, "node_modules", ".bin", "biome");
const projectBiomeConfig = parseJsonc(readFileSync(path.join(repoRoot, "biome.jsonc"), "utf8"));
const projectPluginEntries = projectBiomeConfig.plugins;
const projectPluginByName = new Map(
parseJsonc(readFileSync(path.join(repoRoot, "biome.jsonc"), "utf8")).plugins.map((plugin) => {
const pluginPath = typeof plugin === "string" ? plugin : plugin.path;
projectPluginEntries.map((plugin) => {
const pluginPath = projectPluginPath(plugin);
return [path.basename(pluginPath), plugin];
}),
);
@ -45,6 +47,20 @@ let appServerBoundaryPolicyReportPromise;
let renderingAndCssPolicyReportPromise;
describe.concurrent("GritQL source policy", () => {
it("keeps Biome wired to every checked-in Grit policy", async () => {
const gritPolicyPaths = await listRelativeGritPolicyPaths(path.join(repoRoot, "scripts/grit"));
const configuredGritPluginPaths = projectPluginEntries
.map((plugin) => projectPluginPath(plugin))
.filter((pluginPath) => pluginPath.endsWith(".grit"))
.map(normalizeProjectRelativePath)
.sort();
const configuredGritPluginNames = configuredGritPluginPaths.map((pluginPath) => path.basename(pluginPath));
expect(new Set(configuredGritPluginPaths).size).toBe(configuredGritPluginPaths.length);
expect(new Set(configuredGritPluginNames).size).toBe(configuredGritPluginNames.length);
expect(configuredGritPluginPaths).toEqual(gritPolicyPaths);
});
it("keeps project-wide source-shape policies enforceable as Biome plugin diagnostics", async () => {
const cwd = await tempBiomeWorkspace([
"no-handwritten-reexports.grit",
@ -1458,13 +1474,17 @@ function projectPluginConfig(plugin) {
if (!projectPlugin) {
throw new Error(`Missing ${plugin} in biome.jsonc plugins`);
}
const pluginPath = path.join(repoRoot, "scripts", "lint", plugin);
const pluginPath = path.resolve(repoRoot, projectPluginPath(projectPlugin));
if (typeof projectPlugin === "string") {
return pluginPath;
}
return { ...projectPlugin, path: pluginPath };
}
function projectPluginPath(plugin) {
return typeof plugin === "string" ? plugin : plugin.path;
}
function pluginDiagnostics(report, filePath) {
return report.diagnostics
.filter((diagnostic) => diagnostic.category === "plugin" && diagnosticMatchesFile(report, diagnostic, filePath))
@ -1497,6 +1517,23 @@ function normalizeRelativePath(filePath) {
return filePath.split(path.sep).join("/");
}
function normalizeProjectRelativePath(filePath) {
return normalizeRelativePath(filePath).replace(/^\.\//, "");
}
async function listRelativeGritPolicyPaths(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const paths = await Promise.all(
entries.map(async (entry) => {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) return listRelativeGritPolicyPaths(entryPath);
if (entry.isFile() && entry.name.endsWith(".grit")) return [normalizeRelativePath(path.relative(repoRoot, entryPath))];
return [];
}),
);
return paths.flat().sort();
}
function parseBiomeJsonReport(stdout, stderr) {
const jsonStart = stdout.indexOf("{");
if (jsonStart === -1) {