import { createRequire } from 'node:module' import { nativeAIUIProvenance } from './native-aiui-provenance' import { createHash } from 'node:crypto' import { readFileSync, writeFileSync } from 'node:fs' import path from 'node:path' import postcss from 'postcss' import type { Plugin } from 'vite' import type { OutputChunk } from 'rollup' const nativeLazyAssets = new Set() /** Avoid fetching the whole assistant during first service-worker installation. * Hashed assets are cached on use by the dashboard's existing asset route. */ export async function filterNativePrecache(manifest: T[]) { return { manifest: manifest.filter(entry => !nativeLazyAssets.has(entry.url.replace(/^\//, ''))), warnings: [] } } /** Compile the vendored design system for the lazy shell only. Its reset, * utilities and theme remain scoped away from dashboard chrome. */ export function nativeAIUIStyles(): Plugin { const root = path.resolve(import.meta.dirname, '../aiui/packages/app') let css: string | undefined let nativeReceipt: { schema: number; mode: string; source_commit: string; source_dirty: boolean; native_chunk: string; sha256: string; dynamic_entry: boolean } | undefined return { name: 'native-aiui-styles', resolveId(id) { if (id === 'virtual:aiui-native.css') return '\0aiui-native.css' }, async load(id) { if (id !== '\0aiui-native.css') return if (css) return css const appRequire = createRequire(path.join(root, 'package.json')) const tailwindRequire = createRequire(appRequire.resolve('@tailwindcss/vite')) const { compile } = tailwindRequire('@tailwindcss/node') const { Scanner } = tailwindRequire('@tailwindcss/oxide') const entry = path.join(root, 'src/styles/main.css') const compiler = await compile(readFileSync(entry, 'utf8'), { base: path.dirname(entry), onDependency: (file: string) => this.addWatchFile(file), }) const scanner = new Scanner({ sources: [{ base: path.join(root, 'src'), pattern: '**/*.{vue,ts}', negated: false }] }) const sheet = postcss.parse(compiler.build(scanner.scan())) sheet.walkRules(rule => { rule.selector = rule.selector.replace(/:root|:host\b|(? { if (rule.nodes) rule.replaceWith(...rule.nodes); else rule.remove() }) const animations = new Map() sheet.walkAtRules('keyframes', rule => { const name = rule.params; rule.params = `aiui-native-${name}`; animations.set(name, rule.params) }) sheet.walkDecls(declaration => { if (!/^animation(?:-name)?$/.test(declaration.prop) && !declaration.prop.startsWith('--animate-')) return for (const [name, scoped] of animations) declaration.value = declaration.value.replace(new RegExp(`(? { properties.push(rule.toString()); rule.remove() }) css = properties.join('\n') + '\n@scope (.aiui-native) {\n' + sheet.toString() + '\n}\n' css = css.replaceAll('--tw-', '--aiui-tw-') return css }, generateBundle(_options, bundle) { nativeLazyAssets.clear() const chunks = Object.values(bundle).filter((item): item is OutputChunk => item.type === 'chunk') const shell = chunks.find(chunk => Object.keys(chunk.modules).some(id => id.endsWith('/aiui/packages/app/src/NativeShell.vue'))) if (!shell) return const collect = (roots: string[], skip?: string) => { const files = new Set() const visit = (file: string) => { if (file === skip || files.has(file)) return files.add(file) const chunk = bundle[file] if (chunk?.type !== 'chunk') return const meta = (chunk as OutputChunk & { viteMetadata?: { importedCss: Set; importedAssets: Set } }).viteMetadata for (const asset of [...(meta?.importedCss ?? []), ...(meta?.importedAssets ?? [])]) files.add(asset) for (const dependency of [...chunk.imports, ...chunk.dynamicImports]) visit(dependency) } roots.forEach(visit) return files } const host = collect(chunks.filter(chunk => chunk.isEntry).map(chunk => chunk.fileName), shell.fileName) for (const file of collect([shell.fileName])) if (!host.has(file)) nativeLazyAssets.add(file) const repo = path.resolve(root, '../../..') nativeReceipt = { schema: 1, ...nativeAIUIProvenance(repo), native_chunk: shell.fileName, sha256: '0'.repeat(64), dynamic_entry: shell.isDynamicEntry, } this.emitFile({ type: 'asset', fileName: 'NATIVE-AIUI.json', source: JSON.stringify(nativeReceipt, null, 2) + '\n' }) }, writeBundle(options) { if (!nativeReceipt || !options.dir) return // Vite's later generateBundle hooks rewrite preload/import bytes. Bind // the finished file after those hooks, not an intermediate Rollup chunk. nativeReceipt.sha256 = createHash('sha256').update(readFileSync(path.join(options.dir, nativeReceipt.native_chunk))).digest('hex') writeFileSync(path.join(options.dir, 'NATIVE-AIUI.json'), JSON.stringify(nativeReceipt, null, 2) + '\n') }, handleHotUpdate(ctx) { if (ctx.file.startsWith(path.join(root, 'src'))) css = undefined }, } }