import { Application } from '@hotwired/stimulus'; import invariant from 'tiny-invariant'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type Loader = () => Promise<{ [key: string]: any }>; const controllerAttribute = 'data-controller'; const controllers = import.meta.globEager('../controllers/*.{ts,tsx}'); const lazyControllers = import.meta.glob('../controllers/lazy/*.{ts,tsx}'); const controllerLoaders = new Map(); export function registerControllers(application: Application) { for (const [path, module] of Object.entries(controllers)) { registerController(controllerName(path), module, application); } for (const [path, loader] of Object.entries(lazyControllers)) { registerControllerLoader(controllerName(path), loader); } lazyLoadExistingControllers(application); lazyLoadNewControllers(application); } function lazyLoadExistingControllers( application: Application, element?: Element ) { queryControllerNamesWithin(element ?? application.element).forEach( (controllerName) => loadController(controllerName, application) ); } function lazyLoadNewControllers(application: Application) { new MutationObserver((mutationsList) => { for (const { attributeName, target, type } of mutationsList) { const element = target as Element; switch (type) { case 'attributes': { if ( attributeName == controllerAttribute && element.getAttribute(controllerAttribute) ) { extractControllerNamesFrom(element).forEach((controllerName) => loadController(controllerName, application) ); } break; } case 'childList': lazyLoadExistingControllers(application, element); break; } } }).observe(application.element, { attributes: true, attributeFilter: [controllerAttribute], subtree: true, childList: true }); } function queryControllerNamesWithin(element: Element) { return Array.from( element.querySelectorAll(`[${controllerAttribute}]`) ).flatMap(extractControllerNamesFrom); } function extractControllerNamesFrom(element: Element) { return ( element .getAttribute(controllerAttribute) ?.split(/\s+/) .filter((content) => content.length) ?? [] ); } function loadController(name: string, application: Application) { const loader = controllerLoaders.get(name); controllerLoaders.delete(name); if (loader) { loader() .then((module) => registerController(name, module, application)) .catch((error) => console.error(`Failed to autoload controller: ${name}`, error) ); } } function controllerName(path: string) { const [filename] = path.split('/').reverse(); return filename.replace(/_/g, '-').replace(/-controller\.(ts|tsx)$/, ''); } function registerController( name: string, module: Awaited>, application: Application ) { if (module.default) { console.debug(`Registered default export for "${name}" controller`); application.register(name, module.default); } else { const exports = Object.entries(module); invariant( exports.length == 1, `Expected a single export but ${exports.length} exports were found for "${name}" controller` ); const [exportName, exportModule] = exports[0]; console.debug( `Registered named export "${exportName}" for "${name}" controller` ); application.register(name, exportModule); } } function registerControllerLoader(name: string, loader: Loader) { console.debug(`Registered loader for "${name}" controller`); controllerLoaders.set(name, loader); }