初始化项目版本
This commit is contained in:
878
scripts/check-app-ready.mjs
Normal file
878
scripts/check-app-ready.mjs
Normal file
@@ -0,0 +1,878 @@
|
||||
#!/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 /prototypes/ref-app-home
|
||||
* node scripts/check-app-ready.mjs /prototypes/home
|
||||
*
|
||||
* 跳过构建校验:
|
||||
* node scripts/check-app-ready.mjs --skip-build /prototypes/ref-app-home
|
||||
*
|
||||
* 输出(JSON):
|
||||
* {
|
||||
* status: "READY" | "ERROR" | "TIMEOUT",
|
||||
* phase: "server|build|page|done",
|
||||
* message: "...",
|
||||
* url: "http://localhost:51720/prototypes/ref-app-home",
|
||||
* 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/prototypes/ref-app-home"
|
||||
* 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('themes/') ||
|
||||
keys[0] ||
|
||||
null
|
||||
)
|
||||
} catch (err) {
|
||||
logs.push(`Failed to resolve default entry key: ${err.message}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从页面路径推断入口 key
|
||||
* 例如:/prototypes/ref-app-home -> prototypes/ref-app-home
|
||||
*/
|
||||
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()
|
||||
Reference in New Issue
Block a user