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.
- Escanear transitivas (já é o padrão aqui): o glob acima percorre todo pacote instalado no nível superior de
node_modules/, incluindo pacotes escopados — com o layout plano do npm ou do Bun, isso também cobre as dependências transitivas içadas (hoisted). Dois requisitos: o ADR que contém a regra precisa definirrespectGitignore: false, porquenode_modules/normalmente está no gitignore ectx.globrespeita o.gitignorepor padrão; e árvoresnode_modulesaninhadas (layouts do pnpm, conflitos de versão) não são atravessadas — troque o padrão paranode_modules/**/package.jsonpara incluí-las, ao custo de também casar arquivospackage.jsondistribuídos dentro de pacotes. - Verificar apenas deps de produção: Leia o
package.jsonraiz comctx.readJSON("package.json")e filtrepkgFilespara os nomes presentes no campodependenciesdele, caso você só queira bloquear código empacotado/distribuído, e nãodevDependencies. Note que isso mantém apenas as dependências de produção diretas — os pacotes transitivos que elas trazem saem do escaneamento; cobrir o grafo de produção completo exige uma resolução baseada no lockfile.
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.