UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

434 lines • 20.7 kB
/** * `genuiSandboxKit()` — the Vite plugin that puts the app's design system in the sandbox, whatever * its stack, with no command to run by hand: * * - `styles`: the app's style entries, compiled with Vite's own CSS pipeline (Sass, Less, PostCSS, * CSS modules, Tailwind…), fonts and images inlined — the frame stays network-less. Tailwind is * detected: the frame then runs Tailwind's browser runtime with the app's theme, variants and * utilities; any other CSS is described to the model by its class vocabulary; * - `components`: modules exporting React components, bundled with React as one IIFE and documented * from their TypeScript types (names and propTypes in a JavaScript project); * - `wrapper`: a module whose default export wraps the model's App (theme providers, baselines); * - dev: builds and serves all of it under `/@genui-sandbox-kit/`, writes the descriptor (docs + urls) * for the server, rebuilds when a kit or style file changes and tells open pages (HMR); * - build: emits it all as hashed assets plus `genui-sandbox-kit.json`, listed in the Vite manifest. * * Zero-config: without `styles`, the style entries are found from the app's Vite entrypoints * (`build.rollupOptions.input` — where the AdonisJS plugin puts its `entrypoints` — or `index.html`); * without `components`, the first `components/ui` folder. */ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join, resolve } from 'node:path'; import { themeVarsFromCss } from '../sandbox-theme.js'; import { buildSandboxKitBundle, kitDocsFromBundle } from './bundle.js'; import { generateSandboxKitDocs } from './docs.js'; import { assertNoLegacyKitOptions, resolveSandboxKitFiles, } from './files.js'; import { buildSandboxKitStyles, discoverStyleEntries, purgeSandboxKitStyles, readStylesGuide, resolveStyleEntries, STYLE_EXTENSIONS, stylesDocs, tailwindVitePluginFor, } from './styles.js'; /** Where the dev server serves the assets. */ export const SANDBOX_KIT_DEV_PREFIX = '@genui-sandbox-kit/'; /** The Vite manifest keys the build lists them under. */ export const SANDBOX_KIT_MANIFEST_KEYS = { kit: 'genui-sandbox-kit.js', css: 'genui-sandbox-kit.css', tailwind: 'genui-sandbox-tailwind.js', descriptor: 'genui-sandbox-kit.json', }; /** The event a page gets (`window`) when the kit was rebuilt in dev. */ export const SANDBOX_KIT_UPDATE_EVENT = 'genui-sandbox-kit:update'; /** The manifest keys a named kit's assets are listed under (`genui-sandbox-kit.<name>.js`…). */ export function sandboxKitManifestKeys(name) { if (name === undefined) return { ...SANDBOX_KIT_MANIFEST_KEYS }; const keys = {}; for (const [key, file] of Object.entries(SANDBOX_KIT_MANIFEST_KEYS)) keys[key] = file.replace(/(\.[a-z]+)$/, `.${name}$1`); return keys; } /** A named kit's descriptor path, from the unnamed one's (`genui_sandbox_kit.json` → `genui_sandbox_kit.<name>.json`). */ export function sandboxKitDescriptorPath(path, name) { return name === undefined ? path : path.replace(/(\.json)?$/, `.${name}$1`); } /** Throws unless a kit name is a plain identifier (it goes into urls and file names). */ export function assertSandboxKitName(name) { if (name !== undefined && (typeof name !== 'string' || !/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(name))) throw new TypeError(`genuiSandboxKit: name "${String(name)}" must be letters, digits, "-" or "_" (up to 64)`); } function tailwindRuntimePath() { try { const require = createRequire(import.meta.url); return require.resolve('@tailwindcss/browser'); } catch { return null; } } const HMR_BRIDGE = ` if (import.meta.hot && typeof window !== 'undefined' && !window.__genuiSandboxKitHmr) { window.__genuiSandboxKitHmr = true; import.meta.hot.on(${JSON.stringify(SANDBOX_KIT_UPDATE_EVENT)}, (data) => window.dispatchEvent(new CustomEvent(${JSON.stringify(SANDBOX_KIT_UPDATE_EVENT)}, { detail: data }))); }`; /** A stylesheet under node_modules that is a workspace package (a symlink out of node_modules). */ function WORKSPACE_LINKED(path) { if (!path.includes('/node_modules/')) return false; try { return !realpathSync(path).includes('/node_modules/'); } catch { return false; } } /** A file a change to which may change the compiled styles. */ const STYLE_CHANGE = new RegExp(`(${STYLE_EXTENSIONS.map((ext) => ext.replace('.', '\\.')).join('|')}|tailwind\\.config\\.[cm]?[jt]s)$`, 'i'); function kib(chars) { return `${Math.round(chars / 1024)} KB`; } export function createGenuiSandboxKitPlugin(options, defaults) { assertNoLegacyKitOptions(options, 'genuiSandboxKit'); // Read once: the kit's identity (its urls, files, manifest keys) does not change while it runs. const kitName = options.name; assertSandboxKitName(kitName); const manifestKeys = sandboxKitManifestKeys(kitName); let config; let files = null; let bundle = null; let styles = null; let docs = []; const scannedVars = {}; let descriptor = null; let firstBuild; let onReady; const ready = new Promise((resolveReady) => { onReady = resolveReady; }); const warned = new Set(); const warn = (message) => { if (warned.has(message)) return; warned.add(message); config?.logger.warn(`[genui-sandbox-kit] ${message}`); }; const devDir = `${SANDBOX_KIT_DEV_PREFIX}${kitName === undefined ? '' : `${kitName}/`}`; const devUrl = (name, hash) => `${config?.base ?? '/'}${devDir}${name}${hash ? `?v=${hash}` : ''}`; /** The descriptor, given where each asset is (`null`: none). */ const describe = (urls) => { const vars = styles !== null && Object.keys(styles.vars).length > 0 ? styles.vars : scannedVars; const guide = readStylesGuide(config?.root ?? process.cwd(), options.stylesDocs); return { version: 2, components: docs, jsx: bundle !== null, ...(files?.wrapper !== undefined && bundle !== null ? { wrapper: true } : {}), ...(Object.keys(vars).length > 0 ? { theme: { vars: { ...vars } } } : {}), ...stylesDocs(styles), ...(guide !== undefined ? { guide } : {}), assets: { kit: bundle !== null && urls.kit !== null ? { url: urls.kit, hash: bundle.hash } : null, css: styles !== null && styles.css !== '' && urls.css !== null ? { url: urls.css, hash: styles.hash } : null, tailwind: styles?.tailwind !== undefined && urls.tailwind !== null ? { url: urls.tailwind, ...(styles.tailwind.directives !== '' ? { css: styles.tailwind.directives } : {}), } : null, }, }; }; const describeDev = () => describe({ kit: bundle === null ? null : devUrl('kit.js', bundle.hash), css: styles === null ? null : devUrl('styles.css', styles.hash), tailwind: tailwindRuntimePath() === null ? null : devUrl('tailwind.js'), }); const writeDescriptor = () => { if (config === undefined) return; descriptor = describeDev(); const path = resolve(config.root, options.descriptor ?? sandboxKitDescriptorPath(defaults.descriptor, kitName)); try { mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, `${JSON.stringify(descriptor, null, 2)}\n`); } catch (error) { warn(`could not write ${path}: ${error instanceof Error ? error.message : String(error)}`); } }; const styleEntries = () => { if (config === undefined) return []; if (options.styles === undefined) { return discoverStyleEntries(config.root, { input: config.build.rolldownOptions?.input ?? config.build.rollupOptions?.input, }); } const found = resolveStyleEntries(config.root, options.styles); for (const spec of found.missing) warn(`styles: "${spec}" matched no file`); return found.files; }; // One build at a time, each reading the options as they are when it starts: a change that comes // while a build runs waits for it, so a slower, older build never lands after a newer one. let building = Promise.resolve(); const buildAll = () => { const run = building.then(buildOnce); building = run.catch(() => { }); return run; }; const buildOnce = async () => { if (config === undefined) return; const root = config.root; try { files = resolveSandboxKitFiles(root, options); } catch (error) { warn(error instanceof Error ? error.message : String(error)); files = null; } for (const spec of files?.missing ?? []) warn(`components: "${spec}" matched no module`); const plugins = [ ...(options.plugins ?? []), ...(await tailwindVitePluginFor(root, config.plugins ?? [], options.plugins ?? [])), ]; const shared = { root, resolve: config.resolve, ...(config.css !== undefined ? { css: config.css } : {}), plugins, ...(options.inlineAssetLimit !== undefined ? { inlineAssetLimit: options.inlineAssetLimit } : {}), ...(options.inlineAssetBudget !== undefined ? { inlineAssetBudget: options.inlineAssetBudget } : {}), warn, }; const current = files; const [builtStyles, built, generated] = await Promise.all([ buildSandboxKitStyles({ ...shared, entries: styleEntries(), ...(config.publicDir !== undefined ? { publicDir: config.publicDir } : {}), }).catch((error) => { warn(`the styles did not compile: ${error instanceof Error ? error.message : String(error)}`); return null; }), current === null ? Promise.resolve(null) : buildSandboxKitBundle({ ...shared, files: current }).catch((error) => { warn(`the kit did not build: ${error instanceof Error ? error.message : String(error)}`); return null; }), current === null || (current.files.length === 0 && current.packages.length === 0) ? Promise.resolve([]) : generateSandboxKitDocs({ root, files: current, ...(options.tsconfig !== undefined ? { tsconfig: options.tsconfig } : {}), ...(options.typescript !== undefined ? { typescript: options.typescript } : {}), }).catch((error) => { warn(`kit docs from names (and propTypes) only: ${error instanceof Error ? error.message : String(error)}`); return null; }), ]); const purge = options.purge ?? true; styles = builtStyles === null || purge === false ? builtStyles : purgeSandboxKitStyles(builtStyles, { ...(built !== null ? { kitCode: built.code } : {}), ...(typeof purge === 'object' && purge.safelist !== undefined ? { safelist: purge.safelist } : {}), }); if (styles?.purged !== undefined) config.logger.info(`[genui-sandbox-kit] styles purged: ${kib(styles.purged.before)} → ${kib(styles.css.length)}`); bundle = built; docs = generated ?? (built === null ? [] : kitDocsFromBundle(built.code)); if (generated === null && docs.length === 0 && built !== null) docs = built.exports.map((name) => ({ name, props: [] })); }; let rebuildTimer; const plugin = { name: 'genui-sandbox-kit', enforce: 'pre', api: { kitName, descriptor: () => descriptor, ready: () => ready, }, configResolved(resolved) { config = resolved; }, transform(code, id) { const path = id.split('?')[0] ?? id; // The app's own CSS — workspace packages pnpm injects under node_modules (`file:` deps) too — // never a third-party library's. const appCss = !path.includes('/node_modules/') || /\/node_modules\/\.pnpm\/[^/]*@file\+/.test(path) || WORKSPACE_LINKED(path); if (path.endsWith('.css') && appCss) { // The theme's variables as the app's CSS declares them — used when there are no styles. const found = themeVarsFromCss(code, { rootOnly: true }); let changed = false; for (const [name, value] of Object.entries(found)) { if (scannedVars[name] === undefined) { scannedVars[name] = value; changed = true; } } if (changed && styles === null && config?.command === 'serve' && firstBuild !== undefined) writeDescriptor(); return null; } if (config?.command === 'serve' && /\.[cm]?[jt]sx?$/.test(path) && !path.includes('/node_modules/') && defaults.rendererImport.test(code)) { return { code: `${code}\n${HMR_BRIDGE}`, map: null }; } return null; }, configureServer(server) { const watchAll = () => { const paths = [...(files?.dirs ?? []), ...(styles?.files ?? [])]; if (paths.length > 0) server.watcher.add?.(paths); }; firstBuild = buildAll() .catch((error) => { warn(`the kit did not build: ${error instanceof Error ? error.message : String(error)}`); }) .finally(() => { writeDescriptor(); watchAll(); onReady(); }); server.middlewares.use((req, res, next) => { const url = String(req.url ?? '').split('?')[0] ?? ''; const prefix = `${config?.base ?? '/'}${devDir}`; const rest = url.startsWith(prefix) ? url.slice(prefix.length) : url.startsWith(`/${devDir}`) ? url.slice(devDir.length + 1) : undefined; // Its own files only: `<prefix>kit.js` — another kit's (`<prefix><name>/kit.js`) is not its. if (rest === undefined || rest.includes('/')) return next(); const name = rest; void (async () => { await firstBuild; let body = null; let type = 'text/javascript; charset=utf-8'; if (name === 'kit.js') body = bundle?.code ?? null; else if (name === 'styles.css') { body = styles?.css ?? null; type = 'text/css; charset=utf-8'; } else if (name === 'tailwind.js' && styles?.tailwind !== undefined) { const path = tailwindRuntimePath(); body = path === null ? null : readFileSync(path, 'utf8'); } else if (name === 'descriptor.json') { body = JSON.stringify(descriptor); type = 'application/json'; } if (body === null) { res.statusCode = 404; res.end('not found'); return; } res.setHeader('content-type', type); res.setHeader('cache-control', 'no-cache'); res.end(body); })(); }); const changed = (file) => { if (config === undefined) return; // Under node_modules only what the kit itself read (an injected workspace package). if (file.includes('/node_modules/') && styles?.files.includes(file) !== true && bundle?.modules.includes(file) !== true) return; const inKit = files?.dirs.some((dir) => file.startsWith(dir)) === true || bundle?.modules.includes(file) === true || files?.wrapper === file; const inStyles = styles?.files.includes(file) === true || STYLE_CHANGE.test(file); if (!inKit && !inStyles) return; clearTimeout(rebuildTimer); rebuildTimer = setTimeout(() => { firstBuild = buildAll() .then(() => { writeDescriptor(); watchAll(); server.ws.send({ type: 'custom', event: SANDBOX_KIT_UPDATE_EVENT, data: { ...(kitName !== undefined ? { name: kitName } : {}), hash: bundle?.hash ?? null, url: descriptor?.assets.kit?.url ?? null, css: descriptor?.assets.css?.url ?? null, }, }); config?.logger.info('[genui-sandbox-kit] kit rebuilt'); }) .catch((error) => { warn(`the kit did not rebuild: ${error instanceof Error ? error.message : String(error)}`); }); }, 150); }; server.watcher.on('change', changed); server.watcher.on('add', changed); server.watcher.on('unlink', changed); }, async generateBundle() { if (config?.command !== 'build') return; await buildAll(); const base = config.base ?? '/'; const emit = (key, source) => { const ref = this.emitFile({ type: 'asset', name: manifestKeys[key], originalFileName: manifestKeys[key], source, }); const fileName = this.getFileName(ref); emitted[key] = fileName; return `${base}${fileName}`; }; const kitUrl = bundle !== null ? emit('kit', bundle.code) : null; const cssUrl = styles !== null && styles.css !== '' ? emit('css', styles.css) : null; const runtime = styles?.tailwind !== undefined ? tailwindRuntimePath() : null; const tailwindUrl = runtime !== null ? emit('tailwind', readFileSync(runtime, 'utf8')) : null; descriptor = describe({ kit: kitUrl, css: cssUrl, tailwind: tailwindUrl }); emit('descriptor', JSON.stringify(descriptor)); }, /** The manifest lists them by their names, whatever Vite's own manifest did with them. */ writeBundle() { if (config?.command !== 'build' || config.build.manifest === false) return; const manifestPath = join(resolve(config.root, config.build.outDir), typeof config.build.manifest === 'string' ? config.build.manifest : '.vite/manifest.json'); if (!existsSync(manifestPath)) return; const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); for (const [key, file] of Object.entries(emitted)) { if (file === undefined) continue; const name = manifestKeys[key]; manifest[name] = { file, src: name }; } writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); }, }; const emitted = {}; return plugin; } //# sourceMappingURL=plugin.js.map