license-compatibility
Previna que licenças copyleft ou incompatíveis entrem na sua árvore de dependências.
Detalhes da regra
Seção intitulada “Detalhes da regra”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.
Exemplos de código incorreto
Seção intitulada “Exemplos de código incorreto”{ "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.
Exemplos de código correto
Seção intitulada “Exemplos de código correto”{ "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.
Implementação da regra
Seção intitulada “Implementação da regra”/// <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;Personalização
Seção intitulada “Personalização”- 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
devDependenciesdeallDepsse 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 usar
Seção intitulada “Quando usar”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.
Quando não usar
Seção intitulada “Quando não usar”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.