Skip to content
Open
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
127 changes: 114 additions & 13 deletions src/licenseUtils.js
Original file line number Diff line number Diff line change
@@ -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*";
Expand Down Expand Up @@ -42,16 +42,114 @@ const getLicenseName = pkg => {
return "";
};

const parsePackageName = pnpmPackageDir => {
// Handle scoped packages like:
// @[email protected] -> @github/relative-time-element
// @[email protected]_@[email protected] -> @citation-js/plugin-csl
// [email protected] -> 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) => {
Expand All @@ -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;
}, {});
Expand Down