Pular para o conteúdo

license-compatibility

Previna que licenças copyleft ou incompatíveis entrem na sua árvore de dependências.

Quando você compila ou empacota dependências na sua aplicação, os termos de licença delas se aplicam à obra combinada. Uma única dependência GPL em um projeto permissivo (MIT/Apache-2.0) pode forçar o projeto inteiro a adotar termos copyleft. Esta regra verifica todas as dependências diretas contra uma allowlist de licenças permissivas.

package.json
{
"dependencies": { "zod": "^3.23.0" },
"devDependencies": { "readline-sync": "^1.4.10" }
}

Se readline-sync usa GPL-3.0, dispara uma violação mesmo como devDependency.

package.json
{
"dependencies": { "zod": "^3.23.0" },
"devDependencies": { "fast-check": "^4.7.0" }
}

Tanto zod (MIT) quanto fast-check (MIT) estão na allowlist de licenças permissivas.

/// <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;
}
/**
* Extrai o nome do pacote de um caminho node_modules.
* Trata pacotes regulares e escopados (@scope/name).
*/
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) {
// Escaneia TODOS os pacotes em node_modules — diretos E transitivos.
// Brace expansion cobre pacotes regulares e escopados.
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;
  • Alterar a allowlist: Adicione ou remova identificadores de licença baseado na licença do seu projeto. Projetos GPL podem permitir dependências GPL; projetos Apache-2.0 devem bloqueá-las.
  • Verificar apenas deps de produção: Remova devDependencies de allDeps se você só se preocupa com código empacotado/distribuído.
  • Escanear transitivas: Use ctx.glob("node_modules/{*,@*/*}/package.json") para escanear todos os pacotes instalados (incluindo escopados) recursivamente, não apenas dependências diretas.

Quando seu projeto usa uma licença permissiva (MIT, Apache-2.0, ISC, BSD) e você quer prevenir contaminação copyleft, especialmente em distribuições compiladas/empacotadas.

Em projetos licenciados sob GPL (onde dependências copyleft são compatíveis), ou quando você tem uma ferramenta SaaS dedicada de scanning de licenças (FOSSA, Snyk) que já bloqueia seu pipeline de CI.