@valbuild/cli 0.103.0 → 0.103.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/dist/valbuild-cli-cli.cjs.dev.js +13 -16
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +13 -16
- package/cli/dist/valbuild-cli-cli.esm.js +13 -16
- package/package.json +3 -3
- package/src/__fixtures__/debug-snapshot/.val/patches/11111111-1111-4111-8111-111111111111/patch.json +6 -9
- package/src/__fixtures__/debug-snapshot/.val/patches/22222222-2222-4222-8222-222222222222/patch.json +5 -10
- package/src/__fixtures__/debug-snapshot/.val/patches/33333333-3333-4333-8333-333333333333/patch.json +16 -0
- package/src/__fixtures__/debug-snapshot/.val/patches/patches.log +4 -0
- package/src/debug/snapshot.ts +23 -18
- package/src/debug.test.ts +38 -17
- package/src/__fixtures__/debug-snapshot/.val/patches/head/patch.json +0 -20
|
@@ -1500,8 +1500,11 @@ async function buildSnapshot(ctx, options = {}) {
|
|
|
1500
1500
|
path: p
|
|
1501
1501
|
});
|
|
1502
1502
|
}
|
|
1503
|
-
entries[`.val/patches/${patch.
|
|
1503
|
+
entries[`.val/patches/${patch.patchId}/patch.json`] = JSON.stringify(toFsPatch(patch), null, 2);
|
|
1504
1504
|
}
|
|
1505
|
+
// The order lives in the log, not in the records, so it has to be written too
|
|
1506
|
+
// or the unzipped snapshot is a store with no order at all.
|
|
1507
|
+
entries[".val/patches/patches.log"] = ["val-patch-log v1", ...patches.map(patch => `${patch.patchId} ${patch.createdAt} ${patch.path}`), ""].join("\n");
|
|
1505
1508
|
let includesBinaryFiles = false;
|
|
1506
1509
|
if (options.includeFiles) {
|
|
1507
1510
|
includesBinaryFiles = await writeBinaryFiles(ctx, analysis.fileLastUpdatedByPatchId, patches, entries);
|
|
@@ -1568,34 +1571,29 @@ async function validateSnapshotSources(ctx, analysis) {
|
|
|
1568
1571
|
return validationRes.errors;
|
|
1569
1572
|
}
|
|
1570
1573
|
function toSnapshotPatches(orderedPatches) {
|
|
1571
|
-
return orderedPatches.map(
|
|
1574
|
+
return orderedPatches.map(patch => ({
|
|
1572
1575
|
patchId: patch.patchId,
|
|
1573
1576
|
path: patch.path,
|
|
1574
1577
|
createdAt: patch.createdAt,
|
|
1575
1578
|
authorId: patch.authorId,
|
|
1576
1579
|
baseSha: patch.baseSha,
|
|
1577
1580
|
appliedAt: patch.appliedAt,
|
|
1578
|
-
parentPatchId: i === 0 ? null : orderedPatches[i - 1].patchId,
|
|
1579
1581
|
patch: patch.patch
|
|
1580
1582
|
}));
|
|
1581
1583
|
}
|
|
1582
1584
|
|
|
1583
1585
|
/**
|
|
1584
1586
|
* The shape ValOpsFS writes per patch, so an unzipped snapshot is a patch store
|
|
1585
|
-
* a plain ValOpsFS can read.
|
|
1586
|
-
*
|
|
1587
|
+
* a plain ValOpsFS can read.
|
|
1588
|
+
*
|
|
1589
|
+
* The directory is named after the patch itself and the record points at
|
|
1590
|
+
* nothing: the order is in `patches.log` beside it. A record that named its
|
|
1591
|
+
* parent is how a snapshot could carry a chain that had already lost a link.
|
|
1587
1592
|
*/
|
|
1588
1593
|
function toFsPatch(patch) {
|
|
1589
1594
|
return {
|
|
1590
1595
|
patch: patch.patch,
|
|
1591
1596
|
patchId: patch.patchId,
|
|
1592
|
-
parentRef: patch.parentPatchId === null ? {
|
|
1593
|
-
type: "head",
|
|
1594
|
-
headBaseSha: patch.baseSha
|
|
1595
|
-
} : {
|
|
1596
|
-
type: "patch",
|
|
1597
|
-
patchId: patch.parentPatchId
|
|
1598
|
-
},
|
|
1599
1597
|
path: patch.path,
|
|
1600
1598
|
authorId: patch.authorId,
|
|
1601
1599
|
sessionId: null,
|
|
@@ -1605,14 +1603,13 @@ function toFsPatch(patch) {
|
|
|
1605
1603
|
};
|
|
1606
1604
|
}
|
|
1607
1605
|
async function writeBinaryFiles(ctx, fileLastUpdatedByPatchId, patches, entries) {
|
|
1608
|
-
const
|
|
1606
|
+
const known = new Set(patches.map(p => p.patchId));
|
|
1609
1607
|
let wrote = false;
|
|
1610
1608
|
for (const [filePath, data] of Object.entries(fileLastUpdatedByPatchId)) {
|
|
1611
1609
|
if (data.isDelete) {
|
|
1612
1610
|
continue;
|
|
1613
1611
|
}
|
|
1614
|
-
|
|
1615
|
-
if (parentPatchId === undefined) {
|
|
1612
|
+
if (!known.has(data.patchId)) {
|
|
1616
1613
|
continue;
|
|
1617
1614
|
}
|
|
1618
1615
|
const buffer = await ctx.serverOps.getBase64EncodedBinaryFileFromPatch(filePath, data.patchId, data.remote);
|
|
@@ -1620,7 +1617,7 @@ async function writeBinaryFiles(ctx, fileLastUpdatedByPatchId, patches, entries)
|
|
|
1620
1617
|
continue;
|
|
1621
1618
|
}
|
|
1622
1619
|
// Base64 so the snapshot stays a text-only entry map; the replay decodes it.
|
|
1623
|
-
entries[`.val/patches/${
|
|
1620
|
+
entries[`.val/patches/${data.patchId}/files${filePath}/${path__default["default"].posix.basename(filePath)}.base64`] = buffer.toString("base64");
|
|
1624
1621
|
wrote = true;
|
|
1625
1622
|
}
|
|
1626
1623
|
return wrote;
|
|
@@ -1500,8 +1500,11 @@ async function buildSnapshot(ctx, options = {}) {
|
|
|
1500
1500
|
path: p
|
|
1501
1501
|
});
|
|
1502
1502
|
}
|
|
1503
|
-
entries[`.val/patches/${patch.
|
|
1503
|
+
entries[`.val/patches/${patch.patchId}/patch.json`] = JSON.stringify(toFsPatch(patch), null, 2);
|
|
1504
1504
|
}
|
|
1505
|
+
// The order lives in the log, not in the records, so it has to be written too
|
|
1506
|
+
// or the unzipped snapshot is a store with no order at all.
|
|
1507
|
+
entries[".val/patches/patches.log"] = ["val-patch-log v1", ...patches.map(patch => `${patch.patchId} ${patch.createdAt} ${patch.path}`), ""].join("\n");
|
|
1505
1508
|
let includesBinaryFiles = false;
|
|
1506
1509
|
if (options.includeFiles) {
|
|
1507
1510
|
includesBinaryFiles = await writeBinaryFiles(ctx, analysis.fileLastUpdatedByPatchId, patches, entries);
|
|
@@ -1568,34 +1571,29 @@ async function validateSnapshotSources(ctx, analysis) {
|
|
|
1568
1571
|
return validationRes.errors;
|
|
1569
1572
|
}
|
|
1570
1573
|
function toSnapshotPatches(orderedPatches) {
|
|
1571
|
-
return orderedPatches.map(
|
|
1574
|
+
return orderedPatches.map(patch => ({
|
|
1572
1575
|
patchId: patch.patchId,
|
|
1573
1576
|
path: patch.path,
|
|
1574
1577
|
createdAt: patch.createdAt,
|
|
1575
1578
|
authorId: patch.authorId,
|
|
1576
1579
|
baseSha: patch.baseSha,
|
|
1577
1580
|
appliedAt: patch.appliedAt,
|
|
1578
|
-
parentPatchId: i === 0 ? null : orderedPatches[i - 1].patchId,
|
|
1579
1581
|
patch: patch.patch
|
|
1580
1582
|
}));
|
|
1581
1583
|
}
|
|
1582
1584
|
|
|
1583
1585
|
/**
|
|
1584
1586
|
* The shape ValOpsFS writes per patch, so an unzipped snapshot is a patch store
|
|
1585
|
-
* a plain ValOpsFS can read.
|
|
1586
|
-
*
|
|
1587
|
+
* a plain ValOpsFS can read.
|
|
1588
|
+
*
|
|
1589
|
+
* The directory is named after the patch itself and the record points at
|
|
1590
|
+
* nothing: the order is in `patches.log` beside it. A record that named its
|
|
1591
|
+
* parent is how a snapshot could carry a chain that had already lost a link.
|
|
1587
1592
|
*/
|
|
1588
1593
|
function toFsPatch(patch) {
|
|
1589
1594
|
return {
|
|
1590
1595
|
patch: patch.patch,
|
|
1591
1596
|
patchId: patch.patchId,
|
|
1592
|
-
parentRef: patch.parentPatchId === null ? {
|
|
1593
|
-
type: "head",
|
|
1594
|
-
headBaseSha: patch.baseSha
|
|
1595
|
-
} : {
|
|
1596
|
-
type: "patch",
|
|
1597
|
-
patchId: patch.parentPatchId
|
|
1598
|
-
},
|
|
1599
1597
|
path: patch.path,
|
|
1600
1598
|
authorId: patch.authorId,
|
|
1601
1599
|
sessionId: null,
|
|
@@ -1605,14 +1603,13 @@ function toFsPatch(patch) {
|
|
|
1605
1603
|
};
|
|
1606
1604
|
}
|
|
1607
1605
|
async function writeBinaryFiles(ctx, fileLastUpdatedByPatchId, patches, entries) {
|
|
1608
|
-
const
|
|
1606
|
+
const known = new Set(patches.map(p => p.patchId));
|
|
1609
1607
|
let wrote = false;
|
|
1610
1608
|
for (const [filePath, data] of Object.entries(fileLastUpdatedByPatchId)) {
|
|
1611
1609
|
if (data.isDelete) {
|
|
1612
1610
|
continue;
|
|
1613
1611
|
}
|
|
1614
|
-
|
|
1615
|
-
if (parentPatchId === undefined) {
|
|
1612
|
+
if (!known.has(data.patchId)) {
|
|
1616
1613
|
continue;
|
|
1617
1614
|
}
|
|
1618
1615
|
const buffer = await ctx.serverOps.getBase64EncodedBinaryFileFromPatch(filePath, data.patchId, data.remote);
|
|
@@ -1620,7 +1617,7 @@ async function writeBinaryFiles(ctx, fileLastUpdatedByPatchId, patches, entries)
|
|
|
1620
1617
|
continue;
|
|
1621
1618
|
}
|
|
1622
1619
|
// Base64 so the snapshot stays a text-only entry map; the replay decodes it.
|
|
1623
|
-
entries[`.val/patches/${
|
|
1620
|
+
entries[`.val/patches/${data.patchId}/files${filePath}/${path__default["default"].posix.basename(filePath)}.base64`] = buffer.toString("base64");
|
|
1624
1621
|
wrote = true;
|
|
1625
1622
|
}
|
|
1626
1623
|
return wrote;
|
|
@@ -1468,8 +1468,11 @@ async function buildSnapshot(ctx, options = {}) {
|
|
|
1468
1468
|
path: p
|
|
1469
1469
|
});
|
|
1470
1470
|
}
|
|
1471
|
-
entries[`.val/patches/${patch.
|
|
1471
|
+
entries[`.val/patches/${patch.patchId}/patch.json`] = JSON.stringify(toFsPatch(patch), null, 2);
|
|
1472
1472
|
}
|
|
1473
|
+
// The order lives in the log, not in the records, so it has to be written too
|
|
1474
|
+
// or the unzipped snapshot is a store with no order at all.
|
|
1475
|
+
entries[".val/patches/patches.log"] = ["val-patch-log v1", ...patches.map(patch => `${patch.patchId} ${patch.createdAt} ${patch.path}`), ""].join("\n");
|
|
1473
1476
|
let includesBinaryFiles = false;
|
|
1474
1477
|
if (options.includeFiles) {
|
|
1475
1478
|
includesBinaryFiles = await writeBinaryFiles(ctx, analysis.fileLastUpdatedByPatchId, patches, entries);
|
|
@@ -1536,34 +1539,29 @@ async function validateSnapshotSources(ctx, analysis) {
|
|
|
1536
1539
|
return validationRes.errors;
|
|
1537
1540
|
}
|
|
1538
1541
|
function toSnapshotPatches(orderedPatches) {
|
|
1539
|
-
return orderedPatches.map(
|
|
1542
|
+
return orderedPatches.map(patch => ({
|
|
1540
1543
|
patchId: patch.patchId,
|
|
1541
1544
|
path: patch.path,
|
|
1542
1545
|
createdAt: patch.createdAt,
|
|
1543
1546
|
authorId: patch.authorId,
|
|
1544
1547
|
baseSha: patch.baseSha,
|
|
1545
1548
|
appliedAt: patch.appliedAt,
|
|
1546
|
-
parentPatchId: i === 0 ? null : orderedPatches[i - 1].patchId,
|
|
1547
1549
|
patch: patch.patch
|
|
1548
1550
|
}));
|
|
1549
1551
|
}
|
|
1550
1552
|
|
|
1551
1553
|
/**
|
|
1552
1554
|
* The shape ValOpsFS writes per patch, so an unzipped snapshot is a patch store
|
|
1553
|
-
* a plain ValOpsFS can read.
|
|
1554
|
-
*
|
|
1555
|
+
* a plain ValOpsFS can read.
|
|
1556
|
+
*
|
|
1557
|
+
* The directory is named after the patch itself and the record points at
|
|
1558
|
+
* nothing: the order is in `patches.log` beside it. A record that named its
|
|
1559
|
+
* parent is how a snapshot could carry a chain that had already lost a link.
|
|
1555
1560
|
*/
|
|
1556
1561
|
function toFsPatch(patch) {
|
|
1557
1562
|
return {
|
|
1558
1563
|
patch: patch.patch,
|
|
1559
1564
|
patchId: patch.patchId,
|
|
1560
|
-
parentRef: patch.parentPatchId === null ? {
|
|
1561
|
-
type: "head",
|
|
1562
|
-
headBaseSha: patch.baseSha
|
|
1563
|
-
} : {
|
|
1564
|
-
type: "patch",
|
|
1565
|
-
patchId: patch.parentPatchId
|
|
1566
|
-
},
|
|
1567
1565
|
path: patch.path,
|
|
1568
1566
|
authorId: patch.authorId,
|
|
1569
1567
|
sessionId: null,
|
|
@@ -1573,14 +1571,13 @@ function toFsPatch(patch) {
|
|
|
1573
1571
|
};
|
|
1574
1572
|
}
|
|
1575
1573
|
async function writeBinaryFiles(ctx, fileLastUpdatedByPatchId, patches, entries) {
|
|
1576
|
-
const
|
|
1574
|
+
const known = new Set(patches.map(p => p.patchId));
|
|
1577
1575
|
let wrote = false;
|
|
1578
1576
|
for (const [filePath, data] of Object.entries(fileLastUpdatedByPatchId)) {
|
|
1579
1577
|
if (data.isDelete) {
|
|
1580
1578
|
continue;
|
|
1581
1579
|
}
|
|
1582
|
-
|
|
1583
|
-
if (parentPatchId === undefined) {
|
|
1580
|
+
if (!known.has(data.patchId)) {
|
|
1584
1581
|
continue;
|
|
1585
1582
|
}
|
|
1586
1583
|
const buffer = await ctx.serverOps.getBase64EncodedBinaryFileFromPatch(filePath, data.patchId, data.remote);
|
|
@@ -1588,7 +1585,7 @@ async function writeBinaryFiles(ctx, fileLastUpdatedByPatchId, patches, entries)
|
|
|
1588
1585
|
continue;
|
|
1589
1586
|
}
|
|
1590
1587
|
// Base64 so the snapshot stays a text-only entry map; the replay decodes it.
|
|
1591
|
-
entries[`.val/patches/${
|
|
1588
|
+
entries[`.val/patches/${data.patchId}/files${filePath}/${path.posix.basename(filePath)}.base64`] = buffer.toString("base64");
|
|
1592
1589
|
wrote = true;
|
|
1593
1590
|
}
|
|
1594
1591
|
return wrote;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valbuild/cli",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.103.
|
|
4
|
+
"version": "0.103.1",
|
|
5
5
|
"description": "Val CLI tools",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"zod": "^4.3.5",
|
|
30
30
|
"@valbuild/core": "0.103.0",
|
|
31
31
|
"@valbuild/eslint-plugin": "0.93.0",
|
|
32
|
-
"@valbuild/language-server": "0.103.
|
|
33
|
-
"@valbuild/server": "0.103.
|
|
32
|
+
"@valbuild/language-server": "0.103.1",
|
|
33
|
+
"@valbuild/server": "0.103.1",
|
|
34
34
|
"@valbuild/shared": "0.103.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
package/src/__fixtures__/debug-snapshot/.val/patches/11111111-1111-4111-8111-111111111111/patch.json
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"patch": [
|
|
3
3
|
{
|
|
4
|
-
"op": "
|
|
5
|
-
"path": ["BBL", "
|
|
4
|
+
"op": "replace",
|
|
5
|
+
"path": ["BBL", "title"],
|
|
6
|
+
"value": "BBL Housing"
|
|
6
7
|
}
|
|
7
8
|
],
|
|
8
|
-
"patchId": "
|
|
9
|
-
"parentRef": {
|
|
10
|
-
"type": "patch",
|
|
11
|
-
"patchId": "11111111-1111-4111-8111-111111111111"
|
|
12
|
-
},
|
|
9
|
+
"patchId": "11111111-1111-4111-8111-111111111111",
|
|
13
10
|
"path": "/content/projects.val.ts",
|
|
14
|
-
"authorId": "author-
|
|
11
|
+
"authorId": "author-a",
|
|
15
12
|
"sessionId": null,
|
|
16
13
|
"baseSha": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
17
14
|
"coreVersion": null,
|
|
18
|
-
"createdAt": "2026-08-
|
|
15
|
+
"createdAt": "2026-08-10T12:00:00.000Z"
|
|
19
16
|
}
|
package/src/__fixtures__/debug-snapshot/.val/patches/22222222-2222-4222-8222-222222222222/patch.json
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"patch": [
|
|
3
3
|
{
|
|
4
|
-
"op": "
|
|
5
|
-
"path": ["BBL", "summary", "services", "2"]
|
|
6
|
-
"value": "Development U"
|
|
4
|
+
"op": "remove",
|
|
5
|
+
"path": ["BBL", "summary", "services", "2"]
|
|
7
6
|
}
|
|
8
7
|
],
|
|
9
|
-
"patchId": "
|
|
10
|
-
"parentRef": {
|
|
11
|
-
"type": "patch",
|
|
12
|
-
"patchId": "22222222-2222-4222-8222-222222222222"
|
|
13
|
-
},
|
|
8
|
+
"patchId": "22222222-2222-4222-8222-222222222222",
|
|
14
9
|
"path": "/content/projects.val.ts",
|
|
15
|
-
"authorId": "author-
|
|
10
|
+
"authorId": "author-b",
|
|
16
11
|
"sessionId": null,
|
|
17
12
|
"baseSha": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
18
13
|
"coreVersion": null,
|
|
19
|
-
"createdAt": "2026-08-11T09:00:
|
|
14
|
+
"createdAt": "2026-08-11T09:00:00.000Z"
|
|
20
15
|
}
|
package/src/__fixtures__/debug-snapshot/.val/patches/33333333-3333-4333-8333-333333333333/patch.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"patch": [
|
|
3
|
+
{
|
|
4
|
+
"op": "replace",
|
|
5
|
+
"path": ["BBL", "summary", "services", "2"],
|
|
6
|
+
"value": "Development U"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"patchId": "33333333-3333-4333-8333-333333333333",
|
|
10
|
+
"path": "/content/projects.val.ts",
|
|
11
|
+
"authorId": "author-a",
|
|
12
|
+
"sessionId": null,
|
|
13
|
+
"baseSha": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
14
|
+
"coreVersion": null,
|
|
15
|
+
"createdAt": "2026-08-11T09:00:05.000Z"
|
|
16
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
val-patch-log v1
|
|
2
|
+
11111111-1111-4111-8111-111111111111 2026-08-10T12:00:00.000Z /content/projects.val.ts
|
|
3
|
+
22222222-2222-4222-8222-222222222222 2026-08-11T09:00:00.000Z /content/projects.val.ts
|
|
4
|
+
33333333-3333-4333-8333-333333333333 2026-08-11T09:00:05.000Z /content/projects.val.ts
|
package/src/debug/snapshot.ts
CHANGED
|
@@ -22,8 +22,6 @@ export type SnapshotPatch = {
|
|
|
22
22
|
authorId: string | null;
|
|
23
23
|
baseSha: string;
|
|
24
24
|
appliedAt: { commitSha: string } | null;
|
|
25
|
-
/** Parent in the chain: null means it is the first ("head"). */
|
|
26
|
-
parentPatchId: PatchId | null;
|
|
27
25
|
patch: unknown;
|
|
28
26
|
};
|
|
29
27
|
|
|
@@ -199,9 +197,21 @@ export async function buildSnapshot(
|
|
|
199
197
|
for (const p of elided.elided) {
|
|
200
198
|
elidedPatchValues.push({ patchId: patch.patchId, path: p });
|
|
201
199
|
}
|
|
202
|
-
entries[`.val/patches/${patch.
|
|
203
|
-
|
|
200
|
+
entries[`.val/patches/${patch.patchId}/patch.json`] = JSON.stringify(
|
|
201
|
+
toFsPatch(patch),
|
|
202
|
+
null,
|
|
203
|
+
2,
|
|
204
|
+
);
|
|
204
205
|
}
|
|
206
|
+
// The order lives in the log, not in the records, so it has to be written too
|
|
207
|
+
// or the unzipped snapshot is a store with no order at all.
|
|
208
|
+
entries[".val/patches/patches.log"] = [
|
|
209
|
+
"val-patch-log v1",
|
|
210
|
+
...patches.map(
|
|
211
|
+
(patch) => `${patch.patchId} ${patch.createdAt} ${patch.path}`,
|
|
212
|
+
),
|
|
213
|
+
"",
|
|
214
|
+
].join("\n");
|
|
205
215
|
|
|
206
216
|
let includesBinaryFiles = false;
|
|
207
217
|
if (options.includeFiles) {
|
|
@@ -296,31 +306,29 @@ function toSnapshotPatches(
|
|
|
296
306
|
appliedAt: { commitSha: string } | null;
|
|
297
307
|
}[],
|
|
298
308
|
): SnapshotPatch[] {
|
|
299
|
-
return orderedPatches.map((patch
|
|
309
|
+
return orderedPatches.map((patch) => ({
|
|
300
310
|
patchId: patch.patchId,
|
|
301
311
|
path: patch.path,
|
|
302
312
|
createdAt: patch.createdAt,
|
|
303
313
|
authorId: patch.authorId,
|
|
304
314
|
baseSha: patch.baseSha,
|
|
305
315
|
appliedAt: patch.appliedAt,
|
|
306
|
-
parentPatchId: i === 0 ? null : orderedPatches[i - 1].patchId,
|
|
307
316
|
patch: patch.patch,
|
|
308
317
|
}));
|
|
309
318
|
}
|
|
310
319
|
|
|
311
320
|
/**
|
|
312
321
|
* The shape ValOpsFS writes per patch, so an unzipped snapshot is a patch store
|
|
313
|
-
* a plain ValOpsFS can read.
|
|
314
|
-
*
|
|
322
|
+
* a plain ValOpsFS can read.
|
|
323
|
+
*
|
|
324
|
+
* The directory is named after the patch itself and the record points at
|
|
325
|
+
* nothing: the order is in `patches.log` beside it. A record that named its
|
|
326
|
+
* parent is how a snapshot could carry a chain that had already lost a link.
|
|
315
327
|
*/
|
|
316
328
|
function toFsPatch(patch: SnapshotPatch) {
|
|
317
329
|
return {
|
|
318
330
|
patch: patch.patch,
|
|
319
331
|
patchId: patch.patchId,
|
|
320
|
-
parentRef:
|
|
321
|
-
patch.parentPatchId === null
|
|
322
|
-
? { type: "head", headBaseSha: patch.baseSha }
|
|
323
|
-
: { type: "patch", patchId: patch.parentPatchId },
|
|
324
332
|
path: patch.path,
|
|
325
333
|
authorId: patch.authorId,
|
|
326
334
|
sessionId: null,
|
|
@@ -339,16 +347,13 @@ async function writeBinaryFiles(
|
|
|
339
347
|
patches: SnapshotPatch[],
|
|
340
348
|
entries: Record<string, string>,
|
|
341
349
|
): Promise<boolean> {
|
|
342
|
-
const
|
|
343
|
-
patches.map((p) => [p.patchId, p.parentPatchId ?? "head"]),
|
|
344
|
-
);
|
|
350
|
+
const known = new Set(patches.map((p) => p.patchId));
|
|
345
351
|
let wrote = false;
|
|
346
352
|
for (const [filePath, data] of Object.entries(fileLastUpdatedByPatchId)) {
|
|
347
353
|
if (data.isDelete) {
|
|
348
354
|
continue;
|
|
349
355
|
}
|
|
350
|
-
|
|
351
|
-
if (parentPatchId === undefined) {
|
|
356
|
+
if (!known.has(data.patchId)) {
|
|
352
357
|
continue;
|
|
353
358
|
}
|
|
354
359
|
const buffer = await ctx.serverOps.getBase64EncodedBinaryFileFromPatch(
|
|
@@ -361,7 +366,7 @@ async function writeBinaryFiles(
|
|
|
361
366
|
}
|
|
362
367
|
// Base64 so the snapshot stays a text-only entry map; the replay decodes it.
|
|
363
368
|
entries[
|
|
364
|
-
`.val/patches/${
|
|
369
|
+
`.val/patches/${data.patchId}/files${filePath}/${path.posix.basename(filePath)}.base64`
|
|
365
370
|
] = buffer.toString("base64");
|
|
366
371
|
wrote = true;
|
|
367
372
|
}
|
package/src/debug.test.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
1
3
|
import path from "path";
|
|
2
4
|
import { ModuleFilePath } from "@valbuild/core";
|
|
5
|
+
import { readPatchStore } from "@valbuild/server";
|
|
3
6
|
import { createDebugContext } from "./debug/context";
|
|
4
7
|
import { buildSnapshot } from "./debug/snapshot";
|
|
5
8
|
|
|
@@ -71,25 +74,43 @@ describe("val debug snapshot", () => {
|
|
|
71
74
|
expect(entries["val.modules.original.ts"]).toContain("unrelated");
|
|
72
75
|
});
|
|
73
76
|
|
|
74
|
-
test("writes
|
|
77
|
+
test("writes a patch store ValOpsFS can actually read back", async () => {
|
|
75
78
|
const { entries } = await build();
|
|
76
79
|
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
80
|
+
// Materialise it and read it with the real store, rather than asserting on
|
|
81
|
+
// path strings: "the layout ValOpsFS reads back" is the property, and a
|
|
82
|
+
// snapshot that merely looks right is exactly what a debug tool must not
|
|
83
|
+
// produce.
|
|
84
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "val-snapshot-"));
|
|
85
|
+
for (const [name, content] of Object.entries(entries)) {
|
|
86
|
+
const file = path.join(dir, name);
|
|
87
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
88
|
+
fs.writeFileSync(file, content);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const read = readPatchStore(path.join(dir, ".val", "patches"));
|
|
92
|
+
if (read.status !== "ok") {
|
|
93
|
+
throw new Error(`expected ok, got ${read.status}`);
|
|
94
|
+
}
|
|
95
|
+
expect(read.problems).toEqual([]);
|
|
96
|
+
expect(read.entries.map((entry) => entry.patchId)).toEqual([
|
|
97
|
+
"11111111-1111-4111-8111-111111111111",
|
|
98
|
+
"22222222-2222-4222-8222-222222222222",
|
|
99
|
+
"33333333-3333-4333-8333-333333333333",
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
// Each record sits under its own id and points at nothing. A record that
|
|
103
|
+
// named its parent is how a snapshot could carry a chain that had already
|
|
104
|
+
// lost a link.
|
|
105
|
+
for (const entry of read.entries) {
|
|
106
|
+
const raw = JSON.parse(
|
|
107
|
+
entries[`.val/patches/${entry.patchId}/patch.json`],
|
|
108
|
+
);
|
|
109
|
+
expect(raw.patchId).toBe(entry.patchId);
|
|
110
|
+
expect(raw).not.toHaveProperty("parentRef");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
93
114
|
});
|
|
94
115
|
|
|
95
116
|
test("records why the snapshot may be incomplete rather than failing silently", async () => {
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"patch": [
|
|
3
|
-
{
|
|
4
|
-
"op": "replace",
|
|
5
|
-
"path": ["BBL", "title"],
|
|
6
|
-
"value": "BBL Housing"
|
|
7
|
-
}
|
|
8
|
-
],
|
|
9
|
-
"patchId": "11111111-1111-4111-8111-111111111111",
|
|
10
|
-
"parentRef": {
|
|
11
|
-
"type": "head",
|
|
12
|
-
"headBaseSha": "0000000000000000000000000000000000000000000000000000000000000000"
|
|
13
|
-
},
|
|
14
|
-
"path": "/content/projects.val.ts",
|
|
15
|
-
"authorId": "author-a",
|
|
16
|
-
"sessionId": null,
|
|
17
|
-
"baseSha": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
18
|
-
"coreVersion": null,
|
|
19
|
-
"createdAt": "2026-08-10T12:00:00.000Z"
|
|
20
|
-
}
|