diff --git a/src/licenseUtils.js b/src/licenseUtils.js index 3f58136..0b67864 100644 --- a/src/licenseUtils.js +++ b/src/licenseUtils.js @@ -1,12 +1,12 @@ -const { readFileSync } = require("fs"); -const { resolve } = require("path"); +const { readFileSync, existsSync } = require("fs"); +const { resolve, dirname, join } = require("path"); const glob = require("glob"); const { template } = require("lodash"); const satisfiesGlob = require("minimatch"); const { satisfies: isSatisfiedVersion } = require("semver"); const isValidLicense = require("spdx-expression-validate"); const isSatisfiedLicense = require("spdx-satisfies"); -const wrap = require("wrap-ansi"); +const wrap = require("wrap-ansi").default || require("wrap-ansi"); const LicenseError = require("./LicenseError"); const licenseGlob = "LICEN@(C|S)E*"; @@ -42,16 +42,114 @@ const getLicenseName = pkg => { return ""; }; +const parsePackageName = pnpmPackageDir => { + // Handle scoped packages like: + // @github+relative-time-element@4.4.8 -> @github/relative-time-element + // @citation-js+plugin-csl@0.7.18_@citation-js+core@0.7.18 -> @citation-js/plugin-csl + // lodash@4.17.21 -> lodash + + // Remove peer dependency part (everything after _) + let cleanPackageDir = pnpmPackageDir.split("_")[0]; + + // Find the last @ which separates the version + const lastAtIndex = cleanPackageDir.lastIndexOf("@"); + if (lastAtIndex === -1) { + return cleanPackageDir; // No version specified + } + + let nameWithScope = cleanPackageDir.substring(0, lastAtIndex); + + // Handle scoped packages: replace + with / + if (nameWithScope.startsWith("@")) { + nameWithScope = nameWithScope.replace(/\+/g, "/"); + } + + return nameWithScope; +}; + +const findPackageJson = dependencyPath => { + const directPath = join(dependencyPath, "package.json"); + if (existsSync(directPath)) { + return directPath; + } + + // Handle pnpm .pnpm directory structure + const match = dependencyPath.match(/[/\\]\.pnpm[/\\]([^/\\]+)/); + if (match) { + const pnpmPackageDir = match[1]; + const packageName = parsePackageName(pnpmPackageDir); + + // For pnpm structure: .pnpm/package@version/node_modules/package/ + const pnpmRoot = dependencyPath.substring(0, dependencyPath.indexOf(".pnpm")); + const pnpmBasePath = join(pnpmRoot, ".pnpm", pnpmPackageDir, "node_modules", packageName); + const pnpmPackageJsonPath = join(pnpmBasePath, "package.json"); + + if (existsSync(pnpmPackageJsonPath)) { + return pnpmPackageJsonPath; + } + + const possiblePaths = [ + join(dependencyPath, "node_modules", packageName, "package.json"), + join(dirname(dependencyPath), pnpmPackageDir, "node_modules", packageName, "package.json"), + join( + dirname(dirname(dependencyPath)), + pnpmPackageDir, + "node_modules", + packageName, + "package.json" + ) + ]; + + for (const pnpmPath of possiblePaths) { + if (existsSync(pnpmPath)) { + return pnpmPath; + } + } + } + + // Traverse up directory tree as fallback + let currentPath = dependencyPath; + let attempts = 0; + const maxAttempts = 5; + + while (attempts < maxAttempts) { + const packageJsonPath = join(currentPath, "package.json"); + if (existsSync(packageJsonPath)) { + return packageJsonPath; + } + + const parentPath = dirname(currentPath); + if (parentPath === currentPath) break; + currentPath = parentPath; + attempts++; + } + + return directPath; +}; + const getLicenseInformationForDependency = dependencyPath => { - const pkg = require(`${dependencyPath}/package.json`); - return { - name: pkg.name, - version: pkg.version, - author: (pkg.author && pkg.author.name) || pkg.author, - repository: (pkg.repository && pkg.repository.url) || pkg.repository, - licenseName: getLicenseName(pkg), - licenseText: getLicenseContents(dependencyPath) - }; + try { + const packageJsonPath = findPackageJson(dependencyPath); + const pkg = require(packageJsonPath); + const actualDependencyPath = dirname(packageJsonPath); + + // Skip packages without proper names (likely internal or broken packages) + if (!pkg.name || pkg.name.startsWith(".")) { + return null; + } + + return { + name: pkg.name, + version: pkg.version, + author: (pkg.author && pkg.author.name) || pkg.author, + repository: (pkg.repository && pkg.repository.url) || pkg.repository, + licenseName: getLicenseName(pkg), + licenseText: getLicenseContents(actualDependencyPath) + }; + } catch (error) { + // Skip packages that can't be read + return null; + } }; const getLicenseInformationForCompilation = (compilation, filter) => { @@ -60,7 +158,10 @@ const getLicenseInformationForCompilation = (compilation, filter) => { const match = dependencyPath.match(filter); if (match) { const [, rootPath, dependencyName] = match; - memo[dependencyName] = getLicenseInformationForDependency(rootPath); + const licenseInfo = getLicenseInformationForDependency(rootPath); + if (licenseInfo) { + memo[dependencyName] = licenseInfo; + } } return memo; }, {});