Skip to content

Commit b1c1d02

Browse files
committed
Add license attribution dialog with full dependency listing
1 parent 9a8fc08 commit b1c1d02

File tree

5 files changed

+194
-2
lines changed

5 files changed

+194
-2
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
# dependencies
44
/node_modules
5+
6+
# generated
7+
/public/licenses.json
58
/.pnp
69
.pnp.js
710

@@ -35,6 +38,7 @@ scripts/*
3538
!scripts/generate-language.ts
3639
!scripts/extract-oracle.ts
3740
!scripts/generate-erd.ts
41+
!scripts/generate-licenses.ts
3842
!scripts/hooks/
3943

4044
data/*

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"scripts": {
4141
"dev": "npm-run-all copy-sqljs start-vite",
4242
"start": "npm-run-all copy-sqljs start-vite",
43-
"build": "npm-run-all copy-sqljs build-vite",
43+
"build": "npm-run-all copy-sqljs generate-licenses build-vite",
4444
"preview": "vite preview",
4545
"copy-sqljs": "copyfiles -f node_modules/sql.js/dist/sql-wasm.wasm public/dist/sql.js/",
4646
"start-vite": "vite",
@@ -50,6 +50,7 @@
5050
"decrypt-oracle": "tsx scripts/decrypt-oracle.ts",
5151
"encrypt-oracle": "tsx scripts/encrypt-oracle.ts",
5252
"generate-erd": "tsx scripts/generate-erd.ts",
53+
"generate-licenses": "tsx scripts/generate-licenses.ts",
5354
"generate-all": "tsx scripts/generate-language.ts --all --plain && tsx scripts/generate-erd.ts --all"
5455
},
5556
"eslintConfig": {

scripts/generate-licenses.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Generates a JSON file with license information for all production dependencies.
3+
* Output: public/licenses.json
4+
*/
5+
6+
import { readFileSync, writeFileSync, existsSync } from "fs";
7+
import { join, dirname } from "path";
8+
9+
const ROOT = join(dirname(new URL(import.meta.url).pathname), "..");
10+
const NODE_MODULES = join(ROOT, "node_modules");
11+
12+
interface LicenseEntry {
13+
name: string;
14+
version: string;
15+
license: string;
16+
repository?: string;
17+
author?: string;
18+
}
19+
20+
// Read the root package.json to get production dependencies
21+
const rootPkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
22+
const prodDeps = Object.keys(rootPkg.dependencies || {});
23+
24+
function resolvePackage(name: string): LicenseEntry | null {
25+
const pkgDir = join(NODE_MODULES, name);
26+
const pkgJsonPath = join(pkgDir, "package.json");
27+
if (!existsSync(pkgJsonPath)) return null;
28+
29+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
30+
const license = typeof pkg.license === "string"
31+
? pkg.license
32+
: typeof pkg.license === "object"
33+
? pkg.license.type
34+
: Array.isArray(pkg.licenses)
35+
? pkg.licenses.map((l: { type: string }) => l.type).join(", ")
36+
: "Unknown";
37+
38+
const repo = typeof pkg.repository === "string"
39+
? pkg.repository
40+
: typeof pkg.repository === "object"
41+
? pkg.repository.url
42+
: undefined;
43+
44+
const cleanRepo = repo
45+
?.replace(/^git\+/, "")
46+
?.replace(/^git:\/\//, "https://")
47+
?.replace(/\.git$/, "")
48+
?.replace(/^ssh:\/\/git@github\.com/, "https://github.com");
49+
50+
const author = typeof pkg.author === "string"
51+
? pkg.author
52+
: typeof pkg.author === "object"
53+
? pkg.author.name
54+
: undefined;
55+
56+
return {
57+
name: pkg.name,
58+
version: pkg.version,
59+
license,
60+
repository: cleanRepo,
61+
author,
62+
};
63+
}
64+
65+
const entries: LicenseEntry[] = [];
66+
67+
for (const dep of prodDeps) {
68+
const entry = resolvePackage(dep);
69+
if (entry) entries.push(entry);
70+
}
71+
72+
// Sort alphabetically
73+
entries.sort((a, b) => a.name.localeCompare(b.name));
74+
75+
writeFileSync(join(ROOT, "public", "licenses.json"), JSON.stringify(entries, null, 2));
76+
console.log(`Generated licenses.json with ${entries.length} dependencies`);

src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import sha256 from "crypto-js/sha256";
4848
import { format as formatFns } from "date-fns";
4949
import { toPng } from "html-to-image";
5050
import PrivacyNoticeToggle from "./PrivacyNoticeToggle";
51+
import LicenseDialog from "./LicenseDialog";
5152
import ThemeToggle from "./ThemeToggle";
5253
import useTheme from "./useTheme";
5354
import { isCorrectResult, Result } from "./utils";
@@ -1132,7 +1133,7 @@ function App() {
11321133
<div className="flex flex-wrap justify-center items-center gap-x-4 gap-y-2 text-gray-600 dark:text-gray-400">
11331134
<span>Copyright &copy; <a href="https://github.com/Edwinexd" target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400 hover:underline">Edwin Sundberg</a> {new Date().getFullYear()}</span>
11341135
<span>-</span>
1135-
<a href="https://github.com/Edwinexd/sql-validator?tab=GPL-3.0-1-ov-file" target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400 hover:underline">GPL-3.0</a>
1136+
<LicenseDialog />
11361137
<a href="https://github.com/Edwinexd/sql-validator/issues" target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400 hover:underline">Report Issue</a>
11371138
<ChangelogDialog />
11381139
<PrivacyNoticeToggle></PrivacyNoticeToggle>

src/LicenseDialog.tsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { useEffect, useState } from "react";
2+
import { Dialog, DialogContent } from "@/components/ui/dialog";
3+
import { X } from "lucide-react";
4+
5+
interface LicenseEntry {
6+
name: string;
7+
version: string;
8+
license: string;
9+
repository?: string;
10+
author?: string;
11+
}
12+
13+
const PROJECT_LICENSES = [
14+
{
15+
name: "sql-validator (core)",
16+
license: "GPL-3.0",
17+
description: "The core application is licensed under the GNU General Public License v3.0.",
18+
url: "https://github.com/Edwinexd/sql-validator?tab=GPL-3.0-1-ov-file",
19+
},
20+
{
21+
name: "ra-engine (Relational Algebra)",
22+
license: "BSL-1.1",
23+
description: "The relational algebra engine (src/ra-engine/) is licensed under the Business Source License 1.1, converting to GPL-3.0 on 2035-03-20.",
24+
url: "https://github.com/Edwinexd/sql-validator/blob/master/src/ra-engine/LICENSE.md",
25+
},
26+
];
27+
28+
export default function LicenseDialog() {
29+
const [open, setOpen] = useState(false);
30+
const [deps, setDeps] = useState<LicenseEntry[]>([]);
31+
32+
useEffect(() => {
33+
if (open && deps.length === 0) {
34+
fetch("/licenses.json")
35+
.then(r => r.json())
36+
.then(setDeps)
37+
.catch(() => setDeps([]));
38+
}
39+
}, [open, deps.length]);
40+
41+
return (
42+
<>
43+
<button
44+
onClick={() => setOpen(true)}
45+
className="text-blue-600 dark:text-blue-400 hover:underline"
46+
>
47+
Licenses
48+
</button>
49+
<Dialog open={open} onOpenChange={setOpen}>
50+
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto p-0">
51+
<div className="sticky top-0 bg-white dark:bg-slate-900 border-b border-gray-200 dark:border-slate-700 px-6 py-4 flex items-center justify-between z-10">
52+
<h2 className="text-xl font-semibold">Licenses</h2>
53+
<button onClick={() => setOpen(false)} className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
54+
<X className="w-5 h-5" />
55+
</button>
56+
</div>
57+
58+
<div className="px-6 py-4 space-y-6">
59+
<div>
60+
<h3 className="text-lg font-semibold mb-3">Project Licenses</h3>
61+
<div className="space-y-3">
62+
{PROJECT_LICENSES.map(l => (
63+
<div key={l.name} className="border border-gray-200 dark:border-slate-700 rounded-md p-3">
64+
<div className="flex items-center justify-between">
65+
<span className="font-medium">{l.name}</span>
66+
<a href={l.url} target="_blank" rel="noopener noreferrer" className="text-sm text-blue-600 dark:text-blue-400 hover:underline">{l.license}</a>
67+
</div>
68+
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{l.description}</p>
69+
</div>
70+
))}
71+
</div>
72+
</div>
73+
74+
<div>
75+
<h3 className="text-lg font-semibold mb-3">Third-Party Dependencies</h3>
76+
{deps.length === 0 ? (
77+
<p className="text-sm text-gray-500">Loading...</p>
78+
) : (
79+
<div className="border border-gray-200 dark:border-slate-700 rounded-md overflow-hidden">
80+
<table className="w-full text-sm">
81+
<thead>
82+
<tr className="bg-gray-50 dark:bg-slate-800 text-left">
83+
<th className="px-3 py-2 font-medium">Package</th>
84+
<th className="px-3 py-2 font-medium">Version</th>
85+
<th className="px-3 py-2 font-medium">License</th>
86+
</tr>
87+
</thead>
88+
<tbody>
89+
{deps.map(dep => (
90+
<tr key={dep.name} className="border-t border-gray-100 dark:border-slate-700/50">
91+
<td className="px-3 py-1.5">
92+
{dep.repository ? (
93+
<a href={dep.repository} target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400 hover:underline">{dep.name}</a>
94+
) : dep.name}
95+
</td>
96+
<td className="px-3 py-1.5 text-gray-500 dark:text-gray-400">{dep.version}</td>
97+
<td className="px-3 py-1.5">{dep.license}</td>
98+
</tr>
99+
))}
100+
</tbody>
101+
</table>
102+
</div>
103+
)}
104+
</div>
105+
</div>
106+
</DialogContent>
107+
</Dialog>
108+
</>
109+
);
110+
}

0 commit comments

Comments
 (0)