@jumoo/usync 0.0.0-stage → 17.5.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.
Files changed (199) hide show
  1. package/LICENCE +373 -0
  2. package/README.md +10 -3
  3. package/dist/api/client/client.gen.d.ts +2 -0
  4. package/dist/api/client/client.gen.js +216 -0
  5. package/dist/api/client/index.d.ts +10 -0
  6. package/dist/api/client/index.js +6 -0
  7. package/dist/api/client/types.gen.d.ts +120 -0
  8. package/dist/api/client/types.gen.js +2 -0
  9. package/dist/api/client/utils.gen.d.ts +37 -0
  10. package/dist/api/client/utils.gen.js +230 -0
  11. package/dist/api/client.gen.d.ts +12 -0
  12. package/dist/api/client.gen.js +4 -0
  13. package/dist/api/core/auth.gen.d.ts +25 -0
  14. package/dist/api/core/auth.gen.js +14 -0
  15. package/dist/api/core/bodySerializer.gen.d.ts +25 -0
  16. package/dist/api/core/bodySerializer.gen.js +57 -0
  17. package/dist/api/core/params.gen.d.ts +43 -0
  18. package/dist/api/core/params.gen.js +109 -0
  19. package/dist/api/core/pathSerializer.gen.d.ts +33 -0
  20. package/dist/api/core/pathSerializer.gen.js +106 -0
  21. package/dist/api/core/queryKeySerializer.gen.d.ts +18 -0
  22. package/dist/api/core/queryKeySerializer.gen.js +92 -0
  23. package/dist/api/core/serverSentEvents.gen.d.ts +71 -0
  24. package/dist/api/core/serverSentEvents.gen.js +132 -0
  25. package/dist/api/core/types.gen.d.ts +83 -0
  26. package/dist/api/core/types.gen.js +2 -0
  27. package/dist/api/core/utils.gen.d.ts +19 -0
  28. package/dist/api/core/utils.gen.js +87 -0
  29. package/dist/api/index.d.ts +3 -0
  30. package/dist/api/index.js +4 -0
  31. package/dist/api/sdk.gen.d.ts +44 -0
  32. package/dist/api/sdk.gen.js +147 -0
  33. package/dist/api/types.gen.d.ts +567 -0
  34. package/dist/api/types.gen.js +47 -0
  35. package/dist/components/events.d.ts +11 -0
  36. package/dist/components/events.js +11 -0
  37. package/dist/components/index.d.ts +8 -0
  38. package/dist/components/index.js +9 -0
  39. package/dist/components/usync-action-box.d.ts +22 -0
  40. package/dist/components/usync-action-box.js +119 -0
  41. package/dist/components/usync-change-detail.d.ts +31 -0
  42. package/dist/components/usync-change-detail.js +709 -0
  43. package/dist/components/usync-change-view.d.ts +22 -0
  44. package/dist/components/usync-change-view.js +125 -0
  45. package/dist/components/usync-progress-box.d.ts +20 -0
  46. package/dist/components/usync-progress-box.js +223 -0
  47. package/dist/components/usync-result-row.d.ts +15 -0
  48. package/dist/components/usync-result-row.js +133 -0
  49. package/dist/components/usync-results-group-view.d.ts +19 -0
  50. package/dist/components/usync-results-group-view.js +110 -0
  51. package/dist/components/usync-results-view.d.ts +17 -0
  52. package/dist/components/usync-results-view.js +176 -0
  53. package/dist/components/usync-sync-file-info-view.d.ts +15 -0
  54. package/dist/components/usync-sync-file-info-view.js +62 -0
  55. package/dist/conditions/constants.d.ts +1 -0
  56. package/dist/conditions/constants.js +1 -0
  57. package/dist/conditions/index.d.ts +2 -0
  58. package/dist/conditions/index.js +2 -0
  59. package/dist/conditions/legacy-files.condition.d.ts +10 -0
  60. package/dist/conditions/legacy-files.condition.js +16 -0
  61. package/dist/conditions/manifest.d.ts +1 -0
  62. package/dist/conditions/manifest.js +18 -0
  63. package/dist/conditions/new-section.condition.d.ts +8 -0
  64. package/dist/conditions/new-section.condition.js +48 -0
  65. package/dist/constants.d.ts +30 -0
  66. package/dist/constants.js +38 -0
  67. package/dist/dialogs/details-modal-element.d.ts +18 -0
  68. package/dist/dialogs/details-modal-element.js +98 -0
  69. package/dist/dialogs/details-modal-token.d.ts +10 -0
  70. package/dist/dialogs/details-modal-token.js +7 -0
  71. package/dist/dialogs/error-modal-element.d.ts +7 -0
  72. package/dist/dialogs/error-modal-element.js +54 -0
  73. package/dist/dialogs/error-modal-token.d.ts +9 -0
  74. package/dist/dialogs/error-modal-token.js +6 -0
  75. package/dist/dialogs/index.d.ts +6 -0
  76. package/dist/dialogs/index.js +6 -0
  77. package/dist/dialogs/legacy-modal-element.d.ts +13 -0
  78. package/dist/dialogs/legacy-modal-element.js +78 -0
  79. package/dist/dialogs/manifest.d.ts +1 -0
  80. package/dist/dialogs/manifest.js +25 -0
  81. package/dist/dialogs/single-import-modal.element.d.ts +13 -0
  82. package/dist/dialogs/single-import-modal.element.js +105 -0
  83. package/dist/dialogs/single-import-modal.token.d.ts +9 -0
  84. package/dist/dialogs/single-import-modal.token.js +6 -0
  85. package/dist/exports.d.ts +10 -0
  86. package/dist/exports.js +10 -0
  87. package/dist/external/signalr/index.d.ts +1 -0
  88. package/dist/external/signalr/index.js +1 -0
  89. package/dist/hey-api.d.ts +11 -0
  90. package/dist/hey-api.js +14 -0
  91. package/dist/icons/icons.d.ts +5 -0
  92. package/dist/icons/icons.js +6 -0
  93. package/dist/icons/manifest.d.ts +1 -0
  94. package/dist/icons/manifest.js +7 -0
  95. package/dist/icons/svg/logo.d.ts +2 -0
  96. package/dist/icons/svg/logo.js +21 -0
  97. package/dist/index.d.ts +8 -0
  98. package/dist/index.js +11 -0
  99. package/dist/lang/files/da-dk.d.ts +130 -0
  100. package/dist/lang/files/da-dk.js +141 -0
  101. package/dist/lang/files/de-de.d.ts +130 -0
  102. package/dist/lang/files/de-de.js +141 -0
  103. package/dist/lang/files/en-us.d.ts +161 -0
  104. package/dist/lang/files/en-us.js +172 -0
  105. package/dist/lang/files/es-es.d.ts +130 -0
  106. package/dist/lang/files/es-es.js +141 -0
  107. package/dist/lang/files/fr-fr.d.ts +130 -0
  108. package/dist/lang/files/fr-fr.js +141 -0
  109. package/dist/lang/files/nl-nl.d.ts +130 -0
  110. package/dist/lang/files/nl-nl.js +141 -0
  111. package/dist/lang/manifest.d.ts +1 -0
  112. package/dist/lang/manifest.js +63 -0
  113. package/dist/manifests.d.ts +1 -0
  114. package/dist/manifests.js +14 -0
  115. package/dist/repository/SyncAction.respositoy.d.ts +101 -0
  116. package/dist/repository/SyncAction.respositoy.js +125 -0
  117. package/dist/repository/index.d.ts +4 -0
  118. package/dist/repository/index.js +4 -0
  119. package/dist/repository/sources/SyncAction.source.d.ts +37 -0
  120. package/dist/repository/sources/SyncAction.source.js +57 -0
  121. package/dist/repository/sources/SyncMigration.source.d.ts +13 -0
  122. package/dist/repository/sources/SyncMigration.source.js +30 -0
  123. package/dist/repository/sources/SyncSettings.source.d.ts +23 -0
  124. package/dist/repository/sources/SyncSettings.source.js +33 -0
  125. package/dist/signalr/index.d.ts +3 -0
  126. package/dist/signalr/index.js +3 -0
  127. package/dist/signalr/signalr-reconnect-policy.d.ts +11 -0
  128. package/dist/signalr/signalr-reconnect-policy.js +14 -0
  129. package/dist/signalr/signalr.context.d.ts +34 -0
  130. package/dist/signalr/signalr.context.js +110 -0
  131. package/dist/signalr/signalr.context.token.d.ts +3 -0
  132. package/dist/signalr/signalr.context.token.js +2 -0
  133. package/dist/signalr/types.d.ts +21 -0
  134. package/dist/signalr/types.js +1 -0
  135. package/dist/tree/constants.d.ts +1 -0
  136. package/dist/tree/constants.js +1 -0
  137. package/dist/tree/index.d.ts +2 -0
  138. package/dist/tree/index.js +2 -0
  139. package/dist/tree/manifest.d.ts +1 -0
  140. package/dist/tree/manifest.js +92 -0
  141. package/dist/tree/types.d.ts +14 -0
  142. package/dist/tree/types.js +1 -0
  143. package/dist/tree/usync.menu-element.d.ts +13 -0
  144. package/dist/tree/usync.menu-element.js +74 -0
  145. package/dist/utils/diff/change-name.d.ts +13 -0
  146. package/dist/utils/diff/change-name.js +30 -0
  147. package/dist/utils/diff/classify-change.d.ts +21 -0
  148. package/dist/utils/diff/classify-change.js +111 -0
  149. package/dist/utils/diff/deep-equal.d.ts +13 -0
  150. package/dist/utils/diff/deep-equal.js +57 -0
  151. package/dist/utils/diff/format-value.d.ts +36 -0
  152. package/dist/utils/diff/format-value.js +88 -0
  153. package/dist/utils/diff/index.d.ts +7 -0
  154. package/dist/utils/diff/index.js +7 -0
  155. package/dist/utils/diff/json-diff.d.ts +34 -0
  156. package/dist/utils/diff/json-diff.js +300 -0
  157. package/dist/utils/diff/line-diff.d.ts +16 -0
  158. package/dist/utils/diff/line-diff.js +199 -0
  159. package/dist/utils/diff/types.d.ts +62 -0
  160. package/dist/utils/diff/types.js +12 -0
  161. package/dist/workspace/components/events.d.ts +31 -0
  162. package/dist/workspace/components/events.js +54 -0
  163. package/dist/workspace/components/index.d.ts +3 -0
  164. package/dist/workspace/components/index.js +3 -0
  165. package/dist/workspace/components/usync-action-button.d.ts +19 -0
  166. package/dist/workspace/components/usync-action-button.js +113 -0
  167. package/dist/workspace/components/usync-file-picker.element.d.ts +23 -0
  168. package/dist/workspace/components/usync-file-picker.element.js +106 -0
  169. package/dist/workspace/components/usync-file-upload.element.d.ts +21 -0
  170. package/dist/workspace/components/usync-file-upload.element.js +121 -0
  171. package/dist/workspace/dialogs/import-modal.element.d.ts +11 -0
  172. package/dist/workspace/dialogs/import-modal.element.js +75 -0
  173. package/dist/workspace/dialogs/import-modal.token.d.ts +6 -0
  174. package/dist/workspace/dialogs/import-modal.token.js +6 -0
  175. package/dist/workspace/dialogs/index.d.ts +2 -0
  176. package/dist/workspace/dialogs/index.js +2 -0
  177. package/dist/workspace/dialogs/manifest.d.ts +1 -0
  178. package/dist/workspace/dialogs/manifest.js +7 -0
  179. package/dist/workspace/index.d.ts +3 -0
  180. package/dist/workspace/index.js +3 -0
  181. package/dist/workspace/manifest.d.ts +3 -0
  182. package/dist/workspace/manifest.js +112 -0
  183. package/dist/workspace/types.d.ts +12 -0
  184. package/dist/workspace/types.js +1 -0
  185. package/dist/workspace/views/addons/addons.element.d.ts +7 -0
  186. package/dist/workspace/views/addons/addons.element.js +117 -0
  187. package/dist/workspace/views/default/default.element.d.ts +31 -0
  188. package/dist/workspace/views/default/default.element.js +466 -0
  189. package/dist/workspace/views/legacy/legacy.element.d.ts +14 -0
  190. package/dist/workspace/views/legacy/legacy.element.js +187 -0
  191. package/dist/workspace/views/settings/components/usyncSettingItem.element.d.ts +15 -0
  192. package/dist/workspace/views/settings/components/usyncSettingItem.element.js +96 -0
  193. package/dist/workspace/views/settings/settings.element.d.ts +17 -0
  194. package/dist/workspace/views/settings/settings.element.js +174 -0
  195. package/dist/workspace/workspace.context.d.ts +74 -0
  196. package/dist/workspace/workspace.context.js +448 -0
  197. package/dist/workspace/workspace.element.d.ts +17 -0
  198. package/dist/workspace/workspace.element.js +69 -0
  199. package/package.json +70 -6
