From efee2e87cc978ded6e26a855f40d68696f6f6e20 Mon Sep 17 00:00:00 2001 From: Alois Date: Mon, 20 Apr 2026 21:59:36 +0200 Subject: [PATCH] (feat): add license page (fix): noise suppression error --- .prettierignore | 1 + apps/web/src/routes/settings/licenses.tsx | 144 ++++++++++++++++++++++ apps/web/vite.config.ts | 47 ++++++- packages/shared/src/settings.ts | 3 + scripts/copy-licenses.ts | 70 ++++++----- 5 files changed, 232 insertions(+), 33 deletions(-) create mode 100644 apps/web/src/routes/settings/licenses.tsx diff --git a/.prettierignore b/.prettierignore index 3441b93..75752bc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,4 @@ coverage *.tsbuildinfo bun.lock apps/tauri/src-tauri +licenses diff --git a/apps/web/src/routes/settings/licenses.tsx b/apps/web/src/routes/settings/licenses.tsx new file mode 100644 index 0000000..31293b1 --- /dev/null +++ b/apps/web/src/routes/settings/licenses.tsx @@ -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; + +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 ( +
+
+

Last generated: {generatedAt}

+

Package Count: {packageCount}

+
+
+
+ {packages.map((licensePackage) => ( + + + + {licensePackage.license} {licensePackage.name}{" "} + {licensePackage.version} + + + {licensePackage.description && ( + + + {licensePackage.description} + + + )} + + + {licensePackage.repository ? ( + + + + ) : ( + + )} + {licensePackage.homepage ? ( + + + + ) : ( + + )} + + + ))} +
+
+
+ ); +} + +function LicenseDialog({ + licensePackage, +}: { + licensePackage: (typeof packages)[number]; +}) { + const licenseFiles = getLicenseFiles(licensePackage); + const hasLicenseText = licenseFiles.some(({ text }) => text); + + return ( + + + Open License + + } + /> + +
+ {licenseFiles.map(({ fileName, text }, index) => ( +
+

= 1 && "pt-2"}`} + > + {fileName} +

+
+                {text ||
+                  "License text could not be loaded. Please contact support@tensamin.net"}
+              
+
+ ))} +
+
+
+ ); +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index af46339..7b50ea5 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -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 { fileURLToPath } from "node:url"; -import { defineConfig } from "vite"; +import { defineConfig, type Plugin } from "vite"; import react from "@vitejs/plugin-react"; import tsconfigPaths from "vite-tsconfig-paths"; import tailwindcss from "@tailwindcss/vite"; @@ -15,6 +16,41 @@ function resolveMarkdownDependency(packageName: string): string { 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({ clearScreen: false, resolve: { @@ -63,7 +99,12 @@ export default defineConfig({ minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false, sourcemap: !!process.env.TAURI_ENV_DEBUG, }, - plugins: [react(), tsconfigPaths(), tailwindcss()], + plugins: [ + deepFilterAssetHeaders(resolve(appDir, "public")), + react(), + tsconfigPaths(), + tailwindcss(), + ], worker: { format: "es", }, diff --git a/packages/shared/src/settings.ts b/packages/shared/src/settings.ts index 05bef09..0099f64 100644 --- a/packages/shared/src/settings.ts +++ b/packages/shared/src/settings.ts @@ -30,6 +30,9 @@ const settings = { }, }, }, + application: { + licenses: {}, + }, } as const satisfies SettingsSchema; export default settings; diff --git a/scripts/copy-licenses.ts b/scripts/copy-licenses.ts index 1cafe11..f22b111 100644 --- a/scripts/copy-licenses.ts +++ b/scripts/copy-licenses.ts @@ -46,14 +46,17 @@ const COMPLIANCE_FILE_PATTERNS = [ ]; async function main(): Promise { - 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 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 `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 { const record = await processInstalledPackage( resolved.name, resolved.version, - nodeModulesRoots + nodeModulesRoots, ); if (record) { @@ -86,7 +89,7 @@ async function main(): Promise { await writeCycloneDxSbom(records); 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 { candidates.add(path.join(ROOT, "node_modules")); 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")); } @@ -159,9 +163,12 @@ function extractResolvedThirdPartyPackages(lock: BunLock): ResolvedPackage[] { async function processInstalledPackage( packageName: string, versionFromLock: string, - nodeModulesRoots: string[] + nodeModulesRoots: string[], ): Promise { - const packageDir = await findInstalledPackageDir(packageName, nodeModulesRoots); + const packageDir = await findInstalledPackageDir( + packageName, + nodeModulesRoots, + ); if (!packageDir) return null; const packageJsonPath = path.join(packageDir, "package.json"); @@ -189,7 +196,7 @@ async function processInstalledPackage( path.join(packageDir, fileName), path.join(outDir, fileName), copiedFiles, - fileName + fileName, ); } @@ -202,7 +209,9 @@ async function processInstalledPackage( copiedFiles, license: normalizeLicense(pkgJson.license, pkgJson.licenses), description: - typeof pkgJson.description === "string" ? pkgJson.description : undefined, + typeof pkgJson.description === "string" + ? pkgJson.description + : undefined, homepage: typeof pkgJson.homepage === "string" ? pkgJson.homepage : undefined, repository: normalizeRepository(pkgJson.repository), @@ -214,7 +223,7 @@ async function processInstalledPackage( async function findInstalledPackageDir( packageName: string, - nodeModulesRoots: string[] + nodeModulesRoots: string[], ): Promise { const relativePackagePath = path.join(...packageName.split("/")); @@ -232,7 +241,7 @@ async function findInstalledPackageDir( function normalizeLicense( license: unknown, - licenses: unknown + licenses: unknown, ): string | string[] | undefined { if (typeof license === "string") return license; @@ -288,7 +297,7 @@ async function copyFileIfExists( src: string, dest: string, copiedFiles: string[], - label: string + label: string, ): Promise { try { const stat = await fs.stat(src); @@ -308,7 +317,10 @@ async function safeReadDir(dir: string): Promise { } } -async function ensureExists(targetPath: string, message: string): Promise { +async function ensureExists( + targetPath: string, + message: string, +): Promise { try { await fs.access(targetPath); } catch { @@ -339,7 +351,9 @@ async function writeThirdPartyNotices(records: PackageRecord[]): Promise { lines.push("# Third-Party Notices"); 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(""); for (const record of records) { @@ -351,8 +365,10 @@ async function writeThirdPartyNotices(records: PackageRecord[]): Promise { if (record.description) lines.push(`- Description: ${record.description}`); lines.push( `- 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(`- Source package dir: \`${record.relativePackageDir}\``); @@ -362,7 +378,7 @@ async function writeThirdPartyNotices(records: PackageRecord[]): Promise { await fs.writeFile( path.join(OUTPUT_DIR, "THIRD_PARTY_NOTICES.md"), lines.join("\n"), - "utf8" + "utf8", ); } @@ -386,7 +402,7 @@ async function writeCreditsJson(records: PackageRecord[]): Promise { await fs.writeFile( path.join(OUTPUT_DIR, "third-party-credits.json"), JSON.stringify(payload, null, 2), - "utf8" + "utf8", ); } @@ -400,12 +416,8 @@ async function writeCycloneDxSbom(records: PackageRecord[]): Promise { description: record.description, licenses: toCycloneDxLicenses(record.license), externalReferences: [ - ...(record.homepage - ? [{ type: "website", url: record.homepage }] - : []), - ...(record.repository - ? [{ type: "vcs", url: record.repository }] - : []), + ...(record.homepage ? [{ type: "website", url: record.homepage }] : []), + ...(record.repository ? [{ type: "vcs", url: record.repository }] : []), ], properties: [ { @@ -442,12 +454,12 @@ async function writeCycloneDxSbom(records: PackageRecord[]): Promise { await fs.writeFile( path.join(OUTPUT_DIR, "sbom.cyclonedx.json"), JSON.stringify(sbom, null, 2), - "utf8" + "utf8", ); } function toCycloneDxLicenses( - value: string | string[] | undefined + value: string | string[] | undefined, ): Array<{ license: { id?: string; name?: string } }> | undefined { if (!value) return undefined; @@ -457,9 +469,7 @@ function toCycloneDxLicenses( if (filtered.length === 0) return undefined; return filtered.map((item) => ({ - license: looksLikeSpdxId(item) - ? { id: item } - : { name: item }, + license: looksLikeSpdxId(item) ? { id: item } : { name: item }, })); } @@ -484,4 +494,4 @@ function formatLicense(value: string | string[] | undefined): string { main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); -}); \ No newline at end of file +});