140 lines
4.9 KiB
TypeScript
Raw Normal View History

import * as fs from 'node:fs';
import * as path from 'node:path';
type JsonRecord = Record<string, unknown>;
export interface IMatrixPackageEnvironment {
readonly name: string;
readonly nats?: {
readonly mode?: string;
readonly port?: number;
readonly wsPort?: number;
readonly url?: string;
readonly wsUrl?: string;
readonly credentialsRef?: string;
readonly dataDir?: string;
readonly pidFile?: string;
readonly binaryPath?: string;
};
readonly runtime: {
readonly root: string;
readonly runtimeId?: string;
readonly runtimeMount?: string;
readonly controlMount?: string;
};
readonly package?: {
readonly publicRoot?: string;
};
readonly http?: {
readonly enabled?: boolean;
readonly port?: number;
};
readonly host?: {
readonly matrixDir?: string;
};
readonly topology?: Record<string, unknown>;
readonly authorityLease?: Record<string, unknown> | null;
}
export interface ILoadedMatrixPackageEnvironment {
readonly packageDir: string;
readonly envName: string;
readonly environmentPath: string;
readonly environment: IMatrixPackageEnvironment;
}
export function loadPackageEnvironment(packageDir: string, envName: string): ILoadedMatrixPackageEnvironment {
const resolvedPackageDir = path.resolve(packageDir);
const cleanEnvName = envName.trim();
if (cleanEnvName.length === 0) {
throw new Error('Package environment name must be a non-empty string');
}
const environmentPath = path.join(resolvedPackageDir, '.matrix', `${cleanEnvName}.environment.json`);
if (!fs.existsSync(environmentPath)) {
throw new Error(`Package environment "${cleanEnvName}" not found at ${environmentPath}`);
}
return loadPackageEnvironmentFromPath(resolvedPackageDir, environmentPath, cleanEnvName);
}
export function loadPackageEnvironmentFromPath(
packageDir: string,
environmentPath: string,
envName?: string,
): ILoadedMatrixPackageEnvironment {
const resolvedPackageDir = path.resolve(packageDir);
const resolvedEnvironmentPath = path.resolve(environmentPath);
if (!fs.existsSync(resolvedEnvironmentPath)) {
throw new Error(`Package environment file not found at ${resolvedEnvironmentPath}`);
}
const label = path.basename(resolvedEnvironmentPath);
const parsed = readJsonObject(resolvedEnvironmentPath, label);
const name = readRequiredString(parsed.name, `Package environment "${envName ?? label}" requires name`);
const cleanEnvName = readOptionalString(envName) ?? name;
const runtime = asRecord(parsed.runtime);
const root = readRequiredString(runtime?.root, `Package environment "${cleanEnvName}" requires runtime.root`);
const environment: IMatrixPackageEnvironment = {
name,
runtime: {
root,
...(readOptionalString(runtime?.runtimeId) ? { runtimeId: readOptionalString(runtime?.runtimeId) } : {}),
...(readOptionalString(runtime?.runtimeMount) ? { runtimeMount: readOptionalString(runtime?.runtimeMount) } : {}),
...(readOptionalString(runtime?.controlMount) ? { controlMount: readOptionalString(runtime?.controlMount) } : {}),
},
...(asRecord(parsed.nats) ? { nats: asRecord(parsed.nats) as IMatrixPackageEnvironment['nats'] } : {}),
...(asRecord(parsed.package) ? { package: asRecord(parsed.package) as IMatrixPackageEnvironment['package'] } : {}),
...(asRecord(parsed.http) ? { http: asRecord(parsed.http) as IMatrixPackageEnvironment['http'] } : {}),
...(asRecord(parsed.host) ? { host: asRecord(parsed.host) as IMatrixPackageEnvironment['host'] } : {}),
...(asRecord(parsed.topology) ? { topology: asRecord(parsed.topology) } : {}),
...(parsed.authorityLease === null || asRecord(parsed.authorityLease)
? { authorityLease: parsed.authorityLease === null ? null : asRecord(parsed.authorityLease) }
: {}),
};
return {
packageDir: resolvedPackageDir,
envName: cleanEnvName,
environmentPath: resolvedEnvironmentPath,
environment,
};
}
function readJsonObject(filePath: string, label: string): JsonRecord {
try {
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
if (!isRecord(parsed)) {
throw new Error(`${label} must contain a JSON object`);
}
return parsed;
} catch (err) {
throw new Error(`Failed to parse ${label}: ${err instanceof Error ? err.message : String(err)}`);
}
}
function readRequiredString(value: unknown, message: string): string {
const resolved = readOptionalString(value);
if (!resolved) {
throw new Error(message);
}
return resolved;
}
function readOptionalString(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function isRecord(value: unknown): value is JsonRecord {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function asRecord(value: unknown): JsonRecord | undefined {
return isRecord(value) ? value : undefined;
}