UNPKG

json-merge-resolver

Version:

A rules-based JSON conflict resolver that parses Git conflict markers, reconstructs ours/theirs, and merges with deterministic strategies — beyond line-based merges.

13 lines (12 loc) 15.3 kB
"use strict";var mt=Object.create;var N=Object.defineProperty;var dt=Object.getOwnPropertyDescriptor;var yt=Object.getOwnPropertyNames;var wt=Object.getPrototypeOf,ht=Object.prototype.hasOwnProperty;var St=(t,e)=>{for(var r in e)N(t,r,{get:e[r],enumerable:!0})},W=(t,e,r,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of yt(e))!ht.call(t,n)&&n!==r&&N(t,n,{get:()=>e[n],enumerable:!(s=dt(e,n))||s.enumerable});return t};var S=(t,e,r)=>(r=t!=null?mt(wt(t)):{},W(e||!t||!t.__esModule?N(r,"default",{value:t,enumerable:!0}):r,t)),Ct=t=>W(N({},"__esModule",{value:!0}),t);var Lt={};St(Lt,{resolveConflicts:()=>Nt});module.exports=Ct(Lt);var J=require("child_process"),H=require("util"),K=(0,H.promisify)(J.execFile),G=async(t,e)=>{var h;let r=t.split(` `),s=[],n=[],o;(f=>(f[f.Normal=0]="Normal",f[f.InOurs=1]="InOurs",f[f.InTheirs=2]="InTheirs"))(o||(o={}));let a=0;for(let d of r){if(d.startsWith("<<<<<<<")){a=1;continue}else if(d.startsWith("=======")){a===1&&(a=2);continue}else if(d.startsWith(">>>>>>>")){a===2&&(a=0);continue}switch(a){case 0:s.push(d),n.push(d);break;case 1:s.push(d);break;case 2:n.push(d);break}}let c=s.join(` `),u=n.join(` `),l=await K("git",["show",`:1:${e.filename}`],{maxBuffer:1024*1024*50}).then(({stdout:d})=>d).catch(()=>null);if(c===u&&(c=(h=await K("git",["show",`HEAD:${e.filename}`],{maxBuffer:1024*1024*50}).then(({stdout:d})=>d).catch(()=>null))!=null?h:c),!c||!u)throw new Error("Conflict parsing resulted in empty content.");let m=Tt(e),[g,i]=await B(c,m),[[w],y]=await Promise.all((l?[u,l]:[u]).map(d=>B(d,[i])));return{ours:g,theirs:w,base:y==null?void 0:y[0],format:typeof i=="string"?i:i.name}},xt={json:"json",json5:"json5",yaml:"yaml",yml:"yaml",toml:"toml",xml:"xml"},Tt=t=>{var e,r;if(Array.isArray(t.parsers))return t.parsers;if(t.parsers)return t.parsers==="auto"?["json","json5","yaml","toml","xml"]:[t.parsers];if(t.filename){let s=xt[(r=(e=t.filename.split(".").pop())==null?void 0:e.toLowerCase())!=null?r:""];if(s)return[s]}return["json"]},B=async(t,e)=>{for(let r of e)try{return typeof r!="string"?[r.parser(t),r]:[await bt(r,t),r]}catch(s){console.debug(`Parser ${typeof r=="string"?r:r.name} failed:`,s)}throw new Error(`Failed to parse content. Tried parsers: ${e.map(r=>typeof r=="string"?r:r.name).join(", ")}`)},bt=async(t,e)=>{switch(t){case"json":return JSON.parse(e);case"json5":try{let{parse:r}=await import("json5");return r(e)}catch{throw new Error("json5 parser not installed. Please install as peer dependency.")}case"yaml":try{let{parse:r}=await import("yaml");return r(e)}catch{throw new Error("yaml parser not installed. Please install as peer dependency.")}case"toml":try{let{parse:r}=await import("smol-toml");return r(e)}catch{throw new Error("toml parser not installed. Please install as peer dependency.")}case"xml":try{let{XMLParser:r}=await import("fast-xml-parser");return new r().parse(e)}catch{throw new Error("fast-xml-parser not installed. Please install as peer dependency.")}}};var M=S(require("fs")),L=S(require("path")),V=async(t={},e)=>{var g,i,w,y,h,d,$;let r=(g=t.mode)!=null?g:"memory",s=(i=t.logDir)!=null?i:".logs",n=(w=t.singleFile)!=null?w:!1,o={stdout:(h=(y=t.levels)==null?void 0:y.stdout)!=null?h:e?["debug","info","warn","error"]:["warn","error"],file:($=(d=t.levels)==null?void 0:d.file)!=null?$:e?["info","debug","warn","error"]:["error"]};try{await M.promises.mkdir(s,{recursive:!0})}catch(p){console.warn(`Failed to create log directory: ${p}`)}let a=new Map,c=new Map,u=p=>{let f=n?"all":p;if(!c.has(f)){let x=L.default.join(s,n?"combined.log":`${p}.log`);c.set(f,M.default.createWriteStream(x,{flags:"a"}))}return c.get(f)},l=(p,f,x)=>{var b,O;let T={timestamp:new Date().toISOString(),level:f,message:x};o.stdout.includes(f)&&(f==="error"?console.error:console.log)(`[${p}] [${T.timestamp}] [${f.toUpperCase()}] ${T.message}`),o.file.includes(f)&&(r==="memory"?(a.has(p)||a.set(p,[]),(b=a.get(p))==null||b.push(T)):(O=u(p))==null||O.write(`[${T.timestamp}] [${f.toUpperCase()}] ${T.message} `))};return{info:(p,f)=>l(p,"info",f),warn:(p,f)=>l(p,"warn",f),error:(p,f)=>l(p,"error",f),debug:(p,f)=>l(p,"debug",f),flush:async()=>{if(r==="memory"){let p=new Date().toISOString().replace(/:/g,"-"),f=Array.from(a.entries()).map(async([x,T])=>{try{let b=L.default.join(s,n?`combined-${p}.log`:`${x}-${p}.log`),O=T.map(j=>`[${j.timestamp}] [${j.level.toUpperCase()}] ${j.message}`);await M.promises.mkdir(L.default.dirname(b),{recursive:!0}),await M.promises.appendFile(b,`${O.join(` `)} `)}catch(b){console.warn(`Failed to write log file for ${x}: ${b}`)}});await Promise.all(f)}for(let p of c.values())try{p.end()}catch(f){console.warn(`Failed to close log stream: ${f}`)}await new Promise(p=>setTimeout(p,10))}}};var z=S(require("fs/promises"));var E=async(t,e)=>{switch(t){case"json":case"json5":return JSON.stringify(e,null,2);case"yaml":{let{stringify:r}=await import("yaml");return r(e)}case"toml":{let{stringify:r}=await import("smol-toml");return r(e)}case"xml":{let{XMLBuilder:r}=await import("fast-xml-parser");return new r({}).build(e)}default:throw new Error(`Unknown format: ${t}`)}};var P=S(require("fs/promises")),A=S(require("path")),R=Symbol("MERGE_DROP"),X=".merge-backups",C=0,F=1,q=2,I=3,$t=t=>t.includes("<<<<<<<")&&t.includes("=======")&&t.includes(">>>>>>>"),Q=async({root:t=process.cwd(),include:e,exclude:r,matcher:s,includeNonConflicted:n,debug:o,backupDir:a=X})=>{try{await P.default.rm(a,{recursive:!0,force:!0})}catch{}for(let g of[...e,...r]){if(g.startsWith("!"))throw new Error(`Negation not allowed in include/exclude: ${g}`);g.includes("\\")&&console.warn(`Use '/' as path separator: ${g}`)}r.push(`${a}/**`);let c=g=>{let i=g.replace(/\\/g,"/");return s.isMatch(i,e)&&!s.isMatch(i,r)},u=Mt(e,a?[...r,a]:r,s),l=[],m=async g=>{let i=await P.default.readdir(g,{withFileTypes:!0});for(let w of i){let y=A.default.join(g,w.name),h=A.default.relative(t,y).replace(/\\+/g,"/");if(w.isDirectory())!/node_modules|\.git/.test(w.name)&&!u(h)&&await m(y);else if(c(h))try{let d=await P.default.readFile(y,"utf8");n||$t(d)?l.push({filePath:h,content:d}):o&&console.info(`Skipped (no conflicts): ${y}`)}catch{console.warn(`Skipped (unreadable): ${y}`)}}};return await m(t),l},Mt=(t,e,r)=>{if(t.length>0&&t.every(o=>!o.includes("/")&&!o.includes("**")))return()=>!1;let s=new Set,n=new Set;for(let o of t)if(o.includes("/")){let a=o.split("/").slice(0,-1).join("/");a&&s.add(a)}s.has("**")&&s.clear();for(let o of e)if(o.endsWith("/**")){let a=o.slice(0,-3);a&&n.add(a)}return o=>r.isMatch(o,[...n])||s.size>0&&!r.isMatch(o,[...s])},Y=async(t,e=X)=>{let r=A.default.relative(process.cwd(),t),s=A.default.join(e,r);return await P.default.mkdir(A.default.dirname(s),{recursive:!0}),await P.default.copyFile(t,s),s};var v=(t,e,r=[])=>{if(t!==R){if(t===void 0){let s=`__CONFLICT_MARKER::${e}__`;return r.push(e),s}if(Array.isArray(t))return t.map((s,n)=>v(s,`${e}[${n}]`,r)).filter(s=>s!==void 0);if(t&&typeof t=="object"){let s={};for(let[n,o]of Object.entries(t)){let a=v(o,e?`${e}.${n}`:n,r);a!==void 0&&(s[n]=a)}return s}return t}},et=async(t,e,r,s)=>{let n=[],o=v(t,"",n),a=await E(s,o);for(let c of n){let u=Z(e,c),l=Z(r,c),[m,g]=await Promise.all([u,l].map(y=>E(s,y))),i=["<<<<<<< ours",tt(m,2),"=======",tt(g,2),">>>>>>> theirs"].join(` `),w=`__CONFLICT_MARKER::${c}__`;a=a.replace(/json/.test(s)?JSON.stringify(w):w,i)}return a},Z=(t,e)=>{let r=e.replace(/\[(\d+)\]/g,".$1").split(".").filter(Boolean),s=t;for(let n of r)s=s==null?void 0:s[n];return s},tt=(t,e)=>{let r=" ".repeat(e);return t.split(` `).map(s=>s&&r+s).join(` `)};var rt={isMatch:(t,e)=>{let r=k(t);return e.some(s=>At(r,k(s)))}},st=async t=>{if(t==="micromatch"){let e;try{e=await import("micromatch")}catch{throw new Error("micromatch is not installed. Please add it as a dependency if you want to use it.")}return{isMatch:(r,s)=>{try{return e.isMatch(k(r),s.map(k))}catch(n){throw new Error(`micromatch failed to run isMatch: ${n.message}`)}}}}if(t==="picomatch"){let e;try{e=(await import("picomatch")).default}catch{throw new Error("picomatch is not installed. Please add it as a dependency if you want to use it.")}return{isMatch:(r,s)=>{try{return e(s.map(k))(k(r))}catch(n){throw new Error(`picomatch failed to run isMatch: ${n.message}`)}}}}throw new Error(`Unknown matcher name: ${t}`)},k=t=>t.replace(/\\[./]|\./g,e=>e==="\\."?"\0":e==="\\/"?"":"/"),Pt=t=>{let e=t.startsWith("!"),r=e?t.slice(1):t,s=[],n="",o=!1;for(let a of r)o?(n+=`\\${a}`,o=!1):a==="\\"?o=!0:a==="/"?(s.push(n),n=""):n+=a;return o&&(n+="\\"),s.push(n),e&&(s.negated=!0),s},At=(t,e)=>{let r=t.split("/"),s=Pt(e),n=s.negated===!0,o=nt(r,s);return n?!o:o},nt=(t,e)=>{let r=0,s=0;for(;r<t.length&&s<e.length;){let n=e[s];if(n==="**"){if(s===e.length-1)return!0;for(let o=0;r+o<=t.length;o++)if(nt(t.slice(r+o),e.slice(s+1)))return!0;return!1}if(!Ft(t[r],n))return!1;r++,s++}for(;s<e.length&&e[s]==="**";)s++;return r===t.length&&s===e.length},Ft=(t,e)=>{let r=!1,s=!1,n="",o="";for(let u=0;u<e.length;u++){let l=e[u];if(r){o+=l,r=!1;continue}if(l==="\\"){r=!0;continue}if(l==="*"){if(!s){s=!0,n=o,o="";continue}o+="*";continue}o+=l}if(r&&(o+="\\"),!s){let u=o.replace(/\\(.)/g,"$1");return t===u}let a=o.replace(/\\(.)/g,"$1"),c=n.replace(/\\(.)/g,"$1");return t.startsWith(c)&&t.endsWith(a)};var ot=(t,{rules:e,matcher:r})=>{var l,m,g;let s=(l=e.exact[t])!=null?l:[],n=(g=e.exactFields[(m=t.split(".").pop())!=null?m:""])!=null?g:[],o=Object.entries(e.patterns).filter(([i])=>r.isMatch(t,[i])).flatMap(([,i])=>i),a=[...s.flatMap(i=>i.strategies),...n.flatMap(i=>i.strategies),...o.flatMap(i=>i.strategies),...e.default],c=a.filter(i=>i.important),u=a.filter(i=>!i.important);return[...c,...u].map(i=>i.name)};var D=t=>typeof t=="object"&&t!==null&&!Array.isArray(t),kt={ours:({ours:t})=>({status:C,value:t}),theirs:({theirs:t})=>({status:C,value:t}),base:({base:t})=>({status:C,value:t}),drop:t=>({status:C,value:R}),skip:({path:t})=>({status:I,reason:`Skip strategy applied at ${t}`}),"non-empty":({ours:t,theirs:e,base:r})=>t!=null&&t!==""?{status:C,value:t}:e!=null&&e!==""?{status:C,value:e}:r!=null&&r!==""?{status:C,value:r}:{status:F},update:({ours:t,theirs:e})=>t!==void 0?{status:C,value:e}:{status:C,value:R},merge:async t=>{let{ours:e,theirs:r,base:s,path:n,filePath:o,ctx:a,conflicts:c,logger:u}=t;if(D(e)&&D(r)){let l=new Set([...Object.keys(e),...Object.keys(r)]),m={};for(let g of l){let i=g.replace(/\./g,"\0").replace(/\\/g,"");m[g]=await U({ours:e[g],theirs:r[g],base:D(s)?s[g]:void 0,path:n?`${n}.${i}`:i,filePath:o,ctx:a,conflicts:c,logger:u})}return{status:C,value:m}}return{status:F,reason:"Unmergeable type"}},concat:({ours:t,theirs:e,path:r})=>Array.isArray(t)&&Array.isArray(e)?{status:C,value:[...t,...e]}:{status:F,reason:`Cannot concat at ${r}`},unique:({ours:t,theirs:e,path:r})=>Array.isArray(t)&&Array.isArray(e)?{status:C,value:[...new Set([...t,...e])]}:{status:F,reason:`Cannot concat at ${r}`}},U=async({ours:t,theirs:e,base:r,path:s,filePath:n,ctx:o,conflicts:a,logger:c})=>{var l;if(t===e)return t;o._strategyCache||(o._strategyCache=new Map);let u=o._strategyCache.get(s);u||(u=ot(s,o.config),o._strategyCache.set(s,u)),c.debug(n!=null?n:"all",`path: ${s}, strategies: ${u.join(", ")||"none"}`);for(let m of u){c.debug(n!=null?n:"all",`Applying strategy '${m}' at ${s}`);let g=(l=kt[m])!=null?l:o.strategies[m];if(!g)continue;let i=await g({ours:t,theirs:e,base:r,path:s,filePath:n,ctx:o,conflicts:a,logger:c});switch(i.status){case C:return i.value;case F:continue;case I:a.push({path:s,reason:i.reason});return;case q:throw a.push({path:s,reason:i.reason}),new Error(`Merge failed at ${s}: ${i.reason}`)}}a.push({path:s,reason:`All strategies failed (tried: ${u.join(", ")||"none"})`,...o.config.debug?{ours:t,theirs:e,base:r}:{}})};var Et={defaultStrategy:["merge","ours"],include:["**/*.json","**/*.yaml","**/*.yml","**/*.xml","**/*.toml"],exclude:["**/node_modules/**","**/dist/**"],debug:!1,writeConflictSidecar:!1},lt=async t=>{let{rules:e,byStrategy:r,defaultStrategy:s,matcher:n,plugins:o,pluginConfig:a,customStrategies:c,...u}={...Et,...t},l=await Ot(o,a),m={...c,...l},g={exact:Object.create(null),exactFields:Object.create(null),patterns:Object.create(null),default:Rt(s)},i=typeof n=="string"?await st(n):n!=null?n:rt,w=0;if(r)for(let[y,h]of Object.entries(r)){if(!h)continue;let{name:d,important:$}=at(y);for(let p of h){let{key:f,important:x}=it(p);ct(g,f,{strategies:[{name:d,important:$||x}],order:w++,source:f})}}return e&&ut(e,[],(y,h)=>{let{key:d,important:$}=it(y),p=h.map(f=>{let{name:x,important:T}=at(f);return{name:x,important:T||$}});ct(g,d,{strategies:p,order:w++,source:d})}),{...u,rules:g,matcher:i,customStrategies:m}},Rt=t=>(Array.isArray(t)?t:[t]).map(r=>{let s=r.endsWith("!");return gt(r.slice(0,-1)),{name:s?r.slice(0,-1):r,important:s}}),at=t=>{let e=t.endsWith("!"),r=e?t.slice(0,-1):t;return gt(r),{name:r,important:e}},gt=t=>{if(t.endsWith("!"))throw new Error(`Strategy name "${t}" must not end with "!". Use "!" on field/glob to mark rule importance.`)},it=t=>{let e=t.endsWith("!"),r=e?t.slice(0,-1):t;if(!r)throw new Error(`Invalid rule key "${t}".`);return{key:r,important:e}},ct=(t,e,r)=>{if(/^\[.*\]$/.test(e)){let n=e.slice(1,-1);if(n.replace(/\\\./g,"").includes(".")||n.trim()==="")throw new Error(`Invalid bracket form "${e}". Use a single bare key like "[id]".`);let o=`**.${n}.**`;_(t.patterns,o,r);return}if(/[*?[\]]/.test(e)){_(t.patterns,e,r);return}e.includes(".")?_(t.exact,e,r):_(t.exactFields,e,r)},ut=(t,e,r)=>{for(let[s,n]of Object.entries(t)){let o=[...e,s];Array.isArray(n)?r(o.join("."),n):ut(n,o,r)}},_=(t,e,r)=>{var s;(s=t[e])!=null||(t[e]=[]),t[e].push(r)},Ot=async(t,e)=>{if(!(t!=null&&t.length))return{};let r={};for(let s of t)try{let n=await import(s),o=n.default||n,a=e==null?void 0:e[s],c=o instanceof Function?await o(a):o;if(!c.strategies)throw new Error(`Plugin "${s}" does not export strategies`);c.init&&await c.init(a),Object.assign(r,c.strategies)}catch(n){throw new Error(`Failed to load plugin "${s}": ${n}`)}return r};var jt=new Map,pt=async({ours:t,theirs:e,base:r,format:s,filePath:n,config:o,normalizedConfig:a,logger:c,autoStage:u=!1})=>{var g;let l=[],[m]=await Promise.all([U({ours:t,theirs:e,base:r,filePath:n,conflicts:l,path:"",ctx:{config:a,strategies:(g=a.customStrategies)!=null?g:{},_strategyCache:jt},logger:c}),Y(n,o.backupDir)]);if(c.debug(n,JSON.stringify({merged:m,conflicts:l},null,2)),l.length===0){let i=await E(s,m);if(await z.default.writeFile(n,i,"utf8"),u)try{(0,J.execSync)(`git add ${n}`)}catch(w){c.warn(n,`Failed to stage file: ${w}`)}return{success:!0,conflicts:[]}}else{let i=await et(m,t,e,s);return await Promise.all([z.default.writeFile(n,`${i} `,"utf8"),o.writeConflictSidecar?z.default.writeFile(`${n}.conflict.json`,`${JSON.stringify(l,null,2)} `):null]),{success:!1,conflicts:l}}};var Nt=async t=>{let e=await V(t.loggerConfig,t.debug),r=await lt(t),s=await Q(r);await Promise.all(s.map(async({filePath:n,content:o})=>{var m;let{theirs:a,ours:c,base:u,format:l}=await G(o,{filename:n,parsers:r.parsers});e.debug(n,JSON.stringify({ours:c,theirs:a,base:u,format:l},null,2)),await pt({ours:c,theirs:a,base:u,format:l,filePath:n,config:t,normalizedConfig:r,logger:e,autoStage:(m=r.autoStage)!=null?m:!0})})),await e.flush()};0&&(module.exports={resolveConflicts});