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", () => {
325325 expect(manifest.entries[0]?.noticeText).toBe("Override text");
326326 });
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
328381 it("applies repository overrides across monorepo packages", async () => {
329382 const fixture = await createFixture();
330383 await NodeFSP.rm(NodePath.join(fixture.dependencyRoot, "LICENSE"));
scripts/lib/third-party-licenses.ts+17-10
......@@ -67,7 +67,7 @@ interface CustomNoticeConfigEntry {
6767}
6868
6969interface PackageNoticeOverrideConfigEntry {
70 readonly generatedNotice?: GeneratedNoticeConfigEntry;
70 readonly generatedNotices?: ReadonlyArray<GeneratedNoticeConfigEntry>;
7171 readonly license?: string;
7272 readonly name?: string;
7373 readonly noticeFile?: string;
......@@ -257,8 +257,16 @@ function decodePackageOverrides(value: unknown): ReadonlyArray<PackageNoticeOver
257257 entry.generatedNotice === undefined
258258 ? undefined
259259 : decodeGeneratedNotice(entry.generatedNotice, `${context} generated notice`);
260 if (noticeFile !== undefined && generatedNotice !== undefined) {
261 throw new Error(`${context} cannot define both "noticeFile" and "generatedNotice".`);
260 const generatedNotices = decodeGeneratedNotices(entry.generatedNotices, context);
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 );
262270 }
263271 const sourceUrl = readOptionalString(entry, "sourceUrl", context);
264272 return {
......@@ -267,7 +275,8 @@ function decodePackageOverrides(value: unknown): ReadonlyArray<PackageNoticeOver
267275 ...(version !== undefined ? { version } : {}),
268276 ...(license !== undefined ? { license } : {}),
269277 ...(noticeFile !== undefined ? { noticeFile } : {}),
270 ...(generatedNotice !== undefined ? { generatedNotice } : {}),
278 ...(generatedNotice !== undefined ? { generatedNotices: [generatedNotice] } : {}),
279 ...(generatedNotices !== undefined ? { generatedNotices } : {}),
271280 ...(sourceUrl !== undefined ? { sourceUrl } : {}),
272281 };
273282 });
......@@ -396,9 +405,7 @@ function configuredGeneratedNotices(
396405): ReadonlyArray<GeneratedNoticeConfigEntry> {
397406 return [
398407 ...config.customNotices.flatMap((notice) => notice.generatedNotices ?? []),
399 ...config.packageOverrides.flatMap((override) =>
400 override.generatedNotice ? [override.generatedNotice] : [],
401 ),
408 ...config.packageOverrides.flatMap((override) => override.generatedNotices ?? []),
402409 ];
403410}
404411
......@@ -780,9 +787,9 @@ async function packageEntry(
780787 }
781788
782789 const repositoryKey = repositoryNoticeKey(collected.packageJson, license);
783 const noticeText = override?.generatedNotice
790 const noticeText = override?.generatedNotices
784791 ? await generatedNoticeText(
785 [override.generatedNotice],
792 override.generatedNotices,
786793 configDirectory,
787794 allowMissingGeneratedNotices,
788795 )
......@@ -793,7 +800,7 @@ async function packageEntry(
793800 : ((await packageNoticeText(collected.packageRoot, packageNotices)) ??
794801 (repositoryKey ? repositoryNotices.get(repositoryKey) : undefined));
795802 if (!noticeText) {
796 if (override?.generatedNotice && allowMissingGeneratedNotices) return null;
803 if (override?.generatedNotices && allowMissingGeneratedNotices) return null;
797804 throw new Error(
798805 `${name}@${version} does not include a license or notice file. Add a package override with "noticeFile" or "generatedNotice" in the third-party license config.`,
799806 );