(feat): add license page

(fix): noise suppression error
This commit is contained in:
Alois 2026-04-20 21:59:36 +02:00
commit efee2e87cc
5 changed files with 232 additions and 33 deletions

View file

@ -5,3 +5,4 @@ coverage
*.tsbuildinfo *.tsbuildinfo
bun.lock bun.lock
apps/tauri/src-tauri apps/tauri/src-tauri
licenses

View file

@ -0,0 +1,144 @@
import {
Card,
CardContent,
CardTitle,
CardHeader,
CardDescription,
CardFooter,
Button,
Badge,
Dialog,
DialogContent,
DialogTrigger,
} from "@tensamin/ui";
import {
packages,
packageCount,
generatedAt,
} from "../../../../../licenses/third-party-credits.json";
const licenseTexts = import.meta.glob("../../../../../licenses/**/*", {
eager: true,
import: "default",
query: "?raw",
}) as Record<string, string>;
function getLicenseFiles(licensePackage: (typeof packages)[number]) {
return licensePackage.files.map((fileName) => {
const path =
"../../../../../" + licensePackage.licenseFolder + "/" + fileName;
return {
fileName,
text: licenseTexts[path],
};
});
}
export default function Page() {
return (
<div className="flex h-full min-h-0 flex-col gap-7">
<div className="flex flex-col">
<p>Last generated: {generatedAt}</p>
<p>Package Count: {packageCount}</p>
</div>
<div className="min-h-0 flex-1 max-h-[calc(100vh-180px)] overflow-auto pr-2">
<div className="flex flex-col gap-5">
{packages.map((licensePackage) => (
<Card
key={licensePackage.name + licensePackage.version}
id={licensePackage.name + licensePackage.version}
>
<CardHeader>
<CardTitle className="flex gap-2 items-center">
<Badge>{licensePackage.license}</Badge> {licensePackage.name}{" "}
{licensePackage.version}
</CardTitle>
</CardHeader>
{licensePackage.description && (
<CardContent>
<CardDescription>
{licensePackage.description}
</CardDescription>
</CardContent>
)}
<CardFooter className="gap-2">
<LicenseDialog licensePackage={licensePackage} />
{licensePackage.repository ? (
<a
target="_blank"
rel="noreferrer"
href={licensePackage.repository
?.replace("git+", "")
.replace(".git", "")}
>
<Button variant="outline" className="cursor-pointer">
Open Repository
</Button>
</a>
) : (
<Button disabled variant="outline" className="cursor-pointer">
Open Repository
</Button>
)}
{licensePackage.homepage ? (
<a
target="_blank"
rel="noreferrer"
href={licensePackage.homepage}
>
<Button variant="outline" className="cursor-pointer">
Open Homepage
</Button>
</a>
) : (
<Button disabled variant="outline" className="cursor-pointer">
Open Homepage
</Button>
)}
</CardFooter>
</Card>
))}
</div>
</div>
</div>
);
}
function LicenseDialog({
licensePackage,
}: {
licensePackage: (typeof packages)[number];
}) {
const licenseFiles = getLicenseFiles(licensePackage);
const hasLicenseText = licenseFiles.some(({ text }) => text);
return (
<Dialog>
<DialogTrigger
render={
<Button disabled={!hasLicenseText} className="cursor-pointer">
Open License
</Button>
}
/>
<DialogContent className="flex max-h-[85vh] min-h-0 flex-col overflow-hidden sm:max-w-3xl">
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain pr-2">
{licenseFiles.map(({ fileName, text }, index) => (
<section key={fileName} className="border-b last:border-b-0">
<h3
className={`border-b pb-2 text-sm font-medium ${index >= 1 && "pt-2"}`}
>
{fileName}
</h3>
<pre className="pt-2 whitespace-pre-wrap wrap-break-word text-xs leading-relaxed">
{text ||
"License text could not be loaded. Please contact support@tensamin.net"}
</pre>
</section>
))}
</div>
</DialogContent>
</Dialog>
);
}

