dash-devtools 1.0.0__py3-none-any.whl
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.
- dash_devtools/__init__.py +8 -0
- dash_devtools/__main__.py +11 -0
- dash_devtools/ai_engine.py +441 -0
- dash_devtools/browser.py +541 -0
- dash_devtools/cli.py +1452 -0
- dash_devtools/database.py +338 -0
- dash_devtools/dbdiagram.py +183 -0
- dash_devtools/e2e.py +329 -0
- dash_devtools/fixers/__init__.py +57 -0
- dash_devtools/fixers/migration_fixer.py +115 -0
- dash_devtools/fixers/ux_fixer.py +106 -0
- dash_devtools/fixers/version_bumper.py +115 -0
- dash_devtools/gas_mes_test.py +1241 -0
- dash_devtools/generators/__init__.py +84 -0
- dash_devtools/health.py +476 -0
- dash_devtools/hooks/__init__.py +250 -0
- dash_devtools/hooks/pre_commit.py +161 -0
- dash_devtools/hooks/pre_push.py +275 -0
- dash_devtools/init_test.py +352 -0
- dash_devtools/markdown_report.py +309 -0
- dash_devtools/migrators/__init__.py +21 -0
- dash_devtools/perf.py +321 -0
- dash_devtools/report.py +667 -0
- dash_devtools/reporters/__init__.py +11 -0
- dash_devtools/spec.py +230 -0
- dash_devtools/stats.py +355 -0
- dash_devtools/test_suite.py +690 -0
- dash_devtools/testing.py +416 -0
- dash_devtools/validators/__init__.py +157 -0
- dash_devtools/validators/backend/__init__.py +12 -0
- dash_devtools/validators/backend/nodejs.py +245 -0
- dash_devtools/validators/backend/python.py +439 -0
- dash_devtools/validators/code_quality.py +243 -0
- dash_devtools/validators/common/__init__.py +11 -0
- dash_devtools/validators/common/quality.py +319 -0
- dash_devtools/validators/common/security.py +270 -0
- dash_devtools/validators/common/spec.py +273 -0
- dash_devtools/validators/detector.py +394 -0
- dash_devtools/validators/frontend/__init__.py +14 -0
- dash_devtools/validators/frontend/angular.py +245 -0
- dash_devtools/validators/frontend/gas.py +310 -0
- dash_devtools/validators/frontend/vite.py +539 -0
- dash_devtools/validators/migration.py +292 -0
- dash_devtools/validators/performance.py +167 -0
- dash_devtools/validators/security.py +205 -0
- dash_devtools/vision/__init__.py +368 -0
- dash_devtools/watch.py +266 -0
- dash_devtools/word_report.py +690 -0
- dash_devtools-1.0.0.dist-info/METADATA +834 -0
- dash_devtools-1.0.0.dist-info/RECORD +53 -0
- dash_devtools-1.0.0.dist-info/WHEEL +5 -0
- dash_devtools-1.0.0.dist-info/entry_points.txt +2 -0
- dash_devtools-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""
|
|
2
|
+
專案類型偵測器 v2.1
|
|
3
|
+
|
|
4
|
+
自動偵測專案的技術堆疊:
|
|
5
|
+
- 前端:Angular / Vue+Vite / React / Vanilla JS / GAS (Google Apps Script)
|
|
6
|
+
- 後端:Node.js / Python (FastAPI/Flask/Django)
|
|
7
|
+
- 部署:Vercel Serverless / Vercel Proxy / Render / GAS Web App
|
|
8
|
+
|
|
9
|
+
新增功能:
|
|
10
|
+
- 區分「Serverless API」與「純 Proxy 閘道」
|
|
11
|
+
- 偵測 Vue 3 + DaisyUI 組合
|
|
12
|
+
- 偵測 GAS 專案(appsscript.json)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Set
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ProjectDetector:
|
|
21
|
+
"""專案類型偵測器"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, project_path):
|
|
24
|
+
self.project_path = Path(project_path)
|
|
25
|
+
self.project_name = self.project_path.name
|
|
26
|
+
|
|
27
|
+
def detect(self) -> dict:
|
|
28
|
+
"""偵測專案類型,回傳技術堆疊資訊"""
|
|
29
|
+
result = {
|
|
30
|
+
'name': self.project_name,
|
|
31
|
+
'types': set(),
|
|
32
|
+
'frontend': None,
|
|
33
|
+
'backend': None,
|
|
34
|
+
'ui_framework': None,
|
|
35
|
+
'deployment': None,
|
|
36
|
+
'details': {}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# 優先偵測 GAS 專案(有 appsscript.json)
|
|
40
|
+
gas = self._detect_gas()
|
|
41
|
+
if gas:
|
|
42
|
+
result['types'].add('gas')
|
|
43
|
+
result['frontend'] = gas['type']
|
|
44
|
+
result['ui_framework'] = gas.get('ui_framework')
|
|
45
|
+
result['deployment'] = {'type': 'gas-webapp', 'description': 'Google Apps Script Web App'}
|
|
46
|
+
result['details']['gas'] = gas
|
|
47
|
+
result['types'] = list(result['types'])
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
# 偵測前端
|
|
51
|
+
frontend = self._detect_frontend()
|
|
52
|
+
if frontend:
|
|
53
|
+
result['types'].add('frontend')
|
|
54
|
+
result['frontend'] = frontend['type']
|
|
55
|
+
result['ui_framework'] = frontend.get('ui_framework')
|
|
56
|
+
result['details']['frontend'] = frontend
|
|
57
|
+
|
|
58
|
+
# 偵測後端
|
|
59
|
+
backend = self._detect_backend()
|
|
60
|
+
if backend:
|
|
61
|
+
result['types'].add('backend')
|
|
62
|
+
result['backend'] = backend['type']
|
|
63
|
+
result['details']['backend'] = backend
|
|
64
|
+
|
|
65
|
+
# 偵測部署模式
|
|
66
|
+
deployment = self._detect_deployment()
|
|
67
|
+
if deployment:
|
|
68
|
+
result['deployment'] = deployment
|
|
69
|
+
result['details']['deployment'] = deployment
|
|
70
|
+
|
|
71
|
+
# 轉換 set 為 list(JSON 序列化用)
|
|
72
|
+
result['types'] = list(result['types'])
|
|
73
|
+
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
def _detect_frontend(self) -> dict | None:
|
|
77
|
+
"""偵測前端技術"""
|
|
78
|
+
package_json = self.project_path / 'package.json'
|
|
79
|
+
|
|
80
|
+
if not package_json.exists():
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
pkg = json.loads(package_json.read_text(encoding='utf-8'))
|
|
85
|
+
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
|
|
86
|
+
|
|
87
|
+
# Angular 偵測
|
|
88
|
+
if '@angular/core' in deps:
|
|
89
|
+
ui_framework = None
|
|
90
|
+
if 'primeng' in deps:
|
|
91
|
+
ui_framework = 'primeng'
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
'type': 'angular',
|
|
95
|
+
'version': deps.get('@angular/core', 'unknown'),
|
|
96
|
+
'ui_framework': ui_framework,
|
|
97
|
+
'has_tailwind': 'tailwindcss' in deps
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Vue + Vite 偵測
|
|
101
|
+
if 'vue' in deps:
|
|
102
|
+
ui_framework = None
|
|
103
|
+
if 'daisyui' in deps:
|
|
104
|
+
ui_framework = 'daisyui'
|
|
105
|
+
elif '@shoelace-style/shoelace' in deps:
|
|
106
|
+
ui_framework = 'shoelace'
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
'type': 'vue-vite' if 'vite' in deps else 'vue',
|
|
110
|
+
'version': deps.get('vue', 'unknown'),
|
|
111
|
+
'ui_framework': ui_framework,
|
|
112
|
+
'has_tailwind': 'tailwindcss' in deps or '@tailwindcss/vite' in deps,
|
|
113
|
+
'has_typescript': 'typescript' in deps or 'vue-tsc' in deps
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Vite (非 Vue)
|
|
117
|
+
if 'vite' in deps:
|
|
118
|
+
ui_framework = None
|
|
119
|
+
if 'daisyui' in deps:
|
|
120
|
+
ui_framework = 'daisyui'
|
|
121
|
+
elif '@shoelace-style/shoelace' in deps:
|
|
122
|
+
ui_framework = 'shoelace'
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
'type': 'vite',
|
|
126
|
+
'version': deps.get('vite', 'unknown'),
|
|
127
|
+
'ui_framework': ui_framework,
|
|
128
|
+
'has_tailwind': 'tailwindcss' in deps
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# React 偵測
|
|
132
|
+
if 'react' in deps:
|
|
133
|
+
return {
|
|
134
|
+
'type': 'react',
|
|
135
|
+
'version': deps.get('react', 'unknown'),
|
|
136
|
+
'ui_framework': self._detect_react_ui(deps)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# 原生 JS(有 package.json 但無框架)
|
|
140
|
+
return {
|
|
141
|
+
'type': 'vanilla',
|
|
142
|
+
'ui_framework': None
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
except Exception:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def _detect_react_ui(self, deps: dict) -> str | None:
|
|
149
|
+
"""偵測 React UI 框架"""
|
|
150
|
+
if '@mui/material' in deps:
|
|
151
|
+
return 'mui'
|
|
152
|
+
if 'antd' in deps:
|
|
153
|
+
return 'antd'
|
|
154
|
+
if '@chakra-ui/react' in deps:
|
|
155
|
+
return 'chakra'
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def _detect_gas(self) -> dict | None:
|
|
159
|
+
"""偵測 Google Apps Script 專案"""
|
|
160
|
+
appsscript_json = self.project_path / 'appsscript.json'
|
|
161
|
+
|
|
162
|
+
if not appsscript_json.exists():
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
config = json.loads(appsscript_json.read_text(encoding='utf-8'))
|
|
167
|
+
|
|
168
|
+
# 偵測 UI 框架(從 HTML 檔案內容判斷)
|
|
169
|
+
ui_framework = None
|
|
170
|
+
html_files = list(self.project_path.glob('*.html'))
|
|
171
|
+
|
|
172
|
+
for html_file in html_files:
|
|
173
|
+
content = html_file.read_text(encoding='utf-8')
|
|
174
|
+
if 'shoelace' in content.lower() or 'sl-' in content:
|
|
175
|
+
ui_framework = 'shoelace'
|
|
176
|
+
break
|
|
177
|
+
elif 'daisyui' in content.lower():
|
|
178
|
+
ui_framework = 'daisyui'
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
# 檢查是否有 Vue
|
|
182
|
+
has_vue = any('vue' in f.read_text(encoding='utf-8').lower()
|
|
183
|
+
for f in html_files if f.exists())
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
'type': 'gas',
|
|
187
|
+
'runtime': config.get('runtimeVersion', 'V8'),
|
|
188
|
+
'webapp': config.get('webapp', {}),
|
|
189
|
+
'ui_framework': ui_framework,
|
|
190
|
+
'has_vue': has_vue,
|
|
191
|
+
'html_files': [f.name for f in html_files],
|
|
192
|
+
'js_files': [f.name for f in self.project_path.glob('*.js')]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
except Exception:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
def _detect_backend(self) -> dict | None:
|
|
199
|
+
"""偵測後端技術"""
|
|
200
|
+
# Python 後端偵測
|
|
201
|
+
python_result = self._detect_python_backend()
|
|
202
|
+
if python_result:
|
|
203
|
+
return python_result
|
|
204
|
+
|
|
205
|
+
# Node.js 後端偵測
|
|
206
|
+
nodejs_result = self._detect_nodejs_backend()
|
|
207
|
+
if nodejs_result:
|
|
208
|
+
return nodejs_result
|
|
209
|
+
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
def _detect_python_backend(self) -> dict | None:
|
|
213
|
+
"""偵測 Python 後端"""
|
|
214
|
+
requirements = self.project_path / 'requirements.txt'
|
|
215
|
+
pyproject = self.project_path / 'pyproject.toml'
|
|
216
|
+
setup_py = self.project_path / 'setup.py'
|
|
217
|
+
|
|
218
|
+
if not any(f.exists() for f in [requirements, pyproject, setup_py]):
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
framework = None
|
|
222
|
+
deps_content = ''
|
|
223
|
+
|
|
224
|
+
if requirements.exists():
|
|
225
|
+
deps_content = requirements.read_text(encoding='utf-8').lower()
|
|
226
|
+
elif pyproject.exists():
|
|
227
|
+
deps_content = pyproject.read_text(encoding='utf-8').lower()
|
|
228
|
+
|
|
229
|
+
# 偵測框架
|
|
230
|
+
if 'fastapi' in deps_content:
|
|
231
|
+
framework = 'fastapi'
|
|
232
|
+
elif 'flask' in deps_content:
|
|
233
|
+
framework = 'flask'
|
|
234
|
+
elif 'django' in deps_content:
|
|
235
|
+
framework = 'django'
|
|
236
|
+
elif 'streamlit' in deps_content:
|
|
237
|
+
framework = 'streamlit'
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
'type': 'python',
|
|
241
|
+
'framework': framework
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
def _detect_nodejs_backend(self) -> dict | None:
|
|
245
|
+
"""偵測 Node.js 後端"""
|
|
246
|
+
package_json = self.project_path / 'package.json'
|
|
247
|
+
|
|
248
|
+
if not package_json.exists():
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
pkg = json.loads(package_json.read_text(encoding='utf-8'))
|
|
253
|
+
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
|
|
254
|
+
|
|
255
|
+
# Vercel Serverless 偵測(有 api/ 目錄且有函數檔案)
|
|
256
|
+
api_dir = self.project_path / 'api'
|
|
257
|
+
if api_dir.exists():
|
|
258
|
+
api_files = list(api_dir.rglob('*.js')) + list(api_dir.rglob('*.ts'))
|
|
259
|
+
if api_files:
|
|
260
|
+
return {
|
|
261
|
+
'type': 'nodejs',
|
|
262
|
+
'framework': 'vercel-serverless'
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# Express 偵測
|
|
266
|
+
if 'express' in deps:
|
|
267
|
+
return {
|
|
268
|
+
'type': 'nodejs',
|
|
269
|
+
'framework': 'express'
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
# Fastify 偵測
|
|
273
|
+
if 'fastify' in deps:
|
|
274
|
+
return {
|
|
275
|
+
'type': 'nodejs',
|
|
276
|
+
'framework': 'fastify'
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# NestJS 偵測
|
|
280
|
+
if '@nestjs/core' in deps:
|
|
281
|
+
return {
|
|
282
|
+
'type': 'nodejs',
|
|
283
|
+
'framework': 'nestjs'
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
except Exception:
|
|
287
|
+
pass
|
|
288
|
+
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
def _detect_deployment(self) -> dict | None:
|
|
292
|
+
"""偵測部署模式"""
|
|
293
|
+
vercel_json = self.project_path / 'vercel.json'
|
|
294
|
+
|
|
295
|
+
if not vercel_json.exists():
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
config = json.loads(vercel_json.read_text(encoding='utf-8'))
|
|
300
|
+
rewrites = config.get('rewrites', [])
|
|
301
|
+
|
|
302
|
+
# 檢查是否為純 Proxy 模式
|
|
303
|
+
is_proxy_only = False
|
|
304
|
+
proxy_target = None
|
|
305
|
+
|
|
306
|
+
for rewrite in rewrites:
|
|
307
|
+
dest = rewrite.get('destination', '')
|
|
308
|
+
source = rewrite.get('source', '')
|
|
309
|
+
|
|
310
|
+
# 如果 destination 是外部 URL,則是 Proxy
|
|
311
|
+
if dest.startswith('http://') or dest.startswith('https://'):
|
|
312
|
+
is_proxy_only = True
|
|
313
|
+
proxy_target = dest.split('/')[2] # 取得域名
|
|
314
|
+
|
|
315
|
+
# 檢查是否有 api/ 目錄(Serverless)
|
|
316
|
+
api_dir = self.project_path / 'api'
|
|
317
|
+
has_api_functions = api_dir.exists() and any(api_dir.rglob('*.ts')) or any(api_dir.rglob('*.js'))
|
|
318
|
+
|
|
319
|
+
if is_proxy_only and not has_api_functions:
|
|
320
|
+
return {
|
|
321
|
+
'type': 'vercel-proxy',
|
|
322
|
+
'proxy_target': proxy_target,
|
|
323
|
+
'description': '純前端 + API 代理'
|
|
324
|
+
}
|
|
325
|
+
elif has_api_functions:
|
|
326
|
+
return {
|
|
327
|
+
'type': 'vercel-serverless',
|
|
328
|
+
'has_proxy': is_proxy_only,
|
|
329
|
+
'description': 'Serverless Functions' + (' + Proxy' if is_proxy_only else '')
|
|
330
|
+
}
|
|
331
|
+
else:
|
|
332
|
+
return {
|
|
333
|
+
'type': 'vercel-static',
|
|
334
|
+
'description': '純靜態網站'
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
except Exception:
|
|
338
|
+
pass
|
|
339
|
+
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
def get_applicable_validators(self) -> Set[str]:
|
|
343
|
+
"""取得適用的驗證器類型"""
|
|
344
|
+
info = self.detect()
|
|
345
|
+
validators = {'common'} # 通用驗證器永遠適用
|
|
346
|
+
|
|
347
|
+
frontend_type = info['frontend']
|
|
348
|
+
if frontend_type == 'gas':
|
|
349
|
+
validators.add('frontend.gas')
|
|
350
|
+
elif frontend_type == 'angular':
|
|
351
|
+
validators.add('frontend.angular')
|
|
352
|
+
elif frontend_type in ['vite', 'vanilla', 'vue-vite', 'vue']:
|
|
353
|
+
validators.add('frontend.vite')
|
|
354
|
+
elif frontend_type == 'react':
|
|
355
|
+
validators.add('frontend.react')
|
|
356
|
+
|
|
357
|
+
backend_type = info['backend']
|
|
358
|
+
if backend_type == 'python':
|
|
359
|
+
validators.add('backend.python')
|
|
360
|
+
elif backend_type == 'nodejs':
|
|
361
|
+
validators.add('backend.nodejs')
|
|
362
|
+
|
|
363
|
+
return validators
|
|
364
|
+
|
|
365
|
+
def get_project_summary(self) -> str:
|
|
366
|
+
"""取得專案摘要(供 CLI 顯示)"""
|
|
367
|
+
info = self.detect()
|
|
368
|
+
|
|
369
|
+
parts = []
|
|
370
|
+
|
|
371
|
+
# 前端
|
|
372
|
+
if info['frontend']:
|
|
373
|
+
frontend_str = info['frontend']
|
|
374
|
+
if info['ui_framework']:
|
|
375
|
+
frontend_str += f" + {info['ui_framework']}"
|
|
376
|
+
parts.append(f"Frontend: {frontend_str}")
|
|
377
|
+
|
|
378
|
+
# 後端
|
|
379
|
+
if info['backend']:
|
|
380
|
+
backend = info['details'].get('backend', {})
|
|
381
|
+
framework = backend.get('framework', '')
|
|
382
|
+
backend_str = f"{info['backend']}"
|
|
383
|
+
if framework:
|
|
384
|
+
backend_str += f" ({framework})"
|
|
385
|
+
parts.append(f"Backend: {backend_str}")
|
|
386
|
+
|
|
387
|
+
# 部署
|
|
388
|
+
if info['deployment']:
|
|
389
|
+
deploy = info['deployment']
|
|
390
|
+
parts.append(f"Deploy: {deploy.get('type', 'unknown')}")
|
|
391
|
+
if deploy.get('proxy_target'):
|
|
392
|
+
parts.append(f"Proxy: {deploy['proxy_target']}")
|
|
393
|
+
|
|
394
|
+
return ' | '.join(parts) if parts else 'Unknown project type'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
前端驗證器
|
|
3
|
+
|
|
4
|
+
支援框架:
|
|
5
|
+
- Vite + Tailwind + DaisyUI
|
|
6
|
+
- Angular + PrimeNG
|
|
7
|
+
- GAS (Google Apps Script) + Vue 3 + Shoelace/DaisyUI
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .vite import ViteValidator
|
|
11
|
+
from .angular import AngularValidator
|
|
12
|
+
from .gas import GasValidator
|
|
13
|
+
|
|
14
|
+
__all__ = ['ViteValidator', 'AngularValidator', 'GasValidator']
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Angular 專案驗證器
|
|
3
|
+
|
|
4
|
+
檢查項目:
|
|
5
|
+
1. PrimeNG 設定
|
|
6
|
+
2. 組件結構
|
|
7
|
+
3. Service 注入
|
|
8
|
+
4. 模組匯入
|
|
9
|
+
5. 表單雙向綁定
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AngularValidator:
|
|
18
|
+
"""Angular 專案驗證器"""
|
|
19
|
+
|
|
20
|
+
name = 'angular'
|
|
21
|
+
|
|
22
|
+
# 忽略目錄
|
|
23
|
+
IGNORE_DIRS = [
|
|
24
|
+
'node_modules', '.git', 'dist', '.angular', '__pycache__'
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
def __init__(self, project_path):
|
|
28
|
+
self.project_path = Path(project_path)
|
|
29
|
+
self.project_name = self.project_path.name
|
|
30
|
+
self.src_path = self.project_path / 'src'
|
|
31
|
+
self.result = {
|
|
32
|
+
'name': self.name,
|
|
33
|
+
'passed': True,
|
|
34
|
+
'errors': [],
|
|
35
|
+
'warnings': [],
|
|
36
|
+
'checks': {}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
def run(self):
|
|
40
|
+
"""執行所有驗證"""
|
|
41
|
+
if not self.project_path.exists():
|
|
42
|
+
self.result['passed'] = False
|
|
43
|
+
self.result['errors'].append(f'專案路徑不存在: {self.project_path}')
|
|
44
|
+
return self.result
|
|
45
|
+
|
|
46
|
+
self.check_primeng_config()
|
|
47
|
+
self.check_component_structure()
|
|
48
|
+
self.check_imports()
|
|
49
|
+
self.check_form_bindings()
|
|
50
|
+
self.check_service_injection()
|
|
51
|
+
|
|
52
|
+
return self.result
|
|
53
|
+
|
|
54
|
+
def check_primeng_config(self):
|
|
55
|
+
"""檢查 PrimeNG 設定"""
|
|
56
|
+
app_config = self.src_path / 'app' / 'app.config.ts'
|
|
57
|
+
|
|
58
|
+
if not app_config.exists():
|
|
59
|
+
self.result['checks']['primeng_config'] = {'skipped': '無 app.config.ts'}
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
content = app_config.read_text(encoding='utf-8')
|
|
64
|
+
has_provider = 'providePrimeNG' in content
|
|
65
|
+
has_theme = '@primeng/themes' in content
|
|
66
|
+
|
|
67
|
+
self.result['checks']['primeng_config'] = {
|
|
68
|
+
'has_provider': has_provider,
|
|
69
|
+
'has_theme': has_theme
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if not has_provider:
|
|
73
|
+
self.result['warnings'].append('缺少 providePrimeNG 設定')
|
|
74
|
+
if not has_theme:
|
|
75
|
+
self.result['warnings'].append('缺少 PrimeNG 主題設定')
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
def check_component_structure(self):
|
|
80
|
+
"""檢查組件結構"""
|
|
81
|
+
issues = []
|
|
82
|
+
|
|
83
|
+
for file_path in self.src_path.rglob('*.component.ts'):
|
|
84
|
+
if self._should_skip(file_path):
|
|
85
|
+
continue
|
|
86
|
+
try:
|
|
87
|
+
content = file_path.read_text(encoding='utf-8')
|
|
88
|
+
rel_path = str(file_path.relative_to(self.project_path))
|
|
89
|
+
line_count = len(content.splitlines())
|
|
90
|
+
|
|
91
|
+
# 檢查組件大小
|
|
92
|
+
if line_count > 500:
|
|
93
|
+
issues.append({
|
|
94
|
+
'file': rel_path,
|
|
95
|
+
'issue': f'組件過大 ({line_count} 行)',
|
|
96
|
+
'severity': 'warning'
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
# 檢查是否有 standalone
|
|
100
|
+
if 'standalone: true' not in content and '@Component' in content:
|
|
101
|
+
issues.append({
|
|
102
|
+
'file': rel_path,
|
|
103
|
+
'issue': '建議使用 standalone component',
|
|
104
|
+
'severity': 'info'
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
self.result['checks']['component_structure'] = {
|
|
111
|
+
'count': len(issues),
|
|
112
|
+
'issues': issues
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for issue in issues:
|
|
116
|
+
if issue['severity'] == 'warning':
|
|
117
|
+
self.result['warnings'].append(f"{issue['file']}: {issue['issue']}")
|
|
118
|
+
|
|
119
|
+
def check_imports(self):
|
|
120
|
+
"""檢查模組匯入"""
|
|
121
|
+
issues = []
|
|
122
|
+
|
|
123
|
+
# 檢查是否有遺漏的 PrimeNG 模組匯入
|
|
124
|
+
primeng_components = {
|
|
125
|
+
'p-table': 'TableModule',
|
|
126
|
+
'p-button': 'ButtonModule',
|
|
127
|
+
'p-dialog': 'DialogModule',
|
|
128
|
+
'p-drawer': 'DrawerModule',
|
|
129
|
+
'p-inputtext': 'InputTextModule',
|
|
130
|
+
'p-select': 'SelectModule',
|
|
131
|
+
'p-datepicker': 'DatePickerModule',
|
|
132
|
+
'p-inputnumber': 'InputNumberModule',
|
|
133
|
+
'p-tag': 'TagModule',
|
|
134
|
+
'p-tabs': 'TabsModule',
|
|
135
|
+
'pInputText': 'InputTextModule',
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for file_path in self.src_path.rglob('*.component.ts'):
|
|
139
|
+
if self._should_skip(file_path):
|
|
140
|
+
continue
|
|
141
|
+
try:
|
|
142
|
+
content = file_path.read_text(encoding='utf-8')
|
|
143
|
+
rel_path = str(file_path.relative_to(self.project_path))
|
|
144
|
+
|
|
145
|
+
for component, module in primeng_components.items():
|
|
146
|
+
if component in content and module not in content:
|
|
147
|
+
issues.append({
|
|
148
|
+
'file': rel_path,
|
|
149
|
+
'component': component,
|
|
150
|
+
'missing_module': module
|
|
151
|
+
})
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
self.result['checks']['imports'] = {
|
|
156
|
+
'count': len(issues),
|
|
157
|
+
'issues': issues
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if issues:
|
|
161
|
+
for issue in issues[:5]:
|
|
162
|
+
self.result['errors'].append(
|
|
163
|
+
f"{issue['file']}: 使用 {issue['component']} 但未匯入 {issue['missing_module']}"
|
|
164
|
+
)
|
|
165
|
+
self.result['passed'] = False
|
|
166
|
+
|
|
167
|
+
def check_form_bindings(self):
|
|
168
|
+
"""檢查表單綁定"""
|
|
169
|
+
issues = []
|
|
170
|
+
|
|
171
|
+
for file_path in self.src_path.rglob('*.html'):
|
|
172
|
+
if self._should_skip(file_path):
|
|
173
|
+
continue
|
|
174
|
+
try:
|
|
175
|
+
content = file_path.read_text(encoding='utf-8')
|
|
176
|
+
rel_path = str(file_path.relative_to(self.project_path))
|
|
177
|
+
|
|
178
|
+
# 檢查是否有未綁定的 input
|
|
179
|
+
# 找出 <input> 但沒有 [(ngModel)] 或 [formControl]
|
|
180
|
+
inputs = re.findall(r'<input[^>]*>', content)
|
|
181
|
+
for inp in inputs:
|
|
182
|
+
if 'ngModel' not in inp and 'formControl' not in inp and 'type="submit"' not in inp:
|
|
183
|
+
# 可能是有意為之,只記錄警告
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
# 檢查 PrimeNG 組件綁定
|
|
187
|
+
primeng_inputs = re.findall(r'<p-(?:inputtext|inputnumber|select|datepicker)[^>]*>', content)
|
|
188
|
+
for pinput in primeng_inputs:
|
|
189
|
+
if 'ngModel' not in pinput and 'formControl' not in pinput:
|
|
190
|
+
issues.append({
|
|
191
|
+
'file': rel_path,
|
|
192
|
+
'issue': 'PrimeNG 輸入元件缺少資料綁定'
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
self.result['checks']['form_bindings'] = {
|
|
199
|
+
'count': len(issues),
|
|
200
|
+
'issues': issues
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if issues:
|
|
204
|
+
for issue in issues[:3]:
|
|
205
|
+
self.result['warnings'].append(f"{issue['file']}: {issue['issue']}")
|
|
206
|
+
|
|
207
|
+
def check_service_injection(self):
|
|
208
|
+
"""檢查 Service 注入"""
|
|
209
|
+
issues = []
|
|
210
|
+
|
|
211
|
+
for file_path in self.src_path.rglob('*.service.ts'):
|
|
212
|
+
if self._should_skip(file_path):
|
|
213
|
+
continue
|
|
214
|
+
try:
|
|
215
|
+
content = file_path.read_text(encoding='utf-8')
|
|
216
|
+
rel_path = str(file_path.relative_to(self.project_path))
|
|
217
|
+
|
|
218
|
+
# 檢查是否有 @Injectable
|
|
219
|
+
if 'class ' in content and '@Injectable' not in content:
|
|
220
|
+
issues.append({
|
|
221
|
+
'file': rel_path,
|
|
222
|
+
'issue': '缺少 @Injectable 裝飾器'
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
# 檢查是否使用 providedIn: 'root'
|
|
226
|
+
if '@Injectable' in content and "providedIn: 'root'" not in content:
|
|
227
|
+
issues.append({
|
|
228
|
+
'file': rel_path,
|
|
229
|
+
'issue': "建議使用 providedIn: 'root'"
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
except Exception:
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
self.result['checks']['service_injection'] = {
|
|
236
|
+
'count': len(issues),
|
|
237
|
+
'issues': issues
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
for issue in issues:
|
|
241
|
+
self.result['warnings'].append(f"{issue['file']}: {issue['issue']}")
|
|
242
|
+
|
|
243
|
+
def _should_skip(self, file_path):
|
|
244
|
+
"""檢查是否應該跳過該檔案"""
|
|
245
|
+
return any(ignore in str(file_path) for ignore in self.IGNORE_DIRS)
|