json-conflict-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.
8 lines (7 loc) • 13.2 kB
JavaScript
var ft=Object.create;var E=Object.defineProperty;var mt=Object.getOwnPropertyDescriptor;var pt=Object.getOwnPropertyNames;var dt=Object.getPrototypeOf,yt=Object.prototype.hasOwnProperty;var wt=(t,e)=>{for(var r in e)E(t,r,{get:e[r],enumerable:!0})},W=(t,e,r,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of pt(e))!yt.call(t,n)&&n!==r&&E(t,n,{get:()=>e[n],enumerable:!(s=mt(e,n))||s.enumerable});return t};var d=(t,e,r)=>(r=t!=null?ft(dt(t)):{},W(e||!t||!t.__esModule?E(r,"default",{value:t,enumerable:!0}):r,t)),ht=t=>W(E({},"__esModule",{value:!0}),t);var Rt={};wt(Rt,{processMerge:()=>gt,resolveGitMergeFiles:()=>Et});module.exports=ht(Rt);var lt=require("child_process");var k=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 K={isMatch:(t,e)=>{let r=M(t);return e.some(s=>Ct(r,M(s)))}},B=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(M(r),s.map(M))}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(M))(M(r))}catch(n){throw new Error(`picomatch failed to run isMatch: ${n.message}`)}}}}throw new Error(`Unknown matcher name: ${t}`)},M=t=>t.replace(/\\[./]|\./g,e=>e==="\\."?"\0":e==="\\/"?"":"/"),St=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},Ct=(t,e)=>{let r=t.split("/"),s=St(e),n=s.negated===!0,o=J(r,s);return n?!o:o},J=(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(J(t.slice(r+o),e.slice(s+1)))return!0;return!1}if(!xt(t[r],n))return!1;r++,s++}for(;s<e.length&&e[s]==="**";)s++;return r===t.length&&s===e.length},xt=(t,e)=>{let r=!1,s=!1,n="",o="";for(let m=0;m<e.length;m++){let l=e[m];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 m=o.replace(/\\(.)/g,"$1");return t===m}let a=o.replace(/\\(.)/g,"$1"),c=n.replace(/\\(.)/g,"$1");return t.startsWith(c)&&t.endsWith(a)};var H=(t,{rules:e,matcher:r})=>{var l,p,u;let s=(l=e.exact[t])!=null?l:[],n=(u=e.exactFields[(p=t.split(".").pop())!=null?p:""])!=null?u:[],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),m=a.filter(i=>!i.important);return[...c,...m].map(i=>i.name)};var L=d(require("fs/promises")),R=d(require("path")),F=Symbol("MERGE_DROP"),Tt=".merge-backups",y=0,P=1,G=2,I=3;var V=async(t,e=Tt)=>{let r=R.default.relative(process.cwd(),t),s=R.default.join(e,r);return await L.default.mkdir(R.default.dirname(s),{recursive:!0}),await L.default.copyFile(t,s),s};var v=t=>typeof t=="object"&&t!==null&&!Array.isArray(t),bt={ours:({ours:t})=>({status:y,value:t}),theirs:({theirs:t})=>({status:y,value:t}),base:({base:t})=>({status:y,value:t}),drop:t=>({status:y,value:F}),skip:({path:t})=>({status:I,reason:`Skip strategy applied lt ${t}`}),"non-empty":({ours:t,theirs:e,base:r})=>t!=null&&t!==""?{status:y,value:t}:e!=null&&e!==""?{status:y,value:e}:r!=null&&r!==""?{status:y,value:r}:{status:P},update:({ours:t,theirs:e})=>t!==void 0?{status:y,value:e}:{status:y,value:F},merge:async t=>{let{ours:e,theirs:r,base:s,path:n,filePath:o,ctx:a,conflicts:c,logger:m}=t;if(v(e)&&v(r)){let l=new Set([...Object.keys(e),...Object.keys(r)]),p={};for(let u of l){let i=u.replace(/\./g,"\0").replace(/\\/g,"");p[u]=await z({ours:e[u],theirs:r[u],base:v(s)?s[u]:void 0,path:n?`${n}.${i}`:i,filePath:o,ctx:a,conflicts:c,logger:m})}return{status:y,value:p}}return{status:P,reason:"Unmergeable type"}},concat:({ours:t,theirs:e,path:r})=>Array.isArray(t)&&Array.isArray(e)?{status:y,value:[...t,...e]}:{status:P,reason:`Cannot concat lt ${r}`},unique:({ours:t,theirs:e,path:r})=>Array.isArray(t)&&Array.isArray(e)?{status:y,value:[...new Set([...t,...e])]}:{status:P,reason:`Cannot concat lt ${r}`}},z=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 m=o._strategyCache.get(s);m||(m=H(s,o.config),o._strategyCache.set(s,m)),c.debug(n!=null?n:"all",`path: ${s}, strategies: ${m.join(", ")||"none"}`);for(let p of m){c.debug(n!=null?n:"all",`Applying strategy '${p}' lt ${s}`);let u=(l=bt[p])!=null?l:o.strategies[p];if(!u)continue;let i=await u({ours:t,theirs:e,base:r,path:s,filePath:n,ctx:o,conflicts:a,logger:c});switch(i.status){case y:return i.value;case P:continue;case I:a.push({path:s,reason:i.reason});return;case G:throw a.push({path:s,reason:i.reason}),new Error(`Merge failed lt ${s}: ${i.reason}`)}}a.push({path:s,reason:`All strategies failed (tried: ${m.join(", ")||"none"})`,...o.config.debug?{ours:t,theirs:e,base:r}:{}})};var D=(t,e,r=[])=>{if(t!==F){if(t===void 0){let s=`__CONFLICT_MARKER::${e}__`;return r.push(e),s}if(Array.isArray(t))return t.map((s,n)=>D(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=D(o,e?`${e}.${n}`:n,r);a!==void 0&&(s[n]=a)}return s}return t}},Q=async(t,e,r,s)=>{let n=[],o=D(t,"",n),a=await k(s,o);for(let c of n){let m=X(e,c),l=X(r,c),[p,u]=await Promise.all([m,l].map(h=>k(s,h))),i=["<<<<<<< ours",q(p,2),"=======",q(u,2),">>>>>>> theirs"].join(`
`),w=`__CONFLICT_MARKER::${c}__`;a=a.replace(/json/.test(s)?JSON.stringify(w):w,i)}return a},X=(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},q=(t,e)=>{let r=" ".repeat(e);return t.split(`
`).map(s=>s&&r+s).join(`
`)};var $t={defaultStrategy:["merge","ours"],include:["**/*.json","**/*.yaml","**/*.yml","**/*.xml","**/*.toml"],exclude:["**/node_modules/**","**/dist/**"],debug:!1,writeConflictSidecar:!1},et=async t=>{let{rules:e,byStrategy:r,defaultStrategy:s,matcher:n,plugins:o,pluginConfig:a,customStrategies:c,...m}={...$t,...t},l=await Pt(o,a),p={...c,...l},u={exact:Object.create(null),exactFields:Object.create(null),patterns:Object.create(null),default:Mt(s)},i=typeof n=="string"?await B(n):n!=null?n:K,w=0;if(r)for(let[h,C]of Object.entries(r)){if(!C)continue;let{name:x,important:b}=Y(h);for(let g of C){let{key:f,important:S}=Z(g);tt(u,f,{strategies:[{name:x,important:b||S}],order:w++,source:f})}}return e&&st(e,[],(h,C)=>{let{key:x,important:b}=Z(h),g=C.map(f=>{let{name:S,important:T}=Y(f);return{name:S,important:T||b}});tt(u,x,{strategies:g,order:w++,source:x})}),{...m,rules:u,matcher:i,customStrategies:p}},Mt=t=>(Array.isArray(t)?t:[t]).map(r=>{let s=r.endsWith("!");return rt(r.slice(0,-1)),{name:s?r.slice(0,-1):r,important:s}}),Y=t=>{let e=t.endsWith("!"),r=e?t.slice(0,-1):t;return rt(r),{name:r,important:e}},rt=t=>{if(t.endsWith("!"))throw new Error(`Strategy name "${t}" must not end with "!". Use "!" on field/glob to mark rule importance.`)},Z=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}},tt=(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}.**`;O(t.patterns,o,r);return}if(/[*?[\]]/.test(e)){O(t.patterns,e,r);return}e.includes(".")?O(t.exact,e,r):O(t.exactFields,e,r)},st=(t,e,r)=>{for(let[s,n]of Object.entries(t)){let o=[...e,s];Array.isArray(n)?r(o.join("."),n):st(n,o,r)}},O=(t,e,r)=>{var n;((n=t[e])!=null?n:t[e]=[]).push(r)},Pt=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 nt=d(require("fs")),j=require("fs"),N=d(require("path")),ot=async(t={},e)=>{var u,i,w,h,C,x,b;let r=(u=t.mode)!=null?u:"memory",s=(i=t.logDir)!=null?i:".logs",n=(w=t.singleFile)!=null?w:!1,o={stdout:(C=(h=t.levels)==null?void 0:h.stdout)!=null?C:e?["debug","info","warn","error"]:["warn","error"],file:(b=(x=t.levels)==null?void 0:x.file)!=null?b:e?["info","debug","warn","error"]:["error"]};try{await j.promises.mkdir(s,{recursive:!0})}catch(g){console.warn(`Failed to create log directory: ${g}`)}let a=new Map,c=new Map,m=g=>{let f=n?"all":g;if(!c.has(f)){let S=N.default.join(s,n?"combined.log":`${g}.log`);c.set(f,nt.default.createWriteStream(S,{flags:"a"}))}return c.get(f)},l=(g,f,S)=>{let T={timestamp:new Date().toISOString(),level:f,message:S};o.stdout.includes(f)&&(f==="error"?console.error:console.log)(`[${g}] [${T.timestamp}] [${f.toUpperCase()}] ${T.message}`),o.file.includes(f)&&(r==="memory"?(a.has(g)||a.set(g,[]),a.get(g).push(T)):m(g).write(`[${T.timestamp}] [${f.toUpperCase()}] ${T.message}
`))};return{info:(g,f)=>l(g,"info",f),warn:(g,f)=>l(g,"warn",f),error:(g,f)=>l(g,"error",f),debug:(g,f)=>l(g,"debug",f),flush:async()=>{if(r==="memory"){let g=new Date().toISOString().replace(/:/g,"-"),f=Array.from(a.entries()).map(async([S,T])=>{try{let A=N.default.join(s,n?`combined-${g}.log`:`${S}-${g}.log`),ut=T.map(_=>`[${_.timestamp}] [${_.level.toUpperCase()}] ${_.message}`);await j.promises.mkdir(N.default.dirname(A),{recursive:!0}),await j.promises.appendFile(A,ut.join(`
`)+`
`)}catch(A){console.warn(`Failed to write log file for ${S}: ${A}`)}});await Promise.all(f)}for(let g of c.values())try{g.end()}catch(f){console.warn(`Failed to close log stream: ${f}`)}await new Promise(g=>setTimeout(g,10))}}};var $=d(require("fs/promises"));var it=require("util"),qt=(0,it.promisify)(lt.execFile);var At={json:"json",json5:"json5",yaml:"yaml",yml:"yaml",toml:"toml",xml:"xml"},ct=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=At[(r=(e=t.filename.split(".").pop())==null?void 0:e.toLowerCase())!=null?r:""];if(s)return[s]}return["json"]},U=async(t,e)=>{for(let r of e)try{return typeof r!="string"?[r.parser(t),r]:[await kt(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(", ")}`)},kt=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 Ft=new Map,gt=async({ours:t,theirs:e,base:r,format:s,filePath:n,config:o,normalizedConfig:a,logger:c,autoStage:m=!1})=>{var u;let l=[],[p]=await Promise.all([z({ours:t,theirs:e,base:r,filePath:n,conflicts:l,path:"",ctx:{config:a,strategies:(u=a.customStrategies)!=null?u:{},_strategyCache:Ft},logger:c}),V(n,o.backupDir)]);if(c.debug(n,JSON.stringify({merged:p,conflicts:l},null,2)),l.length===0){let i=await k(s,p);if(await $.default.writeFile(n,i,"utf8"),m)try{(0,lt.execSync)(`git add ${n}`)}catch(w){c.warn(n,`Failed to stage file: ${w}`)}return{success:!0,conflicts:[]}}else{let i=await Q(p,t,e,s);return await Promise.all([$.default.writeFile(n,i,"utf8"),o.writeConflictSidecar?$.default.writeFile(`${n}.conflict.json`,JSON.stringify(l,null,2)):null]),{success:!1,conflicts:l}}},Et=async(t,e,r,s={})=>{let n=await ot(s.loggerConfig,s.debug),o=await et(s);n.debug("git-merge",`Merging files: ours=${t}, base=${e}, theirs=${r}`);let[a,c,m]=await Promise.all([$.default.readFile(t,"utf8"),$.default.readFile(e,"utf8").catch(()=>"{}"),$.default.readFile(r,"utf8")]),l=ct({...o,filename:""}),[p,u]=await U(a,l),[i,w]=await Promise.all([c,m].map(x=>U(x,[u]).then(([b])=>b))),h=typeof u=="string"?u:u.name,{success:C}=await gt({ours:p,theirs:w,base:i,format:h,filePath:t,config:s,normalizedConfig:o,logger:n,autoStage:!1});await n.flush(),process.exit(C?0:1)};0&&(module.exports={processMerge,resolveGitMergeFiles});
;