View file

@ -1,8 +1,9 @@
import { realpathSync } from "node:fs"; import { createReadStream, realpathSync, statSync } from "node:fs";
import type { IncomingMessage, ServerResponse } from "node:http";
import { dirname, resolve } from "node:path"; import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { defineConfig } from "vite"; import { defineConfig, type Plugin } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from "vite-tsconfig-paths";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
@ -15,6 +16,41 @@ function resolveMarkdownDependency(packageName: string): string {
return realpathSync(resolve(markdownPackageDir, "node_modules", packageName)); return realpathSync(resolve(markdownPackageDir, "node_modules", packageName));
} }
function deepFilterAssetHeaders(rootDir: string): Plugin {
const serveModel = (
req: IncomingMessage,
res: ServerResponse,
next: () => void,
) => {
const url = req.url ? new URL(req.url, "http://localhost") : null;
if (url?.pathname !== "/assets/v2/models/DeepFilterNet3_onnx.tar.gz") {
next();
return;
}
const modelPath = resolve(
rootDir,
"assets/v2/models/DeepFilterNet3_onnx.tar.gz",
);
const stat = statSync(modelPath);
res.statusCode = 200;
res.setHeader("Content-Type", "application/gzip");
res.setHeader("Content-Length", stat.size.toString());
res.setHeader("Cache-Control", "no-cache");
createReadStream(modelPath).pipe(res);
};
return {
name: "deepfilter-asset-headers",
configureServer(server) {
server.middlewares.use(serveModel);
},
configurePreviewServer(server) {
server.middlewares.use(serveModel);
},
};
}
export default defineConfig({ export default defineConfig({
clearScreen: false, clearScreen: false,
resolve: { resolve: {
@ -63,7 +99,12 @@ export default defineConfig({
minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false, minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
sourcemap: !!process.env.TAURI_ENV_DEBUG, sourcemap: !!process.env.TAURI_ENV_DEBUG,
}, },
plugins: [react(), tsconfigPaths(), tailwindcss()], plugins: [
deepFilterAssetHeaders(resolve(appDir, "public")),
react(),
tsconfigPaths(),
tailwindcss(),
],
worker: { worker: {
format: "es", format: "es",
}, },

View file

@ -30,6 +30,9 @@ const settings = {
}, },
}, },
}, },
application: {
licenses: {},
},
} as const satisfies SettingsSchema; } as const satisfies SettingsSchema;
export default settings; export default settings;

View file

