@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
195 lines • 8.52 kB
JavaScript
/**
* One in-memory build with the app's Vite (`vite.build`, nothing written) — what both the kit bundle
* and the compiled styles come from. The app's `resolve` (aliases) and `css` options (preprocessor
* options, CSS modules, PostCSS, Lightning CSS) are kept, so `@/lib/utils`, a Sass `additionalData`
* or a PostCSS plugin work as they do in the app; assets referenced by the CSS are inlined as
* `data:` URIs up to a limit and a budget, the rest left out (the frame has no network).
*/
import { createHash } from 'node:crypto';
import { existsSync, readFileSync, statSync } from 'node:fs';
import { extname, join, relative, resolve } from 'node:path';
import { INLINE_ASSET_DEFAULTS, inlineCssUrls } from '../sandbox-styles.js';
/** The entry's file name (Vite resolves an input against the root) and its virtual id. */
const ENTRY_FILE = 'genui-sandbox-kit-entry.js';
const ENTRY_ID = '\0genui-sandbox-kit-entry';
const MIME = {
'.woff2': 'font/woff2',
'.woff': 'font/woff',
'.ttf': 'font/ttf',
'.otf': 'font/otf',
'.eot': 'application/vnd.ms-fontobject',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.avif': 'image/avif',
'.ico': 'image/x-icon',
'.cur': 'image/x-icon',
};
export async function runKitViteBuild(options) {
const vite = (await import('vite'));
const major = Number.parseInt(String(vite.version).split('.')[0] ?? '0', 10);
const limit = options.inlineAssetLimit ?? INLINE_ASSET_DEFAULTS.limit;
const budget = options.inlineAssetBudget ?? INLINE_ASSET_DEFAULTS.budget;
let inlined = 0;
const reasons = new Map();
const result = await vite.build({
configFile: false,
root: options.root,
logLevel: 'silent',
mode: 'production',
publicDir: options.publicDir ?? false,
// One React: components living outside the app (a workspace package, another repo) resolve
// `react` from the app root too, never a second copy that would break their hooks.
resolve: {
...(options.resolve ?? {}),
dedupe: [
...(options.resolve?.dedupe ?? []),
'react',
'react-dom',
],
},
css: quietCss(options.css),
define: { 'process.env.NODE_ENV': JSON.stringify('production') },
// Vite 8 compiles with Oxc, earlier versions with esbuild: the automatic JSX runtime on both.
// Production JSX (`jsx`, never `jsxDEV`) whatever NODE_ENV the dev server runs under.
...(major >= 8
? { oxc: { jsx: { runtime: 'automatic', development: false } } }
: { esbuild: { jsx: 'automatic', jsxDev: false } }),
plugins: [
{
name: 'genui-sandbox-kit-entry',
resolveId(id) {
if (id === ENTRY_ID || id.endsWith(ENTRY_FILE))
return ENTRY_ID;
return null;
},
load(id) {
return id === ENTRY_ID ? options.entry : null;
},
},
...(options.plugins ?? []),
],
build: {
write: false,
emptyOutDir: false,
copyPublicDir: false,
minify: options.minify ?? true,
sourcemap: false,
cssCodeSplit: false,
reportCompressedSize: false,
modulePreload: false,
// Inlined (`data:`) within the limit and the budget; the rest is emitted, then left out below.
assetsInlineLimit: (file, content) => {
const name = relative(options.root, file) || file;
if (content.length > limit) {
reasons.set(file, `${name} (${kb(content.length)}) is over the ${kb(limit)} limit`);
return false;
}
if (inlined + content.length > budget) {
reasons.set(file, `${name} is over the ${kb(budget)} budget for inlined assets`);
return false;
}
inlined += content.length;
return true;
},
rollupOptions: {
input: ENTRY_FILE,
// The app's own build reports its warnings (`"use client"` directives…); this one is quiet.
onwarn() { },
output: {
format: 'iife',
name: '__genuiSandboxKit',
// One file (Rolldown already bundles an IIFE into one, and warns about the option).
...(major >= 8 ? {} : { inlineDynamicImports: true }),
entryFileNames: 'genui-sandbox-kit.js',
},
},
},
});
const outputs = (Array.isArray(result) ? result : [result]);
let code = '';
let css = '';
const modules = new Set();
const emitted = new Map();
for (const out of outputs) {
for (const item of out.output ?? []) {
if (item.type === 'chunk') {
code += item.code ?? '';
for (const id of item.moduleIds ?? Object.keys(item.modules ?? {})) {
if (!id.startsWith('\0') &&
(!id.includes('/node_modules/') || /\/node_modules\/\.pnpm\/[^/]*@file\+/.test(id)))
modules.add(id.split('?')[0] ?? id);
}
}
else if (item.fileName.endsWith('.css')) {
css +=
typeof item.source === 'string'
? item.source
: Buffer.from(item.source ?? new Uint8Array()).toString('utf8');
}
else {
const original = item.originalFileNames?.[0];
emitted.set(item.fileName, original === undefined ? undefined : resolve(options.root, original));
}
}
}
const publicDir = options.publicDir === false || options.publicDir === undefined ? undefined : options.publicDir;
css = inlineCssUrls(css, {
limit,
// Vite counted what it inlined; this pass only checks what is left.
budget: Number.POSITIVE_INFINITY,
...(options.warn !== undefined ? { warn: options.warn } : {}),
resolve(url) {
const path = (url.split(/[?#]/)[0] ?? url).replace(/^\.?\//, '');
for (const [fileName, original] of emitted) {
if (path === fileName || path.endsWith(`/${fileName}`) || path.endsWith(fileName)) {
return {
dropped: (original !== undefined ? reasons.get(original) : undefined) ??
`${fileName} was not inlined`,
};
}
}
if (publicDir !== undefined && url.startsWith('/')) {
const file = join(publicDir, decodeURIComponent(path));
if (existsSync(file) && statSync(file).isFile()) {
const bytes = readFileSync(file);
if (inlined + bytes.length > budget)
return { dropped: `over the ${kb(budget)} budget for inlined assets` };
inlined += bytes.length;
return {
data: bytes.toString('base64'),
mime: MIME[extname(file).toLowerCase()] ?? 'application/octet-stream',
bytes: bytes.length,
};
}
}
return null;
},
});
return { code, css, modules: [...modules] };
}
/** Sass's deprecation warnings (Bootstrap's own, by the thousand) are not the kit's to print. */
const QUIET_SASS = { quietDeps: true, logger: { warn() { }, debug() { } } };
/** The app's `css` options, with Sass told to keep quiet unless the app said otherwise. */
function quietCss(css) {
const given = (css ?? {});
const pre = given.preprocessorOptions ?? {};
return {
...given,
preprocessorOptions: {
...pre,
scss: { ...QUIET_SASS, ...(pre.scss ?? {}) },
sass: { ...QUIET_SASS, ...(pre.sass ?? {}) },
},
};
}
export function contentHash(text) {
return createHash('sha256').update(text).digest('hex').slice(0, 12);
}
function kb(bytes) {
return `${Math.round(bytes / 1024)} KB`;
}
//# sourceMappingURL=vite-build.js.map