@valbuild/server 0.108.3 → 0.108.5
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/declarations/src/Service.d.ts +14 -0
- package/dist/declarations/src/getValidationErrorFileRef.d.ts +1 -1
- package/dist/declarations/src/index.d.ts +2 -0
- package/dist/declarations/src/patch/jsonValuesPatch.d.ts +107 -0
- package/dist/declarations/src/patchStore.d.ts +1 -1
- package/dist/valbuild-server.cjs.dev.js +315 -130
- package/dist/valbuild-server.cjs.prod.js +315 -130
- package/dist/valbuild-server.esm.js +314 -131
- package/package.json +3 -3
|
@@ -1040,7 +1040,7 @@ function errorMessage(e) {
|
|
|
1040
1040
|
return String(e);
|
|
1041
1041
|
}
|
|
1042
1042
|
|
|
1043
|
-
const jsonOps$
|
|
1043
|
+
const jsonOps$2 = new patch.JSONOps();
|
|
1044
1044
|
|
|
1045
1045
|
/**
|
|
1046
1046
|
* Classification of a single patch op against a module's serialized schema,
|
|
@@ -1295,7 +1295,7 @@ function applyJsonValuesEntryPatches(args) {
|
|
|
1295
1295
|
// on an entry that does not exist.
|
|
1296
1296
|
continue;
|
|
1297
1297
|
}
|
|
1298
|
-
const applied = patch.applyPatch(patch.deepClone(content), jsonOps$
|
|
1298
|
+
const applied = patch.applyPatch(patch.deepClone(content), jsonOps$2, [{
|
|
1299
1299
|
op: "add",
|
|
1300
1300
|
path: cls.subPath.concat(...(op.nestedFilePath ?? [])).concat("patch_id"),
|
|
1301
1301
|
value: patchId
|
|
@@ -1349,7 +1349,7 @@ function applyJsonValuesEntryPatches(args) {
|
|
|
1349
1349
|
patchId
|
|
1350
1350
|
};
|
|
1351
1351
|
}
|
|
1352
|
-
const applied = patch.applyPatch(patch.deepClone(content), jsonOps$
|
|
1352
|
+
const applied = patch.applyPatch(patch.deepClone(content), jsonOps$2, [rebased.value]);
|
|
1353
1353
|
if (fp.result.isErr(applied)) {
|
|
1354
1354
|
return {
|
|
1355
1355
|
kind: "error",
|
|
@@ -1387,6 +1387,19 @@ function resolveExistingJsonPath(moduleFilePath, importPath) {
|
|
|
1387
1387
|
return path__namespace.posix.join(moduleDir, importPath);
|
|
1388
1388
|
}
|
|
1389
1389
|
|
|
1390
|
+
/**
|
|
1391
|
+
* The outcome of {@link validateJsonValuesEntries}: the errors found inside the
|
|
1392
|
+
* entries, plus the content of every entry that could be loaded, keyed by entry
|
|
1393
|
+
* key.
|
|
1394
|
+
*
|
|
1395
|
+
* The content is part of the result because the errors are useless without it.
|
|
1396
|
+
* They are reported at paths that point INSIDE an entry, and the module source
|
|
1397
|
+
* on disk holds a `c.json(...)` marker there — so anything that has to resolve
|
|
1398
|
+
* one of those paths (the `val validate --fix` handlers, chiefly) needs the very
|
|
1399
|
+
* content this function has just loaded. Returning it costs nothing and is the
|
|
1400
|
+
* only way to avoid loading every entry twice.
|
|
1401
|
+
*/
|
|
1402
|
+
|
|
1390
1403
|
/**
|
|
1391
1404
|
* Validates the content of every `.jsonValues()` entry in a module by loading
|
|
1392
1405
|
* each backing `*.val.json` (via its lazy import thunk) and checking it against
|
|
@@ -1439,14 +1452,19 @@ function resolveExistingJsonPath(moduleFilePath, importPath) {
|
|
|
1439
1452
|
*/
|
|
1440
1453
|
async function validateJsonValuesEntries(schema, source, modulePath) {
|
|
1441
1454
|
const out = {};
|
|
1455
|
+
const loadedEntries = {};
|
|
1456
|
+
const res = {
|
|
1457
|
+
errors: out,
|
|
1458
|
+
loadedEntries
|
|
1459
|
+
};
|
|
1442
1460
|
if (!(schema instanceof core.RecordSchema)) {
|
|
1443
|
-
return
|
|
1461
|
+
return res;
|
|
1444
1462
|
}
|
|
1445
1463
|
if (!schema["executeSerialize"]().jsonValues) {
|
|
1446
|
-
return
|
|
1464
|
+
return res;
|
|
1447
1465
|
}
|
|
1448
1466
|
if (source === null || typeof source !== "object" || Array.isArray(source)) {
|
|
1449
|
-
return
|
|
1467
|
+
return res;
|
|
1450
1468
|
}
|
|
1451
1469
|
for (const [key, marker] of Object.entries(source)) {
|
|
1452
1470
|
const entryPath = core.Internal.createValPathOfItem(modulePath, key);
|
|
@@ -1482,6 +1500,10 @@ async function validateJsonValuesEntries(schema, source, modulePath) {
|
|
|
1482
1500
|
}];
|
|
1483
1501
|
continue;
|
|
1484
1502
|
}
|
|
1503
|
+
// Kept so a caller can substitute the content back into the module source:
|
|
1504
|
+
// the entries have been paid for here, and a source that still holds
|
|
1505
|
+
// markers cannot resolve a path INTO an entry (see the doc comment above).
|
|
1506
|
+
loadedEntries[key] = content;
|
|
1485
1507
|
const entryErrors = schema.validateJsonEntryContent(entryPath, content);
|
|
1486
1508
|
if (entryErrors) {
|
|
1487
1509
|
for (const [p, errs] of Object.entries(entryErrors)) {
|
|
@@ -1489,9 +1511,155 @@ async function validateJsonValuesEntries(schema, source, modulePath) {
|
|
|
1489
1511
|
}
|
|
1490
1512
|
}
|
|
1491
1513
|
}
|
|
1492
|
-
return
|
|
1514
|
+
return res;
|
|
1493
1515
|
}
|
|
1494
1516
|
|
|
1517
|
+
/**
|
|
1518
|
+
* Given the `source` object-literal expression of a `.jsonValues()` record /
|
|
1519
|
+
* router (the 3rd argument to `c.define`, via {@link analyzeValModule}),
|
|
1520
|
+
* extracts each entry's `c.json(() => import("..."))` import path, keyed by
|
|
1521
|
+
* entry key. Entries whose value is not a `c.json(...)` call are skipped.
|
|
1522
|
+
*/
|
|
1523
|
+
function analyzeJsonValuesEntries(source) {
|
|
1524
|
+
const entries = new Map();
|
|
1525
|
+
if (!ts__default["default"].isObjectLiteralExpression(source)) {
|
|
1526
|
+
return entries;
|
|
1527
|
+
}
|
|
1528
|
+
for (const prop of source.properties) {
|
|
1529
|
+
if (!ts__default["default"].isPropertyAssignment(prop)) {
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1532
|
+
const key = getPropertyKey(prop.name);
|
|
1533
|
+
if (key === null) {
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
const entry = analyzeCJsonCall(prop.initializer);
|
|
1537
|
+
if (entry) {
|
|
1538
|
+
entries.set(key, entry);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
return entries;
|
|
1542
|
+
}
|
|
1543
|
+
function getPropertyKey(name) {
|
|
1544
|
+
if (ts__default["default"].isStringLiteralLike(name)) {
|
|
1545
|
+
return name.text;
|
|
1546
|
+
}
|
|
1547
|
+
if (ts__default["default"].isIdentifier(name)) {
|
|
1548
|
+
return name.text;
|
|
1549
|
+
}
|
|
1550
|
+
return null;
|
|
1551
|
+
}
|
|
1552
|
+
function analyzeCJsonCall(node) {
|
|
1553
|
+
// c.json(() => import("path"))
|
|
1554
|
+
if (!ts__default["default"].isCallExpression(node) || !isCJson(node.expression)) {
|
|
1555
|
+
return null;
|
|
1556
|
+
}
|
|
1557
|
+
const [thunk] = node.arguments;
|
|
1558
|
+
if (!thunk) {
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
1561
|
+
const importPath = getImportPathFromThunk(thunk);
|
|
1562
|
+
if (importPath === null) {
|
|
1563
|
+
return null;
|
|
1564
|
+
}
|
|
1565
|
+
return {
|
|
1566
|
+
importPath
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
function isCJson(expr) {
|
|
1570
|
+
// c.json
|
|
1571
|
+
return ts__default["default"].isPropertyAccessExpression(expr) && ts__default["default"].isIdentifier(expr.expression) && expr.expression.text === "c" && ts__default["default"].isIdentifier(expr.name) && expr.name.text === "json";
|
|
1572
|
+
}
|
|
1573
|
+
function getImportPathFromThunk(thunk) {
|
|
1574
|
+
// () => import("path") (concise body) OR () => { return import("path"); }
|
|
1575
|
+
if (!ts__default["default"].isArrowFunction(thunk)) {
|
|
1576
|
+
return null;
|
|
1577
|
+
}
|
|
1578
|
+
let importCall;
|
|
1579
|
+
if (ts__default["default"].isCallExpression(thunk.body)) {
|
|
1580
|
+
importCall = thunk.body;
|
|
1581
|
+
} else if (ts__default["default"].isBlock(thunk.body)) {
|
|
1582
|
+
for (const stmt of thunk.body.statements) {
|
|
1583
|
+
if (ts__default["default"].isReturnStatement(stmt) && stmt.expression && ts__default["default"].isCallExpression(stmt.expression)) {
|
|
1584
|
+
importCall = stmt.expression;
|
|
1585
|
+
break;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
if (!importCall || !ts__default["default"].isCallExpression(importCall)) {
|
|
1590
|
+
return null;
|
|
1591
|
+
}
|
|
1592
|
+
const isImport = importCall.expression.kind === ts__default["default"].SyntaxKind.ImportKeyword || ts__default["default"].isIdentifier(importCall.expression) && importCall.expression.text === "import";
|
|
1593
|
+
if (!isImport) {
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
const arg = importCall.arguments[0];
|
|
1597
|
+
if (arg && ts__default["default"].isStringLiteralLike(arg)) {
|
|
1598
|
+
return arg.text;
|
|
1599
|
+
}
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* Which `*.val.json` holds a `.jsonValues()` entry's content, given the module's
|
|
1605
|
+
* `.val.ts`.
|
|
1606
|
+
*
|
|
1607
|
+
* Tooling that maps a validation `sourcePath` back to a place in a file needs
|
|
1608
|
+
* this: for a jsonValues module the offending value is NOT in the `.val.ts` at
|
|
1609
|
+
* all — that file only holds `c.json(() => import("./x.val.json"))` — so a
|
|
1610
|
+
* resolver that only ever looks at the `.val.ts` can report the error but not
|
|
1611
|
+
* where it lives, which for a record with hundreds of entries is not much of a
|
|
1612
|
+
* report.
|
|
1613
|
+
*
|
|
1614
|
+
* Returns a path relative to the project root (leading slash), or undefined when
|
|
1615
|
+
* the module has no such entry (not a jsonValues module, unparseable, or the key
|
|
1616
|
+
* is not backed by a `c.json` thunk).
|
|
1617
|
+
*/
|
|
1618
|
+
function findJsonEntryFilePath(moduleFilePath, valTsSourceFile, entryKey) {
|
|
1619
|
+
let analysis;
|
|
1620
|
+
try {
|
|
1621
|
+
analysis = analyzeValModule(valTsSourceFile);
|
|
1622
|
+
} catch {
|
|
1623
|
+
return undefined;
|
|
1624
|
+
}
|
|
1625
|
+
if (fp.result.isErr(analysis)) {
|
|
1626
|
+
return undefined;
|
|
1627
|
+
}
|
|
1628
|
+
const entry = analyzeJsonValuesEntries(analysis.value.source).get(entryKey);
|
|
1629
|
+
if (!entry) {
|
|
1630
|
+
return undefined;
|
|
1631
|
+
}
|
|
1632
|
+
return resolveExistingJsonPath(moduleFilePath, entry.importPath);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
const jsonOps$1 = new patch.JSONOps();
|
|
1636
|
+
|
|
1637
|
+
/**
|
|
1638
|
+
* Substitutes loaded `.jsonValues()` entry content back into a module's root
|
|
1639
|
+
* record, in place of the `c.json(...)` markers the `.val.ts` holds.
|
|
1640
|
+
*
|
|
1641
|
+
* Only entries that actually loaded are replaced: an entry whose file is missing
|
|
1642
|
+
* or unparseable keeps its marker, so the failure stays visible as the
|
|
1643
|
+
* validation error it already produced rather than becoming a silent `undefined`
|
|
1644
|
+
* further down.
|
|
1645
|
+
*/
|
|
1646
|
+
function withLoadedJsonEntries(source, loadedEntries) {
|
|
1647
|
+
if (Object.keys(loadedEntries).length === 0) {
|
|
1648
|
+
return source;
|
|
1649
|
+
}
|
|
1650
|
+
if (!isSourceObject(source)) {
|
|
1651
|
+
return source;
|
|
1652
|
+
}
|
|
1653
|
+
return {
|
|
1654
|
+
...source,
|
|
1655
|
+
...loadedEntries
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
/** A non-null, non-array source object: the shape a record/router source has. */
|
|
1660
|
+
function isSourceObject(source) {
|
|
1661
|
+
return typeof source === "object" && source !== null && !Array.isArray(source);
|
|
1662
|
+
}
|
|
1495
1663
|
async function createService(projectRoot, host = {
|
|
1496
1664
|
...ts__default["default"].sys,
|
|
1497
1665
|
writeFile: (fileName, data, encoding) => {
|
|
@@ -1530,13 +1698,16 @@ class Service {
|
|
|
1530
1698
|
getModuleFilePaths() {
|
|
1531
1699
|
return Object.keys(this.extracted.sources);
|
|
1532
1700
|
}
|
|
1701
|
+
serializedSchemaOf(moduleFilePath) {
|
|
1702
|
+
return this.extracted.serializedSchemas[moduleFilePath];
|
|
1703
|
+
}
|
|
1533
1704
|
async get(moduleFilePath, modulePath, options) {
|
|
1534
1705
|
const opts = options ?? {
|
|
1535
1706
|
validate: true
|
|
1536
1707
|
};
|
|
1537
1708
|
const source = this.extracted.sources[moduleFilePath];
|
|
1538
1709
|
const schema = this.extracted.schemas[moduleFilePath];
|
|
1539
|
-
const serializedSchema = this.
|
|
1710
|
+
const serializedSchema = this.serializedSchemaOf(moduleFilePath);
|
|
1540
1711
|
const moduleError = this.extracted.moduleErrors.find(e => e.path === moduleFilePath);
|
|
1541
1712
|
// A module whose `def()` threw is recorded WITHOUT a path: the import never
|
|
1542
1713
|
// got far enough to reveal one, so it cannot be matched above. Those errors
|
|
@@ -1568,6 +1739,12 @@ class Service {
|
|
|
1568
1739
|
// entry content, or an unsupported nested `.jsonValues()`, as VALID, and CI
|
|
1569
1740
|
// gates on that.
|
|
1570
1741
|
let jsonValuesModuleError;
|
|
1742
|
+
// The source as it is on disk holds a `c.json(...)` marker for every
|
|
1743
|
+
// `.jsonValues()` entry, and validation reports errors at paths INSIDE those
|
|
1744
|
+
// entries. Resolving such a path against the marker source is impossible —
|
|
1745
|
+
// it throws — so the loaded content is substituted back in before anything
|
|
1746
|
+
// downstream (the `val validate --fix` handlers, chiefly) tries.
|
|
1747
|
+
let resolvedFromSource = source;
|
|
1571
1748
|
if (opts.validate) {
|
|
1572
1749
|
const nested = findNestedJsonValuesRecords(serializedSchema);
|
|
1573
1750
|
if (nested.length > 0) {
|
|
@@ -1580,7 +1757,11 @@ class Service {
|
|
|
1580
1757
|
// accepted cost of having no revalidation token (locked decision #3):
|
|
1581
1758
|
// validation is allowed to be slower at scale, but it is not allowed to
|
|
1582
1759
|
// silently skip content.
|
|
1583
|
-
const
|
|
1760
|
+
const {
|
|
1761
|
+
errors: entryErrors,
|
|
1762
|
+
loadedEntries
|
|
1763
|
+
} = await validateJsonValuesEntries(schema, source, moduleFilePath);
|
|
1764
|
+
resolvedFromSource = withLoadedJsonEntries(source, loadedEntries);
|
|
1584
1765
|
if (Object.keys(entryErrors).length > 0) {
|
|
1585
1766
|
// Concatenate per path, never overwrite: an entry written inline is
|
|
1586
1767
|
// reported BOTH by the record-level validation (which checks the
|
|
@@ -1598,7 +1779,7 @@ class Service {
|
|
|
1598
1779
|
}
|
|
1599
1780
|
}
|
|
1600
1781
|
}
|
|
1601
|
-
const resolved = core.Internal.resolvePath(modulePath,
|
|
1782
|
+
const resolved = core.Internal.resolvePath(modulePath, resolvedFromSource, serializedSchema);
|
|
1602
1783
|
const sourcePath = resolved.path ? [moduleFilePath, resolved.path].join(".") : moduleFilePath;
|
|
1603
1784
|
if (!validation && !moduleError && !jsonValuesModuleError) {
|
|
1604
1785
|
return {
|
|
@@ -1629,8 +1810,126 @@ class Service {
|
|
|
1629
1810
|
}
|
|
1630
1811
|
};
|
|
1631
1812
|
}
|
|
1813
|
+
|
|
1814
|
+
/**
|
|
1815
|
+
* Applies a patch to a module's files.
|
|
1816
|
+
*
|
|
1817
|
+
* A `.jsonValues()` entry's content is not in the `.val.ts` — that file holds
|
|
1818
|
+
* only `c.json(() => import("./x.val.json"))` — so an op whose path descends
|
|
1819
|
+
* into an entry has to be replayed against the backing `*.val.json` instead.
|
|
1820
|
+
* That routing is the same one the Studio's commit flow does in
|
|
1821
|
+
* `ValOps.prepare`; both classify with {@link classifyJsonValuesOp} and rebase
|
|
1822
|
+
* with {@link rebaseContentOp}, so a CLI fix and a Studio publish write the
|
|
1823
|
+
* same file the same way.
|
|
1824
|
+
*/
|
|
1632
1825
|
async patch(moduleFilePath, patch) {
|
|
1633
|
-
|
|
1826
|
+
const serializedSchema = this.serializedSchemaOf(moduleFilePath);
|
|
1827
|
+
const valTsOps = [];
|
|
1828
|
+
const entryOps = new Map();
|
|
1829
|
+
const classify = opPath => serializedSchema ? classifyJsonValuesOp(serializedSchema, opPath) : {
|
|
1830
|
+
kind: "normal"
|
|
1831
|
+
};
|
|
1832
|
+
for (const op of patch) {
|
|
1833
|
+
const cls = classify(op.path);
|
|
1834
|
+
// `move` and `copy` read a SECOND path, and `rebaseContentOp` slices
|
|
1835
|
+
// `from` by the same prefix it slices `path` by. So an op whose two ends
|
|
1836
|
+
// are not in the same place is not merely unsupported, it is silently
|
|
1837
|
+
// wrong: a copy INTO an entry from anywhere else gets its `from`
|
|
1838
|
+
// reinterpreted as a path inside the destination's `*.val.json` and reads
|
|
1839
|
+
// the wrong value. `ValOps.prepare` guards this on the publish path; both
|
|
1840
|
+
// ends have to be classified here for the same reason.
|
|
1841
|
+
const fromCls = op.op === "move" || op.op === "copy" ? classify(op.from) : undefined;
|
|
1842
|
+
if (cls.kind === "normal") {
|
|
1843
|
+
// Unreachable while `.jsonValues()` is root-only -- every top-level path
|
|
1844
|
+
// in such a module IS an entry key, so a normal destination means the
|
|
1845
|
+
// module has no jsonValues record and the source cannot be in one
|
|
1846
|
+
// either. Kept because it is the half of the guard that stops being
|
|
1847
|
+
// theoretical the moment root-only is relaxed.
|
|
1848
|
+
if (fromCls && fromCls.kind !== "normal") {
|
|
1849
|
+
throw Error(`Cannot ${op.op} out of the .jsonValues() entry '${fromCls.entryKey}' of ${moduleFilePath}: the entry's content is not in the .val.ts`);
|
|
1850
|
+
}
|
|
1851
|
+
valTsOps.push(op);
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
if (cls.recordPath.length > 0) {
|
|
1855
|
+
// Nested `.jsonValues()` is rejected as a module error before any fix
|
|
1856
|
+
// runs, so reaching here means the contract moved without this moving
|
|
1857
|
+
// with it. Refuse rather than write to the wrong file.
|
|
1858
|
+
throw Error(`Cannot patch a nested .jsonValues() record in ${moduleFilePath}: only a root record/router is supported`);
|
|
1859
|
+
}
|
|
1860
|
+
if (cls.subPath.length === 0) {
|
|
1861
|
+
// Adding, removing or replacing a whole entry means writing the
|
|
1862
|
+
// `*.val.json` AND rewriting the `c.json(...)` reference in the
|
|
1863
|
+
// `.val.ts`. Nothing that reaches `Service.patch` produces those today
|
|
1864
|
+
// (`val validate --fix` only ever corrects values inside an entry), and
|
|
1865
|
+
// silently applying it to the `.val.ts` alone would leave the module
|
|
1866
|
+
// pointing at a file that does not exist.
|
|
1867
|
+
throw Error(`Cannot ${op.op} the whole .jsonValues() entry '${cls.entryKey}' of ${moduleFilePath} through Service.patch: only edits INSIDE an entry are supported`);
|
|
1868
|
+
}
|
|
1869
|
+
if (fromCls && (fromCls.kind !== "entry" || fromCls.entryKey !== cls.entryKey)) {
|
|
1870
|
+
throw Error(`Cannot ${op.op} into the .jsonValues() entry '${cls.entryKey}' of ${moduleFilePath} from outside it: each entry is a separate file`);
|
|
1871
|
+
}
|
|
1872
|
+
const ops = entryOps.get(cls.entryKey);
|
|
1873
|
+
if (ops) {
|
|
1874
|
+
ops.push(op);
|
|
1875
|
+
} else {
|
|
1876
|
+
entryOps.set(cls.entryKey, [op]);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
// When nothing is routed away, hand the patch over untouched: an empty
|
|
1880
|
+
// `valTsOps` would otherwise rewrite the `.val.ts` for no reason.
|
|
1881
|
+
if (entryOps.size === 0) {
|
|
1882
|
+
await patchValFile(moduleFilePath, this.projectRoot, patch, this.sourceFileHandler);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
if (valTsOps.length > 0) {
|
|
1886
|
+
await patchValFile(moduleFilePath, this.projectRoot, valTsOps, this.sourceFileHandler);
|
|
1887
|
+
}
|
|
1888
|
+
for (const [entryKey, ops] of entryOps) {
|
|
1889
|
+
this.patchJsonValuesEntry(moduleFilePath, entryKey, ops);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
/** Replays content ops against one `.jsonValues()` entry's `*.val.json`. */
|
|
1894
|
+
patchJsonValuesEntry(moduleFilePath, entryKey, ops) {
|
|
1895
|
+
const valTsPath = this.sourceFileHandler.resolveSourceModulePath(getSyntheticContainingPath(this.projectRoot), `.${moduleFilePath.replace(".val.ts", ".val").replace(".val.js", ".val").replace(".val.jsx", ".val").replace(".val.tsx", ".val")}`);
|
|
1896
|
+
const sourceFile = this.sourceFileHandler.getSourceFile(valTsPath);
|
|
1897
|
+
if (!sourceFile) {
|
|
1898
|
+
throw Error(`Source file ${valTsPath} not found`);
|
|
1899
|
+
}
|
|
1900
|
+
// The import specifier is authoritative, not the derived path: entries may
|
|
1901
|
+
// be hand-placed (hybrid authoring), so deriving would write a file the
|
|
1902
|
+
// module does not read.
|
|
1903
|
+
const jsonPath = findJsonEntryFilePath(moduleFilePath, sourceFile, entryKey);
|
|
1904
|
+
if (!jsonPath) {
|
|
1905
|
+
throw Error(`Could not find the '*.val.json' backing .jsonValues() entry '${entryKey}' of ${moduleFilePath}`);
|
|
1906
|
+
}
|
|
1907
|
+
const absoluteJsonPath = path__namespace["default"].join(this.projectRoot, jsonPath);
|
|
1908
|
+
const text = this.sourceFileHandler.host.readFile(absoluteJsonPath);
|
|
1909
|
+
if (text === undefined) {
|
|
1910
|
+
throw Error(`Could not read jsonValues entry file: ${jsonPath}`);
|
|
1911
|
+
}
|
|
1912
|
+
let content;
|
|
1913
|
+
try {
|
|
1914
|
+
content = JSON.parse(text);
|
|
1915
|
+
} catch (err) {
|
|
1916
|
+
throw new Error(`Could not parse jsonValues entry file ${jsonPath}`, {
|
|
1917
|
+
cause: err
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
for (const op of ops) {
|
|
1921
|
+
// Root record / router only, so the prefix is exactly the entry key.
|
|
1922
|
+
const rebased = rebaseContentOp(op, 1);
|
|
1923
|
+
if (fp.result.isErr(rebased)) {
|
|
1924
|
+
throw Error(`Could not apply ${op.op} to jsonValues entry '${entryKey}' of ${moduleFilePath}: ${rebased.error.message}`);
|
|
1925
|
+
}
|
|
1926
|
+
const applied = patch.applyPatch(patch.deepClone(content), jsonOps$1, [rebased.value]);
|
|
1927
|
+
if (fp.result.isErr(applied)) {
|
|
1928
|
+
throw Error(`Could not apply ${op.op} to ${jsonPath}: ${applied.error.message}`);
|
|
1929
|
+
}
|
|
1930
|
+
content = applied.value;
|
|
1931
|
+
}
|
|
1932
|
+
this.sourceFileHandler.writeFile(absoluteJsonPath, JSON.stringify(content, null, 2) + "\n", "utf8");
|
|
1634
1933
|
}
|
|
1635
1934
|
dispose() {
|
|
1636
1935
|
// No-op: the vm-based loader holds no disposable resources.
|
|
@@ -1695,92 +1994,6 @@ function encodeJwt(payload, sessionKey) {
|
|
|
1695
1994
|
return `${jwtHeaderBase64}.${payloadBase64}.${crypto__default["default"].createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
|
|
1696
1995
|
}
|
|
1697
1996
|
|
|
1698
|
-
/**
|
|
1699
|
-
* Given the `source` object-literal expression of a `.jsonValues()` record /
|
|
1700
|
-
* router (the 3rd argument to `c.define`, via {@link analyzeValModule}),
|
|
1701
|
-
* extracts each entry's `c.json(() => import("..."))` import path, keyed by
|
|
1702
|
-
* entry key. Entries whose value is not a `c.json(...)` call are skipped.
|
|
1703
|
-
*/
|
|
1704
|
-
function analyzeJsonValuesEntries(source) {
|
|
1705
|
-
const entries = new Map();
|
|
1706
|
-
if (!ts__default["default"].isObjectLiteralExpression(source)) {
|
|
1707
|
-
return entries;
|
|
1708
|
-
}
|
|
1709
|
-
for (const prop of source.properties) {
|
|
1710
|
-
if (!ts__default["default"].isPropertyAssignment(prop)) {
|
|
1711
|
-
continue;
|
|
1712
|
-
}
|
|
1713
|
-
const key = getPropertyKey(prop.name);
|
|
1714
|
-
if (key === null) {
|
|
1715
|
-
continue;
|
|
1716
|
-
}
|
|
1717
|
-
const entry = analyzeCJsonCall(prop.initializer);
|
|
1718
|
-
if (entry) {
|
|
1719
|
-
entries.set(key, entry);
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
return entries;
|
|
1723
|
-
}
|
|
1724
|
-
function getPropertyKey(name) {
|
|
1725
|
-
if (ts__default["default"].isStringLiteralLike(name)) {
|
|
1726
|
-
return name.text;
|
|
1727
|
-
}
|
|
1728
|
-
if (ts__default["default"].isIdentifier(name)) {
|
|
1729
|
-
return name.text;
|
|
1730
|
-
}
|
|
1731
|
-
return null;
|
|
1732
|
-
}
|
|
1733
|
-
function analyzeCJsonCall(node) {
|
|
1734
|
-
// c.json(() => import("path"))
|
|
1735
|
-
if (!ts__default["default"].isCallExpression(node) || !isCJson(node.expression)) {
|
|
1736
|
-
return null;
|
|
1737
|
-
}
|
|
1738
|
-
const [thunk] = node.arguments;
|
|
1739
|
-
if (!thunk) {
|
|
1740
|
-
return null;
|
|
1741
|
-
}
|
|
1742
|
-
const importPath = getImportPathFromThunk(thunk);
|
|
1743
|
-
if (importPath === null) {
|
|
1744
|
-
return null;
|
|
1745
|
-
}
|
|
1746
|
-
return {
|
|
1747
|
-
importPath
|
|
1748
|
-
};
|
|
1749
|
-
}
|
|
1750
|
-
function isCJson(expr) {
|
|
1751
|
-
// c.json
|
|
1752
|
-
return ts__default["default"].isPropertyAccessExpression(expr) && ts__default["default"].isIdentifier(expr.expression) && expr.expression.text === "c" && ts__default["default"].isIdentifier(expr.name) && expr.name.text === "json";
|
|
1753
|
-
}
|
|
1754
|
-
function getImportPathFromThunk(thunk) {
|
|
1755
|
-
// () => import("path") (concise body) OR () => { return import("path"); }
|
|
1756
|
-
if (!ts__default["default"].isArrowFunction(thunk)) {
|
|
1757
|
-
return null;
|
|
1758
|
-
}
|
|
1759
|
-
let importCall;
|
|
1760
|
-
if (ts__default["default"].isCallExpression(thunk.body)) {
|
|
1761
|
-
importCall = thunk.body;
|
|
1762
|
-
} else if (ts__default["default"].isBlock(thunk.body)) {
|
|
1763
|
-
for (const stmt of thunk.body.statements) {
|
|
1764
|
-
if (ts__default["default"].isReturnStatement(stmt) && stmt.expression && ts__default["default"].isCallExpression(stmt.expression)) {
|
|
1765
|
-
importCall = stmt.expression;
|
|
1766
|
-
break;
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
if (!importCall || !ts__default["default"].isCallExpression(importCall)) {
|
|
1771
|
-
return null;
|
|
1772
|
-
}
|
|
1773
|
-
const isImport = importCall.expression.kind === ts__default["default"].SyntaxKind.ImportKeyword || ts__default["default"].isIdentifier(importCall.expression) && importCall.expression.text === "import";
|
|
1774
|
-
if (!isImport) {
|
|
1775
|
-
return null;
|
|
1776
|
-
}
|
|
1777
|
-
const arg = importCall.arguments[0];
|
|
1778
|
-
if (arg && ts__default["default"].isStringLiteralLike(arg)) {
|
|
1779
|
-
return arg.text;
|
|
1780
|
-
}
|
|
1781
|
-
return null;
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
1997
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
1785
1998
|
const jsonOps = new patch.JSONOps();
|
|
1786
1999
|
const tsOps = new TSOps(document => {
|
|
@@ -2655,7 +2868,9 @@ class ValOps {
|
|
|
2655
2868
|
const res = schema["executeValidate"](path, source);
|
|
2656
2869
|
// For `.jsonValues()` records, executeValidate only checks the entry
|
|
2657
2870
|
// markers; load + validate each entry's backing `*.val.json` content here.
|
|
2658
|
-
const
|
|
2871
|
+
const {
|
|
2872
|
+
errors: jsonValuesErrors
|
|
2873
|
+
} = await validateJsonValuesEntries(schema, source, path);
|
|
2659
2874
|
for (const [sourcePathS, entryErrors] of Object.entries(jsonValuesErrors)) {
|
|
2660
2875
|
const sourcePath = sourcePathS;
|
|
2661
2876
|
if (!errors[path]) {
|
|
@@ -12520,38 +12735,6 @@ function traverseObjectLiteral(node, sourceFile) {
|
|
|
12520
12735
|
}, {});
|
|
12521
12736
|
}
|
|
12522
12737
|
|
|
12523
|
-
/**
|
|
12524
|
-
* Which `*.val.json` holds a `.jsonValues()` entry's content, given the module's
|
|
12525
|
-
* `.val.ts`.
|
|
12526
|
-
*
|
|
12527
|
-
* Tooling that maps a validation `sourcePath` back to a place in a file needs
|
|
12528
|
-
* this: for a jsonValues module the offending value is NOT in the `.val.ts` at
|
|
12529
|
-
* all — that file only holds `c.json(() => import("./x.val.json"))` — so a
|
|
12530
|
-
* resolver that only ever looks at the `.val.ts` can report the error but not
|
|
12531
|
-
* where it lives, which for a record with hundreds of entries is not much of a
|
|
12532
|
-
* report.
|
|
12533
|
-
*
|
|
12534
|
-
* Returns a path relative to the project root (leading slash), or undefined when
|
|
12535
|
-
* the module has no such entry (not a jsonValues module, unparseable, or the key
|
|
12536
|
-
* is not backed by a `c.json` thunk).
|
|
12537
|
-
*/
|
|
12538
|
-
function findJsonEntryFilePath(moduleFilePath, valTsSourceFile, entryKey) {
|
|
12539
|
-
let analysis;
|
|
12540
|
-
try {
|
|
12541
|
-
analysis = analyzeValModule(valTsSourceFile);
|
|
12542
|
-
} catch {
|
|
12543
|
-
return undefined;
|
|
12544
|
-
}
|
|
12545
|
-
if (fp.result.isErr(analysis)) {
|
|
12546
|
-
return undefined;
|
|
12547
|
-
}
|
|
12548
|
-
const entry = analyzeJsonValuesEntries(analysis.value.source).get(entryKey);
|
|
12549
|
-
if (!entry) {
|
|
12550
|
-
return undefined;
|
|
12551
|
-
}
|
|
12552
|
-
return resolveExistingJsonPath(moduleFilePath, entry.importPath);
|
|
12553
|
-
}
|
|
12554
|
-
|
|
12555
12738
|
/**
|
|
12556
12739
|
* Replays a `val debug` snapshot: applies its patches the way /save does and
|
|
12557
12740
|
* validates the result.
|
|
@@ -12668,6 +12851,7 @@ exports.ValSourceFileHandler = ValSourceFileHandler;
|
|
|
12668
12851
|
exports.analyzeValModule = analyzeValModule;
|
|
12669
12852
|
exports.awaitValLoginConfirmation = awaitValLoginConfirmation;
|
|
12670
12853
|
exports.checkRemoteRef = checkRemoteRef;
|
|
12854
|
+
exports.classifyJsonValuesOp = classifyJsonValuesOp;
|
|
12671
12855
|
exports.compareWithCapturedReport = compareWithCapturedReport;
|
|
12672
12856
|
exports.createDefaultValFSHost = createDefaultValFSHost;
|
|
12673
12857
|
exports.createFixPatch = createFixPatch;
|
|
@@ -12714,6 +12898,7 @@ exports.patchSourceFile = patchSourceFile;
|
|
|
12714
12898
|
exports.persistPersonalAccessToken = persistPersonalAccessToken;
|
|
12715
12899
|
exports.readCapturedReport = readCapturedReport;
|
|
12716
12900
|
exports.readPatchStore = readPatchStore;
|
|
12901
|
+
exports.rebaseContentOp = rebaseContentOp;
|
|
12717
12902
|
exports.replaySnapshot = replaySnapshot;
|
|
12718
12903
|
exports.safeReadGit = safeReadGit;
|
|
12719
12904
|
exports.startValLogin = startValLogin;
|