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.
Files changed (53) hide show
  1. dash_devtools/__init__.py +8 -0
  2. dash_devtools/__main__.py +11 -0
  3. dash_devtools/ai_engine.py +441 -0
  4. dash_devtools/browser.py +541 -0
  5. dash_devtools/cli.py +1452 -0
  6. dash_devtools/database.py +338 -0
  7. dash_devtools/dbdiagram.py +183 -0
  8. dash_devtools/e2e.py +329 -0
  9. dash_devtools/fixers/__init__.py +57 -0
  10. dash_devtools/fixers/migration_fixer.py +115 -0
  11. dash_devtools/fixers/ux_fixer.py +106 -0
  12. dash_devtools/fixers/version_bumper.py +115 -0
  13. dash_devtools/gas_mes_test.py +1241 -0
  14. dash_devtools/generators/__init__.py +84 -0
  15. dash_devtools/health.py +476 -0
  16. dash_devtools/hooks/__init__.py +250 -0
  17. dash_devtools/hooks/pre_commit.py +161 -0
  18. dash_devtools/hooks/pre_push.py +275 -0
  19. dash_devtools/init_test.py +352 -0
  20. dash_devtools/markdown_report.py +309 -0
  21. dash_devtools/migrators/__init__.py +21 -0
  22. dash_devtools/perf.py +321 -0
  23. dash_devtools/report.py +667 -0
  24. dash_devtools/reporters/__init__.py +11 -0
  25. dash_devtools/spec.py +230 -0
  26. dash_devtools/stats.py +355 -0
  27. dash_devtools/test_suite.py +690 -0
  28. dash_devtools/testing.py +416 -0
  29. dash_devtools/validators/__init__.py +157 -0
  30. dash_devtools/validators/backend/__init__.py +12 -0
  31. dash_devtools/validators/backend/nodejs.py +245 -0
  32. dash_devtools/validators/backend/python.py +439 -0
  33. dash_devtools/validators/code_quality.py +243 -0
  34. dash_devtools/validators/common/__init__.py +11 -0
  35. dash_devtools/validators/common/quality.py +319 -0
  36. dash_devtools/validators/common/security.py +270 -0
  37. dash_devtools/validators/common/spec.py +273 -0
  38. dash_devtools/validators/detector.py +394 -0
  39. dash_devtools/validators/frontend/__init__.py +14 -0
  40. dash_devtools/validators/frontend/angular.py +245 -0
  41. dash_devtools/validators/frontend/gas.py +310 -0
  42. dash_devtools/validators/frontend/vite.py +539 -0
  43. dash_devtools/validators/migration.py +292 -0
  44. dash_devtools/validators/performance.py +167 -0
  45. dash_devtools/validators/security.py +205 -0
  46. dash_devtools/vision/__init__.py +368 -0
  47. dash_devtools/watch.py +266 -0
  48. dash_devtools/word_report.py +690 -0
  49. dash_devtools-1.0.0.dist-info/METADATA +834 -0
  50. dash_devtools-1.0.0.dist-info/RECORD +53 -0
  51. dash_devtools-1.0.0.dist-info/WHEEL +5 -0
  52. dash_devtools-1.0.0.dist-info/entry_points.txt +2 -0
  53. 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)