mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
chore: upgrade TypeScript 7 (#38575)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
d72ee32ba1
commit
512f39dede
@ -67,7 +67,7 @@
|
|||||||
"test:e2e:local": "DIFY_E2E_MODE=local vp test --config vitest.e2e.config.ts",
|
"test:e2e:local": "DIFY_E2E_MODE=local vp test --config vitest.e2e.config.ts",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"lint:fix": "eslint --fix",
|
"lint:fix": "eslint --fix",
|
||||||
"type-check": "tsgo",
|
"type-check": "tsc",
|
||||||
"tree:gen": "bun scripts/generate-command-tree.ts",
|
"tree:gen": "bun scripts/generate-command-tree.ts",
|
||||||
"tree:check": "bun scripts/generate-command-tree.ts --check",
|
"tree:check": "bun scripts/generate-command-tree.ts --check",
|
||||||
"prebuild": "pnpm tree:gen",
|
"prebuild": "pnpm tree:gen",
|
||||||
@ -101,7 +101,7 @@
|
|||||||
"@types/js-yaml": "catalog:",
|
"@types/js-yaml": "catalog:",
|
||||||
"@types/lockfile": "catalog:",
|
"@types/lockfile": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native": "catalog:",
|
||||||
"@vitest/coverage-v8": "catalog:",
|
"@vitest/coverage-v8": "catalog:",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
"hono": "catalog:",
|
"hono": "catalog:",
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
"e2e:middleware:up": "tsx ./scripts/setup.ts middleware-up",
|
"e2e:middleware:up": "tsx ./scripts/setup.ts middleware-up",
|
||||||
"e2e:reset": "tsx ./scripts/setup.ts reset",
|
"e2e:reset": "tsx ./scripts/setup.ts reset",
|
||||||
"seed": "tsx ./scripts/seed.ts",
|
"seed": "tsx ./scripts/seed.ts",
|
||||||
"type-check": "tsgo"
|
"type-check": "tsc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cucumber/cucumber": "catalog:",
|
"@cucumber/cucumber": "catalog:",
|
||||||
@ -23,7 +23,7 @@
|
|||||||
"@playwright/test": "catalog:",
|
"@playwright/test": "catalog:",
|
||||||
"@t3-oss/env-core": "catalog:",
|
"@t3-oss/env-core": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native": "catalog:",
|
||||||
"tsx": "catalog:",
|
"tsx": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
|
|||||||
@ -6910,11 +6910,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/scripts/component-analyzer.js": {
|
|
||||||
"regexp/no-unused-capturing-group": {
|
|
||||||
"count": 6
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web/service/__tests__/base.spec.ts": {
|
"web/service/__tests__/base.spec.ts": {
|
||||||
"no-restricted-imports": {
|
"no-restricted-imports": {
|
||||||
"count": 1
|
"count": 1
|
||||||
|
|||||||
@ -24,11 +24,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "catalog:",
|
"@antfu/eslint-config": "catalog:",
|
||||||
|
"@typescript/native": "catalog:",
|
||||||
"concurrently": "catalog:",
|
"concurrently": "catalog:",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
"eslint-markdown": "catalog:",
|
"eslint-markdown": "catalog:",
|
||||||
"eslint-plugin-markdown-preferences": "catalog:",
|
"eslint-plugin-markdown-preferences": "catalog:",
|
||||||
"eslint-plugin-no-barrel-files": "catalog:",
|
"eslint-plugin-no-barrel-files": "catalog:",
|
||||||
|
"typescript": "catalog:",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"vite-plus": "catalog:"
|
"vite-plus": "catalog:"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
"gen-api-contract-from-openapi": "node -e \"fs.rmSync('generated/api', { recursive: true, force: true })\" && openapi-ts -f openapi-ts.api.config.ts && vp fmt generated/api && eslint --fix generated/api",
|
"gen-api-contract-from-openapi": "node -e \"fs.rmSync('generated/api', { recursive: true, force: true })\" && openapi-ts -f openapi-ts.api.config.ts && vp fmt generated/api && eslint --fix generated/api",
|
||||||
"gen-api-openapi": "uv run --project ../../api ../../api/dev/generate_swagger_specs.py --output-dir openapi",
|
"gen-api-openapi": "uv run --project ../../api ../../api/dev/generate_swagger_specs.py --output-dir openapi",
|
||||||
"gen-enterprise-contract": "openapi-ts -f openapi-ts.enterprise.config.ts",
|
"gen-enterprise-contract": "openapi-ts -f openapi-ts.enterprise.config.ts",
|
||||||
"type-check": "tsgo"
|
"type-check": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@orpc/contract": "catalog:",
|
"@orpc/contract": "catalog:",
|
||||||
@ -33,7 +33,7 @@
|
|||||||
"@hey-api/openapi-ts": "catalog:",
|
"@hey-api/openapi-ts": "catalog:",
|
||||||
"@types/js-yaml": "catalog:",
|
"@types/js-yaml": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native": "catalog:",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
"js-yaml": "catalog:",
|
"js-yaml": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
"build": "vp pack",
|
"build": "vp pack",
|
||||||
"prepare": "pnpm run build",
|
"prepare": "pnpm run build",
|
||||||
"test": "vp test",
|
"test": "vp test",
|
||||||
"type-check": "tsgo",
|
"type-check": "tsc",
|
||||||
"prepublish": "pnpm run build"
|
"prepublish": "pnpm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -36,7 +36,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dify/tsconfig": "workspace:*",
|
"@dify/tsconfig": "workspace:*",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native": "catalog:",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"vite-plus": "catalog:",
|
"vite-plus": "catalog:",
|
||||||
"vitest": "catalog:"
|
"vitest": "catalog:"
|
||||||
|
|||||||
@ -156,7 +156,7 @@
|
|||||||
"test": "vp test --project unit",
|
"test": "vp test --project unit",
|
||||||
"test:storybook": "vp test --project storybook --run",
|
"test:storybook": "vp test --project storybook --run",
|
||||||
"test:watch": "vp test --project unit --watch",
|
"test:watch": "vp test --project unit --watch",
|
||||||
"type-check": "tsgo"
|
"type-check": "tsc"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@base-ui/react": "catalog:",
|
"@base-ui/react": "catalog:",
|
||||||
@ -186,7 +186,7 @@
|
|||||||
"@tanstack/react-virtual": "catalog:",
|
"@tanstack/react-virtual": "catalog:",
|
||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
"@types/react-dom": "catalog:",
|
"@types/react-dom": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native": "catalog:",
|
||||||
"@vitejs/plugin-react": "catalog:",
|
"@vitejs/plugin-react": "catalog:",
|
||||||
"@vitest/browser": "catalog:",
|
"@vitest/browser": "catalog:",
|
||||||
"@vitest/browser-playwright": "catalog:",
|
"@vitest/browser-playwright": "catalog:",
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vp test",
|
"test": "vp test",
|
||||||
"type-check": "tsgo"
|
"type-check": "tsc"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tanstack/form-core": "catalog:",
|
"@tanstack/form-core": "catalog:",
|
||||||
@ -20,7 +20,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dify/tsconfig": "workspace:*",
|
"@dify/tsconfig": "workspace:*",
|
||||||
"@tanstack/form-core": "catalog:",
|
"@tanstack/form-core": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native": "catalog:",
|
||||||
"jotai": "catalog:",
|
"jotai": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
|
|||||||
@ -8,10 +8,10 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vp pack",
|
"build": "vp pack",
|
||||||
"type-check": "tsgo"
|
"type-check": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native": "catalog:",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
2122
pnpm-lock.yaml
generated
2122
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -131,7 +131,7 @@ catalog:
|
|||||||
'@types/sortablejs': 1.15.9
|
'@types/sortablejs': 1.15.9
|
||||||
'@typescript-eslint/eslint-plugin': 8.62.0
|
'@typescript-eslint/eslint-plugin': 8.62.0
|
||||||
'@typescript-eslint/parser': 8.62.0
|
'@typescript-eslint/parser': 8.62.0
|
||||||
'@typescript/native-preview': 7.0.0-dev.20260627.2
|
'@typescript/native': npm:typescript@7.0.2
|
||||||
'@vitejs/plugin-react': 6.0.3
|
'@vitejs/plugin-react': 6.0.3
|
||||||
'@vitejs/plugin-rsc': 0.5.27
|
'@vitejs/plugin-rsc': 0.5.27
|
||||||
'@vitest/browser': 4.1.9
|
'@vitest/browser': 4.1.9
|
||||||
@ -169,7 +169,6 @@ catalog:
|
|||||||
eslint-plugin-markdown-preferences: 0.41.1
|
eslint-plugin-markdown-preferences: 0.41.1
|
||||||
eslint-plugin-no-barrel-files: 1.3.1
|
eslint-plugin-no-barrel-files: 1.3.1
|
||||||
eslint-plugin-react-refresh: 0.5.3
|
eslint-plugin-react-refresh: 0.5.3
|
||||||
eslint-plugin-sonarjs: 4.1.0
|
|
||||||
eslint-plugin-storybook: 10.4.6
|
eslint-plugin-storybook: 10.4.6
|
||||||
eventsource-parser: 3.1.0
|
eventsource-parser: 3.1.0
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
@ -242,7 +241,7 @@ catalog:
|
|||||||
tailwindcss: 4.3.1
|
tailwindcss: 4.3.1
|
||||||
tldts: 7.4.4
|
tldts: 7.4.4
|
||||||
tsx: 4.22.4
|
tsx: 4.22.4
|
||||||
typescript: 6.0.3
|
typescript: npm:@typescript/typescript6@6.0.2
|
||||||
uglify-js: 3.19.3
|
uglify-js: 3.19.3
|
||||||
undici: 7.28.0
|
undici: 7.28.0
|
||||||
unist-util-visit: 5.1.0
|
unist-util-visit: 5.1.0
|
||||||
|
|||||||
@ -48,7 +48,7 @@
|
|||||||
"build": "vp pack",
|
"build": "vp pack",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"lint:fix": "eslint --fix",
|
"lint:fix": "eslint --fix",
|
||||||
"type-check": "tsgo",
|
"type-check": "tsc",
|
||||||
"test": "vp test",
|
"test": "vp test",
|
||||||
"test:coverage": "vp test --coverage",
|
"test:coverage": "vp test --coverage",
|
||||||
"publish:check": "./scripts/publish.sh --dry-run",
|
"publish:check": "./scripts/publish.sh --dry-run",
|
||||||
@ -60,7 +60,7 @@
|
|||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||||
"@typescript-eslint/parser": "catalog:",
|
"@typescript-eslint/parser": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native": "catalog:",
|
||||||
"@vitest/coverage-v8": "catalog:",
|
"@vitest/coverage-v8": "catalog:",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
|
|||||||
@ -140,16 +140,6 @@ If you are not familiar with writing tests, refer to:
|
|||||||
|
|
||||||
- [index.spec.tsx] - Component test example
|
- [index.spec.tsx] - Component test example
|
||||||
|
|
||||||
### Analyze Component Complexity
|
|
||||||
|
|
||||||
Before writing tests, use the script to analyze component complexity:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm analyze-component app/components/your-component/index.tsx
|
|
||||||
```
|
|
||||||
|
|
||||||
This will help you determine the testing strategy. See [web/docs/test.md] for details.
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Visit <https://docs.dify.ai> to view the full documentation.
|
Visit <https://docs.dify.ai> to view the full documentation.
|
||||||
|
|||||||
@ -30,11 +30,6 @@ pnpm test path/to/file.spec.tsx
|
|||||||
- **Global setup**: `vitest.setup.ts` already imports `@testing-library/jest-dom`, runs `cleanup()` after every test, and defines shared mocks (for example `react-i18next`). Add any environment-level mocks (for example `ResizeObserver`, `matchMedia`, `IntersectionObserver`, `TextEncoder`, `crypto`) here so they are shared consistently.
|
- **Global setup**: `vitest.setup.ts` already imports `@testing-library/jest-dom`, runs `cleanup()` after every test, and defines shared mocks (for example `react-i18next`). Add any environment-level mocks (for example `ResizeObserver`, `matchMedia`, `IntersectionObserver`, `TextEncoder`, `crypto`) here so they are shared consistently.
|
||||||
- **Reusable mocks**: Place shared mock factories inside `web/__mocks__/` and use `vi.mock('module-name')` to point to them rather than redefining mocks in every spec.
|
- **Reusable mocks**: Place shared mock factories inside `web/__mocks__/` and use `vi.mock('module-name')` to point to them rather than redefining mocks in every spec.
|
||||||
- **Mocking behavior**: Modules are not mocked automatically. Use `vi.mock(...)` in tests, or place global mocks in `vitest.setup.ts`.
|
- **Mocking behavior**: Modules are not mocked automatically. Use `vi.mock(...)` in tests, or place global mocks in `vitest.setup.ts`.
|
||||||
- **Script utilities**: `web/scripts/analyze-component.js` analyzes component complexity and generates test prompts for AI assistants. Commands:
|
|
||||||
- `pnpm analyze-component <path>` - Analyze and generate test prompt
|
|
||||||
- `pnpm analyze-component <path> --json` - Output analysis as JSON
|
|
||||||
- `pnpm analyze-component <path> --review` - Generate test review prompt
|
|
||||||
- `pnpm analyze-component --help` - Show help
|
|
||||||
- **Integration suites**: Files in `web/__tests__/` exercise cross-component flows. Prefer adding new end-to-end style specs there rather than mixing them into component directories.
|
- **Integration suites**: Files in `web/__tests__/` exercise cross-component flows. Prefer adding new end-to-end style specs there rather than mixing them into component directories.
|
||||||
|
|
||||||
## Test Authoring Principles
|
## Test Authoring Principles
|
||||||
@ -52,29 +47,6 @@ pnpm test path/to/file.spec.tsx
|
|||||||
- **Fast & stable**: Keep unit tests running in milliseconds; reserve integration tests for cross-module behavior with isolation.
|
- **Fast & stable**: Keep unit tests running in milliseconds; reserve integration tests for cross-module behavior with isolation.
|
||||||
- **Structured describe blocks**: Organize tests with `describe` sections and add a brief comment before each block to explain the scenario it covers so readers can quickly understand the scope.
|
- **Structured describe blocks**: Organize tests with `describe` sections and add a brief comment before each block to explain the scenario it covers so readers can quickly understand the scope.
|
||||||
|
|
||||||
## Component Complexity Guidelines
|
|
||||||
|
|
||||||
Use `pnpm analyze-component <path>` to analyze component complexity and adopt different testing strategies based on the results.
|
|
||||||
|
|
||||||
### 🔴 Very Complex Components (Complexity > 50)
|
|
||||||
|
|
||||||
- **Refactor first**: Break component into smaller pieces
|
|
||||||
- **Integration tests**: Test complex workflows end-to-end
|
|
||||||
- **Data-driven tests**: Use `test.each()` for multiple scenarios
|
|
||||||
- **Performance benchmarks**: Add performance tests for critical paths
|
|
||||||
|
|
||||||
### ⚠️ Complex Components (Complexity 30-50)
|
|
||||||
|
|
||||||
- **Multiple describe blocks**: Group related test cases
|
|
||||||
- **Integration scenarios**: Test feature combinations
|
|
||||||
- **Organized structure**: Keep tests maintainable
|
|
||||||
|
|
||||||
### 📏 Large Components (500+ lines)
|
|
||||||
|
|
||||||
- **Consider refactoring**: Split into smaller components if possible
|
|
||||||
- **Section testing**: Test major sections separately
|
|
||||||
- **Helper functions**: Reduce test complexity with utilities
|
|
||||||
|
|
||||||
## Basic Guidelines
|
## Basic Guidelines
|
||||||
|
|
||||||
- ✅ AAA pattern: Arrange (setup) → Act (execute) → Assert (verify)
|
- ✅ AAA pattern: Arrange (setup) → Act (execute) → Assert (verify)
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import hyoban from 'eslint-plugin-hyoban'
|
|||||||
import jsxA11y from 'eslint-plugin-jsx-a11y'
|
import jsxA11y from 'eslint-plugin-jsx-a11y'
|
||||||
import markdownPreferences from 'eslint-plugin-markdown-preferences'
|
import markdownPreferences from 'eslint-plugin-markdown-preferences'
|
||||||
import noBarrelFiles from 'eslint-plugin-no-barrel-files'
|
import noBarrelFiles from 'eslint-plugin-no-barrel-files'
|
||||||
import sonar from 'eslint-plugin-sonarjs'
|
|
||||||
import storybook from 'eslint-plugin-storybook'
|
import storybook from 'eslint-plugin-storybook'
|
||||||
import {
|
import {
|
||||||
GENERATED_IGNORES,
|
GENERATED_IGNORES,
|
||||||
@ -105,16 +104,6 @@ export default antfu(
|
|||||||
},
|
},
|
||||||
storybook.configs['flat/recommended'],
|
storybook.configs['flat/recommended'],
|
||||||
...pluginQuery.configs['flat/recommended'],
|
...pluginQuery.configs['flat/recommended'],
|
||||||
// sonar
|
|
||||||
{
|
|
||||||
rules: {
|
|
||||||
// Manually pick rules that are actually useful and not slow.
|
|
||||||
// Or we can just drop the plugin entirely.
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
sonarjs: sonar,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
files: [GLOB_TS, GLOB_TSX],
|
files: [GLOB_TS, GLOB_TSX],
|
||||||
ignores: GLOB_TESTS,
|
ignores: GLOB_TESTS,
|
||||||
@ -150,7 +139,6 @@ export default antfu(
|
|||||||
{
|
{
|
||||||
files: ['i18n/**/*.json'],
|
files: ['i18n/**/*.json'],
|
||||||
rules: {
|
rules: {
|
||||||
'sonarjs/max-lines': 'off',
|
|
||||||
'max-lines': 'off',
|
'max-lines': 'off',
|
||||||
'jsonc/sort-keys': 'error',
|
'jsonc/sort-keys': 'error',
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,6 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "next experimental-analyze",
|
"analyze": "next experimental-analyze",
|
||||||
"analyze-component": "node ./scripts/analyze-component.js",
|
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"build:vinext": "vinext build",
|
"build:vinext": "vinext build",
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@ -39,7 +38,6 @@
|
|||||||
"knip:production-unused-check": "node ./scripts/check-production-unused-after-knip-fix.mjs",
|
"knip:production-unused-check": "node ./scripts/check-production-unused-after-knip-fix.mjs",
|
||||||
"lint:tss": "tsslint --project tsconfig.json",
|
"lint:tss": "tsslint --project tsconfig.json",
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"refactor-component": "node ./scripts/refactor-component.js",
|
|
||||||
"start": "node ./scripts/copy-and-start.mjs",
|
"start": "node ./scripts/copy-and-start.mjs",
|
||||||
"start:vinext": "vinext start",
|
"start:vinext": "vinext start",
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
@ -47,7 +45,7 @@
|
|||||||
"test": "vp test",
|
"test": "vp test",
|
||||||
"test:coverage": "vp test --coverage",
|
"test:coverage": "vp test --coverage",
|
||||||
"test:watch": "vp test --watch",
|
"test:watch": "vp test --watch",
|
||||||
"type-check": "tsgo",
|
"type-check": "tsc",
|
||||||
"uglify-embed": "node ./bin/uglify-embed"
|
"uglify-embed": "node ./bin/uglify-embed"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -205,7 +203,7 @@
|
|||||||
"@types/react-dom": "catalog:",
|
"@types/react-dom": "catalog:",
|
||||||
"@types/sortablejs": "catalog:",
|
"@types/sortablejs": "catalog:",
|
||||||
"@typescript-eslint/parser": "catalog:",
|
"@typescript-eslint/parser": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native": "catalog:",
|
||||||
"@vitejs/plugin-react": "catalog:",
|
"@vitejs/plugin-react": "catalog:",
|
||||||
"@vitejs/plugin-rsc": "catalog:",
|
"@vitejs/plugin-rsc": "catalog:",
|
||||||
"@vitest/coverage-v8": "catalog:",
|
"@vitest/coverage-v8": "catalog:",
|
||||||
@ -219,7 +217,6 @@
|
|||||||
"eslint-plugin-markdown-preferences": "catalog:",
|
"eslint-plugin-markdown-preferences": "catalog:",
|
||||||
"eslint-plugin-no-barrel-files": "catalog:",
|
"eslint-plugin-no-barrel-files": "catalog:",
|
||||||
"eslint-plugin-react-refresh": "catalog:",
|
"eslint-plugin-react-refresh": "catalog:",
|
||||||
"eslint-plugin-sonarjs": "catalog:",
|
|
||||||
"eslint-plugin-storybook": "catalog:",
|
"eslint-plugin-storybook": "catalog:",
|
||||||
"happy-dom": "catalog:",
|
"happy-dom": "catalog:",
|
||||||
"knip": "catalog:",
|
"knip": "catalog:",
|
||||||
|
|||||||
@ -1,583 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import { spawnSync } from 'node:child_process'
|
|
||||||
import fs from 'node:fs'
|
|
||||||
import path from 'node:path'
|
|
||||||
import {
|
|
||||||
ComponentAnalyzer,
|
|
||||||
extractCopyContent,
|
|
||||||
getComplexityLevel,
|
|
||||||
listAnalyzableFiles,
|
|
||||||
resolveDirectoryEntry,
|
|
||||||
} from './component-analyzer.js'
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Prompt Builder for AI Assistants
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
class TestPromptBuilder {
|
|
||||||
build(analysis) {
|
|
||||||
const testPath = analysis.path.replace(/\.tsx?$/, '.spec.tsx')
|
|
||||||
|
|
||||||
return `
|
|
||||||
╔════════════════════════════════════════════════════════════════════════════╗
|
|
||||||
║ 📋 GENERATE TEST FOR DIFY COMPONENT ║
|
|
||||||
╚════════════════════════════════════════════════════════════════════════════╝
|
|
||||||
|
|
||||||
📍 Component: ${analysis.name}
|
|
||||||
📂 Path: ${analysis.path}
|
|
||||||
🎯 Test File: ${testPath}
|
|
||||||
|
|
||||||
📊 Component Analysis:
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
Type: ${analysis.type}
|
|
||||||
Total Complexity: ${analysis.complexity}/100 ${getComplexityLevel(analysis.complexity)}
|
|
||||||
Max Func Complexity: ${analysis.maxComplexity}/100 ${getComplexityLevel(analysis.maxComplexity)}
|
|
||||||
Lines: ${analysis.lineCount}
|
|
||||||
Usage: ${analysis.usageCount} reference${analysis.usageCount !== 1 ? 's' : ''}
|
|
||||||
Test Priority: ${analysis.priority.score} ${analysis.priority.level}
|
|
||||||
|
|
||||||
Features Detected:
|
|
||||||
${analysis.hasProps ? '✓' : '✗'} Props/TypeScript interfaces
|
|
||||||
${analysis.hasState ? '✓' : '✗'} Local state (useState/useReducer)
|
|
||||||
${analysis.hasEffects ? '✓' : '✗'} Side effects (useEffect)
|
|
||||||
${analysis.hasCallbacks ? '✓' : '✗'} Callbacks (useCallback)
|
|
||||||
${analysis.hasMemo ? '✓' : '✗'} Memoization (useMemo)
|
|
||||||
${analysis.hasEvents ? '✓' : '✗'} Event handlers
|
|
||||||
${analysis.hasRouter ? '✓' : '✗'} Next.js routing
|
|
||||||
${analysis.hasAPI ? '✓' : '✗'} API calls
|
|
||||||
${analysis.hasReactQuery ? '✓' : '✗'} React Query
|
|
||||||
${analysis.hasAhooks ? '✓' : '✗'} ahooks
|
|
||||||
${analysis.hasForwardRef ? '✓' : '✗'} Ref forwarding (forwardRef)
|
|
||||||
${analysis.hasComponentMemo ? '✓' : '✗'} Component memoization (React.memo)
|
|
||||||
${analysis.hasImperativeHandle ? '✓' : '✗'} Imperative handle
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
📝 TASK:
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
Please generate a comprehensive test file for this component at:
|
|
||||||
${testPath}
|
|
||||||
|
|
||||||
The component is located at:
|
|
||||||
${analysis.path}
|
|
||||||
|
|
||||||
${this.getSpecificGuidelines(analysis)}
|
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
📋 PROMPT FOR AI ASSISTANT (COPY THIS TO YOUR AI ASSISTANT):
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
Generate a comprehensive test file for all files in @${path.dirname(analysis.path)}
|
|
||||||
|
|
||||||
Including but not limited to:
|
|
||||||
${this.buildFocusPoints(analysis)}
|
|
||||||
|
|
||||||
Create the test file at: ${testPath}
|
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFocusPoints(analysis) {
|
|
||||||
const points = []
|
|
||||||
|
|
||||||
if (analysis.hasState)
|
|
||||||
points.push('- Testing state management and updates')
|
|
||||||
if (analysis.hasEffects)
|
|
||||||
points.push('- Testing side effects and cleanup')
|
|
||||||
if (analysis.hasCallbacks)
|
|
||||||
points.push('- Testing callback stability and memoization')
|
|
||||||
if (analysis.hasMemo)
|
|
||||||
points.push('- Testing memoization logic and dependencies')
|
|
||||||
if (analysis.hasEvents)
|
|
||||||
points.push('- Testing user interactions and event handlers')
|
|
||||||
if (analysis.hasRouter)
|
|
||||||
points.push('- Mocking Next.js router hooks')
|
|
||||||
if (analysis.hasAPI)
|
|
||||||
points.push('- Mocking API calls')
|
|
||||||
if (analysis.hasForwardRef)
|
|
||||||
points.push('- Testing ref forwarding behavior')
|
|
||||||
if (analysis.hasComponentMemo)
|
|
||||||
points.push('- Testing component memoization')
|
|
||||||
if (analysis.hasSuspense)
|
|
||||||
points.push('- Testing Suspense boundaries and lazy loading')
|
|
||||||
if (analysis.hasPortal)
|
|
||||||
points.push('- Testing Portal rendering')
|
|
||||||
if (analysis.hasImperativeHandle)
|
|
||||||
points.push('- Testing imperative handle methods')
|
|
||||||
points.push('- Testing edge cases and error handling')
|
|
||||||
points.push('- Testing all prop variations')
|
|
||||||
|
|
||||||
return points.join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
getSpecificGuidelines(analysis) {
|
|
||||||
const guidelines = []
|
|
||||||
|
|
||||||
// ===== Test Priority Guidance =====
|
|
||||||
if (analysis.priority.level.includes('CRITICAL')) {
|
|
||||||
guidelines.push('🔴 CRITICAL PRIORITY component:')
|
|
||||||
guidelines.push(` - Used in ${analysis.usageCount} places across the codebase`)
|
|
||||||
guidelines.push(' - Changes will have WIDE impact')
|
|
||||||
guidelines.push(' - Require comprehensive test coverage')
|
|
||||||
guidelines.push(' - Add regression tests for all use cases')
|
|
||||||
guidelines.push(' - Consider integration tests with dependent components')
|
|
||||||
}
|
|
||||||
else if (analysis.usageCount > 50) {
|
|
||||||
guidelines.push('🟠 VERY HIGH USAGE component:')
|
|
||||||
guidelines.push(` - Referenced ${analysis.usageCount} times in the codebase`)
|
|
||||||
guidelines.push(' - Changes may affect many parts of the application')
|
|
||||||
guidelines.push(' - Comprehensive test coverage is CRITICAL')
|
|
||||||
guidelines.push(' - Add tests for all common usage patterns')
|
|
||||||
guidelines.push(' - Consider regression tests')
|
|
||||||
}
|
|
||||||
else if (analysis.usageCount > 20) {
|
|
||||||
guidelines.push('🟡 HIGH USAGE component:')
|
|
||||||
guidelines.push(` - Referenced ${analysis.usageCount} times in the codebase`)
|
|
||||||
guidelines.push(' - Test coverage is important to prevent widespread bugs')
|
|
||||||
guidelines.push(' - Add tests for common usage patterns')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Complexity Warning =====
|
|
||||||
if (analysis.complexity > 75) {
|
|
||||||
guidelines.push(`🔴 HIGH Total Complexity (${analysis.complexity}/100). Consider:`)
|
|
||||||
guidelines.push(' - Splitting component into smaller pieces before testing')
|
|
||||||
guidelines.push(' - Creating integration tests for complex workflows')
|
|
||||||
guidelines.push(' - Using test.each() for data-driven tests')
|
|
||||||
}
|
|
||||||
else if (analysis.complexity > 50) {
|
|
||||||
guidelines.push(`⚠️ MODERATE Total Complexity (${analysis.complexity}/100). Consider:`)
|
|
||||||
guidelines.push(' - Breaking tests into multiple describe blocks')
|
|
||||||
guidelines.push(' - Testing integration scenarios')
|
|
||||||
guidelines.push(' - Grouping related test cases')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Max Function Complexity Warning =====
|
|
||||||
if (analysis.maxComplexity > 75) {
|
|
||||||
guidelines.push(`🔴 HIGH Single Function Complexity (max: ${analysis.maxComplexity}/100). Consider:`)
|
|
||||||
guidelines.push(' - Breaking down the complex function into smaller helpers')
|
|
||||||
guidelines.push(' - Extracting logic into custom hooks or utility functions')
|
|
||||||
}
|
|
||||||
else if (analysis.maxComplexity > 50) {
|
|
||||||
guidelines.push(`⚠️ MODERATE Single Function Complexity (max: ${analysis.maxComplexity}/100). Consider:`)
|
|
||||||
guidelines.push(' - Simplifying conditional logic')
|
|
||||||
guidelines.push(' - Using early returns to reduce nesting')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== State Management =====
|
|
||||||
if (analysis.hasState && analysis.hasEffects) {
|
|
||||||
guidelines.push('🔄 State + Effects detected:')
|
|
||||||
guidelines.push(' - Test state initialization and updates')
|
|
||||||
guidelines.push(' - Test useEffect dependencies array')
|
|
||||||
guidelines.push(' - Test cleanup functions (return from useEffect)')
|
|
||||||
guidelines.push(' - Use waitFor() for async state changes')
|
|
||||||
}
|
|
||||||
else if (analysis.hasState) {
|
|
||||||
guidelines.push('📊 State management detected:')
|
|
||||||
guidelines.push(' - Test initial state values')
|
|
||||||
guidelines.push(' - Test all state transitions')
|
|
||||||
guidelines.push(' - Test state reset/cleanup scenarios')
|
|
||||||
}
|
|
||||||
else if (analysis.hasEffects) {
|
|
||||||
guidelines.push('⚡ Side effects detected:')
|
|
||||||
guidelines.push(' - Test effect execution conditions')
|
|
||||||
guidelines.push(' - Verify dependencies array correctness')
|
|
||||||
guidelines.push(' - Test cleanup on unmount')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Performance Optimization =====
|
|
||||||
if (analysis.hasCallbacks || analysis.hasMemo || analysis.hasComponentMemo) {
|
|
||||||
const features = []
|
|
||||||
if (analysis.hasCallbacks)
|
|
||||||
features.push('useCallback')
|
|
||||||
if (analysis.hasMemo)
|
|
||||||
features.push('useMemo')
|
|
||||||
if (analysis.hasComponentMemo)
|
|
||||||
features.push('React.memo')
|
|
||||||
|
|
||||||
guidelines.push(`🚀 Performance optimization (${features.join(', ')}):`)
|
|
||||||
guidelines.push(' - Verify callbacks maintain referential equality')
|
|
||||||
guidelines.push(' - Test memoization dependencies')
|
|
||||||
guidelines.push(' - Ensure expensive computations are cached')
|
|
||||||
if (analysis.hasComponentMemo) {
|
|
||||||
guidelines.push(' - Test component re-render behavior with prop changes')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Ref Forwarding =====
|
|
||||||
if (analysis.hasForwardRef || analysis.hasImperativeHandle) {
|
|
||||||
guidelines.push('🔗 Ref forwarding detected:')
|
|
||||||
guidelines.push(' - Test ref attachment to DOM elements')
|
|
||||||
if (analysis.hasImperativeHandle) {
|
|
||||||
guidelines.push(' - Test all exposed imperative methods')
|
|
||||||
guidelines.push(' - Verify method behavior with different ref types')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Suspense and Lazy Loading =====
|
|
||||||
if (analysis.hasSuspense) {
|
|
||||||
guidelines.push('⏳ Suspense/Lazy loading detected:')
|
|
||||||
guidelines.push(' - Test fallback UI during loading')
|
|
||||||
guidelines.push(' - Test component behavior after lazy load completes')
|
|
||||||
guidelines.push(' - Test error boundaries with failed loads')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Portal =====
|
|
||||||
if (analysis.hasPortal) {
|
|
||||||
guidelines.push('🚪 Portal rendering detected:')
|
|
||||||
guidelines.push(' - Test content renders in portal target')
|
|
||||||
guidelines.push(' - Test portal cleanup on unmount')
|
|
||||||
guidelines.push(' - Verify event bubbling through portal')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== API Calls =====
|
|
||||||
if (analysis.hasAPI) {
|
|
||||||
guidelines.push('🌐 API calls detected:')
|
|
||||||
guidelines.push(' - Mock API calls/hooks (useQuery, useMutation, fetch, etc.)')
|
|
||||||
guidelines.push(' - Test loading, success, and error states')
|
|
||||||
guidelines.push(' - Focus on component behavior, not the data fetching lib')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== ahooks =====
|
|
||||||
if (analysis.hasAhooks) {
|
|
||||||
guidelines.push('🪝 ahooks detected (mock only, no need to test the lib):')
|
|
||||||
guidelines.push(' - Mock ahooks utilities (useBoolean, useRequest, etc.)')
|
|
||||||
guidelines.push(' - Focus on testing how your component uses the hooks')
|
|
||||||
guidelines.push(' - Use fake timers if debounce/throttle is involved')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Routing =====
|
|
||||||
if (analysis.hasRouter) {
|
|
||||||
guidelines.push('🔀 Next.js routing detected:')
|
|
||||||
guidelines.push(' - Mock useRouter, usePathname, useSearchParams')
|
|
||||||
guidelines.push(' - Test navigation behavior and parameters')
|
|
||||||
guidelines.push(' - Test query string handling')
|
|
||||||
guidelines.push(' - Verify route guards/redirects if any')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Event Handlers =====
|
|
||||||
if (analysis.hasEvents) {
|
|
||||||
guidelines.push('🎯 Event handlers detected:')
|
|
||||||
guidelines.push(' - Test all onClick, onChange, onSubmit handlers')
|
|
||||||
guidelines.push(' - Test keyboard events (Enter, Escape, etc.)')
|
|
||||||
guidelines.push(' - Verify event.preventDefault() calls if needed')
|
|
||||||
guidelines.push(' - Test event bubbling/propagation')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Domain-Specific Components =====
|
|
||||||
if (analysis.path.includes('workflow')) {
|
|
||||||
guidelines.push('⚙️ Workflow component:')
|
|
||||||
guidelines.push(' - Test node configuration and validation')
|
|
||||||
guidelines.push(' - Test data flow and variable passing')
|
|
||||||
guidelines.push(' - Test edge connections and graph structure')
|
|
||||||
guidelines.push(' - Verify error handling for invalid configs')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (analysis.path.includes('dataset')) {
|
|
||||||
guidelines.push('📚 Dataset component:')
|
|
||||||
guidelines.push(' - Test file upload and validation')
|
|
||||||
guidelines.push(' - Test pagination and data loading')
|
|
||||||
guidelines.push(' - Test search and filtering')
|
|
||||||
guidelines.push(' - Verify data format handling')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (analysis.path.includes('app/configuration') || analysis.path.includes('config')) {
|
|
||||||
guidelines.push('⚙️ Configuration component:')
|
|
||||||
guidelines.push(' - Test form validation thoroughly')
|
|
||||||
guidelines.push(' - Test save/reset functionality')
|
|
||||||
guidelines.push(' - Test required vs optional fields')
|
|
||||||
guidelines.push(' - Verify configuration persistence')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== File Size Warning =====
|
|
||||||
if (analysis.lineCount > 500) {
|
|
||||||
guidelines.push('📏 Large component (500+ lines):')
|
|
||||||
guidelines.push(' - Consider splitting into smaller components')
|
|
||||||
guidelines.push(' - Test major sections separately')
|
|
||||||
guidelines.push(' - Use helper functions to reduce test complexity')
|
|
||||||
}
|
|
||||||
|
|
||||||
return guidelines.length > 0 ? `\n${guidelines.join('\n')}\n` : ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestReviewPromptBuilder {
|
|
||||||
build({ analysis, testPath, originalPromptSection }) {
|
|
||||||
const formattedOriginalPrompt = originalPromptSection
|
|
||||||
? originalPromptSection
|
|
||||||
.split('\n')
|
|
||||||
.map(line => (line.trim().length > 0 ? ` ${line}` : ''))
|
|
||||||
.join('\n')
|
|
||||||
.trimEnd()
|
|
||||||
: ' (original generation prompt unavailable)'
|
|
||||||
|
|
||||||
return `
|
|
||||||
╔════════════════════════════════════════════════════════════════════════════╗
|
|
||||||
║ ✅ REVIEW TEST FOR DIFY COMPONENT ║
|
|
||||||
╚════════════════════════════════════════════════════════════════════════════╝
|
|
||||||
|
|
||||||
📂 Component Path: ${analysis.path}
|
|
||||||
🧪 Test File: ${testPath}
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
📝 REVIEW TASK:
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
📋 PROMPT FOR AI ASSISTANT (COPY THIS TO YOUR AI ASSISTANT):
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
You are reviewing the frontend test coverage for @${analysis.path}.
|
|
||||||
|
|
||||||
Original generation requirements:
|
|
||||||
${formattedOriginalPrompt}
|
|
||||||
|
|
||||||
Test file under review:
|
|
||||||
${testPath}
|
|
||||||
|
|
||||||
Checklist (ensure every item is addressed in your review):
|
|
||||||
- Confirm the tests satisfy all requirements listed above and in web/docs/test.md.
|
|
||||||
- Verify Arrange → Act → Assert structure, mocks, and cleanup follow project conventions.
|
|
||||||
- Ensure all detected component features (state, effects, routing, API, events, etc.) are exercised, including edge cases and error paths.
|
|
||||||
- Check coverage of prop variations, null/undefined inputs, and high-priority workflows implied by usage score.
|
|
||||||
- Validate mocks/stubs interact correctly with Next.js router, network calls, and async updates.
|
|
||||||
- Ensure naming, describe/it structure, and placement match repository standards.
|
|
||||||
|
|
||||||
Output format:
|
|
||||||
1. Start with a single word verdict: PASS or FAIL.
|
|
||||||
2. If FAIL, list each missing requirement or defect as a separate bullet with actionable fixes.
|
|
||||||
3. Highlight any optional improvements or refactors after mandatory issues.
|
|
||||||
4. Mention any additional tests or tooling steps (e.g., pnpm lint/test) the developer should run.
|
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Main Function
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
function showHelp() {
|
|
||||||
console.log(`
|
|
||||||
📋 Component Analyzer - Generate test prompts for AI assistants
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
node analyze-component.js <component-path> [options]
|
|
||||||
pnpm analyze-component <component-path> [options]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--help Show this help message
|
|
||||||
--json Output analysis result as JSON (for programmatic use)
|
|
||||||
--review Generate a review prompt for existing test file
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
# Analyze a component and generate test prompt
|
|
||||||
pnpm analyze-component app/components/base/action-button/index.tsx
|
|
||||||
|
|
||||||
# Output as JSON
|
|
||||||
pnpm analyze-component app/components/base/action-button/index.tsx --json
|
|
||||||
|
|
||||||
# Review existing test
|
|
||||||
pnpm analyze-component app/components/base/action-button/index.tsx --review
|
|
||||||
|
|
||||||
For complete testing guidelines, see: web/docs/test.md
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
const rawArgs = process.argv.slice(2)
|
|
||||||
|
|
||||||
let isReviewMode = false
|
|
||||||
let isJsonMode = false
|
|
||||||
const args = []
|
|
||||||
|
|
||||||
rawArgs.forEach((arg) => {
|
|
||||||
if (arg === '--review') {
|
|
||||||
isReviewMode = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (arg === '--json') {
|
|
||||||
isJsonMode = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (arg === '--help' || arg === '-h') {
|
|
||||||
showHelp()
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
args.push(arg)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (args.length === 0) {
|
|
||||||
showHelp()
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
let componentPath = args[0]
|
|
||||||
let absolutePath = path.resolve(process.cwd(), componentPath)
|
|
||||||
|
|
||||||
// Check if path exists
|
|
||||||
if (!fs.existsSync(absolutePath)) {
|
|
||||||
console.error(`❌ Error: Path not found: ${componentPath}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If directory, try to find entry file
|
|
||||||
if (fs.statSync(absolutePath).isDirectory()) {
|
|
||||||
const resolvedFile = resolveDirectoryEntry(absolutePath, componentPath)
|
|
||||||
if (resolvedFile) {
|
|
||||||
absolutePath = resolvedFile.absolutePath
|
|
||||||
componentPath = resolvedFile.componentPath
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// List available files for user to choose
|
|
||||||
const availableFiles = listAnalyzableFiles(absolutePath)
|
|
||||||
console.error(`❌ Error: Directory does not contain a recognizable entry file: ${componentPath}`)
|
|
||||||
if (availableFiles.length > 0) {
|
|
||||||
console.error(`\n Available files to analyze:`)
|
|
||||||
availableFiles.forEach(f => console.error(` - ${path.join(componentPath, f)}`))
|
|
||||||
console.error(`\n Please specify the exact file path, e.g.:`)
|
|
||||||
console.error(` pnpm analyze-component ${path.join(componentPath, availableFiles[0])}`)
|
|
||||||
}
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read source code
|
|
||||||
const sourceCode = fs.readFileSync(absolutePath, 'utf-8')
|
|
||||||
|
|
||||||
// Analyze
|
|
||||||
const analyzer = new ComponentAnalyzer()
|
|
||||||
const analysis = analyzer.analyze(sourceCode, componentPath, absolutePath)
|
|
||||||
|
|
||||||
// Check if component is too complex - suggest refactoring instead of testing
|
|
||||||
// Skip this check in JSON mode to always output analysis result
|
|
||||||
if (!isReviewMode && !isJsonMode && (analysis.complexity > 75 || analysis.lineCount > 300)) {
|
|
||||||
console.log(`
|
|
||||||
╔════════════════════════════════════════════════════════════════════════════╗
|
|
||||||
║ ⚠️ COMPONENT TOO COMPLEX TO TEST ║
|
|
||||||
╚════════════════════════════════════════════════════════════════════════════╝
|
|
||||||
|
|
||||||
📍 Component: ${analysis.name}
|
|
||||||
📂 Path: ${analysis.path}
|
|
||||||
|
|
||||||
📊 Component Metrics:
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
Total Complexity: ${analysis.complexity}/100 ${analysis.complexity > 75 ? '🔴 TOO HIGH' : analysis.complexity > 50 ? '⚠️ WARNING' : '🟢 OK'}
|
|
||||||
Max Func Complexity: ${analysis.maxComplexity}/100 ${analysis.maxComplexity > 75 ? '🔴 TOO HIGH' : analysis.maxComplexity > 50 ? '⚠️ WARNING' : '🟢 OK'}
|
|
||||||
Lines: ${analysis.lineCount} ${analysis.lineCount > 300 ? '🔴 TOO LARGE' : '🟢 OK'}
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
🚫 RECOMMENDATION: REFACTOR BEFORE TESTING
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
This component is too complex to test effectively. Please consider:
|
|
||||||
|
|
||||||
1️⃣ **Split into smaller components**
|
|
||||||
- Extract reusable UI sections into separate components
|
|
||||||
- Separate business logic from presentation
|
|
||||||
- Create smaller, focused components (< 300 lines each)
|
|
||||||
|
|
||||||
2️⃣ **Extract custom hooks**
|
|
||||||
- Move state management logic to custom hooks
|
|
||||||
- Extract complex data transformation logic
|
|
||||||
- Separate API calls into dedicated hooks
|
|
||||||
|
|
||||||
3️⃣ **Simplify logic**
|
|
||||||
- Reduce nesting depth
|
|
||||||
- Break down complex conditions
|
|
||||||
- Extract helper functions
|
|
||||||
|
|
||||||
4️⃣ **After refactoring**
|
|
||||||
- Run this tool again on each smaller component
|
|
||||||
- Generate tests for the refactored components
|
|
||||||
- Tests will be easier to write and maintain
|
|
||||||
|
|
||||||
💡 TIP: Aim for components with:
|
|
||||||
- Cognitive Complexity < 50/100 (preferably < 25/100)
|
|
||||||
- Line count < 300 (preferably < 200)
|
|
||||||
- Single responsibility principle
|
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
`)
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build prompt for AI assistant
|
|
||||||
const builder = new TestPromptBuilder()
|
|
||||||
const generationPrompt = builder.build(analysis)
|
|
||||||
|
|
||||||
let prompt = generationPrompt
|
|
||||||
|
|
||||||
if (isReviewMode) {
|
|
||||||
const providedTestPath = args[1]
|
|
||||||
const inferredTestPath = inferTestPath(componentPath)
|
|
||||||
const testPath = providedTestPath ?? inferredTestPath
|
|
||||||
const absoluteTestPath = path.resolve(process.cwd(), testPath)
|
|
||||||
|
|
||||||
if (!fs.existsSync(absoluteTestPath)) {
|
|
||||||
console.error(`❌ Error: Test file not found: ${testPath}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const testCode = fs.readFileSync(absoluteTestPath, 'utf-8')
|
|
||||||
const reviewBuilder = new TestReviewPromptBuilder()
|
|
||||||
const originalPromptSection = extractCopyContent(generationPrompt)
|
|
||||||
const normalizedTestPath = path.relative(process.cwd(), absoluteTestPath) || testPath
|
|
||||||
|
|
||||||
prompt = reviewBuilder.build({
|
|
||||||
analysis,
|
|
||||||
testPath: normalizedTestPath,
|
|
||||||
testCode,
|
|
||||||
originalPromptSection,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON output mode
|
|
||||||
if (isJsonMode) {
|
|
||||||
console.log(JSON.stringify(analysis, null, 2))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
console.log(prompt)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const checkPbcopy = spawnSync('which', ['pbcopy'], { stdio: 'pipe' })
|
|
||||||
if (checkPbcopy.status !== 0)
|
|
||||||
return
|
|
||||||
const copyContent = extractCopyContent(prompt)
|
|
||||||
if (!copyContent)
|
|
||||||
return
|
|
||||||
|
|
||||||
const result = spawnSync('pbcopy', [], {
|
|
||||||
input: copyContent,
|
|
||||||
encoding: 'utf-8',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.status === 0) {
|
|
||||||
console.log('\n📋 Prompt copied to clipboard!')
|
|
||||||
console.log(' Paste it in your AI assistant:')
|
|
||||||
console.log(' - Cursor: Cmd+L (Chat) or Cmd+I (Composer)')
|
|
||||||
console.log(' - GitHub Copilot Chat: Cmd+I')
|
|
||||||
console.log(' - Or any other AI coding tool\n')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// pbcopy failed, but don't break the script
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function inferTestPath(componentPath) {
|
|
||||||
const ext = path.extname(componentPath)
|
|
||||||
if (!ext)
|
|
||||||
return `${componentPath}.spec.ts`
|
|
||||||
return componentPath.replace(ext, `.spec${ext}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Run
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
main()
|
|
||||||
@ -1,484 +0,0 @@
|
|||||||
/**
|
|
||||||
* Component Analyzer - Shared module for analyzing React component complexity
|
|
||||||
*
|
|
||||||
* This module is used by:
|
|
||||||
* - analyze-component.js (for test generation)
|
|
||||||
* - refactor-component.js (for refactoring suggestions)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import fs from 'node:fs'
|
|
||||||
import path from 'node:path'
|
|
||||||
import tsParser from '@typescript-eslint/parser'
|
|
||||||
import { Linter } from 'eslint'
|
|
||||||
import sonarPlugin from 'eslint-plugin-sonarjs'
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Component Analyzer
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export class ComponentAnalyzer {
|
|
||||||
analyze(code, filePath, absolutePath) {
|
|
||||||
const resolvedPath = absolutePath ?? path.resolve(process.cwd(), filePath)
|
|
||||||
const fileName = path.basename(filePath, path.extname(filePath))
|
|
||||||
const lineCount = code.split('\n').length
|
|
||||||
const hasReactQuery = /\buse(?:Query|Queries|InfiniteQuery|SuspenseQuery|SuspenseInfiniteQuery|Mutation)\b/.test(code)
|
|
||||||
|
|
||||||
// Calculate complexity metrics
|
|
||||||
const { total: rawComplexity, max: rawMaxComplexity } = this.calculateCognitiveComplexity(code)
|
|
||||||
const complexity = this.normalizeComplexity(rawComplexity)
|
|
||||||
const maxComplexity = this.normalizeComplexity(rawMaxComplexity)
|
|
||||||
|
|
||||||
// Count usage references (may take a few seconds)
|
|
||||||
const usageCount = this.countUsageReferences(filePath, resolvedPath)
|
|
||||||
|
|
||||||
// Calculate test priority
|
|
||||||
const priority = this.calculateTestPriority(complexity, usageCount)
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: fileName.charAt(0).toUpperCase() + fileName.slice(1),
|
|
||||||
path: filePath,
|
|
||||||
type: this.detectType(filePath, code),
|
|
||||||
hasProps: code.includes('Props') || code.includes('interface'),
|
|
||||||
hasState: code.includes('useState') || code.includes('useReducer'),
|
|
||||||
hasEffects: code.includes('useEffect'),
|
|
||||||
hasCallbacks: code.includes('useCallback'),
|
|
||||||
hasMemo: code.includes('useMemo'),
|
|
||||||
hasEvents: /on[A-Z]\w+/.test(code),
|
|
||||||
hasRouter: code.includes('useRouter') || code.includes('usePathname'),
|
|
||||||
hasAPI: code.includes('service/') || code.includes('fetch(') || hasReactQuery,
|
|
||||||
hasForwardRef: code.includes('forwardRef'),
|
|
||||||
hasComponentMemo: /React\.memo|memo\(/.test(code),
|
|
||||||
hasSuspense: code.includes('Suspense') || /\blazy\(/.test(code),
|
|
||||||
hasPortal: code.includes('createPortal'),
|
|
||||||
hasImperativeHandle: code.includes('useImperativeHandle'),
|
|
||||||
hasReactQuery,
|
|
||||||
hasAhooks: code.includes('from \'ahooks\''),
|
|
||||||
complexity,
|
|
||||||
maxComplexity,
|
|
||||||
rawComplexity,
|
|
||||||
rawMaxComplexity,
|
|
||||||
lineCount,
|
|
||||||
usageCount,
|
|
||||||
priority,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
detectType(filePath, code) {
|
|
||||||
const normalizedPath = filePath.replace(/\\/g, '/')
|
|
||||||
if (normalizedPath.includes('/hooks/'))
|
|
||||||
return 'hook'
|
|
||||||
if (normalizedPath.includes('/utils/'))
|
|
||||||
return 'util'
|
|
||||||
if (/\/page\.(t|j)sx?$/.test(normalizedPath))
|
|
||||||
return 'page'
|
|
||||||
if (/\/layout\.(t|j)sx?$/.test(normalizedPath))
|
|
||||||
return 'layout'
|
|
||||||
if (/\/providers?\//.test(normalizedPath))
|
|
||||||
return 'provider'
|
|
||||||
// Dify-specific types
|
|
||||||
if (normalizedPath.includes('/components/base/'))
|
|
||||||
return 'base-component'
|
|
||||||
if (normalizedPath.includes('/context/'))
|
|
||||||
return 'context'
|
|
||||||
if (normalizedPath.includes('/store/'))
|
|
||||||
return 'store'
|
|
||||||
if (normalizedPath.includes('/service/'))
|
|
||||||
return 'service'
|
|
||||||
if (/use[A-Z]\w+/.test(code))
|
|
||||||
return 'component'
|
|
||||||
return 'component'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate Cognitive Complexity using SonarJS ESLint plugin
|
|
||||||
* Reference: https://www.sonarsource.com/blog/5-clean-code-tips-for-reducing-cognitive-complexity/
|
|
||||||
*
|
|
||||||
* Returns raw (unnormalized) complexity values:
|
|
||||||
* - total: sum of all functions' complexity in the file
|
|
||||||
* - max: highest single function complexity in the file
|
|
||||||
*
|
|
||||||
* Raw Score Thresholds (per function):
|
|
||||||
* 0-15: Simple | 16-30: Medium | 31-50: Complex | 51+: Very Complex
|
|
||||||
*
|
|
||||||
* @returns {{ total: number, max: number }} raw total and max complexity
|
|
||||||
*/
|
|
||||||
calculateCognitiveComplexity(code) {
|
|
||||||
const linter = new Linter()
|
|
||||||
const baseConfig = {
|
|
||||||
languageOptions: {
|
|
||||||
parser: tsParser,
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
sourceType: 'module',
|
|
||||||
ecmaFeatures: { jsx: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: { sonarjs: sonarPlugin },
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get total complexity using 'metric' option (more stable)
|
|
||||||
const totalConfig = {
|
|
||||||
...baseConfig,
|
|
||||||
rules: { 'sonarjs/cognitive-complexity': ['error', 0, 'metric'] },
|
|
||||||
}
|
|
||||||
const totalMessages = linter.verify(code, totalConfig)
|
|
||||||
const totalMsg = totalMessages.find(
|
|
||||||
msg => msg.ruleId === 'sonarjs/cognitive-complexity'
|
|
||||||
&& msg.messageId === 'fileComplexity',
|
|
||||||
)
|
|
||||||
const total = totalMsg ? Number.parseInt(totalMsg.message, 10) : 0
|
|
||||||
|
|
||||||
// Get max function complexity by analyzing each function
|
|
||||||
const maxConfig = {
|
|
||||||
...baseConfig,
|
|
||||||
rules: { 'sonarjs/cognitive-complexity': ['error', 0] },
|
|
||||||
}
|
|
||||||
const maxMessages = linter.verify(code, maxConfig)
|
|
||||||
let max = 0
|
|
||||||
const complexityPattern = /reduce its Cognitive Complexity from (\d+)/
|
|
||||||
|
|
||||||
maxMessages.forEach((msg) => {
|
|
||||||
if (msg.ruleId === 'sonarjs/cognitive-complexity') {
|
|
||||||
const match = complexityPattern.exec(msg.message)
|
|
||||||
if (match && match[1])
|
|
||||||
max = Math.max(max, Number.parseInt(match[1], 10))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return { total, max }
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return { total: 0, max: 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize cognitive complexity to 0-100 scale
|
|
||||||
*
|
|
||||||
* Mapping (aligned with SonarJS thresholds):
|
|
||||||
* Raw 0-15 (Simple) -> Normalized 0-25
|
|
||||||
* Raw 16-30 (Medium) -> Normalized 25-50
|
|
||||||
* Raw 31-50 (Complex) -> Normalized 50-75
|
|
||||||
* Raw 51+ (Very Complex) -> Normalized 75-100 (asymptotic)
|
|
||||||
*/
|
|
||||||
normalizeComplexity(rawComplexity) {
|
|
||||||
if (rawComplexity <= 15) {
|
|
||||||
// Linear: 0-15 -> 0-25
|
|
||||||
return Math.round((rawComplexity / 15) * 25)
|
|
||||||
}
|
|
||||||
else if (rawComplexity <= 30) {
|
|
||||||
// Linear: 16-30 -> 25-50
|
|
||||||
return Math.round(25 + ((rawComplexity - 15) / 15) * 25)
|
|
||||||
}
|
|
||||||
else if (rawComplexity <= 50) {
|
|
||||||
// Linear: 31-50 -> 50-75
|
|
||||||
return Math.round(50 + ((rawComplexity - 30) / 20) * 25)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Asymptotic: 51+ -> 75-100
|
|
||||||
// Formula ensures score approaches but never exceeds 100
|
|
||||||
return Math.round(75 + 25 * (1 - 1 / (1 + (rawComplexity - 50) / 100)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Count how many times a component is referenced in the codebase
|
|
||||||
* Scans TypeScript sources for import statements referencing the component
|
|
||||||
*/
|
|
||||||
countUsageReferences(filePath, absolutePath) {
|
|
||||||
try {
|
|
||||||
const resolvedComponentPath = absolutePath ?? path.resolve(process.cwd(), filePath)
|
|
||||||
const fileName = path.basename(resolvedComponentPath, path.extname(resolvedComponentPath))
|
|
||||||
|
|
||||||
let searchName = fileName
|
|
||||||
if (fileName === 'index') {
|
|
||||||
const parentDir = path.dirname(resolvedComponentPath)
|
|
||||||
searchName = path.basename(parentDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!searchName)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
const searchRoots = this.collectSearchRoots(resolvedComponentPath)
|
|
||||||
if (searchRoots.length === 0)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
const escapedName = ComponentAnalyzer.escapeRegExp(searchName)
|
|
||||||
const patterns = [
|
|
||||||
new RegExp(`from\\s+['"][^'"]*(?:/|^)${escapedName}(?:['"/]|$)`),
|
|
||||||
new RegExp(`import\\s*\\(\\s*['"][^'"]*(?:/|^)${escapedName}(?:['"/]|$)`),
|
|
||||||
new RegExp(`export\\s+(?:\\*|{[^}]*})\\s*from\\s+['"][^'"]*(?:/|^)${escapedName}(?:['"/]|$)`),
|
|
||||||
new RegExp(`require\\s*\\(\\s*['"][^'"]*(?:/|^)${escapedName}(?:['"/]|$)`),
|
|
||||||
]
|
|
||||||
|
|
||||||
const visited = new Set()
|
|
||||||
let usageCount = 0
|
|
||||||
|
|
||||||
const stack = [...searchRoots]
|
|
||||||
while (stack.length > 0) {
|
|
||||||
const currentDir = stack.pop()
|
|
||||||
if (!currentDir || visited.has(currentDir))
|
|
||||||
continue
|
|
||||||
visited.add(currentDir)
|
|
||||||
|
|
||||||
const entries = fs.readdirSync(currentDir, { withFileTypes: true })
|
|
||||||
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
const entryPath = path.join(currentDir, entry.name)
|
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
if (this.shouldSkipDir(entry.name))
|
|
||||||
return
|
|
||||||
stack.push(entryPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.shouldInspectFile(entry.name))
|
|
||||||
return
|
|
||||||
|
|
||||||
const normalizedEntryPath = path.resolve(entryPath)
|
|
||||||
if (normalizedEntryPath === path.resolve(resolvedComponentPath))
|
|
||||||
return
|
|
||||||
|
|
||||||
const source = fs.readFileSync(entryPath, 'utf-8')
|
|
||||||
if (!source.includes(searchName))
|
|
||||||
return
|
|
||||||
|
|
||||||
if (patterns.some((pattern) => {
|
|
||||||
pattern.lastIndex = 0
|
|
||||||
return pattern.test(source)
|
|
||||||
})) {
|
|
||||||
usageCount += 1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return usageCount
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// If command fails, return 0
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
collectSearchRoots(resolvedComponentPath) {
|
|
||||||
const roots = new Set()
|
|
||||||
|
|
||||||
let currentDir = path.dirname(resolvedComponentPath)
|
|
||||||
const workspaceRoot = process.cwd()
|
|
||||||
|
|
||||||
while (currentDir && currentDir !== path.dirname(currentDir)) {
|
|
||||||
if (path.basename(currentDir) === 'app') {
|
|
||||||
roots.add(currentDir)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentDir === workspaceRoot)
|
|
||||||
break
|
|
||||||
currentDir = path.dirname(currentDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallbackRoots = [
|
|
||||||
path.join(workspaceRoot, 'app'),
|
|
||||||
path.join(workspaceRoot, 'web', 'app'),
|
|
||||||
path.join(workspaceRoot, 'src'),
|
|
||||||
]
|
|
||||||
|
|
||||||
fallbackRoots.forEach((root) => {
|
|
||||||
if (fs.existsSync(root) && fs.statSync(root).isDirectory())
|
|
||||||
roots.add(root)
|
|
||||||
})
|
|
||||||
|
|
||||||
return Array.from(roots)
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldSkipDir(dirName) {
|
|
||||||
const normalized = dirName.toLowerCase()
|
|
||||||
return [
|
|
||||||
'node_modules',
|
|
||||||
'.git',
|
|
||||||
'.next',
|
|
||||||
'dist',
|
|
||||||
'out',
|
|
||||||
'coverage',
|
|
||||||
'build',
|
|
||||||
'__tests__',
|
|
||||||
'__mocks__',
|
|
||||||
].includes(normalized)
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldInspectFile(fileName) {
|
|
||||||
const normalized = fileName.toLowerCase()
|
|
||||||
if (!(/\.(ts|tsx)$/i.test(fileName)))
|
|
||||||
return false
|
|
||||||
if (normalized.endsWith('.d.ts'))
|
|
||||||
return false
|
|
||||||
if (/\.(spec|test)\.(ts|tsx)$/.test(normalized))
|
|
||||||
return false
|
|
||||||
if (normalized.endsWith('.stories.tsx'))
|
|
||||||
return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
static escapeRegExp(value) {
|
|
||||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate test priority based on cognitive complexity and usage
|
|
||||||
*
|
|
||||||
* Priority Score = 0.7 * Complexity + 0.3 * Usage Score (all normalized to 0-100)
|
|
||||||
* - Complexity Score: 0-100 (normalized from SonarJS)
|
|
||||||
* - Usage Score: 0-100 (based on reference count)
|
|
||||||
*
|
|
||||||
* Priority Levels (0-100):
|
|
||||||
* - 0-25: 🟢 LOW
|
|
||||||
* - 26-50: 🟡 MEDIUM
|
|
||||||
* - 51-75: 🟠 HIGH
|
|
||||||
* - 76-100: 🔴 CRITICAL
|
|
||||||
*/
|
|
||||||
calculateTestPriority(complexity, usageCount) {
|
|
||||||
const complexityScore = complexity
|
|
||||||
|
|
||||||
// Normalize usage score to 0-100
|
|
||||||
let usageScore
|
|
||||||
if (usageCount === 0)
|
|
||||||
usageScore = 0
|
|
||||||
else if (usageCount <= 5)
|
|
||||||
usageScore = 20
|
|
||||||
else if (usageCount <= 20)
|
|
||||||
usageScore = 40
|
|
||||||
else if (usageCount <= 50)
|
|
||||||
usageScore = 70
|
|
||||||
else
|
|
||||||
usageScore = 100
|
|
||||||
|
|
||||||
// Weighted average: complexity (70%) + usage (30%)
|
|
||||||
const totalScore = Math.round(0.7 * complexityScore + 0.3 * usageScore)
|
|
||||||
|
|
||||||
return {
|
|
||||||
score: totalScore,
|
|
||||||
level: this.getPriorityLevel(totalScore),
|
|
||||||
usageScore,
|
|
||||||
complexityScore,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get priority level based on score (0-100 scale)
|
|
||||||
*/
|
|
||||||
getPriorityLevel(score) {
|
|
||||||
if (score > 75)
|
|
||||||
return '🔴 CRITICAL'
|
|
||||||
if (score > 50)
|
|
||||||
return '🟠 HIGH'
|
|
||||||
if (score > 25)
|
|
||||||
return '🟡 MEDIUM'
|
|
||||||
return '🟢 LOW'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Helper Functions
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve directory to entry file
|
|
||||||
* Priority: index files > common entry files (node.tsx, panel.tsx, etc.)
|
|
||||||
*/
|
|
||||||
export function resolveDirectoryEntry(absolutePath, componentPath) {
|
|
||||||
// Entry files in priority order: index files first, then common entry files
|
|
||||||
const entryFiles = [
|
|
||||||
'index.tsx',
|
|
||||||
'index.ts', // Priority 1: index files
|
|
||||||
'node.tsx',
|
|
||||||
'panel.tsx',
|
|
||||||
'component.tsx',
|
|
||||||
'main.tsx',
|
|
||||||
'container.tsx', // Priority 2: common entry files
|
|
||||||
]
|
|
||||||
for (const entryFile of entryFiles) {
|
|
||||||
const entryPath = path.join(absolutePath, entryFile)
|
|
||||||
if (fs.existsSync(entryPath)) {
|
|
||||||
return {
|
|
||||||
absolutePath: entryPath,
|
|
||||||
componentPath: path.join(componentPath, entryFile),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List analyzable files in directory (for user guidance)
|
|
||||||
*/
|
|
||||||
export function listAnalyzableFiles(dirPath) {
|
|
||||||
try {
|
|
||||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
||||||
return entries
|
|
||||||
.filter(entry => !entry.isDirectory() && /\.(tsx?|jsx?)$/.test(entry.name) && !entry.name.endsWith('.d.ts'))
|
|
||||||
.map(entry => entry.name)
|
|
||||||
.sort((a, b) => {
|
|
||||||
// Prioritize common entry files
|
|
||||||
const priority = ['index.tsx', 'index.ts', 'node.tsx', 'panel.tsx', 'component.tsx', 'main.tsx', 'container.tsx']
|
|
||||||
const aIdx = priority.indexOf(a)
|
|
||||||
const bIdx = priority.indexOf(b)
|
|
||||||
if (aIdx !== -1 && bIdx !== -1)
|
|
||||||
return aIdx - bIdx
|
|
||||||
if (aIdx !== -1)
|
|
||||||
return -1
|
|
||||||
if (bIdx !== -1)
|
|
||||||
return 1
|
|
||||||
return a.localeCompare(b)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract copy content from prompt (for clipboard)
|
|
||||||
*/
|
|
||||||
export function extractCopyContent(prompt) {
|
|
||||||
const marker = '📋 PROMPT FOR AI ASSISTANT (COPY THIS TO YOUR AI ASSISTANT):'
|
|
||||||
const markerIndex = prompt.indexOf(marker)
|
|
||||||
if (markerIndex === -1)
|
|
||||||
return ''
|
|
||||||
|
|
||||||
const section = prompt.slice(markerIndex)
|
|
||||||
const lines = section.split('\n')
|
|
||||||
const firstDivider = lines.findIndex(line => line.includes('━━━━━━━━'))
|
|
||||||
if (firstDivider === -1)
|
|
||||||
return ''
|
|
||||||
|
|
||||||
const startIdx = firstDivider + 1
|
|
||||||
let endIdx = lines.length
|
|
||||||
|
|
||||||
for (let i = startIdx; i < lines.length; i++) {
|
|
||||||
if (lines[i].includes('━━━━━━━━')) {
|
|
||||||
endIdx = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startIdx >= endIdx)
|
|
||||||
return ''
|
|
||||||
|
|
||||||
return lines.slice(startIdx, endIdx).join('\n').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get complexity level label
|
|
||||||
*/
|
|
||||||
export function getComplexityLevel(score) {
|
|
||||||
if (score <= 25)
|
|
||||||
return '🟢 Simple'
|
|
||||||
if (score <= 50)
|
|
||||||
return '🟡 Medium'
|
|
||||||
if (score <= 75)
|
|
||||||
return '🟠 Complex'
|
|
||||||
return '🔴 Very Complex'
|
|
||||||
}
|
|
||||||
@ -1,415 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import { spawnSync } from 'node:child_process'
|
|
||||||
import fs from 'node:fs'
|
|
||||||
import path from 'node:path'
|
|
||||||
import {
|
|
||||||
ComponentAnalyzer,
|
|
||||||
extractCopyContent,
|
|
||||||
getComplexityLevel,
|
|
||||||
listAnalyzableFiles,
|
|
||||||
resolveDirectoryEntry,
|
|
||||||
} from './component-analyzer.js'
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Extended Analyzer for Refactoring
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
class RefactorAnalyzer extends ComponentAnalyzer {
|
|
||||||
analyze(code, filePath, absolutePath) {
|
|
||||||
// Get base analysis from parent class
|
|
||||||
const baseAnalysis = super.analyze(code, filePath, absolutePath)
|
|
||||||
|
|
||||||
// Add refactoring-specific metrics
|
|
||||||
// Note: These counts use regex matching which may include import statements.
|
|
||||||
// For most components this results in +1 over actual usage, which is acceptable
|
|
||||||
// for heuristic analysis. For precise AST-based counting, consider using
|
|
||||||
// @typescript-eslint/parser to traverse the AST.
|
|
||||||
const stateCount = (code.match(/useState\s*[(<]/g) || []).length
|
|
||||||
const effectCount = (code.match(/useEffect\s*\(/g) || []).length
|
|
||||||
const callbackCount = (code.match(/useCallback\s*\(/g) || []).length
|
|
||||||
const memoCount = (code.match(/useMemo\s*\(/g) || []).length
|
|
||||||
const conditionalBlocks = this.countConditionalBlocks(code)
|
|
||||||
const nestedTernaries = this.countNestedTernaries(code)
|
|
||||||
const hasContext = code.includes('useContext') || code.includes('createContext')
|
|
||||||
const hasReducer = code.includes('useReducer')
|
|
||||||
const hasModals = this.countModals(code)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...baseAnalysis,
|
|
||||||
stateCount,
|
|
||||||
effectCount,
|
|
||||||
callbackCount,
|
|
||||||
memoCount,
|
|
||||||
conditionalBlocks,
|
|
||||||
nestedTernaries,
|
|
||||||
hasContext,
|
|
||||||
hasReducer,
|
|
||||||
hasModals,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
countModals(code) {
|
|
||||||
const modalPatterns = [
|
|
||||||
/Modal/g,
|
|
||||||
/Dialog/g,
|
|
||||||
/Drawer/g,
|
|
||||||
/Confirm/g,
|
|
||||||
/showModal|setShowModal|isShown|isShowing/g,
|
|
||||||
]
|
|
||||||
let count = 0
|
|
||||||
modalPatterns.forEach((pattern) => {
|
|
||||||
const matches = code.match(pattern)
|
|
||||||
if (matches)
|
|
||||||
count += matches.length
|
|
||||||
})
|
|
||||||
return Math.floor(count / 3) // Rough estimate of actual modals
|
|
||||||
}
|
|
||||||
|
|
||||||
countConditionalBlocks(code) {
|
|
||||||
const ifBlocks = (code.match(/\bif\s*\(/g) || []).length
|
|
||||||
const ternaries = (code.match(/\?.*:/g) || []).length
|
|
||||||
const switchCases = (code.match(/\bswitch\s*\(/g) || []).length
|
|
||||||
return ifBlocks + ternaries + switchCases
|
|
||||||
}
|
|
||||||
|
|
||||||
countNestedTernaries(code) {
|
|
||||||
const nestedInTrueBranch = (code.match(/\?[^:?]*\?[^:]*:/g) || []).length
|
|
||||||
const nestedInFalseBranch = (code.match(/\?[^:?]*:[^?]*\?[^:]*:/g) || []).length
|
|
||||||
|
|
||||||
return nestedInTrueBranch + nestedInFalseBranch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Refactor Prompt Builder
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
class RefactorPromptBuilder {
|
|
||||||
build(analysis) {
|
|
||||||
const refactorActions = this.identifyRefactorActions(analysis)
|
|
||||||
|
|
||||||
return `
|
|
||||||
╔════════════════════════════════════════════════════════════════════════════╗
|
|
||||||
║ 🔧 REFACTOR DIFY COMPONENT ║
|
|
||||||
╚════════════════════════════════════════════════════════════════════════════╝
|
|
||||||
|
|
||||||
📍 Component: ${analysis.name}
|
|
||||||
📂 Path: ${analysis.path}
|
|
||||||
|
|
||||||
📊 Complexity Analysis:
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
Total Complexity: ${analysis.complexity}/100 ${getComplexityLevel(analysis.complexity)}
|
|
||||||
Max Func Complexity: ${analysis.maxComplexity}/100 ${getComplexityLevel(analysis.maxComplexity)}
|
|
||||||
Lines: ${analysis.lineCount} ${analysis.lineCount > 300 ? '⚠️ TOO LARGE' : ''}
|
|
||||||
Usage: ${analysis.usageCount} reference${analysis.usageCount !== 1 ? 's' : ''}
|
|
||||||
|
|
||||||
📈 Code Metrics:
|
|
||||||
useState calls: ${analysis.stateCount}
|
|
||||||
useEffect calls: ${analysis.effectCount}
|
|
||||||
useCallback calls: ${analysis.callbackCount}
|
|
||||||
useMemo calls: ${analysis.memoCount}
|
|
||||||
Conditional blocks: ${analysis.conditionalBlocks}
|
|
||||||
Nested ternaries: ${analysis.nestedTernaries}
|
|
||||||
Modal components: ${analysis.hasModals}
|
|
||||||
|
|
||||||
🔍 Features Detected:
|
|
||||||
${analysis.hasState ? '✓' : '✗'} Local state (useState/useReducer)
|
|
||||||
${analysis.hasEffects ? '✓' : '✗'} Side effects (useEffect)
|
|
||||||
${analysis.hasCallbacks ? '✓' : '✗'} Callbacks (useCallback)
|
|
||||||
${analysis.hasMemo ? '✓' : '✗'} Memoization (useMemo)
|
|
||||||
${analysis.hasContext ? '✓' : '✗'} Context (useContext/createContext)
|
|
||||||
${analysis.hasEvents ? '✓' : '✗'} Event handlers
|
|
||||||
${analysis.hasRouter ? '✓' : '✗'} Next.js routing
|
|
||||||
${analysis.hasAPI ? '✓' : '✗'} API calls
|
|
||||||
${analysis.hasReactQuery ? '✓' : '✗'} React Query
|
|
||||||
${analysis.hasAhooks ? '✓' : '✗'} ahooks
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
🎯 RECOMMENDED REFACTORING ACTIONS:
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
${refactorActions.map((action, i) => `${i + 1}. ${action}`).join('\n')}
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
📋 PROMPT FOR AI ASSISTANT (COPY THIS TO YOUR AI ASSISTANT):
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
Please refactor the component at @${analysis.path}
|
|
||||||
|
|
||||||
Component metrics:
|
|
||||||
- Complexity: ${analysis.complexity}/100 (target: < 50)
|
|
||||||
- Lines: ${analysis.lineCount} (target: < 300)
|
|
||||||
- useState: ${analysis.stateCount}, useEffect: ${analysis.effectCount}
|
|
||||||
|
|
||||||
Refactoring tasks:
|
|
||||||
${refactorActions.map(action => `- ${action}`).join('\n')}
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
${this.buildRequirements(analysis)}
|
|
||||||
|
|
||||||
Follow Dify project conventions:
|
|
||||||
- Place extracted hooks in \`hooks/\` subdirectory or as \`use-<feature>.ts\`
|
|
||||||
- Use React Query (\`@tanstack/react-query\`) for data fetching
|
|
||||||
- Follow existing patterns in \`web/service/use-*.ts\` for API hooks
|
|
||||||
- Keep each new file under 300 lines
|
|
||||||
- Maintain TypeScript strict typing
|
|
||||||
|
|
||||||
After refactoring, verify:
|
|
||||||
- \`pnpm lint:fix\` passes
|
|
||||||
- \`pnpm type-check\` passes
|
|
||||||
- Re-run \`pnpm refactor-component ${analysis.path}\` to confirm complexity < 50
|
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
identifyRefactorActions(analysis) {
|
|
||||||
const actions = []
|
|
||||||
|
|
||||||
// Priority 1: Extract hooks for complex state management
|
|
||||||
if (analysis.stateCount >= 3 || (analysis.stateCount >= 2 && analysis.effectCount >= 2)) {
|
|
||||||
actions.push(`🪝 EXTRACT CUSTOM HOOK: ${analysis.stateCount} useState + ${analysis.effectCount} useEffect detected. Extract related state and effects into a custom hook (e.g., \`use${analysis.name}State.ts\`)`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 2: Extract API/data logic
|
|
||||||
if (analysis.hasAPI)
|
|
||||||
actions.push('🌐 EXTRACT DATA HOOK: Move API calls and data fetching logic into a dedicated hook using React Query')
|
|
||||||
|
|
||||||
// Priority 3: Split large components
|
|
||||||
if (analysis.lineCount > 300) {
|
|
||||||
actions.push(`📦 SPLIT COMPONENT: ${analysis.lineCount} lines exceeds limit. Extract UI sections into sub-components`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 4: Extract modal management
|
|
||||||
if (analysis.hasModals >= 2) {
|
|
||||||
actions.push(`🔲 EXTRACT MODAL MANAGEMENT: ${analysis.hasModals} modal-related patterns detected. Create a useModalState hook or separate modal components`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 5: Simplify conditionals
|
|
||||||
if (analysis.conditionalBlocks > 10 || analysis.nestedTernaries >= 2) {
|
|
||||||
actions.push('🔀 SIMPLIFY CONDITIONALS: Use lookup tables, early returns, or extract complex conditions into named functions')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 6: Extract callbacks
|
|
||||||
if (analysis.callbackCount >= 4) {
|
|
||||||
actions.push(`⚡ CONSOLIDATE CALLBACKS: ${analysis.callbackCount} useCallback calls. Consider extracting related callbacks into a custom hook`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 7: Context provider extraction
|
|
||||||
if (analysis.hasContext && analysis.complexity > 50) {
|
|
||||||
actions.push('🎯 EXTRACT CONTEXT LOGIC: Move context provider logic into separate files or split into domain-specific contexts')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 8: Memoization review
|
|
||||||
if (analysis.memoCount >= 3 && analysis.complexity > 50) {
|
|
||||||
actions.push(`📝 REVIEW MEMOIZATION: ${analysis.memoCount} useMemo calls. Extract complex computations into utility functions or hooks`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no specific issues, provide general guidance
|
|
||||||
if (actions.length === 0) {
|
|
||||||
if (analysis.complexity > 50) {
|
|
||||||
actions.push('🔍 ANALYZE FUNCTIONS: Review individual functions for complexity and extract helper functions')
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
actions.push('✅ Component complexity is acceptable. Consider minor improvements for maintainability')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions
|
|
||||||
}
|
|
||||||
|
|
||||||
buildRequirements(analysis) {
|
|
||||||
const requirements = []
|
|
||||||
|
|
||||||
if (analysis.stateCount >= 3) {
|
|
||||||
requirements.push('- Group related useState calls into a single custom hook')
|
|
||||||
requirements.push('- Move associated useEffect calls with the state they depend on')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (analysis.hasAPI) {
|
|
||||||
requirements.push('- Create data fetching hook following web/service/use-*.ts patterns')
|
|
||||||
requirements.push('- Use useQuery with proper queryKey and enabled options')
|
|
||||||
requirements.push('- Export invalidation hook (useInvalidXxx) for cache management')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (analysis.lineCount > 300) {
|
|
||||||
requirements.push('- Extract logical UI sections into separate components')
|
|
||||||
requirements.push('- Keep parent component focused on orchestration')
|
|
||||||
requirements.push('- Pass minimal props to child components')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (analysis.hasModals >= 2) {
|
|
||||||
requirements.push('- Create unified modal state management')
|
|
||||||
requirements.push('- Consider extracting modals to separate file')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (analysis.conditionalBlocks > 10) {
|
|
||||||
requirements.push('- Replace switch statements with lookup tables')
|
|
||||||
requirements.push('- Use early returns to reduce nesting')
|
|
||||||
requirements.push('- Extract complex boolean logic to named functions')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requirements.length === 0) {
|
|
||||||
requirements.push('- Maintain existing code structure')
|
|
||||||
requirements.push('- Focus on readability improvements')
|
|
||||||
}
|
|
||||||
|
|
||||||
return requirements.join('\n')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Main Function
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
function showHelp() {
|
|
||||||
console.log(`
|
|
||||||
🔧 Component Refactor Tool - Generate refactoring prompts for AI assistants
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
node refactor-component.js <component-path> [options]
|
|
||||||
pnpm refactor-component <component-path> [options]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--help Show this help message
|
|
||||||
--json Output analysis result as JSON (for programmatic use)
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
# Analyze and generate refactoring prompt
|
|
||||||
pnpm refactor-component app/components/app/configuration/index.tsx
|
|
||||||
|
|
||||||
# Output as JSON
|
|
||||||
pnpm refactor-component app/components/tools/mcp/modal.tsx --json
|
|
||||||
|
|
||||||
Complexity Thresholds:
|
|
||||||
🟢 0-25: Simple (no refactoring needed)
|
|
||||||
🟡 26-50: Medium (consider minor refactoring)
|
|
||||||
🟠 51-75: Complex (should refactor)
|
|
||||||
🔴 76-100: Very Complex (must refactor)
|
|
||||||
|
|
||||||
For complete refactoring guidelines, see:
|
|
||||||
.claude/skills/component-refactoring/SKILL.md
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
const rawArgs = process.argv.slice(2)
|
|
||||||
|
|
||||||
let isJsonMode = false
|
|
||||||
const args = []
|
|
||||||
|
|
||||||
rawArgs.forEach((arg) => {
|
|
||||||
if (arg === '--json') {
|
|
||||||
isJsonMode = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (arg === '--help' || arg === '-h') {
|
|
||||||
showHelp()
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
args.push(arg)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (args.length === 0) {
|
|
||||||
showHelp()
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
let componentPath = args[0]
|
|
||||||
let absolutePath = path.resolve(process.cwd(), componentPath)
|
|
||||||
|
|
||||||
if (!fs.existsSync(absolutePath)) {
|
|
||||||
console.error(`❌ Error: Path not found: ${componentPath}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fs.statSync(absolutePath).isDirectory()) {
|
|
||||||
const resolvedFile = resolveDirectoryEntry(absolutePath, componentPath)
|
|
||||||
if (resolvedFile) {
|
|
||||||
absolutePath = resolvedFile.absolutePath
|
|
||||||
componentPath = resolvedFile.componentPath
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const availableFiles = listAnalyzableFiles(absolutePath)
|
|
||||||
console.error(`❌ Error: Directory does not contain a recognizable entry file: ${componentPath}`)
|
|
||||||
if (availableFiles.length > 0) {
|
|
||||||
console.error(`\n Available files to analyze:`)
|
|
||||||
availableFiles.forEach(f => console.error(` - ${path.join(componentPath, f)}`))
|
|
||||||
console.error(`\n Please specify the exact file path, e.g.:`)
|
|
||||||
console.error(` pnpm refactor-component ${path.join(componentPath, availableFiles[0])}`)
|
|
||||||
}
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceCode = fs.readFileSync(absolutePath, 'utf-8')
|
|
||||||
|
|
||||||
const analyzer = new RefactorAnalyzer()
|
|
||||||
const analysis = analyzer.analyze(sourceCode, componentPath, absolutePath)
|
|
||||||
|
|
||||||
// JSON output mode
|
|
||||||
if (isJsonMode) {
|
|
||||||
console.log(JSON.stringify(analysis, null, 2))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if refactoring is needed
|
|
||||||
if (analysis.complexity <= 25 && analysis.lineCount <= 200) {
|
|
||||||
console.log(`
|
|
||||||
╔════════════════════════════════════════════════════════════════════════════╗
|
|
||||||
║ ✅ COMPONENT IS WELL-STRUCTURED ║
|
|
||||||
╚════════════════════════════════════════════════════════════════════════════╝
|
|
||||||
|
|
||||||
📍 Component: ${analysis.name}
|
|
||||||
📂 Path: ${analysis.path}
|
|
||||||
|
|
||||||
📊 Metrics:
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
Complexity: ${analysis.complexity}/100 🟢 Simple
|
|
||||||
Lines: ${analysis.lineCount} ✓ Within limits
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
This component has good structure. No immediate refactoring needed.
|
|
||||||
You can proceed with testing using: pnpm analyze-component ${componentPath}
|
|
||||||
`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build refactoring prompt
|
|
||||||
const builder = new RefactorPromptBuilder()
|
|
||||||
const prompt = builder.build(analysis)
|
|
||||||
|
|
||||||
console.log(prompt)
|
|
||||||
|
|
||||||
// Copy to clipboard (macOS)
|
|
||||||
try {
|
|
||||||
const checkPbcopy = spawnSync('which', ['pbcopy'], { stdio: 'pipe' })
|
|
||||||
if (checkPbcopy.status !== 0)
|
|
||||||
return
|
|
||||||
const copyContent = extractCopyContent(prompt)
|
|
||||||
if (!copyContent)
|
|
||||||
return
|
|
||||||
|
|
||||||
const result = spawnSync('pbcopy', [], {
|
|
||||||
input: copyContent,
|
|
||||||
encoding: 'utf-8',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.status === 0) {
|
|
||||||
console.log('\n📋 Refactoring prompt copied to clipboard!')
|
|
||||||
console.log(' Paste it in your AI assistant:')
|
|
||||||
console.log(' - Cursor: Cmd+L (Chat) or Cmd+I (Composer)')
|
|
||||||
console.log(' - GitHub Copilot Chat: Cmd+I')
|
|
||||||
console.log(' - Or any other AI coding tool\n')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// pbcopy failed, but don't break the script
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Run
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
main()
|
|
||||||
Loading…
Reference in New Issue
Block a user