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.
17 lines (16 loc) • 18.3 kB
JavaScript
var At=Object.create;var O=Object.defineProperty;var Et=Object.getOwnPropertyDescriptor;var Rt=Object.getOwnPropertyNames;var jt=Object.getPrototypeOf,Ot=Object.prototype.hasOwnProperty;var Nt=(t,e)=>{for(var r in e)O(t,r,{get:e[r],enumerable:!0})},X=(t,e,r,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of Rt(e))!Ot.call(t,n)&&n!==r&&O(t,n,{get:()=>e[n],enumerable:!(s=Et(e,n))||s.enumerable});return t};var S=(t,e,r)=>(r=t!=null?At(jt(t)):{},X(e||!t||!t.__esModule?O(r,"default",{value:t,enumerable:!0}):r,t)),Lt=t=>X(O({},"__esModule",{value:!0}),t);var Ht={};Nt(Ht,{findGitRoot:()=>Mt,initConfig:()=>Pt,loadConfigFile:()=>$t,parseArgs:()=>kt});module.exports=Lt(Ht);var V=S(require("path")),z=S(require("fs")),xt=require("child_process"),bt=require("url");var Y=require("util"),q=(0,Y.promisify)(xt.execFile),Z=async(t,e)=>{var h;let r=t.split(`
`),s=[],n=[],o;(m=>(m[m.Normal=0]="Normal",m[m.InOurs=1]="InOurs",m[m.InTheirs=2]="InTheirs"))(o||(o={}));let i=0;for(let d of r){if(d.startsWith("<<<<<<<")){i=1;continue}else if(d.startsWith("=======")){i===1&&(i=2);continue}else if(d.startsWith(">>>>>>>")){i===2&&(i=0);continue}switch(i){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(`
`),a=n.join(`
`),g=await q("git",["show",`:1:${e.filename}`],{maxBuffer:1024*1024*50}).then(({stdout:d})=>d).catch(()=>null);if(c===a&&(c=(h=await q("git",["show",`HEAD:${e.filename}`],{maxBuffer:1024*1024*50}).then(({stdout:d})=>d).catch(()=>null))!=null?h:c),!c||!a)throw new Error("Conflict parsing resulted in empty content.");let f=U(e),[u,l]=await E(c,f),[[w],y]=await Promise.all((g?[a,g]:[a]).map(d=>E(d,[l])));return{ours:u,theirs:w,base:y==null?void 0:y[0],format:typeof l=="string"?l:l.name}},vt={json:"json",json5:"json5",yaml:"yaml",yml:"yaml",toml:"toml",xml:"xml"},U=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=vt[(r=(e=t.filename.split(".").pop())==null?void 0:e.toLowerCase())!=null?r:""];if(s)return[s]}return["json"]},E=async(t,e)=>{for(let r of e)try{return typeof r!="string"?[r.parser(t),r]:[await _t(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(", ")}`)},_t=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 tt={isMatch:(t,e)=>{let r=k(t);return e.some(s=>zt(r,k(s)))}},et=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==="\\/"?"":"/"),It=t=>{let e=t.startsWith("!"),r=e?t.slice(1):t,s=[],n="",o=!1;for(let i of r)o?(n+="\\"+i,o=!1):i==="\\"?o=!0:i==="/"?(s.push(n),n=""):n+=i;return o&&(n+="\\"),s.push(n),e&&(s.negated=!0),s},zt=(t,e)=>{let r=t.split("/"),s=It(e),n=s.negated===!0,o=rt(r,s);return n?!o:o},rt=(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(rt(t.slice(r+o),e.slice(s+1)))return!0;return!1}if(!Dt(t[r],n))return!1;r++,s++}for(;s<e.length&&e[s]==="**";)s++;return r===t.length&&s===e.length},Dt=(t,e)=>{let r=!1,s=!1,n="",o="";for(let a=0;a<e.length;a++){let g=e[a];if(r){o+=g,r=!1;continue}if(g==="\\"){r=!0;continue}if(g==="*"){if(!s){s=!0,n=o,o="";continue}o+="*";continue}o+=g}if(r&&(o+="\\"),!s){let a=o.replace(/\\(.)/g,"$1");return t===a}let i=o.replace(/\\(.)/g,"$1"),c=n.replace(/\\(.)/g,"$1");return t.startsWith(c)&&t.endsWith(i)};var W={defaultStrategy:["merge","ours"],include:["**/*.json","**/*.yaml","**/*.yml","**/*.xml","**/*.toml"],exclude:["**/node_modules/**","**/dist/**"],debug:!1,writeConflictSidecar:!1},L=async t=>{let{rules:e,byStrategy:r,defaultStrategy:s,matcher:n,plugins:o,pluginConfig:i,customStrategies:c,...a}={...W,...t},g=await Wt(o,i),f={...c,...g},u={exact:Object.create(null),exactFields:Object.create(null),patterns:Object.create(null),default:Ut(s)},l=typeof n=="string"?await et(n):n!=null?n:tt,w=0;if(r)for(let[y,h]of Object.entries(r)){if(!h)continue;let{name:d,important:$}=st(y);for(let p of h){let{key:m,important:x}=nt(p);ot(u,m,{strategies:[{name:d,important:$||x}],order:w++,source:m})}}return e&&at(e,[],(y,h)=>{let{key:d,important:$}=nt(y),p=h.map(m=>{let{name:x,important:T}=st(m);return{name:x,important:T||$}});ot(u,d,{strategies:p,order:w++,source:d})}),{...a,rules:u,matcher:l,customStrategies:f}},Ut=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}.**`;N(t.patterns,o,r);return}if(/[*?[\]]/.test(e)){N(t.patterns,e,r);return}e.includes(".")?N(t.exact,e,r):N(t.exactFields,e,r)},at=(t,e,r)=>{for(let[s,n]of Object.entries(t)){let o=[...e,s];Array.isArray(n)?r(o.join("."),n):at(n,o,r)}},N=(t,e,r)=>{var n;((n=t[e])!=null?n:t[e]=[]).push(r)},Wt=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,i=e==null?void 0:e[s],c=o instanceof Function?await o(i):o;if(!c.strategies)throw new Error(`Plugin "${s}" does not export strategies`);c.init&&await c.init(i),Object.assign(r,c.strategies)}catch(n){throw new Error(`Failed to load plugin "${s}": ${n}`)}return r};var M=S(require("fs/promises")),b=S(require("path")),R=Symbol("MERGE_DROP"),ct=".merge-backups",C=0,F=1,lt=2,K=3,Kt=t=>t.includes("<<<<<<<")&&t.includes("=======")&&t.includes(">>>>>>>"),gt=async({root:t=process.cwd(),include:e,exclude:r,matcher:s,includeNonConflicted:n,debug:o,backupDir:i=ct})=>{try{await M.default.rm(i,{recursive:!0,force:!0})}catch{}for(let u of[...e,...r]){if(u.startsWith("!"))throw new Error(`Negation not allowed in include/exclude: ${u}`);u.includes("\\")&&console.warn(`Use '/' as path separator: ${u}`)}r.push(`${i}/**`);let c=u=>{let l=u.replace(/\\/g,"/");return s.isMatch(l,e)&&!s.isMatch(l,r)},a=Bt(e,i?[...r,i]:r,s),g=[],f=async u=>{let l=await M.default.readdir(u,{withFileTypes:!0});for(let w of l){let y=b.default.join(u,w.name),h=b.default.relative(t,y).replace(/\\+/g,"/");if(w.isDirectory())!/node_modules|\.git/.test(w.name)&&!a(h)&&await f(y);else if(c(h))try{let d=await M.default.readFile(y,"utf8");n||Kt(d)?g.push({filePath:h,content:d}):o&&console.info(`Skipped (no conflicts): ${y}`)}catch{console.warn(`Skipped (unreadable): ${y}`)}}};return await f(t),g},Bt=(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 i=o.split("/").slice(0,-1).join("/");i&&s.add(i)}s.has("**")&&s.clear();for(let o of e)if(o.endsWith("/**")){let i=o.slice(0,-3);i&&n.add(i)}return o=>r.isMatch(o,[...n])||s.size>0&&!r.isMatch(o,[...s])},ut=async(t,e=ct)=>{let r=b.default.relative(process.cwd(),t),s=b.default.join(e,r);return await M.default.mkdir(b.default.dirname(s),{recursive:!0}),await M.default.copyFile(t,s),s},ft=async(t=".merge-backups")=>{let e=async(r,s="")=>{try{let n=await M.default.readdir(r,{withFileTypes:!0});for(let o of n){let i=b.default.join(r,o.name),c=b.default.join(s,o.name),a=b.default.join(process.cwd(),c);if(o.isDirectory())await e(i,c);else try{await M.default.mkdir(b.default.dirname(a),{recursive:!0}),await M.default.copyFile(i,a)}catch(g){console.warn(`Failed to restore ${i.replace(/[\r\n\t]/g,"")}: ${g}`)}}}catch(n){console.warn(`Failed to read backup directory ${r}: ${n}`)}};await e(t)};var mt=S(require("fs")),v=require("fs"),_=S(require("path")),I=async(t={},e)=>{var u,l,w,y,h,d,$;let r=(u=t.mode)!=null?u:"memory",s=(l=t.logDir)!=null?l:".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 v.promises.mkdir(s,{recursive:!0})}catch(p){console.warn(`Failed to create log directory: ${p}`)}let i=new Map,c=new Map,a=p=>{let m=n?"all":p;if(!c.has(m)){let x=_.default.join(s,n?"combined.log":`${p}.log`);c.set(m,mt.default.createWriteStream(x,{flags:"a"}))}return c.get(m)},g=(p,m,x)=>{let T={timestamp:new Date().toISOString(),level:m,message:x};o.stdout.includes(m)&&(m==="error"?console.error:console.log)(`[${p}] [${T.timestamp}] [${m.toUpperCase()}] ${T.message}`),o.file.includes(m)&&(r==="memory"?(i.has(p)||i.set(p,[]),i.get(p).push(T)):a(p).write(`[${T.timestamp}] [${m.toUpperCase()}] ${T.message}
`))};return{info:(p,m)=>g(p,"info",m),warn:(p,m)=>g(p,"warn",m),error:(p,m)=>g(p,"error",m),debug:(p,m)=>g(p,"debug",m),flush:async()=>{if(r==="memory"){let p=new Date().toISOString().replace(/:/g,"-"),m=Array.from(i.entries()).map(async([x,T])=>{try{let A=_.default.join(s,n?`combined-${p}.log`:`${x}-${p}.log`),Ft=T.map(D=>`[${D.timestamp}] [${D.level.toUpperCase()}] ${D.message}`);await v.promises.mkdir(_.default.dirname(A),{recursive:!0}),await v.promises.appendFile(A,Ft.join(`
`)+`
`)}catch(A){console.warn(`Failed to write log file for ${x}: ${A}`)}});await Promise.all(m)}for(let p of c.values())try{p.end()}catch(m){console.warn(`Failed to close log stream: ${m}`)}await new Promise(p=>setTimeout(p,10))}}};var j=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 pt=(t,{rules:e,matcher:r})=>{var g,f,u;let s=(g=e.exact[t])!=null?g:[],n=(u=e.exactFields[(f=t.split(".").pop())!=null?f:""])!=null?u:[],o=Object.entries(e.patterns).filter(([l])=>r.isMatch(t,[l])).flatMap(([,l])=>l),i=[...s.flatMap(l=>l.strategies),...n.flatMap(l=>l.strategies),...o.flatMap(l=>l.strategies),...e.default],c=i.filter(l=>l.important),a=i.filter(l=>!l.important);return[...c,...a].map(l=>l.name)};var B=t=>typeof t=="object"&&t!==null&&!Array.isArray(t),Gt={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:K,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:i,conflicts:c,logger:a}=t;if(B(e)&&B(r)){let g=new Set([...Object.keys(e),...Object.keys(r)]),f={};for(let u of g){let l=u.replace(/\./g,"\0").replace(/\\/g,"");f[u]=await G({ours:e[u],theirs:r[u],base:B(s)?s[u]:void 0,path:n?`${n}.${l}`:l,filePath:o,ctx:i,conflicts:c,logger:a})}return{status:C,value:f}}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}`}},G=async({ours:t,theirs:e,base:r,path:s,filePath:n,ctx:o,conflicts:i,logger:c})=>{var g;if(t===e)return t;o._strategyCache||(o._strategyCache=new Map);let a=o._strategyCache.get(s);a||(a=pt(s,o.config),o._strategyCache.set(s,a)),c.debug(n!=null?n:"all",`path: ${s}, strategies: ${a.join(", ")||"none"}`);for(let f of a){c.debug(n!=null?n:"all",`Applying strategy '${f}' at ${s}`);let u=(g=Gt[f])!=null?g:o.strategies[f];if(!u)continue;let l=await u({ours:t,theirs:e,base:r,path:s,filePath:n,ctx:o,conflicts:i,logger:c});switch(l.status){case C:return l.value;case F:continue;case K:i.push({path:s,reason:l.reason});return;case lt:throw i.push({path:s,reason:l.reason}),new Error(`Merge failed at ${s}: ${l.reason}`)}}i.push({path:s,reason:`All strategies failed (tried: ${a.join(", ")||"none"})`,...o.config.debug?{ours:t,theirs:e,base:r}:{}})};var J=(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)=>J(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 i=J(o,e?`${e}.${n}`:n,r);i!==void 0&&(s[n]=i)}return s}return t}},wt=async(t,e,r,s)=>{let n=[],o=J(t,"",n),i=await j(s,o);for(let c of n){let a=dt(e,c),g=dt(r,c),[f,u]=await Promise.all([a,g].map(y=>j(s,y))),l=["<<<<<<< ours",yt(f,2),"=======",yt(u,2),">>>>>>> theirs"].join(`
`),w=`__CONFLICT_MARKER::${c}__`;i=i.replace(/json/.test(s)?JSON.stringify(w):w,l)}return i},dt=(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},yt=(t,e)=>{let r=" ".repeat(e);return t.split(`
`).map(s=>s&&r+s).join(`
`)};var P=S(require("fs/promises"));var Jt=new Map,H=async({ours:t,theirs:e,base:r,format:s,filePath:n,config:o,normalizedConfig:i,logger:c,autoStage:a=!1})=>{var u;let g=[],[f]=await Promise.all([G({ours:t,theirs:e,base:r,filePath:n,conflicts:g,path:"",ctx:{config:i,strategies:(u=i.customStrategies)!=null?u:{},_strategyCache:Jt},logger:c}),ut(n,o.backupDir)]);if(c.debug(n,JSON.stringify({merged:f,conflicts:g},null,2)),g.length===0){let l=await j(s,f);if(await P.default.writeFile(n,l,"utf8"),a)try{(0,xt.execSync)(`git add ${n}`)}catch(w){c.warn(n,`Failed to stage file: ${w}`)}return{success:!0,conflicts:[]}}else{let l=await wt(f,t,e,s);return await Promise.all([P.default.writeFile(n,l,"utf8"),o.writeConflictSidecar?P.default.writeFile(`${n}.conflict.json`,JSON.stringify(g,null,2)):null]),{success:!1,conflicts:g}}},St=async(t,e,r,s={})=>{let n=await I(s.loggerConfig,s.debug),o=await L(s);n.debug("git-merge",`Merging files: ours=${t}, base=${e}, theirs=${r}`);let[i,c,a]=await Promise.all([P.default.readFile(t,"utf8"),P.default.readFile(e,"utf8").catch(()=>"{}"),P.default.readFile(r,"utf8")]),g=U({...o,filename:""}),[f,u]=await E(i,g),[l,w]=await Promise.all([c,a].map(d=>E(d,[u]).then(([$])=>$))),y=typeof u=="string"?u:u.name,{success:h}=await H({ours:f,theirs:w,base:l,format:y,filePath:t,config:s,normalizedConfig:o,logger:n,autoStage:!1});await n.flush(),process.exit(h?0:1)};var Ct=async t=>{let e=await I(t.loggerConfig,t.debug),r=await L(t),s=await gt(r);await Promise.all(s.map(async({filePath:n,content:o})=>{var f;let{theirs:i,ours:c,base:a,format:g}=await Z(o,{filename:n,parsers:r.parsers});e.debug(n,JSON.stringify({ours:c,theirs:i,base:a,format:g},null,2)),await H({ours:c,theirs:i,base:a,format:g,filePath:n,config:t,normalizedConfig:r,logger:e,autoStage:(f=r.autoStage)!=null?f:!0})})),await e.flush()};var Tt="git-json-resolver.config.js",Mt=()=>{try{return(0,xt.execSync)("git rev-parse --show-toplevel",{encoding:"utf8"}).trim()}catch{return process.cwd()}},$t=async()=>{let t=[process.cwd(),Mt()],e=[Tt,"git-json-resolver.config.ts"];for(let r of t)for(let s of e){let n=V.default.join(r,s);if(z.default.existsSync(n)){let o=await import((0,bt.pathToFileURL)(n).href);return o.default||o}}return{}},Pt=t=>{let e=V.default.join(t,Tt);z.default.existsSync(e)&&(console.error(`Config file already exists: ${e}`),process.exit(1));let r=`/**
* git-json-resolver configuration
* Docs: https://github.com/react18-tools/git-json-resolver
*/
module.exports = ${JSON.stringify(W,null,2)};
`;z.default.writeFileSync(e,r,"utf8"),console.log(`Created starter config at ${e}`)},kt=t=>{var i,c;let e={},r=!1,s,n,o=t.slice(2).filter(a=>!a.startsWith("--"));o.length===3&&(n=[o[0],o[1],o[2]]);for(let a=2;a<t.length;a++){let g=t[a],f=t[a+1];if(!(n&&!g.startsWith("--")))switch(g){case"--include":e.include=(i=f==null?void 0:f.split(","))!=null?i:[],a++;break;case"--exclude":e.exclude=(c=f==null?void 0:f.split(","))!=null?c:[],a++;break;case"--matcher":e.matcher=f,a++;break;case"--debug":e.debug=!0;break;case"--sidecar":e.writeConflictSidecar=!0;break;case"--init":r=!0;break;case"--restore":s=f,a++;break;default:g.startsWith("--")&&console.warn(`Unknown option: ${g}`)}}return{overrides:e,init:r,restore:s,gitMergeFiles:n}};(async()=>{try{let{overrides:t,init:e,restore:r,gitMergeFiles:s}=kt(process.argv);e&&(Pt(process.cwd()),process.exit(0));let n=await $t(),o={...n,...t};if(r&&(await ft(r||n.backupDir||".merge-backups"),console.log(`Restored backups from ${r}`),process.exit(0)),s){let[i,c,a]=s;await St(i,c,a,o);return}await Ct(o)}catch(t){console.error("Failed:",t),process.exit(1)}})();0&&(module.exports={findGitRoot,initConfig,loadConfigFile,parseArgs});
;