113 lines
3.4 KiB
TypeScript

import * as fs from 'node:fs';
import * as path from 'node:path';
type JsonRecord = Record<string, unknown>;
export interface ILoadedMatrixWebappManifest {
readonly packageDir: string;
readonly packageName: string;
readonly manifestPath: string;
readonly distDir: string;
readonly entryFile: string;
readonly appName: string;
readonly displayName?: string;
readonly icon?: string;
readonly navOrder?: number;
readonly description?: string;
readonly shells?: readonly string[];
}
export function loadMatrixWebappManifest(packageDir: string): ILoadedMatrixWebappManifest | null {
const resolvedPackageDir = path.resolve(packageDir);
const manifestPath = path.join(resolvedPackageDir, 'matrix.json');
if (!fs.existsSync(manifestPath)) {
return null;
}
const manifest = readJsonObject(manifestPath, 'matrix.json');
const webapp = asRecord(manifest.webapp);
if (!webapp) {
return null;
}
const packageName = readRequiredString(
manifest.name,
`matrix.json in ${resolvedPackageDir} requires a non-empty "name"`,
);
const distDir = path.resolve(
resolvedPackageDir,
readOptionalString(webapp.distDir) ?? 'dist',
);
const entryFile = readOptionalString(webapp.entry) ?? 'index.html';
const appName = readOptionalString(webapp.appName) ?? packageName.replace(/^@[^/]+\//, '');
const displayName = readOptionalString(webapp.displayName);
const icon = readOptionalString(webapp.icon);
const navOrder = readOptionalNumber(webapp.navOrder);
const description = readOptionalString(webapp.description);
const shells = readStringArray(webapp.shells);
return {
packageDir: resolvedPackageDir,
packageName,
manifestPath,
distDir,
entryFile,
appName,
...(displayName ? { displayName } : {}),
...(icon ? { icon } : {}),
...(typeof navOrder === 'number' ? { navOrder } : {}),
...(description ? { description } : {}),
...(shells.length > 0 ? { shells } : {}),
};
}
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 (error) {
throw new Error(`Failed to parse ${label}: ${error instanceof Error ? error.message : String(error)}`);
}
}
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 readOptionalNumber(value: unknown): number | undefined {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}
function readStringArray(value: unknown): readonly string[] {
if (!Array.isArray(value)) {
return [];
}
const entries = value
.map((entry) => readOptionalString(entry))
.filter((entry): entry is string => typeof entry === 'string');
return [...new Set(entries)];
}
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;
}