257 lines
7.1 KiB
TypeScript
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 };
|