(feat): add custom lint rules
(qol): update todo (chore): update licenses
This commit is contained in:
parent
bf8f449f03
commit
17db895203
28 changed files with 585 additions and 513 deletions
45
utils/scripts/build-packages.ts
Normal file
45
utils/scripts/build-packages.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { existsSync, readdirSync, statSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
const _dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const packagesDir = join(_dirname, "..", "..", "packages");
|
||||
|
||||
function getPackageDirs(dir: string): string[] {
|
||||
const entries = readdirSync(dir);
|
||||
const dirs: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
if (!statSync(fullPath).isDirectory()) continue;
|
||||
|
||||
if (existsSync(join(fullPath, "package.json"))) {
|
||||
dirs.push(fullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
dirs.push(...getPackageDirs(fullPath));
|
||||
}
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
const packageDirs = getPackageDirs(packagesDir);
|
||||
|
||||
for (const fullPath of packageDirs) {
|
||||
const entry = fullPath.replace(packagesDir + "/", "");
|
||||
console.log(`Building ${entry}...`);
|
||||
try {
|
||||
execSync("pnpm run build", { cwd: fullPath, stdio: "inherit" });
|
||||
console.log(`${entry} built successfully.`);
|
||||
} catch {
|
||||
console.error(`Failed to build ${entry}.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Done!");
|
||||
531
utils/scripts/copy-licenses.ts
Normal file
531
utils/scripts/copy-licenses.ts
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { parse } from "yaml";
|
||||
|
||||
type PnpmLock = {
|
||||
lockfileVersion?: number;
|
||||
importers?: Record<string, WorkspaceEntry>;
|
||||
packages?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type WorkspaceEntry = {
|
||||
name?: string;
|
||||
version?: string;
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
peerDependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
type PackageRecord = {
|
||||
name: string;
|
||||
version: string;
|
||||
packageDir: string;
|
||||
relativePackageDir: string;
|
||||
outDirName: string;
|
||||
copiedFiles: string[];
|
||||
license?: string | string[];
|
||||
description?: string;
|
||||
homepage?: string;
|
||||
repository?: string;
|
||||
};
|
||||
|
||||
type ResolvedPackage = {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const LOCKFILE_PATH = path.join(ROOT, "pnpm-lock.yaml");
|
||||
const OUTPUT_DIR = path.join(ROOT, "licenses");
|
||||
|
||||
const COMPLIANCE_FILE_PATTERNS = [
|
||||
/^license.*$/i,
|
||||
/^licence.*$/i,
|
||||
/^notice.*$/i,
|
||||
];
|
||||
|
||||
async function main(): Promise<void> {
|
||||
await ensureExists(
|
||||
LOCKFILE_PATH,
|
||||
"Could not find pnpm-lock.yaml in the project root.",
|
||||
);
|
||||
|
||||
const lock = await readPnpmLock();
|
||||
const nodeModulesRoots = await getNodeModulesRoots(lock);
|
||||
|
||||
if (nodeModulesRoots.length === 0) {
|
||||
throw new Error(
|
||||
"Could not find any node_modules directories in the repo root or workspace folders. Run `pnpm install` first.",
|
||||
);
|
||||
}
|
||||
|
||||
await fs.rm(OUTPUT_DIR, { recursive: true, force: true });
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
const resolvedPackages = extractResolvedThirdPartyPackages(lock);
|
||||
const records: PackageRecord[] = [];
|
||||
|
||||
for (const resolved of resolvedPackages) {
|
||||
const record = await processInstalledPackage(
|
||||
resolved.name,
|
||||
resolved.version,
|
||||
nodeModulesRoots,
|
||||
);
|
||||
|
||||
if (record) {
|
||||
records.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueRecords = dedupePackageRecords(records);
|
||||
|
||||
uniqueRecords.sort((a, b) => {
|
||||
const byName = a.name.localeCompare(b.name);
|
||||
if (byName !== 0) return byName;
|
||||
return a.version.localeCompare(b.version);
|
||||
});
|
||||
|
||||
await writeThirdPartyNotices(uniqueRecords);
|
||||
await writeCreditsJson(uniqueRecords);
|
||||
await writeCycloneDxSbom(uniqueRecords);
|
||||
|
||||
console.log(
|
||||
`Generated ${uniqueRecords.length} third-party package records in ${OUTPUT_DIR}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function readPnpmLock(): Promise<PnpmLock> {
|
||||
const raw = await fs.readFile(LOCKFILE_PATH, "utf8");
|
||||
const parsed = parse(raw);
|
||||
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
throw new Error("Failed to parse pnpm-lock.yaml");
|
||||
}
|
||||
|
||||
return parsed as PnpmLock;
|
||||
}
|
||||
|
||||
async function getNodeModulesRoots(lock: PnpmLock): Promise<string[]> {
|
||||
const candidates = new Set<string>();
|
||||
|
||||
candidates.add(path.join(ROOT, "node_modules"));
|
||||
|
||||
for (const workspacePath of Object.keys(lock.importers ?? {})) {
|
||||
const workspaceDir =
|
||||
workspacePath === "." ? ROOT : path.join(ROOT, workspacePath);
|
||||
candidates.add(path.join(workspaceDir, "node_modules"));
|
||||
}
|
||||
|
||||
const existing: string[] = [];
|
||||
for (const candidate of candidates) {
|
||||
if (await isDirectory(candidate)) {
|
||||
existing.push(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer deeper workspace-local installs over root-hoisted installs.
|
||||
existing.sort((a, b) => b.split(path.sep).length - a.split(path.sep).length);
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
function extractResolvedThirdPartyPackages(lock: PnpmLock): ResolvedPackage[] {
|
||||
const results = new Map<string, ResolvedPackage>();
|
||||
const packages = lock.packages ?? {};
|
||||
|
||||
for (const rawKey of Object.keys(packages)) {
|
||||
const resolved = parsePnpmPackageKey(rawKey);
|
||||
if (!resolved) continue;
|
||||
|
||||
const { name, version } = resolved;
|
||||
|
||||
if (!name || !version) continue;
|
||||
if (version.startsWith("workspace:")) continue;
|
||||
|
||||
const key = `${name}@${version}`;
|
||||
if (!results.has(key)) {
|
||||
results.set(key, { name, version });
|
||||
}
|
||||
}
|
||||
|
||||
return [...results.values()];
|
||||
}
|
||||
|
||||
function parsePnpmPackageKey(rawKey: string): ResolvedPackage | null {
|
||||
const withoutPeerSuffix = rawKey.replace(/\(.+\)$/, "");
|
||||
const normalized = withoutPeerSuffix.startsWith("/")
|
||||
? withoutPeerSuffix.slice(1)
|
||||
: withoutPeerSuffix;
|
||||
const atIndex = normalized.lastIndexOf("@");
|
||||
|
||||
if (atIndex <= 0) return null;
|
||||
|
||||
return {
|
||||
name: normalized.slice(0, atIndex),
|
||||
version: normalized.slice(atIndex + 1),
|
||||
};
|
||||
}
|
||||
|
||||
async function processInstalledPackage(
|
||||
packageName: string,
|
||||
versionFromLock: string,
|
||||
nodeModulesRoots: string[],
|
||||
): Promise<PackageRecord | null> {
|
||||
const packageDir = await findInstalledPackageDir(
|
||||
packageName,
|
||||
nodeModulesRoots,
|
||||
);
|
||||
if (!packageDir) return null;
|
||||
|
||||
const packageJsonPath = path.join(packageDir, "package.json");
|
||||
|
||||
try {
|
||||
const pkgJsonRaw = await fs.readFile(packageJsonPath, "utf8");
|
||||
const pkgJson = JSON.parse(pkgJsonRaw) as Record<string, unknown>;
|
||||
|
||||
const actualName =
|
||||
typeof pkgJson.name === "string" ? pkgJson.name : packageName;
|
||||
const actualVersion =
|
||||
typeof pkgJson.version === "string" ? pkgJson.version : versionFromLock;
|
||||
|
||||
const outDirName = sanitizeDirName(`${actualName}@${actualVersion}`);
|
||||
const outDir = path.join(OUTPUT_DIR, outDirName);
|
||||
await fs.mkdir(outDir, { recursive: true });
|
||||
|
||||
const copiedFiles: string[] = [];
|
||||
const fileNames = await safeReadDir(packageDir);
|
||||
|
||||
for (const fileName of fileNames) {
|
||||
if (!matchesComplianceFile(fileName)) continue;
|
||||
|
||||
await copyFileIfExists(
|
||||
path.join(packageDir, fileName),
|
||||
path.join(outDir, fileName),
|
||||
copiedFiles,
|
||||
fileName,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
name: actualName,
|
||||
version: actualVersion,
|
||||
packageDir,
|
||||
relativePackageDir: path.relative(ROOT, packageDir),
|
||||
outDirName,
|
||||
copiedFiles,
|
||||
license: normalizeLicense(pkgJson.license, pkgJson.licenses),
|
||||
description:
|
||||
typeof pkgJson.description === "string"
|
||||
? pkgJson.description
|
||||
: undefined,
|
||||
homepage:
|
||||
typeof pkgJson.homepage === "string" ? pkgJson.homepage : undefined,
|
||||
repository: normalizeRepository(pkgJson.repository),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function findInstalledPackageDir(
|
||||
packageName: string,
|
||||
nodeModulesRoots: string[],
|
||||
): Promise<string | null> {
|
||||
const relativePackagePath = path.join(...packageName.split("/"));
|
||||
|
||||
for (const nodeModulesRoot of nodeModulesRoots) {
|
||||
const candidate = path.join(nodeModulesRoot, relativePackagePath);
|
||||
const packageJsonPath = path.join(candidate, "package.json");
|
||||
|
||||
if (await isFile(packageJsonPath)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeLicense(
|
||||
license: unknown,
|
||||
licenses: unknown,
|
||||
): string | string[] | undefined {
|
||||
if (typeof license === "string") return license;
|
||||
|
||||
if (Array.isArray(licenses)) {
|
||||
return licenses
|
||||
.map((item) => {
|
||||
if (typeof item === "string") return item;
|
||||
if (
|
||||
item &&
|
||||
typeof item === "object" &&
|
||||
"type" in item &&
|
||||
typeof (item as { type?: unknown }).type === "string"
|
||||
) {
|
||||
return (item as { type: string }).type;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((value): value is string => Boolean(value));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizeRepository(value: unknown): string | undefined {
|
||||
if (typeof value === "string") return value;
|
||||
|
||||
if (
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
"url" in value &&
|
||||
typeof (value as { url?: unknown }).url === "string"
|
||||
) {
|
||||
return (value as { url: string }).url;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function sanitizeDirName(name: string): string {
|
||||
return Array.from(name, (ch) => {
|
||||
const code = ch.charCodeAt(0);
|
||||
const isControl = code >= 0 && code <= 31;
|
||||
const isForbidden = '<>:"/\\|?*'.includes(ch);
|
||||
return isControl || isForbidden ? "_" : ch;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function matchesComplianceFile(fileName: string): boolean {
|
||||
return COMPLIANCE_FILE_PATTERNS.some((pattern) => pattern.test(fileName));
|
||||
}
|
||||
|
||||
async function copyFileIfExists(
|
||||
src: string,
|
||||
dest: string,
|
||||
copiedFiles: string[],
|
||||
label: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const stat = await fs.stat(src);
|
||||
if (!stat.isFile()) return;
|
||||
await fs.copyFile(src, dest);
|
||||
copiedFiles.push(label);
|
||||
} catch {
|
||||
// ignore missing/unreadable files
|
||||
}
|
||||
}
|
||||
|
||||
async function safeReadDir(dir: string): Promise<string[]> {
|
||||
try {
|
||||
return await fs.readdir(dir);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureExists(
|
||||
targetPath: string,
|
||||
message: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await fs.access(targetPath);
|
||||
} catch {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function isDirectory(targetPath: string): Promise<boolean> {
|
||||
try {
|
||||
const stat = await fs.stat(targetPath);
|
||||
return stat.isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function isFile(targetPath: string): Promise<boolean> {
|
||||
try {
|
||||
const stat = await fs.stat(targetPath);
|
||||
return stat.isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeThirdPartyNotices(records: PackageRecord[]): Promise<void> {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push("# Third-Party Notices");
|
||||
lines.push("");
|
||||
lines.push(
|
||||
"Generated from pnpm-lock.yaml and installed packages in workspace node_modules folders.",
|
||||
);
|
||||
lines.push("");
|
||||
|
||||
for (const record of records) {
|
||||
lines.push(`## ${record.name}@${record.version}`);
|
||||
lines.push("");
|
||||
lines.push(`- License: ${formatLicense(record.license)}`);
|
||||
if (record.homepage) lines.push(`- Homepage: ${record.homepage}`);
|
||||
if (record.repository) lines.push(`- Repository: ${record.repository}`);
|
||||
if (record.description) lines.push(`- Description: ${record.description}`);
|
||||
lines.push(
|
||||
`- Included files: ${
|
||||
record.copiedFiles.length > 0
|
||||
? record.copiedFiles.join(", ")
|
||||
: "none found"
|
||||
}`,
|
||||
);
|
||||
lines.push(`- Folder: \`licenses/${record.outDirName}\``);
|
||||
lines.push(`- Source package dir: \`${record.relativePackageDir}\``);
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "THIRD_PARTY_NOTICES.md"),
|
||||
lines.join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
async function writeCreditsJson(records: PackageRecord[]): Promise<void> {
|
||||
const payload = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
packageCount: records.length,
|
||||
packages: records.map((record) => ({
|
||||
name: record.name,
|
||||
version: record.version,
|
||||
license: formatLicense(record.license),
|
||||
homepage: record.homepage ?? null,
|
||||
repository: record.repository ?? null,
|
||||
description: record.description ?? null,
|
||||
files: record.copiedFiles,
|
||||
licenseFolder: `licenses/${record.outDirName}`,
|
||||
sourcePackageDir: record.relativePackageDir,
|
||||
})),
|
||||
};
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "third-party-credits.json"),
|
||||
JSON.stringify(payload, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
async function writeCycloneDxSbom(records: PackageRecord[]): Promise<void> {
|
||||
const components = records.map((record) => ({
|
||||
type: "library",
|
||||
bomRef: `pkg:npm/${encodePurlName(record.name)}@${record.version}`,
|
||||
name: record.name,
|
||||
version: record.version,
|
||||
purl: `pkg:npm/${encodePurlName(record.name)}@${record.version}`,
|
||||
description: record.description,
|
||||
licenses: toCycloneDxLicenses(record.license),
|
||||
externalReferences: [
|
||||
...(record.homepage ? [{ type: "website", url: record.homepage }] : []),
|
||||
...(record.repository ? [{ type: "vcs", url: record.repository }] : []),
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
name: "local:licenseFolder",
|
||||
value: `licenses/${record.outDirName}`,
|
||||
},
|
||||
{
|
||||
name: "local:sourcePackageDir",
|
||||
value: record.relativePackageDir,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
const sbom = {
|
||||
bomFormat: "CycloneDX",
|
||||
specVersion: "1.5",
|
||||
version: 1,
|
||||
metadata: {
|
||||
timestamp: new Date().toISOString(),
|
||||
tools: [
|
||||
{
|
||||
vendor: "OpenAI",
|
||||
name: "custom pnpm license generator",
|
||||
},
|
||||
],
|
||||
component: {
|
||||
type: "application",
|
||||
name: path.basename(ROOT),
|
||||
},
|
||||
},
|
||||
components,
|
||||
};
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "sbom.cyclonedx.json"),
|
||||
JSON.stringify(sbom, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
function dedupePackageRecords(records: PackageRecord[]): PackageRecord[] {
|
||||
const unique = new Map<string, PackageRecord>();
|
||||
|
||||
for (const record of records) {
|
||||
const key = `${record.name}@${record.version}`;
|
||||
const existing = unique.get(key);
|
||||
|
||||
if (!existing) {
|
||||
unique.set(key, record);
|
||||
continue;
|
||||
}
|
||||
|
||||
unique.set(key, {
|
||||
...existing,
|
||||
copiedFiles: [
|
||||
...new Set([...existing.copiedFiles, ...record.copiedFiles]),
|
||||
],
|
||||
license: existing.license ?? record.license,
|
||||
description: existing.description ?? record.description,
|
||||
homepage: existing.homepage ?? record.homepage,
|
||||
repository: existing.repository ?? record.repository,
|
||||
});
|
||||
}
|
||||
|
||||
return [...unique.values()];
|
||||
}
|
||||
|
||||
function toCycloneDxLicenses(
|
||||
value: string | string[] | undefined,
|
||||
): Array<{ license: { id?: string; name?: string } }> | undefined {
|
||||
if (!value) return undefined;
|
||||
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
const filtered = values.map((item) => item.trim()).filter(Boolean);
|
||||
|
||||
if (filtered.length === 0) return undefined;
|
||||
|
||||
return filtered.map((item) => ({
|
||||
license: looksLikeSpdxId(item) ? { id: item } : { name: item },
|
||||
}));
|
||||
}
|
||||
|
||||
function looksLikeSpdxId(value: string): boolean {
|
||||
return /^[A-Za-z0-9-.+]+$/.test(value);
|
||||
}
|
||||
|
||||
function encodePurlName(name: string): string {
|
||||
if (name.startsWith("@")) {
|
||||
const [scope, pkg] = name.split("/");
|
||||
return `${encodeURIComponent(scope)}/${encodeURIComponent(pkg)}`;
|
||||
}
|
||||
return encodeURIComponent(name);
|
||||
}
|
||||
|
||||
function formatLicense(value: string | string[] | undefined): string {
|
||||
if (Array.isArray(value) && value.length > 0) return value.join(", ");
|
||||
if (typeof value === "string" && value.trim()) return value;
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
128
utils/scripts/copy-releases.ts
Normal file
128
utils/scripts/copy-releases.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import {
|
||||
copyFileSync,
|
||||
createReadStream,
|
||||
existsSync,
|
||||
writeFileSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
} from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { basename, join } from "node:path";
|
||||
import packageJson from "../../package.json" with { type: "json" };
|
||||
|
||||
const { version } = packageJson;
|
||||
const releaseVersion = process.env.TENSAMIN_RELEASE_VERSION || version;
|
||||
const releaseTag = process.env.TENSAMIN_RELEASE_TAG || releaseVersion;
|
||||
const releasesDir = join("releases");
|
||||
const releaseAssetBaseUrl = process.env.FORGEJO_RELEASE_ASSET_BASE_URL;
|
||||
|
||||
function sha256(filePath: string) {
|
||||
const hash = createHash("sha256");
|
||||
const stream = createReadStream(filePath);
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
stream.on("data", (chunk) => hash.update(chunk));
|
||||
stream.on("error", reject);
|
||||
stream.on("end", () => resolve(hash.digest("hex")));
|
||||
});
|
||||
}
|
||||
|
||||
function platformFor(file: string) {
|
||||
if (/\.apk$/i.test(file)) return "android";
|
||||
if (/win|nsis|portable|\.exe$/i.test(file)) return "windows";
|
||||
if (/mac|darwin|\.dmg$/i.test(file)) return "macos";
|
||||
return "linux";
|
||||
}
|
||||
|
||||
function archFor(file: string) {
|
||||
if (/arm64|aarch64/i.test(file)) return "arm64";
|
||||
if (/\.apk$/i.test(file)) return "universal";
|
||||
return "x64";
|
||||
}
|
||||
|
||||
// directory
|
||||
rmSync(releasesDir, { recursive: true, force: true });
|
||||
mkdirSync(releasesDir, { recursive: true });
|
||||
|
||||
// apk
|
||||
const apkSrc =
|
||||
"apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk";
|
||||
if (existsSync(apkSrc)) {
|
||||
renameSync(apkSrc, join(releasesDir, `Tensamin-${version}.apk`));
|
||||
} else {
|
||||
console.warn(`Skipping .apk: ${apkSrc} does not exist.`);
|
||||
}
|
||||
|
||||
// Electron desktop artifacts
|
||||
const electronReleaseDir = "apps/electron/release";
|
||||
const electronArtifactPattern = /\.(AppImage|deb|rpm|exe|dmg|zip)$/i;
|
||||
|
||||
function readFilesRecursive(dir: string): string[] {
|
||||
return readdirSync(dir).flatMap((file) => {
|
||||
const filePath = join(dir, file);
|
||||
const stat = statSync(filePath);
|
||||
|
||||
if (stat.isDirectory()) return readFilesRecursive(filePath);
|
||||
if (stat.isFile()) return [filePath];
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
const copyElectronArtifacts = () => {
|
||||
if (!existsSync(electronReleaseDir)) {
|
||||
console.warn(
|
||||
`Skipping Electron artifacts: ${electronReleaseDir} does not exist.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const source of readFilesRecursive(electronReleaseDir)) {
|
||||
const file = basename(source);
|
||||
if (!electronArtifactPattern.test(file)) continue;
|
||||
|
||||
copyFileSync(source, join(releasesDir, file));
|
||||
}
|
||||
};
|
||||
|
||||
copyElectronArtifacts();
|
||||
|
||||
const artifacts = await Promise.all(
|
||||
readdirSync(releasesDir)
|
||||
.map((file) => join(releasesDir, file))
|
||||
.filter((file) => statSync(file).isFile())
|
||||
.filter(
|
||||
(file) =>
|
||||
!file.endsWith("electron-release-metadata.json") &&
|
||||
!file.endsWith("SHA256SUMS"),
|
||||
)
|
||||
.map(async (filePath) => {
|
||||
const name = filePath.split(/[\\/]/).at(-1)!;
|
||||
|
||||
return {
|
||||
name,
|
||||
platform: platformFor(name),
|
||||
arch: archFor(name),
|
||||
url: releaseAssetBaseUrl
|
||||
? `${releaseAssetBaseUrl.replace(/\/$/, "")}/${encodeURIComponent(name)}`
|
||||
: `__FORGEJO_RELEASE_ASSET_URL__/${encodeURIComponent(name)}`,
|
||||
sha256: await sha256(filePath),
|
||||
size: statSync(filePath).size,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
join(releasesDir, "electron-release-metadata.json"),
|
||||
`${JSON.stringify({ version: releaseVersion, tag: releaseTag, publishedAt: new Date().toISOString(), artifacts: artifacts.filter((artifact) => artifact.platform !== "android") }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
join(releasesDir, "SHA256SUMS"),
|
||||
`${artifacts.map((artifact) => `${artifact.sha256} ${artifact.name}`).join("\n")}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
console.log("Releases copied successfully.");
|
||||
48
utils/scripts/lint-packages.ts
Normal file
48
utils/scripts/lint-packages.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { existsSync, readdirSync, statSync } from "fs";
|
||||
import { join, relative } from "path";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
const _dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const rootDir = join(_dirname, "..", "..");
|
||||
const targetDirs = [join(rootDir, "packages"), join(rootDir, "apps")];
|
||||
|
||||
function getPackageDirs(dir: string): string[] {
|
||||
const entries = readdirSync(dir);
|
||||
const dirs: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
if (!statSync(fullPath).isDirectory()) continue;
|
||||
|
||||
if (existsSync(join(fullPath, "package.json"))) {
|
||||
dirs.push(fullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
dirs.push(...getPackageDirs(fullPath));
|
||||
}
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
for (const targetDir of targetDirs) {
|
||||
const packageDirs = getPackageDirs(targetDir);
|
||||
|
||||
for (const fullPath of packageDirs) {
|
||||
const entry = relative(rootDir, fullPath);
|
||||
console.log(`Linting ${entry}...`);
|
||||
try {
|
||||
execSync("pnpm run lint", { cwd: fullPath, stdio: "inherit" });
|
||||
console.log(`${entry} linted successfully.`);
|
||||
} catch {
|
||||
console.error(`Failed to lint ${entry}.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Done!");
|
||||
45
utils/scripts/update-packages.ts
Normal file
45
utils/scripts/update-packages.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { existsSync, readdirSync, statSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
const _dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const packagesDir = join(_dirname, "..", "..", "packages");
|
||||
|
||||
function getPackageDirs(dir: string): string[] {
|
||||
const entries = readdirSync(dir);
|
||||
const dirs: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
if (!statSync(fullPath).isDirectory()) continue;
|
||||
|
||||
if (existsSync(join(fullPath, "package.json"))) {
|
||||
dirs.push(fullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
dirs.push(...getPackageDirs(fullPath));
|
||||
}
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
const packageDirs = getPackageDirs(packagesDir);
|
||||
|
||||
for (const fullPath of packageDirs) {
|
||||
const entry = fullPath.replace(packagesDir + "/", "");
|
||||
console.log(`Updating ${entry}...`);
|
||||
try {
|
||||
execSync("pnpm update --interactive", { cwd: fullPath, stdio: "inherit" });
|
||||
console.log(`${entry} updated successfully.`);
|
||||
} catch {
|
||||
console.error(`Failed to update ${entry}.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Done!");
|
||||
Loading…
Reference in a new issue