license-compatibility
Prevent copyleft or incompatible licenses from entering your dependency tree.
Rule details
Section titled “Rule details”When you compile or bundle dependencies into your application, their license terms apply to the combined work. A single GPL dependency in a permissive (MIT/Apache-2.0) project can force the entire project to adopt copyleft terms. This rule checks all direct dependencies against a permissive-license allowlist.
Examples of incorrect code
Section titled “Examples of incorrect code”{ "dependencies": { "zod": "^3.23.0" }, "devDependencies": { "readline-sync": "^1.4.10" }}If readline-sync uses GPL-3.0, it triggers a violation even as a devDependency.
Examples of correct code
Section titled “Examples of correct code”{ "dependencies": { "zod": "^3.23.0" }, "devDependencies": { "fast-check": "^4.7.0" }}Both zod (MIT) and fast-check (MIT) are on the permissive allowlist.
Rule implementation
Section titled “Rule implementation”/// <reference path="../rules.d.ts" />
const ALLOWED_LICENSES = new Set([ "MIT", "Apache-2.0", "ISC", "BSD-2-Clause", "BSD-3-Clause", "0BSD", "CC0-1.0", "Unlicense", "BlueOak-1.0.0",]);
function isAllowed(license: string | undefined): boolean { if (!license) return false; if (ALLOWED_LICENSES.has(license)) return true;
// Handle SPDX OR expressions: at least one option must be allowed const normalized = license.trim().replace(/^\(/u, "").replace(/\)$/u, ""); if (ALLOWED_LICENSES.has(normalized)) return true;
if (normalized.includes(" OR ")) { return normalized.split(" OR ").some((l) => ALLOWED_LICENSES.has(l.trim())); }
return false;}
/** * Extract package name from a node_modules path. * Handles both regular and scoped (@scope/name) packages. */function extractPackageName(path: string): string { const parts = path.replaceAll("\\", "/").split("/"); const nmIdx = parts.lastIndexOf("node_modules"); if (nmIdx === -1) return path; const afterNm = parts.slice(nmIdx + 1); if (afterNm[0]?.startsWith("@") && afterNm.length >= 2) { return `${afterNm[0]}/${afterNm[1]}`; } return afterNm[0] ?? path;}
export default { rules: { "no-copyleft-deps": { description: "All dependencies (including transitive) must use permissive licenses", async check(ctx) { // Scan ALL packages in node_modules, direct AND transitive. // Brace expansion covers both regular and scoped packages. const pkgFiles = await ctx.glob("node_modules/{*,@*/*}/package.json");
const depResults = await Promise.all( pkgFiles.map(async (pkgPath) => { try { const depPkg = (await ctx.readJSON(pkgPath)) as { license?: string; }; return { dep: extractPackageName(pkgPath), license: depPkg.license, }; } catch { return null; } }) );
for (const result of depResults) { if (result === null) continue; if (!isAllowed(result.license)) { ctx.report.violation({ message: `Dependency "${result.dep}" has disallowed license: "${result.license ?? "(none)"}".`, file: "package.json", fix: `Remove "${result.dep}" or find an alternative with a permissive license.`, }); } } }, }, },} satisfies RuleSet;Customization
Section titled “Customization”- Change the allowlist: Add or remove license identifiers based on your project’s license. GPL projects can allow GPL dependencies; Apache-2.0 projects should block them.
- Scan transitives (the default here): the glob above walks every package installed at the top level of
node_modules/, scoped packages included — with npm’s or Bun’s flat layout that covers hoisted transitive dependencies too. Two requirements: the containing ADR must setrespectGitignore: false, becausenode_modules/is normally gitignored andctx.globhonors.gitignoreby default; and nestednode_modulestrees (pnpm layouts, version conflicts) are not crossed — switch the pattern tonode_modules/**/package.jsonto include them, at the cost of also matchingpackage.jsonfiles that ship inside packages. - Check only production deps: Read the root
package.jsonwithctx.readJSON("package.json")and filterpkgFilesdown to names present in itsdependenciesfield if you only want to gate bundled/shipped code, notdevDependencies. Note this keeps direct production dependencies only — the transitive packages they pull in are dropped from the scan; covering the full production graph needs a lockfile-aware resolution.
When to use it
Section titled “When to use it”When your project uses a permissive license (MIT, Apache-2.0, ISC, BSD) and you want to prevent copyleft contamination, especially in compiled/bundled distributions.
When not to use it
Section titled “When not to use it”In GPL-licensed projects (where copyleft dependencies are compatible), or when you have a dedicated license-scanning SaaS tool (FOSSA, Snyk) that already gates your CI pipeline.