import { statSync } from 'node:fs' import { resolve } from 'node:path' import MagicString from 'magic-string' import type { Plugin } from 'vite' const CONFIG_EXTENSIONS = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'] const toPosix = (p: string) => p.replace(/\\/g, '/') const existingConfigFiles = (rootDirs: string[]): Set => { const files = new Set() for (const dir of rootDirs) { for (const ext of CONFIG_EXTENSIONS) { const file = resolve(dir, `app.config${ext}`) try { if (statSync(file).isFile()) files.add(toPosix(file)) } catch { // not present in this layer } } } return files } /** * Dev-only plugin: restart the Vite server when any layer's `app.config.*` changes. * * `app.config.ts` is loaded out-of-band by c12 (not part of Vite's module graph or config-file * dependencies), so Vite never restarts on its own when you edit feature flags / layer config — the * baked `__FEATURES__` `define` and aliases go stale. We watch each resolved layer's config file * (including layers outside the project root via `watcher.add`) and call `server.restart()`, which * re-runs `buildViteConfig` → `resolveLayerStack` (c12 reads fresh) → new `define`. */ export function configWatchPlugin(rootDirs: string[]): Plugin { return { name: 'vite-layers:config-watch', apply: 'serve', configureServer(server) { const files = existingConfigFiles(rootDirs) if (files.size === 0) return server.watcher.add([...files]) // ensure extended layers outside the root are watched too const onChange = (file: string) => { if (!files.has(toPosix(file))) return server.config.logger.info('[vite-layers] app config changed — restarting…', { timestamp: true }) void server.restart() } server.watcher.on('change', onChange) }, } } /** Matches a standalone `__FEATURES__` reference (not a `.__FEATURES__` property access). */ const STANDALONE_FEATURES_RE = /(?`, not in template expressions. */ export function featuresRuntimePlugin(features: Record = {}): Plugin { const json = JSON.stringify(features) return { name: 'vite-layers:features-runtime', apply: 'serve', transform(code, id) { if (id.includes('/node_modules/') || !STANDALONE_FEATURES_RE.test(code)) return null // NOTE: rolldown's *native* magic-string (the transform `meta.magicString` in the rolldown // docs) is NOT surfaced by Vite plugins — `meta` is `{ inMap, moduleType, ssr }` with no // `magicString` in dev or build. So we use the npm `magic-string` fallback the rolldown docs // recommend for non-native hosts; it also produces clean cross-platform sourcemaps. // Prepend on line 1 (keeps line numbers); module-local const shadows the missing global. const s = new MagicString(code) s.prepend(`const __FEATURES__=${json};`) return { code: s.toString(), map: s.generateMap({ source: id, hires: true }) } }, } }