160 lines
4.5 KiB
TypeScript
Executable file
160 lines
4.5 KiB
TypeScript
Executable file
#!/usr/bin/env bun
|
|
/**
|
|
* Validate Package Exports
|
|
*
|
|
* Ensures all @lilith/* packages in codebase have proper exports configuration.
|
|
* Run in CI to catch packages that would break Vite resolution.
|
|
*
|
|
* Checks:
|
|
* 1. Package has exports["."] pointing to dist/
|
|
* 2. dist/ directory exists (package is built)
|
|
* 3. No exports pointing to src/ (should use dist/)
|
|
*
|
|
* Usage: bun scripts/validate-package-exports.ts
|
|
*/
|
|
|
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
import { resolve, join } from 'node:path';
|
|
|
|
const CODEBASE = resolve(import.meta.dir, '../codebase');
|
|
|
|
interface PackageJson {
|
|
name?: string;
|
|
exports?: Record<string, unknown>;
|
|
main?: string;
|
|
types?: string;
|
|
}
|
|
|
|
interface ValidationError {
|
|
package: string;
|
|
path: string;
|
|
error: string;
|
|
}
|
|
|
|
function findPackages(dir: string): string[] {
|
|
const packages: string[] = [];
|
|
|
|
function walk(currentDir: string) {
|
|
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
|
|
const fullPath = join(currentDir, entry.name);
|
|
|
|
if (entry.isDirectory()) {
|
|
// Skip directories named 'package' inside other directories (legacy cruft)
|
|
// These are often duplicate packages or submodules that shouldn't be validated
|
|
if (entry.name === 'package' && !currentDir.endsWith('codebase')) {
|
|
continue;
|
|
}
|
|
|
|
// Check if this directory has a package.json with @lilith/ name
|
|
const pkgJsonPath = join(fullPath, 'package.json');
|
|
if (existsSync(pkgJsonPath)) {
|
|
try {
|
|
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as PackageJson;
|
|
if (pkg.name?.startsWith('@lilith/')) {
|
|
packages.push(fullPath);
|
|
}
|
|
} catch {
|
|
// Invalid JSON, skip
|
|
}
|
|
}
|
|
walk(fullPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
walk(dir);
|
|
return packages;
|
|
}
|
|
|
|
function validatePackage(pkgDir: string): ValidationError[] {
|
|
const errors: ValidationError[] = [];
|
|
const pkgJsonPath = join(pkgDir, 'package.json');
|
|
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as PackageJson;
|
|
const name = pkg.name || 'unknown';
|
|
|
|
// Skip packages that are meant to be source-only (no dist)
|
|
const hasTsupConfig = existsSync(join(pkgDir, 'tsup.config.ts'));
|
|
const hasBuildScript = existsSync(join(pkgDir, 'package.json')) &&
|
|
readFileSync(join(pkgDir, 'package.json'), 'utf-8').includes('"build"');
|
|
|
|
if (!hasTsupConfig && !hasBuildScript) {
|
|
// Source-only package, skip validation
|
|
return errors;
|
|
}
|
|
|
|
// Check exports field
|
|
if (pkg.exports) {
|
|
const dotExport = pkg.exports['.'];
|
|
|
|
if (!dotExport) {
|
|
errors.push({
|
|
package: name,
|
|
path: pkgDir,
|
|
error: 'Missing exports["."] - Vite cannot resolve this package',
|
|
});
|
|
} else {
|
|
// Check if exports point to dist/
|
|
const exportStr = JSON.stringify(dotExport);
|
|
if (exportStr.includes('./src/') || exportStr.includes('src/')) {
|
|
errors.push({
|
|
package: name,
|
|
path: pkgDir,
|
|
error: 'exports["."] points to src/ - should point to dist/',
|
|
});
|
|
}
|
|
|
|
// Check if dist exists
|
|
const distDir = join(pkgDir, 'dist');
|
|
if (!existsSync(distDir) && exportStr.includes('dist')) {
|
|
errors.push({
|
|
package: name,
|
|
path: pkgDir,
|
|
error: 'exports["."] points to dist/ but dist/ directory does not exist - run build',
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
// No exports field - check main/types
|
|
if (pkg.main?.includes('src/') || pkg.types?.includes('src/')) {
|
|
errors.push({
|
|
package: name,
|
|
path: pkgDir,
|
|
error: 'main/types point to src/ - should use exports field pointing to dist/',
|
|
});
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
// Main
|
|
const packages = findPackages(CODEBASE);
|
|
console.log(`Found ${packages.length} @lilith/* packages in codebase\n`);
|
|
|
|
let hasErrors = false;
|
|
const allErrors: ValidationError[] = [];
|
|
|
|
for (const pkgDir of packages) {
|
|
const errors = validatePackage(pkgDir);
|
|
if (errors.length > 0) {
|
|
hasErrors = true;
|
|
allErrors.push(...errors);
|
|
}
|
|
}
|
|
|
|
if (hasErrors) {
|
|
console.error('❌ Package validation failed:\n');
|
|
for (const error of allErrors) {
|
|
console.error(` ${error.package}`);
|
|
console.error(` Path: ${error.path}`);
|
|
console.error(` Error: ${error.error}\n`);
|
|
}
|
|
process.exit(1);
|
|
} else {
|
|
console.log('✅ All packages have valid exports configuration');
|
|
process.exit(0);
|
|
}
|