@kitn.ai/ui
Version:
Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. Authored in SolidJS.
90 lines (81 loc) • 4.34 kB
text/typescript
// The kai-* autoloader (opt-in, additive — the Web Awesome model).
//
// Include this ONE module (via a CDN/static `<script type="module">` tag) and it
// watches the DOM for undefined kai-* elements and dynamically imports each
// element's module on demand — so a page that only uses <kai-chat> never downloads
// the others. The register-all bundle (@kitn.ai/ui/elements) stays the default;
// this is the delivery for NO-BUILD / CDN / static-served pages.
//
// IMPORTANT — this is a CDN / static-file pattern (it resolves sibling element
// modules relative to its own URL). It is NOT importable through a bundler: Vite /
// webpack relocate `import.meta.url` away from the element files and cannot analyze
// the dynamic import, so it 404s. In a BUNDLED app use per-element imports
// (`import '@kitn.ai/ui/elements/<el>'`) or the register-all bundle. (Advanced: host
// dist/elements/ yourself and call setAutoloaderBasePath('<url>/') before use.)
//
// Tag → module is resolved through the generated manifest (filenames don't always
// equal the tag, and some modules register more than one tag), and modules are
// fetched relative to THIS module's URL.
import manifest from './element-manifest.json';
const tagToModule: Record<string, string> = manifest.tags;
const inFlight = new Set<string>();
// Base = the directory of this module. Computed via string ops on import.meta.url
// (NOT `new URL('./x', import.meta.url)`, which Vite rewrites into a static-asset
// glob that resolves to undefined at runtime).
const BASE = import.meta.url.slice(0, import.meta.url.lastIndexOf('/') + 1);
/** Override where element modules are fetched from (e.g. a CDN). Call before use. */
let baseOverride: string | null = null;
export function setAutoloaderBasePath(path: string): void {
baseOverride = path.endsWith('/') ? path : path + '/';
}
async function register(tag: string): Promise<void> {
if (customElements.get(tag) || inFlight.has(tag)) return;
const file = tagToModule[tag];
if (!file) return; // not one of ours
inFlight.add(tag);
try {
await import(/* @vite-ignore */ `${baseOverride ?? BASE}${file}.js`);
} catch (err) {
inFlight.delete(tag);
warnOnce(tag, err);
}
}
// One actionable warning per session (the failure is almost always "imported
// through a bundler" — see the header note), instead of a silent per-tag error.
let warned = false;
function warnOnce(tag: string, err: unknown): void {
if (warned) return;
warned = true;
const msg = (err as { message?: string })?.message ?? String(err);
// eslint-disable-next-line no-console
console.warn(
`[kai-autoloader] could not load "${tag}" (${msg}). The autoloader is a CDN / ` +
`static-file tool — load it from a <script type="module" src=".../@kitn.ai/ui/dist/elements/autoloader.js">. ` +
`It is NOT importable through a bundler. In a bundled app, register elements with per-element imports ` +
`(import '@kitn.ai/ui/elements/<el>') or the register-all bundle (import '@kitn.ai/ui/elements'). To drive ` +
`the autoloader from a bundler, host dist/elements/ and call setAutoloaderBasePath('<url>/') before use.`,
);
}
function discover(root: ParentNode | Element): void {
const tags = new Set<string>();
const self = (root as Element).tagName?.toLowerCase?.();
if (self && self in tagToModule && !customElements.get(self)) tags.add(self);
root.querySelectorAll?.(':not(:defined)').forEach((el) => {
const t = el.tagName.toLowerCase();
if (t in tagToModule) tags.add(t);
});
tags.forEach(register);
}
/** Start watching `root` (default: the whole document) for undefined kai-* elements. */
export function startAutoloader(root: ParentNode = document): void {
discover(root as Element);
new MutationObserver((mutations) => {
for (const m of mutations)
for (const node of m.addedNodes)
if (node.nodeType === 1) discover(node as Element);
}).observe(document.documentElement, { childList: true, subtree: true });
}
// Self-start on import (the <script type="module"> use case). Deferred to a
// microtask so a consumer that calls setAutoloaderBasePath() right after importing
// (the advanced bundler-from-CDN path) takes effect before the first discovery pass.
if (typeof document !== 'undefined') queueMicrotask(() => startAutoloader());