94 lines
5.4 KiB
TypeScript
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 },
|
|
}
|
|
}
|