diff --git a/.oxlintrc.json b/.oxlintrc.json index 90fce46..1732b44 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -29,6 +29,7 @@ "eslint/no-eq-null": "off", "eslint/no-magic-numbers": "warn", "eslint/no-ternary": "off", + "eslint/no-nested-ternary": "off", "eslint/no-undefined": "off", "eslint/no-void": "off", "eslint/no-warning-comments": ["warn", { "terms": ["TODO"] }], diff --git a/mise.toml b/mise.toml index d213465..e806049 100644 --- a/mise.toml +++ b/mise.toml @@ -46,6 +46,13 @@ run = "hongdown --check" description = "Check mise.toml formatting" run = "mise fmt --check" +[tasks."check:license"] +description = "Check the license comments of files in the whole codebase." +run = """ +#!/usr/bin/env nu +node scripts/add-license/main.mts --check +""" + [tasks."check:versions"] description = "Check that all package versions are in sync" run = "node scripts/check-versions.mts" @@ -63,6 +70,13 @@ run = "hongdown --write" description = "Format mise.toml" run = "mise fmt" +[tasks."fmt:license"] +description = "Add license comments to the files that don't have them." +run = """ +#!/usr/bin/env nu +node scripts/add-license/main.mts +""" + [tasks.build] description = "Build the project" run = "pnpm run --parallel --recursive build" diff --git a/scripts/add-license/gpl.mts b/scripts/add-license/gpl.mts new file mode 100644 index 0000000..12f8463 --- /dev/null +++ b/scripts/add-license/gpl.mts @@ -0,0 +1,41 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// 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 . + +import { readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const GPL_TXT_PATH = join(dirname(fileURLToPath(import.meta.url)), "gpl.txt"); +const GPL = (await readFile(GPL_TXT_PATH, { encoding: "utf-8" })) + .trimEnd() + .split("\n"); +const JS_GPL = GPL.map((line) => (line === "" ? "//" : `// ${line}`)).join( + "\n", +); +const CSS_GPL = ["/*", ...GPL, "*/"].join("\n"); +const GPL_BY_EXT = { + ts: JS_GPL, + tsx: JS_GPL, + mts: JS_GPL, + cts: JS_GPL, + js: JS_GPL, + jsx: JS_GPL, + mjs: JS_GPL, + cjs: JS_GPL, + css: CSS_GPL, +}; + +export default GPL_BY_EXT; diff --git a/scripts/add-license/gpl.txt b/scripts/add-license/gpl.txt new file mode 100644 index 0000000..3521ade --- /dev/null +++ b/scripts/add-license/gpl.txt @@ -0,0 +1,15 @@ +DrFed: A web-based platform for developing and debugging ActivityPub apps +Copyright (C) 2026 DrFed team + +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 . diff --git a/scripts/add-license/main.mts b/scripts/add-license/main.mts new file mode 100644 index 0000000..1946ae2 --- /dev/null +++ b/scripts/add-license/main.mts @@ -0,0 +1,131 @@ +// DrFed: A web-based platform for developing and debugging ActivityPub apps +// Copyright (C) 2026 DrFed team +// +// 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 . + +// oxlint-disable no-console no-magic-numbers +import { glob, readFile, writeFile } from "node:fs/promises"; +import { dirname, extname, join, relative } from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +import GPL from "./gpl.mts"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); +const INCLUDED = ["scripts/", "packages/*/src/", "packages/*/bin/"]; +const EXCLUDED = ["**/dist/"]; + +type Extension = keyof typeof GPL; + +interface AddLicenseProps { + check?: true | undefined; +} + +interface LicenseTarget { + readonly path: string; + readonly ext: Extension; +} + +interface LicenseCheck extends LicenseTarget { + readonly hasLicense: boolean; +} + +export default async function addLicense(opt: AddLicenseProps) { + const files = await findTargets().then(filterLicense); + if (files.length === 0) { + process.exit(0); + } + if (opt.check) { + console.warn("Licenses are missing from some files:"); + listFiles(files); + process.exit(1); + } + console.warn("Added licenses to the files that were missing them:"); + listFiles(files); + + await Promise.all(files.map(addLicenseHeader)); +} + +const findTargets = async (): Promise => + (await Array.fromAsync(glob(INCLUDED.map(addAsteriskIfDir), GLOB_OPTION))) + .filter((entry) => entry.isFile()) + .map((entry) => join(entry.parentPath, entry.name)) + .map((path) => ({ path, ext: extensionOf(path) })) + .filter(isLicenseTarget); + +const GLOB_OPTION = { + cwd: ROOT, + exclude: EXCLUDED, + withFileTypes: true, +} as const; + +const addAsteriskIfDir = (dir: string) => + dir.endsWith("/") ? `${dir}**/*` : dir; + +const filterLicense = async (files: LicenseTarget[]): Promise => + (await Array.fromAsync(files, checkLicense)).filter( + (check) => !check.hasLicense, + ); + +const extensionOf = (path: string): string => extname(path).slice(1); + +const isLicenseTarget = ( + file: Readonly<{ path: string; ext: string }>, +): file is LicenseTarget => Object.hasOwn(GPL, file.ext); + +async function checkLicense(target: LicenseTarget): Promise { + const header = GPL[target.ext]; + const lines = await readLines(target.path); + const offset = shebangOffset(lines); + const headerLineCount = header.split("\n").length; + const actualHeader = lines.slice(offset, offset + headerLineCount).join("\n"); + return { ...target, hasLicense: actualHeader === header }; +} + +async function addLicenseHeader(target: LicenseTarget): Promise { + const added = updateLines(GPL[target.ext], await readLines(target.path)); + await writeFile(target.path, added, { encoding: "utf-8" }); +} + +const readLines = async (path: string): Promise => + (await readFile(path, { encoding: "utf-8" })).split("\n"); + +const shebangOffset = (lines: readonly string[]): number => + lines[0]?.startsWith("#!") ? (lines[1]?.trim() === "" ? 2 : 1) : 0; + +const updateLines = (header: string, lines: string[]): string => + Array.from(genLines(header, lines)).join("\n"); + +function* genLines(header: string, lines: string[]): Generator { + const offset = shebangOffset(lines); + yield* lines.slice(0, offset); + yield header; + const next = lines.at(offset); + if (next != null && next.trim() !== "") { + yield ""; + } + yield* lines.slice(offset); +} + +function listFiles(files: LicenseTarget[]) { + for (const file of files) { + console.warn(` - ${relative(ROOT, file.path)}`); + } +} + +if (import.meta.main) { + await addLicense({ + check: process.argv.includes("--check") ? true : undefined, + }); +}