UNPKG

one

Version:

One is a new React Framework that makes Vite serve both native and web.

157 lines (132 loc) 5.14 kB
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs' import { join, dirname } from 'node:path' import type { Plugin, ResolvedConfig } from 'vite' const WARM_DEPS_FILE = 'one-warm-deps.json' // how long after server start to track new deps (ms) const TRACKING_WINDOW = 5 * 60 * 1000 /** * Passively tracks deps that Vite discovers during normal dev use. * On next startup, pre-includes them in optimizeDeps to avoid reload cascades. * * Use as a top-level plugin in vite.config.ts: * * import { autoWarmPlugin } from 'one/vite' * plugins: [autoWarmPlugin(), ...] * * Pass a path string to persist permanently instead of in .vite cache: * * autoWarmPlugin('./src/dev/warmDeps.json') */ export function autoWarmPlugin(persistPath?: string): Plugin { let cacheFile: string let cachedDeps: string[] = [] let excludeSet: Set<string> return { name: 'one:auto-warm', apply: 'serve', config() { const volatileCache = join(process.cwd(), 'node_modules', '.vite', WARM_DEPS_FILE) cacheFile = typeof persistPath === 'string' ? join(process.cwd(), persistPath) : volatileCache try { if (existsSync(cacheFile)) { const cached = JSON.parse(readFileSync(cacheFile, 'utf-8')) if (Array.isArray(cached.deps) && cached.deps.length > 0) { cachedDeps = cached.deps console.info(`[one] loading ${cached.deps.length} cached warm deps`) // inject as include, will be filtered in configResolved return { optimizeDeps: { include: cached.deps, }, } } } } catch { // cache corrupted, ignore } }, configResolved(config: ResolvedConfig) { // build the exclude set from the resolved config excludeSet = new Set(config.optimizeDeps.exclude || []) if (cachedDeps.length > 0 && excludeSet.size > 0) { // remove any cached deps that are in the exclude list const conflicts = cachedDeps.filter((d) => excludeSet.has(d)) if (conflicts.length > 0) { console.info(`[one] filtered ${conflicts.length} excluded deps from warm cache`) // mutate the resolved include to remove conflicts if (config.optimizeDeps.include) { ;(config.optimizeDeps as any).include = config.optimizeDeps.include.filter( (d: string) => !excludeSet.has(d) ) } } } }, configureServer(server) { let lastDepsCount = 0 let timer: ReturnType<typeof setInterval> // deps that were already in the user's config before we added ours const userInclude = new Set( (server.config.optimizeDeps.include || []).filter((d) => !cachedDeps.includes(d)) ) function snapshotDeps() { try { const optimizer = (server as any).environments?.client?.depsOptimizer ?? (server as any)._depsOptimizer if (!optimizer?.metadata) return const optimized = optimizer.metadata.optimized as Record<string, unknown> const discovered = optimizer.metadata.discovered as Record<string, unknown> const currentDeps = [ ...Object.keys(optimized || {}), ...Object.keys(discovered || {}), ] if (currentDeps.length === 0 || currentDeps.length === lastDepsCount) return lastDepsCount = currentDeps.length // only cache deps that: // 1. aren't in the user's original config (no point caching what they already have) // 2. aren't in the exclude list const depsToCache = currentDeps.filter( (d) => !userInclude.has(d) && !excludeSet.has(d) ) // merge with existing cache const allDeps = new Set(depsToCache) try { if (existsSync(cacheFile)) { const existing = JSON.parse(readFileSync(cacheFile, 'utf-8')) if (Array.isArray(existing.deps)) { for (const d of existing.deps) { if (!excludeSet.has(d)) allDeps.add(d) } } } } catch {} const sorted = [...allDeps].sort() const dir = dirname(cacheFile) if (!existsSync(dir)) mkdirSync(dir, { recursive: true }) writeFileSync(cacheFile, JSON.stringify({ deps: sorted }, null, 2)) console.info(`[one] cached ${sorted.length} deps for next startup`) } catch { // not ready yet, will retry } } server.httpServer?.once('listening', () => { timer = setInterval(snapshotDeps, 5000) setTimeout(() => { clearInterval(timer) snapshotDeps() }, TRACKING_WINDOW) }) // snapshot on server close const origClose = server.close.bind(server) server.close = async () => { clearInterval(timer) snapshotDeps() return origClose() } }, } } // keep old name as alias export const warmRoutesPlugin = autoWarmPlugin