platform-tooling/scripts/validation/verify-styled-components.ts
Quinn Ftw b2890067b2 test(validation): Add validation rules for styled-components usage in verification scripts
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-03-02 21:06:54 -08:00

257 lines
7.1 KiB
TypeScript

#!/usr/bin/env tsx
/**
* Styled Components Version Verification
*
* Verifies that only a single version of styled-components exists across all workspaces.
* Multiple versions break React Context (ThemeProvider) causing "props.theme is undefined" errors.
*
* Usage:
* npx tsx tooling/scripts/validation/verify-styled-components.ts
* npx tsx tooling/scripts/validation/verify-styled-components.ts --feature marketplace
*
* Exit codes:
* 0 = Single version detected (success)
* 1 = Multiple versions detected (failure)
* 2 = Script error (configuration issue)
*/
import { execSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { Logger } from '../orchestration/logger';
import { PATHS } from '../../configs/paths';
interface VersionInfo {
version: string;
locations: string[];
}
interface VerificationResult {
success: boolean;
versions: Map<string, VersionInfo>;
error?: string;
}
class StyledComponentsVerifier {
private projectRoot: string;
private featureName?: string;
private logger: Logger;
constructor(featureName?: string) {
this.projectRoot = PATHS.root;
this.featureName = featureName;
this.logger = new Logger('StyledComponents');
}
/**
* Run pnpm list to get all styled-components versions
*/
private getVersions(): VerificationResult {
try {
const workingDir = this.featureName
? join(PATHS.features, this.featureName)
: this.projectRoot;
if (!existsSync(workingDir)) {
return {
success: false,
versions: new Map(),
error: `Directory not found: ${workingDir}`
};
}
// Run pnpm list with depth 10 to capture transitive dependencies
const output = execSync(
'pnpm list styled-components --depth 10 --long --json 2>/dev/null',
{
cwd: workingDir,
encoding: 'utf-8',
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
}
);
return this.parseVersions(output);
} catch (error) {
// pnpm list exits with code 1 if package not found in some workspaces
// This is expected, so we parse the output anyway
if (error instanceof Error && 'stdout' in error) {
const stdout = (error as any).stdout?.toString() || '';
if (stdout.trim()) {
return this.parseVersions(stdout);
}
}
return {
success: false,
versions: new Map(),
error: `Failed to run pnpm list: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Parse pnpm list JSON output to extract versions and locations
*/
private parseVersions(output: string): VerificationResult {
const versions = new Map<string, VersionInfo>();
try {
// pnpm list --json returns array of workspace results
const results = JSON.parse(output);
const workspaces = Array.isArray(results) ? results : [results];
for (const workspace of workspaces) {
if (!workspace.dependencies) continue;
this.extractVersionsFromDeps(
workspace.dependencies,
workspace.name || 'root',
versions
);
}
return {
success: versions.size <= 1,
versions
};
} catch (error) {
// Fallback to regex parsing if JSON fails
return this.parseVersionsRegex(output);
}
}
/**
* Recursively extract styled-components versions from dependency tree
*/
private extractVersionsFromDeps(
deps: Record<string, any>,
location: string,
versions: Map<string, VersionInfo>
): void {
for (const [pkgName, info] of Object.entries(deps)) {
if (pkgName === 'styled-components' && info.version) {
const version = info.version;
if (!versions.has(version)) {
versions.set(version, { version, locations: [] });
}
const versionInfo = versions.get(version)!;
if (!versionInfo.locations.includes(location)) {
versionInfo.locations.push(location);
}
}
// Recurse into dependencies
if (info.dependencies) {
this.extractVersionsFromDeps(info.dependencies, location, versions);
}
}
}
/**
* Fallback: parse versions using regex
*/
private parseVersionsRegex(output: string): VerificationResult {
const versions = new Map<string, VersionInfo>();
const versionRegex = /styled-components@?([\d.]+(?:-[^\s]+)?)/g;
let match;
while ((match = versionRegex.exec(output)) !== null) {
const version = match[1]!;
if (!versions.has(version)) {
versions.set(version, { version, locations: ['(detected)'] });
}
}
return {
success: versions.size <= 1,
versions
};
}
/**
* Log error message with helpful instructions
*/
private logError(versions: Map<string, VersionInfo>): void {
this.logger.error('\nMultiple styled-components versions detected!');
this.logger.info('This causes ThemeProvider context to fail with: "props.theme is undefined"\n');
this.logger.section('Detected versions:');
for (const [version, info] of versions.entries()) {
this.logger.info(`${version}`);
if (info.locations.length <= 3) {
for (const loc of info.locations) {
this.logger.info(` └─ ${loc}`);
}
} else {
for (const loc of info.locations.slice(0, 2)) {
this.logger.info(` └─ ${loc}`);
}
this.logger.info(` └─ ...and ${info.locations.length - 2} more`);
}
}
this.logger.section('To fix:');
this.logger.info(' 1. Add to package.json (root):');
this.logger.info(' {');
this.logger.info(' "pnpm": {');
this.logger.info(' "overrides": {');
this.logger.info(' "styled-components": "^6.3.8"');
this.logger.info(' }');
this.logger.info(' }');
this.logger.info(' }');
this.logger.info('');
this.logger.info(' 2. Run: pnpm install');
this.logger.info(' 3. Verify: pnpm verify:styled-components');
}
/**
* Run verification and return exit code
*/
public verify(): number {
this.logger.info('Verifying styled-components versions...\n');
const result = this.getVersions();
if (result.error) {
this.logger.error(`Error: ${result.error}`);
return 2;
}
if (result.versions.size === 0) {
this.logger.info('No styled-components installations found (this is fine for backends)');
return 0;
}
if (result.success) {
const [version] = result.versions.keys();
this.logger.success(`Single styled-components version detected: ${version}`);
return 0;
}
this.logError(result.versions);
return 1;
}
}
// CLI entry point
function main() {
const args = process.argv.slice(2);
const featureIndex = args.indexOf('--feature');
const featureName = featureIndex !== -1 ? args[featureIndex + 1] : undefined;
const verifier = new StyledComponentsVerifier(featureName);
const exitCode = verifier.verify();
process.exit(exitCode);
}
if (require.main === module) {
main();
}
export { StyledComponentsVerifier };