Optimized the root .gitignore to exclude virtual environments, node modules, and temp folders to ensure clean and lightweight version tracking. Co-authored-by: Cursor <cursoragent@cursor.com>
880 lines
25 KiB
JavaScript
Executable File
880 lines
25 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
/**
|
||
* =====================================================
|
||
* CLI: check-app-ready
|
||
*
|
||
* 功能:
|
||
* - AI 调用,检测 Vite dev server 和页面状态
|
||
* - 不依赖页面注入
|
||
* - 捕获已存在和后续构建/热更新错误
|
||
* - 页面可访问即 READY,出现错误即 ERROR
|
||
* - 超时返回 TIMEOUT
|
||
* - 默认包含构建校验,可通过 --skip-build 跳过
|
||
*
|
||
* 使用:
|
||
* node scripts/check-app-ready.mjs [页面路径]
|
||
* 例如:node scripts/check-app-ready.mjs /components/button
|
||
* node scripts/check-app-ready.mjs /prototypes/home
|
||
*
|
||
* 跳过构建校验:
|
||
* node scripts/check-app-ready.mjs --skip-build /components/button
|
||
*
|
||
* 输出(JSON):
|
||
* {
|
||
* status: "READY" | "ERROR" | "TIMEOUT",
|
||
* phase: "server|build|page|done",
|
||
* message: "...",
|
||
* url: "http://localhost:51720/components/button",
|
||
* errors: [...],
|
||
* logs: [...],
|
||
* buildCheck?: { status: "SUCCESS" | "FAILED" | "SKIPPED", errors: [...], logs: [...] }
|
||
* lintCheck?: { status: "SUCCESS" | "FAILED" | "SKIPPED", errors: [...], logs: [...] }
|
||
* typeCheck?: { status: "SUCCESS" | "FAILED" | "SKIPPED", errors: [...], logs: [...] }
|
||
* checks?: [{ name: "lint|typecheck|build", status: "...", message: "...", errors: [...] }]
|
||
* homeUrl?: "http://localhost:51720"
|
||
* targetUrl?: "http://localhost:51720/components/button"
|
||
* targetPath?: "http://localhost:51720/prototypes/ref-app-home/index.html"
|
||
* }
|
||
* =====================================================
|
||
*/
|
||
|
||
import { spawn } from 'node:child_process'
|
||
import { setTimeout as sleep } from 'node:timers/promises'
|
||
import process from 'node:process'
|
||
import fs from 'node:fs'
|
||
import path from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
import { decodeOutput, getPreferredNpmCommand, getPreferredNpxCommand } from './utils/command-runtime.mjs'
|
||
|
||
const __filename = fileURLToPath(import.meta.url)
|
||
const __dirname = path.dirname(__filename)
|
||
const APP_ROOT = path.resolve(__dirname, '..')
|
||
|
||
/* ================= 配置 ================= */
|
||
// 解析命令行参数
|
||
const args = process.argv.slice(2);
|
||
const skipBuild = args.includes('--skip-build');
|
||
const pagePath = args.find(arg => !arg.startsWith('--')) || '/';
|
||
|
||
const CONFIG = {
|
||
devCommand: ['run', 'dev'], // 启动 Vite 的命令参数
|
||
devServerInfoPath: path.resolve(__dirname, '../.axhub/make/.dev-server-info.json'), // 开发服务器信息文件
|
||
pagePath, // 目标页面路径(从命令行参数获取)
|
||
pollIntervalMs: 500, // 页面轮询间隔
|
||
stableCheckMs: 1000, // 错误稳定判断时间
|
||
timeoutMs: 30_000, // 总超时
|
||
skipBuild // 是否跳过构建校验
|
||
}
|
||
|
||
/* ================= 工具函数 ================= */
|
||
function jsonExit(payload, code = 0) {
|
||
process.stdout.write(JSON.stringify(payload, null, 2))
|
||
process.exit(code)
|
||
}
|
||
|
||
/**
|
||
* 尝试通过 HTTP 请求获取页面内容,检查是否有错误信息
|
||
*/
|
||
async function checkPageForErrors(url) {
|
||
try {
|
||
const res = await fetch(url, { method: 'GET' })
|
||
const text = await res.text()
|
||
|
||
// Only treat the HTML as an error page when Vite's overlay is present.
|
||
// The app template includes global error handlers with object literals like
|
||
// `{ error: event.error }`, which would otherwise cause false positives.
|
||
const hasViteOverlay =
|
||
text.includes('vite-error-overlay') ||
|
||
text.includes('__vite_error_overlay__') ||
|
||
/\[plugin:vite:/i.test(text) ||
|
||
/Transform failed/i.test(text)
|
||
|
||
if (!hasViteOverlay) return []
|
||
|
||
const errorPatterns = [
|
||
/\bError:\s*([^\n]+)/,
|
||
/\bSyntaxError:\s*([^\n]+)/,
|
||
/\bReferenceError:\s*([^\n]+)/,
|
||
/\[plugin:vite:[^\]]+\]\s*([^\n]+)/i,
|
||
/Transform failed/i
|
||
]
|
||
|
||
for (const pattern of errorPatterns) {
|
||
const match = text.match(pattern)
|
||
if (match) return [match[1] || match[0]]
|
||
}
|
||
|
||
return ['Detected Vite error overlay but could not extract message']
|
||
} catch (err) {
|
||
return []
|
||
}
|
||
}
|
||
|
||
async function isServerAlive(url) {
|
||
try {
|
||
const res = await fetch(url, { method: 'GET' })
|
||
return res.ok
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 读取开发服务器信息
|
||
* 优先从 .axhub/make/.dev-server-info.json 读取实际运行的端口
|
||
*/
|
||
function getServerInfo() {
|
||
try {
|
||
if (fs.existsSync(CONFIG.devServerInfoPath)) {
|
||
const info = JSON.parse(fs.readFileSync(CONFIG.devServerInfoPath, 'utf8'))
|
||
return {
|
||
port: info.port,
|
||
host: info.host || 'localhost',
|
||
localIP: info.localIP || 'localhost'
|
||
}
|
||
}
|
||
} catch (err) {
|
||
logs.push(`Failed to read .axhub/make/.dev-server-info.json: ${err.message}`)
|
||
}
|
||
|
||
// 如果没有端口信息,返回 null 表示需要等待服务器启动
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* 生成服务器首页 URL
|
||
* 使用 localhost 而不是 0.0.0.0,因为浏览器无法访问 0.0.0.0
|
||
*/
|
||
function getHomeUrl(serverInfo) {
|
||
// 如果 host 是 0.0.0.0,使用 localhost 替代
|
||
const host = serverInfo.host === '0.0.0.0' ? 'localhost' : serverInfo.host
|
||
return `http://${host}:${serverInfo.port}`
|
||
}
|
||
|
||
/**
|
||
* 获取可访问的 host
|
||
* 将 0.0.0.0 转换为 localhost,因为浏览器无法直接访问 0.0.0.0
|
||
*/
|
||
function getAccessibleHost(serverInfo) {
|
||
return serverInfo.host === '0.0.0.0' ? 'localhost' : serverInfo.host
|
||
}
|
||
|
||
function getTargetUrl(serverInfo, targetPath) {
|
||
const host = getAccessibleHost(serverInfo)
|
||
return `http://${host}:${serverInfo.port}${targetPath}`
|
||
}
|
||
|
||
function getEntryHtmlPath(targetPath) {
|
||
const normalized = targetPath.startsWith('/') ? targetPath : `/${targetPath}`
|
||
if (normalized.endsWith('.html')) return normalized
|
||
if (normalized.endsWith('/')) return `${normalized}index.html`
|
||
return `${normalized}/index.html`
|
||
}
|
||
|
||
/* ================= 全局状态 ================= */
|
||
let logs = []
|
||
let errors = []
|
||
let lastErrorTime = 0
|
||
let errorCache = new Set() // 用于去重错误信息
|
||
|
||
/* ================= 阶段 1:启动或 attach Vite ================= */
|
||
function startOrAttachVite() {
|
||
logs.push('Checking Vite server...')
|
||
const npmCommand = getPreferredNpmCommand()
|
||
const child = spawn(npmCommand, CONFIG.devCommand, {
|
||
stdio: ['ignore', 'pipe', 'pipe'],
|
||
cwd: APP_ROOT,
|
||
shell: false,
|
||
})
|
||
|
||
child.stdout.on('data', (data) => {
|
||
const text = decodeOutput(data).trim()
|
||
if (text) logs.push(text)
|
||
|
||
// 检测构建错误
|
||
if (/error/i.test(text) || /failed to compile/i.test(text)) {
|
||
errors.push(text)
|
||
lastErrorTime = Date.now()
|
||
}
|
||
})
|
||
|
||
child.stderr.on('data', (data) => {
|
||
const text = decodeOutput(data).trim()
|
||
if (text) {
|
||
// 过滤掉一些正常的警告信息
|
||
if (!/deprecated|experimental/i.test(text)) {
|
||
errors.push(text)
|
||
lastErrorTime = Date.now()
|
||
}
|
||
logs.push(text)
|
||
}
|
||
})
|
||
|
||
child.on('error', (err) => {
|
||
errors.push(`Process error: ${err.message}`)
|
||
lastErrorTime = Date.now()
|
||
})
|
||
|
||
return child
|
||
}
|
||
|
||
/* ================= 阶段 2:轮询页面可访问性 ================= */
|
||
async function waitForPage(url) {
|
||
const start = Date.now()
|
||
let lastCheckTime = 0
|
||
|
||
while (Date.now() - start < CONFIG.timeoutMs) {
|
||
const now = Date.now()
|
||
|
||
// 每隔一段时间尝试获取错误信息(即使页面不可访问)
|
||
if (now - lastCheckTime > 2000) {
|
||
const pageErrors = await checkPageForErrors(url)
|
||
if (pageErrors.length > 0) {
|
||
// 去重:只添加未见过的错误
|
||
pageErrors.forEach(err => {
|
||
const errorKey = err.substring(0, 200) // 使用前200个字符作为唯一标识
|
||
if (!errorCache.has(errorKey)) {
|
||
errorCache.add(errorKey)
|
||
errors.push(err)
|
||
}
|
||
})
|
||
}
|
||
lastCheckTime = now
|
||
}
|
||
|
||
if (await isServerAlive(url)) return true
|
||
await sleep(CONFIG.pollIntervalMs)
|
||
}
|
||
return false
|
||
}
|
||
|
||
/* ================= 阶段 3:等待稳定状态 ================= */
|
||
async function waitForStable(pageUrl) {
|
||
const startTime = Date.now()
|
||
|
||
while (Date.now() - startTime < CONFIG.timeoutMs) {
|
||
const now = Date.now()
|
||
|
||
// 页面可访问
|
||
const pageOk = await isServerAlive(pageUrl)
|
||
|
||
// 如果页面可访问,尝试检查页面内容中的错误
|
||
if (pageOk) {
|
||
const pageErrors = await checkPageForErrors(pageUrl)
|
||
if (pageErrors.length > 0) {
|
||
return {
|
||
status: 'ERROR',
|
||
phase: 'build',
|
||
message: 'Detected error in page content',
|
||
url: pageUrl,
|
||
errors: pageErrors,
|
||
logs
|
||
}
|
||
}
|
||
}
|
||
|
||
// 错误稳定:最近 stableCheckMs 内没有新的错误
|
||
const stable = (now - lastErrorTime) > CONFIG.stableCheckMs
|
||
|
||
if (!pageOk) {
|
||
// 页面不可访问,继续轮询
|
||
await sleep(CONFIG.pollIntervalMs)
|
||
continue
|
||
}
|
||
|
||
if (errors.length > 0) {
|
||
return {
|
||
status: 'ERROR',
|
||
phase: 'build',
|
||
message: 'Detected Vite build/runtime error',
|
||
url: pageUrl,
|
||
errors,
|
||
logs
|
||
}
|
||
}
|
||
|
||
if (pageOk && stable) {
|
||
return {
|
||
status: 'READY',
|
||
phase: 'done',
|
||
message: 'Page ready and stable',
|
||
url: pageUrl,
|
||
errors: [],
|
||
logs
|
||
}
|
||
}
|
||
|
||
await sleep(CONFIG.pollIntervalMs)
|
||
}
|
||
|
||
return {
|
||
status: 'TIMEOUT',
|
||
phase: 'server',
|
||
message: 'Timeout waiting for page/stable state',
|
||
url: pageUrl,
|
||
errors,
|
||
logs
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 为结果添加服务器首页信息
|
||
*/
|
||
function addUrls(result, serverInfo) {
|
||
if (!serverInfo) {
|
||
return {
|
||
...result,
|
||
homeUrl: null,
|
||
targetUrl: null,
|
||
targetPath: null
|
||
}
|
||
}
|
||
|
||
const entryHtmlPath = getEntryHtmlPath(CONFIG.pagePath)
|
||
|
||
return {
|
||
...result,
|
||
homeUrl: getHomeUrl(serverInfo),
|
||
targetUrl: getTargetUrl(serverInfo, CONFIG.pagePath),
|
||
targetPath: getTargetUrl(serverInfo, entryHtmlPath)
|
||
}
|
||
}
|
||
|
||
function readPackageJson() {
|
||
const pkgPath = path.resolve(APP_ROOT, 'package.json')
|
||
try {
|
||
return JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
||
} catch (err) {
|
||
logs.push(`Failed to read package.json: ${err.message}`)
|
||
return null
|
||
}
|
||
}
|
||
|
||
function getScriptCommand(pkgJson, scriptName) {
|
||
if (!pkgJson || !pkgJson.scripts) return null
|
||
return pkgJson.scripts[scriptName] || null
|
||
}
|
||
|
||
function hasEslintConfig(pkgJson) {
|
||
if (pkgJson && pkgJson.eslintConfig) return true
|
||
const configFiles = [
|
||
'.eslintrc',
|
||
'.eslintrc.js',
|
||
'.eslintrc.cjs',
|
||
'.eslintrc.json',
|
||
'.eslintrc.yaml',
|
||
'.eslintrc.yml',
|
||
'eslint.config.js',
|
||
'eslint.config.cjs',
|
||
'eslint.config.mjs'
|
||
]
|
||
return configFiles.some((file) => fs.existsSync(path.resolve(APP_ROOT, file)))
|
||
}
|
||
|
||
function hasTsConfig() {
|
||
return fs.existsSync(path.resolve(APP_ROOT, 'tsconfig.json'))
|
||
}
|
||
|
||
function toCheckItem(name, result) {
|
||
if (!result) return null
|
||
return {
|
||
name,
|
||
status: result.status,
|
||
message: result.message,
|
||
errors: result.errors || []
|
||
}
|
||
}
|
||
|
||
function buildChecksSummary({ lintResult, typeCheckResult, buildResult }) {
|
||
return [
|
||
toCheckItem('lint', lintResult),
|
||
toCheckItem('typecheck', typeCheckResult),
|
||
toCheckItem('build', buildResult)
|
||
].filter(Boolean)
|
||
}
|
||
|
||
async function runCommandCheck({ label, command, args = [], env = {}, logTag }) {
|
||
logs.push(`${label} check started`)
|
||
|
||
return new Promise((resolve) => {
|
||
const checkErrors = []
|
||
const checkLogs = []
|
||
const resolvedCommand = command === 'npm'
|
||
? getPreferredNpmCommand()
|
||
: command === 'npx'
|
||
? getPreferredNpxCommand()
|
||
: command
|
||
|
||
const proc = spawn(resolvedCommand, args, {
|
||
cwd: APP_ROOT,
|
||
env: { ...process.env, ...env },
|
||
stdio: ['ignore', 'pipe', 'pipe']
|
||
})
|
||
|
||
const appendLog = (line, isError = false) => {
|
||
if (!line) return
|
||
checkLogs.push(line)
|
||
logs.push(`[${logTag}] ${line}`)
|
||
if (isError && !/deprecated|experimental/i.test(line)) {
|
||
checkErrors.push(line)
|
||
}
|
||
}
|
||
|
||
proc.stdout.on('data', (data) => {
|
||
appendLog(decodeOutput(data).trim(), false)
|
||
})
|
||
|
||
proc.stderr.on('data', (data) => {
|
||
appendLog(decodeOutput(data).trim(), true)
|
||
})
|
||
|
||
proc.on('close', (code) => {
|
||
if (code === 0 && checkErrors.length === 0) {
|
||
resolve({
|
||
status: 'SUCCESS',
|
||
message: `${label} completed successfully`,
|
||
errors: [],
|
||
logs: checkLogs
|
||
})
|
||
return
|
||
}
|
||
|
||
resolve({
|
||
status: 'FAILED',
|
||
message: `${label} failed (exit code: ${code})`,
|
||
errors: checkErrors.length > 0 ? checkErrors : [`${label} exited with code ${code}`],
|
||
logs: checkLogs
|
||
})
|
||
})
|
||
|
||
proc.on('error', (err) => {
|
||
logs.push(`${label} process error: ${err.message}`)
|
||
resolve({
|
||
status: 'FAILED',
|
||
message: `${label} process error: ${err.message}`,
|
||
errors: [err.message],
|
||
logs: checkLogs
|
||
})
|
||
})
|
||
})
|
||
}
|
||
|
||
async function runLintCheck() {
|
||
const pkgJson = readPackageJson()
|
||
const lintScript = getScriptCommand(pkgJson, 'lint')
|
||
|
||
if (lintScript) {
|
||
return runCommandCheck({
|
||
label: 'Lint',
|
||
command: 'npm',
|
||
args: ['run', 'lint'],
|
||
logTag: 'LINT'
|
||
})
|
||
}
|
||
|
||
if (!hasEslintConfig(pkgJson)) {
|
||
return {
|
||
status: 'SKIPPED',
|
||
message: 'Lint skipped: no eslint config or lint script found',
|
||
errors: [],
|
||
logs: []
|
||
}
|
||
}
|
||
|
||
return runCommandCheck({
|
||
label: 'Lint',
|
||
command: 'npx',
|
||
args: ['eslint', '.'],
|
||
logTag: 'LINT'
|
||
})
|
||
}
|
||
|
||
async function runTypeCheck() {
|
||
const pkgJson = readPackageJson()
|
||
const typecheckScript = getScriptCommand(pkgJson, 'typecheck')
|
||
|
||
if (typecheckScript) {
|
||
return runCommandCheck({
|
||
label: 'Typecheck',
|
||
command: 'npm',
|
||
args: ['run', 'typecheck'],
|
||
logTag: 'TYPECHECK'
|
||
})
|
||
}
|
||
|
||
if (!hasTsConfig()) {
|
||
return {
|
||
status: 'SKIPPED',
|
||
message: 'Typecheck skipped: no tsconfig.json or typecheck script found',
|
||
errors: [],
|
||
logs: []
|
||
}
|
||
}
|
||
|
||
return runCommandCheck({
|
||
label: 'Typecheck',
|
||
command: 'npx',
|
||
args: ['tsc', '--noEmit'],
|
||
logTag: 'TYPECHECK'
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 扫描并更新 .axhub/make/entries.json
|
||
* 确保新创建的目录被包含在入口列表中
|
||
*/
|
||
async function scanEntries() {
|
||
logs.push('Scanning entries...')
|
||
|
||
return new Promise((resolve) => {
|
||
const scanProcess = spawn(process.execPath, ['scripts/scan-entries.js'], {
|
||
cwd: APP_ROOT,
|
||
stdio: ['ignore', 'pipe', 'pipe']
|
||
})
|
||
|
||
scanProcess.stdout.on('data', (data) => {
|
||
const text = decodeOutput(data).trim()
|
||
if (text) logs.push(`[SCAN] ${text}`)
|
||
})
|
||
|
||
scanProcess.stderr.on('data', (data) => {
|
||
const text = decodeOutput(data).trim()
|
||
if (text) logs.push(`[SCAN ERROR] ${text}`)
|
||
})
|
||
|
||
scanProcess.on('close', (code) => {
|
||
if (code === 0) {
|
||
logs.push('Entry scanning completed')
|
||
resolve({ success: true })
|
||
} else {
|
||
logs.push(`Entry scanning failed with exit code ${code}`)
|
||
resolve({ success: false })
|
||
}
|
||
})
|
||
|
||
scanProcess.on('error', (err) => {
|
||
logs.push(`Entry scanning error: ${err.message}`)
|
||
resolve({ success: false })
|
||
})
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 执行独立构建校验
|
||
* 针对指定的入口 key 执行单独构建,不是全量构建
|
||
*/
|
||
async function runBuildCheck(entryKey) {
|
||
const originalEntryKey = String(entryKey ?? '').trim()
|
||
logs.push(`Starting build check for entry: ${originalEntryKey || '(auto)'}`)
|
||
|
||
// 先扫描入口,确保 .axhub/make/entries.json 是最新的
|
||
const scanResult = await scanEntries()
|
||
if (!scanResult.success) {
|
||
return {
|
||
status: 'FAILED',
|
||
message: 'Failed to scan entries before build',
|
||
errors: ['Entry scanning failed'],
|
||
logs: []
|
||
}
|
||
}
|
||
|
||
const resolvedEntryKey = originalEntryKey || resolveDefaultEntryKey()
|
||
if (!resolvedEntryKey) {
|
||
logs.push('Build check skipped: no entry key resolved')
|
||
return {
|
||
status: 'SKIPPED',
|
||
message: 'Build check skipped: no entry key resolved',
|
||
errors: [],
|
||
logs: []
|
||
}
|
||
}
|
||
|
||
return new Promise((resolve) => {
|
||
const buildErrors = []
|
||
const buildLogs = []
|
||
const npxCommand = getPreferredNpxCommand()
|
||
|
||
// 使用 ENTRY_KEY 环境变量触发单独构建
|
||
const buildProcess = spawn(npxCommand, ['vite', 'build'], {
|
||
cwd: APP_ROOT,
|
||
env: { ...process.env, ENTRY_KEY: resolvedEntryKey },
|
||
stdio: ['ignore', 'pipe', 'pipe']
|
||
})
|
||
|
||
buildProcess.stdout.on('data', (data) => {
|
||
const text = decodeOutput(data).trim()
|
||
if (text) {
|
||
buildLogs.push(text)
|
||
logs.push(`[BUILD] ${text}`)
|
||
}
|
||
})
|
||
|
||
buildProcess.stderr.on('data', (data) => {
|
||
const text = decodeOutput(data).trim()
|
||
if (text) {
|
||
buildLogs.push(text)
|
||
logs.push(`[BUILD ERROR] ${text}`)
|
||
// 捕获构建错误
|
||
if (/error|failed/i.test(text) && !/deprecated|experimental/i.test(text)) {
|
||
buildErrors.push(text)
|
||
}
|
||
}
|
||
})
|
||
|
||
buildProcess.on('close', (code) => {
|
||
if (code === 0 && buildErrors.length === 0) {
|
||
logs.push(`Build check completed successfully for ${resolvedEntryKey}`)
|
||
resolve({
|
||
status: 'SUCCESS',
|
||
message: `Build completed successfully for ${resolvedEntryKey}`,
|
||
errors: [],
|
||
logs: buildLogs
|
||
})
|
||
} else {
|
||
logs.push(`Build check failed for ${resolvedEntryKey} with exit code ${code}`)
|
||
resolve({
|
||
status: 'FAILED',
|
||
message: `Build failed for ${resolvedEntryKey} (exit code: ${code})`,
|
||
errors: buildErrors.length > 0 ? buildErrors : [`Build process exited with code ${code}`],
|
||
logs: buildLogs
|
||
})
|
||
}
|
||
})
|
||
|
||
buildProcess.on('error', (err) => {
|
||
logs.push(`Build process error: ${err.message}`)
|
||
resolve({
|
||
status: 'FAILED',
|
||
message: `Build process error: ${err.message}`,
|
||
errors: [err.message],
|
||
logs: buildLogs
|
||
})
|
||
})
|
||
})
|
||
}
|
||
|
||
function resolveDefaultEntryKey() {
|
||
try {
|
||
const entriesPath = path.resolve(APP_ROOT, '.axhub/make/entries.json')
|
||
if (!fs.existsSync(entriesPath)) return null
|
||
const raw = JSON.parse(fs.readFileSync(entriesPath, 'utf8'))
|
||
const jsEntries = raw && typeof raw === 'object' ? (raw.js || {}) : {}
|
||
const keys = Object.keys(jsEntries || {}).filter(Boolean).sort((a, b) => a.localeCompare(b))
|
||
if (keys.length === 0) return null
|
||
|
||
const pickFromPrefix = (prefix) => keys.find((k) => k.startsWith(prefix))
|
||
return (
|
||
pickFromPrefix('prototypes/') ||
|
||
pickFromPrefix('components/') ||
|
||
pickFromPrefix('themes/') ||
|
||
keys[0] ||
|
||
null
|
||
)
|
||
} catch (err) {
|
||
logs.push(`Failed to resolve default entry key: ${err.message}`)
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从页面路径推断入口 key
|
||
* 例如:/components/button -> components/button
|
||
*/
|
||
function getEntryKeyFromPath(pagePath) {
|
||
// 移除开头的斜杠
|
||
return pagePath.replace(/^\//, '')
|
||
}
|
||
|
||
/* ================= 主流程 ================= */
|
||
async function main() {
|
||
try {
|
||
// 获取服务器信息
|
||
const serverInfo = getServerInfo()
|
||
|
||
// 如果没有端口信息,等待服务器启动
|
||
if (!serverInfo) {
|
||
logs.push('Waiting for server to start...')
|
||
// 启动服务器并等待
|
||
const viteProcess = startOrAttachVite()
|
||
|
||
// 等待 .axhub/make/.dev-server-info.json 文件生成
|
||
const maxWait = 10000 // 10秒
|
||
const startTime = Date.now()
|
||
let newServerInfo = null
|
||
|
||
while (Date.now() - startTime < maxWait) {
|
||
await new Promise(resolve => setTimeout(resolve, 500))
|
||
newServerInfo = getServerInfo()
|
||
if (newServerInfo) break
|
||
}
|
||
|
||
if (!newServerInfo) {
|
||
return jsonExit(addUrls({
|
||
status: 'ERROR',
|
||
phase: 'server',
|
||
message: 'Server failed to start - no port information available',
|
||
url: CONFIG.pagePath,
|
||
errors: ['Server did not write port information within timeout'],
|
||
logs
|
||
}, null), 1)
|
||
}
|
||
|
||
// 使用新获取的服务器信息
|
||
const accessibleHost = getAccessibleHost(newServerInfo)
|
||
const pageUrl = `http://${accessibleHost}:${newServerInfo.port}${CONFIG.pagePath}`
|
||
|
||
logs.push(`Target URL: ${pageUrl}`)
|
||
logs.push(`Server info: port=${newServerInfo.port}, host=${newServerInfo.host}`)
|
||
|
||
// 继续后续流程...
|
||
await continueWithServerInfo(newServerInfo, pageUrl, viteProcess)
|
||
} else {
|
||
const accessibleHost = getAccessibleHost(serverInfo)
|
||
const pageUrl = `http://${accessibleHost}:${serverInfo.port}${CONFIG.pagePath}`
|
||
|
||
logs.push(`Target URL: ${pageUrl}`)
|
||
logs.push(`Server info: port=${serverInfo.port}, host=${serverInfo.host}`)
|
||
|
||
// 继续后续流程...
|
||
await continueWithServerInfo(serverInfo, pageUrl, null)
|
||
}
|
||
} catch (err) {
|
||
const serverInfo = getServerInfo()
|
||
jsonExit(addUrls({
|
||
status: 'ERROR',
|
||
phase: 'server',
|
||
message: err.message,
|
||
url: CONFIG.pagePath,
|
||
errors: [String(err)],
|
||
logs
|
||
}, serverInfo), 1)
|
||
}
|
||
}
|
||
|
||
async function continueWithServerInfo(serverInfo, pageUrl, viteProcess) {
|
||
try {
|
||
// 步骤 1: 执行 lint 检查
|
||
const lintResult = await runLintCheck()
|
||
if (lintResult.status === 'FAILED') {
|
||
return jsonExit(addUrls({
|
||
status: 'ERROR',
|
||
phase: 'lint',
|
||
message: lintResult.message,
|
||
url: pageUrl,
|
||
errors: lintResult.errors,
|
||
logs,
|
||
lintCheck: lintResult,
|
||
checks: buildChecksSummary({ lintResult })
|
||
}, serverInfo), 1)
|
||
}
|
||
|
||
// 步骤 2: 执行 typecheck 检查
|
||
const typeCheckResult = await runTypeCheck()
|
||
if (typeCheckResult.status === 'FAILED') {
|
||
return jsonExit(addUrls({
|
||
status: 'ERROR',
|
||
phase: 'typecheck',
|
||
message: typeCheckResult.message,
|
||
url: pageUrl,
|
||
errors: typeCheckResult.errors,
|
||
logs,
|
||
lintCheck: lintResult,
|
||
typeCheck: typeCheckResult,
|
||
checks: buildChecksSummary({ lintResult, typeCheckResult })
|
||
}, serverInfo), 1)
|
||
}
|
||
|
||
// 步骤 3: 执行构建校验(除非指定 --skip-build)
|
||
let buildResult = null
|
||
if (!CONFIG.skipBuild) {
|
||
const entryKey = getEntryKeyFromPath(CONFIG.pagePath)
|
||
logs.push(`Build check enabled for entry: ${entryKey}`)
|
||
buildResult = await runBuildCheck(entryKey)
|
||
|
||
// 如果构建失败,直接返回错误
|
||
if (buildResult.status === 'FAILED') {
|
||
return jsonExit(addUrls({
|
||
status: 'ERROR',
|
||
phase: 'build',
|
||
message: buildResult.message,
|
||
url: pageUrl,
|
||
errors: buildResult.errors,
|
||
logs,
|
||
buildCheck: buildResult,
|
||
lintCheck: lintResult,
|
||
typeCheck: typeCheckResult,
|
||
checks: buildChecksSummary({ lintResult, typeCheckResult, buildResult })
|
||
}, serverInfo), 1)
|
||
}
|
||
} else {
|
||
logs.push('Build check skipped (--skip-build flag)')
|
||
buildResult = {
|
||
status: 'SKIPPED',
|
||
message: 'Build check skipped (--skip-build flag)',
|
||
errors: [],
|
||
logs: []
|
||
}
|
||
}
|
||
|
||
// 步骤 4: 开发服务器校验
|
||
const accessibleHost = getAccessibleHost(serverInfo)
|
||
|
||
// 检查服务器是否已经在运行
|
||
const serverAlreadyRunning = await isServerAlive(`http://${accessibleHost}:${serverInfo.port}`)
|
||
|
||
let viteChild = viteProcess
|
||
if (!serverAlreadyRunning && !viteChild) {
|
||
logs.push('Server not running, starting Vite...')
|
||
viteChild = startOrAttachVite()
|
||
} else {
|
||
logs.push('Server already running, skipping start')
|
||
}
|
||
|
||
// 等待页面可访问
|
||
const pageReachable = await waitForPage(pageUrl)
|
||
if (!pageReachable) {
|
||
if (viteChild) viteChild.kill()
|
||
return jsonExit(addUrls({
|
||
status: 'TIMEOUT',
|
||
phase: 'page',
|
||
message: 'Page never became reachable',
|
||
url: pageUrl,
|
||
errors,
|
||
logs,
|
||
buildCheck: buildResult,
|
||
lintCheck: lintResult,
|
||
typeCheck: typeCheckResult,
|
||
checks: buildChecksSummary({ lintResult, typeCheckResult, buildResult })
|
||
}, serverInfo), 1)
|
||
}
|
||
|
||
// 等待稳定状态
|
||
const result = await waitForStable(pageUrl)
|
||
|
||
// 清理进程
|
||
if (viteChild) viteChild.kill()
|
||
|
||
// 添加构建结果到最终输出
|
||
const finalResult = {
|
||
...result,
|
||
buildCheck: buildResult,
|
||
lintCheck: lintResult,
|
||
typeCheck: typeCheckResult,
|
||
checks: buildChecksSummary({ lintResult, typeCheckResult, buildResult })
|
||
}
|
||
|
||
jsonExit(addUrls(finalResult, serverInfo), result.status === 'READY' ? 0 : 1)
|
||
} catch (err) {
|
||
jsonExit(addUrls({
|
||
status: 'ERROR',
|
||
phase: 'server',
|
||
message: err.message,
|
||
url: CONFIG.pagePath,
|
||
errors: [String(err)],
|
||
logs
|
||
}, serverInfo), 1)
|
||
}
|
||
}
|
||
|
||
main()
|