/** * Pure, pi-independent helpers for the lazy loader. * * Kept side-effect-free and free of pi imports so they are unit-testable in * isolation (see `test/shared.test.mjs`). The extension entry in * `extensions/lazy-loader.ts` re-uses these. */ /** A discovered lazy extension before it is loaded. */ export interface ExtensionMeta { path: string; name: string; } export interface LazyLoaderConfig { enabled?: boolean; /** Delay (ms) before the background load begins. 0 = load as soon as the session is ready. */ startDelay?: number; /** * Per-extension load timeout (ms). If a factory has not resolved within this * window it is reported as timed out, but the underlying load is *not* * cancelled (a running factory cannot be safely aborted). If the load * eventually completes, the extension is recorded as loaded so that hot * reload does not register it twice. */ timeout?: number; /** Override the directory scanned for lazy extensions. Defaults to `/extensions/lazy`. */ lazyDir?: string; whitelist?: string[]; blacklist?: string[]; hotReload?: boolean; } export const DEFAULT_CONFIG: Required> = { enabled: true, startDelay: 0, timeout: 5000, whitelist: [], blacklist: [], hotReload: false, }; /** * 判断顶层文件是否为可加载的扩展文件。 * 接受 .ts / .js,排除 index.* 与类型声明 .d.ts / .d.js * (否则 foo.d.ts 会被 .endsWith(".ts") 误判为扩展)。 */ export function isExtensionFile(name: string): boolean { if (name === "index.ts" || name === "index.js") return false; if (name.endsWith(".d.ts") || name.endsWith(".d.js")) return false; return name.endsWith(".ts") || name.endsWith(".js"); } /** 配置健壮化:settings.json 手改错型时不致让后续逻辑抛错。 */ export function sanitizeStringArray(v: unknown, fallback: string[]): string[] { return Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : fallback; } export function sanitizeNonNegativeInt(v: unknown, fallback: number): number { return typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.floor(v) : fallback; } export function sanitizePositiveInt(v: unknown, fallback: number): number { return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : fallback; } export function sanitizeBoolean(v: unknown, fallback: boolean): boolean { return typeof v === "boolean" ? v : fallback; } export function sanitizeString(v: unknown, fallback: string): string { return typeof v === "string" && v.length > 0 ? v : fallback; } /** * Merge a user-provided `lazyLoader` block over defaults with full input * sanitization. Non-boolean `enabled`/`hotReload`, non-numeric timeouts, * and non-array filters all fall back to defaults instead of crashing later. */ export function mergeConfig( userConfig: Partial | undefined ): LazyLoaderConfig & Required> { const u = userConfig ?? {}; return { enabled: sanitizeBoolean(u.enabled, DEFAULT_CONFIG.enabled), startDelay: sanitizeNonNegativeInt(u.startDelay, DEFAULT_CONFIG.startDelay), timeout: sanitizePositiveInt(u.timeout, DEFAULT_CONFIG.timeout), whitelist: sanitizeStringArray(u.whitelist, DEFAULT_CONFIG.whitelist), blacklist: sanitizeStringArray(u.blacklist, DEFAULT_CONFIG.blacklist), hotReload: sanitizeBoolean(u.hotReload, DEFAULT_CONFIG.hotReload), lazyDir: u.lazyDir !== undefined ? sanitizeString(u.lazyDir, "") : undefined, }; }