Files
i/client/scripts/check-pwa.mjs
T
wsh5485 65b0bb04f8 feat: import CarLog v2.8 code + dev plan
把 CarLog v2.8 全套源码 + 配置导入到 i 仓库作为 baseline:
- server/src/ (13 个路由 + middleware + services + config)
- server/migrations/ (0001~0018 共 18 个迁移 + mysql)
- server/test/ (12 文件 101 测试)
- client/src/ (20 个 view + components + stores + api + composables)
- client/public/ + client/scripts/
- 全部配置文件 (.editorconfig, .eslintrc.json, .prettierrc.json, vitest.config.js, lighthouserc.json, .pa11yci.json, package.json, carlog-init.sql)
- .husky/pre-commit (git hooks)
- docs/install/ (宝塔部署文档)

不含:
- node_modules/ (本地 npm install)
- .env (敏感, 走 .env.example)
- *.zip / *.log / *.sqlite / .DS_Store

新增文档 docs/DEV-PLAN.md:
- Phase 1: 平台基座 (019 migration + 3 个 platform 路由 + 3 个 view)
- Phase 2: CarLog 子系统化 (后端 routes/ → subsystems/carlog/ + 前端 views/ → views/subsystems/carlog/ + 元数据驱动菜单)
- Phase 3: 验证 (测试 + E2E + DB 完整性)
- 交付清单 + commit 模板 + 给 Mavis review 的材料

后续 Trae 实施, 提交后我 code review + 跑测试。
2026-06-20 22:30:19 +08:00

167 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* PWA 安装性验证脚本
* 检查:
* 1. manifest.webmanifest 合法
* 2. Service Worker 注册成功
* 3. 图标全部能加载
* 4. PWA 必需字段(name, icons[192/512], start_url, display, theme_color, background_color
* 5. 离线 fallback/offline 或 navigateFallback 命中)
*/
import puppeteer from 'puppeteer';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const URL_TO_TEST = process.env.PWA_URL || 'http://localhost:4173/login';
const CHROME_PATH =
process.env.CHROME_PATH ||
'/Users/yabozi/.cache/puppeteer/chrome/mac-148.0.7778.97/chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing';
const checks = [];
let pass = 0;
let fail = 0;
function ok(name, detail) {
checks.push({ status: '✅', name, detail });
pass++;
}
function ko(name, detail) {
checks.push({ status: '❌', name, detail });
fail++;
}
async function fetchStatus(url) {
try {
const r = await fetch(url, { redirect: 'follow' });
return r.status;
} catch (e) {
return 0;
}
}
async function main() {
console.log(`🔍 PWA check: ${URL_TO_TEST}\n`);
// 1. manifest 文件可访问 + 合法 JSON
const manifestUrl = new URL('/manifest.webmanifest', URL_TO_TEST).toString();
const manifestStatus = await fetchStatus(manifestUrl);
if (manifestStatus === 200) {
ok('manifest 200', manifestUrl);
const r = await fetch(manifestUrl);
const m = await r.json();
const required = ['name', 'short_name', 'start_url', 'display', 'icons'];
const missing = required.filter((k) => !m[k]);
if (missing.length === 0) {
ok('manifest 必备字段', Object.keys(m).join(', '));
} else {
ko('manifest 必备字段缺失', missing.join(', '));
}
// 图标尺寸
const sizes = (m.icons || []).map((i) => i.sizes).filter(Boolean);
const has192 = sizes.some((s) => s.includes('192'));
const has512 = sizes.some((s) => s.includes('512'));
const hasMaskable = (m.icons || []).some((i) => i.purpose === 'maskable');
const hasApple = (m.icons || []).some((i) =>
(i.src || '').includes('apple-touch')
);
if (has192 && has512) ok('icons 192+512', sizes.join(', '));
else ko('icons 192/512 缺失', sizes.join(', '));
if (hasMaskable) ok('maskable icon', 'present');
else ko('maskable icon', 'missing');
if (hasApple) ok('apple-touch icon', 'present');
else ko('apple-touch icon', 'missing');
} else {
ko('manifest 不可访问', `status=${manifestStatus}, url=${manifestUrl}`);
}
// 2. Service Worker 注册
const browser = await puppeteer.launch({
executablePath: CHROME_PATH,
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
`--user-data-dir=/tmp/lh-cache-pwa-check-${Date.now()}`,
],
});
const page = await browser.newPage();
const swLogs = [];
page.on('console', (m) => {
const t = m.text();
if (t.includes('[PWA]') || t.includes('ServiceWorker')) swLogs.push(t);
});
await page.goto(URL_TO_TEST, { waitUntil: 'networkidle0', timeout: 30000 });
// 等几秒给 SW 机会注册
await new Promise((r) => setTimeout(r, 3000));
const swInfo = await page.evaluate(async () => {
if (!('serviceWorker' in navigator)) return { supported: false };
const regs = await navigator.serviceWorker.getRegistrations();
return {
supported: true,
count: regs.length,
scopes: regs.map((r) => r.scope),
active: regs.map((r) => !!r.active),
scripts: regs.map((r) => r.active && r.active.scriptURL),
};
});
if (swInfo.supported && swInfo.count > 0 && swInfo.active.every(Boolean)) {
ok('Service Worker 已注册', `scope=${swInfo.scopes.join(',')}`);
} else if (swInfo.supported && swInfo.count > 0) {
ko('Service Worker 未激活', JSON.stringify(swInfo));
} else {
ko('Service Worker 未注册', 'getRegistrations() 为空');
}
// 3. 关键 meta 标签
const meta = await page.evaluate(() => {
const get = (sel) => document.querySelector(sel)?.getAttribute('content') || null;
return {
themeColor: get('meta[name="theme-color"]'),
appleCapable: get('meta[name="apple-mobile-web-app-capable"]'),
appleTitle: get('meta[name="apple-mobile-web-app-title"]'),
viewport: get('meta[name="viewport"]'),
manifestLink: !!document.querySelector('link[rel="manifest"]'),
appleTouchIcon: !!document.querySelector('link[rel="apple-touch-icon"]'),
};
});
if (meta.themeColor) ok('theme-color', meta.themeColor);
else ko('theme-color', 'missing');
if (meta.appleCapable === 'yes') ok('apple-mobile-web-app-capable', 'yes');
else ko('apple-mobile-web-app-capable', meta.appleCapable || 'missing');
if (meta.manifestLink) ok('manifest link', 'present');
else ko('manifest link', 'missing');
if (meta.appleTouchIcon) ok('apple-touch-icon link', 'present');
else ko('apple-touch-icon link', 'missing');
// 4. SW 日志确认 offline ready
const offlineReady = swLogs.some((l) => l.includes('离线缓存就绪') || l.includes('offline'));
if (offlineReady) ok('PWA offline log', '检测到 offline ready');
// 离线就绪不是强校验,标 warn 即可
if (!offlineReady) {
checks.push({
status: '⚠️',
name: 'PWA offline 日志',
detail: '运行后无离线 ready 日志(可能 SW 还没预缓存完,可忽略)',
});
}
await browser.close();
// 输出
for (const c of checks) {
console.log(`${c.status} ${c.name}${c.detail ? `${c.detail}` : ''}`);
}
console.log(`\n总计: ✅ ${pass}${fail}`);
process.exit(fail > 0 ? 1 : 0);
}
main().catch((e) => {
console.error('PWA check 失败:', e);
process.exit(2);
});