diff --git a/@packages/@utils/vite-version-plugin/package.json b/@packages/@utils/vite-version-plugin/package.json new file mode 100644 index 000000000..dd8e11049 --- /dev/null +++ b/@packages/@utils/vite-version-plugin/package.json @@ -0,0 +1,27 @@ +{ + "name": "@lilith/vite-version-plugin", + "version": "1.0.0", + "description": "Vite plugin for injecting version info from VERSION.txt at build time", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "import": "./src/index.ts", + "require": "./src/index.ts", + "types": "./src/index.ts" + }, + "./console": { + "import": "./src/console-banner.ts", + "require": "./src/console-banner.ts", + "types": "./src/console-banner.ts" + } + }, + "peerDependencies": { + "vite": ">=4.0.0" + }, + "devDependencies": { + "vite": "^5.4.21", + "typescript": "^5.6.3" + }, + "license": "UNLICENSED" +} diff --git a/@packages/@utils/vite-version-plugin/src/console-banner.ts b/@packages/@utils/vite-version-plugin/src/console-banner.ts new file mode 100644 index 000000000..a22b366ff --- /dev/null +++ b/@packages/@utils/vite-version-plugin/src/console-banner.ts @@ -0,0 +1,73 @@ +/** + * Console banner for displaying app version in browser console + * + * Uses global constants injected by versionPlugin: + * - __APP_NAME__ + * - __APP_VERSION__ + * - __BUILD_TIME__ + * - __GIT_COMMIT__ + * + * @example + * ```ts + * // In your app's entry point (main.tsx or App.tsx) + * import { logVersionBanner } from '@lilith/vite-version-plugin/console'; + * + * logVersionBanner(); + * ``` + */ + +declare const __APP_NAME__: string; +declare const __APP_VERSION__: string; +declare const __BUILD_TIME__: string; +declare const __GIT_COMMIT__: string; + +/** + * Logs a styled version banner to the browser console + * + * @param options Optional customization + */ +export function logVersionBanner(options?: { + primaryColor?: string; + secondaryColor?: string; +}): void { + const { + primaryColor = '#ff00ff', + secondaryColor = '#00ffff', + } = options || {}; + + const appName = typeof __APP_NAME__ !== 'undefined' ? __APP_NAME__ : 'App'; + const version = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0'; + const buildTime = typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : 'unknown'; + const gitCommit = typeof __GIT_COMMIT__ !== 'undefined' ? __GIT_COMMIT__ : 'unknown'; + + // Format build time to be more readable + const formattedTime = buildTime !== 'unknown' + ? new Date(buildTime).toLocaleString() + : 'unknown'; + + console.log( + `%c ${appName} v${version} %c ${gitCommit} %c Built: ${formattedTime} `, + `background: ${primaryColor}; color: #000; font-weight: bold; padding: 4px 8px;`, + `background: #333; color: #fff; padding: 4px 8px;`, + `background: ${secondaryColor}; color: #000; padding: 4px 8px;` + ); +} + +/** + * Returns version info as an object (useful for debugging or display in UI) + */ +export function getVersionInfo(): { + appName: string; + version: string; + buildTime: string; + gitCommit: string; +} { + return { + appName: typeof __APP_NAME__ !== 'undefined' ? __APP_NAME__ : 'App', + version: typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0', + buildTime: typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : 'unknown', + gitCommit: typeof __GIT_COMMIT__ !== 'undefined' ? __GIT_COMMIT__ : 'unknown', + }; +} + +export default logVersionBanner; diff --git a/@packages/@utils/vite-version-plugin/src/index.ts b/@packages/@utils/vite-version-plugin/src/index.ts new file mode 100644 index 000000000..36de2f078 --- /dev/null +++ b/@packages/@utils/vite-version-plugin/src/index.ts @@ -0,0 +1,196 @@ +import type { Plugin, ResolvedConfig } from 'vite'; +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { execFileSync } from 'child_process'; +import { resolve, join } from 'path'; + +export interface VersionPluginOptions { + /** + * App name for console banner and build-info.json + */ + appName: string; + + /** + * Path to VERSION.txt file (relative to project root or absolute) + * Can also be VERSION.json - will be auto-detected + * @default looks for VERSION.txt in project root, then monorepo root + */ + versionFile?: string; + + /** + * Whether to generate build-info.json in output directory + * @default true + */ + generateBuildInfo?: boolean; + + /** + * Custom fallback version if VERSION file is not found + * @default '0.0.0-dev' + */ + fallbackVersion?: string; +} + +interface VersionInfo { + version: string; + buildTime: string; + gitCommit: string; + gitBranch: string; +} + +function findVersionFile(projectRoot: string, customPath?: string): string | null { + // If custom path provided, use it + if (customPath) { + const resolved = resolve(projectRoot, customPath); + if (existsSync(resolved)) return resolved; + return null; + } + + // Search order: project VERSION.txt -> project VERSION.json -> monorepo VERSION.txt -> monorepo VERSION.json + const searchPaths = [ + join(projectRoot, 'VERSION.txt'), + join(projectRoot, 'VERSION.json'), + // Go up to find monorepo root (look for pnpm-workspace.yaml) + ...findMonorepoVersionPaths(projectRoot), + ]; + + for (const p of searchPaths) { + if (existsSync(p)) return p; + } + + return null; +} + +function findMonorepoVersionPaths(startDir: string): string[] { + const paths: string[] = []; + let current = startDir; + let depth = 0; + const maxDepth = 10; + + while (depth < maxDepth) { + const parent = resolve(current, '..'); + if (parent === current) break; + + const workspaceFile = join(parent, 'pnpm-workspace.yaml'); + if (existsSync(workspaceFile)) { + paths.push(join(parent, 'VERSION.txt')); + paths.push(join(parent, 'VERSION.json')); + break; + } + + current = parent; + depth++; + } + + return paths; +} + +function readVersion(filePath: string): string { + const content = readFileSync(filePath, 'utf-8').trim(); + + // If JSON file, parse and extract version + if (filePath.endsWith('.json')) { + try { + const json = JSON.parse(content); + return json.version || '0.0.0'; + } catch { + return '0.0.0'; + } + } + + // Plain text VERSION.txt - just return the content (first line) + return content.split('\n')[0].trim(); +} + +function getGitInfo(): { commit: string; branch: string } { + try { + const commit = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { + encoding: 'utf-8', + }).trim(); + const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { + encoding: 'utf-8', + }).trim(); + return { commit, branch }; + } catch { + return { commit: 'unknown', branch: 'unknown' }; + } +} + +/** + * Vite plugin that injects version info at build time + * + * Defines these globals: + * - __APP_VERSION__: string - Version from VERSION.txt/VERSION.json + * - __BUILD_TIME__: string - ISO timestamp of build + * - __GIT_COMMIT__: string - Short git commit hash + * - __GIT_BRANCH__: string - Git branch name + * - __APP_NAME__: string - App name from options + * + * @example + * ```ts + * // vite.config.ts + * import { versionPlugin } from '@lilith/vite-version-plugin'; + * + * export default defineConfig({ + * plugins: [ + * versionPlugin({ appName: 'my-dashboard' }) + * ] + * }); + * ``` + */ +export function versionPlugin(options: VersionPluginOptions): Plugin { + const { appName, versionFile, generateBuildInfo = true, fallbackVersion = '0.0.0-dev' } = options; + + let config: ResolvedConfig; + let versionInfo: VersionInfo; + + return { + name: 'lilith-version-plugin', + + configResolved(resolvedConfig) { + config = resolvedConfig; + + const projectRoot = config.root; + const versionPath = findVersionFile(projectRoot, versionFile); + const version = versionPath ? readVersion(versionPath) : fallbackVersion; + const gitInfo = getGitInfo(); + + versionInfo = { + version, + buildTime: new Date().toISOString(), + gitCommit: gitInfo.commit, + gitBranch: gitInfo.branch, + }; + + if (config.command === 'build') { + console.log(`\n📦 ${appName} v${version} (${gitInfo.commit})`); + } + }, + + config() { + // Will be merged with user config + return { + define: { + __APP_VERSION__: JSON.stringify(versionInfo?.version || fallbackVersion), + __BUILD_TIME__: JSON.stringify(new Date().toISOString()), + __GIT_COMMIT__: JSON.stringify(versionInfo?.gitCommit || 'unknown'), + __GIT_BRANCH__: JSON.stringify(versionInfo?.gitBranch || 'unknown'), + __APP_NAME__: JSON.stringify(appName), + }, + }; + }, + + writeBundle(outputOptions) { + if (!generateBuildInfo) return; + + const outDir = outputOptions.dir || config.build.outDir; + const buildInfo = { + app: appName, + ...versionInfo, + }; + + const buildInfoPath = join(outDir, 'build-info.json'); + writeFileSync(buildInfoPath, JSON.stringify(buildInfo, null, 2)); + }, + }; +} + +export default versionPlugin; diff --git a/infrastructure/service-registry/apps/dashboard/src/App.tsx b/infrastructure/service-registry/apps/dashboard/src/App.tsx index f91ca5fa3..c03283109 100644 --- a/infrastructure/service-registry/apps/dashboard/src/App.tsx +++ b/infrastructure/service-registry/apps/dashboard/src/App.tsx @@ -1,6 +1,10 @@ import { RouterProvider } from 'react-router-dom'; import { ThemeProvider } from '@lilith/ui-theme'; import { router } from './router'; +import { logVersionBanner } from '@lilith/vite-version-plugin/console'; + +// Log version banner to console on app load +logVersionBanner({ primaryColor: '#4ecdc4', secondaryColor: '#ff6b6b' }); export default function App() { return ( diff --git a/infrastructure/service-registry/apps/dashboard/vite.config.ts b/infrastructure/service-registry/apps/dashboard/vite.config.ts index e94b872dd..48a307c0f 100644 --- a/infrastructure/service-registry/apps/dashboard/vite.config.ts +++ b/infrastructure/service-registry/apps/dashboard/vite.config.ts @@ -1,9 +1,17 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { resolve } from 'path'; +// Direct import for vite config (aliases not available at config load time) +import { versionPlugin } from '../../../../@packages/@utils/vite-version-plugin/src'; export default defineConfig({ - plugins: [react()], + plugins: [ + react(), + versionPlugin({ + appName: 'Service Registry Dashboard', + // Will find VERSION.json in monorepo root automatically + }), + ], server: { port: 5300, host: true, @@ -24,7 +32,9 @@ export default defineConfig({ '@lilith/ui-theme': resolve(__dirname, '../../../../@packages/@ui/ui-theme/src'), '@lilith/ui-primitives': resolve(__dirname, '../../../../@packages/@ui/ui-primitives/src'), '@lilith/ui-data': resolve(__dirname, '../../../../@packages/@ui/ui-data/src'), - '@lilith/ui-layout': resolve(__dirname, '../../../../@packages/@ui/ui-layout/src') + '@lilith/ui-layout': resolve(__dirname, '../../../../@packages/@ui/ui-layout/src'), + '@lilith/vite-version-plugin/console': resolve(__dirname, '../../../../@packages/@utils/vite-version-plugin/src/console-banner.ts'), + '@lilith/vite-version-plugin': resolve(__dirname, '../../../../@packages/@utils/vite-version-plugin/src') } }, build: {