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,539 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Vite 專案驗證器 v2.0
|
|
3
|
+
|
|
4
|
+
支援:
|
|
5
|
+
- Vue 3 + Vite + DaisyUI (新架構)
|
|
6
|
+
- Vite + Shoelace (舊架構,向下相容)
|
|
7
|
+
|
|
8
|
+
檢查項目:
|
|
9
|
+
1. DaisyUI/Tailwind 設定
|
|
10
|
+
2. Vue SFC 語法檢查
|
|
11
|
+
3. 禁止 Emoji 圖示
|
|
12
|
+
4. HTML 標籤完整性
|
|
13
|
+
5. 空白按鈕/事件處理器
|
|
14
|
+
6. Bundle 大小
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
import json
|
|
19
|
+
import subprocess
|
|
20
|
+
from fnmatch import fnmatch
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# 常見 Emoji 圖示(這些應該用圖示庫取代)
|
|
25
|
+
ICON_EMOJI_PATTERNS = [
|
|
26
|
+
r'[\U0001F527\U0001F528\U0001F529]', # wrench/hammer
|
|
27
|
+
r'[\U0001F504\U0001F503]', # refresh
|
|
28
|
+
r'[\U0001F50D\U0001F50E]', # search
|
|
29
|
+
r'[\u2699]', # gear
|
|
30
|
+
r'[\U0001F5D1]', # trash
|
|
31
|
+
r'[\u270F]', # pencil
|
|
32
|
+
r'[\u2795]', # plus
|
|
33
|
+
r'[\u2796]', # minus
|
|
34
|
+
r'[\u2705]', # check mark
|
|
35
|
+
r'[\u274C]', # cross mark
|
|
36
|
+
r'[\u26A0]', # warning
|
|
37
|
+
r'[\U0001F6A8]', # alert
|
|
38
|
+
r'[\u2139]', # info
|
|
39
|
+
r'[\U0001F3E2]', # building
|
|
40
|
+
r'[\U0001F4CB]', # clipboard
|
|
41
|
+
r'[\U0001F4C5\U0001F4C6]', # calendar
|
|
42
|
+
r'[\U0001F464\U0001F465]', # person/people
|
|
43
|
+
r'[\U0001F512\U0001F513]', # lock
|
|
44
|
+
r'[\U0001F510]', # key lock
|
|
45
|
+
r'[\u231B\u23F3]', # hourglass
|
|
46
|
+
r'[1-9]\uFE0F?\u20E3', # 1️⃣ 2️⃣ etc
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ViteValidator:
|
|
51
|
+
"""Vite 專案驗證器 (支援 Vue 3 + DaisyUI)"""
|
|
52
|
+
|
|
53
|
+
name = 'vite'
|
|
54
|
+
|
|
55
|
+
IGNORE_DIRS = [
|
|
56
|
+
'node_modules', '.git', 'dist', 'build', '.cache', '.vercel'
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
def __init__(self, project_path):
|
|
60
|
+
self.project_path = Path(project_path)
|
|
61
|
+
self.project_name = self.project_path.name
|
|
62
|
+
self.src_path = self.project_path / 'src'
|
|
63
|
+
self.scanignore = self._parse_scanignore()
|
|
64
|
+
self.result = {
|
|
65
|
+
'name': self.name,
|
|
66
|
+
'passed': True,
|
|
67
|
+
'errors': [],
|
|
68
|
+
'warnings': [],
|
|
69
|
+
'checks': {}
|
|
70
|
+
}
|
|
71
|
+
# 偵測專案類型
|
|
72
|
+
self.ui_framework = self._detect_ui_framework()
|
|
73
|
+
self.is_vue = self._detect_vue()
|
|
74
|
+
|
|
75
|
+
def _detect_ui_framework(self) -> str | None:
|
|
76
|
+
"""偵測 UI 框架"""
|
|
77
|
+
pkg_path = self.project_path / 'package.json'
|
|
78
|
+
if not pkg_path.exists():
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
pkg = json.loads(pkg_path.read_text(encoding='utf-8'))
|
|
83
|
+
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
|
|
84
|
+
|
|
85
|
+
if 'daisyui' in deps:
|
|
86
|
+
return 'daisyui'
|
|
87
|
+
if '@shoelace-style/shoelace' in deps:
|
|
88
|
+
return 'shoelace'
|
|
89
|
+
if 'tailwindcss' in deps:
|
|
90
|
+
return 'tailwind'
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
def _detect_vue(self) -> bool:
|
|
96
|
+
"""偵測是否為 Vue 專案"""
|
|
97
|
+
pkg_path = self.project_path / 'package.json'
|
|
98
|
+
if not pkg_path.exists():
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
pkg = json.loads(pkg_path.read_text(encoding='utf-8'))
|
|
103
|
+
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
|
|
104
|
+
return 'vue' in deps
|
|
105
|
+
except Exception:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
def run(self):
|
|
109
|
+
"""執行所有驗證"""
|
|
110
|
+
if not self.project_path.exists():
|
|
111
|
+
self.result['passed'] = False
|
|
112
|
+
self.result['errors'].append(f'專案路徑不存在: {self.project_path}')
|
|
113
|
+
return self.result
|
|
114
|
+
|
|
115
|
+
# 根據 UI 框架選擇驗證
|
|
116
|
+
if self.ui_framework == 'daisyui':
|
|
117
|
+
self.check_daisyui_setup()
|
|
118
|
+
elif self.ui_framework == 'shoelace':
|
|
119
|
+
self.check_shoelace_setup()
|
|
120
|
+
|
|
121
|
+
# Vue SFC 檢查
|
|
122
|
+
if self.is_vue:
|
|
123
|
+
self.check_vue_sfc()
|
|
124
|
+
|
|
125
|
+
# 通用檢查
|
|
126
|
+
self.check_emoji_icons()
|
|
127
|
+
self.check_incomplete_html_tags()
|
|
128
|
+
self.check_empty_buttons()
|
|
129
|
+
self.check_bundle_size()
|
|
130
|
+
|
|
131
|
+
return self.result
|
|
132
|
+
|
|
133
|
+
def check_daisyui_setup(self):
|
|
134
|
+
"""檢查 DaisyUI + Tailwind CSS v4 設定"""
|
|
135
|
+
pkg_path = self.project_path / 'package.json'
|
|
136
|
+
has_daisyui = False
|
|
137
|
+
has_tailwind = False
|
|
138
|
+
daisyui_version = None
|
|
139
|
+
tailwind_version = None
|
|
140
|
+
|
|
141
|
+
if pkg_path.exists():
|
|
142
|
+
try:
|
|
143
|
+
pkg = json.loads(pkg_path.read_text(encoding='utf-8'))
|
|
144
|
+
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
|
|
145
|
+
|
|
146
|
+
has_daisyui = 'daisyui' in deps
|
|
147
|
+
has_tailwind = 'tailwindcss' in deps or '@tailwindcss/vite' in deps
|
|
148
|
+
|
|
149
|
+
daisyui_version = deps.get('daisyui')
|
|
150
|
+
tailwind_version = deps.get('tailwindcss') or deps.get('@tailwindcss/vite')
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
# 檢查 CSS 配置 (Tailwind v4 使用 @import/@plugin)
|
|
155
|
+
css_config_valid = False
|
|
156
|
+
css_file = self.src_path / 'style.css'
|
|
157
|
+
if not css_file.exists():
|
|
158
|
+
css_file = self.src_path / 'index.css'
|
|
159
|
+
if not css_file.exists():
|
|
160
|
+
css_file = self.src_path / 'main.css'
|
|
161
|
+
|
|
162
|
+
if css_file.exists():
|
|
163
|
+
try:
|
|
164
|
+
content = css_file.read_text(encoding='utf-8')
|
|
165
|
+
# Tailwind v4 語法
|
|
166
|
+
if '@import "tailwindcss"' in content or '@tailwind' in content:
|
|
167
|
+
css_config_valid = True
|
|
168
|
+
if '@plugin "daisyui"' in content:
|
|
169
|
+
css_config_valid = True
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
# 檢查 vite.config.ts 是否有 tailwindcss plugin
|
|
174
|
+
vite_config = self.project_path / 'vite.config.ts'
|
|
175
|
+
vite_config_valid = False
|
|
176
|
+
if vite_config.exists():
|
|
177
|
+
try:
|
|
178
|
+
content = vite_config.read_text(encoding='utf-8')
|
|
179
|
+
if 'tailwindcss' in content or '@tailwindcss/vite' in content:
|
|
180
|
+
vite_config_valid = True
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
self.result['checks']['daisyui_setup'] = {
|
|
185
|
+
'has_daisyui': has_daisyui,
|
|
186
|
+
'has_tailwind': has_tailwind,
|
|
187
|
+
'daisyui_version': daisyui_version,
|
|
188
|
+
'tailwind_version': tailwind_version,
|
|
189
|
+
'css_config_valid': css_config_valid,
|
|
190
|
+
'vite_config_valid': vite_config_valid
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if has_daisyui and not css_config_valid:
|
|
194
|
+
self.result['warnings'].append('DaisyUI 已安裝但 CSS 配置可能不完整')
|
|
195
|
+
|
|
196
|
+
if has_tailwind and not vite_config_valid:
|
|
197
|
+
self.result['warnings'].append('Tailwind 已安裝但 vite.config 可能缺少 plugin')
|
|
198
|
+
|
|
199
|
+
def check_shoelace_setup(self):
|
|
200
|
+
"""檢查 Shoelace 設定 (向下相容)"""
|
|
201
|
+
index_html = self.project_path / 'index.html'
|
|
202
|
+
has_shoelace_css = False
|
|
203
|
+
has_shoelace_js = False
|
|
204
|
+
|
|
205
|
+
if index_html.exists():
|
|
206
|
+
try:
|
|
207
|
+
content = index_html.read_text(encoding='utf-8')
|
|
208
|
+
has_shoelace_css = 'shoelace' in content.lower() and '.css' in content
|
|
209
|
+
has_shoelace_js = 'shoelace' in content.lower() and '.js' in content
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
self.result['checks']['shoelace_setup'] = {
|
|
214
|
+
'has_css': has_shoelace_css,
|
|
215
|
+
'has_js': has_shoelace_js
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
def check_vue_sfc(self):
|
|
219
|
+
"""檢查 Vue SFC 語法"""
|
|
220
|
+
if not self.src_path.exists():
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
vue_files = list(self.src_path.rglob('*.vue'))
|
|
224
|
+
issues = []
|
|
225
|
+
|
|
226
|
+
for file_path in vue_files:
|
|
227
|
+
if self._should_skip(file_path):
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
content = file_path.read_text(encoding='utf-8')
|
|
232
|
+
rel_path = str(file_path.relative_to(self.project_path))
|
|
233
|
+
file_issues = []
|
|
234
|
+
|
|
235
|
+
# 1. 檢查基本結構
|
|
236
|
+
has_template = '<template>' in content or '<template ' in content
|
|
237
|
+
has_script = '<script' in content
|
|
238
|
+
|
|
239
|
+
if not has_template:
|
|
240
|
+
file_issues.append('缺少 <template> 區塊')
|
|
241
|
+
|
|
242
|
+
# 2. 檢查 script setup 語法
|
|
243
|
+
if '<script setup' in content:
|
|
244
|
+
# 檢查是否有未使用的 import
|
|
245
|
+
imports = re.findall(r'import\s+{([^}]+)}\s+from', content)
|
|
246
|
+
for import_group in imports:
|
|
247
|
+
items = [i.strip() for i in import_group.split(',')]
|
|
248
|
+
for item in items:
|
|
249
|
+
# 簡單檢查:import 的項目是否在 template 中使用
|
|
250
|
+
clean_item = item.split(' as ')[-1].strip()
|
|
251
|
+
template_match = re.search(r'<template[^>]*>([\s\S]*)</template>', content)
|
|
252
|
+
if template_match:
|
|
253
|
+
template_content = template_match.group(1)
|
|
254
|
+
# 檢查元件使用 (PascalCase 或 kebab-case)
|
|
255
|
+
kebab_case = re.sub(r'(?<!^)(?=[A-Z])', '-', clean_item).lower()
|
|
256
|
+
if clean_item not in template_content and kebab_case not in template_content:
|
|
257
|
+
# 可能是未使用的 import,但不確定,只記錄為 info
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
# 3. 檢查 template 中的常見錯誤
|
|
261
|
+
template_match = re.search(r'<template[^>]*>([\s\S]*)</template>', content)
|
|
262
|
+
if template_match:
|
|
263
|
+
template = template_match.group(1)
|
|
264
|
+
|
|
265
|
+
# 檢查 v-for 是否有 :key
|
|
266
|
+
v_for_without_key = re.findall(r'v-for="[^"]*"(?![^>]*:key)', template)
|
|
267
|
+
if v_for_without_key:
|
|
268
|
+
file_issues.append(f'v-for 缺少 :key ({len(v_for_without_key)} 處)')
|
|
269
|
+
|
|
270
|
+
# 檢查空的 @click
|
|
271
|
+
empty_click = re.findall(r'@click="\s*"', template)
|
|
272
|
+
if empty_click:
|
|
273
|
+
file_issues.append(f'空的 @click 事件處理器 ({len(empty_click)} 處)')
|
|
274
|
+
|
|
275
|
+
if file_issues:
|
|
276
|
+
issues.append({
|
|
277
|
+
'file': rel_path,
|
|
278
|
+
'issues': file_issues
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
except Exception:
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
self.result['checks']['vue_sfc'] = {
|
|
285
|
+
'total_files': len(vue_files),
|
|
286
|
+
'files_with_issues': len(issues),
|
|
287
|
+
'issues': issues
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# 只有嚴重問題才報錯
|
|
291
|
+
for issue in issues:
|
|
292
|
+
for problem in issue['issues']:
|
|
293
|
+
if '缺少' in problem:
|
|
294
|
+
self.result['errors'].append(f"Vue SFC: {issue['file']} - {problem}")
|
|
295
|
+
else:
|
|
296
|
+
self.result['warnings'].append(f"Vue SFC: {issue['file']} - {problem}")
|
|
297
|
+
|
|
298
|
+
def check_vue_tsc(self):
|
|
299
|
+
"""執行 vue-tsc 類型檢查"""
|
|
300
|
+
pkg_path = self.project_path / 'package.json'
|
|
301
|
+
has_vue_tsc = False
|
|
302
|
+
|
|
303
|
+
if pkg_path.exists():
|
|
304
|
+
try:
|
|
305
|
+
content = pkg_path.read_text(encoding='utf-8')
|
|
306
|
+
has_vue_tsc = 'vue-tsc' in content
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
if not has_vue_tsc:
|
|
311
|
+
self.result['checks']['vue_tsc'] = {'skipped': '未安裝 vue-tsc'}
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
result = subprocess.run(
|
|
316
|
+
['npx', 'vue-tsc', '--noEmit'],
|
|
317
|
+
cwd=self.project_path,
|
|
318
|
+
capture_output=True,
|
|
319
|
+
text=True,
|
|
320
|
+
timeout=60
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
self.result['checks']['vue_tsc'] = {
|
|
324
|
+
'passed': result.returncode == 0,
|
|
325
|
+
'output': result.stdout[:500] if result.stdout else result.stderr[:500]
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if result.returncode != 0:
|
|
329
|
+
self.result['warnings'].append('vue-tsc 類型檢查有警告或錯誤')
|
|
330
|
+
|
|
331
|
+
except subprocess.TimeoutExpired:
|
|
332
|
+
self.result['checks']['vue_tsc'] = {'error': '執行逾時'}
|
|
333
|
+
except Exception as e:
|
|
334
|
+
self.result['checks']['vue_tsc'] = {'error': str(e)}
|
|
335
|
+
|
|
336
|
+
def check_emoji_icons(self):
|
|
337
|
+
"""檢查 Emoji 圖示"""
|
|
338
|
+
if not self.src_path.exists():
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
total_count = 0
|
|
342
|
+
file_issues = {}
|
|
343
|
+
combined_pattern = '|'.join(ICON_EMOJI_PATTERNS)
|
|
344
|
+
|
|
345
|
+
# 根據專案類型決定要檢查的副檔名
|
|
346
|
+
extensions = ['*.js', '*.ts', '*.html']
|
|
347
|
+
if self.is_vue:
|
|
348
|
+
extensions.append('*.vue')
|
|
349
|
+
|
|
350
|
+
for ext in extensions:
|
|
351
|
+
for file_path in self.src_path.rglob(ext):
|
|
352
|
+
if self._should_skip(file_path):
|
|
353
|
+
continue
|
|
354
|
+
try:
|
|
355
|
+
content = file_path.read_text(encoding='utf-8')
|
|
356
|
+
template_content = self._extract_template_strings(content)
|
|
357
|
+
matches = re.findall(combined_pattern, template_content)
|
|
358
|
+
if matches:
|
|
359
|
+
rel_path = str(file_path.relative_to(self.project_path))
|
|
360
|
+
file_issues[rel_path] = len(matches)
|
|
361
|
+
total_count += len(matches)
|
|
362
|
+
except Exception:
|
|
363
|
+
pass
|
|
364
|
+
|
|
365
|
+
self.result['checks']['emoji_icons'] = {
|
|
366
|
+
'count': total_count,
|
|
367
|
+
'files': file_issues
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if total_count > 0:
|
|
371
|
+
icon_lib = 'lucide-vue-next' if self.is_vue else 'sl-icon'
|
|
372
|
+
self.result['warnings'].append(
|
|
373
|
+
f'Emoji 圖示: {total_count} 個(應改用 {icon_lib})'
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def _extract_template_strings(self, content):
|
|
377
|
+
"""提取模板字串內容"""
|
|
378
|
+
# JavaScript 模板字串
|
|
379
|
+
template_matches = re.findall(r'`[^`]*`', content, re.DOTALL)
|
|
380
|
+
# Vue template
|
|
381
|
+
vue_template = re.findall(r'<template[^>]*>([\s\S]*?)</template>', content)
|
|
382
|
+
return '\n'.join(template_matches + vue_template)
|
|
383
|
+
|
|
384
|
+
def check_incomplete_html_tags(self):
|
|
385
|
+
"""檢查不完整的 HTML 標籤"""
|
|
386
|
+
if not self.src_path.exists():
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
tags_to_check = ['select', 'textarea', 'table', 'ul', 'ol', 'div']
|
|
390
|
+
issues = []
|
|
391
|
+
|
|
392
|
+
extensions = ['*.js', '*.ts']
|
|
393
|
+
if self.is_vue:
|
|
394
|
+
extensions.append('*.vue')
|
|
395
|
+
|
|
396
|
+
for ext in extensions:
|
|
397
|
+
for file_path in self.src_path.rglob(ext):
|
|
398
|
+
if self._should_skip(file_path):
|
|
399
|
+
continue
|
|
400
|
+
try:
|
|
401
|
+
content = file_path.read_text(encoding='utf-8')
|
|
402
|
+
rel_path = str(file_path.relative_to(self.project_path))
|
|
403
|
+
|
|
404
|
+
# 檢查 [pattern:HTML 標籤不完整] 排除
|
|
405
|
+
if self._is_pattern_ignored(rel_path, 'HTML 標籤不完整'):
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
for tag in tags_to_check:
|
|
409
|
+
open_count = len(re.findall(rf'<{tag}[^>]*>', content))
|
|
410
|
+
close_count = len(re.findall(rf'</{tag}>', content))
|
|
411
|
+
|
|
412
|
+
if open_count > close_count:
|
|
413
|
+
diff = open_count - close_count
|
|
414
|
+
issues.append({
|
|
415
|
+
'file': rel_path,
|
|
416
|
+
'tag': tag,
|
|
417
|
+
'missing': diff
|
|
418
|
+
})
|
|
419
|
+
except Exception:
|
|
420
|
+
pass
|
|
421
|
+
|
|
422
|
+
self.result['checks']['incomplete_html'] = {
|
|
423
|
+
'count': len(issues),
|
|
424
|
+
'issues': issues
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if issues:
|
|
428
|
+
self.result['passed'] = False
|
|
429
|
+
for issue in issues[:5]:
|
|
430
|
+
self.result['errors'].append(
|
|
431
|
+
f"HTML 標籤不完整: {issue['file']} 缺少 {issue['missing']} 個 </{issue['tag']}>"
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
def check_empty_buttons(self):
|
|
435
|
+
"""檢查空白按鈕內容"""
|
|
436
|
+
if not self.src_path.exists():
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
patterns = [
|
|
440
|
+
r'<button[^>]*>\s*\n?\s*</button>',
|
|
441
|
+
r'<sl-button[^>]*>\s*\n?\s*</sl-button>'
|
|
442
|
+
]
|
|
443
|
+
issues = []
|
|
444
|
+
|
|
445
|
+
extensions = ['*.js', '*.ts']
|
|
446
|
+
if self.is_vue:
|
|
447
|
+
extensions.append('*.vue')
|
|
448
|
+
|
|
449
|
+
for ext in extensions:
|
|
450
|
+
for file_path in self.src_path.rglob(ext):
|
|
451
|
+
if self._should_skip(file_path):
|
|
452
|
+
continue
|
|
453
|
+
try:
|
|
454
|
+
content = file_path.read_text(encoding='utf-8')
|
|
455
|
+
total_matches = 0
|
|
456
|
+
for pattern in patterns:
|
|
457
|
+
matches = re.findall(pattern, content)
|
|
458
|
+
total_matches += len(matches)
|
|
459
|
+
|
|
460
|
+
if total_matches > 0:
|
|
461
|
+
rel_path = str(file_path.relative_to(self.project_path))
|
|
462
|
+
issues.append({
|
|
463
|
+
'file': rel_path,
|
|
464
|
+
'count': total_matches
|
|
465
|
+
})
|
|
466
|
+
except Exception:
|
|
467
|
+
pass
|
|
468
|
+
|
|
469
|
+
self.result['checks']['empty_buttons'] = {
|
|
470
|
+
'count': sum(i['count'] for i in issues),
|
|
471
|
+
'files': issues
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if issues:
|
|
475
|
+
total = sum(i['count'] for i in issues)
|
|
476
|
+
self.result['warnings'].append(f'空白按鈕: {total} 個')
|
|
477
|
+
|
|
478
|
+
def check_bundle_size(self):
|
|
479
|
+
"""檢查 Bundle 大小"""
|
|
480
|
+
dist_path = self.project_path / 'dist'
|
|
481
|
+
if not dist_path.exists():
|
|
482
|
+
self.result['checks']['bundle_size'] = {'skipped': '無 dist 目錄'}
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
css_size = sum(f.stat().st_size for f in dist_path.rglob('*.css'))
|
|
486
|
+
js_size = sum(f.stat().st_size for f in dist_path.rglob('*.js'))
|
|
487
|
+
|
|
488
|
+
css_kb = css_size / 1024
|
|
489
|
+
js_kb = js_size / 1024
|
|
490
|
+
|
|
491
|
+
self.result['checks']['bundle_size'] = {
|
|
492
|
+
'css_kb': round(css_kb, 2),
|
|
493
|
+
'js_kb': round(js_kb, 2)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if css_kb > 200:
|
|
497
|
+
self.result['warnings'].append(f'CSS Bundle 過大: {css_kb:.2f} KB (建議 < 200 KB)')
|
|
498
|
+
if js_kb > 500:
|
|
499
|
+
self.result['warnings'].append(f'JS Bundle 過大: {js_kb:.2f} KB (建議 < 500 KB)')
|
|
500
|
+
|
|
501
|
+
def _parse_scanignore(self):
|
|
502
|
+
"""解析 .scanignore 檔案"""
|
|
503
|
+
scanignore_file = self.project_path / '.scanignore'
|
|
504
|
+
result = {'paths': [], 'pattern_paths': []}
|
|
505
|
+
|
|
506
|
+
if not scanignore_file.exists():
|
|
507
|
+
return result
|
|
508
|
+
|
|
509
|
+
try:
|
|
510
|
+
for line in scanignore_file.read_text(encoding='utf-8').splitlines():
|
|
511
|
+
line = line.strip()
|
|
512
|
+
if not line or line.startswith('#'):
|
|
513
|
+
continue
|
|
514
|
+
|
|
515
|
+
pattern_match = re.match(r'^\[pattern:(.+?)\]\s+(.+)$', line)
|
|
516
|
+
if pattern_match:
|
|
517
|
+
pattern_name = pattern_match.group(1).strip()
|
|
518
|
+
path_glob = pattern_match.group(2).strip()
|
|
519
|
+
result['pattern_paths'].append((pattern_name, path_glob))
|
|
520
|
+
else:
|
|
521
|
+
result['paths'].append(line)
|
|
522
|
+
except Exception:
|
|
523
|
+
pass
|
|
524
|
+
|
|
525
|
+
return result
|
|
526
|
+
|
|
527
|
+
def _is_pattern_ignored(self, rel_path, pattern_name):
|
|
528
|
+
"""檢查檔案是否被 .scanignore 的特定 pattern 排除"""
|
|
529
|
+
for p_name, p_glob in self.scanignore['pattern_paths']:
|
|
530
|
+
if p_name == pattern_name:
|
|
531
|
+
if fnmatch(rel_path, p_glob):
|
|
532
|
+
return True
|
|
533
|
+
if rel_path == p_glob:
|
|
534
|
+
return True
|
|
535
|
+
return False
|
|
536
|
+
|
|
537
|
+
def _should_skip(self, file_path):
|
|
538
|
+
"""檢查是否應該跳過該檔案"""
|
|
539
|
+
return any(ignore in str(file_path) for ignore in self.IGNORE_DIRS)
|