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