Files
archy/neode-ui/vite-aiui-native.ts

94 lines
5.4 KiB
TypeScript

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<string>()
/** 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<T extends { url: string }>(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|(?<![\w.-])(?:html|body)(?![\w-])|#app\b/g, ':scope').replace(/:scope\s+:scope/g, ':scope')
})
sheet.walkAtRules('layer', rule => { if (rule.nodes) rule.replaceWith(...rule.nodes); else rule.remove() })
const animations = new Map<string, string>()
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(`(?<![\\w-])${name}(?![\\w-])`, 'g'), scoped)
})
const properties: string[] = []
sheet.walkAtRules('property', rule => { 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<string>()
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<string>; importedAssets: Set<string> } }).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 },
}
}