Files
self/scripts/check-license-headers.mjs
Justin Hernandez 03635abaaf chore: add kmp license headers; update license year range (#1752)
* add kmp license headers and update year

* formatting
2026-02-15 16:56:06 -08:00

275 lines
7.3 KiB
JavaScript

#!/usr/bin/env node
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
/**
* Script to check and fix license header formatting
* Ensures there's a newline after license headers
*/
import { readdirSync, statSync, readFileSync, writeFileSync } from 'fs';
import path from 'path';
// Legacy composite format (being phased out)
const LEGACY_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';
// Canonical multi-line format (preferred)
const CANONICAL_HEADER_LINES = [
'// SPDX-FileCopyrightText: 2025-2026 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', '.kt', '.swift'],
) {
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 node_modules, .git, and other common directories
if (
![
'node_modules',
'.git',
'dist',
'build',
'coverage',
'ios',
'android',
'.next',
'.turbo',
'.tamagui',
'DerivedData',
'Pods',
'.gradle',
'vendor',
].includes(item)
) {
traverse(fullPath);
}
} else if (extensions.some(ext => item.endsWith(ext))) {
files.push(fullPath);
}
}
}
traverse(dir);
return files;
}
function findLicenseHeaderIndex(lines) {
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];
// Check for legacy composite format
if (currentLine === LEGACY_HEADER) {
return { index: i, type: 'legacy', valid: true, endIndex: i };
}
// Check for canonical multi-line format (current or previous year)
const isCurrentHeader = currentLine === CANONICAL_HEADER_LINES[0];
const isPreviousYearHeader =
currentLine === '// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.';
if (isCurrentHeader || isPreviousYearHeader) {
const hasAllLines =
lines[i + 1] === CANONICAL_HEADER_LINES[1] &&
lines[i + 2] === CANONICAL_HEADER_LINES[2];
return {
index: i,
type: 'canonical',
valid: hasAllLines,
needsYearUpdate: isPreviousYearHeader && hasAllLines,
endIndex: hasAllLines ? i + 2 : i,
};
}
return { index: -1, type: 'none', valid: false };
}
function shouldRequireHeader(filePath, projectRoot) {
const relativePath = path.relative(projectRoot, filePath);
return (
relativePath.startsWith('app/') ||
relativePath.startsWith('packages/mobile-sdk-alpha/') ||
relativePath.startsWith('packages/kmp-test-app/') ||
relativePath.startsWith('packages/kmp-sdk/')
);
}
function checkLicenseHeader(
filePath,
{ requireHeader = false, projectRoot = process.cwd() } = {},
) {
const content = readFileSync(filePath, 'utf8');
const lines = content.split('\n');
const headerInfo = findLicenseHeaderIndex(lines);
const shouldHaveHeader =
requireHeader || shouldRequireHeader(filePath, projectRoot);
if (headerInfo.index === -1) {
if (shouldHaveHeader) {
return {
file: filePath,
issue: 'Missing or incorrect license header',
fixed: false,
};
}
return null;
}
if (!headerInfo.valid) {
return {
file: filePath,
issue: 'Incomplete or malformed license header',
fixed: false,
};
}
if (headerInfo.needsYearUpdate) {
return {
file: filePath,
issue: 'Copyright year needs updating to 2025-2026',
fixed: false,
};
}
// Check if there's a newline after the license header
const headerEndIndex = headerInfo.endIndex;
if (lines[headerEndIndex + 1] !== '') {
return {
file: filePath,
issue: 'Missing newline after license header',
fixed: false,
};
}
return null;
}
function fixLicenseHeader(filePath) {
const content = readFileSync(filePath, 'utf8');
const lines = content.split('\n');
const headerInfo = findLicenseHeaderIndex(lines);
if (headerInfo.index === -1) {
// No header exists - add the canonical header
const newLines = [
...CANONICAL_HEADER_LINES,
'', // Add newline after header
...lines,
];
const fixedContent = newLines.join('\n');
writeFileSync(filePath, fixedContent, 'utf8');
return true;
}
if (headerInfo.valid) {
// Update copyright year if needed
if (headerInfo.needsYearUpdate) {
lines[headerInfo.index] = CANONICAL_HEADER_LINES[0];
const fixedContent = lines.join('\n');
writeFileSync(filePath, fixedContent, 'utf8');
return true;
}
const headerEndIndex = headerInfo.endIndex;
if (lines[headerEndIndex + 1] !== '') {
// Insert empty line after license header
lines.splice(headerEndIndex + 1, 0, '');
const fixedContent = lines.join('\n');
writeFileSync(filePath, fixedContent, 'utf8');
return true;
}
}
return false;
}
function main() {
const args = process.argv.slice(2);
const isFix = args.includes('--fix');
const isCheck = args.includes('--check') || !isFix;
const requireHeader = args.includes('--require');
// Get all directory arguments (non-flag arguments)
const dirArgs = args.filter(arg => !arg.startsWith('--'));
const projectRoots =
dirArgs.length > 0
? dirArgs.map(dir => path.resolve(dir))
: [process.cwd()];
// Collect files from all directories
const files = [];
for (const projectRoot of projectRoots) {
files.push(...findFiles(projectRoot));
}
const issues = [];
for (const file of files) {
const issue = checkLicenseHeader(file, {
requireHeader,
projectRoot: process.cwd(),
});
if (issue) {
issues.push(issue);
if (isFix) {
const fixed = fixLicenseHeader(file);
if (fixed) {
issue.fixed = true;
console.log(`✅ Fixed: ${file}`);
}
}
}
}
if (isCheck) {
// Show which directories require headers
const requiredDirs = [
'app/',
'packages/mobile-sdk-alpha/',
'packages/kmp-test-app/',
'packages/kmp-sdk/',
];
console.log(`📋 License headers required in: ${requiredDirs.join(', ')}`);
if (issues.length === 0) {
console.log('✅ All license headers are properly formatted');
} else {
console.log(
`❌ Found ${issues.length} files with license header issues:`,
);
for (const issue of issues) {
console.log(` - ${issue.file}: ${issue.issue}`);
}
console.log('\nRun with --fix to automatically fix these issues');
process.exit(1);
}
} else if (isFix) {
const fixedCount = issues.filter(issue => issue.fixed).length;
console.log(`\n✅ Fixed ${fixedCount} files`);
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}