chore(web): каркас веб-приложения — Vite, TypeScript, ESLint, Vitest, make web/check (#21)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bc610a7734
commit
bad6c73371
3
.gitignore
vendored
3
.gitignore
vendored
@ -12,3 +12,6 @@ __pycache__/
|
||||
*.sqlite*
|
||||
.pytest_cache/
|
||||
.wt/
|
||||
src/jira_hub/web/app/
|
||||
web/test-results/
|
||||
web/playwright-report/
|
||||
|
||||
10
Makefile
Normal file
10
Makefile
Normal file
@ -0,0 +1,10 @@
|
||||
.PHONY: web check
|
||||
|
||||
# сборка веб-приложения в src/jira_hub/web/app/ (каталог не в git; хаб читает его на каждый запрос)
|
||||
web:
|
||||
cd web && npm ci && npm run build
|
||||
|
||||
# весь набор проверок: хаб и фронт; зелёным считается только всё вместе
|
||||
check:
|
||||
uv run pytest -q
|
||||
cd web && npm run lint && npm test
|
||||
16
web/eslint.config.js
Normal file
16
web/eslint.config.js
Normal file
@ -0,0 +1,16 @@
|
||||
import js from "@eslint/js";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["node_modules", "playwright-report", "test-results"] },
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
files: ["**/*.{ts,tsx,js}"],
|
||||
languageOptions: { globals: { ...globals.browser, ...globals.node } },
|
||||
plugins: { "react-hooks": reactHooks },
|
||||
rules: { "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "error" },
|
||||
},
|
||||
);
|
||||
14
web/index.html
Normal file
14
web/index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Хаб Jira</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@500&display=swap">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
3892
web/package-lock.json
generated
Normal file
3892
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
web/package.json
Normal file
39
web/package.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "jira-hub-web",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc --noEmit -p . && vite build",
|
||||
"lint": "tsc --noEmit -p . && eslint .",
|
||||
"test": "vitest run",
|
||||
"e2e": "playwright test",
|
||||
"golden": "node scripts/golden-from-links.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "5.104.0",
|
||||
"react": "19.3.0",
|
||||
"react-dom": "19.3.0",
|
||||
"react-router": "8.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "10.0.1",
|
||||
"@playwright/test": "1.63.0",
|
||||
"@testing-library/dom": "10.4.2",
|
||||
"@testing-library/jest-dom": "7.0.1",
|
||||
"@testing-library/react": "16.3.3",
|
||||
"@testing-library/user-event": "14.6.7",
|
||||
"@types/node": "26.6.3",
|
||||
"@types/react": "19.3.0",
|
||||
"@types/react-dom": "19.3.0",
|
||||
"@vitejs/plugin-react": "6.1.1",
|
||||
"eslint": "10.11.0",
|
||||
"eslint-plugin-react-hooks": "7.1.1",
|
||||
"globals": "17.12.0",
|
||||
"jsdom": "29.1.1",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.71.0",
|
||||
"vite": "8.3.1",
|
||||
"vitest": "5.0.2"
|
||||
}
|
||||
}
|
||||
8
web/src/App.test.tsx
Normal file
8
web/src/App.test.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { expect, it } from "vitest";
|
||||
import { App } from "./App.tsx";
|
||||
|
||||
it("показывает заголовок", () => {
|
||||
render(<App />);
|
||||
expect(screen.getByRole("heading", { name: "Хаб Jira" })).toBeInTheDocument();
|
||||
});
|
||||
3
web/src/App.tsx
Normal file
3
web/src/App.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export function App() {
|
||||
return <h1>Хаб Jira</h1>;
|
||||
}
|
||||
9
web/src/main.tsx
Normal file
9
web/src/main.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "./App.tsx";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
14
web/src/test/canon.test.ts
Normal file
14
web/src/test/canon.test.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import vm from "node:vm";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { canon } from "./canon.ts";
|
||||
|
||||
describe("canon", () => {
|
||||
it("сортирует ключи объектов и выбрасывает undefined", () => {
|
||||
expect(JSON.stringify(canon({ b: 1, a: { d: undefined, c: [2, 1] } }))).toBe('{"a":{"c":[2,1]},"b":1}');
|
||||
});
|
||||
it("Set — отсортированный массив, в том числе из другого realm", () => {
|
||||
const foreign = vm.runInNewContext('new Set(["b", "a"])');
|
||||
expect(canon(foreign)).toEqual(["a", "b"]);
|
||||
expect(canon(new Set([{ k: 2 }, { k: 1 }]))).toEqual([{ k: 1 }, { k: 2 }]);
|
||||
});
|
||||
});
|
||||
13
web/src/test/canon.ts
Normal file
13
web/src/test/canon.ts
Normal file
@ -0,0 +1,13 @@
|
||||
// нормализация значений для сравнения с golden-эталоном: ключи объектов по алфавиту, Set — отсортированный массив,
|
||||
// undefined-поля выбрасываются (как в JSON). Set распознаётся по тегу: объекты из node:vm — из другого realm
|
||||
const isSet = (v: unknown): v is Set<unknown> => Object.prototype.toString.call(v) === "[object Set]";
|
||||
|
||||
export function canon(v: unknown): unknown {
|
||||
if (isSet(v)) return [...v].map(canon).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
||||
if (Array.isArray(v)) return v.map(canon);
|
||||
if (v && typeof v === "object") {
|
||||
const o = v as Record<string, unknown>;
|
||||
return Object.fromEntries(Object.keys(o).sort().filter((k) => o[k] !== undefined).map((k) => [k, canon(o[k])]));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
8
web/src/test/setup.ts
Normal file
8
web/src/test/setup.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { afterEach } from "vitest";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
localStorage.clear();
|
||||
});
|
||||
1
web/src/vite-env.d.ts
vendored
Normal file
1
web/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
21
web/tsconfig.json
Normal file
21
web/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node", "vite/client", "@testing-library/jest-dom"]
|
||||
},
|
||||
"include": ["src", "scripts", "e2e", "vite.config.ts", "playwright.config.ts"]
|
||||
}
|
||||
20
web/vite.config.ts
Normal file
20
web/vite.config.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
// сервер разработки: приложение под /app/, маршруты API — прокси на хаб (по умолчанию тестовый :8791, R-5)
|
||||
const hub = process.env.JIRA_HUB_URL ?? "http://127.0.0.1:8791";
|
||||
const API = ["/board", "/issues", "/settings", "/agent", "/refresh", "/batches", "/commands", "/sync-runs"];
|
||||
|
||||
export default defineConfig({
|
||||
base: "/app/",
|
||||
plugins: [react()],
|
||||
build: { outDir: "../src/jira_hub/web/app", emptyOutDir: true },
|
||||
server: { host: "127.0.0.1", port: 5173, proxy: Object.fromEntries(API.map((p) => [p, { target: hub }])) },
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
setupFiles: ["./src/test/setup.ts"],
|
||||
include: ["src/**/*.test.{ts,tsx}"],
|
||||
restoreMocks: true,
|
||||
unstubGlobals: true,
|
||||
},
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user