/** * Component runtime for lifecycle-managed UI modules. * * Contract: * - component init signature: init(root, config) * - init result must expose destroy(): void */ import { warnOnce, resolveHost } from './app-dom.js'; import { readPageConfig } from './app-page-config.js'; const toObject = (value) => { if (!value || typeof value !== 'object' || Array.isArray(value)) { return {}; } return value; }; const readByPath = (value, path) => { const normalizedPath = String(path || '').trim(); if (normalizedPath === '') { return undefined; } const segments = normalizedPath.split('.').map((item) => item.trim()).filter(Boolean); let current = value; for (const segment of segments) { if (!current || typeof current !== 'object' || Array.isArray(current)) { return undefined; } current = current[segment]; } return current; }; export function createComponentRuntime(options = {}) { const { root = document, configId = 'app-components', } = options; const host = resolveHost(root); const pageConfig = toObject(readPageConfig(configId, document) || {}); const componentRegistry = new Map(); const register = (name, init, componentOptions = {}) => { const key = String(name || '').trim(); if (key === '') { warnOnce('UI_COMPONENT_INVALID', 'Cannot register component without name', { module: 'component-runtime' }); return false; } if (typeof init !== 'function') { warnOnce('UI_COMPONENT_INVALID', `Component "${key}" init is not a function`, { module: 'component-runtime', component: key }); return false; } componentRegistry.set(key, { name: key, init, options: toObject(componentOptions), api: null, }); return true; }; const mount = (name) => { const key = String(name || '').trim(); const definition = componentRegistry.get(key); if (!definition) { return null; } const destroyDefinition = () => { if (Array.isArray(definition.api)) { definition.api.forEach((instance) => { if (instance && typeof instance.destroy === 'function') { instance.destroy(); } }); definition.api = null; return; } if (definition.api && typeof definition.api.destroy === 'function') { definition.api.destroy(); } definition.api = null; }; destroyDefinition(); const configPath = String(definition.options.configPath || '').trim(); const fallbackConfig = toObject(definition.options.defaultConfig || {}); const resolvedConfig = toObject( configPath !== '' ? readByPath(pageConfig, configPath) : undefined ); const mergedConfig = { ...fallbackConfig, ...resolvedConfig }; const selector = String(definition.options.selector || '').trim(); const scope = String(definition.options.scope || '').trim().toLowerCase(); const targets = scope === 'global' || selector === '' ? [host] : Array.from(host.querySelectorAll(selector)); if (targets.length === 0) { return null; } const instances = []; targets.forEach((target) => { const api = definition.init(target, mergedConfig); if (!api || typeof api.destroy !== 'function') { warnOnce( 'UI_COMPONENT_CONTRACT', `Component "${key}" must return an API with destroy()`, { module: 'component-runtime', component: key } ); return; } instances.push(api); }); if (instances.length === 0) { return null; } definition.api = instances; return instances.length === 1 ? instances[0] : instances; }; const mountAll = () => { componentRegistry.forEach((_, key) => { mount(key); }); }; const destroy = (name = null) => { const destroyDefinition = (definition) => { if (Array.isArray(definition.api)) { definition.api.forEach((instance) => { if (instance && typeof instance.destroy === 'function') { instance.destroy(); } }); } else if (definition.api && typeof definition.api.destroy === 'function') { definition.api.destroy(); } definition.api = null; }; if (name !== null) { const key = String(name || '').trim(); const definition = componentRegistry.get(key); if (definition) { destroyDefinition(definition); } return; } componentRegistry.forEach((definition) => { destroyDefinition(definition); }); }; return { register, mount, mountAll, destroy, getPageConfig: () => pageConfig, }; }