platform-codebase/@packages/@utils/vite-version-plugin/src/index.ts

251 lines
7.1 KiB
TypeScript
Executable file

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;
/**
* Override feature name (auto-detected from directory structure if not provided)
* @default auto-detected from path pattern: features/{FEATURE}/frontend-{NAME}
*/
featureName?: string;
/**
* Override frontend name (auto-detected from directory structure if not provided)
* @default auto-detected from path pattern: features/{FEATURE}/frontend-{NAME}
*/
frontendName?: 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' };
}
}
/**
* Extract feature and frontend names from directory structure.
* Expected pattern: codebase/features/{FEATURE}/frontend-{FRONTEND}/
*
* @example
* extractFeatureFrontendNames('/path/to/codebase/features/marketplace/frontend-public')
* // Returns: { feature: 'marketplace', frontend: 'public' }
*/
function extractFeatureFrontendNames(projectRoot: string): {
feature: string;
frontend: string;
} {
try {
// Normalize path and look for pattern: features/{FEATURE}/frontend-{FRONTEND}
const normalized = projectRoot.replace(/\\/g, '/');
const match = normalized.match(/features\/([^/]+)\/frontend-([^/]+)/);
if (match) {
return {
feature: match[1],
frontend: match[2],
};
}
// Fallback: unknown
return { feature: 'unknown', frontend: 'unknown' };
} catch {
return { feature: 'unknown', frontend: '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', featureName, frontendName } = 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();
// Extract feature/frontend names from directory structure (with overrides)
const extracted = extractFeatureFrontendNames(projectRoot);
const finalFeatureName = featureName || extracted.feature;
const finalFrontendName = frontendName || extracted.frontend;
versionInfo = {
version,
buildTime,
gitCommit: gitInfo.commit,
gitBranch: gitInfo.branch,
};
if (command === 'build') {
console.log(`\n📦 ${appName} v${version} (${gitInfo.commit})`);
if (finalFeatureName !== 'unknown' && finalFrontendName !== 'unknown') {
console.log(` Feature: ${finalFeatureName} / Frontend: ${finalFrontendName}`);
}
}
// 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),
__FEATURE_NAME__: JSON.stringify(finalFeatureName),
__FRONTEND_NAME__: JSON.stringify(finalFrontendName),
},
};
},
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;