@leadcms/sdk 3.2.0 → 3.3.0
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 +15 -3
- package/dist/cli/bin/generate-env.d.ts +1 -1
- package/dist/cli/bin/generate-env.d.ts.map +1 -1
- package/dist/cli/bin/generate-env.js +2 -2
- package/dist/cli/bin/generate-env.js.map +1 -1
- package/dist/cli/bin/pull-comments.js +3 -1
- package/dist/cli/bin/pull-comments.js.map +1 -1
- package/dist/cli/bin/pull-content.js +2 -1
- package/dist/cli/bin/pull-content.js.map +1 -1
- package/dist/cli/bin/pull-media.js +3 -1
- package/dist/cli/bin/pull-media.js.map +1 -1
- package/dist/cli/index.js +7 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/content-merge.d.ts +80 -0
- package/dist/lib/content-merge.d.ts.map +1 -0
- package/dist/lib/content-merge.js +350 -0
- package/dist/lib/content-merge.js.map +1 -0
- package/dist/scripts/fetch-leadcms-content.d.ts +1 -0
- package/dist/scripts/fetch-leadcms-content.d.ts.map +1 -1
- package/dist/scripts/fetch-leadcms-content.js +106 -13
- package/dist/scripts/fetch-leadcms-content.js.map +1 -1
- package/dist/scripts/generate-env-js.d.ts +5 -0
- package/dist/scripts/generate-env-js.d.ts.map +1 -1
- package/dist/scripts/generate-env-js.js +12 -10
- package/dist/scripts/generate-env-js.js.map +1 -1
- package/dist/scripts/pull-all.d.ts +16 -1
- package/dist/scripts/pull-all.d.ts.map +1 -1
- package/dist/scripts/pull-all.js +49 -24
- package/dist/scripts/pull-all.js.map +1 -1
- package/dist/scripts/pull-comments.d.ts +5 -1
- package/dist/scripts/pull-comments.d.ts.map +1 -1
- package/dist/scripts/pull-comments.js +8 -1
- package/dist/scripts/pull-comments.js.map +1 -1
- package/dist/scripts/pull-content.d.ts +2 -0
- package/dist/scripts/pull-content.d.ts.map +1 -1
- package/dist/scripts/pull-content.js +7 -1
- package/dist/scripts/pull-content.js.map +1 -1
- package/dist/scripts/pull-media.d.ts +5 -1
- package/dist/scripts/pull-media.d.ts.map +1 -1
- package/dist/scripts/pull-media.js +8 -1
- package/dist/scripts/pull-media.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -152,14 +152,17 @@ npm run test:watch
|
|
|
152
152
|
|
|
153
153
|
### Test Coverage
|
|
154
154
|
|
|
155
|
-
The SDK maintains high test coverage with
|
|
155
|
+
The SDK maintains high test coverage with **481 tests across 30 test suites**, covering:
|
|
156
156
|
|
|
157
|
-
- 📄 Content retrieval and
|
|
157
|
+
- 📄 Content retrieval, parsing, and transformation
|
|
158
158
|
- 🌍 Multi-language support and translations
|
|
159
159
|
- 📝 Draft content handling and user-specific overrides
|
|
160
160
|
- 🏗️ Build-time optimizations and caching
|
|
161
161
|
- 🔧 Configuration management and validation
|
|
162
162
|
- 🔄 Push/Pull synchronization with conflict detection
|
|
163
|
+
- 🗂️ Content rename, type change, and deletion handling
|
|
164
|
+
- 🗑️ Media deletion sync and file cleanup
|
|
165
|
+
- 🔁 Sync token migration and edge cases
|
|
163
166
|
- 🖥️ CLI command functionality with mocked API responses
|
|
164
167
|
|
|
165
168
|
### Testing with Mock Data
|
|
@@ -395,6 +398,9 @@ npx leadcms pull-media
|
|
|
395
398
|
|
|
396
399
|
# Pull only comments
|
|
397
400
|
npx leadcms pull-comments
|
|
401
|
+
|
|
402
|
+
# Reset and pull everything from scratch
|
|
403
|
+
npx leadcms pull --reset
|
|
398
404
|
```
|
|
399
405
|
|
|
400
406
|
> **Note:** `npx leadcms fetch` is still supported as an alias for backward compatibility.
|
|
@@ -402,11 +408,17 @@ npx leadcms pull-comments
|
|
|
402
408
|
What each command does:
|
|
403
409
|
|
|
404
410
|
- `npx leadcms pull` - Syncs content, media and comments into your project using the configured directories. Updates incremental sync tokens so subsequent runs are faster.
|
|
411
|
+
- `npx leadcms pull --reset` - Deletes all local content, media, comments, and sync tokens, then performs a full pull from scratch. Useful when local state has become inconsistent or after configuration changes.
|
|
405
412
|
- `npx leadcms pull-content` - Downloads only content entities (MDX/JSON files) and updates local metadata.
|
|
406
413
|
- `npx leadcms pull-media` - Downloads media files to your `mediaDir` (e.g., `public/media`). Use this when you changed media or want to refresh assets separately from content.
|
|
407
414
|
- `npx leadcms pull-comments` - Downloads comments to the comments directory (e.g., `.leadcms/comments/`). Useful when you only need comment updates.
|
|
408
415
|
|
|
409
|
-
|
|
416
|
+
**Intelligent sync handling:**
|
|
417
|
+
|
|
418
|
+
- **Incremental sync** — Sync tokens avoid re-downloading unchanged items on subsequent pulls.
|
|
419
|
+
- **Rename & type-change cleanup** — When content is renamed (slug change), moved to a different type, or switched format (MDX ↔ JSON), the old file is automatically removed before the new version is written.
|
|
420
|
+
- **Deleted content removal** — Content and media deleted on the server are removed locally during the next pull.
|
|
421
|
+
- **Sync token migration** — When upgrading from SDK ≤ 3.1, legacy sync tokens are automatically migrated to their new location inside each data directory.
|
|
410
422
|
|
|
411
423
|
### Push local content to LeadCMS
|
|
412
424
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-env.d.ts","sourceRoot":"","sources":["../../../src/cli/bin/generate-env.ts"],"names":[],"mappings":";AACA;;GAEG
|
|
1
|
+
{"version":3,"file":"generate-env.d.ts","sourceRoot":"","sources":["../../../src/cli/bin/generate-env.ts"],"names":[],"mappings":";AACA;;GAEG"}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* LeadCMS Generate Env CLI Entry Point
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { generateEnv } from '../../scripts/generate-env-js.js';
|
|
6
|
+
generateEnv();
|
|
7
7
|
//# sourceMappingURL=generate-env.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-env.js","sourceRoot":"","sources":["../../../src/cli/bin/generate-env.ts"],"names":[],"mappings":";AACA;;GAEG;AAEH,
|
|
1
|
+
{"version":3,"file":"generate-env.js","sourceRoot":"","sources":["../../../src/cli/bin/generate-env.ts"],"names":[],"mappings":";AACA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAE/D,WAAW,EAAE,CAAC"}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* LeadCMS Pull Comments CLI Entry Point
|
|
4
4
|
*/
|
|
5
5
|
import { pullComments } from '../../scripts/pull-comments.js';
|
|
6
|
-
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const reset = args.includes('--reset');
|
|
8
|
+
pullComments({ reset }).catch((error) => {
|
|
7
9
|
console.error('Error running LeadCMS pull comments:', error.message);
|
|
8
10
|
process.exit(1);
|
|
9
11
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pull-comments.js","sourceRoot":"","sources":["../../../src/cli/bin/pull-comments.ts"],"names":[],"mappings":";AACA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAE9D,YAAY,EAAE,CAAC,KAAK,CAAC,CAAC,KAAU,EAAE,EAAE;
|
|
1
|
+
{"version":3,"file":"pull-comments.js","sourceRoot":"","sources":["../../../src/cli/bin/pull-comments.ts"],"names":[],"mappings":";AACA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAE9D,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;AAEvC,YAAY,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAU,EAAE,EAAE;IAC3C,OAAO,CAAC,KAAK,CAAC,sCAAsC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACrE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -15,7 +15,8 @@ const slugIndex = args.findIndex(arg => arg === '--slug');
|
|
|
15
15
|
if (slugIndex !== -1 && args[slugIndex + 1]) {
|
|
16
16
|
targetSlug = args[slugIndex + 1];
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
const reset = args.includes('--reset');
|
|
19
|
+
pullContent({ targetId, targetSlug, reset }).catch((error) => {
|
|
19
20
|
console.error('Error running LeadCMS pull content:', error.message);
|
|
20
21
|
process.exit(1);
|
|
21
22
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pull-content.js","sourceRoot":"","sources":["../../../src/cli/bin/pull-content.ts"],"names":[],"mappings":";AACA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAE5D,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,0BAA0B;AAC1B,IAAI,QAA4B,CAAC;AACjC,IAAI,UAA8B,CAAC;AAEnC,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC;AACtD,IAAI,OAAO,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,CAAC;IACxC,QAAQ,GAAG,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC;AAC1D,IAAI,SAAS,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,EAAE,CAAC;IAC5C,UAAU,GAAG,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;AACnC,CAAC;AAED,WAAW,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAU,EAAE,EAAE;
|
|
1
|
+
{"version":3,"file":"pull-content.js","sourceRoot":"","sources":["../../../src/cli/bin/pull-content.ts"],"names":[],"mappings":";AACA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAE5D,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,0BAA0B;AAC1B,IAAI,QAA4B,CAAC;AACjC,IAAI,UAA8B,CAAC;AAEnC,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC;AACtD,IAAI,OAAO,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,CAAC;IACxC,QAAQ,GAAG,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC;AAC1D,IAAI,SAAS,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,EAAE,CAAC;IAC5C,UAAU,GAAG,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;AAEvC,WAAW,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAU,EAAE,EAAE;IAChE,OAAO,CAAC,KAAK,CAAC,qCAAqC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACpE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* LeadCMS Pull Media CLI Entry Point
|
|
4
4
|
*/
|
|
5
5
|
import { pullMedia } from '../../scripts/pull-media.js';
|
|
6
|
-
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const reset = args.includes('--reset');
|
|
8
|
+
pullMedia({ reset }).catch((error) => {
|
|
7
9
|
console.error('Error running LeadCMS pull media:', error.message);
|
|
8
10
|
process.exit(1);
|
|
9
11
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pull-media.js","sourceRoot":"","sources":["../../../src/cli/bin/pull-media.ts"],"names":[],"mappings":";AACA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAExD,SAAS,EAAE,CAAC,KAAK,CAAC,CAAC,KAAU,EAAE,EAAE;
|
|
1
|
+
{"version":3,"file":"pull-media.js","sourceRoot":"","sources":["../../../src/cli/bin/pull-media.ts"],"names":[],"mappings":";AACA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAExD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;AAEvC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAU,EAAE,EAAE;IACxC,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAClE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/cli/index.js
CHANGED
|
@@ -69,10 +69,10 @@ switch (command) {
|
|
|
69
69
|
runScript('pull-content.js', commandArgs);
|
|
70
70
|
break;
|
|
71
71
|
case 'pull-media':
|
|
72
|
-
runScript('pull-media.js');
|
|
72
|
+
runScript('pull-media.js', commandArgs);
|
|
73
73
|
break;
|
|
74
74
|
case 'pull-comments':
|
|
75
|
-
runScript('pull-comments.js');
|
|
75
|
+
runScript('pull-comments.js', commandArgs);
|
|
76
76
|
break;
|
|
77
77
|
case 'push':
|
|
78
78
|
runScript('push-all.js', commandArgs);
|
|
@@ -127,8 +127,11 @@ Usage:
|
|
|
127
127
|
leadcms pull-content [options] - Pull only content from LeadCMS
|
|
128
128
|
--id <content-id> - Pull specific content by ID
|
|
129
129
|
--slug <slug> - Pull specific content by slug
|
|
130
|
-
|
|
131
|
-
leadcms pull-
|
|
130
|
+
--reset - Delete local content files and sync token, then pull fresh
|
|
131
|
+
leadcms pull-media [options] - Pull only media files from LeadCMS
|
|
132
|
+
--reset - Delete local media files and sync token, then pull fresh
|
|
133
|
+
leadcms pull-comments [options] - Pull only comments from LeadCMS
|
|
134
|
+
--reset - Delete local comment files and sync token, then pull fresh
|
|
132
135
|
leadcms fetch - Alias for 'pull' (backward compatibility)
|
|
133
136
|
|
|
134
137
|
Push commands:
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,5 +3,7 @@ export * from './lib/config.js';
|
|
|
3
3
|
export * from './lib/locale-utils.js';
|
|
4
4
|
export * from './lib/comment-types.js';
|
|
5
5
|
export * from './lib/cms-config-types.js';
|
|
6
|
+
export { threeWayMerge, threeWayMergeJson, isLocallyModified } from './lib/content-merge.js';
|
|
7
|
+
export type { MergeResult } from './lib/content-merge.js';
|
|
6
8
|
export type { CommentTreeNode, CommentTreeOptions, CommentStatistics, CommentSortOrder } from './lib/comment-utils.js';
|
|
7
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAG1C,YAAY,EAAE,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAG1C,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC7F,YAAY,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAG1D,YAAY,EAAE,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ export * from './lib/config.js';
|
|
|
4
4
|
export * from './lib/locale-utils.js';
|
|
5
5
|
export * from './lib/comment-types.js';
|
|
6
6
|
export * from './lib/cms-config-types.js';
|
|
7
|
+
// Content merge utilities for three-way merge support
|
|
8
|
+
export { threeWayMerge, threeWayMergeJson, isLocallyModified } from './lib/content-merge.js';
|
|
7
9
|
// LeadCMS SDK - Framework-agnostic content management
|
|
8
10
|
//
|
|
9
11
|
// Core functions for accessing LeadCMS content:
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAe;AACf,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAe;AACf,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAE1C,sDAAsD;AACtD,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAM7F,sDAAsD;AACtD,EAAE;AACF,gDAAgD;AAChD,yDAAyD;AACzD,0DAA0D;AAC1D,wEAAwE;AACxE,iEAAiE;AACjE,sDAAsD;AACtD,EAAE;AACF,gBAAgB;AAChB,4DAA4D;AAC5D,uEAAuE;AACvE,kEAAkE;AAClE,wFAAwF;AACxF,iGAAiG;AACjG,sFAAsF;AACtF,EAAE;AACF,6FAA6F;AAC7F,4FAA4F;AAC5F,2DAA2D;AAC3D,EAAE;AACF,oBAAoB;AACpB,0DAA0D;AAC1D,EAAE;AACF,yBAAyB;AACzB,4CAA4C;AAC5C,sCAAsC;AACtC,kDAAkD;AAClD,EAAE;AACF,iBAAiB;AACjB,qFAAqF;AACrF,oFAAoF;AACpF,mEAAmE;AACnE,gEAAgE;AAChE,oEAAoE;AACpE,wDAAwD;AACxD,yDAAyD;AACzD,EAAE;AACF,mFAAmF;AACnF,qFAAqF"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three-way merge utilities for LeadCMS content
|
|
3
|
+
*
|
|
4
|
+
* Uses the node-diff3 library to perform git-style three-way merges between
|
|
5
|
+
* a base version, a local version, and a remote version of content files.
|
|
6
|
+
*
|
|
7
|
+
* For JSON content, a structural (field-level) merge is used instead of
|
|
8
|
+
* line-based diff to avoid false conflicts from adjacent line changes.
|
|
9
|
+
*
|
|
10
|
+
* The base version comes from the server via the sync API (the state of the
|
|
11
|
+
* content at the time of the client's last sync token). This avoids needing
|
|
12
|
+
* any local storage of base snapshots.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Result of a three-way content merge
|
|
16
|
+
*/
|
|
17
|
+
export interface MergeResult {
|
|
18
|
+
/** Whether the merge completed without conflicts */
|
|
19
|
+
success: boolean;
|
|
20
|
+
/** The merged content (may contain conflict markers if success is false) */
|
|
21
|
+
merged: string;
|
|
22
|
+
/** Whether there were conflicts that require manual resolution */
|
|
23
|
+
hasConflicts: boolean;
|
|
24
|
+
/** Number of conflict regions found */
|
|
25
|
+
conflictCount: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Perform a three-way merge between base, local, and remote content.
|
|
29
|
+
*
|
|
30
|
+
* This works like git merge:
|
|
31
|
+
* - Changes that don't overlap are merged automatically
|
|
32
|
+
* - Changes that modify the same lines produce conflict markers
|
|
33
|
+
* - Server-controlled fields (updatedAt, createdAt) in YAML frontmatter
|
|
34
|
+
* are auto-resolved to the remote value, never producing conflicts
|
|
35
|
+
*
|
|
36
|
+
* For JSON content, prefer threeWayMergeJson() which does structural merging
|
|
37
|
+
* and avoids false conflicts from adjacent line changes.
|
|
38
|
+
*
|
|
39
|
+
* @param base - The original content at the time of last sync (from server's baseItems)
|
|
40
|
+
* @param local - The current local file content (possibly user-modified)
|
|
41
|
+
* @param remote - The current remote content (fetched from server)
|
|
42
|
+
* @returns MergeResult with merged content and conflict information
|
|
43
|
+
*/
|
|
44
|
+
export declare function threeWayMerge(base: string, local: string, remote: string): MergeResult;
|
|
45
|
+
/**
|
|
46
|
+
* Perform a structural three-way merge on JSON content.
|
|
47
|
+
*
|
|
48
|
+
* Instead of doing a line-based diff (which produces false conflicts for
|
|
49
|
+
* adjacent field changes), this parses the JSON and merges field-by-field:
|
|
50
|
+
*
|
|
51
|
+
* - Fields changed only locally → keep local value
|
|
52
|
+
* - Fields changed only remotely → take remote value
|
|
53
|
+
* - Fields changed identically on both sides → take either (same value)
|
|
54
|
+
* - Fields changed differently on both sides → conflict
|
|
55
|
+
* - Server-controlled fields (updatedAt, createdAt) → always take remote
|
|
56
|
+
*
|
|
57
|
+
* For nested objects, the merge recurses into each level.
|
|
58
|
+
*
|
|
59
|
+
* @param base - The base JSON content (from server's baseItems, transformed to local format)
|
|
60
|
+
* @param local - The current local JSON file content
|
|
61
|
+
* @param remote - The current remote JSON content (transformed to local format)
|
|
62
|
+
* @returns MergeResult with the merged JSON string
|
|
63
|
+
*/
|
|
64
|
+
export declare function threeWayMergeJson(base: string, local: string, remote: string): MergeResult;
|
|
65
|
+
/**
|
|
66
|
+
* Determine whether a local file has been modified compared to the base version.
|
|
67
|
+
*
|
|
68
|
+
* This is used to decide whether three-way merge is needed:
|
|
69
|
+
* - If local === base → local is unmodified, safe to overwrite with remote
|
|
70
|
+
* - If local !== base → local was modified, need three-way merge
|
|
71
|
+
*
|
|
72
|
+
* Uses timestamp normalization to avoid false positives from precision differences
|
|
73
|
+
* (e.g. server returns 7 decimal places but local file has 6).
|
|
74
|
+
*
|
|
75
|
+
* @param base - The base content (from server's baseItems, transformed to local format)
|
|
76
|
+
* @param local - The current local file content
|
|
77
|
+
* @returns true if local content differs from base
|
|
78
|
+
*/
|
|
79
|
+
export declare function isLocallyModified(base: string, local: string): boolean;
|
|
80
|
+
//# sourceMappingURL=content-merge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-merge.d.ts","sourceRoot":"","sources":["../../src/lib/content-merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,oDAAoD;IACpD,OAAO,EAAE,OAAO,CAAC;IACjB,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,YAAY,EAAE,OAAO,CAAC;IACtB,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;CACvB;AAsBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CA4CtF;AAsED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CAwB1F;AAiJD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAEtE"}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three-way merge utilities for LeadCMS content
|
|
3
|
+
*
|
|
4
|
+
* Uses the node-diff3 library to perform git-style three-way merges between
|
|
5
|
+
* a base version, a local version, and a remote version of content files.
|
|
6
|
+
*
|
|
7
|
+
* For JSON content, a structural (field-level) merge is used instead of
|
|
8
|
+
* line-based diff to avoid false conflicts from adjacent line changes.
|
|
9
|
+
*
|
|
10
|
+
* The base version comes from the server via the sync API (the state of the
|
|
11
|
+
* content at the time of the client's last sync token). This avoids needing
|
|
12
|
+
* any local storage of base snapshots.
|
|
13
|
+
*/
|
|
14
|
+
import { diff3Merge } from 'node-diff3';
|
|
15
|
+
/**
|
|
16
|
+
* Fields that are controlled by the server and should always take the remote
|
|
17
|
+
* value during merge, even if they differ between base and local.
|
|
18
|
+
* These fields are set/updated by the server automatically and should never
|
|
19
|
+
* be treated as meaningful local edits.
|
|
20
|
+
*/
|
|
21
|
+
const SERVER_CONTROLLED_FIELDS = new Set([
|
|
22
|
+
'updatedAt',
|
|
23
|
+
'createdAt',
|
|
24
|
+
]);
|
|
25
|
+
/**
|
|
26
|
+
* Regex to match a YAML frontmatter line for a server-controlled field.
|
|
27
|
+
* Matches lines like:
|
|
28
|
+
* updatedAt: "2026-02-01T00:00:00Z"
|
|
29
|
+
* createdAt: "2026-01-01T00:00:00Z"
|
|
30
|
+
* updatedAt: 2026-02-01T00:00:00Z
|
|
31
|
+
*/
|
|
32
|
+
const SERVER_CONTROLLED_YAML_LINE = /^\s*(updatedAt|createdAt)\s*:/;
|
|
33
|
+
/**
|
|
34
|
+
* Perform a three-way merge between base, local, and remote content.
|
|
35
|
+
*
|
|
36
|
+
* This works like git merge:
|
|
37
|
+
* - Changes that don't overlap are merged automatically
|
|
38
|
+
* - Changes that modify the same lines produce conflict markers
|
|
39
|
+
* - Server-controlled fields (updatedAt, createdAt) in YAML frontmatter
|
|
40
|
+
* are auto-resolved to the remote value, never producing conflicts
|
|
41
|
+
*
|
|
42
|
+
* For JSON content, prefer threeWayMergeJson() which does structural merging
|
|
43
|
+
* and avoids false conflicts from adjacent line changes.
|
|
44
|
+
*
|
|
45
|
+
* @param base - The original content at the time of last sync (from server's baseItems)
|
|
46
|
+
* @param local - The current local file content (possibly user-modified)
|
|
47
|
+
* @param remote - The current remote content (fetched from server)
|
|
48
|
+
* @returns MergeResult with merged content and conflict information
|
|
49
|
+
*/
|
|
50
|
+
export function threeWayMerge(base, local, remote) {
|
|
51
|
+
const baseLines = base.split('\n');
|
|
52
|
+
const localLines = local.split('\n');
|
|
53
|
+
const remoteLines = remote.split('\n');
|
|
54
|
+
const regions = diff3Merge(localLines, baseLines, remoteLines);
|
|
55
|
+
let conflictCount = 0;
|
|
56
|
+
const resultLines = [];
|
|
57
|
+
for (const region of regions) {
|
|
58
|
+
if ('ok' in region && region.ok) {
|
|
59
|
+
resultLines.push(...region.ok);
|
|
60
|
+
}
|
|
61
|
+
else if ('conflict' in region && region.conflict) {
|
|
62
|
+
const localConflictLines = region.conflict.a;
|
|
63
|
+
const remoteConflictLines = region.conflict.b;
|
|
64
|
+
// Try to auto-resolve server-controlled fields within the conflict
|
|
65
|
+
const resolved = resolveServerControlledConflict(localConflictLines, remoteConflictLines);
|
|
66
|
+
if (resolved.remainingConflict) {
|
|
67
|
+
// There are still real conflicts after extracting server-controlled fields
|
|
68
|
+
conflictCount++;
|
|
69
|
+
resultLines.push(...resolved.resolvedLines);
|
|
70
|
+
resultLines.push('<<<<<<< local');
|
|
71
|
+
resultLines.push(...resolved.remainingConflict.local);
|
|
72
|
+
resultLines.push('=======');
|
|
73
|
+
resultLines.push(...resolved.remainingConflict.remote);
|
|
74
|
+
resultLines.push('>>>>>>> remote');
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// All lines in this conflict were server-controlled → fully auto-resolved
|
|
78
|
+
resultLines.push(...resolved.resolvedLines);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const merged = resultLines.join('\n');
|
|
83
|
+
return {
|
|
84
|
+
success: conflictCount === 0,
|
|
85
|
+
merged,
|
|
86
|
+
hasConflicts: conflictCount > 0,
|
|
87
|
+
conflictCount,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Attempt to auto-resolve server-controlled fields within a conflict region.
|
|
92
|
+
*
|
|
93
|
+
* For each line in the conflict, if it's a server-controlled YAML field
|
|
94
|
+
* (updatedAt, createdAt), take the remote version. Non-server-controlled
|
|
95
|
+
* lines remain as conflicts.
|
|
96
|
+
*
|
|
97
|
+
* Returns:
|
|
98
|
+
* - resolvedLines: lines that were auto-resolved (server-controlled)
|
|
99
|
+
* - remainingConflict: null if fully resolved, or { local, remote } with
|
|
100
|
+
* the non-server-controlled lines that still conflict
|
|
101
|
+
*/
|
|
102
|
+
function resolveServerControlledConflict(localLines, remoteLines) {
|
|
103
|
+
const resolvedLines = [];
|
|
104
|
+
const remainingLocal = [];
|
|
105
|
+
const remainingRemote = [];
|
|
106
|
+
// Separate server-controlled from non-server-controlled lines on each side
|
|
107
|
+
const localServerControlled = [];
|
|
108
|
+
const localOther = [];
|
|
109
|
+
for (const line of localLines) {
|
|
110
|
+
if (SERVER_CONTROLLED_YAML_LINE.test(line)) {
|
|
111
|
+
localServerControlled.push(line);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
localOther.push(line);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const remoteServerControlled = [];
|
|
118
|
+
const remoteOther = [];
|
|
119
|
+
for (const line of remoteLines) {
|
|
120
|
+
if (SERVER_CONTROLLED_YAML_LINE.test(line)) {
|
|
121
|
+
remoteServerControlled.push(line);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
remoteOther.push(line);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Auto-resolve server-controlled fields: always take remote version
|
|
128
|
+
resolvedLines.push(...remoteServerControlled);
|
|
129
|
+
// Check if there are remaining non-server-controlled lines
|
|
130
|
+
if (localOther.length === 0 && remoteOther.length === 0) {
|
|
131
|
+
// Entire conflict was server-controlled → fully resolved
|
|
132
|
+
return { resolvedLines, remainingConflict: null };
|
|
133
|
+
}
|
|
134
|
+
// There are real conflicting lines remaining
|
|
135
|
+
return {
|
|
136
|
+
resolvedLines,
|
|
137
|
+
remainingConflict: { local: localOther, remote: remoteOther },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Perform a structural three-way merge on JSON content.
|
|
142
|
+
*
|
|
143
|
+
* Instead of doing a line-based diff (which produces false conflicts for
|
|
144
|
+
* adjacent field changes), this parses the JSON and merges field-by-field:
|
|
145
|
+
*
|
|
146
|
+
* - Fields changed only locally → keep local value
|
|
147
|
+
* - Fields changed only remotely → take remote value
|
|
148
|
+
* - Fields changed identically on both sides → take either (same value)
|
|
149
|
+
* - Fields changed differently on both sides → conflict
|
|
150
|
+
* - Server-controlled fields (updatedAt, createdAt) → always take remote
|
|
151
|
+
*
|
|
152
|
+
* For nested objects, the merge recurses into each level.
|
|
153
|
+
*
|
|
154
|
+
* @param base - The base JSON content (from server's baseItems, transformed to local format)
|
|
155
|
+
* @param local - The current local JSON file content
|
|
156
|
+
* @param remote - The current remote JSON content (transformed to local format)
|
|
157
|
+
* @returns MergeResult with the merged JSON string
|
|
158
|
+
*/
|
|
159
|
+
export function threeWayMergeJson(base, local, remote) {
|
|
160
|
+
let baseObj;
|
|
161
|
+
let localObj;
|
|
162
|
+
let remoteObj;
|
|
163
|
+
try {
|
|
164
|
+
baseObj = JSON.parse(base);
|
|
165
|
+
localObj = JSON.parse(local);
|
|
166
|
+
remoteObj = JSON.parse(remote);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// If any version is not valid JSON, fall back to line-based merge
|
|
170
|
+
return threeWayMerge(base, local, remote);
|
|
171
|
+
}
|
|
172
|
+
const { value: mergedObj, conflicted, conflictCount } = mergeValues(baseObj, localObj, remoteObj);
|
|
173
|
+
const merged = JSON.stringify(mergedObj, null, 2);
|
|
174
|
+
return {
|
|
175
|
+
success: !conflicted,
|
|
176
|
+
merged,
|
|
177
|
+
hasConflicts: conflicted,
|
|
178
|
+
conflictCount,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Recursively merge three values (base, local, remote).
|
|
183
|
+
* Returns the merged value and whether any conflicts were found.
|
|
184
|
+
*/
|
|
185
|
+
function mergeValues(base, local, remote) {
|
|
186
|
+
// If both local and remote are objects (not arrays), merge field by field
|
|
187
|
+
if (isPlainObject(base) && isPlainObject(local) && isPlainObject(remote)) {
|
|
188
|
+
return mergeObjects(base, local, remote);
|
|
189
|
+
}
|
|
190
|
+
// For non-object values (primitives, arrays, etc.): compare as JSON strings
|
|
191
|
+
const baseStr = JSON.stringify(base);
|
|
192
|
+
const localStr = JSON.stringify(local);
|
|
193
|
+
const remoteStr = JSON.stringify(remote);
|
|
194
|
+
if (baseStr === localStr && baseStr === remoteStr) {
|
|
195
|
+
// No changes
|
|
196
|
+
return { value: local, conflicted: false, conflictCount: 0 };
|
|
197
|
+
}
|
|
198
|
+
if (baseStr === localStr) {
|
|
199
|
+
// Only remote changed → take remote
|
|
200
|
+
return { value: remote, conflicted: false, conflictCount: 0 };
|
|
201
|
+
}
|
|
202
|
+
if (baseStr === remoteStr) {
|
|
203
|
+
// Only local changed → keep local
|
|
204
|
+
return { value: local, conflicted: false, conflictCount: 0 };
|
|
205
|
+
}
|
|
206
|
+
if (localStr === remoteStr) {
|
|
207
|
+
// Both changed identically → take either
|
|
208
|
+
return { value: local, conflicted: false, conflictCount: 0 };
|
|
209
|
+
}
|
|
210
|
+
// Both changed differently → conflict
|
|
211
|
+
// Use a special marker object that will be serialized with conflict info
|
|
212
|
+
return {
|
|
213
|
+
value: {
|
|
214
|
+
'<<<<<<< local': local,
|
|
215
|
+
'=======': '---',
|
|
216
|
+
'>>>>>>> remote': remote,
|
|
217
|
+
},
|
|
218
|
+
conflicted: true,
|
|
219
|
+
conflictCount: 1,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Merge three plain objects field by field.
|
|
224
|
+
*/
|
|
225
|
+
function mergeObjects(base, local, remote) {
|
|
226
|
+
const allKeys = new Set([
|
|
227
|
+
...Object.keys(base),
|
|
228
|
+
...Object.keys(local),
|
|
229
|
+
...Object.keys(remote),
|
|
230
|
+
]);
|
|
231
|
+
const merged = {};
|
|
232
|
+
let hasConflict = false;
|
|
233
|
+
let totalConflicts = 0;
|
|
234
|
+
for (const key of allKeys) {
|
|
235
|
+
const inBase = key in base;
|
|
236
|
+
const inLocal = key in local;
|
|
237
|
+
const inRemote = key in remote;
|
|
238
|
+
// Server-controlled fields: always take remote value
|
|
239
|
+
if (SERVER_CONTROLLED_FIELDS.has(key)) {
|
|
240
|
+
if (inRemote) {
|
|
241
|
+
merged[key] = remote[key];
|
|
242
|
+
}
|
|
243
|
+
else if (inLocal) {
|
|
244
|
+
merged[key] = local[key];
|
|
245
|
+
}
|
|
246
|
+
// If only in base, it was deleted from both → omit
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (inBase && inLocal && inRemote) {
|
|
250
|
+
// Key exists in all three — merge the values
|
|
251
|
+
const result = mergeValues(base[key], local[key], remote[key]);
|
|
252
|
+
merged[key] = result.value;
|
|
253
|
+
if (result.conflicted) {
|
|
254
|
+
hasConflict = true;
|
|
255
|
+
totalConflicts += result.conflictCount;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else if (!inBase && inLocal && inRemote) {
|
|
259
|
+
// Key added by both sides
|
|
260
|
+
const result = mergeValues(undefined, local[key], remote[key]);
|
|
261
|
+
merged[key] = result.value;
|
|
262
|
+
if (result.conflicted) {
|
|
263
|
+
hasConflict = true;
|
|
264
|
+
totalConflicts += result.conflictCount;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else if (!inBase && inLocal && !inRemote) {
|
|
268
|
+
// Key added only locally → keep
|
|
269
|
+
merged[key] = local[key];
|
|
270
|
+
}
|
|
271
|
+
else if (!inBase && !inLocal && inRemote) {
|
|
272
|
+
// Key added only remotely → take
|
|
273
|
+
merged[key] = remote[key];
|
|
274
|
+
}
|
|
275
|
+
else if (inBase && !inLocal && inRemote) {
|
|
276
|
+
// Key deleted locally — check if remote also changed it
|
|
277
|
+
const baseStr = JSON.stringify(base[key]);
|
|
278
|
+
const remoteStr = JSON.stringify(remote[key]);
|
|
279
|
+
if (baseStr === remoteStr) {
|
|
280
|
+
// Remote didn't change it, local deleted it → keep deleted (omit)
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
// Remote changed it but local deleted it → conflict, prefer remote
|
|
284
|
+
merged[key] = remote[key];
|
|
285
|
+
hasConflict = true;
|
|
286
|
+
totalConflicts++;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else if (inBase && inLocal && !inRemote) {
|
|
290
|
+
// Key deleted remotely — check if local also changed it
|
|
291
|
+
const baseStr = JSON.stringify(base[key]);
|
|
292
|
+
const localStr = JSON.stringify(local[key]);
|
|
293
|
+
if (baseStr === localStr) {
|
|
294
|
+
// Local didn't change it, remote deleted it → keep deleted (omit)
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
// Local changed it but remote deleted it → conflict, prefer local
|
|
298
|
+
merged[key] = local[key];
|
|
299
|
+
hasConflict = true;
|
|
300
|
+
totalConflicts++;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else if (inBase && !inLocal && !inRemote) {
|
|
304
|
+
// Deleted by both sides → omit
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return { value: merged, conflicted: hasConflict, conflictCount: totalConflicts };
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Check if a value is a plain object (not array, null, Date, etc.)
|
|
311
|
+
*/
|
|
312
|
+
function isPlainObject(value) {
|
|
313
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Determine whether a local file has been modified compared to the base version.
|
|
317
|
+
*
|
|
318
|
+
* This is used to decide whether three-way merge is needed:
|
|
319
|
+
* - If local === base → local is unmodified, safe to overwrite with remote
|
|
320
|
+
* - If local !== base → local was modified, need three-way merge
|
|
321
|
+
*
|
|
322
|
+
* Uses timestamp normalization to avoid false positives from precision differences
|
|
323
|
+
* (e.g. server returns 7 decimal places but local file has 6).
|
|
324
|
+
*
|
|
325
|
+
* @param base - The base content (from server's baseItems, transformed to local format)
|
|
326
|
+
* @param local - The current local file content
|
|
327
|
+
* @returns true if local content differs from base
|
|
328
|
+
*/
|
|
329
|
+
export function isLocallyModified(base, local) {
|
|
330
|
+
return normalizeForMergeComparison(base) !== normalizeForMergeComparison(local);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Normalize content for merge comparison.
|
|
334
|
+
* Handles trivial whitespace and timestamp precision differences that shouldn't
|
|
335
|
+
* trigger a merge.
|
|
336
|
+
*/
|
|
337
|
+
function normalizeForMergeComparison(content) {
|
|
338
|
+
return content
|
|
339
|
+
.replace(/\r\n/g, '\n')
|
|
340
|
+
.replace(/\s+\n/g, '\n')
|
|
341
|
+
// Normalize ISO timestamp precision: truncate fractional seconds to 6 decimal
|
|
342
|
+
// places (microsecond precision) then strip trailing zeros.
|
|
343
|
+
// e.g. "2026-02-13T10:32:20.2939836Z" → "2026-02-13T10:32:20.293983Z"
|
|
344
|
+
// This prevents false diffs from servers returning 7-digit precision while
|
|
345
|
+
// local serializers use 6-digit precision.
|
|
346
|
+
.replace(/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{1,6})\d*Z/g, '$1Z')
|
|
347
|
+
.replace(/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+?)0+Z/g, '$1Z')
|
|
348
|
+
.trimEnd();
|
|
349
|
+
}
|
|
350
|
+
//# sourceMappingURL=content-merge.js.map
|