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
dash_devtools/perf.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
效能測試模組
|
|
3
|
+
使用 Lighthouse 進行網站效能分析
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
import json
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, List, Optional
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
# Lighthouse Node.js 腳本
|
|
19
|
+
LIGHTHOUSE_SCRIPT = '''
|
|
20
|
+
const { execSync } = require("child_process");
|
|
21
|
+
|
|
22
|
+
const url = process.argv[2];
|
|
23
|
+
const categories = process.argv[3] || "performance,accessibility,best-practices,seo";
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// 使用 lighthouse CLI
|
|
27
|
+
const result = execSync(
|
|
28
|
+
`npx lighthouse "${url}" --output=json --quiet --chrome-flags="--headless --no-sandbox" --only-categories=${categories}`,
|
|
29
|
+
{
|
|
30
|
+
encoding: "utf-8",
|
|
31
|
+
timeout: 120000,
|
|
32
|
+
maxBuffer: 10 * 1024 * 1024
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const report = JSON.parse(result);
|
|
37
|
+
|
|
38
|
+
// 擷取關鍵數據
|
|
39
|
+
const output = {
|
|
40
|
+
url: url,
|
|
41
|
+
success: true,
|
|
42
|
+
scores: {
|
|
43
|
+
performance: Math.round((report.categories.performance?.score || 0) * 100),
|
|
44
|
+
accessibility: Math.round((report.categories.accessibility?.score || 0) * 100),
|
|
45
|
+
bestPractices: Math.round((report.categories["best-practices"]?.score || 0) * 100),
|
|
46
|
+
seo: Math.round((report.categories.seo?.score || 0) * 100)
|
|
47
|
+
},
|
|
48
|
+
metrics: {
|
|
49
|
+
fcp: report.audits["first-contentful-paint"]?.numericValue || 0,
|
|
50
|
+
lcp: report.audits["largest-contentful-paint"]?.numericValue || 0,
|
|
51
|
+
tbt: report.audits["total-blocking-time"]?.numericValue || 0,
|
|
52
|
+
cls: report.audits["cumulative-layout-shift"]?.numericValue || 0,
|
|
53
|
+
si: report.audits["speed-index"]?.numericValue || 0,
|
|
54
|
+
tti: report.audits["interactive"]?.numericValue || 0
|
|
55
|
+
},
|
|
56
|
+
opportunities: [],
|
|
57
|
+
diagnostics: []
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// 擷取改善建議 (有節省時間的項目)
|
|
61
|
+
for (const [id, audit] of Object.entries(report.audits)) {
|
|
62
|
+
if (audit.details?.overallSavingsMs > 100) {
|
|
63
|
+
output.opportunities.push({
|
|
64
|
+
id: id,
|
|
65
|
+
title: audit.title,
|
|
66
|
+
savings: Math.round(audit.details.overallSavingsMs),
|
|
67
|
+
description: audit.description?.substring(0, 150)
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 排序: 節省時間最多的優先
|
|
73
|
+
output.opportunities.sort((a, b) => b.savings - a.savings);
|
|
74
|
+
output.opportunities = output.opportunities.slice(0, 5);
|
|
75
|
+
|
|
76
|
+
// 擷取診斷資訊
|
|
77
|
+
const diagnosticIds = [
|
|
78
|
+
"dom-size",
|
|
79
|
+
"bootup-time",
|
|
80
|
+
"mainthread-work-breakdown",
|
|
81
|
+
"font-display",
|
|
82
|
+
"uses-responsive-images"
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
for (const id of diagnosticIds) {
|
|
86
|
+
const audit = report.audits[id];
|
|
87
|
+
if (audit && audit.score !== null && audit.score < 1) {
|
|
88
|
+
output.diagnostics.push({
|
|
89
|
+
id: id,
|
|
90
|
+
title: audit.title,
|
|
91
|
+
score: Math.round(audit.score * 100),
|
|
92
|
+
displayValue: audit.displayValue || ""
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(JSON.stringify(output));
|
|
98
|
+
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.log(JSON.stringify({
|
|
101
|
+
url: url,
|
|
102
|
+
success: false,
|
|
103
|
+
error: err.message,
|
|
104
|
+
scores: { performance: 0, accessibility: 0, bestPractices: 0, seo: 0 },
|
|
105
|
+
metrics: {},
|
|
106
|
+
opportunities: [],
|
|
107
|
+
diagnostics: []
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
'''
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def run_perf_test(
|
|
114
|
+
url: str,
|
|
115
|
+
categories: str = "performance,accessibility,best-practices,seo",
|
|
116
|
+
timeout: int = 120000
|
|
117
|
+
) -> Dict:
|
|
118
|
+
"""
|
|
119
|
+
執行 Lighthouse 效能測試
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
url: 要測試的網址
|
|
123
|
+
categories: 要測試的類別
|
|
124
|
+
timeout: 超時時間 (毫秒)
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
測試結果字典
|
|
128
|
+
"""
|
|
129
|
+
# 建立臨時腳本檔案
|
|
130
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f:
|
|
131
|
+
f.write(LIGHTHOUSE_SCRIPT)
|
|
132
|
+
script_path = f.name
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
# 執行 Node.js 腳本
|
|
136
|
+
result = subprocess.run(
|
|
137
|
+
['node', script_path, url, categories],
|
|
138
|
+
capture_output=True,
|
|
139
|
+
text=True,
|
|
140
|
+
timeout=timeout / 1000 + 30,
|
|
141
|
+
cwd=get_node_cwd()
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if result.returncode != 0 and not result.stdout:
|
|
145
|
+
return {
|
|
146
|
+
'url': url,
|
|
147
|
+
'success': False,
|
|
148
|
+
'error': f"Script error: {result.stderr}",
|
|
149
|
+
'scores': {'performance': 0, 'accessibility': 0, 'bestPractices': 0, 'seo': 0},
|
|
150
|
+
'metrics': {},
|
|
151
|
+
'opportunities': [],
|
|
152
|
+
'diagnostics': []
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# 解析 JSON 輸出
|
|
156
|
+
try:
|
|
157
|
+
return json.loads(result.stdout.strip())
|
|
158
|
+
except json.JSONDecodeError:
|
|
159
|
+
return {
|
|
160
|
+
'url': url,
|
|
161
|
+
'success': False,
|
|
162
|
+
'error': f"Invalid JSON: {result.stdout[:200]}",
|
|
163
|
+
'scores': {'performance': 0, 'accessibility': 0, 'bestPractices': 0, 'seo': 0},
|
|
164
|
+
'metrics': {},
|
|
165
|
+
'opportunities': [],
|
|
166
|
+
'diagnostics': []
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
except subprocess.TimeoutExpired:
|
|
170
|
+
return {
|
|
171
|
+
'url': url,
|
|
172
|
+
'success': False,
|
|
173
|
+
'error': 'Timeout exceeded',
|
|
174
|
+
'scores': {'performance': 0, 'accessibility': 0, 'bestPractices': 0, 'seo': 0},
|
|
175
|
+
'metrics': {},
|
|
176
|
+
'opportunities': [],
|
|
177
|
+
'diagnostics': []
|
|
178
|
+
}
|
|
179
|
+
except FileNotFoundError:
|
|
180
|
+
return {
|
|
181
|
+
'url': url,
|
|
182
|
+
'success': False,
|
|
183
|
+
'error': 'Node.js not found',
|
|
184
|
+
'scores': {'performance': 0, 'accessibility': 0, 'bestPractices': 0, 'seo': 0},
|
|
185
|
+
'metrics': {},
|
|
186
|
+
'opportunities': [],
|
|
187
|
+
'diagnostics': []
|
|
188
|
+
}
|
|
189
|
+
finally:
|
|
190
|
+
Path(script_path).unlink(missing_ok=True)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_node_cwd() -> str:
|
|
194
|
+
"""取得有 Node.js 的工作目錄"""
|
|
195
|
+
return '/Users/dash/Documents/github/smai-process-vision'
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_score_color(score: int) -> str:
|
|
199
|
+
"""根據分數取得顏色"""
|
|
200
|
+
if score >= 90:
|
|
201
|
+
return "green"
|
|
202
|
+
elif score >= 50:
|
|
203
|
+
return "yellow"
|
|
204
|
+
else:
|
|
205
|
+
return "red"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def get_score_emoji(score: int) -> str:
|
|
209
|
+
"""根據分數取得狀態符號"""
|
|
210
|
+
if score >= 90:
|
|
211
|
+
return "[green]OK[/green]"
|
|
212
|
+
elif score >= 50:
|
|
213
|
+
return "[yellow]!![/yellow]"
|
|
214
|
+
else:
|
|
215
|
+
return "[red]XX[/red]"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def format_time(ms: float) -> str:
|
|
219
|
+
"""格式化時間"""
|
|
220
|
+
if ms >= 1000:
|
|
221
|
+
return f"{ms/1000:.1f}s"
|
|
222
|
+
return f"{ms:.0f}ms"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def print_perf_report(result: Dict, verbose: bool = False):
|
|
226
|
+
"""印出效能報告"""
|
|
227
|
+
|
|
228
|
+
if not result.get('success', False):
|
|
229
|
+
console.print(f"[red]測試失敗: {result.get('error', 'Unknown error')}[/red]")
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
scores = result.get('scores', {})
|
|
233
|
+
metrics = result.get('metrics', {})
|
|
234
|
+
|
|
235
|
+
# 分數表格
|
|
236
|
+
score_table = Table(title="Lighthouse 效能評分", show_header=True)
|
|
237
|
+
score_table.add_column("類別", style="cyan")
|
|
238
|
+
score_table.add_column("分數", justify="right")
|
|
239
|
+
score_table.add_column("狀態", justify="center")
|
|
240
|
+
|
|
241
|
+
categories = [
|
|
242
|
+
("Performance", scores.get('performance', 0)),
|
|
243
|
+
("Accessibility", scores.get('accessibility', 0)),
|
|
244
|
+
("Best Practices", scores.get('bestPractices', 0)),
|
|
245
|
+
("SEO", scores.get('seo', 0))
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
for name, score in categories:
|
|
249
|
+
color = get_score_color(score)
|
|
250
|
+
status = get_score_emoji(score)
|
|
251
|
+
score_table.add_row(name, f"[{color}]{score}[/{color}]", status)
|
|
252
|
+
|
|
253
|
+
console.print(score_table)
|
|
254
|
+
console.print()
|
|
255
|
+
|
|
256
|
+
# Core Web Vitals
|
|
257
|
+
if metrics:
|
|
258
|
+
vitals_table = Table(title="Core Web Vitals", show_header=True)
|
|
259
|
+
vitals_table.add_column("指標", style="cyan")
|
|
260
|
+
vitals_table.add_column("數值", justify="right")
|
|
261
|
+
vitals_table.add_column("說明")
|
|
262
|
+
|
|
263
|
+
vitals = [
|
|
264
|
+
("FCP", metrics.get('fcp', 0), "First Contentful Paint"),
|
|
265
|
+
("LCP", metrics.get('lcp', 0), "Largest Contentful Paint"),
|
|
266
|
+
("TBT", metrics.get('tbt', 0), "Total Blocking Time"),
|
|
267
|
+
("CLS", metrics.get('cls', 0), "Cumulative Layout Shift"),
|
|
268
|
+
("SI", metrics.get('si', 0), "Speed Index"),
|
|
269
|
+
("TTI", metrics.get('tti', 0), "Time to Interactive")
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
for abbr, value, desc in vitals:
|
|
273
|
+
if abbr == "CLS":
|
|
274
|
+
formatted = f"{value:.3f}"
|
|
275
|
+
else:
|
|
276
|
+
formatted = format_time(value)
|
|
277
|
+
vitals_table.add_row(abbr, formatted, desc)
|
|
278
|
+
|
|
279
|
+
console.print(vitals_table)
|
|
280
|
+
console.print()
|
|
281
|
+
|
|
282
|
+
# 改善建議
|
|
283
|
+
opportunities = result.get('opportunities', [])
|
|
284
|
+
if opportunities:
|
|
285
|
+
console.print("[bold yellow]改善建議 (可節省時間):[/bold yellow]")
|
|
286
|
+
for i, opp in enumerate(opportunities, 1):
|
|
287
|
+
savings = format_time(opp.get('savings', 0))
|
|
288
|
+
console.print(f" {i}. {opp.get('title')} [dim](-{savings})[/dim]")
|
|
289
|
+
console.print()
|
|
290
|
+
|
|
291
|
+
# 診斷資訊
|
|
292
|
+
diagnostics = result.get('diagnostics', [])
|
|
293
|
+
if diagnostics and verbose:
|
|
294
|
+
console.print("[bold cyan]診斷資訊:[/bold cyan]")
|
|
295
|
+
for diag in diagnostics:
|
|
296
|
+
display = diag.get('displayValue', '')
|
|
297
|
+
console.print(f" - {diag.get('title')}: {display}")
|
|
298
|
+
console.print()
|
|
299
|
+
|
|
300
|
+
# 總結
|
|
301
|
+
perf_score = scores.get('performance', 0)
|
|
302
|
+
if perf_score >= 90:
|
|
303
|
+
console.print("[green]效能優秀! 繼續保持[/green]")
|
|
304
|
+
elif perf_score >= 50:
|
|
305
|
+
console.print("[yellow]效能尚可,建議參考上述改善建議[/yellow]")
|
|
306
|
+
else:
|
|
307
|
+
console.print("[red]效能需要改善,請優先處理上述建議[/red]")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def check_lighthouse_installed() -> bool:
|
|
311
|
+
"""檢查 Lighthouse 是否可用"""
|
|
312
|
+
try:
|
|
313
|
+
result = subprocess.run(
|
|
314
|
+
['npx', 'lighthouse', '--version'],
|
|
315
|
+
capture_output=True,
|
|
316
|
+
timeout=30,
|
|
317
|
+
cwd=get_node_cwd()
|
|
318
|
+
)
|
|
319
|
+
return result.returncode == 0
|
|
320
|
+
except:
|
|
321
|
+
return False
|