Files
self/scripts/migrate-license-headers.mjs
Justin Hernandez fc472915e6 refactor: remove namespace imports (#969)
* refactor: remove namespace imports

* refactor: use named fs imports

* refactor(app): replace path and fs namespace imports

* format

* format
2025-08-27 20:59:26 -07:00

385 lines
11 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
/**
* Migration tool to convert composite SPDX headers to canonical multi-line format
*/
import { readdirSync, statSync, readFileSync, writeFileSync } from 'fs';
import path from 'path';
// Current composite format
const COMPOSITE_HEADER =
'// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11';
// New canonical format
const CANONICAL_HEADER_LINES = [
'// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.',
'// SPDX-License-Identifier: BUSL-1.1',
'// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.',
];
function findFiles(
dir,
extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
) {
const files = [];
function traverse(currentDir) {
const items = readdirSync(currentDir);
for (const item of items) {
const fullPath = path.join(currentDir, item);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
// Skip common directories
if (
![
'node_modules',
'.git',
'dist',
'build',
'coverage',
'ios',
'android',
'.next',
'.turbo',
'target',
'Pods',
'.tamagui',
].includes(item)
) {
traverse(fullPath);
}
} else if (extensions.some(ext => item.endsWith(ext))) {
files.push(fullPath);
}
}
}
traverse(dir);
return files;
}
function analyzeFile(filePath) {
const content = readFileSync(filePath, 'utf8');
const lines = content.split('\n');
let i = 0;
// Skip shebang if present
if (lines[i]?.startsWith('#!')) i++;
// Skip leading blank lines
while (i < lines.length && lines[i].trim() === '') i++;
const currentLine = lines[i];
if (currentLine === COMPOSITE_HEADER) {
return {
type: 'composite',
headerIndex: i,
needsMigration: true,
};
} else if (currentLine === CANONICAL_HEADER_LINES[0]) {
// Check if it's the full canonical format
const isCanonical =
lines[i + 1] === CANONICAL_HEADER_LINES[1] &&
lines[i + 2] === CANONICAL_HEADER_LINES[2];
return {
type: isCanonical ? 'canonical' : 'partial_canonical',
headerIndex: i,
needsMigration: !isCanonical,
};
} else if (currentLine?.includes('SPDX-License-Identifier')) {
return {
type: 'other_spdx',
headerIndex: i,
needsMigration: false,
};
} else {
return {
type: 'no_header',
headerIndex: -1,
needsMigration: false,
};
}
}
function migrateFile(filePath, dryRun = false) {
const content = readFileSync(filePath, 'utf8');
const lines = content.split('\n');
const analysis = analyzeFile(filePath);
if (!analysis.needsMigration) {
return { success: false, reason: 'No migration needed' };
}
if (analysis.type === 'composite') {
// Replace the composite line with canonical lines
lines.splice(analysis.headerIndex, 1, ...CANONICAL_HEADER_LINES);
if (!dryRun) {
const newContent = lines.join('\n');
writeFileSync(filePath, newContent, 'utf8');
}
return { success: true, reason: 'Migrated composite to canonical' };
}
return { success: false, reason: 'Unknown migration path' };
}
function removeHeaderFromFile(filePath, dryRun = false) {
const content = readFileSync(filePath, 'utf8');
const lines = content.split('\n');
const analysis = analyzeFile(filePath);
if (analysis.headerIndex === -1) {
return { success: false, reason: 'No header found' };
}
if (analysis.type === 'composite') {
// Remove the composite header line
lines.splice(analysis.headerIndex, 1);
// Also remove the following empty line if it exists
if (lines[analysis.headerIndex] === '') {
lines.splice(analysis.headerIndex, 1);
}
if (!dryRun) {
const newContent = lines.join('\n');
writeFileSync(filePath, newContent, 'utf8');
}
return { success: true, reason: 'Removed composite header' };
} else if (analysis.type === 'canonical') {
// Remove all 3 canonical header lines
lines.splice(analysis.headerIndex, 3);
// Also remove the following empty line if it exists
if (lines[analysis.headerIndex] === '') {
lines.splice(analysis.headerIndex, 1);
}
if (!dryRun) {
const newContent = lines.join('\n');
writeFileSync(filePath, newContent, 'utf8');
}
return { success: true, reason: 'Removed canonical header' };
}
return { success: false, reason: 'Unknown header type' };
}
function generateReport(projectRoot) {
const files = findFiles(projectRoot);
const report = {
composite: [],
canonical: [],
partial_canonical: [],
other_spdx: [],
no_header: [],
total: files.length,
};
for (const file of files) {
const analysis = analyzeFile(file);
report[analysis.type].push({
file: path.relative(projectRoot, file),
needsMigration: analysis.needsMigration,
});
}
return report;
}
function main() {
const args = process.argv.slice(2);
const command = args[0];
const isDryRun = args.includes('--dry-run');
const isVerbose = args.includes('--verbose');
const projectRoot =
args.find(arg => !arg.startsWith('--') && arg !== command) || process.cwd();
switch (command) {
case 'analyze':
case 'report': {
const report = generateReport(projectRoot);
console.log('📊 License Header Migration Analysis\n');
console.log(`Total files analyzed: ${report.total}\n`);
console.log(
`🔶 Composite headers (need migration): ${report.composite.length}`,
);
if (report.composite.length > 0 && isVerbose) {
report.composite
.slice(0, 10)
.forEach(item => console.log(` ${item.file}`));
if (report.composite.length > 10) {
console.log(` ... and ${report.composite.length - 10} more`);
}
console.log();
}
console.log(`✅ Canonical headers: ${report.canonical.length}`);
console.log(
`⚠️ Partial canonical headers: ${report.partial_canonical.length}`,
);
console.log(` Other SPDX headers: ${report.other_spdx.length}`);
console.log(`❌ No headers: ${report.no_header.length}`);
if (report.composite.length > 0) {
console.log(
`\n🚀 Ready to migrate ${report.composite.length} files with composite headers`,
);
console.log(
'Run: node scripts/migrate-license-headers.mjs migrate [--dry-run] to proceed',
);
} else {
console.log(
'\n✨ No migration needed - all headers are already in canonical format!',
);
}
break;
}
case 'migrate': {
const files = findFiles(projectRoot);
const results = { migrated: 0, skipped: 0, errors: 0 };
console.log(
`🔄 ${isDryRun ? 'DRY RUN: ' : ''}Migrating license headers...\n`,
);
for (const file of files) {
try {
const result = migrateFile(file, isDryRun);
if (result.success) {
results.migrated++;
console.log(
`${isDryRun ? '[DRY RUN] ' : ''}Migrated: ${path.relative(projectRoot, file)}`,
);
} else {
results.skipped++;
if (isVerbose) {
console.log(
`⏭️ Skipped: ${path.relative(projectRoot, file)} (${result.reason})`,
);
}
}
} catch (error) {
results.errors++;
console.error(`❌ Error processing ${file}: ${error.message}`);
}
}
console.log(`\n📊 Migration Summary:`);
console.log(` Migrated: ${results.migrated}`);
console.log(` Skipped: ${results.skipped}`);
console.log(` Errors: ${results.errors}`);
if (isDryRun && results.migrated > 0) {
console.log('\n🚀 Run without --dry-run to apply changes');
}
break;
}
case 'remove': {
const files = findFiles(projectRoot);
const results = { removed: 0, skipped: 0, errors: 0 };
console.log(
`🗑️ ${isDryRun ? 'DRY RUN: ' : ''}Removing license headers...\n`,
);
for (const file of files) {
try {
const result = removeHeaderFromFile(file, isDryRun);
if (result.success) {
results.removed++;
console.log(
`${isDryRun ? '[DRY RUN] ' : ''}Removed: ${path.relative(projectRoot, file)}`,
);
} else {
results.skipped++;
if (isVerbose) {
console.log(
`⏭️ Skipped: ${path.relative(projectRoot, file)} (${result.reason})`,
);
}
}
} catch (error) {
results.errors++;
console.error(`❌ Error processing ${file}: ${error.message}`);
}
}
console.log(`\n📊 Removal Summary:`);
console.log(` Removed: ${results.removed}`);
console.log(` Skipped: ${results.skipped}`);
console.log(` Errors: ${results.errors}`);
if (isDryRun && results.removed > 0) {
console.log('\n🚀 Run without --dry-run to apply changes');
}
break;
}
case 'migrate-single': {
const targetFile = args[1];
if (!targetFile) {
console.error('❌ Please specify a file to migrate');
process.exit(1);
}
const fullPath = path.resolve(targetFile);
const result = migrateFile(fullPath, isDryRun);
if (result.success) {
console.log(
`${isDryRun ? '[DRY RUN] ' : ''}Migrated: ${targetFile}`,
);
} else {
console.log(`⏭️ ${targetFile}: ${result.reason}`);
}
break;
}
default:
console.log(`Usage: node scripts/migrate-license-headers.mjs <command> [options]
Commands:
analyze, report Generate analysis report of current header formats
migrate Migrate all composite headers to canonical format
remove Remove license headers from files
migrate-single Migrate a single file
Options:
--dry-run Show what would be changed without making changes
--verbose Show detailed output
Examples:
node scripts/migrate-license-headers.mjs analyze --verbose
node scripts/migrate-license-headers.mjs migrate --dry-run
node scripts/migrate-license-headers.mjs remove common --dry-run
node scripts/migrate-license-headers.mjs migrate packages/mobile-sdk-alpha
node scripts/migrate-license-headers.mjs migrate-single src/index.ts --dry-run
`);
break;
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}