import fs from 'node:fs'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { env } from '../config/env.js'; import { resolveMainProjectRoot } from './main-project-root-service.js'; export type WorkServerBuildInfo = { version: string; buildId: string; builtAt: string; }; export type WorkServerSourceChangeInfo = { changedAt: string; path: string; }; const MODULE_DIR_PATH = path.dirname(fileURLToPath(import.meta.url)); const WORK_SERVER_ROOT_PATH = path.resolve(MODULE_DIR_PATH, '..', '..'); const SOURCE_TARGET_PATH_NAMES = ['src', 'scripts', 'package.json', 'tsconfig.json'] as const; const WORK_SERVER_SOURCE_EXCLUDED_FILE_PATTERNS = [/\.test\.[^/]+$/i, /\.spec\.[^/]+$/i] as const; function normalizeRootPath(value: string | null | undefined) { const normalized = String(value ?? '').trim(); return normalized ? path.resolve(normalized) : null; } function resolveSourceTargetRoots() { const mainProjectRoot = normalizeRootPath(resolveMainProjectRoot()); if (mainProjectRoot) { const mirroredWorkServerRoot = path.join(mainProjectRoot, 'etc', 'servers', 'work-server'); if (fs.existsSync(mirroredWorkServerRoot)) { return [mirroredWorkServerRoot]; } } return [WORK_SERVER_ROOT_PATH]; } function resolveBuildInfoDirectoryPath(rootPath: string, configuredDistDir: string) { return path.resolve(rootPath, configuredDistDir); } export function resolveWorkServerBuildInfoFilePaths(options?: { workServerRootPath?: string; mainProjectRoot?: string | null; configuredDistDir?: string | null; }) { const workServerRootPath = path.resolve(options?.workServerRootPath ?? WORK_SERVER_ROOT_PATH); const configuredDistDir = String(options?.configuredDistDir ?? env.WORK_SERVER_DIST_DIR ?? 'dist').trim() || 'dist'; const mainProjectRoot = normalizeRootPath( options?.mainProjectRoot ?? resolveMainProjectRoot(), ); const candidates = [ path.join(resolveBuildInfoDirectoryPath(workServerRootPath, configuredDistDir), 'build-info.json'), path.join(workServerRootPath, 'dist', 'build-info.json'), ]; if (mainProjectRoot) { const mirroredWorkServerRoot = path.join(mainProjectRoot, 'etc', 'servers', 'work-server'); candidates.push(path.join(resolveBuildInfoDirectoryPath(mirroredWorkServerRoot, configuredDistDir), 'build-info.json')); candidates.push(path.join(mirroredWorkServerRoot, 'dist', 'build-info.json')); } return candidates.filter((candidate, index, array) => array.indexOf(candidate) === index); } function normalizeBuildInfo(value: unknown): WorkServerBuildInfo | null { if (!value || typeof value !== 'object') { return null; } const candidate = value as Partial>; if ( typeof candidate.version !== 'string' || typeof candidate.buildId !== 'string' || typeof candidate.builtAt !== 'string' ) { return null; } const builtAt = new Date(candidate.builtAt); if (Number.isNaN(builtAt.getTime())) { return null; } return { version: candidate.version, buildId: candidate.buildId, builtAt: builtAt.toISOString(), }; } function readBuildInfoFromDiskSync(filePath: string) { try { if (!fs.existsSync(filePath)) { return null; } return normalizeBuildInfo(JSON.parse(fs.readFileSync(filePath, 'utf8'))); } catch { return null; } } export async function readLatestWorkServerBuildInfo() { let latestBuildInfo: WorkServerBuildInfo | null = null; for (const candidatePath of resolveWorkServerBuildInfoFilePaths()) { try { const buildInfo = normalizeBuildInfo(JSON.parse(await readFile(candidatePath, 'utf8'))); if (!buildInfo) { continue; } if (!latestBuildInfo || buildInfo.builtAt > latestBuildInfo.builtAt) { latestBuildInfo = buildInfo; } } catch { continue; } } return latestBuildInfo; } const runtimeWorkServerBuildInfo = readBuildInfoFromDiskSync( path.join(resolveBuildInfoDirectoryPath(WORK_SERVER_ROOT_PATH, env.WORK_SERVER_DIST_DIR), 'build-info.json'), ); export function getRuntimeWorkServerBuildInfo() { return runtimeWorkServerBuildInfo; } function isExcludedWorkServerSourcePath(rootPath: string, targetPath: string) { const relativePath = path.relative(rootPath, targetPath).replace(/\\/g, '/'); const baseName = path.basename(relativePath); return WORK_SERVER_SOURCE_EXCLUDED_FILE_PATTERNS.some((pattern) => pattern.test(baseName)); } async function findLatestSourceChangeInPath(rootPath: string, targetPath: string): Promise { try { if (isExcludedWorkServerSourcePath(rootPath, targetPath)) { return null; } const stats = await fs.promises.stat(targetPath); if (stats.isFile()) { return { changedAt: stats.mtime.toISOString(), path: path.relative(rootPath, targetPath) || path.basename(targetPath), }; } if (!stats.isDirectory()) { return null; } const entries = await fs.promises.readdir(targetPath, { withFileTypes: true }); let latest: WorkServerSourceChangeInfo | null = null; for (const entry of entries) { if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.docker') { continue; } const childPath = path.join(targetPath, entry.name); const childLatest = await findLatestSourceChangeInPath(rootPath, childPath); if (!childLatest) { continue; } if (!latest || childLatest.changedAt > latest.changedAt) { latest = childLatest; } } return latest; } catch { return null; } } export async function readLatestWorkServerSourceChange() { let latest: WorkServerSourceChangeInfo | null = null; for (const rootPath of resolveSourceTargetRoots()) { for (const targetName of SOURCE_TARGET_PATH_NAMES) { const candidate = await findLatestSourceChangeInPath(rootPath, path.join(rootPath, targetName)); if (!candidate) { continue; } if (!latest || candidate.changedAt > latest.changedAt) { latest = candidate; } } } return latest; }