reactjsquality-check911 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/reactjsquality-check911.js +41 -0
- package/commands/coverage.js +117 -0
- package/commands/hooks.js +74 -0
- package/commands/init.js +227 -0
- package/commands/playwright.js +52 -0
- package/commands/quality.js +158 -0
- package/commands/scan.js +243 -0
- package/mcp-server/requirements.txt +1 -0
- package/mcp-server/server.py +61 -0
- package/mcp-server/tools/code_fixer.py +130 -0
- package/mcp-server/tools/component_scanner.py +47 -0
- package/mcp-server/tools/playwright_helper.py +78 -0
- package/mcp-server/tools/test_generator.py +119 -0
- package/package.json +44 -0
- package/scripts/setup-mcp.js +18 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { program } = require("commander");
|
|
5
|
+
|
|
6
|
+
program
|
|
7
|
+
.name("reactjsquality-check911")
|
|
8
|
+
.description("React/JS quality agent — ESLint, Jest coverage, Playwright on every commit")
|
|
9
|
+
.version(require("../package.json").version);
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.command("init")
|
|
13
|
+
.description("Choose framework and quality checks, configure Copilot and MCP")
|
|
14
|
+
.action(() => require("../commands/init")());
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command("quality")
|
|
18
|
+
.description("Run ESLint and TypeScript check on staged files (changed chunks only)")
|
|
19
|
+
.action(() => require("../commands/quality")());
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.command("coverage")
|
|
23
|
+
.description("Run Jest coverage on staged/changed files (80% threshold per file)")
|
|
24
|
+
.action(() => require("../commands/coverage")());
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.command("playwright")
|
|
28
|
+
.description("Run Playwright smoke tests against the current build")
|
|
29
|
+
.action(() => require("../commands/playwright")());
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command("scan")
|
|
33
|
+
.description("Scan components, pages, and services — rebuild architecture.md")
|
|
34
|
+
.action(() => require("../commands/scan")());
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command("hooks")
|
|
38
|
+
.description("Install pre-commit (ESLint + coverage) and pre-push (Playwright) git hooks")
|
|
39
|
+
.action(() => require("../commands/hooks")());
|
|
40
|
+
|
|
41
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const THRESHOLD = 80;
|
|
8
|
+
const CONFIG_FILE = ".reactjs-quality-agent.json";
|
|
9
|
+
|
|
10
|
+
function loadConfig() {
|
|
11
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8")); } catch (_) { return null; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getStagedSourceFiles() {
|
|
15
|
+
try {
|
|
16
|
+
const out = execSync("git diff --cached --name-only", { encoding: "utf8" });
|
|
17
|
+
return out.split("\n").map(f => f.trim()).filter(f =>
|
|
18
|
+
f &&
|
|
19
|
+
/\.(js|jsx|ts|tsx|vue)$/.test(f) &&
|
|
20
|
+
!f.includes(".test.") && !f.includes(".spec.") &&
|
|
21
|
+
!f.includes("__tests__") &&
|
|
22
|
+
fs.existsSync(f)
|
|
23
|
+
);
|
|
24
|
+
} catch (_) { return []; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findTestFile(srcFile) {
|
|
28
|
+
const base = srcFile.replace(/\.(js|jsx|ts|tsx|vue)$/, "");
|
|
29
|
+
const ext = srcFile.match(/\.(js|jsx|ts|tsx|vue)$/)[1];
|
|
30
|
+
const testExt = ext === "js" ? "js" : ext === "jsx" ? "tsx" : "tsx";
|
|
31
|
+
return [
|
|
32
|
+
`${base}.test.${testExt}`,
|
|
33
|
+
`${base}.test.${ext}`,
|
|
34
|
+
`${base}.spec.${testExt}`,
|
|
35
|
+
`${base}.spec.${ext}`,
|
|
36
|
+
base.replace(/\/src\//, "/src/__tests__/") + `.test.${testExt}`,
|
|
37
|
+
base.replace(/\/src\//, "/src/__tests__/") + `.test.${ext}`,
|
|
38
|
+
].find(f => fs.existsSync(f));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = function coverage() {
|
|
42
|
+
const config = loadConfig();
|
|
43
|
+
if (!config) {
|
|
44
|
+
console.error("No .reactjs-quality-agent.json found — run `reactjsquality-check911 init` first.");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
if (!config.checks.coverage) {
|
|
48
|
+
console.log("Coverage check is disabled — skipping.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const sources = getStagedSourceFiles();
|
|
53
|
+
if (!sources.length) {
|
|
54
|
+
console.log("No staged source files — skipping coverage.");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`Verifying Jest coverage for: ${sources.map(f => path.basename(f)).join(", ")}\n`);
|
|
59
|
+
const results = [];
|
|
60
|
+
let failed = false;
|
|
61
|
+
|
|
62
|
+
for (const file of sources) {
|
|
63
|
+
const testFile = findTestFile(file);
|
|
64
|
+
if (!testFile) {
|
|
65
|
+
const expected = path.basename(file).replace(/\.(js|jsx|ts|tsx)$/, ".test.$1");
|
|
66
|
+
console.log(` ⚠️ ${path.basename(file)} — no test file found (expected ${expected})`);
|
|
67
|
+
results.push(`⚠️ ${path.basename(file)}: no test file`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
execSync(
|
|
73
|
+
`npx jest "${testFile}" --coverage --coverageReporters=json-summary --passWithNoTests --silent`,
|
|
74
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const summaryPath = "coverage/coverage-summary.json";
|
|
78
|
+
if (!fs.existsSync(summaryPath)) {
|
|
79
|
+
results.push(`⚠️ ${path.basename(file)}: coverage-summary.json not generated`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const summary = JSON.parse(fs.readFileSync(summaryPath, "utf8"));
|
|
84
|
+
const absFile = path.resolve(file).replace(/\\/g, "/");
|
|
85
|
+
const matched = Object.entries(summary).find(([k]) =>
|
|
86
|
+
k.replace(/\\/g, "/") === absFile ||
|
|
87
|
+
k.replace(/\\/g, "/").endsWith("/" + file.replace(/\\/g, "/"))
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (!matched) {
|
|
91
|
+
results.push(`⚠️ ${path.basename(file)}: not found in coverage report`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const pct = matched[1].lines.pct;
|
|
96
|
+
const icon = pct >= THRESHOLD ? "✅" : "❌";
|
|
97
|
+
console.log(` ${icon} ${path.basename(file)}: ${pct}% line coverage`);
|
|
98
|
+
results.push(`${icon} ${path.basename(file)}: ${pct}%`);
|
|
99
|
+
if (pct < THRESHOLD) failed = true;
|
|
100
|
+
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.log(` ❌ ${path.basename(file)}: Jest failed`);
|
|
103
|
+
results.push(`❌ ${path.basename(file)}: Jest execution failed`);
|
|
104
|
+
failed = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log("\n--- Coverage Summary ---");
|
|
109
|
+
results.forEach(r => console.log(" ", r));
|
|
110
|
+
|
|
111
|
+
if (failed) {
|
|
112
|
+
console.log(`\nCoverage below ${THRESHOLD}% — add tests and try again.`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(`\n✅ All changed files meet the ${THRESHOLD}% coverage threshold.`);
|
|
117
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const PRE_COMMIT = `#!/bin/sh
|
|
8
|
+
# reactjsquality-check911 — pre-commit hook
|
|
9
|
+
# Runs ESLint and Jest coverage on staged files (changed chunks only)
|
|
10
|
+
|
|
11
|
+
set -e
|
|
12
|
+
|
|
13
|
+
if ! command -v reactjsquality-check911 >/dev/null 2>&1; then
|
|
14
|
+
echo "reactjsquality-check911 not found — run: npm install -g reactjsquality-check911"
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
CONFIG=".reactjs-quality-agent.json"
|
|
19
|
+
if [ ! -f "$CONFIG" ]; then
|
|
20
|
+
echo "No $CONFIG found — run: reactjsquality-check911 init"
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
reactjsquality-check911 quality
|
|
25
|
+
reactjsquality-check911 coverage
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const PRE_PUSH = `#!/bin/sh
|
|
29
|
+
# reactjsquality-check911 — pre-push hook
|
|
30
|
+
# Runs Playwright smoke tests before every push
|
|
31
|
+
|
|
32
|
+
set -e
|
|
33
|
+
|
|
34
|
+
if ! command -v reactjsquality-check911 >/dev/null 2>&1; then
|
|
35
|
+
echo "reactjsquality-check911 not found — run: npm install -g reactjsquality-check911"
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
CONFIG=".reactjs-quality-agent.json"
|
|
40
|
+
if [ ! -f "$CONFIG" ]; then
|
|
41
|
+
echo "No $CONFIG found — run: reactjsquality-check911 init"
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
reactjsquality-check911 playwright
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
module.exports = function hooks() {
|
|
49
|
+
fs.mkdirSync(".githooks", { recursive: true });
|
|
50
|
+
|
|
51
|
+
fs.writeFileSync(".githooks/pre-commit", PRE_COMMIT);
|
|
52
|
+
fs.chmodSync(".githooks/pre-commit", 0o755);
|
|
53
|
+
console.log("✅ .githooks/pre-commit created (ESLint + Jest coverage on staged files)");
|
|
54
|
+
|
|
55
|
+
fs.writeFileSync(".githooks/pre-push", PRE_PUSH);
|
|
56
|
+
fs.chmodSync(".githooks/pre-push", 0o755);
|
|
57
|
+
console.log("✅ .githooks/pre-push created (Playwright smoke tests before every push)");
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
execSync("git config core.hooksPath .githooks", { stdio: "pipe" });
|
|
61
|
+
console.log("✅ git configured to use .githooks/");
|
|
62
|
+
} catch (_) {
|
|
63
|
+
console.log("⚠️ Could not set core.hooksPath — run manually: git config core.hooksPath .githooks");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(`
|
|
67
|
+
Hooks installed. From now on:
|
|
68
|
+
|
|
69
|
+
git commit → ESLint errors on changed chunks block the commit
|
|
70
|
+
Jest coverage < 80% on changed files blocks the commit
|
|
71
|
+
|
|
72
|
+
git push → Playwright smoke tests must all pass before push is allowed
|
|
73
|
+
`);
|
|
74
|
+
};
|
package/commands/init.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const readline = require("readline");
|
|
6
|
+
|
|
7
|
+
const PKG_ROOT = path.join(__dirname, "..");
|
|
8
|
+
|
|
9
|
+
const FRAMEWORKS = [
|
|
10
|
+
{ id: "react", label: "React" },
|
|
11
|
+
{ id: "nextjs", label: "Next.js" },
|
|
12
|
+
{ id: "vue", label: "Vue / Nuxt" },
|
|
13
|
+
{ id: "angular", label: "Angular" },
|
|
14
|
+
{ id: "vanilla", label: "Vanilla JS / TypeScript" }
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const CHECKS = [
|
|
18
|
+
{ id: "eslint", label: "ESLint — code style and error rules on staged chunks" },
|
|
19
|
+
{ id: "coverage", label: "Coverage — Jest line coverage (80% threshold per changed file)" },
|
|
20
|
+
{ id: "playwright", label: "Playwright — smoke tests run before every git push" }
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const COPILOT_INSTRUCTIONS = {
|
|
24
|
+
react: `# React Quality Agent
|
|
25
|
+
|
|
26
|
+
You are a Senior React Engineer embedded in this repository.
|
|
27
|
+
|
|
28
|
+
## Your Responsibilities
|
|
29
|
+
- Generate Jest + React Testing Library tests for every component and hook
|
|
30
|
+
- Fix ESLint and TypeScript violations in staged files
|
|
31
|
+
- Ensure 80% Jest line coverage on changed files
|
|
32
|
+
- Generate Playwright tests for new pages and user flows
|
|
33
|
+
|
|
34
|
+
## How to Work Efficiently
|
|
35
|
+
Use the \`index\` MCP tool to find relevant components before answering.
|
|
36
|
+
Use the \`read_file\` MCP tool to read only the specific file needed.
|
|
37
|
+
Use the \`fix\` MCP tool when the user asks to fix quality issues.
|
|
38
|
+
|
|
39
|
+
## Test Generation Rules — Jest + React Testing Library
|
|
40
|
+
- File naming: \`{ComponentName}.test.tsx\`
|
|
41
|
+
- Method naming: \`should {expected behavior} when {condition}\`
|
|
42
|
+
- Cover: render output, user interactions, props, error boundaries, loading states
|
|
43
|
+
- Use \`@testing-library/user-event\` for all user interactions (click, type, select)
|
|
44
|
+
- Mock API calls with \`jest.fn()\` or \`msw\` (Mock Service Worker)
|
|
45
|
+
- Use \`screen.getByRole\`, \`getByText\`, \`getByTestId\` — avoid querying by class/id
|
|
46
|
+
|
|
47
|
+
## Test Generation Rules — Playwright
|
|
48
|
+
- File naming: \`tests/{page-name}.spec.ts\`
|
|
49
|
+
- Cover: happy path, form validation, navigation flows, error states
|
|
50
|
+
- Use page object model pattern
|
|
51
|
+
- Assertions: \`expect(locator).toBeVisible()\`, \`toHaveText()\`, \`toHaveURL()\`
|
|
52
|
+
|
|
53
|
+
## Quality Standards
|
|
54
|
+
- ESLint: react/recommended + typescript-eslint/recommended
|
|
55
|
+
- No unused variables, no console.log in production code, no implicit \`any\`
|
|
56
|
+
- Jest: 80% minimum line coverage on changed files
|
|
57
|
+
- Playwright: all smoke tests must pass before every push
|
|
58
|
+
`,
|
|
59
|
+
nextjs: `# Next.js Quality Agent
|
|
60
|
+
|
|
61
|
+
You are a Senior Next.js Engineer embedded in this repository.
|
|
62
|
+
|
|
63
|
+
## Your Responsibilities
|
|
64
|
+
- Generate Jest + React Testing Library tests for components and API routes
|
|
65
|
+
- Fix ESLint and TypeScript violations in staged files
|
|
66
|
+
- Ensure 80% Jest line coverage on changed files
|
|
67
|
+
- Generate Playwright tests for new pages
|
|
68
|
+
|
|
69
|
+
## Test Generation Rules — Jest
|
|
70
|
+
- File naming: \`{name}.test.tsx\` (components), \`{route}.test.ts\` (API routes)
|
|
71
|
+
- For API routes: use \`NextRequest\` / \`NextResponse\` mocks
|
|
72
|
+
- For server components: test the data-fetching logic separately from rendering
|
|
73
|
+
- For client components: use React Testing Library same as React
|
|
74
|
+
|
|
75
|
+
## Test Generation Rules — Playwright
|
|
76
|
+
- File naming: \`tests/{page-name}.spec.ts\`
|
|
77
|
+
- Cover: page navigation, SSR content visible, client-side transitions
|
|
78
|
+
- Use \`page.goto()\` with full paths matching Next.js routing
|
|
79
|
+
|
|
80
|
+
## Quality Standards
|
|
81
|
+
- ESLint: next/core-web-vitals rules
|
|
82
|
+
- No \`<img>\` — use Next.js \`<Image>\`
|
|
83
|
+
- No \`<a>\` for internal links — use \`<Link>\`
|
|
84
|
+
- Jest: 80% minimum line coverage
|
|
85
|
+
`,
|
|
86
|
+
vue: `# Vue Quality Agent
|
|
87
|
+
|
|
88
|
+
You are a Senior Vue Engineer embedded in this repository.
|
|
89
|
+
|
|
90
|
+
## Your Responsibilities
|
|
91
|
+
- Generate Jest + Vue Test Utils tests for every component
|
|
92
|
+
- Fix ESLint and TypeScript violations in staged files
|
|
93
|
+
- Ensure 80% Jest line coverage on changed files
|
|
94
|
+
- Generate Playwright tests for new pages
|
|
95
|
+
|
|
96
|
+
## Test Generation Rules — Jest
|
|
97
|
+
- File naming: \`{ComponentName}.test.ts\`
|
|
98
|
+
- Use \`mount\` for full rendering, \`shallowMount\` for unit isolation
|
|
99
|
+
- Cover: props, emits, slots, user interactions, computed values
|
|
100
|
+
- Mock Pinia stores with \`createTestingPinia()\`
|
|
101
|
+
|
|
102
|
+
## Quality Standards
|
|
103
|
+
- ESLint: vue3/recommended + TypeScript
|
|
104
|
+
- Composition API only — no Options API
|
|
105
|
+
- Jest: 80% minimum line coverage
|
|
106
|
+
`,
|
|
107
|
+
angular: `# Angular Quality Agent
|
|
108
|
+
|
|
109
|
+
You are a Senior Angular Engineer embedded in this repository.
|
|
110
|
+
|
|
111
|
+
## Your Responsibilities
|
|
112
|
+
- Generate Jest tests for components, services, guards, and pipes
|
|
113
|
+
- Fix ESLint and TypeScript violations in staged files
|
|
114
|
+
- Ensure 80% Jest line coverage on changed files
|
|
115
|
+
|
|
116
|
+
## Test Generation Rules — Jest
|
|
117
|
+
- File naming: \`{name}.spec.ts\`
|
|
118
|
+
- Use \`TestBed.configureTestingModule()\` for component and service tests
|
|
119
|
+
- Use \`HttpClientTestingModule\` + \`HttpTestingController\` for HTTP services
|
|
120
|
+
- Cover: component rendering, service methods, guards, directive behavior
|
|
121
|
+
|
|
122
|
+
## Quality Standards
|
|
123
|
+
- ESLint: Angular recommended + TypeScript strict
|
|
124
|
+
- Jest: 80% minimum line coverage
|
|
125
|
+
`,
|
|
126
|
+
vanilla: `# Vanilla JS/TS Quality Agent
|
|
127
|
+
|
|
128
|
+
You are a Senior JavaScript/TypeScript Engineer embedded in this repository.
|
|
129
|
+
|
|
130
|
+
## Your Responsibilities
|
|
131
|
+
- Generate Jest tests for every exported function and class
|
|
132
|
+
- Fix ESLint and TypeScript violations in staged files
|
|
133
|
+
- Ensure 80% Jest line coverage on changed files
|
|
134
|
+
|
|
135
|
+
## Test Generation Rules — Jest
|
|
136
|
+
- File naming: \`{module-name}.test.ts\`
|
|
137
|
+
- Cover: return values, side effects, edge cases, thrown exceptions
|
|
138
|
+
- Mock dependencies with \`jest.fn()\` and \`jest.spyOn()\`
|
|
139
|
+
|
|
140
|
+
## Quality Standards
|
|
141
|
+
- ESLint: recommended + TypeScript strict
|
|
142
|
+
- Jest: 80% minimum line coverage
|
|
143
|
+
`
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function ask(rl, question) {
|
|
147
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = async function init() {
|
|
151
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
152
|
+
|
|
153
|
+
console.log("\nreactjsquality-check911 — setup\n");
|
|
154
|
+
|
|
155
|
+
// ── Framework selection ───────────────────────────────────────────────────
|
|
156
|
+
console.log("Select your frontend framework:");
|
|
157
|
+
FRAMEWORKS.forEach((f, i) => console.log(` ${i + 1}. ${f.label}`));
|
|
158
|
+
const fwRaw = (await ask(rl, "\nEnter number (default: 1 — React): ")).trim() || "1";
|
|
159
|
+
const fwIdx = parseInt(fwRaw) - 1;
|
|
160
|
+
const framework = FRAMEWORKS[fwIdx] || FRAMEWORKS[0];
|
|
161
|
+
console.log(`Selected: ${framework.label}\n`);
|
|
162
|
+
|
|
163
|
+
// ── Quality checks selection ──────────────────────────────────────────────
|
|
164
|
+
console.log("Select quality checks to enable:");
|
|
165
|
+
CHECKS.forEach((c, i) => console.log(` ${i + 1}. ${c.label}`));
|
|
166
|
+
const checksRaw = (await ask(rl, "\nEnter numbers separated by commas (press Enter to enable all): ")).trim();
|
|
167
|
+
rl.close();
|
|
168
|
+
|
|
169
|
+
let selectedIds;
|
|
170
|
+
if (!checksRaw) {
|
|
171
|
+
selectedIds = CHECKS.map(c => c.id);
|
|
172
|
+
} else {
|
|
173
|
+
selectedIds = checksRaw.split(",").map(n => {
|
|
174
|
+
const idx = parseInt(n.trim()) - 1;
|
|
175
|
+
return CHECKS[idx] ? CHECKS[idx].id : null;
|
|
176
|
+
}).filter(Boolean);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const checks = {};
|
|
180
|
+
for (const c of CHECKS) checks[c.id] = selectedIds.includes(c.id);
|
|
181
|
+
|
|
182
|
+
// ── Write .reactjs-quality-agent.json ────────────────────────────────────
|
|
183
|
+
const config = { framework: framework.id, checks };
|
|
184
|
+
fs.writeFileSync(".reactjs-quality-agent.json", JSON.stringify(config, null, 2));
|
|
185
|
+
console.log("\n✅ .reactjs-quality-agent.json created");
|
|
186
|
+
|
|
187
|
+
// ── .github/copilot-instructions.md ──────────────────────────────────────
|
|
188
|
+
fs.mkdirSync(".github/instructions", { recursive: true });
|
|
189
|
+
const instructions = COPILOT_INSTRUCTIONS[framework.id] || COPILOT_INSTRUCTIONS.react;
|
|
190
|
+
fs.writeFileSync(".github/copilot-instructions.md", instructions);
|
|
191
|
+
console.log("✅ .github/copilot-instructions.md created");
|
|
192
|
+
|
|
193
|
+
// ── .vscode/mcp.json ─────────────────────────────────────────────────────
|
|
194
|
+
fs.mkdirSync(".vscode", { recursive: true });
|
|
195
|
+
const mcpServerPath = path.join(PKG_ROOT, "mcp-server", "server.py");
|
|
196
|
+
const mcpConfig = {
|
|
197
|
+
servers: {
|
|
198
|
+
"reactjs-quality-agent": {
|
|
199
|
+
type: "stdio",
|
|
200
|
+
command: "python",
|
|
201
|
+
args: [mcpServerPath],
|
|
202
|
+
env: {}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
fs.writeFileSync(".vscode/mcp.json", JSON.stringify(mcpConfig, null, 2));
|
|
207
|
+
console.log("✅ .vscode/mcp.json configured");
|
|
208
|
+
|
|
209
|
+
// ── docs stubs ────────────────────────────────────────────────────────────
|
|
210
|
+
fs.mkdirSync("docs", { recursive: true });
|
|
211
|
+
if (!fs.existsSync("docs/architecture.md"))
|
|
212
|
+
fs.writeFileSync("docs/architecture.md", `# Frontend Architecture\n\nRun \`reactjsquality-check911 scan\` to generate this file.\n`);
|
|
213
|
+
if (!fs.existsSync("docs/component-index.md"))
|
|
214
|
+
fs.writeFileSync("docs/component-index.md", `# Component Index\n\nRun \`reactjsquality-check911 scan\` to generate this file.\n`);
|
|
215
|
+
console.log("✅ docs/ stubs created");
|
|
216
|
+
|
|
217
|
+
console.log(`
|
|
218
|
+
Setup complete for ${framework.label}!
|
|
219
|
+
|
|
220
|
+
Enabled checks:
|
|
221
|
+
${Object.entries(checks).map(([k, v]) => ` ${v ? "✅" : "❌"} ${k}`).join("\n")}
|
|
222
|
+
|
|
223
|
+
Next steps:
|
|
224
|
+
reactjsquality-check911 scan — index your components for Copilot
|
|
225
|
+
reactjsquality-check911 hooks — install pre-commit and pre-push git hooks
|
|
226
|
+
`);
|
|
227
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
|
|
6
|
+
const CONFIG_FILE = ".reactjs-quality-agent.json";
|
|
7
|
+
|
|
8
|
+
module.exports = function playwright() {
|
|
9
|
+
const config = (() => {
|
|
10
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8")); } catch (_) { return null; }
|
|
11
|
+
})();
|
|
12
|
+
|
|
13
|
+
if (!config) {
|
|
14
|
+
console.error("No .reactjs-quality-agent.json found — run `reactjsquality-check911 init` first.");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
if (!config.checks.playwright) {
|
|
18
|
+
console.log("Playwright check is disabled — skipping.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Verify playwright is installed
|
|
23
|
+
if (!fs.existsSync("node_modules/.bin/playwright") && !fs.existsSync("node_modules/@playwright/test")) {
|
|
24
|
+
console.log("⚠️ Playwright not installed — skipping.");
|
|
25
|
+
console.log(" Run: npm install -D @playwright/test && npx playwright install");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Verify playwright config exists
|
|
30
|
+
const configFiles = ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs"];
|
|
31
|
+
if (!configFiles.some(f => fs.existsSync(f))) {
|
|
32
|
+
console.log("⚠️ No playwright.config.ts found — skipping.");
|
|
33
|
+
console.log(" Run: npx playwright init");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log("Running Playwright smoke tests...\n");
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const out = execSync("npx playwright test --reporter=list", {
|
|
41
|
+
encoding: "utf8",
|
|
42
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
43
|
+
});
|
|
44
|
+
console.log(out);
|
|
45
|
+
console.log("✅ All Playwright tests passed.");
|
|
46
|
+
} catch (e) {
|
|
47
|
+
const output = (e.stdout || "") + (e.stderr || "");
|
|
48
|
+
console.log(output);
|
|
49
|
+
console.log("\n❌ Playwright tests failed — fix failing tests before pushing.");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const CONFIG_FILE = ".reactjs-quality-agent.json";
|
|
8
|
+
|
|
9
|
+
function loadConfig() {
|
|
10
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8")); } catch (_) { return null; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getStagedFiles() {
|
|
14
|
+
try {
|
|
15
|
+
const out = execSync("git diff --cached --name-only", { encoding: "utf8" });
|
|
16
|
+
return out.split("\n").map(f => f.trim())
|
|
17
|
+
.filter(f => f && /\.(js|jsx|ts|tsx|vue|svelte)$/.test(f) && fs.existsSync(f));
|
|
18
|
+
} catch (_) { return []; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getChangedLineRanges() {
|
|
22
|
+
try {
|
|
23
|
+
const diff = execSync("git diff --cached --unified=0", { encoding: "utf8" });
|
|
24
|
+
const ranges = {};
|
|
25
|
+
let cur = null;
|
|
26
|
+
for (const line of diff.split("\n")) {
|
|
27
|
+
const fileM = /^\+\+\+ b\/(.+)$/.exec(line);
|
|
28
|
+
if (fileM) { cur = fileM[1]; if (!ranges[cur]) ranges[cur] = []; continue; }
|
|
29
|
+
if (line.startsWith("+++ /dev/null")) { cur = null; continue; }
|
|
30
|
+
const hunkM = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line);
|
|
31
|
+
if (hunkM && cur) {
|
|
32
|
+
const start = parseInt(hunkM[1]);
|
|
33
|
+
const count = hunkM[2] !== undefined ? parseInt(hunkM[2]) : 1;
|
|
34
|
+
if (count > 0) ranges[cur].push([start, start + count - 1]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return ranges;
|
|
38
|
+
} catch (_) { return {}; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findRanges(filePath, changedRanges) {
|
|
42
|
+
const norm = filePath.replace(/\\/g, "/").toLowerCase();
|
|
43
|
+
for (const [gitPath, ranges] of Object.entries(changedRanges)) {
|
|
44
|
+
const gn = gitPath.toLowerCase();
|
|
45
|
+
if (norm === gn || norm.endsWith("/" + gn)) return ranges;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function inChangedRange(lineNum, ranges) {
|
|
51
|
+
if (!ranges || isNaN(lineNum)) return false;
|
|
52
|
+
return ranges.some(([s, e]) => lineNum >= s && lineNum <= e);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function runEslint(staged, changedRanges) {
|
|
56
|
+
process.stdout.write("Running ESLint... ");
|
|
57
|
+
try {
|
|
58
|
+
const fileArgs = staged.map(f => `"${f}"`).join(" ");
|
|
59
|
+
let output = "";
|
|
60
|
+
try {
|
|
61
|
+
output = execSync(`npx eslint ${fileArgs} --format json`, {
|
|
62
|
+
encoding: "utf8", stdio: ["pipe", "pipe", "pipe"]
|
|
63
|
+
});
|
|
64
|
+
} catch (e) {
|
|
65
|
+
// ESLint exits with code 1 when violations found — stdout still has JSON
|
|
66
|
+
output = e.stdout || "[]";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const data = JSON.parse(output || "[]");
|
|
70
|
+
const viols = [];
|
|
71
|
+
const errors = [];
|
|
72
|
+
|
|
73
|
+
for (const file of data) {
|
|
74
|
+
const ranges = findRanges(file.filePath, changedRanges);
|
|
75
|
+
if (!ranges) continue;
|
|
76
|
+
for (const msg of file.messages) {
|
|
77
|
+
if (!inChangedRange(msg.line, ranges)) continue;
|
|
78
|
+
const label = msg.severity === 2 ? "error" : "warn";
|
|
79
|
+
const entry = ` [ESLint:${msg.ruleId || "unknown"}] ${path.basename(file.filePath)}:${msg.line} — ${msg.message} (${label})`;
|
|
80
|
+
viols.push(entry);
|
|
81
|
+
if (msg.severity === 2) errors.push(entry);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (errors.length) {
|
|
86
|
+
console.log("❌");
|
|
87
|
+
viols.forEach(v => console.log(v));
|
|
88
|
+
return { passed: false, summary: `❌ ESLint: ${errors.length} error(s) in your changed lines` };
|
|
89
|
+
}
|
|
90
|
+
console.log("✅");
|
|
91
|
+
if (viols.length) viols.forEach(v => console.log(v)); // show warnings only
|
|
92
|
+
return { passed: true, summary: "✅ ESLint passed" };
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.log("⚠️");
|
|
95
|
+
return { passed: true, summary: "⚠️ ESLint: not installed — run `npm install eslint` in your project" };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function runTypeScript(staged) {
|
|
100
|
+
const tsFiles = staged.filter(f => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
101
|
+
process.stdout.write("Running TypeScript... ");
|
|
102
|
+
if (!tsFiles.length) {
|
|
103
|
+
console.log("✅ (no TS files staged)");
|
|
104
|
+
return { passed: true, summary: "✅ TypeScript: no TS files staged" };
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
execSync("npx tsc --noEmit", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
108
|
+
console.log("✅");
|
|
109
|
+
return { passed: true, summary: "✅ TypeScript passed" };
|
|
110
|
+
} catch (e) {
|
|
111
|
+
const lines = ((e.stdout || "") + (e.stderr || ""))
|
|
112
|
+
.split("\n").filter(l => l.includes("error TS")).slice(0, 5);
|
|
113
|
+
console.log("❌");
|
|
114
|
+
lines.forEach(l => console.log(" ", l.trim()));
|
|
115
|
+
return { passed: false, summary: "❌ TypeScript: type errors found" };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = function quality() {
|
|
120
|
+
const config = loadConfig();
|
|
121
|
+
if (!config) {
|
|
122
|
+
console.error("No .reactjs-quality-agent.json found — run `reactjsquality-check911 init` first.");
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const staged = getStagedFiles();
|
|
127
|
+
if (!staged.length) {
|
|
128
|
+
console.log("No staged JS/TS files — skipping quality checks.");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const changedRanges = getChangedLineRanges();
|
|
133
|
+
console.log(`Checking changed chunks in: ${staged.map(f => path.basename(f)).join(", ")}\n`);
|
|
134
|
+
|
|
135
|
+
const results = [];
|
|
136
|
+
let failed = false;
|
|
137
|
+
|
|
138
|
+
if (config.checks.eslint !== false) {
|
|
139
|
+
const r = runEslint(staged, changedRanges);
|
|
140
|
+
results.push(r.summary);
|
|
141
|
+
if (!r.passed) failed = true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const r2 = runTypeScript(staged);
|
|
145
|
+
results.push(r2.summary);
|
|
146
|
+
if (!r2.passed) failed = true;
|
|
147
|
+
|
|
148
|
+
console.log("\n--- Quality Summary ---");
|
|
149
|
+
results.forEach(r => console.log(r));
|
|
150
|
+
|
|
151
|
+
if (failed) {
|
|
152
|
+
console.log("\nFix violations and commit again, or ask Copilot:");
|
|
153
|
+
console.log(" 'Fix the quality violations in my staged files'");
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log("\nAll quality checks passed.");
|
|
158
|
+
};
|