UNPKG

pastoralist

Version:

A tool to watch over node module resolutions and overrides 🐑 👩🏽‍🌾

9 lines (8 loc) 8.2 kB
#!/usr/bin/env node import{program as I}from"commander";import Q from"ora";import Y from"gradient-string";import{readFileSync as L,writeFileSync as E,promises as C}from"fs";import{resolve as U}from"path";import G from"fast-glob";var v=process.env.DEBUG==="true"||!1,J="\u{1F411} \u{1F469}\u{1F3FD}\u200D\u{1F33E} Pastoralist:";var{writeFile:T}=C,{sync:A}=G,R=async e=>{let t=e?.path||"package.json",s=e?.root||"./",r=e?.isTesting||!1,o=v||e?.debug||!1,n=k({file:"scripts.ts",isLogging:o}),i=await F(t);if(!i){n.debug("no config found","update");return}let d=X(s),p=Object.keys(d);p.length>0&&n.debug(`Found patches for packages: ${p.join(", ")}`,"update");let u=B({options:e,config:i}),a=$(u);if(!a||Object.keys(a).length===0){n.debug("No overrides found","update"),await w({appendix:{},path:t,config:i,overrides:{}});return}let g={};if(e?.depPaths&&e?.depPaths.length>0){n.debug(`Using depPaths to find package.json files: ${e.depPaths.join(", ")}`,"update");let l=V(e.depPaths,e.ignore||[],s,n);l.length>0?(n.debug(`Processing ${l.length} package.json files from depPaths`,"update"),g=await K(l,u,n)):n.debug("No package.json files found matching depPaths","update")}else{let{dependencies:l={},devDependencies:j={},peerDependencies:b={}}=i;g=await D({overrides:a,dependencies:l,devDependencies:j,peerDependencies:b,packageName:i.name||"root"})}for(let l of Object.keys(g)){let j=l.split("@")[0],b=q(j,d);b.length>0&&(g[l].patches=b)}let{dependencies:m={},devDependencies:O={},peerDependencies:f={}}=i,P={...m,...O,...f},y=z(d,P);y.length>0&&(n.info(`Found ${y.length} potentially unused patch files:`,"update"),y.forEach(l=>n.info(` - ${l}`,"update")),n.info("Consider removing these patches if the packages are no longer used.","update"));let h=H(a,P),S=a;h.length>0&&(n.debug(`Found ${h.length} packages to remove from overrides: ${h.join(", ")}`,"update"),S=M(u,h)||a),!r&&await w({appendix:g,path:t,config:i,overrides:S})};var M=(e,t=[])=>{if(!e)return;let s=$(e);if(!s||Object.keys(s).length===0){c.debug("No overrides found to update","updateOverrides");return}return Object.entries(s).reduce((r,[o,n])=>(t.includes(o)||(r[o]=n),r),{})};async function w({appendix:e,path:t,config:s,overrides:r,isTesting:o=!1}){let n=r&&Object.keys(r).length>0,i=e&&Object.keys(e).length>0;if(v&&c.debug(`Processing package.json update: hasOverrides=${n}, hasAppendix=${i} Current config: ${JSON.stringify(s,null,2)}`,"updatePackageJSON"),!n&&!i){let u=["pastoralist","resolutions","overrides","pnpm"];for(let a of u)a==="pnpm"&&s.pnpm?(delete s.pnpm.overrides,Object.keys(s.pnpm).length===0&&delete s[a]):delete s[a]}else i&&(s.pastoralist={appendix:e});if(s?.resolutions&&(s.resolutions=r),s?.overrides&&(s.overrides=r),s?.pnpm?.overrides&&(s.pnpm.overrides=r),o)return s;let d=U(t),p=JSON.stringify(s,null,2);v&&c.debug(`Writing updated package.json: ${p}`,"updatePackageJSON"),E(d,p)}function B({config:e={}}){let t="resolveOverrides",s=_(e);if(!s){c.debug("No overrides configuration found",t);return}let{type:r,overrides:o}=s;if(!(Object.keys(o)?.length>0)||!r){c.debug("No active overrides found",t);return}let i=Object.keys(o)||[];if(i.some(u=>typeof o[u]=="object")){c.error("Pastoralist only supports simple overrides!",t),c.error("Pastoralist is bypassing the specified complex overrides. \u{1F44C}",t);return}let p=i.reduce((u,a)=>Object.assign(u,{[a]:o[a]}),{});return r==="pnpmOverrides"?{type:"pnpm",pnpm:{overrides:p}}:r==="resolutions"?{type:"resolutions",resolutions:p}:{type:"npm",overrides:p}}var _=({overrides:e={},pnpm:t={},resolutions:s={}}={})=>{let r=t?.overrides||{},o=Object.keys(e).length>0,n=Object.keys(r).length>0,i=Object.keys(s).length>0;if(!o&&!n&&!i)return;let d=[{type:"overrides",overrides:e},{type:"pnpmOverrides",overrides:r},{type:"resolutions",overrides:s}].filter(({overrides:a})=>Object.keys(a).length>0),p="defineOverride";if(d?.length>1){c.error("Only 1 override object allowed",p);return}return d[0]},$=e=>{let t=e?.type;if(!t){c.error("no type found","resolveOverridesProp");return}return t==="resolutions"?e?.resolutions:t==="pnpm"?e?.pnpm?.overrides:e?.overrides};async function W(e,t,s){let r=await F(e);if(!r)return;let{name:o,dependencies:n={},devDependencies:i={},peerDependencies:d={}}=r,p={...n,...i,...d};if(!Object.keys(p).some(O=>s.includes(O)))return;let g=D({overrides:t,dependencies:n,devDependencies:i,peerDependencies:d,packageName:o});return g&&Object.keys(g).length>0&&(r.pastoralist={appendix:g},await T(e,JSON.stringify(r,null,2))),{name:o,dependencies:n,devDependencies:i,appendix:g}}var D=({overrides:e={},appendix:t={},dependencies:s={},devDependencies:r={},peerDependencies:o={},packageName:n="",cache:i=new Map})=>{let d=Object.keys(e),p={...s,...r,...o},u=Object.keys(p);for(let a of d){if(!u.includes(a))continue;let m=e[a],O=p[a],f=`${a}@${m}`;if(i.has(f)){t[f]=i.get(f);continue}let h={dependents:{...t?.[f]?.dependents||{},[n]:`${a}@${O}`}};t[f]=h,i.set(f,h)}return Object.keys(t).forEach(a=>{(!t[a].dependents||Object.keys(t[a].dependents).length===0)&&delete t[a]}),t};function F(e){if(N.has(e))return N.get(e);try{let t=L(e,"utf8"),s=JSON.parse(t);return N.set(e,s),s}catch(t){c.error(`\u{1F411} \u{1F469}\u{1F3FD}\u200D\u{1F33E} Pastoralist found invalid JSON at: ${e}`,"resolveJSON",t);return}}var x=(e,t,s)=>(r,o,...n)=>{if(!t)return;let i=o?`[${o}]`:"";console[e](`${J}[${s}]${i} ${r}`,...n)},k=({file:e,isLogging:t=!1})=>({debug:x("debug",t,e),error:x("error",t,e),info:x("info",t,e)}),c=k({file:"scripts.ts",isLogging:v}),N=new Map,V=(e=[],t=[],s="./",r=c)=>{if(e.length===0)return r.debug("No depPaths provided","findPackageJsonFiles"),[];try{r.debug(`Searching with patterns: ${e.join(", ")}, ignoring: ${t.join(", ")}`,"findPackageJsonFiles");let o=A(e,{cwd:s,ignore:t,absolute:!1,onlyFiles:!0});return r.debug(`Found ${o.length} files`,"findPackageJsonFiles"),o}catch(o){return r.error("Error finding package.json files","findPackageJsonFiles",o),[]}},X=(e="./")=>{let t=["patches/*.patch",".patches/*.patch","*.patch","patches/**/*.patch"];try{let s=A(t,{cwd:e}),r={};return s.forEach(o=>{let n=o.split("/").pop()||"";if(!n.endsWith(".patch"))return;let i=n.replace(".patch",""),d;if(!i.includes("+"))d=i;else{let p=i.split("+");i.startsWith("@")&&p.length>=2?d=`${p[0]}/${p[1]}`:d=p[0]}d&&(r[d]||(r[d]=[]),r[d].push(o),c.debug(`Found patch for ${d}: ${o}`,"detectPatches"))}),r}catch(s){return c.error("Error detecting patches","detectPatches",s),{}}},q=(e,t)=>t[e]||[],z=(e,t)=>{let s=[];return Object.entries(e).forEach(([r,o])=>{t[r]||(s.push(...o),c.debug(`Found unused patches for ${r}: ${o.join(", ")}`,"findUnusedPatches"))}),s},H=(e={},t={})=>{let s=[];return Object.keys(e).forEach(r=>{t[r]||(s.push(r),c.debug(`Found unused override for ${r}: no longer in dependencies`,"findUnusedOverrides"))}),s};async function K(e,t,s=c){if(!t)return s.debug("No overrides data provided","constructAppendix"),{};let r=$(t);if(!r||Object.keys(r).length===0)return s.debug("No overrides found","constructAppendix"),{};let o=Object.keys(r),n={};for(let i of e){let d=await W(i,r,o);if(d?.appendix)for(let[p,u]of Object.entries(d.appendix))n[p]||(n[p]={dependents:{}}),n[p].dependents={...n[p].dependents,...u.dependents}}return n}async function Z(e={}){let t=v||e.debug,s=k({file:"program.ts",isLogging:t}),{isTestingCLI:r=!1,...o}=e;if(r){s.debug("action:options:","action",{options:e});return}try{let n=Y("green","tan"),i=Q(`\u{1F469}\u{1F3FD}\u200D\u{1F33E} ${n("pastoralist")} checking herd... `).start();await R(o),i.succeed(`\u{1F469}\u{1F3FD}\u200D\u{1F33E} ${n("pastoralist")} the herd is safe!`)}catch(n){s.error("action:fn","action",{error:n}),process.exit(1)}}I.description("Pastoralist, a utility CLI to manage your dependency overrides").option("--debug","enables debug mode").option("-p, --path <path>","specifies a path to a package.json").option("-d, --depPaths [depPaths...]","specifies a glob path to a package.jsons").option("--ignore [ignore...]","specifies a glob path to ignore").option("-r, --root <root>","specifies a root path").option("-t, --isTestingCLI","enables CLI testing, no scripts are run").option("--isTesting","enables testing, no scripts are run").action(Z).parse(process.argv);