react-monaco-json-merge 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +333 -0
- package/dist/assets/codicon-B_Z2XQ3P.ttf +0 -0
- package/dist/assets/index-Cv-cgfL5.css +1 -0
- package/dist/assets/index-Dn9w1aoG.js +216 -0
- package/dist/assets/index-Dn9w1aoG.js.map +1 -0
- package/dist/assets/monaco-editor-DOc7Ojit.css +1 -0
- package/dist/index.html +14 -0
- package/package.json +116 -0
package/README.md
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# React Monaco JSON Merge
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
**A powerful React component for 3-way JSON merging with semantic comparison, built on Monaco Editor**
|
|
6
|
+
|
|
7
|
+
[](https://react.dev/)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://vite.dev/)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
12
|
+
**[๐ Live Demo](https://ivkirill.github.io/react-monaco-json-merge/)**
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
## โจ Features
|
|
17
|
+
|
|
18
|
+
### Core Capabilities
|
|
19
|
+
|
|
20
|
+
- **๐ 3-Way Merge** - Compare base, theirs, and ours versions side-by-side
|
|
21
|
+
- **๐จ Monaco Editor Integration** - Powered by VS Code's Monaco Editor for a familiar editing experience
|
|
22
|
+
- **๐ง Semantic JSON Comparison** - Uses JSON Patch (RFC 6902) for intelligent, structure-aware diffing
|
|
23
|
+
- **๐ Schema-Aware** - Optional JSON Schema support for enhanced conflict detection and validation
|
|
24
|
+
- **โ
Interactive Resolution** - Checkboxes for accepting/rejecting changes
|
|
25
|
+
- **๐ Smart Merging** - Automatically merges compatible conflicts
|
|
26
|
+
- **โก Real-time Validation** - JSON validation with error highlighting
|
|
27
|
+
- **๐ฏ 4-Column Mode** - Optional result preview column for live merge preview
|
|
28
|
+
- **๐จ Theme Support** - Light and dark themes (VS Code/monaco themes)
|
|
29
|
+
- **โฟ Accessible** - Keyboard navigation and screen reader support
|
|
30
|
+
|
|
31
|
+
### Technical Highlights
|
|
32
|
+
|
|
33
|
+
- **Zero Line-based Diffs** - Uses semantic JSON comparison, ignoring formatting changes
|
|
34
|
+
- **Array Matching by ID** - Schema-aware array item matching (not just by index)
|
|
35
|
+
- **Conflict Type Detection** - Identifies SAME_CHANGE, INPUT1_ONLY, INPUT2_ONLY, and TRUE_CONFLICT
|
|
36
|
+
- **Deep Merge Support** - Automatically merges nested objects when possible
|
|
37
|
+
- **TypeScript First** - Fully typed with comprehensive type definitions
|
|
38
|
+
|
|
39
|
+
## ๐ Quick Start
|
|
40
|
+
|
|
41
|
+
> **๐บ [Try the Live Demo](https://ivkirill.github.io/react-monaco-json-merge/)** - See the editor in action!
|
|
42
|
+
|
|
43
|
+
### Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install react-monaco-json-merge
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Basic Usage
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { JsonDiffMergeEditor } from 'react-monaco-json-merge';
|
|
53
|
+
import 'react-monaco-json-merge/dist/style.css';
|
|
54
|
+
|
|
55
|
+
function App() {
|
|
56
|
+
const base = JSON.stringify({ name: "John", age: 30 }, null, 2);
|
|
57
|
+
const theirs = JSON.stringify({ name: "John", age: 31, city: "NYC" }, null, 2);
|
|
58
|
+
const ours = JSON.stringify({ name: "Jane", age: 30 }, null, 2);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<JsonDiffMergeEditor
|
|
62
|
+
base={base}
|
|
63
|
+
original={theirs}
|
|
64
|
+
modified={ours}
|
|
65
|
+
showResultColumn={true}
|
|
66
|
+
height="600px"
|
|
67
|
+
onMergeResolve={(content, resolution) => {
|
|
68
|
+
if (resolution?.isValid) {
|
|
69
|
+
console.log('Merged JSON:', JSON.parse(content));
|
|
70
|
+
}
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## ๐ Documentation
|
|
78
|
+
|
|
79
|
+
### Props
|
|
80
|
+
|
|
81
|
+
| Prop | Type | Default | Description |
|
|
82
|
+
|------|------|---------|-------------|
|
|
83
|
+
| `base` | `string` | `""` | Common ancestor JSON (stringified) |
|
|
84
|
+
| `original` | `string` | `""` | "Theirs" version JSON (stringified) |
|
|
85
|
+
| `modified` | `string` | `""` | "Ours" version JSON (stringified) |
|
|
86
|
+
| `showResultColumn` | `boolean` | `false` | Show merged result preview column |
|
|
87
|
+
| `theme` | `string` | `"vs"` | Monaco Editor theme (`"vs"`, `"vs-dark"`, `"hc-black"`) |
|
|
88
|
+
| `height` | `string \| number` | `"100%"` | Editor height |
|
|
89
|
+
| `width` | `string \| number` | `"100%"` | Editor width |
|
|
90
|
+
| `schema` | `JSONSchema` | `undefined` | JSON Schema for validation and array matching |
|
|
91
|
+
| `labels` | `object` | `undefined` | Custom column labels |
|
|
92
|
+
| `onMergeResolve` | `function` | `undefined` | Callback when merge resolution changes |
|
|
93
|
+
| `options` | `object` | `{}` | Monaco Editor options |
|
|
94
|
+
| `comparisonMode` | `"split" \| "sequential"` | `"split"` | How to display the comparison |
|
|
95
|
+
| `baseIndex` | `0 \| 1 \| 2` | `1` | Position of base column (0=left, 1=middle, 2=right) |
|
|
96
|
+
|
|
97
|
+
### Advanced Example
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { JsonDiffMergeEditor } from 'react-monaco-json-merge';
|
|
101
|
+
import type { JSONSchema } from 'react-monaco-json-merge';
|
|
102
|
+
|
|
103
|
+
const schema: JSONSchema = {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
users: {
|
|
107
|
+
type: "array",
|
|
108
|
+
items: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
id: { type: "string" },
|
|
112
|
+
name: { type: "string" }
|
|
113
|
+
},
|
|
114
|
+
required: ["id"]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
function AdvancedEditor() {
|
|
121
|
+
return (
|
|
122
|
+
<JsonDiffMergeEditor
|
|
123
|
+
base={baseJSON}
|
|
124
|
+
original={theirsJSON}
|
|
125
|
+
modified={oursJSON}
|
|
126
|
+
schema={schema}
|
|
127
|
+
showResultColumn={true}
|
|
128
|
+
theme="vs-dark"
|
|
129
|
+
height="700px"
|
|
130
|
+
labels={{
|
|
131
|
+
input1: "Remote Changes",
|
|
132
|
+
base: "Common Ancestor",
|
|
133
|
+
input2: "Local Changes",
|
|
134
|
+
result: "Merged Result"
|
|
135
|
+
}}
|
|
136
|
+
onMergeResolve={(content, resolution) => {
|
|
137
|
+
if (resolution?.isValid) {
|
|
138
|
+
// Save merged content
|
|
139
|
+
saveToFile(content);
|
|
140
|
+
} else {
|
|
141
|
+
// Handle validation errors
|
|
142
|
+
console.error('Merge has conflicts:', resolution?.conflictIssues);
|
|
143
|
+
}
|
|
144
|
+
}}
|
|
145
|
+
options={{
|
|
146
|
+
fontSize: 14,
|
|
147
|
+
minimap: { enabled: false },
|
|
148
|
+
lineNumbers: 'on'
|
|
149
|
+
}}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## ๐๏ธ Architecture
|
|
156
|
+
|
|
157
|
+
### Semantic vs Line-Based Diff
|
|
158
|
+
|
|
159
|
+
Unlike traditional diff tools that compare text line-by-line, this editor uses **semantic JSON comparison**:
|
|
160
|
+
|
|
161
|
+
1. **Parses JSON** to structured objects
|
|
162
|
+
2. **Generates JSON Patch** operations (RFC 6902)
|
|
163
|
+
3. **Groups patches** by JSON path
|
|
164
|
+
4. **Maps to line numbers** using jsonc-parser
|
|
165
|
+
5. **Applies Monaco decorations** based on JSON structure
|
|
166
|
+
|
|
167
|
+
**Benefits:**
|
|
168
|
+
- Ignores formatting changes (whitespace, key order)
|
|
169
|
+
- Schema-aware array matching (by ID, not index)
|
|
170
|
+
- Better conflict detection for nested JSON
|
|
171
|
+
- Highlights actual value changes, not formatting
|
|
172
|
+
|
|
173
|
+
### Conflict Types
|
|
174
|
+
|
|
175
|
+
The editor identifies four conflict types:
|
|
176
|
+
|
|
177
|
+
- **`SAME_CHANGE`** - Both sides made identical changes (auto-merged)
|
|
178
|
+
- **`INPUT1_ONLY`** - Only "theirs" changed (can be accepted)
|
|
179
|
+
- **`INPUT2_ONLY`** - Only "ours" changed (can be accepted)
|
|
180
|
+
- **`TRUE_CONFLICT`** - Both sides changed to different values (requires resolution)
|
|
181
|
+
|
|
182
|
+
### Smart Merging
|
|
183
|
+
|
|
184
|
+
When both checkboxes are selected for a `TRUE_CONFLICT`:
|
|
185
|
+
- If both values are objects: **deep merge** is performed
|
|
186
|
+
- If values are identical: uses either value
|
|
187
|
+
- If incompatible types: merge fails, shows warning
|
|
188
|
+
|
|
189
|
+
## ๐ ๏ธ Development
|
|
190
|
+
|
|
191
|
+
### Prerequisites
|
|
192
|
+
|
|
193
|
+
- **Node.js** >= 20.19.0
|
|
194
|
+
- **npm** >= 10.0.0
|
|
195
|
+
|
|
196
|
+
### Setup
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# Clone the repository
|
|
200
|
+
git clone <repository-url>
|
|
201
|
+
cd react-monaco-json-merge
|
|
202
|
+
|
|
203
|
+
# Install dependencies
|
|
204
|
+
npm install
|
|
205
|
+
|
|
206
|
+
# Start development server
|
|
207
|
+
npm run dev
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Available Scripts
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# Development
|
|
214
|
+
npm run dev # Start Vite dev server
|
|
215
|
+
npm run build # Build for production
|
|
216
|
+
npm run preview # Preview production build
|
|
217
|
+
|
|
218
|
+
# Code Quality
|
|
219
|
+
npm run type-check # TypeScript type checking
|
|
220
|
+
npm run lint # Lint code with Biome
|
|
221
|
+
npm run lint:fix # Fix linting issues
|
|
222
|
+
npm run format # Format code
|
|
223
|
+
|
|
224
|
+
# Testing
|
|
225
|
+
npm run test # Run tests in watch mode
|
|
226
|
+
npm run test:run # Run tests once
|
|
227
|
+
npm run test:ui # Run tests with UI
|
|
228
|
+
npm run test:coverage # Generate coverage report
|
|
229
|
+
|
|
230
|
+
# Utilities
|
|
231
|
+
npm run validate # Run all checks (type-check + lint + test)
|
|
232
|
+
npm run clean # Remove build artifacts
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Project Structure
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
react-monaco-json-merge/
|
|
239
|
+
โโโ src/
|
|
240
|
+
โ โโโ components/
|
|
241
|
+
โ โ โโโ editor.tsx # Main JsonDiffMergeEditor component
|
|
242
|
+
โ โโโ data/
|
|
243
|
+
โ โ โโโ sampleData.ts # Sample data for demo
|
|
244
|
+
โ โโโ utils/
|
|
245
|
+
โ โ โโโ diffMerge.ts # Merge logic
|
|
246
|
+
โ โ โโโ jsonPatchDiff.ts # Diff computation
|
|
247
|
+
โ โ โโโ editorDecorations.ts # Monaco decorations
|
|
248
|
+
โ โ โโโ helpers.ts # Utility functions
|
|
249
|
+
โ โ โโโ schema.ts # Schema utilities
|
|
250
|
+
โ โโโ types/
|
|
251
|
+
โ โ โโโ index.ts # TypeScript definitions
|
|
252
|
+
โ โโโ styles/
|
|
253
|
+
โ โ โโโ editor.css # Editor styles
|
|
254
|
+
โ โโโ Demo.tsx # Demo application
|
|
255
|
+
โ โโโ main.tsx # Entry point
|
|
256
|
+
โโโ package.json
|
|
257
|
+
โโโ tsconfig.json
|
|
258
|
+
โโโ vite.config.ts
|
|
259
|
+
โโโ README.md
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## ๐งช Testing
|
|
263
|
+
|
|
264
|
+
The project includes comprehensive test coverage:
|
|
265
|
+
|
|
266
|
+
- **Unit Tests** - Utilities and helpers (94+ tests)
|
|
267
|
+
- **Integration Tests** - Full editor rendering scenarios
|
|
268
|
+
- **Rendering Tests** - Conflict detection and highlighting
|
|
269
|
+
- **Schema Tests** - JSON Schema variant handling
|
|
270
|
+
|
|
271
|
+
Run tests with:
|
|
272
|
+
```bash
|
|
273
|
+
npm run test
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
View coverage report:
|
|
277
|
+
```bash
|
|
278
|
+
npm run test:coverage
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## ๐ฆ Dependencies
|
|
282
|
+
|
|
283
|
+
### Core
|
|
284
|
+
- **React 19** - UI framework
|
|
285
|
+
- **Monaco Editor** - Code editor
|
|
286
|
+
- **fast-json-patch** - JSON Patch (RFC 6902) implementation
|
|
287
|
+
- **jsonc-parser** - JSON with comments parsing
|
|
288
|
+
|
|
289
|
+
### Utilities
|
|
290
|
+
- **fast-deep-equal** - Deep equality comparison
|
|
291
|
+
- **sort-keys** - Object key sorting
|
|
292
|
+
|
|
293
|
+
### Development
|
|
294
|
+
- **TypeScript** - Type safety
|
|
295
|
+
- **Vite** - Build tool
|
|
296
|
+
- **Vitest** - Testing framework
|
|
297
|
+
- **Biome** - Linting and formatting
|
|
298
|
+
|
|
299
|
+
## ๐ค Contributing
|
|
300
|
+
|
|
301
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
302
|
+
|
|
303
|
+
1. Fork the repository
|
|
304
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
305
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
306
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
307
|
+
5. Open a Pull Request
|
|
308
|
+
|
|
309
|
+
### Development Guidelines
|
|
310
|
+
|
|
311
|
+
- Follow TypeScript best practices
|
|
312
|
+
- Write tests for new features
|
|
313
|
+
- Ensure all tests pass (`npm run validate`)
|
|
314
|
+
- Follow the existing code style (enforced by Biome)
|
|
315
|
+
- Update documentation as needed
|
|
316
|
+
|
|
317
|
+
## ๐ License
|
|
318
|
+
|
|
319
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
320
|
+
|
|
321
|
+
## ๐ Acknowledgments
|
|
322
|
+
|
|
323
|
+
- [Monaco Editor](https://microsoft.github.io/monaco-editor/) - The editor that powers this component
|
|
324
|
+
- [fast-json-patch](https://github.com/Starcounter-Jack/JSON-Patch) - JSON Patch implementation
|
|
325
|
+
- [JSON Schema](https://json-schema.org/) - Schema validation standard
|
|
326
|
+
|
|
327
|
+
## ๐ Support
|
|
328
|
+
|
|
329
|
+
For issues, questions, or contributions, please open an issue on GitHub.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
**Made with โค๏ธ for better JSON merging experiences**
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.monaco-diff-editor .monaco-editor .codicon-diff-insert,.monaco-diff-editor .monaco-editor .codicon-diff-remove{left:44px!important;right:initial!important}.monaco-diff-editor.side-by-side .monaco-editor .codicon-diff-remove{left:62px!important}.monaco-diff-editor .lightbulb-glyph,.monaco-diff-editor .codicon-lightbulb,.monaco-diff-editor .lightBulbWidget{display:none!important}.monaco-editor .conflict-issue-line.conflict-issue-error{background-color:#ff00001a}.monaco-editor .conflict-issue-line.conflict-issue-warning{background-color:#ffaa001a}.monaco-editor .conflict-issue-line.conflict-issue-smart-merge{background-color:#0096ff1a}.monaco-editor.vs .conflict-issue-line.conflict-issue-error{background-color:#ff00000d}.monaco-editor.vs .conflict-issue-line.conflict-issue-warning{background-color:#ff88000d}.monaco-editor.vs .conflict-issue-line.conflict-issue-smart-merge{background-color:#0078d40d}*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:#1e1e1e;color:#d4d4d4}.demo-container{max-width:100%;margin:0 auto;padding:20px}.demo-header{text-align:center;margin-bottom:30px;padding:20px;background:#252526;border-radius:8px}.demo-header h1{color:#4fc3f7;margin-bottom:10px;font-size:28px}.demo-header p{color:#888;font-size:14px}.demo-content{display:grid;grid-template-columns:320px 1fr;gap:20px}.controls-panel{background:#252526;padding:20px;border-radius:8px;height:fit-content;position:sticky;top:20px}.controls-panel h2{color:#4fc3f7;font-size:18px;margin-bottom:20px;padding-bottom:10px;border-bottom:1px solid #3c3c3c}.control-group{margin-bottom:20px}.control-group label{display:block;margin-bottom:8px}.control-group label:has(input[type=checkbox]){display:flex;align-items:center;gap:8px;white-space:nowrap}.control-group strong{color:#9cdcfe;font-size:13px;display:block;margin-bottom:8px}.control-group select{width:100%;padding:8px 12px;background:#3c3c3c;color:#d4d4d4;border:1px solid #555;border-radius:4px;font-size:13px;cursor:pointer}.control-group select:hover{border-color:#0e639c}.control-group select:focus{outline:none;border-color:#4fc3f7}.radio-group{display:flex;flex-direction:column;gap:8px;margin-top:8px}.radio-group label{display:flex;align-items:center;gap:8px;padding:8px;background:#3c3c3c;border-radius:4px;cursor:pointer;transition:background .2s}.radio-group label:hover{background:#4a4a4a}.radio-group input[type=radio]{cursor:pointer}.control-group input[type=checkbox]{margin-right:8px;cursor:pointer;flex-shrink:0}.control-group input[type=range]{cursor:pointer;accent-color:#0e639c}.control-group input[type=range]::-webkit-slider-thumb{cursor:pointer}.control-group input[type=range]::-moz-range-thumb{cursor:pointer}.info-section{background:#1e1e1e;padding:15px;border-radius:6px;margin-top:20px;border-left:3px solid #0e639c}.info-section h3{color:#ce9178;font-size:14px;margin-bottom:12px}.info-section ul{padding-left:20px;line-height:1.8}.info-section li{margin-bottom:6px;color:#d4d4d4;font-size:13px}.info-section strong{color:#9cdcfe}.editor-section{display:flex;flex-direction:column;gap:20px}.editor-container{background:#252526;padding:20px;border-radius:8px;min-height:600px}.resolution-panel{background:#252526;padding:20px;border-radius:8px}.resolution-panel h3{color:#4fc3f7;margin-bottom:15px;font-size:18px}.resolution-panel p{margin-bottom:10px;line-height:1.6;font-size:14px}.resolution-panel .error{color:#f48771;background:#f487711a;padding:8px 12px;border-radius:4px;border-left:3px solid #f48771}.resolution-panel ul{padding-left:20px;margin:10px 0}.resolution-panel li{margin-bottom:6px;font-size:13px}.resolution-panel li.error{color:#f48771}.resolution-panel li.warning{color:#dcdcaa}.resolution-panel li.smart-merge{color:#4fc3f7}.merged-preview{margin-top:15px}.merged-preview pre{background:#1e1e1e;padding:15px;border-radius:4px;overflow-x:auto;font-size:12px;margin:10px 0;max-height:200px;overflow-y:auto;border:1px solid #3c3c3c;font-family:Monaco,Menlo,Consolas,monospace}.resolution-panel button{margin-top:15px;padding:12px 24px;background:#0e639c;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:14px;font-weight:500;transition:background .2s;width:100%}.resolution-panel button:hover:not(:disabled){background:#17b}.resolution-panel button:disabled{background:#555;cursor:not-allowed;opacity:.6}.demo-footer{margin-top:30px;padding:20px;background:#252526;border-radius:8px;text-align:center;color:#888;font-size:14px}.demo-footer strong{color:#4fc3f7}@media(max-width:1200px){.demo-content{grid-template-columns:1fr}.controls-panel{position:static}}@media(max-width:768px){.demo-container{padding:10px}.demo-header h1{font-size:22px}.editor-container{padding:10px}}
|