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,310 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google Apps Script (GAS) 專案驗證器 v1.1
|
|
3
|
+
|
|
4
|
+
支援 UI 框架:
|
|
5
|
+
- DaisyUI + Vue 3 CDN(主要)
|
|
6
|
+
- Shoelace(向下相容)
|
|
7
|
+
|
|
8
|
+
檢查項目:
|
|
9
|
+
1. appsscript.json 設定
|
|
10
|
+
2. Code.js 版本號管理
|
|
11
|
+
3. HTML 模板品質(v-for :key、標籤閉合)
|
|
12
|
+
4. DaisyUI 主題設定
|
|
13
|
+
5. Shoelace 綁定語法(僅限 Shoelace 專案)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
import json
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GasValidator:
|
|
22
|
+
"""Google Apps Script 專案驗證器"""
|
|
23
|
+
|
|
24
|
+
name = 'gas'
|
|
25
|
+
|
|
26
|
+
def __init__(self, project_path):
|
|
27
|
+
self.project_path = Path(project_path)
|
|
28
|
+
self.project_name = self.project_path.name
|
|
29
|
+
self.result = {
|
|
30
|
+
'name': self.name,
|
|
31
|
+
'passed': True,
|
|
32
|
+
'errors': [],
|
|
33
|
+
'warnings': [],
|
|
34
|
+
'checks': {}
|
|
35
|
+
}
|
|
36
|
+
# 偵測 UI 框架
|
|
37
|
+
self.ui_framework = self._detect_ui_framework()
|
|
38
|
+
self.has_vue = self._detect_vue()
|
|
39
|
+
|
|
40
|
+
def _detect_ui_framework(self) -> str | None:
|
|
41
|
+
"""偵測 UI 框架(從 HTML 檔案判斷)"""
|
|
42
|
+
for html_file in self.project_path.glob('*.html'):
|
|
43
|
+
try:
|
|
44
|
+
content = html_file.read_text(encoding='utf-8')
|
|
45
|
+
# DaisyUI 優先檢測(更常見)
|
|
46
|
+
if 'daisyui' in content.lower():
|
|
47
|
+
return 'daisyui'
|
|
48
|
+
elif 'shoelace' in content.lower() or 'sl-' in content:
|
|
49
|
+
return 'shoelace'
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
def _detect_vue(self) -> bool:
|
|
55
|
+
"""偵測是否使用 Vue"""
|
|
56
|
+
for html_file in self.project_path.glob('*.html'):
|
|
57
|
+
try:
|
|
58
|
+
content = html_file.read_text(encoding='utf-8')
|
|
59
|
+
if 'vue' in content.lower() and ('v-if' in content or 'v-for' in content or ':' in content):
|
|
60
|
+
return True
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def run(self):
|
|
66
|
+
"""執行所有驗證"""
|
|
67
|
+
if not self.project_path.exists():
|
|
68
|
+
self.result['passed'] = False
|
|
69
|
+
self.result['errors'].append(f'專案路徑不存在: {self.project_path}')
|
|
70
|
+
return self.result
|
|
71
|
+
|
|
72
|
+
# 檢查 appsscript.json
|
|
73
|
+
self.check_appsscript_config()
|
|
74
|
+
|
|
75
|
+
# UI 框架特定檢查
|
|
76
|
+
if self.ui_framework == 'daisyui':
|
|
77
|
+
self.check_daisyui_setup()
|
|
78
|
+
elif self.ui_framework == 'shoelace':
|
|
79
|
+
self.check_shoelace_binding()
|
|
80
|
+
if self.has_vue:
|
|
81
|
+
self.check_vue_custom_element()
|
|
82
|
+
|
|
83
|
+
# Code.js 版本檢查
|
|
84
|
+
self.check_version_management()
|
|
85
|
+
|
|
86
|
+
# HTML 品質檢查
|
|
87
|
+
self.check_html_quality()
|
|
88
|
+
|
|
89
|
+
return self.result
|
|
90
|
+
|
|
91
|
+
def check_appsscript_config(self):
|
|
92
|
+
"""檢查 appsscript.json 設定"""
|
|
93
|
+
config_file = self.project_path / 'appsscript.json'
|
|
94
|
+
|
|
95
|
+
if not config_file.exists():
|
|
96
|
+
self.result['passed'] = False
|
|
97
|
+
self.result['errors'].append('缺少 appsscript.json')
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
config = json.loads(config_file.read_text(encoding='utf-8'))
|
|
102
|
+
|
|
103
|
+
runtime = config.get('runtimeVersion', 'DEPRECATED_ES5')
|
|
104
|
+
webapp = config.get('webapp', {})
|
|
105
|
+
|
|
106
|
+
self.result['checks']['appsscript_config'] = {
|
|
107
|
+
'runtime': runtime,
|
|
108
|
+
'has_webapp': bool(webapp),
|
|
109
|
+
'access': webapp.get('access', 'unknown')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if runtime == 'DEPRECATED_ES5':
|
|
113
|
+
self.result['warnings'].append('建議使用 V8 runtime')
|
|
114
|
+
|
|
115
|
+
except json.JSONDecodeError as e:
|
|
116
|
+
self.result['passed'] = False
|
|
117
|
+
self.result['errors'].append(f'appsscript.json 格式錯誤: {e}')
|
|
118
|
+
|
|
119
|
+
def check_daisyui_setup(self):
|
|
120
|
+
"""檢查 DaisyUI 設定"""
|
|
121
|
+
has_theme = False
|
|
122
|
+
theme_value = None
|
|
123
|
+
|
|
124
|
+
# 檢查 index.html 的 data-theme 設定
|
|
125
|
+
index_html = self.project_path / 'index.html'
|
|
126
|
+
if index_html.exists():
|
|
127
|
+
try:
|
|
128
|
+
content = index_html.read_text(encoding='utf-8')
|
|
129
|
+
theme_match = re.search(r'data-theme\s*=\s*["\']([^"\']+)["\']', content)
|
|
130
|
+
if theme_match:
|
|
131
|
+
has_theme = True
|
|
132
|
+
theme_value = theme_match.group(1)
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
self.result['checks']['daisyui_setup'] = {
|
|
137
|
+
'has_theme': has_theme,
|
|
138
|
+
'theme': theme_value,
|
|
139
|
+
'ui_framework': 'daisyui'
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if not has_theme:
|
|
143
|
+
self.result['warnings'].append(
|
|
144
|
+
'DaisyUI 未設定 data-theme,建議在 <html> 加入:\n'
|
|
145
|
+
' <html data-theme="light">'
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def check_shoelace_binding(self):
|
|
149
|
+
"""檢查 Shoelace 元件綁定語法(僅限 Shoelace 專案)"""
|
|
150
|
+
issues = []
|
|
151
|
+
|
|
152
|
+
# 錯誤的 v-model 使用模式
|
|
153
|
+
wrong_patterns = [
|
|
154
|
+
(r'<sl-input[^>]*v-model\s*=\s*["\'][^"\']+["\']', 'sl-input'),
|
|
155
|
+
(r'<sl-select[^>]*v-model\s*=\s*["\'][^"\']+["\']', 'sl-select'),
|
|
156
|
+
(r'<sl-checkbox[^>]*v-model\s*=\s*["\'][^"\']+["\']', 'sl-checkbox'),
|
|
157
|
+
(r'<sl-textarea[^>]*v-model\s*=\s*["\'][^"\']+["\']', 'sl-textarea'),
|
|
158
|
+
(r'<sl-radio-group[^>]*v-model\s*=\s*["\'][^"\']+["\']', 'sl-radio-group'),
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
for html_file in self.project_path.glob('*.html'):
|
|
162
|
+
try:
|
|
163
|
+
content = html_file.read_text(encoding='utf-8')
|
|
164
|
+
rel_path = html_file.name
|
|
165
|
+
|
|
166
|
+
for pattern, component in wrong_patterns:
|
|
167
|
+
matches = re.findall(pattern, content, re.IGNORECASE)
|
|
168
|
+
if matches:
|
|
169
|
+
issues.append({
|
|
170
|
+
'file': rel_path,
|
|
171
|
+
'component': component,
|
|
172
|
+
'count': len(matches)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
self.result['checks']['shoelace_binding'] = {
|
|
179
|
+
'issues_count': len(issues),
|
|
180
|
+
'issues': issues
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if issues:
|
|
184
|
+
self.result['passed'] = False
|
|
185
|
+
for issue in issues:
|
|
186
|
+
self.result['errors'].append(
|
|
187
|
+
f"Shoelace 綁定錯誤: {issue['file']} - {issue['component']} 使用 v-model(應改用 :value + @sl-input)"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
self.result['warnings'].append(
|
|
191
|
+
'正確寫法範例:\n'
|
|
192
|
+
' <sl-input :value="formData.name" @sl-input="e => formData.name = e.target.value"></sl-input>\n'
|
|
193
|
+
' <sl-select :value="formData.type" @sl-change="e => formData.type = e.target.value"></sl-select>'
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def check_vue_custom_element(self):
|
|
197
|
+
"""檢查 Vue isCustomElement 設定(僅限 Shoelace 專案)"""
|
|
198
|
+
has_config = False
|
|
199
|
+
config_location = None
|
|
200
|
+
|
|
201
|
+
for html_file in self.project_path.glob('*.html'):
|
|
202
|
+
try:
|
|
203
|
+
content = html_file.read_text(encoding='utf-8')
|
|
204
|
+
|
|
205
|
+
if 'isCustomElement' in content:
|
|
206
|
+
has_config = True
|
|
207
|
+
config_location = html_file.name
|
|
208
|
+
|
|
209
|
+
if "tag.startsWith('sl-')" in content or "tag.startsWith(\"sl-\")" in content:
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
except Exception:
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
self.result['checks']['vue_custom_element'] = {
|
|
216
|
+
'has_config': has_config,
|
|
217
|
+
'config_location': config_location
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if not has_config:
|
|
221
|
+
self.result['warnings'].append(
|
|
222
|
+
'Vue 未設定 isCustomElement,可能導致 Shoelace 元件警告\n'
|
|
223
|
+
' 建議在 Vue 初始化時加入:\n'
|
|
224
|
+
" app.config.compilerOptions.isCustomElement = tag => tag.startsWith('sl-');"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def check_version_management(self):
|
|
228
|
+
"""檢查 Code.js 版本號管理"""
|
|
229
|
+
code_js = self.project_path / 'Code.js'
|
|
230
|
+
|
|
231
|
+
if not code_js.exists():
|
|
232
|
+
self.result['checks']['version_management'] = {'skipped': '無 Code.js'}
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
content = code_js.read_text(encoding='utf-8')
|
|
237
|
+
|
|
238
|
+
# 尋找版本號
|
|
239
|
+
version_match = re.search(r"case\s+['\"]getVersion['\"]:\s*\n?\s*return\s*{\s*success:\s*true,\s*data:\s*['\"]([^'\"]+)['\"]", content)
|
|
240
|
+
|
|
241
|
+
if version_match:
|
|
242
|
+
version = version_match.group(1)
|
|
243
|
+
self.result['checks']['version_management'] = {
|
|
244
|
+
'version': version,
|
|
245
|
+
'has_version_api': True
|
|
246
|
+
}
|
|
247
|
+
else:
|
|
248
|
+
self.result['checks']['version_management'] = {
|
|
249
|
+
'has_version_api': False
|
|
250
|
+
}
|
|
251
|
+
self.result['warnings'].append(
|
|
252
|
+
'Code.js 未找到 getVersion API,建議加入版本管理:\n'
|
|
253
|
+
" case 'getVersion':\n"
|
|
254
|
+
" return { success: true, data: '1.0.0' };"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
except Exception as e:
|
|
258
|
+
self.result['checks']['version_management'] = {'error': str(e)}
|
|
259
|
+
|
|
260
|
+
def check_html_quality(self):
|
|
261
|
+
"""檢查 HTML 品質"""
|
|
262
|
+
html_files = list(self.project_path.glob('*.html'))
|
|
263
|
+
issues = []
|
|
264
|
+
|
|
265
|
+
for html_file in html_files:
|
|
266
|
+
try:
|
|
267
|
+
content = html_file.read_text(encoding='utf-8')
|
|
268
|
+
rel_path = html_file.name
|
|
269
|
+
file_issues = []
|
|
270
|
+
|
|
271
|
+
# 檢查 v-for 是否有 :key
|
|
272
|
+
v_for_without_key = re.findall(r'v-for="[^"]*"(?![^>]*:key)', content)
|
|
273
|
+
if v_for_without_key:
|
|
274
|
+
file_issues.append(f'v-for 缺少 :key ({len(v_for_without_key)} 處)')
|
|
275
|
+
|
|
276
|
+
# 檢查空的事件處理器
|
|
277
|
+
empty_handlers = re.findall(r'@\w+\s*=\s*["\']["\']', content)
|
|
278
|
+
if empty_handlers:
|
|
279
|
+
file_issues.append(f'空的事件處理器 ({len(empty_handlers)} 處)')
|
|
280
|
+
|
|
281
|
+
# 檢查未閉合的 HTML 標籤(簡單檢查)
|
|
282
|
+
tags_to_check = ['div', 'span', 'table', 'ul', 'ol', 'select']
|
|
283
|
+
for tag in tags_to_check:
|
|
284
|
+
open_count = len(re.findall(rf'<{tag}[^>]*(?<!/)>', content))
|
|
285
|
+
close_count = len(re.findall(rf'</{tag}>', content))
|
|
286
|
+
self_closing = len(re.findall(rf'<{tag}[^>]*/>', content))
|
|
287
|
+
open_count -= self_closing
|
|
288
|
+
|
|
289
|
+
if open_count > close_count + 2:
|
|
290
|
+
file_issues.append(f'<{tag}> 標籤可能未正確閉合')
|
|
291
|
+
|
|
292
|
+
if file_issues:
|
|
293
|
+
issues.append({
|
|
294
|
+
'file': rel_path,
|
|
295
|
+
'issues': file_issues
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
except Exception:
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
self.result['checks']['html_quality'] = {
|
|
302
|
+
'total_files': len(html_files),
|
|
303
|
+
'files_with_issues': len(issues),
|
|
304
|
+
'issues': issues
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
for issue in issues:
|
|
308
|
+
for problem in issue['issues']:
|
|
309
|
+
if '缺少' in problem or '未正確閉合' in problem:
|
|
310
|
+
self.result['warnings'].append(f"HTML: {issue['file']} - {problem}")
|