113 lines
3.4 KiB
TypeScript
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;
|
||
|
|
}
|