Add a Bun script to apply and normalize AGPL copyright headers across the default frontend source files. The script keeps headers idempotent, upgrades existing headers to the 2023-2026 QuantumNous range, and is exposed through `bun run copyright` for future maintenance.
244 lines
6.1 KiB
JavaScript
Vendored
244 lines
6.1 KiB
JavaScript
Vendored
/*
|
|
Copyright (C) 2023-2026 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
import fs from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
|
|
const TARGET_DIRS = ['src', 'scripts']
|
|
const SOURCE_EXTENSIONS = new Set([
|
|
'.cjs',
|
|
'.css',
|
|
'.js',
|
|
'.jsx',
|
|
'.mjs',
|
|
'.scss',
|
|
'.ts',
|
|
'.tsx',
|
|
])
|
|
const EXCLUDED_DIRS = new Set([
|
|
'.git',
|
|
'.rsbuild',
|
|
'.turbo',
|
|
'build',
|
|
'coverage',
|
|
'dist',
|
|
'node_modules',
|
|
])
|
|
const GENERATED_FILE_MARKERS = [
|
|
'This file was automatically generated',
|
|
'This file is auto-generated',
|
|
'This file is generated',
|
|
'DO NOT EDIT',
|
|
'You should NOT make any changes in this file',
|
|
]
|
|
|
|
const COPYRIGHT_HEADER = `/*
|
|
Copyright (C) 2023-2026 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
`
|
|
|
|
const PROJECT_COPYRIGHT_BLOCK_PATTERN =
|
|
/^\/\*\r?\nCopyright \(C\) .+? QuantumNous\r?\n[\s\S]*?For commercial licensing, please contact support@quantumnous\.com\r?\n\*\/\r?\n?/
|
|
const THIRD_PARTY_COPYRIGHT_PATTERN =
|
|
/^\/\*[\s\S]*?Copyright[\s\S]*?\*\/\r?\n?/i
|
|
|
|
const checkMode = process.argv.includes('--check')
|
|
|
|
function isGeneratedFile(filePath) {
|
|
return path.basename(filePath).includes('.gen.')
|
|
}
|
|
|
|
function hasGeneratedMarker(text) {
|
|
return GENERATED_FILE_MARKERS.some((marker) => text.includes(marker))
|
|
}
|
|
|
|
function hasThirdPartyCopyright(text) {
|
|
return (
|
|
THIRD_PARTY_COPYRIGHT_PATTERN.test(text) &&
|
|
!PROJECT_COPYRIGHT_BLOCK_PATTERN.test(text)
|
|
)
|
|
}
|
|
|
|
async function collectSourceFiles(dir) {
|
|
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
const files = []
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dir, entry.name)
|
|
|
|
if (entry.isDirectory()) {
|
|
if (!EXCLUDED_DIRS.has(entry.name)) {
|
|
files.push(...(await collectSourceFiles(fullPath)))
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (
|
|
entry.isFile() &&
|
|
SOURCE_EXTENSIONS.has(path.extname(entry.name)) &&
|
|
!isGeneratedFile(fullPath)
|
|
) {
|
|
files.push(fullPath)
|
|
}
|
|
}
|
|
|
|
return files
|
|
}
|
|
|
|
async function collectTargetFiles(rootDir) {
|
|
const files = []
|
|
|
|
for (const targetDir of TARGET_DIRS) {
|
|
const fullPath = path.join(rootDir, targetDir)
|
|
|
|
try {
|
|
const stat = await fs.stat(fullPath)
|
|
if (stat.isDirectory()) {
|
|
files.push(...(await collectSourceFiles(fullPath)))
|
|
}
|
|
} catch (error) {
|
|
if (error.code !== 'ENOENT') {
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
return files.sort()
|
|
}
|
|
|
|
function splitShebang(text) {
|
|
if (!text.startsWith('#!')) {
|
|
return ['', text]
|
|
}
|
|
|
|
const lineEnd = text.indexOf('\n')
|
|
if (lineEnd === -1) {
|
|
return [text, '']
|
|
}
|
|
|
|
return [text.slice(0, lineEnd + 1), text.slice(lineEnd + 1)]
|
|
}
|
|
|
|
function applyHeader(text) {
|
|
const newline = text.includes('\r\n') ? '\r\n' : '\n'
|
|
const header = COPYRIGHT_HEADER.replaceAll('\n', newline)
|
|
const [shebang, body] = splitShebang(text)
|
|
const hadHeader = PROJECT_COPYRIGHT_BLOCK_PATTERN.test(body)
|
|
const strippedBody = body
|
|
.replace(PROJECT_COPYRIGHT_BLOCK_PATTERN, '')
|
|
.replace(/^(?:\r?\n)+/, '')
|
|
|
|
if (strippedBody.length === 0) {
|
|
return {
|
|
action: hadHeader ? 'updated' : 'added',
|
|
text: shebang + header,
|
|
}
|
|
}
|
|
|
|
return {
|
|
action: hadHeader ? 'updated' : 'added',
|
|
text: shebang + header + strippedBody,
|
|
}
|
|
}
|
|
|
|
function formatPath(rootDir, filePath) {
|
|
return path.relative(rootDir, filePath).replaceAll(path.sep, '/')
|
|
}
|
|
|
|
async function main() {
|
|
const rootDir = process.cwd()
|
|
const sourceFiles = await collectTargetFiles(rootDir)
|
|
const stats = {
|
|
added: 0,
|
|
checked: 0,
|
|
skippedGenerated: 0,
|
|
skippedThirdParty: 0,
|
|
updated: 0,
|
|
}
|
|
const pendingFiles = []
|
|
|
|
for (const file of sourceFiles) {
|
|
stats.checked += 1
|
|
|
|
const originalText = await fs.readFile(file, 'utf8')
|
|
const bom = originalText.startsWith('\uFEFF') ? '\uFEFF' : ''
|
|
const text = bom ? originalText.slice(1) : originalText
|
|
const [, body] = splitShebang(text)
|
|
|
|
if (hasGeneratedMarker(body)) {
|
|
stats.skippedGenerated += 1
|
|
continue
|
|
}
|
|
|
|
if (hasThirdPartyCopyright(body)) {
|
|
stats.skippedThirdParty += 1
|
|
continue
|
|
}
|
|
|
|
const result = applyHeader(text)
|
|
const nextText = bom + result.text
|
|
|
|
if (nextText !== originalText) {
|
|
stats[result.action] += 1
|
|
pendingFiles.push(formatPath(rootDir, file))
|
|
|
|
if (!checkMode) {
|
|
await fs.writeFile(file, nextText)
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
[
|
|
`copyright: checked ${stats.checked}`,
|
|
`added ${stats.added}`,
|
|
`updated ${stats.updated}`,
|
|
`skipped generated ${stats.skippedGenerated}`,
|
|
`skipped third-party ${stats.skippedThirdParty}`,
|
|
].join(', ')
|
|
)
|
|
|
|
if (checkMode && pendingFiles.length > 0) {
|
|
console.error('copyright: headers need to be updated in:')
|
|
for (const file of pendingFiles) {
|
|
console.error(`- ${file}`)
|
|
}
|
|
process.exitCode = 1
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error)
|
|
process.exitCode = 1
|
|
})
|