211 lines
6.1 KiB
TypeScript
211 lines
6.1 KiB
TypeScript
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<Record<keyof WorkServerBuildInfo, unknown>>;
|
|
|
|
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<WorkServerSourceChangeInfo | null> {
|
|
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;
|
|
}
|