/
githubmirror
/
angular-cli
Обзор
Документация
Войти
/
githubmirror
/
angular-cli
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/angular/build/src/tools/esbuild/license-extractor.ts
185 строк
6 KB
Alan Agius
fix(@angular/build): support case-insensitive and alternative license file names
04 авг 2026, 19:31
04 авг 2026, 19:31
e1c7193
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import type { Metafile } from 'esbuild'; import { readFile, readdir } from 'node:fs/promises'; import path from 'node:path'; /** * The path segment used to signify that a file is part of a package. */ const NODE_MODULE_SEGMENT = 'node_modules'; /** * String constant for the NPM recommended custom license wording. * * See: https://docs.npmjs.com/cli/v9/configuring-npm/package-json#license * * Example: * ``` * { * "license" : "SEE LICENSE IN <filename>" * } * ``` */ const CUSTOM_LICENSE_TEXT = 'SEE LICENSE IN '; /** * A regular expression for commonly named license files found within packages. */ const LICENSE_FILE_REGEXP = /^(?:mit-)?licen[cs]e(?:$|[-._])/i; /** * Header text that will be added to the top of the output license extraction file. */ const EXTRACTION_FILE_HEADER = ''; /** * The package entry separator to use within the output license extraction file. */ const EXTRACTION_FILE_SEPARATOR = '-'.repeat(80) + '\n'; /** * Extracts license information for each node module package included in the output * files of the built code. This includes JavaScript and CSS output files. The esbuild * metafile generated during the bundling steps is used as the source of information * regarding what input files where included and where they are located. A path segment * of `node_modules` is used to indicate that a file belongs to a package and its license * should be include in the output licenses file. * * The package name and license field are extracted from the `package.json` file for the * package. If a license file (e.g., `LICENSE`) is present in the root of the package, it * will also be included in the output licenses file. * * @param metafile An esbuild metafile object. * @param rootDirectory The root directory of the workspace. * @returns A string containing the content of the output licenses file. */ 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)) { for (const [inputPath, { bytesInOutput }] of Object.entries(entry.inputs)) { // Skip if not included in output if (bytesInOutput <= 0) { continue; } // Skip already processed paths if (seenPaths.has(inputPath)) { continue; } seenPaths.add(inputPath); // Skip non-package paths if (!inputPath.includes(NODE_MODULE_SEGMENT)) { continue; } // Extract the package name from the path let baseDirectory = path.join(rootDirectory, inputPath); let nameOrScope, nameOrFile; let found = false; while (baseDirectory !== path.dirname(baseDirectory)) { const segment = path.basename(baseDirectory); if (segment === NODE_MODULE_SEGMENT) { found = true; break; } nameOrFile = nameOrScope; nameOrScope = segment; baseDirectory = path.dirname(baseDirectory); } // Skip non-package path edge cases that are not caught in the includes check above if (!found || !nameOrScope) { continue; } const packageName = nameOrScope.startsWith('@') ? `${nameOrScope}/${nameOrFile}` : 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; try { packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8')) as { name: string; version: string; // The object form is deprecated and should only be present in old packages license?: string | { type: string }; }; } catch { // Invalid package continue; } // Skip already processed packages const packageId = `${packageName}@${packageJson.version}`; if (seenPackages.has(packageId)) { continue; } seenPackages.add(packageId); // Attempt to find license text inside package let licenseText = ''; if ( typeof packageJson.license === 'string' && 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).trim(), ); if (customLicenseFile.startsWith('..') || path.isAbsolute(customLicenseFile)) { // Path is attempting to access files outside of the package // TODO: Issue warning? } else { customLicensePath = path.join(packageDirectory, customLicenseFile); try { licenseText = await readFile(customLicensePath, 'utf-8'); } catch {} } } else { // Search for a license file within the root of the package 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 {} } } } // Generate the package's license entry in the output content extractedLicenseContent += `Package: ${packageJson.name}\n`; extractedLicenseContent += `License: ${JSON.stringify(packageJson.license, null, 2)}\n`; extractedLicenseContent += `\n${licenseText}\n`; extractedLicenseContent += EXTRACTION_FILE_SEPARATOR; } } return extractedLicenseContent; }