mapples 0.2.0-beta.3 → 0.2.0-beta.4
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/dist/api/objects.d.ts +3 -1
- package/dist/api/objects.js +24 -1
- package/dist/api/objects.js.map +1 -1
- package/dist/codegen/emit.d.ts +2 -31
- package/dist/codegen/emit.js +7 -147
- package/dist/codegen/emit.js.map +1 -1
- package/dist/codegen/imports.d.ts +1 -17
- package/dist/codegen/imports.js +2 -116
- package/dist/codegen/imports.js.map +1 -1
- package/dist/codegen/instances.d.ts +2 -13
- package/dist/codegen/instances.js +23 -134
- package/dist/codegen/instances.js.map +1 -1
- package/dist/codegen/styles.d.ts +1 -11
- package/dist/codegen/styles.js +2 -130
- package/dist/codegen/styles.js.map +1 -1
- package/dist/merge/threeWay.d.ts +1 -1
- package/dist/merge/toEdits.d.ts +1 -3
- package/dist/merge/toEdits.js +3 -94
- package/dist/merge/toEdits.js.map +1 -1
- package/dist/project/baseStore.d.ts +11 -1
- package/dist/project/baseStore.js +13 -0
- package/dist/project/baseStore.js.map +1 -1
- package/dist/services/syncEngine.d.ts +1 -1
- package/dist/services/syncEngine.js +260 -39
- package/dist/services/syncEngine.js.map +1 -1
- package/dist/types.d.ts +19 -1
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -1
- package/package.json +3 -2
|
@@ -16,7 +16,7 @@ import { HeadMismatchError, JpatchError } from '../api/errors.js';
|
|
|
16
16
|
import { pullDelta, SnapshotRequired } from '../api/poll.js';
|
|
17
17
|
import { fetchProjectState, } from '../api/objects.js';
|
|
18
18
|
import { PRIMITIVE_MAPPING } from '../adopt/mappingTable.js';
|
|
19
|
-
import { emitComponent, emitPage } from '../codegen/emit.js';
|
|
19
|
+
import { emitComponent, emitPage, withoutRouteMarker } from '../codegen/emit.js';
|
|
20
20
|
import { importNeedsFor } from '../codegen/imports.js';
|
|
21
21
|
import { instanceLinker } from '../codegen/instances.js';
|
|
22
22
|
import { deprecateFile } from '../merge/deprecate.js';
|
|
@@ -35,8 +35,11 @@ import { mintEntityFile } from '../nav/placement.js';
|
|
|
35
35
|
import { syncNavigation } from '../nav/routeTree.js';
|
|
36
36
|
import { syncStyleFile } from '../style/theme.js';
|
|
37
37
|
import { emptyReport } from '../ui/report.js';
|
|
38
|
+
import { sourceIdFor, } from '../types.js';
|
|
38
39
|
const MAX_REBASE_RETRIES = 5;
|
|
39
40
|
const MAX_OPS_PER_TX = 200;
|
|
41
|
+
/** Source ops carry whole files (≤256 KiB each): fewer per transaction than node ops. */
|
|
42
|
+
const SOURCE_OPS_PER_TX = 10;
|
|
40
43
|
// buildRemoteModel needs the project root to read the stored head; carried as
|
|
41
44
|
// a non-enumerable tag so BaseSnapshot's shape stays unchanged for callers.
|
|
42
45
|
const ROOT_TAG = Symbol('projectRoot');
|
|
@@ -112,8 +115,11 @@ export async function runSyncEngine({ root, client }) {
|
|
|
112
115
|
let txId = mintTxId();
|
|
113
116
|
let attempt = 0;
|
|
114
117
|
for (;;) {
|
|
115
|
-
const { pushOps, mergedBase, pagesMap, componentsMap } = await mergeAndWrite(root, client, config, base, remote, report);
|
|
116
|
-
|
|
118
|
+
const { pushOps, mergedBase, pagesMap, componentsMap, sourceUploads } = await mergeAndWrite(root, client, config, base, remote, report);
|
|
119
|
+
// Source text goes out AFTER the node ops (its `$sid`s must be server
|
|
120
|
+
// ids, which the node push mints), as its own transaction(s).
|
|
121
|
+
let sourceOps = [];
|
|
122
|
+
if (pushOps.length === 0 && sourceUploads.size === 0) {
|
|
117
123
|
finalize(root, baseStore, mergedBase, pagesMap, componentsMap, remote.head, remote.seq);
|
|
118
124
|
report.head = remote.head;
|
|
119
125
|
return report;
|
|
@@ -124,15 +130,41 @@ export async function runSyncEngine({ root, client }) {
|
|
|
124
130
|
}
|
|
125
131
|
const progress = newProgress(remote.head, remote.seq);
|
|
126
132
|
try {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
if (pushOps.length > 0) {
|
|
134
|
+
await pushChunked(client, pushOps, txId, progress, {
|
|
135
|
+
rebaseInPlace: false,
|
|
136
|
+
report,
|
|
137
|
+
// Rewrite as each chunk lands, so files and base never hold a cid the
|
|
138
|
+
// server has already resolved: a re-merge or a park after a later
|
|
139
|
+
// chunk fails then sees the committed nodes by their real ids.
|
|
140
|
+
onChunk: (idMap) => rewriteIds(root, mergedBase, [pagesMap, componentsMap], idMap, report),
|
|
141
|
+
});
|
|
142
|
+
report.pushedOps += pushOps.length;
|
|
143
|
+
}
|
|
144
|
+
sourceOps = sourceOpsFor(mergedBase, remote, sourceUploads, progress.idMap);
|
|
145
|
+
try {
|
|
146
|
+
for (let i = 0; i < sourceOps.length; i += SOURCE_OPS_PER_TX) {
|
|
147
|
+
await pushChunked(client, sourceOps.slice(i, i + SOURCE_OPS_PER_TX), pushOps.length === 0 && i === 0 ? txId : mintTxId(), progress, { rebaseInPlace: false, report });
|
|
148
|
+
}
|
|
149
|
+
if (sourceOps.length > 0) {
|
|
150
|
+
report.pushedOps += sourceOps.length;
|
|
151
|
+
report.notes.push(`uploaded the source text of ${sourceOps.length} file(s) to Creator`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
if (!(err instanceof JpatchError && err.code === 'UNKNOWN_ENTITY'))
|
|
156
|
+
throw err;
|
|
157
|
+
// A Creator server from before A12 rejects the entity. The node ops
|
|
158
|
+
// already landed; the code stays local and is offered again once the
|
|
159
|
+
// server stores source text — never silently.
|
|
160
|
+
for (const id of sourceUploads) {
|
|
161
|
+
const entry = mergedBase.sources.get(progress.idMap[id] ?? id);
|
|
162
|
+
if (entry)
|
|
163
|
+
entry.onServer = false;
|
|
164
|
+
}
|
|
165
|
+
sourceOps = [];
|
|
166
|
+
report.notes.push('this Creator server does not store source text yet — the code around your elements stays local until it does');
|
|
167
|
+
}
|
|
136
168
|
finalize(root, baseStore, mergedBase, pagesMap, componentsMap, progress.head, progress.seq);
|
|
137
169
|
report.head = progress.head;
|
|
138
170
|
return report;
|
|
@@ -157,12 +189,15 @@ export async function runSyncEngine({ root, client }) {
|
|
|
157
189
|
// Files already show the merged view; park what did not land (with
|
|
158
190
|
// the committed chunks' cids resolved) and adopt the remote head so
|
|
159
191
|
// the drain pushes exactly these ops next time.
|
|
192
|
+
// Source ops not yet computed (offline during the node push) are
|
|
193
|
+
// simply recomputed next time: the files still differ from the base.
|
|
194
|
+
const allOps = [...pushOps, ...sourceOps];
|
|
160
195
|
outbox.park({
|
|
161
196
|
txId: progress.committedOps > 0 ? mintTxId() : txId,
|
|
162
|
-
ops: resolveCids(
|
|
197
|
+
ops: resolveCids(allOps.slice(progress.committedOps), progress.idMap),
|
|
163
198
|
parkedAt: new Date().toISOString(),
|
|
164
199
|
});
|
|
165
|
-
report.pushedOps =
|
|
200
|
+
report.pushedOps = allOps.length;
|
|
166
201
|
report.offline = true;
|
|
167
202
|
finalize(root, baseStore, remoteAsBase(remote), readPagesMap(root), readComponentsMap(root), remote.head, remote.seq);
|
|
168
203
|
return report;
|
|
@@ -182,26 +217,33 @@ async function bootstrap(root, client, config, report) {
|
|
|
182
217
|
const componentsMap = readComponentsMap(root);
|
|
183
218
|
placeComponents(config.componentsDir ?? DEFAULT_COMPONENTS_DIR, state.components, componentsMap);
|
|
184
219
|
const linker = instanceLinker(componentsMap, state.components);
|
|
220
|
+
// A container with stored source text (A12) materializes from that text —
|
|
221
|
+
// hooks and handlers written in the Creator's editor included — patched to
|
|
222
|
+
// its current tree; the rest is generated.
|
|
185
223
|
const targets = [];
|
|
186
224
|
for (const page of state.pages.values()) {
|
|
187
225
|
const mapping = pagesMap[page.id];
|
|
188
226
|
if (!mapping)
|
|
189
227
|
throw new Error(`internal: page ${page.id} was not placed`);
|
|
228
|
+
const tree = linker.unlink(page.root, mapping.file, warnInto(report, mapping.file));
|
|
190
229
|
targets.push({
|
|
230
|
+
id: page.id,
|
|
191
231
|
file: mapping.file,
|
|
192
|
-
emit: () =>
|
|
232
|
+
emit: async () => renderFromSource(state.sources.get(page.id) ?? null, tree, mapping.file, report) ??
|
|
233
|
+
emitPage(emitInput(page.name, tree, mapping), join(root, mapping.file)),
|
|
193
234
|
});
|
|
194
235
|
}
|
|
195
236
|
for (const component of state.components.values()) {
|
|
196
237
|
const mapping = componentsMap[component.id];
|
|
238
|
+
const tree = linker.unlink(component.root, mapping.file, warnInto(report, mapping.file));
|
|
197
239
|
targets.push({
|
|
240
|
+
id: component.id,
|
|
198
241
|
file: mapping.file,
|
|
199
|
-
emit: () =>
|
|
200
|
-
name: component.name,
|
|
201
|
-
root: linker.unlink(component.root, mapping.file, warnInto(report, mapping.file)),
|
|
202
|
-
}, join(root, mapping.file)),
|
|
242
|
+
emit: async () => renderFromSource(state.sources.get(component.id) ?? null, tree, mapping.file, report) ??
|
|
243
|
+
emitComponent({ name: component.name, root: tree }, join(root, mapping.file)),
|
|
203
244
|
});
|
|
204
245
|
}
|
|
246
|
+
const baseSources = new Map();
|
|
205
247
|
for (const target of targets) {
|
|
206
248
|
const filePath = join(root, target.file);
|
|
207
249
|
if (existsSync(filePath)) {
|
|
@@ -211,11 +253,23 @@ async function bootstrap(root, client, config, report) {
|
|
|
211
253
|
action: 'unchanged',
|
|
212
254
|
detail: 'exists — left untouched',
|
|
213
255
|
});
|
|
214
|
-
continue;
|
|
215
256
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
257
|
+
else {
|
|
258
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
259
|
+
writeFileSync(filePath, await target.emit());
|
|
260
|
+
report.files.push({ file: target.file, action: 'created' });
|
|
261
|
+
}
|
|
262
|
+
// The text 3-way's base: the server's text where it has one, else the
|
|
263
|
+
// file as it stands (an untouched pre-existing file included — its own
|
|
264
|
+
// code then reads as a local change and uploads on the next sync).
|
|
265
|
+
const remoteSource = state.sources.get(target.id) ?? null;
|
|
266
|
+
baseSources.set(target.id, {
|
|
267
|
+
id: remoteSource?.id ?? sourceIdFor(target.id),
|
|
268
|
+
of: target.id,
|
|
269
|
+
lang: remoteSource?.lang ?? 'tsx',
|
|
270
|
+
text: remoteSource?.text ?? readFileSync(filePath, 'utf8'),
|
|
271
|
+
onServer: remoteSource !== null,
|
|
272
|
+
});
|
|
219
273
|
}
|
|
220
274
|
const styleResult = syncStyleFile(root, state.style, state.style, report);
|
|
221
275
|
const assetsResult = await syncAssets(root, client, state.assets, report);
|
|
@@ -228,6 +282,7 @@ async function bootstrap(root, client, config, report) {
|
|
|
228
282
|
navigation: navResult.mergedNavigation,
|
|
229
283
|
style: styleResult.mergedStyle,
|
|
230
284
|
assets: state.assets,
|
|
285
|
+
sources: baseSources,
|
|
231
286
|
});
|
|
232
287
|
baseStore.writeStamp(state.head);
|
|
233
288
|
writePagesMap(root, pagesMap);
|
|
@@ -287,6 +342,9 @@ async function buildRemoteModel(client, base, sinceSeq, report) {
|
|
|
287
342
|
navigation: structuredClone(base.navigation),
|
|
288
343
|
style: structuredClone(base.style),
|
|
289
344
|
assets: structuredClone(base.assets),
|
|
345
|
+
sources: new Map([...base.sources]
|
|
346
|
+
.filter(([, source]) => source.onServer)
|
|
347
|
+
.map(([id, { onServer: _onServer, ...source }]) => [id, source])),
|
|
290
348
|
head: storedHead.head,
|
|
291
349
|
seq: storedHead.seq,
|
|
292
350
|
};
|
|
@@ -299,6 +357,7 @@ async function buildRemoteModel(client, base, sinceSeq, report) {
|
|
|
299
357
|
navigation: state.navigation,
|
|
300
358
|
style: state.style,
|
|
301
359
|
assets: state.assets,
|
|
360
|
+
sources: state.sources,
|
|
302
361
|
head: state.head,
|
|
303
362
|
seq: state.seq,
|
|
304
363
|
};
|
|
@@ -309,12 +368,14 @@ async function mergeAndWrite(root, client, config, base, remote, report) {
|
|
|
309
368
|
const pagesMap = readPagesMap(root);
|
|
310
369
|
const componentsMap = readComponentsMap(root);
|
|
311
370
|
const pushOps = [];
|
|
371
|
+
const sourceUploads = new Set();
|
|
312
372
|
const mergedBase = {
|
|
313
373
|
pages: new Map(),
|
|
314
374
|
components: new Map(),
|
|
315
375
|
navigation: remote.navigation,
|
|
316
376
|
style: remote.style,
|
|
317
377
|
assets: remote.assets,
|
|
378
|
+
sources: new Map(),
|
|
318
379
|
};
|
|
319
380
|
// Reset per-attempt report fields (the rebase loop re-runs this).
|
|
320
381
|
report.files = [];
|
|
@@ -336,6 +397,10 @@ async function mergeAndWrite(root, client, config, base, remote, report) {
|
|
|
336
397
|
linker,
|
|
337
398
|
emit: (entity, mapping, tree, filePath) => emitPage(emitInput(entity.name, tree, mapping), filePath),
|
|
338
399
|
mergedOut: mergedBase.pages,
|
|
400
|
+
baseSources: base.sources,
|
|
401
|
+
remoteSources: remote.sources,
|
|
402
|
+
sourcesOut: mergedBase.sources,
|
|
403
|
+
sourceUploads,
|
|
339
404
|
pushOps,
|
|
340
405
|
report,
|
|
341
406
|
});
|
|
@@ -348,6 +413,10 @@ async function mergeAndWrite(root, client, config, base, remote, report) {
|
|
|
348
413
|
linker,
|
|
349
414
|
emit: (entity, _mapping, tree, filePath) => emitComponent({ name: entity.name, root: tree }, filePath),
|
|
350
415
|
mergedOut: mergedBase.components,
|
|
416
|
+
baseSources: base.sources,
|
|
417
|
+
remoteSources: remote.sources,
|
|
418
|
+
sourcesOut: mergedBase.sources,
|
|
419
|
+
sourceUploads,
|
|
351
420
|
pushOps,
|
|
352
421
|
report,
|
|
353
422
|
});
|
|
@@ -358,7 +427,7 @@ async function mergeAndWrite(root, client, config, base, remote, report) {
|
|
|
358
427
|
const assetsResult = await syncAssets(root, client, remote.assets, report);
|
|
359
428
|
pushOps.push(...assetsResult.pushOps);
|
|
360
429
|
mergedBase.assets = assetsResult.mergedAssets;
|
|
361
|
-
return { pushOps, mergedBase, pagesMap, componentsMap };
|
|
430
|
+
return { pushOps, mergedBase, pagesMap, componentsMap, sourceUploads };
|
|
362
431
|
}
|
|
363
432
|
/**
|
|
364
433
|
* The 3-way file merge for one entity kind: extract the file, merge against
|
|
@@ -366,7 +435,28 @@ async function mergeAndWrite(root, client, config, base, remote, report) {
|
|
|
366
435
|
* deprecate on conflict, and collect the ops that push local changes.
|
|
367
436
|
*/
|
|
368
437
|
async function mergeFiles(args) {
|
|
369
|
-
const { root, kind, base, remote, mapping: mappings, linker, emit, mergedOut, pushOps, report, } = args;
|
|
438
|
+
const { root, kind, base, remote, mapping: mappings, linker, emit, mergedOut, baseSources, remoteSources, sourcesOut, sourceUploads, pushOps, report, } = args;
|
|
439
|
+
/**
|
|
440
|
+
* The file's text after this sync becomes the next base. It goes to the
|
|
441
|
+
* server when a stored text exists and differs from the file (the file
|
|
442
|
+
* always wins: the merge above already folded the Creator's side in), or
|
|
443
|
+
* when the developer changed the code of a file the server has no text for.
|
|
444
|
+
*/
|
|
445
|
+
const recordSource = (id, remoteSource, text, codeChanged) => {
|
|
446
|
+
const upload = remoteSource !== null ? remoteSource.text !== text : codeChanged;
|
|
447
|
+
if (upload)
|
|
448
|
+
sourceUploads.add(id);
|
|
449
|
+
sourcesOut.set(id, {
|
|
450
|
+
id: remoteSource?.id ?? sourceIdFor(id),
|
|
451
|
+
of: id,
|
|
452
|
+
lang: remoteSource?.lang ?? 'tsx',
|
|
453
|
+
text,
|
|
454
|
+
onServer: remoteSource !== null || upload,
|
|
455
|
+
});
|
|
456
|
+
};
|
|
457
|
+
/** A container's file text: its stored source patched to `tree`, else generated. */
|
|
458
|
+
const fileTextFor = async (entity, mapping, remoteSource, tree, filePath) => renderFromSource(remoteSource, tree, mapping.file, report) ??
|
|
459
|
+
(await emit(entity, mapping, tree, filePath));
|
|
370
460
|
const ids = new Set([...base.keys(), ...remote.keys()]);
|
|
371
461
|
for (const id of ids) {
|
|
372
462
|
const baseEntity = base.get(id) ?? null;
|
|
@@ -380,12 +470,20 @@ async function mergeFiles(args) {
|
|
|
380
470
|
}
|
|
381
471
|
const filePath = join(root, mapping.file);
|
|
382
472
|
const fileExists = existsSync(filePath);
|
|
473
|
+
const baseSource = baseSources.get(id) ?? null;
|
|
474
|
+
const remoteSource = remoteSources.get(id) ?? null;
|
|
383
475
|
// local = fresh extract, with NON_LITERAL props refilled from base.
|
|
384
476
|
let local;
|
|
385
477
|
// The same tree as the file reads it (linked instances still Customs):
|
|
386
478
|
// what edits to the file are computed against.
|
|
387
479
|
let localFile = null;
|
|
388
480
|
let fileSource = null;
|
|
481
|
+
/**
|
|
482
|
+
* The file as the developer left it, after import healing (a stripped
|
|
483
|
+
* import is an interrupted sync's damage, not a code edit) and before
|
|
484
|
+
* tagging (an element added without a `$sid` IS one).
|
|
485
|
+
*/
|
|
486
|
+
let originalSource = null;
|
|
389
487
|
if (fileExists) {
|
|
390
488
|
fileSource = readFileSync(filePath, 'utf8');
|
|
391
489
|
// Pre-extraction import healing: a file left behind by an interrupted
|
|
@@ -401,6 +499,7 @@ async function mergeFiles(args) {
|
|
|
401
499
|
report.notes.push(`${mapping.file}: restored missing imports before extraction`);
|
|
402
500
|
}
|
|
403
501
|
}
|
|
502
|
+
originalSource = fileSource;
|
|
404
503
|
// An element the developer added without a `$sid` is invisible to
|
|
405
504
|
// extraction — and so to the Creator — for as long as it stays untagged.
|
|
406
505
|
// Tag it the way adoption did the file: react-native primitives swapped
|
|
@@ -460,7 +559,9 @@ async function mergeFiles(args) {
|
|
|
460
559
|
report.conflicts.push({ ...c, file: mapping.file, deprecatedTo });
|
|
461
560
|
if (remoteEntity) {
|
|
462
561
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
463
|
-
|
|
562
|
+
const regenerated = await fileTextFor(remoteEntity, mapping, remoteSource, linker.unlink(remoteEntity.root, mapping.file, warnInto(report, mapping.file)), filePath);
|
|
563
|
+
writeFileSync(filePath, regenerated);
|
|
564
|
+
recordSource(id, remoteSource, regenerated, false);
|
|
464
565
|
report.files.push({
|
|
465
566
|
file: mapping.file,
|
|
466
567
|
action: 'deprecated',
|
|
@@ -511,27 +612,76 @@ async function mergeFiles(args) {
|
|
|
511
612
|
}
|
|
512
613
|
// Write the merged view into the file — in the file's language.
|
|
513
614
|
const mergedFile = linker.unlink(merged, mapping.file, warnInto(report, mapping.file));
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
615
|
+
/** The file's text after this sync. */
|
|
616
|
+
let written;
|
|
617
|
+
/** The developer changed the file since the last sync (unknowable without a base). */
|
|
618
|
+
let localTextChanged = false;
|
|
619
|
+
if (fileExists && fileSource !== null && localFile !== null && originalSource !== null) {
|
|
620
|
+
// The text 3-way (A12): the code outside `$sid` elements. The
|
|
621
|
+
// document merge above already settled every element; here only the
|
|
622
|
+
// surrounding code is at stake. Remote wins when the developer left the
|
|
623
|
+
// file alone; both changed is a conflict, and P0 keeps the local file.
|
|
624
|
+
const remoteTextChanged = remoteSource !== null && baseSource !== null && remoteSource.text !== baseSource.text;
|
|
625
|
+
// Route markers are the CLI's own (placement rewrites them), never a code edit.
|
|
626
|
+
localTextChanged =
|
|
627
|
+
baseSource !== null &&
|
|
628
|
+
withoutRouteMarker(originalSource) !== withoutRouteMarker(baseSource.text);
|
|
629
|
+
const fromRemote = remoteTextChanged && remoteSource !== null
|
|
630
|
+
? renderFromSource(remoteSource, mergedFile, mapping.file, report)
|
|
631
|
+
: null;
|
|
632
|
+
if (fromRemote !== null) {
|
|
633
|
+
if (localTextChanged) {
|
|
634
|
+
const deprecatedTo = relative(root, deprecateFile(filePath));
|
|
635
|
+
report.conflicts.push({
|
|
636
|
+
sid: mergedFile.sid,
|
|
637
|
+
reason: 'source-text',
|
|
638
|
+
detail: 'the code around the elements changed both here and in Creator',
|
|
639
|
+
file: mapping.file,
|
|
640
|
+
deprecatedTo,
|
|
641
|
+
});
|
|
642
|
+
report.files.push({
|
|
643
|
+
file: mapping.file,
|
|
644
|
+
action: 'deprecated',
|
|
645
|
+
detail: `code conflict — your version kept at ${deprecatedTo}; Creator's code applied`,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
report.files.push({
|
|
650
|
+
file: mapping.file,
|
|
651
|
+
action: 'updated',
|
|
652
|
+
detail: 'code edited in Creator',
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
656
|
+
writeFileSync(filePath, fromRemote);
|
|
657
|
+
written = fromRemote;
|
|
525
658
|
}
|
|
526
659
|
else {
|
|
527
|
-
|
|
660
|
+
const edits = treeToEdits(localFile, mergedFile);
|
|
661
|
+
// Import healing: patch-inserted elements must be importable, and files
|
|
662
|
+
// touched by an interrupted earlier sync may be missing imports already.
|
|
663
|
+
const healed = upsertImports(fileSource, importNeedsFor(mergedFile));
|
|
664
|
+
if (edits.length > 0 || healed !== fileSource) {
|
|
665
|
+
const { source: patched, warnings } = patch(healed, edits);
|
|
666
|
+
for (const w of warnings)
|
|
667
|
+
report.warnings.push({ ...w, file: mapping.file });
|
|
668
|
+
writeFileSync(filePath, patched);
|
|
669
|
+
report.files.push({ file: mapping.file, action: 'updated' });
|
|
670
|
+
written = patched;
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
report.files.push({ file: mapping.file, action: 'unchanged' });
|
|
674
|
+
written = fileSource;
|
|
675
|
+
}
|
|
528
676
|
}
|
|
529
677
|
}
|
|
530
678
|
else {
|
|
531
679
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
532
|
-
|
|
680
|
+
written = await fileTextFor(remoteEntity, mapping, remoteSource, mergedFile, filePath);
|
|
681
|
+
writeFileSync(filePath, written);
|
|
533
682
|
report.files.push({ file: mapping.file, action: fileExists ? 'regenerated' : 'created' });
|
|
534
683
|
}
|
|
684
|
+
recordSource(id, remoteSource, written, localTextChanged);
|
|
535
685
|
// Push = local changes rebased onto remote.
|
|
536
686
|
pushOps.push(...diffTree(remoteRoot, merged));
|
|
537
687
|
mergedOut.set(id, { ...remoteEntity, root: merged });
|
|
@@ -544,6 +694,65 @@ function warnInto(report, file) {
|
|
|
544
694
|
report.warnings.push({ ...w, file });
|
|
545
695
|
};
|
|
546
696
|
}
|
|
697
|
+
/**
|
|
698
|
+
* A container's stored source (A12) brought up to date with `tree` — the
|
|
699
|
+
* FILE-language tree the file must show: its `$sid` elements are re-patched
|
|
700
|
+
* from the document (which is the truth for them), everything around them is
|
|
701
|
+
* the text as stored. Null when the text holds no `$sid` root, which is
|
|
702
|
+
* reported and regenerated rather than silently guessed.
|
|
703
|
+
*/
|
|
704
|
+
function renderFromSource(source, tree, file, report) {
|
|
705
|
+
if (source === null)
|
|
706
|
+
return null;
|
|
707
|
+
const extracted = extract(source.text);
|
|
708
|
+
if (extracted.dom === null) {
|
|
709
|
+
report.notes.push(`${file}: the source stored in Creator has no $sid root — regenerated from the canvas instead`);
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
fillNonLiteralProps(extracted.dom, tree, extracted.warnings);
|
|
713
|
+
const healed = upsertImports(source.text, importNeedsFor(tree));
|
|
714
|
+
const edits = treeToEdits(extracted.dom, tree);
|
|
715
|
+
if (edits.length === 0)
|
|
716
|
+
return healed;
|
|
717
|
+
const { source: patched, warnings } = patch(healed, edits);
|
|
718
|
+
for (const w of warnings)
|
|
719
|
+
report.warnings.push({ ...w, file });
|
|
720
|
+
return patched;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Source ops for the containers the merge marked for upload: a create where
|
|
724
|
+
* the server has no Source yet, an update otherwise. `idMap` resolves
|
|
725
|
+
* containers created by the node push that just landed.
|
|
726
|
+
*/
|
|
727
|
+
function sourceOpsFor(mergedBase, remote, uploads, idMap) {
|
|
728
|
+
const ops = [];
|
|
729
|
+
for (const containerId of uploads) {
|
|
730
|
+
const serverId = idMap[containerId] ?? containerId;
|
|
731
|
+
if (serverId.startsWith('c_'))
|
|
732
|
+
continue; // its page never landed (offline): next sync
|
|
733
|
+
const source = mergedBase.sources.get(serverId);
|
|
734
|
+
if (!source)
|
|
735
|
+
throw new Error(`internal: no merged source for ${serverId}`);
|
|
736
|
+
const remoteSource = remote.sources.get(serverId) ?? null;
|
|
737
|
+
if (remoteSource === null) {
|
|
738
|
+
ops.push({
|
|
739
|
+
op: 'create',
|
|
740
|
+
entity: 'Source',
|
|
741
|
+
cid: mintCid(),
|
|
742
|
+
data: { of: serverId, lang: source.lang, text: source.text },
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
else if (remoteSource.text !== source.text) {
|
|
746
|
+
ops.push({
|
|
747
|
+
op: 'update',
|
|
748
|
+
entity: 'Source',
|
|
749
|
+
id: remoteSource.id,
|
|
750
|
+
patch: { text: source.text },
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return ops;
|
|
755
|
+
}
|
|
547
756
|
/** NON_LITERAL_PROP contract: expression props keep their last-known base value. */
|
|
548
757
|
function fillNonLiteralProps(local, baseRoot, warnings) {
|
|
549
758
|
const baseIndex = new Map();
|
|
@@ -623,6 +832,17 @@ function rewriteIds(root, mergedBase, fileMaps, idMap, report) {
|
|
|
623
832
|
renameTree(page.root);
|
|
624
833
|
for (const component of mergedBase.components.values())
|
|
625
834
|
renameTree(component.root);
|
|
835
|
+
// Source text mirrors the files: same rewrite, and a container minted by
|
|
836
|
+
// this push re-keys its entry under the server id.
|
|
837
|
+
const renamedSources = new Map();
|
|
838
|
+
for (const [containerId, source] of mergedBase.sources) {
|
|
839
|
+
let text = source.text;
|
|
840
|
+
for (const [cid, id] of entries)
|
|
841
|
+
text = text.split(`"${cid}"`).join(`"${id}"`);
|
|
842
|
+
const of = idMap[containerId] ?? containerId;
|
|
843
|
+
renamedSources.set(of, { ...source, id: sourceIdFor(of), of, text });
|
|
844
|
+
}
|
|
845
|
+
mergedBase.sources = renamedSources;
|
|
626
846
|
if (mergedBase.assets) {
|
|
627
847
|
mergedBase.assets = resolveAssetIds(mergedBase.assets, idMap);
|
|
628
848
|
writeAssetMap(root, mergedBase.assets);
|
|
@@ -637,6 +857,7 @@ function remoteAsBase(remote) {
|
|
|
637
857
|
navigation: remote.navigation,
|
|
638
858
|
style: remote.style,
|
|
639
859
|
assets: remote.assets,
|
|
860
|
+
sources: new Map([...remote.sources].map(([id, source]) => [id, { ...source, onServer: true }])),
|
|
640
861
|
};
|
|
641
862
|
}
|
|
642
863
|
function finalize(root, baseStore, mergedBase, pagesMap, componentsMap, head, seq) {
|