@ -46,14 +46,17 @@ const COMPLIANCE_FILE_PATTERNS = [
]; ];
async function main(): Promise<void> { async function main(): Promise<void> {
await ensureExists(LOCKFILE_PATH, "Could not find bun.lock in the project root."); await ensureExists(
LOCKFILE_PATH,
"Could not find bun.lock in the project root.",
);
const lock = await readBunLock(); const lock = await readBunLock();
const nodeModulesRoots = await getNodeModulesRoots(lock); const nodeModulesRoots = await getNodeModulesRoots(lock);
if (nodeModulesRoots.length === 0) { if (nodeModulesRoots.length === 0) {
throw new Error( throw new Error(
"Could not find any node_modules directories in the repo root or workspace folders. Run `bun install` first." "Could not find any node_modules directories in the repo root or workspace folders. Run `bun install` first.",
); );
} }
@ -67,7 +70,7 @@ async function main(): Promise<void> {
const record = await processInstalledPackage( const record = await processInstalledPackage(
resolved.name, resolved.name,
resolved.version, resolved.version,
nodeModulesRoots nodeModulesRoots,
); );
if (record) { if (record) {
@ -86,7 +89,7 @@ async function main(): Promise<void> {
await writeCycloneDxSbom(records); await writeCycloneDxSbom(records);
console.log( console.log(
`Generated ${records.length} third-party package records in ${OUTPUT_DIR}` `Generated ${records.length} third-party package records in ${OUTPUT_DIR}`,
); );
} }
@ -107,7 +110,8 @@ async function getNodeModulesRoots(lock: BunLock): Promise<string[]> {
candidates.add(path.join(ROOT, "node_modules")); candidates.add(path.join(ROOT, "node_modules"));
for (const workspacePath of Object.keys(lock.workspaces ?? {})) { for (const workspacePath of Object.keys(lock.workspaces ?? {})) {
const workspaceDir = workspacePath === "" ? ROOT : path.join(ROOT, workspacePath); const workspaceDir =
workspacePath === "" ? ROOT : path.join(ROOT, workspacePath);
candidates.add(path.join(workspaceDir, "node_modules")); candidates.add(path.join(workspaceDir, "node_modules"));
} }
@ -159,9 +163,12 @@ function extractResolvedThirdPartyPackages(lock: BunLock): ResolvedPackage[] {
async function processInstalledPackage( async function processInstalledPackage(
packageName: string, packageName: string,
versionFromLock: string, versionFromLock: string,
nodeModulesRoots: string[] nodeModulesRoots: string[],
): Promise<PackageRecord | null> { ): Promise<PackageRecord | null> {
const packageDir = await findInstalledPackageDir(packageName, nodeModulesRoots); const packageDir = await findInstalledPackageDir(
packageName,
nodeModulesRoots,
);
if (!packageDir) return null; if (!packageDir) return null;
const packageJsonPath = path.join(packageDir, "package.json"); const packageJsonPath = path.join(packageDir, "package.json");
@ -189,7 +196,7 @@ async function processInstalledPackage(
path.join(packageDir, fileName), path.join(packageDir, fileName),
path.join(outDir, fileName), path.join(outDir, fileName),
copiedFiles, copiedFiles,
fileName fileName,
); );
} }
@ -202,7 +209,9 @@ async function processInstalledPackage(
copiedFiles, copiedFiles,
license: normalizeLicense(pkgJson.license, pkgJson.licenses), license: normalizeLicense(pkgJson.license, pkgJson.licenses),
description: description:
typeof pkgJson.description === "string" ? pkgJson.description : undefined, typeof pkgJson.description === "string"
? pkgJson.description
: undefined,
homepage: homepage:
typeof pkgJson.homepage === "string" ? pkgJson.homepage : undefined, typeof pkgJson.homepage === "string" ? pkgJson.homepage : undefined,
repository: normalizeRepository(pkgJson.repository), repository: normalizeRepository(pkgJson.repository),
@ -214,7 +223,7 @@ async function processInstalledPackage(
async function findInstalledPackageDir( async function findInstalledPackageDir(
packageName: string, packageName: string,
nodeModulesRoots: string[] nodeModulesRoots: string[],
): Promise<string | null> { ): Promise<string | null> {
const relativePackagePath = path.join(...packageName.split("/")); const relativePackagePath = path.join(...packageName.split("/"));
@ -232,7 +241,7 @@ async function findInstalledPackageDir(
function normalizeLicense( function normalizeLicense(
license: unknown, license: unknown,
licenses: unknown licenses: unknown,
): string | string[] | undefined { ): string | string[] | undefined {
if (typeof license === "string") return license; if (typeof license === "string") return license;
@ -288,7 +297,7 @@ async function copyFileIfExists(
src: string, src: string,
dest: string, dest: string,
copiedFiles: string[], copiedFiles: string[],
label: string label: string,
): Promise<void> { ): Promise<void> {
try { try {
const stat = await fs.stat(src); const stat = await fs.stat(src);
@ -308,7 +317,10 @@ async function safeReadDir(dir: string): Promise<string[]> {
} }
} }
async function ensureExists(targetPath: string, message: string): Promise<void> { async function ensureExists(
targetPath: string,
message: string,
): Promise<void> {
try { try {
await fs.access(targetPath); await fs.access(targetPath);
} catch { } catch {
@ -339,7 +351,9 @@ async function writeThirdPartyNotices(records: PackageRecord[]): Promise<void> {
lines.push("# Third-Party Notices"); lines.push("# Third-Party Notices");
lines.push(""); lines.push("");
lines.push("Generated from bun.lock and installed packages in workspace node_modules folders."); lines.push(
"Generated from bun.lock and installed packages in workspace node_modules folders.",
);
lines.push(""); lines.push("");
for (const record of records) { for (const record of records) {
@ -351,8 +365,10 @@ async function writeThirdPartyNotices(records: PackageRecord[]): Promise<void> {
if (record.description) lines.push(`- Description: ${record.description}`); if (record.description) lines.push(`- Description: ${record.description}`);
lines.push( lines.push(
`- Included files: ${ `- Included files: ${
record.copiedFiles.length > 0 ? record.copiedFiles.join(", ") : "none found" record.copiedFiles.length > 0
}` ? record.copiedFiles.join(", ")
: "none found"
}`,
); );
lines.push(`- Folder: \`licenses/${record.outDirName}\``); lines.push(`- Folder: \`licenses/${record.outDirName}\``);
lines.push(`- Source package dir: \`${record.relativePackageDir}\``); lines.push(`- Source package dir: \`${record.relativePackageDir}\``);
@ -362,7 +378,7 @@ async function writeThirdPartyNotices(records: PackageRecord[]): Promise<void> {
await fs.writeFile( await fs.writeFile(
path.join(OUTPUT_DIR, "THIRD_PARTY_NOTICES.md"), path.join(OUTPUT_DIR, "THIRD_PARTY_NOTICES.md"),
lines.join("\n"), lines.join("\n"),
"utf8" "utf8",
); );
} }
@ -386,7 +402,7 @@ async function writeCreditsJson(records: PackageRecord[]): Promise<void> {
await fs.writeFile( await fs.writeFile(
path.join(OUTPUT_DIR, "third-party-credits.json"), path.join(OUTPUT_DIR, "third-party-credits.json"),
JSON.stringify(payload, null, 2), JSON.stringify(payload, null, 2),
"utf8" "utf8",
); );
} }
@ -400,12 +416,8 @@ async function writeCycloneDxSbom(records: PackageRecord[]): Promise<void> {
description: record.description, description: record.description,
licenses: toCycloneDxLicenses(record.license), licenses: toCycloneDxLicenses(record.license),
externalReferences: [ externalReferences: [
...(record.homepage ...(record.homepage ? [{ type: "website", url: record.homepage }] : []),
? [{ type: "website", url: record.homepage }] ...(record.repository ? [{ type: "vcs", url: record.repository }] : []),
: []),
...(record.repository
? [{ type: "vcs", url: record.repository }]
: []),
], ],
properties: [ properties: [
{ {
@ -442,12 +454,12 @@ async function writeCycloneDxSbom(records: PackageRecord[]): Promise<void> {
await fs.writeFile( await fs.writeFile(
path.join(OUTPUT_DIR, "sbom.cyclonedx.json"), path.join(OUTPUT_DIR, "sbom.cyclonedx.json"),
JSON.stringify(sbom, null, 2), JSON.stringify(sbom, null, 2),
"utf8" "utf8",
); );
} }
function toCycloneDxLicenses( function toCycloneDxLicenses(
value: string | string[] | undefined value: string | string[] | undefined,
): Array<{ license: { id?: string; name?: string } }> | undefined { ): Array<{ license: { id?: string; name?: string } }> | undefined {
if (!value) return undefined; if (!value) return undefined;
@ -457,9 +469,7 @@ function toCycloneDxLicenses(
if (filtered.length === 0) return undefined; if (filtered.length === 0) return undefined;
return filtered.map((item) => ({ return filtered.map((item) => ({
license: looksLikeSpdxId(item) license: looksLikeSpdxId(item) ? { id: item } : { name: item },
? { id: item }
: { name: item },
})); }));
} }
@ -484,4 +494,4 @@ function formatLicense(value: string | string[] | undefined): string {
main().catch((error) => { main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error)); console.error(error instanceof Error ? error.message : String(error));
process.exit(1); process.exit(1);
}); });