1// @effect-diagnostics nodeBuiltinImport:off - Tests exercise the Node filesystem build boundary.
2
3import * as NodeFSP from "node:fs/promises";
4import * as NodeChildProcess from "node:child_process";
5import * as NodeOS from "node:os";
6import * as NodePath from "node:path";
7import * as NodeURL from "node:url";
8
9import { afterEach, describe, expect, it } from "vite-plus/test";
10
11import {
12 generateThirdPartyLicenseManifest,
13 THIRD_PARTY_LICENSES_FILE_NAME,
14 thirdPartyLicensesPlugin,
15} from "./third-party-licenses.js";
16
17const tempDirectories: string[] = [];
18const REPOSITORY_ROOT = NodePath.resolve(
19 NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)),
20 "../..",
21);
22const DEVICE_TOOL_VERSIONS = JSON.parse(
23 await NodeFSP.readFile(
24 NodePath.join(REPOSITORY_ROOT, "apps/server/src/device/deviceToolVersions.json"),
25 "utf8",
26 ),
27) as { readonly "agent-device": string; readonly "expo-device-hub": string };
28
29async function writeJson(path: string, value: unknown): Promise<void> {
30 await NodeFSP.mkdir(NodePath.dirname(path), { recursive: true });
31 await NodeFSP.writeFile(path, `${JSON.stringify(value)}\n`, "utf8");
32}
33
34async function createFixture(): Promise<{
35 readonly appManifest: string;
36 readonly configFile: string;
37 readonly dependencyRoot: string;
38 readonly root: string;
39}> {
40 const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3code-licenses-"));
41 tempDirectories.push(root);
42 const appManifest = NodePath.join(root, "package.json");
43 const dependencyRoot = NodePath.join(root, "node_modules", "demo-dependency");
44 const configFile = NodePath.join(root, "third-party-licenses.config.json");
45
46 await writeJson(appManifest, {
47 name: "fixture-app",
48 dependencies: { "demo-dependency": "1.2.3" },
49 });
50 await writeJson(NodePath.join(dependencyRoot, "package.json"), {
51 name: "demo-dependency",
52 version: "1.2.3",
53 license: "MIT",
54 main: "index.js",
55 repository: "example/demo-dependency",
56 });
57 await NodeFSP.writeFile(NodePath.join(dependencyRoot, "index.js"), "export {};\n", "utf8");
58 await NodeFSP.writeFile(
59 NodePath.join(dependencyRoot, "LICENSE"),
60 "Demo MIT license text\n",
61 "utf8",
62 );
63 await NodeFSP.writeFile(NodePath.join(root, "asset-notice.txt"), "Asset notice text\n", "utf8");
64 await writeJson(configFile, {
65 customNotices: [
66 {
67 name: "demo-asset",
68 license: "CC-BY-4.0",
69 noticeFile: "asset-notice.txt",
70 bundles: ["assets", "web"],
71 },
72 ],
73 packageOverrides: [],
74 });
75 return { appManifest, configFile, dependencyRoot, root };
76}
77
78afterEach(async () => {
79 await Promise.all(
80 tempDirectories
81 .splice(0)
82 .map((directory) => NodeFSP.rm(directory, { force: true, recursive: true })),
83 );
84});
85
86describe("third-party license generation", () => {
87 it("generates mobile notices without scripts workspace dependency links", async () => {
88 const fixture = await createFixture();
89 const generatorPath = NodePath.join(fixture.root, "scripts/lib/third-party-licenses.ts");
90 const versionsPath = NodePath.join(
91 fixture.root,
92 "apps/server/src/device/deviceToolVersions.json",
93 );
94 await Promise.all([
95 NodeFSP.mkdir(NodePath.dirname(generatorPath), { recursive: true }),
96 NodeFSP.mkdir(NodePath.dirname(versionsPath), { recursive: true }),
97 ]);
98 await Promise.all([
99 writeJson(NodePath.join(fixture.root, "scripts/package.json"), { type: "module" }),
100 NodeFSP.copyFile(
101 NodePath.join(REPOSITORY_ROOT, "scripts/lib/third-party-licenses.ts"),
102 generatorPath,
103 ),
104 NodeFSP.copyFile(
105 NodePath.join(REPOSITORY_ROOT, "apps/server/src/device/deviceToolVersions.json"),
106 versionsPath,
107 ),
108 writeJson(fixture.configFile, {
109 customNotices: Object.keys(DEVICE_TOOL_VERSIONS).map((name) => ({
110 name,
111 license: "MIT",
112 noticeFile: "asset-notice.txt",
113 bundles: ["device-tools"],
114 includeInBundles: ["mobile", "web"],
115 })),
116 }),
117 ]);
118
119 const output = NodeChildProcess.execFileSync(
120 process.execPath,
121 [
122 "--input-type=module",
123 "--eval",
124 `
125 import { pathToFileURL } from "node:url";
126 const { generateThirdPartyLicenseManifest } = await import(
127 pathToFileURL(process.argv[1]).href + "?version=1"
128 );
129 const manifest = await generateThirdPartyLicenseManifest({
130 configFile: process.argv[2],
131 packageManifests: [{ bundle: "mobile", path: process.argv[3] }],
132 });
133 process.stdout.write(JSON.stringify(manifest));
134 `,
135 generatorPath,
136 fixture.configFile,
137 fixture.appManifest,
138 ],
139 { cwd: fixture.root, encoding: "utf8" },
140 );
141 const manifest = JSON.parse(output) as {
142 entries: ReadonlyArray<{ name: string; version: string }>;
143 };
144 expect(manifest.entries).toEqual(
145 expect.arrayContaining([
146 expect.objectContaining({
147 name: "agent-device",
148 version: DEVICE_TOOL_VERSIONS["agent-device"],
149 }),
150 expect.objectContaining({
151 name: "expo-device-hub",
152 version: DEVICE_TOOL_VERSIONS["expo-device-hub"],
153 }),
154 ]),
155 );
156 });
157
158 it.each(["web", "mobile"])(
159 "uses installer pins for device tool notices in the %s manifest",
160 async (bundle) => {
161 const fixture = await createFixture();
162 await writeJson(fixture.configFile, {
163 customNotices: [
164 {
165 name: "agent-device",
166 license: "MIT",
167 noticeFile: "asset-notice.txt",
168 bundles: ["device-tools"],
169 includeInBundles: ["web", "mobile"],
170 },
171 {
172 name: "expo-device-hub",
173 license: "MIT",
174 noticeFile: "asset-notice.txt",
175 version: "0.0.0",
176 bundles: ["device-tools"],
177 includeInBundles: ["web", "mobile"],
178 },
179 {
180 name: "vendored-asset",
181 license: "MIT",
182 noticeFile: "asset-notice.txt",
183 version: "1.2.3",
184 bundles: ["web", "mobile"],
185 },
186 ],
187 });
188
189 const manifest = await generateThirdPartyLicenseManifest({
190 configFile: fixture.configFile,
191 packageManifests: [{ bundle, path: fixture.appManifest }],
192 });
193
194 expect(manifest.entries.find((entry) => entry.name === "agent-device")).toMatchObject({
195 version: DEVICE_TOOL_VERSIONS["agent-device"],
196 bundles: ["device-tools"],
197 });
198 expect(manifest.entries.find((entry) => entry.name === "expo-device-hub")).toMatchObject({
199 version: DEVICE_TOOL_VERSIONS["expo-device-hub"],
200 bundles: ["device-tools"],
201 });
202 expect(manifest.entries.find((entry) => entry.name === "vendored-asset")?.version).toBe(
203 "1.2.3",
204 );
205 },
206 );
207
208 it("keeps the GhosttyKit notice pinned to the vendored framework revision", async () => {
209 const [config, revision] = await Promise.all([
210 NodeFSP.readFile(NodePath.join(REPOSITORY_ROOT, "third-party-licenses.config.json"), "utf8"),
211 NodeFSP.readFile(
212 NodePath.join(REPOSITORY_ROOT, "apps/mobile/modules/t3-terminal/Vendor/libghostty/VERSION"),
213 "utf8",
214 ),
215 ]);
216
217 expect(config).toContain(revision.trim());
218 expect(config).toContain(
219 "https://github.com/Yash-Singh1/ghostty/tree/t3code/custom-io-ordered-feed",
220 );
221 });
222
223 it("collects production packages and custom asset notices", async () => {
224 const fixture = await createFixture();
225 const manifest = await generateThirdPartyLicenseManifest({
226 configFile: fixture.configFile,
227 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
228 });
229
230 expect(manifest).toEqual({
231 schemaVersion: 1,
232 entries: [
233 {
234 bundles: ["assets", "web"],
235 kind: "custom",
236 license: "CC-BY-4.0",
237 name: "demo-asset",
238 noticeText: "Asset notice text",
239 sourceUrl: null,
240 version: null,
241 },
242 {
243 bundles: ["web"],
244 kind: "package",
245 license: "MIT",
246 name: "demo-dependency",
247 noticeText: "Demo MIT license text",
248 sourceUrl: "https://github.com/example/demo-dependency",
249 version: "1.2.3",
250 },
251 ],
252 });
253 });
254
255 it("renders generated notices from the ignored SPDX cache", async () => {
256 const fixture = await createFixture();
257 await writeJson(
258 NodePath.join(fixture.root, ".generated/third-party-licenses/spdx/v3.28.0/MIT.json"),
259 {
260 licenseId: "MIT",
261 licenseText: "MIT License\n\nCopyright (c) <year> <copyright holders>\n\nPermission text",
262 },
263 );
264 await writeJson(fixture.configFile, {
265 customNotices: [
266 {
267 name: "generated-asset",
268 license: "MIT",
269 generatedNotices: [
270 {
271 licenseId: "MIT",
272 copyrights: ["Copyright (c) 2026 Example Author"],
273 preamble: ["Adapted for T3 Code."],
274 },
275 ],
276 bundles: ["assets", "web"],
277 },
278 ],
279 packageOverrides: [],
280 });
281
282 const manifest = await generateThirdPartyLicenseManifest({
283 configFile: fixture.configFile,
284 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
285 });
286
287 expect(manifest.entries.find((entry) => entry.name === "generated-asset")?.noticeText).toBe(
288 "Adapted for T3 Code.\n\nMIT License\n\nCopyright (c) 2026 Example Author\n\nPermission text",
289 );
290 });
291
292 it("omits generated rows without a cache during optional development", async () => {
293 const fixture = await createFixture();
294 await writeJson(fixture.configFile, {
295 customNotices: [
296 {
297 name: "generated-asset",
298 license: "MIT",
299 generatedNotices: [{ licenseId: "MIT" }],
300 bundles: ["assets", "web"],
301 },
302 ],
303 packageOverrides: [],
304 });
305
306 const manifest = await generateThirdPartyLicenseManifest({
307 configFile: fixture.configFile,
308 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
309 allowMissingGeneratedNotices: true,
310 });
311
312 expect(manifest.entries.some((entry) => entry.name === "generated-asset")).toBe(false);
313 });
314
315 it("finds packages whose exports hide both their manifest and entry point", async () => {
316 const fixture = await createFixture();
317 await writeJson(NodePath.join(fixture.dependencyRoot, "package.json"), {
318 name: "demo-dependency",
319 version: "1.2.3",
320 license: "MIT",
321 exports: {},
322 repository: "example/demo-dependency",
323 });
324
325 const manifest = await generateThirdPartyLicenseManifest({
326 configFile: fixture.configFile,
327 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
328 });
329
330 expect(manifest.entries.some((entry) => entry.name === "demo-dependency")).toBe(true);
331 });
332
333 it("includes custom notices selected by the dev server bundle", async () => {
334 const fixture = await createFixture();
335 await writeJson(fixture.configFile, {
336 customNotices: [
337 {
338 name: "desktop-only-asset",
339 license: "CC-BY-4.0",
340 noticeFile: "asset-notice.txt",
341 bundles: ["assets"],
342 includeInBundles: ["desktop"],
343 },
344 ],
345 packageOverrides: [],
346 });
347
348 const plugin = thirdPartyLicensesPlugin({
349 bundleName: "desktop",
350 configFile: fixture.configFile,
351 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
352 });
353 let middleware:
354 | ((
355 request: { readonly url: string },
356 response: {
357 statusCode: number;
358 setHeader(name: string, value: string): void;
359 end(body: string): void;
360 },
361 next: (error?: Error) => void,
362 ) => void)
363 | undefined;
364 if (typeof plugin.configureServer !== "function") {
365 throw new Error("Expected the license plugin to define a configureServer hook.");
366 }
367 plugin.configureServer.call(
368 {} as never,
369 {
370 middlewares: {
371 use(handler: typeof middleware) {
372 middleware = handler;
373 },
374 },
375 } as never,
376 );
377 if (!middleware) throw new Error("Expected the license plugin to register middleware.");
378
379 const responseBody = await new Promise<string>((resolve, reject) => {
380 middleware!(
381 { url: `/${THIRD_PARTY_LICENSES_FILE_NAME}` },
382 {
383 statusCode: 0,
384 setHeader() {},
385 end: resolve,
386 },
387 (error) => reject(error ?? new Error("License middleware skipped the request.")),
388 );
389 });
390 const manifest = JSON.parse(responseBody) as { entries: ReadonlyArray<{ name: string }> };
391
392 expect(manifest.entries.some((entry) => entry.name === "desktop-only-asset")).toBe(true);
393 });
394
395 it("fails when a production package has no distributable notice text", async () => {
396 const fixture = await createFixture();
397 await NodeFSP.rm(NodePath.join(fixture.dependencyRoot, "LICENSE"));
398
399 await expect(
400 generateThirdPartyLicenseManifest({
401 configFile: fixture.configFile,
402 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
403 }),
404 ).rejects.toThrow("does not include a license or notice file");
405 });
406
407 it("collects nested notices even when a package also has a root license", async () => {
408 const fixture = await createFixture();
409 await NodeFSP.mkdir(NodePath.join(fixture.dependencyRoot, "dist", "third-party"), {
410 recursive: true,
411 });
412 await NodeFSP.mkdir(NodePath.join(fixture.dependencyRoot, "lib"), { recursive: true });
413 await NodeFSP.writeFile(
414 NodePath.join(fixture.dependencyRoot, "dist", "third-party", "NOTICE.txt"),
415 "Nested notice\n",
416 "utf8",
417 );
418 await NodeFSP.writeFile(
419 NodePath.join(fixture.dependencyRoot, "lib", "license_header.js"),
420 "require('not-a-license');\n",
421 "utf8",
422 );
423
424 const manifest = await generateThirdPartyLicenseManifest({
425 configFile: fixture.configFile,
426 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
427 });
428
429 expect(manifest.entries.find((entry) => entry.name === "demo-dependency")?.noticeText).toBe(
430 "dist/third-party/NOTICE.txt\n\nNested notice\n\n---\n\nLICENSE\n\nDemo MIT license text",
431 );
432 });
433
434 it("uses package overrides for notices published outside the npm archive", async () => {
435 const fixture = await createFixture();
436 await NodeFSP.rm(NodePath.join(fixture.dependencyRoot, "LICENSE"));
437 await NodeFSP.writeFile(NodePath.join(fixture.root, "override.txt"), "Override text\n", "utf8");
438 await writeJson(fixture.configFile, {
439 customNotices: [],
440 packageOverrides: [
441 {
442 name: "demo-dependency",
443 noticeFile: "override.txt",
444 },
445 ],
446 });
447
448 const manifest = await generateThirdPartyLicenseManifest({
449 configFile: fixture.configFile,
450 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
451 });
452
453 expect(manifest.entries[0]?.noticeText).toBe("Override text");
454 });
455
456 it("keeps all licenses and attributions in a package with multiple generated notices", async () => {
457 const fixture = await createFixture();
458 await NodeFSP.rm(NodePath.join(fixture.dependencyRoot, "LICENSE"));
459 for (const licenseId of ["Apache-2.0", "BSD-3-Clause"]) {
460 await writeJson(
461 NodePath.join(
462 fixture.root,
463 `.generated/third-party-licenses/spdx/v3.28.0/${licenseId}.json`,
464 ),
465 { licenseId, licenseText: `${licenseId} terms` },
466 );
467 }
468 await writeJson(fixture.configFile, {
469 packageOverrides: [
470 {
471 name: "demo-dependency",
472 license: "(Apache-2.0 AND BSD-3-Clause)",
473 generatedNotices: [
474 { licenseId: "Apache-2.0", copyrights: ["Copyright primary author"] },
475 { licenseId: "BSD-3-Clause", copyrights: ["Copyright vendored author"] },
476 ],
477 },
478 ],
479 });
480 const manifest = await generateThirdPartyLicenseManifest({
481 configFile: fixture.configFile,
482 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
483 });
484 expect(manifest.entries[0]?.license).toBe("(Apache-2.0 AND BSD-3-Clause)");
485 expect(manifest.entries[0]?.noticeText).toBe(
486 "Copyright primary author\n\nApache-2.0 terms\n\n---\n\nCopyright vendored author\n\nBSD-3-Clause terms",
487 );
488 });
489
490 it("rejects conflicting package notice sources", async () => {
491 const fixture = await createFixture();
492 await writeJson(fixture.configFile, {
493 packageOverrides: [
494 {
495 name: "demo-dependency",
496 generatedNotice: { licenseId: "MIT" },
497 generatedNotices: [{ licenseId: "BSD-3-Clause" }],
498 },
499 ],
500 });
501 await expect(
502 generateThirdPartyLicenseManifest({
503 configFile: fixture.configFile,
504 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
505 }),
506 ).rejects.toThrow("can define only one");
507 });
508
509 it("applies repository overrides across monorepo packages", async () => {
510 const fixture = await createFixture();
511 await NodeFSP.rm(NodePath.join(fixture.dependencyRoot, "LICENSE"));
512 await NodeFSP.writeFile(
513 NodePath.join(fixture.root, "override.txt"),
514 "Repository text\n",
515 "utf8",
516 );
517 await writeJson(fixture.configFile, {
518 customNotices: [],
519 packageOverrides: [
520 {
521 repositoryUrl: "https://github.com/example/demo-dependency",
522 noticeFile: "override.txt",
523 },
524 ],
525 });
526
527 const manifest = await generateThirdPartyLicenseManifest({
528 configFile: fixture.configFile,
529 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
530 });
531
532 expect(manifest.entries[0]?.noticeText).toBe("Repository text");
533 });
534
535 it("reuses a repository license for packages from the same monorepo", async () => {
536 const fixture = await createFixture();
537 const siblingRoot = NodePath.join(fixture.root, "node_modules", "demo-sibling");
538 await writeJson(fixture.appManifest, {
539 name: "fixture-app",
540 dependencies: { "demo-dependency": "1.2.3", "demo-sibling": "2.0.0" },
541 });
542 await writeJson(NodePath.join(siblingRoot, "package.json"), {
543 name: "demo-sibling",
544 version: "2.0.0",
545 license: "MIT",
546 main: "index.js",
547 repository: "https://github.com/example/demo-dependency.git#main",
548 });
549 await NodeFSP.writeFile(NodePath.join(siblingRoot, "index.js"), "export {};\n", "utf8");
550
551 const manifest = await generateThirdPartyLicenseManifest({
552 configFile: fixture.configFile,
553 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
554 });
555
556 expect(manifest.entries.find((entry) => entry.name === "demo-sibling")?.noticeText).toBe(
557 "Demo MIT license text",
558 );
559 });
560
561 it("prefers version-specific repository overrides", async () => {
562 const fixture = await createFixture();
563 await NodeFSP.writeFile(NodePath.join(fixture.root, "generic.txt"), "Generic text\n", "utf8");
564 await NodeFSP.writeFile(NodePath.join(fixture.root, "exact.txt"), "Exact text\n", "utf8");
565 await writeJson(fixture.configFile, {
566 customNotices: [],
567 packageOverrides: [
568 {
569 repositoryUrl: "https://github.com/example/demo-dependency",
570 noticeFile: "generic.txt",
571 },
572 {
573 repositoryUrl: "https://github.com/example/demo-dependency",
574 version: "1.2.3",
575 noticeFile: "exact.txt",
576 },
577 ],
578 });
579
580 const manifest = await generateThirdPartyLicenseManifest({
581 configFile: fixture.configFile,
582 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
583 });
584
585 expect(manifest.entries[0]?.noticeText).toBe("Exact text");
586 });
587
588 it("omits custom notices for other bundles", async () => {
589 const fixture = await createFixture();
590 await writeJson(fixture.configFile, {
591 customNotices: [
592 {
593 name: "web-only-asset",
594 license: "MIT",
595 noticeFile: "asset-notice.txt",
596 bundles: ["assets", "web"],
597 },
598 ],
599 packageOverrides: [],
600 });
601
602 const manifest = await generateThirdPartyLicenseManifest({
603 configFile: fixture.configFile,
604 packageManifests: [{ bundle: "mobile", path: fixture.appManifest }],
605 });
606
607 expect(manifest.entries.some((entry) => entry.name === "web-only-asset")).toBe(false);
608 });
609
610 it("can show a multi-file notice under a label that differs from its client manifests", async () => {
611 const fixture = await createFixture();
612 await NodeFSP.writeFile(
613 NodePath.join(fixture.root, "tool-license.txt"),
614 "Tool license\n",
615 "utf8",
616 );
617 await NodeFSP.writeFile(
618 NodePath.join(fixture.root, "vendor-notice.txt"),
619 "Vendor notice\n",
620 "utf8",
621 );
622 await writeJson(fixture.configFile, {
623 customNotices: [
624 {
625 name: "optional-tool",
626 license: "MIT AND Apache-2.0",
627 noticeFiles: ["tool-license.txt", "vendor-notice.txt"],
628 bundles: ["device-tools"],
629 includeInBundles: ["mobile", "web"],
630 },
631 ],
632 packageOverrides: [],
633 });
634
635 const manifest = await generateThirdPartyLicenseManifest({
636 configFile: fixture.configFile,
637 packageManifests: [{ bundle: "mobile", path: fixture.appManifest }],
638 });
639
640 expect(manifest.entries.find((entry) => entry.name === "optional-tool")).toMatchObject({
641 bundles: ["device-tools"],
642 noticeText: "Tool license\n\n---\n\nVendor notice",
643 });
644 });
645
646 it("fails when a custom notice file is empty", async () => {
647 const fixture = await createFixture();
648 await NodeFSP.writeFile(NodePath.join(fixture.root, "asset-notice.txt"), "\n", "utf8");
649
650 await expect(
651 generateThirdPartyLicenseManifest({
652 configFile: fixture.configFile,
653 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
654 }),
655 ).rejects.toThrow('Custom third-party notice "demo-asset" is empty');
656 });
657
658 it("fails generation when custom notices produce duplicate navigation keys", async () => {
659 const fixture = await createFixture();
660 await writeJson(fixture.configFile, {
661 customNotices: [
662 {
663 name: "duplicate-asset",
664 license: "MIT",
665 noticeFile: "asset-notice.txt",
666 bundles: ["assets", "web"],
667 },
668 {
669 name: "duplicate-asset",
670 license: "CC0-1.0",
671 noticeFile: "asset-notice.txt",
672 bundles: ["assets", "web"],
673 },
674 ],
675 packageOverrides: [],
676 });
677
678 await expect(
679 generateThirdPartyLicenseManifest({
680 configFile: fixture.configFile,
681 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
682 }),
683 ).rejects.toThrow("duplicate custom notice for duplicate-asset");
684 });
685});