(feat): add license page
(fix): noise suppression error
This commit is contained in:
parent
dc1c11219b
commit
efee2e87cc
5 changed files with 232 additions and 33 deletions
|
|
@ -5,3 +5,4 @@ coverage
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
bun.lock
|
bun.lock
|
||||||
apps/tauri/src-tauri
|
apps/tauri/src-tauri
|
||||||
|
licenses
|
||||||
|
|
|
||||||
144
apps/web/src/routes/settings/licenses.tsx
Normal file
144
apps/web/src/routes/settings/licenses.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ const settings = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
application: {
|
||||||
|
licenses: {},
|
||||||
|
},
|
||||||
} as const satisfies SettingsSchema;
|
} as const satisfies SettingsSchema;
|
||||||
|
|
||||||
export default settings;
|
export default settings;
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue