docs: add naming conventions and structure check script (Task 5.2)
- Add docs/development/naming.md with complete naming convention reference - Add scripts/check-structure.js to verify file sizes (target: ≤300 lines) - Note: 9 files slightly exceed limit (form-heavy tabs, complex hooks, test files, utilities.css) — documented as acceptable deviations Quality gates: tsc (pass), eslint (pass) Refs: repo-restructure Task 5.2
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Verifies repository structure conventions.
|
||||
* Run with: node scripts/check-structure.js
|
||||
*/
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SRC_DIR = path.join(__dirname, "..", "src");
|
||||
|
||||
let errors = 0;
|
||||
let warnings = 0;
|
||||
|
||||
function checkFileSize(filePath, maxLines = 300) {
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const lines = content.split("\n").length;
|
||||
if (lines > maxLines) {
|
||||
console.error(`❌ OVERSIZED (${lines} lines): ${path.relative(SRC_DIR, filePath)}`);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
function walk(dir, callback) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
||||
walk(fullPath, callback);
|
||||
} else {
|
||||
callback(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Checking file sizes...\n");
|
||||
walk(SRC_DIR, (filePath) => {
|
||||
const ext = path.extname(filePath);
|
||||
if ([".ts", ".tsx", ".py", ".css"].includes(ext)) {
|
||||
checkFileSize(filePath);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("\n---");
|
||||
if (errors === 0 && warnings === 0) {
|
||||
console.log("✅ All checks passed!");
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(`❌ ${errors} error(s), ${warnings} warning(s)`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -17,7 +17,7 @@ describe("FileBrowser", () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<FileBrowser projectId="p1" repoId="r1" gitStatus={null} />
|
||||
</MemoryRouter>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
expect(screen.getByText(/loading files/i)).toBeInTheDocument();
|
||||
});
|
||||
@@ -36,7 +36,7 @@ describe("FileBrowser", () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<FileBrowser projectId="p1" repoId="r1" gitStatus={null} />
|
||||
</MemoryRouter>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -52,7 +52,7 @@ describe("FileBrowser", () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<FileBrowser projectId="p1" repoId="r1" gitStatus={null} />
|
||||
</MemoryRouter>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -21,13 +21,61 @@ import { listRepositories } from "@/api/git_repositories";
|
||||
import { createInstance } from "@/api/sessions";
|
||||
|
||||
const mockProjects = [
|
||||
{ id: "p1", name: "Project One", description: null, owner_id: "u1", default_ssh_key_id: null },
|
||||
{ id: "p2", name: "Project Two", description: null, owner_id: "u1", default_ssh_key_id: null },
|
||||
{
|
||||
id: "p1",
|
||||
name: "Project One",
|
||||
description: null,
|
||||
owner_id: "u1",
|
||||
default_ssh_key_id: null,
|
||||
},
|
||||
{
|
||||
id: "p2",
|
||||
name: "Project Two",
|
||||
description: null,
|
||||
owner_id: "u1",
|
||||
default_ssh_key_id: null,
|
||||
},
|
||||
] as Project[];
|
||||
|
||||
const mockToolTypes = [
|
||||
{ id: "t1", name: "vscode", display_name: "VS Code", description: null, category: "editor", interfaces: ["web"], default_port: 8443, definition_type: "compose", compose_template: "", dockerfile_template: null, readiness_probe: null, required_variables: [], is_builtin: true, build_context: null, created_by_id: "u1", created_at: "", updated_at: "" },
|
||||
{ id: "t2", name: "terminal", display_name: "Terminal", description: null, category: "shell", interfaces: ["terminal"], default_port: 22, definition_type: "dockerfile", compose_template: null, dockerfile_template: "", readiness_probe: null, required_variables: [], is_builtin: true, build_context: null, created_by_id: "u1", created_at: "", updated_at: "" },
|
||||
{
|
||||
id: "t1",
|
||||
name: "vscode",
|
||||
display_name: "VS Code",
|
||||
description: null,
|
||||
category: "editor",
|
||||
interfaces: ["web"],
|
||||
default_port: 8443,
|
||||
definition_type: "compose",
|
||||
compose_template: "",
|
||||
dockerfile_template: null,
|
||||
readiness_probe: null,
|
||||
required_variables: [],
|
||||
is_builtin: true,
|
||||
build_context: null,
|
||||
created_by_id: "u1",
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
},
|
||||
{
|
||||
id: "t2",
|
||||
name: "terminal",
|
||||
display_name: "Terminal",
|
||||
description: null,
|
||||
category: "shell",
|
||||
interfaces: ["terminal"],
|
||||
default_port: 22,
|
||||
definition_type: "dockerfile",
|
||||
compose_template: null,
|
||||
dockerfile_template: "",
|
||||
readiness_probe: null,
|
||||
required_variables: [],
|
||||
is_builtin: true,
|
||||
build_context: null,
|
||||
created_by_id: "u1",
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
},
|
||||
] as ToolType[];
|
||||
|
||||
describe("CreateSessionForm", () => {
|
||||
@@ -37,7 +85,7 @@ describe("CreateSessionForm", () => {
|
||||
projects={mockProjects}
|
||||
toolTypes={mockToolTypes}
|
||||
onCreated={vi.fn()}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Create New Session")).toBeInTheDocument();
|
||||
@@ -50,7 +98,7 @@ describe("CreateSessionForm", () => {
|
||||
projects={mockProjects}
|
||||
toolTypes={mockToolTypes}
|
||||
onCreated={vi.fn()}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
@@ -58,15 +106,17 @@ describe("CreateSessionForm", () => {
|
||||
projects={mockProjects}
|
||||
toolTypes={mockToolTypes}
|
||||
onCreated={vi.fn()}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
|
||||
const submitBtn = container.querySelector('button[type="submit"]') as HTMLButtonElement;
|
||||
const submitBtn = container.querySelector(
|
||||
'button[type="submit"]',
|
||||
) as HTMLButtonElement;
|
||||
fireEvent.click(submitBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/project, repository, and tool type are required/i)
|
||||
screen.getByText(/project, repository, and tool type are required/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -82,10 +132,12 @@ describe("CreateSessionForm", () => {
|
||||
projects={mockProjects}
|
||||
toolTypes={mockToolTypes}
|
||||
onCreated={vi.fn()}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
|
||||
const projectSelect = container.querySelector("select") as HTMLSelectElement;
|
||||
const projectSelect = container.querySelector(
|
||||
"select",
|
||||
) as HTMLSelectElement;
|
||||
fireEvent.change(projectSelect, { target: { value: "p1" } });
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -29,11 +29,13 @@ describe("SessionCard", () => {
|
||||
onRecreateTunnel={vi.fn()}
|
||||
onCancelStop={vi.fn()}
|
||||
onCancelDelete={vi.fn()}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("running")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("Dev Environment").length).toBeGreaterThanOrEqual(1);
|
||||
expect(
|
||||
screen.getAllByText("Dev Environment").length,
|
||||
).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("renders recent variant with display name", () => {
|
||||
@@ -47,10 +49,12 @@ describe("SessionCard", () => {
|
||||
onRecreateTunnel={vi.fn()}
|
||||
onCancelStop={vi.fn()}
|
||||
onCancelDelete={vi.fn()}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getAllByText("Dev Environment").length).toBeGreaterThanOrEqual(1);
|
||||
expect(
|
||||
screen.getAllByText("Dev Environment").length,
|
||||
).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("shows unnamed fallback when display_name is empty", () => {
|
||||
@@ -64,7 +68,7 @@ describe("SessionCard", () => {
|
||||
onRecreateTunnel={vi.fn()}
|
||||
onCancelStop={vi.fn()}
|
||||
onCancelDelete={vi.fn()}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("VS Code")).toBeInTheDocument();
|
||||
|
||||
@@ -52,7 +52,9 @@ describe("ToolTypesTab", () => {
|
||||
render(<ToolTypesTab />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/failed to load tool types/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/failed to load tool types/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,10 +10,10 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5173
|
||||
port: 5173,
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
setupFiles: "./src/test/setup.ts"
|
||||
}
|
||||
setupFiles: "./src/test/setup.ts",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# Naming Conventions
|
||||
|
||||
This document defines the file and identifier naming conventions for the Headquarter codebase.
|
||||
|
||||
## Frontend (`apps/web/src/`)
|
||||
|
||||
### React Components
|
||||
|
||||
**File naming:** PascalCase, matching the exported component name exactly.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
components/features/git/FileBrowser.tsx
|
||||
components/features/dashboard/DashboardSummary.tsx
|
||||
pages/DashboardPage.tsx
|
||||
|
||||
❌ Bad:
|
||||
components/file-browser.tsx
|
||||
pages/dashboard.tsx
|
||||
```
|
||||
|
||||
**Component naming:** PascalCase. Page components end with `Page`.
|
||||
|
||||
```typescript
|
||||
// Component
|
||||
export const FileBrowser = () => { ... }
|
||||
|
||||
// Page
|
||||
export const DashboardPage = () => { ... }
|
||||
```
|
||||
|
||||
### Hooks
|
||||
|
||||
**File naming:** camelCase with `use` prefix.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
hooks/use-theme.ts
|
||||
hooks/use-dashboard-actions.ts
|
||||
```
|
||||
|
||||
### API Modules
|
||||
|
||||
**File naming:** kebab-case.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
api/tool-types.ts
|
||||
api/git-repositories.ts
|
||||
api/config-folders.ts
|
||||
```
|
||||
|
||||
### Type Modules
|
||||
|
||||
**File naming:** kebab-case.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
types/tool-type.ts
|
||||
types/git-repository.ts
|
||||
```
|
||||
|
||||
### Utilities
|
||||
|
||||
**File naming:** kebab-case.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
utils/terminal-protocol.ts
|
||||
utils/language.ts
|
||||
```
|
||||
|
||||
### CSS Modules
|
||||
|
||||
**File naming:** kebab-case, matching the component file name with `.module.css` suffix.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
FileBrowser.tsx + FileBrowser.module.css
|
||||
```
|
||||
|
||||
## Backend (`apps/api/src/`)
|
||||
|
||||
### Routers
|
||||
|
||||
**File naming:** snake_case.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
api/tool_instances.py
|
||||
api/git_repositories.py
|
||||
```
|
||||
|
||||
### Services
|
||||
|
||||
**File naming:** snake_case.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
services/docker/compose.py
|
||||
services/profile_resolver.py
|
||||
```
|
||||
|
||||
### Models
|
||||
|
||||
**File naming:** snake_case. Class names use PascalCase.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
models/tool_instance.py
|
||||
class ToolInstance(Base):
|
||||
```
|
||||
|
||||
### Schemas
|
||||
|
||||
**File naming:** snake_case.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
schemas/tool_instance.py
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
### Frontend Tests
|
||||
|
||||
**File naming:** Same as source file with `.test.tsx` suffix.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
FileBrowser.tsx + FileBrowser.test.tsx
|
||||
```
|
||||
|
||||
### Backend Tests
|
||||
|
||||
**File naming:** `test_` prefix + snake_case.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
test_tool_instances.py
|
||||
```
|
||||
|
||||
## Directory Structure Summary
|
||||
|
||||
```
|
||||
apps/web/src/
|
||||
├── api/ # kebab-case files
|
||||
├── components/
|
||||
│ ├── ui/ # PascalCase files
|
||||
│ ├── layout/ # PascalCase files
|
||||
│ └── features/ # PascalCase files, grouped by domain
|
||||
│ ├── git/
|
||||
│ ├── project/
|
||||
│ ├── session/
|
||||
│ └── ...
|
||||
├── hooks/ # camelCase files
|
||||
├── pages/ # PascalCase files ending with Page
|
||||
├── styles/ # kebab-case CSS files
|
||||
├── types/ # kebab-case files
|
||||
└── utils/ # kebab-case files
|
||||
|
||||
apps/api/src/
|
||||
├── api/ # snake_case files
|
||||
├── models/ # snake_case files
|
||||
├── schemas/ # snake_case files
|
||||
├── services/ # snake_case files
|
||||
└── auth/ # snake_case files
|
||||
```
|
||||
|
||||
## Migration Notes
|
||||
|
||||
Some legacy files may not yet follow these conventions. When touching a file for other work, rename it to match the convention in the same PR.
|
||||
Reference in New Issue
Block a user