platform-codebase/@packages/@utils/vite-version-plugin/src/index.ts
Quinn Ftw 65cf81299f fix(vite-version-plugin): calculate version in config() hook instead of configResolved()
The config() hook runs before configResolved(), so version info must be
calculated there for the define values to be properly injected at build time.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 04:22:02 -08:00

198 lines
5.3 KiB
TypeScript

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 resolvedConfig: ResolvedConfig;
let versionInfo: VersionInfo;
return {
name: 'lilith-version-plugin',
// config() runs first - calculate version here using cwd as fallback
config(userConfig, { command }) {
const projectRoot = userConfig.root || process.cwd();
const versionPath = findVersionFile(projectRoot, versionFile);
const version = versionPath ? readVersion(versionPath) : fallbackVersion;
const gitInfo = getGitInfo();
const buildTime = new Date().toISOString();
versionInfo = {
version,
buildTime,
gitCommit: gitInfo.commit,
gitBranch: gitInfo.branch,
};
if (command === 'build') {
console.log(`\n📦 ${appName} v${version} (${gitInfo.commit})`);
}
// Return define config to be merged
return {
define: {
__APP_VERSION__: JSON.stringify(version),
__BUILD_TIME__: JSON.stringify(buildTime),
__GIT_COMMIT__: JSON.stringify(gitInfo.commit),
__GIT_BRANCH__: JSON.stringify(gitInfo.branch),
__APP_NAME__: JSON.stringify(appName),
},
};
},
configResolved(config) {
resolvedConfig = config;
},
writeBundle(outputOptions) {
if (!generateBuildInfo) return;
const outDir = outputOptions.dir || resolvedConfig.build.outDir;
const buildInfo = {
app: appName,
...versionInfo,
};
const buildInfoPath = join(outDir, 'build-info.json');
writeFileSync(buildInfoPath, JSON.stringify(buildInfo, null, 2));
},
};
}
export default versionPlugin;