authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-18 11:02:24-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-18 11:02:24-07:00
loged6fe339d71863f39a1d5405554a3bac892e55b6
treeac34b7178f35a00abc463db99cfa198960263faa
parent9118b6d3e868db161ca3f330c0a8011527348dce
signature Signed by PGP key B5690EEEBB952194

fix(build): retain multiple license notices for one package (#12489)


2 files changed, 70 insertions(+), 10 deletions(-)

scripts/lib/third-party-licenses.test.ts+53
...@@ -325,6 +325,59 @@ describe("third-party license generation", () => {...@@ -325,6 +325,59 @@ describe("third-party license generation", () => {
325 expect(manifest.entries[0]?.noticeText).toBe("Override text");325 expect(manifest.entries[0]?.noticeText).toBe("Override text");
326 });326 });
327327
328 it("keeps all licenses and attributions in a package with multiple generated notices", async () => {
329 const fixture = await createFixture();
330 await NodeFSP.rm(NodePath.join(fixture.dependencyRoot, "LICENSE"));
331 for (const licenseId of ["Apache-2.0", "BSD-3-Clause"]) {
332 await writeJson(
333 NodePath.join(
334 fixture.root,
335 `.generated/third-party-licenses/spdx/v3.28.0/${licenseId}.json`,
336 ),
337 { licenseId, licenseText: `${licenseId} terms` },
338 );
339 }
340 await writeJson(fixture.configFile, {
341 packageOverrides: [
342 {
343 name: "demo-dependency",
344 license: "(Apache-2.0 AND BSD-3-Clause)",
345 generatedNotices: [
346 { licenseId: "Apache-2.0", copyrights: ["Copyright primary author"] },
347 { licenseId: "BSD-3-Clause", copyrights: ["Copyright vendored author"] },
348 ],
349 },
350 ],
351 });
352 const manifest = await generateThirdPartyLicenseManifest({
353 configFile: fixture.configFile,
354 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
355 });
356 expect(manifest.entries[0]?.license).toBe("(Apache-2.0 AND BSD-3-Clause)");
357 expect(manifest.entries[0]?.noticeText).toBe(
358 "Copyright primary author\n\nApache-2.0 terms\n\n---\n\nCopyright vendored author\n\nBSD-3-Clause terms",
359 );
360 });
361
362 it("rejects conflicting package notice sources", async () => {
363 const fixture = await createFixture();
364 await writeJson(fixture.configFile, {
365 packageOverrides: [
366 {
367 name: "demo-dependency",
368 generatedNotice: { licenseId: "MIT" },
369 generatedNotices: [{ licenseId: "BSD-3-Clause" }],
370 },
371 ],
372 });
373 await expect(
374 generateThirdPartyLicenseManifest({
375 configFile: fixture.configFile,
376 packageManifests: [{ bundle: "web", path: fixture.appManifest }],
377 }),
378 ).rejects.toThrow("can define only one");
379 });
380
328 it("applies repository overrides across monorepo packages", async () => {381 it("applies repository overrides across monorepo packages", async () => {
329 const fixture = await createFixture();382 const fixture = await createFixture();
330 await NodeFSP.rm(NodePath.join(fixture.dependencyRoot, "LICENSE"));383 await NodeFSP.rm(NodePath.join(fixture.dependencyRoot, "LICENSE"));
scripts/lib/third-party-licenses.ts+17-10
...@@ -67,7 +67,7 @@ interface CustomNoticeConfigEntry {...@@ -67,7 +67,7 @@ interface CustomNoticeConfigEntry {
67}67}
6868
69interface PackageNoticeOverrideConfigEntry {69interface PackageNoticeOverrideConfigEntry {
70 readonly generatedNotice?: GeneratedNoticeConfigEntry;70 readonly generatedNotices?: ReadonlyArray<GeneratedNoticeConfigEntry>;
71 readonly license?: string;71 readonly license?: string;
72 readonly name?: string;72 readonly name?: string;
73 readonly noticeFile?: string;73 readonly noticeFile?: string;
...@@ -257,8 +257,16 @@ function decodePackageOverrides(value: unknown): ReadonlyArray<PackageNoticeOver...@@ -257,8 +257,16 @@ function decodePackageOverrides(value: unknown): ReadonlyArray<PackageNoticeOver
257 entry.generatedNotice === undefined257 entry.generatedNotice === undefined
258 ? undefined258 ? undefined
259 : decodeGeneratedNotice(entry.generatedNotice, `${context} generated notice`);259 : decodeGeneratedNotice(entry.generatedNotice, `${context} generated notice`);
260 if (noticeFile !== undefined && generatedNotice !== undefined) {260 const generatedNotices = decodeGeneratedNotices(entry.generatedNotices, context);
261 throw new Error(`${context} cannot define both "noticeFile" and "generatedNotice".`);261 if (
262 Number(noticeFile !== undefined) +
263 Number(generatedNotice !== undefined) +
264 Number(generatedNotices !== undefined) >
265 1
266 ) {
267 throw new Error(
268 `${context} can define only one of "noticeFile", "generatedNotice", or "generatedNotices".`,
269 );
262 }270 }
263 const sourceUrl = readOptionalString(entry, "sourceUrl", context);271 const sourceUrl = readOptionalString(entry, "sourceUrl", context);
264 return {272 return {
...@@ -267,7 +275,8 @@ function decodePackageOverrides(value: unknown): ReadonlyArray<PackageNoticeOver...@@ -267,7 +275,8 @@ function decodePackageOverrides(value: unknown): ReadonlyArray<PackageNoticeOver
267 ...(version !== undefined ? { version } : {}),275 ...(version !== undefined ? { version } : {}),
268 ...(license !== undefined ? { license } : {}),276 ...(license !== undefined ? { license } : {}),
269 ...(noticeFile !== undefined ? { noticeFile } : {}),277 ...(noticeFile !== undefined ? { noticeFile } : {}),
270 ...(generatedNotice !== undefined ? { generatedNotice } : {}),278 ...(generatedNotice !== undefined ? { generatedNotices: [generatedNotice] } : {}),
279 ...(generatedNotices !== undefined ? { generatedNotices } : {}),
271 ...(sourceUrl !== undefined ? { sourceUrl } : {}),280 ...(sourceUrl !== undefined ? { sourceUrl } : {}),
272 };281 };
273 });282 });
...@@ -396,9 +405,7 @@ function configuredGeneratedNotices(...@@ -396,9 +405,7 @@ function configuredGeneratedNotices(
396): ReadonlyArray<GeneratedNoticeConfigEntry> {405): ReadonlyArray<GeneratedNoticeConfigEntry> {
397 return [406 return [
398 ...config.customNotices.flatMap((notice) => notice.generatedNotices ?? []),407 ...config.customNotices.flatMap((notice) => notice.generatedNotices ?? []),
399 ...config.packageOverrides.flatMap((override) =>408 ...config.packageOverrides.flatMap((override) => override.generatedNotices ?? []),
400 override.generatedNotice ? [override.generatedNotice] : [],
401 ),
402 ];409 ];
403}410}
404411
...@@ -780,9 +787,9 @@ async function packageEntry(...@@ -780,9 +787,9 @@ async function packageEntry(
780 }787 }
781788
782 const repositoryKey = repositoryNoticeKey(collected.packageJson, license);789 const repositoryKey = repositoryNoticeKey(collected.packageJson, license);
783 const noticeText = override?.generatedNotice790 const noticeText = override?.generatedNotices
784 ? await generatedNoticeText(791 ? await generatedNoticeText(
785 [override.generatedNotice],792 override.generatedNotices,
786 configDirectory,793 configDirectory,
787 allowMissingGeneratedNotices,794 allowMissingGeneratedNotices,
788 )795 )
...@@ -793,7 +800,7 @@ async function packageEntry(...@@ -793,7 +800,7 @@ async function packageEntry(
793 : ((await packageNoticeText(collected.packageRoot, packageNotices)) ??800 : ((await packageNoticeText(collected.packageRoot, packageNotices)) ??
794 (repositoryKey ? repositoryNotices.get(repositoryKey) : undefined));801 (repositoryKey ? repositoryNotices.get(repositoryKey) : undefined));
795 if (!noticeText) {802 if (!noticeText) {
796 if (override?.generatedNotice && allowMissingGeneratedNotices) return null;803 if (override?.generatedNotices && allowMissingGeneratedNotices) return null;
797 throw new Error(804 throw new Error(
798 `${name}@${version} does not include a license or notice file. Add a package override with "noticeFile" or "generatedNotice" in the third-party license config.`,805 `${name}@${version} does not include a license or notice file. Add a package override with "noticeFile" or "generatedNotice" in the third-party license config.`,
799 );806 );