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>
198 lines
5.3 KiB
TypeScript
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;
|