@@ -0,0 +1,300 @@
1
+ import { deepEqual, isPlainObject, typeTag } from './deep-equal.js';
2
+ import { elementPath, joinKey } from './format-value.js';
3
+ import { tryParseJsonContainer } from './classify-change.js';
4
+ import { USYNC_DIFF_LIMITS, } from './types.js';
5
+ // A private sentinel for "this side has no value at all" - distinct from
6
+ // `undefined`, which can legitimately appear as the result of a string
7
+ // expansion round-trip. This keeps `{"a": null}` vs `{}` distinguishable from
8
+ // a missing key.
9
+ const MISSING = Symbol('usync-diff-missing');
10
+ /**
11
+ * Composite identities are tried before single fields, and in the order
12
+ * listed - a block's `values` entries and property values key on
13
+ * alias(+culture/segment) almost always, and that composite survives
14
+ * serialisation round-trips even when raw GUIDs don't.
15
+ */
16
+ export const USYNC_COMPOSITE_IDENTITIES = [
17
+ ['alias', 'culture', 'segment'], // block `values`, property values
18
+ ['propertyAlias', 'culture', 'segment'],
19
+ ['contentKey', 'culture', 'segment'],
20
+ ];
21
+ // `$type` is deliberately excluded - it's a label, not an identity: it's
22
+ // never unique within a block array.
23
+ export const USYNC_IDENTITY_FIELDS = [
24
+ 'key',
25
+ 'udi',
26
+ 'contentUdi',
27
+ 'contentKey',
28
+ 'settingsKey',
29
+ 'id',
30
+ 'alias',
31
+ 'editorAlias',
32
+ 'name',
33
+ ];
34
+ function isUniqueAndIntersecting(a, b, keyOf) {
35
+ const aKeys = a.map((element) => keyOf(element));
36
+ const bKeys = b.map((element) => keyOf(element));
37
+ if (new Set(aKeys).size !== aKeys.length)
38
+ return false;
39
+ if (new Set(bKeys).size !== bKeys.length)
40
+ return false;
41
+ // The load-bearing check: Umbraco regenerates block `key` GUIDs on some
42
+ // serialisation paths. If both sides have unique but entirely disjoint
43
+ // keys, identity matching would report "everything removed, everything
44
+ // added" - strictly worse than index matching. Requiring a shared key
45
+ // means we only key when the key actually carries continuity across the
46
+ // two sides; otherwise we correctly fall back to index matching.
47
+ const bSet = new Set(bKeys);
48
+ return aKeys.some((key) => bSet.has(key));
49
+ }
50
+ function tryCompositeStrategy(fields, a, b) {
51
+ const primary = fields[0];
52
+ const hasPrimary = (elements) => elements.every((element) => {
53
+ if (!isPlainObject(element))
54
+ return false;
55
+ const value = element[primary];
56
+ return typeof value === 'string' && value.length > 0;
57
+ });
58
+ if (!hasPrimary(a) || !hasPrimary(b))
59
+ return null;
60
+ const keyOf = (element) => fields
61
+ .map((field) => typeof element[field] === 'string' ? element[field] : '')
62
+ .join('\u0000');
63
+ const labelOf = (element) => {
64
+ const primaryValue = String(element[primary]);
65
+ const culture = fields.includes('culture') ? element.culture : undefined;
66
+ return typeof culture === 'string' && culture.length > 0
67
+ ? `${primaryValue} (${culture})`
68
+ : primaryValue;
69
+ };
70
+ if (!isUniqueAndIntersecting(a, b, keyOf))
71
+ return null;
72
+ return { isComposite: true, keyOf, labelOf };
73
+ }
74
+ function trySingleFieldStrategy(field, a, b) {
75
+ const hasField = (elements) => elements.every((element) => {
76
+ if (!isPlainObject(element))
77
+ return false;
78
+ const value = element[field];
79
+ return typeof value === 'string' || typeof value === 'number';
80
+ });
81
+ if (!hasField(a) || !hasField(b))
82
+ return null;
83
+ const keyOf = (element) => String(element[field]);
84
+ const labelOf = (element) => String(element[field]);
85
+ if (!isUniqueAndIntersecting(a, b, keyOf))
86
+ return null;
87
+ return { isComposite: false, keyOf, labelOf };
88
+ }
89
+ /**
90
+ * Picks an identity strategy for matching elements of two arrays by identity
91
+ * rather than position, or `null` when neither array offers one (the caller
92
+ * should fall back to index matching).
93
+ */
94
+ export function pickIdentity(a, b) {
95
+ if (a.length === 0 || b.length === 0)
96
+ return null;
97
+ if (!a.every(isPlainObject) || !b.every(isPlainObject))
98
+ return null;
99
+ for (const fields of USYNC_COMPOSITE_IDENTITIES) {
100
+ const strategy = tryCompositeStrategy(fields, a, b);
101
+ if (strategy)
102
+ return strategy;
103
+ }
104
+ for (const field of USYNC_IDENTITY_FIELDS) {
105
+ const strategy = trySingleFieldStrategy(field, a, b);
106
+ if (strategy)
107
+ return strategy;
108
+ }
109
+ return null;
110
+ }
111
+ function record(ctx, expansions, entry) {
112
+ ctx.totalCount++;
113
+ if (ctx.changes.length < ctx.maxChanges) {
114
+ // Any leaf recorded while inside an expanded encoded-JSON string
115
+ // carries `expanded: true`, not just the leaf that triggered the
116
+ // expansion - it's the whole subtree that crossed into a string.
117
+ ctx.changes.push(expansions > 0 ? { ...entry, expanded: true } : entry);
118
+ }
119
+ // Hard-stop: don't keep walking a wholesale change (e.g. a 5000-element
120
+ // array rewrite) just to increment a counter past a few times the cap.
121
+ if (ctx.totalCount > ctx.maxChanges * 4) {
122
+ ctx.stopped = true;
123
+ }
124
+ }
125
+ function identityChildPath(path, strategy, element, index) {
126
+ return strategy.isComposite
127
+ ? elementPath(path, element, { identityLabel: strategy.labelOf(element) })
128
+ : elementPath(path, element, { index });
129
+ }
130
+ function walk(ctx, path, a, b, depth, expansions) {
131
+ if (ctx.stopped)
132
+ return;
133
+ if (a === MISSING) {
134
+ record(ctx, expansions, { path, kind: 'added', newValue: b });
135
+ return;
136
+ }
137
+ if (b === MISSING) {
138
+ record(ctx, expansions, { path, kind: 'removed', oldValue: a });
139
+ return;
140
+ }
141
+ if (deepEqual(a, b))
142
+ return;
143
+ if (depth >= ctx.maxDepth) {
144
+ record(ctx, expansions, { path, kind: 'changed', oldValue: a, newValue: b });
145
+ return;
146
+ }
147
+ if (typeof a === 'string' && typeof b === 'string') {
148
+ if (ctx.expandJsonStrings && expansions < ctx.maxStringExpansions) {
149
+ const ea = tryParseJsonContainer(a);
150
+ const eb = tryParseJsonContainer(b);
151
+ // Only expand when BOTH sides parse - expanding one side only
152
+ // would compare a JSON structure against a raw string.
153
+ if (ea.ok && eb.ok) {
154
+ walk(ctx, `${path} » `, ea.value, eb.value, depth + 1, expansions + 1);
155
+ return;
156
+ }
157
+ }
158
+ record(ctx, expansions, { path, kind: 'changed', oldValue: a, newValue: b });
159
+ return;
160
+ }
161
+ if (typeTag(a) !== typeTag(b)) {
162
+ record(ctx, expansions, { path, kind: 'changed', oldValue: a, newValue: b });
163
+ return;
164
+ }
165
+ if (Array.isArray(a) && Array.isArray(b)) {
166
+ walkArray(ctx, path, a, b, depth, expansions);
167
+ return;
168
+ }
169
+ if (isPlainObject(a) && isPlainObject(b)) {
170
+ walkObject(ctx, path, a, b, depth, expansions);
171
+ return;
172
+ }
173
+ record(ctx, expansions, { path, kind: 'changed', oldValue: a, newValue: b });
174
+ }
175
+ function walkObject(ctx, path, a, b, depth, expansions) {
176
+ for (const key of Object.keys(a)) {
177
+ if (ctx.stopped)
178
+ return;
179
+ const childPath = joinKey(path, key);
180
+ const bHasKey = Object.prototype.hasOwnProperty.call(b, key);
181
+ walk(ctx, childPath, a[key], bHasKey ? b[key] : MISSING, depth + 1, expansions);
182
+ }
183
+ for (const key of Object.keys(b)) {
184
+ if (ctx.stopped)
185
+ return;
186
+ if (Object.prototype.hasOwnProperty.call(a, key))
187
+ continue;
188
+ walk(ctx, joinKey(path, key), MISSING, b[key], depth + 1, expansions);
189
+ }
190
+ }
191
+ function sameMultiset(a, b) {
192
+ if (a.length !== b.length)
193
+ return false;
194
+ const sorted = (arr) => [...arr].sort();
195
+ const sa = sorted(a);
196
+ const sb = sorted(b);
197
+ return sa.every((key, i) => key === sb[i]);
198
+ }
199
+ function sameOrder(a, b) {
200
+ return a.length === b.length && a.every((key, i) => key === b[i]);
201
+ }
202
+ function walkArray(ctx, path, a, b, depth, expansions) {
203
+ const strategy = pickIdentity(a, b);
204
+ if (!strategy) {
205
+ walkArrayByIndex(ctx, path, a, b, depth, expansions);
206
+ return;
207
+ }
208
+ const aEntries = a.map((element, index) => ({
209
+ element: element,
210
+ index,
211
+ key: strategy.keyOf(element),
212
+ }));
213
+ const bEntries = b.map((element, index) => ({
214
+ element: element,
215
+ index,
216
+ key: strategy.keyOf(element),
217
+ }));
218
+ const bByKey = new Map(bEntries.map((entry) => [entry.key, entry]));
219
+ const aByKey = new Map(aEntries.map((entry) => [entry.key, entry]));
220
+ for (const aEntry of aEntries) {
221
+ if (ctx.stopped)
222
+ return;
223
+ const bEntry = bByKey.get(aEntry.key);
224
+ if (!bEntry) {
225
+ record(ctx, expansions, {
226
+ path: identityChildPath(path, strategy, aEntry.element, aEntry.index),
227
+ kind: 'removed',
228
+ oldValue: aEntry.element,
229
+ });
230
+ continue;
231
+ }
232
+ walk(ctx, identityChildPath(path, strategy, bEntry.element, bEntry.index), aEntry.element, bEntry.element, depth + 1, expansions);
233
+ }
234
+ for (const bEntry of bEntries) {
235
+ if (ctx.stopped)
236
+ return;
237
+ if (!aByKey.has(bEntry.key)) {
238
+ record(ctx, expansions, {
239
+ path: identityChildPath(path, strategy, bEntry.element, bEntry.index),
240
+ kind: 'added',
241
+ newValue: bEntry.element,
242
+ });
243
+ }
244
+ }
245
+ const aKeys = aEntries.map((entry) => entry.key);
246
+ const bKeys = bEntries.map((entry) => entry.key);
247
+ if (sameMultiset(aKeys, bKeys) && !sameOrder(aKeys, bKeys)) {
248
+ ctx.reordered.push(path.length > 0 ? path : '(root)');
249
+ }
250
+ }
251
+ function walkArrayByIndex(ctx, path, a, b, depth, expansions) {
252
+ const minLen = Math.min(a.length, b.length);
253
+ let start = 0;
254
+ while (start < minLen && deepEqual(a[start], b[start]))
255
+ start++;
256
+ let end = 0;
257
+ while (end < minLen - start &&
258
+ deepEqual(a[a.length - 1 - end], b[b.length - 1 - end])) {
259
+ end++;
260
+ }
261
+ const aMid = a.slice(start, a.length - end);
262
+ const bMid = b.slice(start, b.length - end);
263
+ const midLen = Math.max(aMid.length, bMid.length);
264
+ for (let i = 0; i < midLen; i++) {
265
+ if (ctx.stopped)
266
+ return;
267
+ const aEl = i < aMid.length ? aMid[i] : MISSING;
268
+ const bEl = i < bMid.length ? bMid[i] : MISSING;
269
+ const index = start + i;
270
+ const sample = aEl !== MISSING ? aEl : bEl;
271
+ const childPath = elementPath(path, sample, { index });
272
+ walk(ctx, childPath, aEl, bEl, depth + 1, expansions);
273
+ }
274
+ }
275
+ /**
276
+ * Structurally diffs two parsed JSON values, producing a flat list of leaf
277
+ * changes with readable paths instead of a wholesale before/after dump.
278
+ * Arrays are matched by identity where possible (see `pickIdentity`) so that
279
+ * inserting one block does not make every other block look changed.
280
+ */
281
+ export function diffJsonValues(oldValue, newValue, options) {
282
+ const ctx = {
283
+ changes: [],
284
+ totalCount: 0,
285
+ reordered: [],
286
+ maxChanges: options?.maxChanges ?? USYNC_DIFF_LIMITS.maxLeafChanges,
287
+ maxDepth: options?.maxDepth ?? USYNC_DIFF_LIMITS.maxJsonDepth,
288
+ expandJsonStrings: options?.expandJsonStrings ?? true,
289
+ maxStringExpansions: options?.maxStringExpansions ?? USYNC_DIFF_LIMITS.maxStringExpansions,
290
+ stopped: false,
291
+ };
292
+ walk(ctx, '', oldValue, newValue, 0, 0);
293
+ return {
294
+ changes: ctx.changes,
295
+ totalCount: ctx.totalCount,
296
+ truncated: ctx.totalCount > ctx.changes.length,
297
+ stopped: ctx.stopped,
298
+ reordered: ctx.reordered,
299
+ };
300
+ }
@@ -0,0 +1,16 @@
1
+ import { type USyncLineDiffBlock, type USyncLineDiffResult, type USyncLineDiffRow } from './types.js';
2
+ /**
3
+ * Diffs two texts line by line. Normalises `\r\n` to `\n` before splitting
4
+ * (uSync files get written on one OS and imported on another - without this
5
+ * a line-ending-only change would diff the whole file), trims the identical
6
+ * common prefix/suffix so large files with one changed line stay cheap, and
7
+ * degrades to a wholesale remove/add - bounded, honest output - rather than
8
+ * letting the O(ND) middle blow up.
9
+ */
10
+ export declare function diffTextLines(oldText: string, newText: string): USyncLineDiffResult;
11
+ /**
12
+ * Collapses long runs of unchanged context rows down to a small lead-in/
13
+ * lead-out plus a single collapsed block, keeping the hidden rows so a
14
+ * consumer can splice them back in on click without re-diffing.
15
+ */
16
+ export declare function collapseUnchanged(rows: USyncLineDiffRow[], context?: number): USyncLineDiffBlock[];
@@ -0,0 +1,199 @@
1
+ import { USYNC_DIFF_LIMITS, } from './types.js';
2
+ function normalize(text) {
3
+ return text.replace(/\r\n/g, '\n');
4
+ }
5
+ function contextRow(oldLine, newLine, text) {
6
+ return { type: 'context', oldLine, newLine, text };
7
+ }
8
+ function removeRow(oldLine, text) {
9
+ return { type: 'remove', oldLine, text };
10
+ }
11
+ function addRow(newLine, text) {
12
+ return { type: 'add', newLine, text };
13
+ }
14
+ /**
15
+ * Emits the whole [oldStart, oldEnd) / [newStart, newEnd) middle range as a
16
+ * wholesale remove-then-add - used both when the pathological-size guard
17
+ * trips and as the degrade path if Myers' `d` bound is hit.
18
+ */
19
+ function wholesaleRows(oldLines, newLines, oldStart, oldEnd, newStart, newEnd) {
20
+ const rows = [];
21
+ for (let i = oldStart; i < oldEnd; i++)
22
+ rows.push(removeRow(i + 1, oldLines[i]));
23
+ for (let i = newStart; i < newEnd; i++)
24
+ rows.push(addRow(i + 1, newLines[i]));
25
+ return rows;
26
+ }
27
+ const MAX_D = 2000;
28
+ /**
29
+ * Myers greedy forward diff (the classic O(ND) algorithm) over the given
30
+ * line ranges. Returns `null` if the edit distance exceeds `MAX_D`, in which
31
+ * case the caller should degrade to a wholesale remove/add instead of
32
+ * continuing to search.
33
+ */
34
+ function myersDiff(oldLines, newLines, oldStart, oldEnd, newStart, newEnd) {
35
+ const n = oldEnd - oldStart;
36
+ const m = newEnd - newStart;
37
+ const max = Math.min(n + m, MAX_D);
38
+ if (n === 0 && m === 0)
39
+ return [];
40
+ const offset = max;
41
+ const v = new Int32Array(2 * max + 1);
42
+ const trace = [];
43
+ let found = false;
44
+ let foundD = -1;
45
+ for (let d = 0; d <= max; d++) {
46
+ const snapshot = v.slice();
47
+ trace.push(snapshot);
48
+ for (let k = -d; k <= d; k += 2) {
49
+ let x;
50
+ if (k === -d || (k !== d && v[k - 1 + offset] < v[k + 1 + offset])) {
51
+ x = v[k + 1 + offset];
52
+ }
53
+ else {
54
+ x = v[k - 1 + offset] + 1;
55
+ }
56
+ let y = x - k;
57
+ while (x < n && y < m && oldLines[oldStart + x] === newLines[newStart + y]) {
58
+ x++;
59
+ y++;
60
+ }
61
+ v[k + offset] = x;
62
+ if (x >= n && y >= m) {
63
+ found = true;
64
+ foundD = d;
65
+ break;
66
+ }
67
+ }
68
+ if (found)
69
+ break;
70
+ }
71
+ if (!found)
72
+ return null;
73
+ // Backtrack through the trace to recover the edit script.
74
+ const rows = [];
75
+ let x = n;
76
+ let y = m;
77
+ for (let d = foundD; d > 0; d--) {
78
+ const vPrev = trace[d];
79
+ const k = x - y;
80
+ let prevK;
81
+ if (k === -d || (k !== d && vPrev[k - 1 + offset] < vPrev[k + 1 + offset])) {
82
+ prevK = k + 1;
83
+ }
84
+ else {
85
+ prevK = k - 1;
86
+ }
87
+ const prevX = vPrev[prevK + offset];
88
+ const prevY = prevX - prevK;
89
+ while (x > prevX && y > prevY) {
90
+ rows.push(contextRow(oldStart + x, newStart + y, oldLines[oldStart + x - 1]));
91
+ x--;
92
+ y--;
93
+ }
94
+ if (x === prevX) {
95
+ rows.push(addRow(newStart + y, newLines[newStart + y - 1]));
96
+ }
97
+ else {
98
+ rows.push(removeRow(oldStart + x, oldLines[oldStart + x - 1]));
99
+ }
100
+ x = prevX;
101
+ y = prevY;
102
+ }
103
+ while (x > 0 && y > 0) {
104
+ rows.push(contextRow(oldStart + x, newStart + y, oldLines[oldStart + x - 1]));
105
+ x--;
106
+ y--;
107
+ }
108
+ rows.reverse();
109
+ return rows;
110
+ }
111
+ /**
112
+ * Diffs two texts line by line. Normalises `\r\n` to `\n` before splitting
113
+ * (uSync files get written on one OS and imported on another - without this
114
+ * a line-ending-only change would diff the whole file), trims the identical
115
+ * common prefix/suffix so large files with one changed line stay cheap, and
116
+ * degrades to a wholesale remove/add - bounded, honest output - rather than
117
+ * letting the O(ND) middle blow up.
118
+ */
119
+ export function diffTextLines(oldText, newText) {
120
+ const oldLines = normalize(oldText).split('\n');
121
+ const newLines = normalize(newText).split('\n');
122
+ const minLen = Math.min(oldLines.length, newLines.length);
123
+ let prefix = 0;
124
+ while (prefix < minLen && oldLines[prefix] === newLines[prefix])
125
+ prefix++;
126
+ let suffix = 0;
127
+ while (suffix < minLen - prefix &&
128
+ oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]) {
129
+ suffix++;
130
+ }
131
+ const oldMidStart = prefix;
132
+ const oldMidEnd = oldLines.length - suffix;
133
+ const newMidStart = prefix;
134
+ const newMidEnd = newLines.length - suffix;
135
+ const oldMidLen = oldMidEnd - oldMidStart;
136
+ const newMidLen = newMidEnd - newMidStart;
137
+ let degraded = false;
138
+ let middleRows;
139
+ if (oldMidLen * newMidLen > USYNC_DIFF_LIMITS.maxLineDiffArea) {
140
+ degraded = true;
141
+ middleRows = wholesaleRows(oldLines, newLines, oldMidStart, oldMidEnd, newMidStart, newMidEnd);
142
+ }
143
+ else {
144
+ const myers = myersDiff(oldLines, newLines, oldMidStart, oldMidEnd, newMidStart, newMidEnd);
145
+ if (myers === null) {
146
+ degraded = true;
147
+ middleRows = wholesaleRows(oldLines, newLines, oldMidStart, oldMidEnd, newMidStart, newMidEnd);
148
+ }
149
+ else {
150
+ middleRows = myers;
151
+ }
152
+ }
153
+ const rows = [];
154
+ for (let i = 0; i < prefix; i++)
155
+ rows.push(contextRow(i + 1, i + 1, oldLines[i]));
156
+ rows.push(...middleRows);
157
+ for (let i = 0; i < suffix; i++) {
158
+ const oldLine = oldLines.length - suffix + i;
159
+ const newLine = newLines.length - suffix + i;
160
+ rows.push(contextRow(oldLine + 1, newLine + 1, oldLines[oldLine]));
161
+ }
162
+ return { oldLines, newLines, degraded, rows };
163
+ }
164
+ /**
165
+ * Collapses long runs of unchanged context rows down to a small lead-in/
166
+ * lead-out plus a single collapsed block, keeping the hidden rows so a
167
+ * consumer can splice them back in on click without re-diffing.
168
+ */
169
+ export function collapseUnchanged(rows, context = USYNC_DIFF_LIMITS.lineContext) {
170
+ const blocks = [];
171
+ let i = 0;
172
+ while (i < rows.length) {
173
+ if (rows[i].type !== 'context') {
174
+ blocks.push({ type: 'row', row: rows[i] });
175
+ i++;
176
+ continue;
177
+ }
178
+ let end = i;
179
+ while (end < rows.length && rows[end].type === 'context')
180
+ end++;
181
+ const run = rows.slice(i, end);
182
+ if (run.length <= 2 * context + 1) {
183
+ for (const row of run)
184
+ blocks.push({ type: 'row', row });
185
+ }
186
+ else {
187
+ const lead = run.slice(0, context);
188
+ const hidden = run.slice(context, run.length - context);
189
+ const trail = run.slice(run.length - context);
190
+ for (const row of lead)
191
+ blocks.push({ type: 'row', row });
192
+ blocks.push({ type: 'collapsed', count: hidden.length, rows: hidden });
193
+ for (const row of trail)
194
+ blocks.push({ type: 'row', row });
195
+ }
196
+ i = end;
197
+ }
198
+ return blocks;
199
+ }
@@ -0,0 +1,62 @@
1
+ export type USyncLeafChangeKind = 'added' | 'removed' | 'changed';
2
+ export interface USyncLeafChange {
3
+ path: string;
4
+ kind: USyncLeafChangeKind;
5
+ oldValue?: unknown;
6
+ newValue?: unknown;
7
+ expanded?: boolean;
8
+ }
9
+ export interface USyncJsonDiffResult {
10
+ changes: USyncLeafChange[];
11
+ totalCount: number;
12
+ truncated: boolean;
13
+ /**
14
+ * True when the walk aborted early on a runaway subtree, which makes
15
+ * `totalCount` a floor rather than an exact count - report it as
16
+ * "N or more changes".
17
+ */
18
+ stopped: boolean;
19
+ reordered: string[];
20
+ }
21
+ export type USyncDiffStrategy = 'skip' | 'notice' | 'masked' | 'single' | 'json' | 'lines' | 'words' | 'scalar' | 'oversized';
22
+ export interface USyncClassifiedChange {
23
+ strategy: USyncDiffStrategy;
24
+ oldText: string;
25
+ newText: string;
26
+ oldJson?: unknown;
27
+ newJson?: unknown;
28
+ language: 'json' | 'xml' | 'text';
29
+ bytes: number;
30
+ }
31
+ export type USyncLineRowType = 'context' | 'add' | 'remove';
32
+ export interface USyncLineDiffRow {
33
+ type: USyncLineRowType;
34
+ oldLine?: number;
35
+ newLine?: number;
36
+ text: string;
37
+ }
38
+ export interface USyncLineDiffResult {
39
+ oldLines: string[];
40
+ newLines: string[];
41
+ degraded: boolean;
42
+ rows: USyncLineDiffRow[];
43
+ }
44
+ export type USyncLineDiffBlock = {
45
+ type: 'row';
46
+ row: USyncLineDiffRow;
47
+ } | {
48
+ type: 'collapsed';
49
+ count: number;
50
+ rows: USyncLineDiffRow[];
51
+ };
52
+ export declare const USYNC_DIFF_LIMITS: {
53
+ readonly maxDiffBytes: 512000;
54
+ readonly maxWordDiffBytes: 8000;
55
+ readonly maxScalarWordDiffBytes: 2000;
56
+ readonly inlineScalarChars: 80;
57
+ readonly maxLeafChanges: 40;
58
+ readonly maxJsonDepth: 12;
59
+ readonly maxStringExpansions: 3;
60
+ readonly maxLineDiffArea: 4000000;
61
+ readonly lineContext: 3;
62
+ };
@@ -0,0 +1,12 @@
1
+ // All thresholds in one exported const so they are tunable in one line.
2
+ export const USYNC_DIFF_LIMITS = {
3
+ maxDiffBytes: 512000, // above: raw only, no walk/diff at all
4
+ maxWordDiffBytes: 8000, // diffWords is ~quadratic; also ~where a word diff stops being readable
5
+ maxScalarWordDiffBytes: 2000,
6
+ inlineScalarChars: 80, // at or below: render inline as "old → new"
7
+ maxLeafChanges: 40,
8
+ maxJsonDepth: 12,
9
+ maxStringExpansions: 3,
10
+ maxLineDiffArea: 4000000, // |old| * |new| lines before degrading
11
+ lineContext: 3,
12
+ };
@@ -0,0 +1,31 @@
1
+ import { SyncActionButton, SyncActionGroup, UploadImportResult } from '../../api';
2
+ export declare const USYNC_FILE_PICKER_CHANGE_EVENT = "change";
3
+ export declare const USYNC_FILE_PICKER_UPLOADED_EVENT = "uploaded";
4
+ /**
5
+ * 'change' event fires when the file input changes, either by selecting a new file or removing the current one.
6
+ */
7
+ export declare class uSyncFilePickerChangeEvent extends Event {
8
+ constructor(file: File | null | undefined);
9
+ file: File | null | undefined;
10
+ }
11
+ /**
12
+ * 'uploaded' event fires when a file has been successfully uploaded and extracted.
13
+ */
14
+ export declare class uSyncFilePickerUploadedEvent extends Event {
15
+ constructor(result: UploadImportResult);
16
+ result: UploadImportResult;
17
+ }
18
+ export declare const USYNC_ACTION_BUTTON_CLICK_EVENT = "usync-action-click";
19
+ export declare class uSyncActionButtonClickEvent extends Event {
20
+ constructor(button: SyncActionButton);
21
+ button: SyncActionButton;
22
+ }
23
+ export declare const USYNC_ACTION_PERFORM_EVENT = "perform-action";
24
+ export declare class uSyncActionPerformEvent extends Event {
25
+ constructor(group: SyncActionGroup, key: string, force?: boolean, clean?: boolean, file?: boolean);
26
+ group: SyncActionGroup;
27
+ key: string;
28
+ force?: boolean;
29
+ clean?: boolean;
30
+ file?: boolean;
31
+ }
@@ -0,0 +1,54 @@
1
+ export const USYNC_FILE_PICKER_CHANGE_EVENT = 'change';
2
+ export const USYNC_FILE_PICKER_UPLOADED_EVENT = 'uploaded';
3
+ /**
4
+ * 'change' event fires when the file input changes, either by selecting a new file or removing the current one.
5
+ */
6
+ export class uSyncFilePickerChangeEvent extends Event {
7
+ constructor(file) {
8
+ super(USYNC_FILE_PICKER_CHANGE_EVENT, {
9
+ bubbles: true,
10
+ composed: true,
11
+ cancelable: false,
12
+ });
13
+ this.file = file;
14
+ }
15
+ }
16
+ /**
17
+ * 'uploaded' event fires when a file has been successfully uploaded and extracted.
18
+ */
19
+ export class uSyncFilePickerUploadedEvent extends Event {
20
+ constructor(result) {
21
+ super(USYNC_FILE_PICKER_UPLOADED_EVENT, {
22
+ bubbles: true,
23
+ composed: true,
24
+ cancelable: false,
25
+ });
26
+ this.result = result;
27
+ }
28
+ }
29
+ export const USYNC_ACTION_BUTTON_CLICK_EVENT = 'usync-action-click';
30
+ export class uSyncActionButtonClickEvent extends Event {
31
+ constructor(button) {
32
+ super(USYNC_ACTION_BUTTON_CLICK_EVENT, {
33
+ bubbles: true,
34
+ composed: true,
35
+ cancelable: false,
36
+ });
37
+ this.button = button;
38
+ }
39
+ }
40
+ export const USYNC_ACTION_PERFORM_EVENT = 'perform-action';
41
+ export class uSyncActionPerformEvent extends Event {
42
+ constructor(group, key, force, clean, file) {
43
+ super(USYNC_ACTION_PERFORM_EVENT, {
44
+ bubbles: true,
45
+ composed: true,
46
+ cancelable: false,
47
+ });
48
+ this.group = group;
49
+ this.key = key;
50
+ this.force = force;
51
+ this.clean = clean;
52
+ this.file = file;
53
+ }
54
+ }
@@ -0,0 +1,3 @@
1
+ export * from './usync-action-button.js';
2
+ export * from './usync-file-picker.element.js';
3
+ export * from './usync-file-upload.element.js';
@@ -0,0 +1,3 @@
1
+ export * from './usync-action-button.js';
2
+ export * from './usync-file-picker.element.js';
3
+ export * from './usync-file-upload.element.js';