import { isAbsolute, relative, resolve } from 'node:path' import { generate, type GenerateOptions } from './generate' import type { Plugin } from 'vite' export interface OnZeroPluginOptions extends Omit { /** base data directory. defaults to src/data */ dir?: string /** additional paths to apply HMR fix to */ hmrInclude?: string[] /** disable code generation (HMR only) */ disableGenerate?: boolean } function createOnZeroHmrPlugin(hmrInclude: string[] = [], dataDir = 'src/data'): Plugin { const normalizedDataDir = `/${dataDir.replaceAll('\\', '/').replace(/^\/+|\/+$/g, '')}/` const hmrPaths = [normalizedDataDir, ...hmrInclude] return { name: 'on-zero:hmr', apply: 'serve', enforce: 'post', transform(code, id) { if (!hmrPaths.some((p) => id.includes(p)) || !/\.tsx?$/.test(id)) return let transformed = code // strip zero's default invalidate behavior so models HMR in-place if (code.includes('import.meta.hot.invalidate')) { transformed = transformed.replace( /if\s*\(invalidateMessage\)\s*import\.meta\.hot\.invalidate\(invalidateMessage\);?/g, '/* on-zero: HMR invalidate disabled */' ) } // inject HMR accept boundary for generated models file if (id.includes('/generated/models.') && !code.includes('import.meta.hot.accept')) { transformed += `\nif (import.meta.hot) {\n import.meta.hot.accept()\n}\n` } if (transformed === code) return return { code: transformed, map: null } }, } } function isWithinDirectory(file: string, dir: string): boolean { const rel = relative(dir, file) return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel) } export function onZeroPlugin(options: OnZeroPluginOptions = {}): Plugin[] { const dir = options.dir ?? 'src/data' let dataDir: string let databaseDir: string const runGenerate = (silent: boolean) => generate({ dir: dataDir, after: options.after, silent, force: options.force, }) return [ { name: 'on-zero:serve', apply: 'serve', configResolved(config) { dataDir = resolve(config.root, dir) databaseDir = resolve(dataDir, '../database') }, async buildStart() { if (!options.disableGenerate) await runGenerate(false) }, configureServer(server) { if (options.disableGenerate) return const handler = async (file: string) => { if (!/\.tsx?$/.test(file)) return if ( (isWithinDirectory(file, dataDir) && !isWithinDirectory(file, resolve(dataDir, 'generated'))) || isWithinDirectory(file, databaseDir) ) { await runGenerate(false) } } server.watcher.on('change', handler) server.watcher.on('add', handler) server.watcher.on('unlink', handler) }, }, { name: 'on-zero:build', apply: 'build', configResolved(config) { dataDir = resolve(config.root, dir) }, async buildStart() { if (!options.disableGenerate) await runGenerate(true) }, }, createOnZeroHmrPlugin(options.hmrInclude, dir), ] } export const onZeroHmrPlugin = (options?: { include?: string[] }): Plugin => { return createOnZeroHmrPlugin(options?.include) } export default onZeroPlugin