Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,96 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
harness.expectFile('dist/3rdpartylicenses.txt').content.toContain('MIT');
harness.expectFile('dist/browser/en-US/main.js').toExist();
});

it(`should extract license from a package with a lowercase 'license' file`, async () => {
await harness.writeFile(
'node_modules/test-package-a/package.json',
JSON.stringify({
name: 'test-package-a',
version: '1.0.0',
main: 'index.js',
license: 'MIT',
}),
);
await harness.writeFile(
'node_modules/test-package-a/index.js',
'console.log("test-package-a");',
);
await harness.writeFile('node_modules/test-package-a/license', 'TEST_LOWERCASE_LICENSE_TEXT');
await harness.appendToFile('src/main.ts', "\nimport 'test-package-a';\n");

harness.useTarget('build', {
...BASE_OPTIONS,
extractLicenses: true,
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
harness
.expectFile('dist/3rdpartylicenses.txt')
.content.toContain('TEST_LOWERCASE_LICENSE_TEXT');
});

it(`should extract license from a package with an alternative license file name (e.g., 'MIT-LICENCE.txt')`, async () => {
await harness.writeFile(
'node_modules/test-package-b/package.json',
JSON.stringify({
name: 'test-package-b',
version: '1.0.0',
main: 'index.js',
license: 'MIT',
}),
);
await harness.writeFile(
'node_modules/test-package-b/index.js',
'console.log("test-package-b");',
);
await harness.writeFile(
'node_modules/test-package-b/MIT-LICENCE.txt',
'TEST_ALTERNATIVE_LICENSE_TEXT',
);
await harness.appendToFile('src/main.ts', "\nimport 'test-package-b';\n");

harness.useTarget('build', {
...BASE_OPTIONS,
extractLicenses: true,
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
harness
.expectFile('dist/3rdpartylicenses.txt')
.content.toContain('TEST_ALTERNATIVE_LICENSE_TEXT');
});

it(`should extract license from a package with a custom license file specified in package.json`, async () => {
await harness.writeFile(
'node_modules/test-package-c/package.json',
JSON.stringify({
name: 'test-package-c',
version: '1.0.0',
main: 'index.js',
license: 'SEE LICENSE IN custom-license.md',
}),
);
await harness.writeFile(
'node_modules/test-package-c/index.js',
'console.log("test-package-c");',
);
await harness.writeFile(
'node_modules/test-package-c/custom-license.md',
'TEST_CUSTOM_LICENSE_TEXT',
);
await harness.appendToFile('src/main.ts', "\nimport 'test-package-c';\n");

harness.useTarget('build', {
...BASE_OPTIONS,
extractLicenses: true,
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
harness.expectFile('dist/3rdpartylicenses.txt').content.toContain('TEST_CUSTOM_LICENSE_TEXT');
});
});
});
33 changes: 21 additions & 12 deletions packages/angular/build/src/tools/esbuild/license-extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import type { Metafile } from 'esbuild';
import { readFile } from 'node:fs/promises';
import { readFile, readdir } from 'node:fs/promises';
import path from 'node:path';

/**
Expand All @@ -30,9 +30,9 @@ const NODE_MODULE_SEGMENT = 'node_modules';
const CUSTOM_LICENSE_TEXT = 'SEE LICENSE IN ';

/**
* A list of commonly named license files found within packages.
* A regular expression for commonly named license files found within packages.
*/
const LICENSE_FILES = ['LICENSE', 'LICENSE.txt', 'LICENSE.md'];
const LICENSE_FILE_REGEXP = /^(?:mit-)?licen[cs]e(?:$|[-._])/i;

/**
* Header text that will be added to the top of the output license extraction file.
Expand Down Expand Up @@ -64,6 +64,7 @@ export async function extractLicenses(metafile: Metafile, rootDirectory: string)
let extractedLicenseContent = `${EXTRACTION_FILE_HEADER}\n${EXTRACTION_FILE_SEPARATOR}`;

const seenPaths = new Set<string>();
const seenPackageDirectories = new Set<string>();
const seenPackages = new Set<string>();

for (const entry of Object.values(metafile.outputs)) {
Expand Down Expand Up @@ -110,6 +111,11 @@ export async function extractLicenses(metafile: Metafile, rootDirectory: string)
: nameOrScope;
const packageDirectory = path.join(baseDirectory, packageName);

if (seenPackageDirectories.has(packageDirectory)) {
continue;
}
seenPackageDirectories.add(packageDirectory);

// Load the package's metadata to find the package's name, version, and license type
const packageJsonPath = path.join(packageDirectory, 'package.json');
let packageJson;
Expand All @@ -136,12 +142,12 @@ export async function extractLicenses(metafile: Metafile, rootDirectory: string)
let licenseText = '';
if (
typeof packageJson.license === 'string' &&
packageJson.license.toLowerCase().startsWith(CUSTOM_LICENSE_TEXT)
packageJson.license.toUpperCase().startsWith(CUSTOM_LICENSE_TEXT)
) {
// Attempt to load the package's custom license
let customLicensePath;
const customLicenseFile = path.normalize(
packageJson.license.slice(CUSTOM_LICENSE_TEXT.length + 1).trim(),
packageJson.license.slice(CUSTOM_LICENSE_TEXT.length).trim(),
);
if (customLicenseFile.startsWith('..') || path.isAbsolute(customLicenseFile)) {
// Path is attempting to access files outside of the package
Expand All @@ -150,17 +156,20 @@ export async function extractLicenses(metafile: Metafile, rootDirectory: string)
customLicensePath = path.join(packageDirectory, customLicenseFile);
try {
licenseText = await readFile(customLicensePath, 'utf-8');
break;
} catch {}
}
} else {
// Search for a license file within the root of the package
for (const potentialLicense of LICENSE_FILES) {
const packageLicensePath = path.join(packageDirectory, potentialLicense);
try {
licenseText = await readFile(packageLicensePath, 'utf-8');
break;
} catch {}
const entries = await readdir(packageDirectory, { withFileTypes: true }).catch(() => []);

for (const entry of entries) {
if ((entry.isFile() || entry.isSymbolicLink()) && LICENSE_FILE_REGEXP.test(entry.name)) {
const packageLicensePath = path.join(packageDirectory, entry.name);
try {
licenseText = await readFile(packageLicensePath, 'utf-8');
break;
} catch {}
}
}
}

Expand Down