@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
|
@@ -1006,7 +1006,7 @@ function errorMessage(e) {
|
|
|
1006
1006
|
return String(e);
|
|
1007
1007
|
}
|
|
1008
1008
|
|
|
1009
|
-
const jsonOps$
|
|
1009
|
+
const jsonOps$2 = new JSONOps();
|
|
1010
1010
|
|
|
1011
1011
|
/**
|
|
1012
1012
|
* Classification of a single patch op against a module's serialized schema,
|
|
@@ -1261,7 +1261,7 @@ function applyJsonValuesEntryPatches(args) {
|
|
|
1261
1261
|
// on an entry that does not exist.
|
|
1262
1262
|
continue;
|
|
1263
1263
|
}
|
|
1264
|
-
const applied = applyPatch(deepClone(content), jsonOps$
|
|
1264
|
+
const applied = applyPatch(deepClone(content), jsonOps$2, [{
|
|
1265
1265
|
op: "add",
|
|
1266
1266
|
path: cls.subPath.concat(...(op.nestedFilePath ?? [])).concat("patch_id"),
|
|
1267
1267
|
value: patchId
|
|
@@ -1315,7 +1315,7 @@ function applyJsonValuesEntryPatches(args) {
|
|
|
1315
1315
|
patchId
|
|
1316
1316
|
};
|
|
1317
1317
|
}
|
|
1318
|
-
const applied = applyPatch(deepClone(content), jsonOps$
|
|
1318
|
+
const applied = applyPatch(deepClone(content), jsonOps$2, [rebased.value]);
|
|
1319
1319
|
if (result.isErr(applied)) {
|
|
1320
1320
|
return {
|
|
1321
1321
|
kind: "error",
|
|
@@ -1353,6 +1353,19 @@ function resolveExistingJsonPath(moduleFilePath, importPath) {
|
|
|
1353
1353
|
return path.posix.join(moduleDir, importPath);
|
|
1354
1354
|
}
|
|
1355
1355
|
|
|
1356
|
+
/**
|
|
1357
|
+
* The outcome of {@link validateJsonValuesEntries}: the errors found inside the
|
|
1358
|
+
* entries, plus the content of every entry that could be loaded, keyed by entry
|
|
1359
|
+
* key.
|
|
1360
|
+
*
|
|
1361
|
+
* The content is part of the result because the errors are useless without it.
|
|
1362
|
+
* They are reported at paths that point INSIDE an entry, and the module source
|
|
1363
|
+
* on disk holds a `c.json(...)` marker there — so anything that has to resolve
|
|
1364
|
+
* one of those paths (the `val validate --fix` handlers, chiefly) needs the very
|
|
1365
|
+
* content this function has just loaded. Returning it costs nothing and is the
|
|
1366
|
+
* only way to avoid loading every entry twice.
|
|
1367
|
+
*/
|
|
1368
|
+
|
|
1356
1369
|
/**
|
|
1357
1370
|
* Validates the content of every `.jsonValues()` entry in a module by loading
|
|
1358
1371
|
* each backing `*.val.json` (via its lazy import thunk) and checking it against
|
|
@@ -1405,14 +1418,19 @@ function resolveExistingJsonPath(moduleFilePath, importPath) {
|
|
|
1405
1418
|
*/
|
|
1406
1419
|
async function validateJsonValuesEntries(schema, source, modulePath) {
|
|
1407
1420
|
const out = {};
|
|
1421
|
+
const loadedEntries = {};
|
|
1422
|
+
const res = {
|
|
1423
|
+
errors: out,
|
|
1424
|
+
loadedEntries
|
|
1425
|
+
};
|
|
1408
1426
|
if (!(schema instanceof RecordSchema)) {
|
|
1409
|
-
return
|
|
1427
|
+
return res;
|
|
1410
1428
|
}
|
|
1411
1429
|
if (!schema["executeSerialize"]().jsonValues) {
|
|
1412
|
-
return
|
|
1430
|
+
return res;
|
|
1413
1431
|
}
|
|
1414
1432
|
if (source === null || typeof source !== "object" || Array.isArray(source)) {
|
|
1415
|
-
return
|
|
1433
|
+
return res;
|
|
1416
1434
|
}
|
|
1417
1435
|
for (const [key, marker] of Object.entries(source)) {
|
|
1418
1436
|
const entryPath = Internal.createValPathOfItem(modulePath, key);
|
|
@@ -1448,6 +1466,10 @@ async function validateJsonValuesEntries(schema, source, modulePath) {
|
|
|
1448
1466
|
}];
|
|
1449
1467
|
continue;
|
|
1450
1468
|
}
|
|
1469
|
+
// Kept so a caller can substitute the content back into the module source:
|
|
1470
|
+
// the entries have been paid for here, and a source that still holds
|
|
1471
|
+
// markers cannot resolve a path INTO an entry (see the doc comment above).
|
|
1472
|
+
loadedEntries[key] = content;
|
|
1451
1473
|
const entryErrors = schema.validateJsonEntryContent(entryPath, content);
|
|
1452
1474
|
if (entryErrors) {
|
|
1453
1475
|
for (const [p, errs] of Object.entries(entryErrors)) {
|
|
@@ -1455,9 +1477,155 @@ async function validateJsonValuesEntries(schema, source, modulePath) {
|
|
|
1455
1477
|
}
|
|
1456
1478
|
}
|
|
1457
1479
|
}
|
|
1458
|
-
return
|
|
1480
|
+
return res;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
/**
|
|
1484
|
+
* Given the `source` object-literal expression of a `.jsonValues()` record /
|
|
1485
|
+
* router (the 3rd argument to `c.define`, via {@link analyzeValModule}),
|
|
1486
|
+
* extracts each entry's `c.json(() => import("..."))` import path, keyed by
|
|
1487
|
+
* entry key. Entries whose value is not a `c.json(...)` call are skipped.
|
|
1488
|
+
*/
|
|
1489
|
+
function analyzeJsonValuesEntries(source) {
|
|
1490
|
+
const entries = new Map();
|
|
1491
|
+
if (!ts.isObjectLiteralExpression(source)) {
|
|
1492
|
+
return entries;
|
|
1493
|
+
}
|
|
1494
|
+
for (const prop of source.properties) {
|
|
1495
|
+
if (!ts.isPropertyAssignment(prop)) {
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
const key = getPropertyKey(prop.name);
|
|
1499
|
+
if (key === null) {
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
const entry = analyzeCJsonCall(prop.initializer);
|
|
1503
|
+
if (entry) {
|
|
1504
|
+
entries.set(key, entry);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
return entries;
|
|
1508
|
+
}
|
|
1509
|
+
function getPropertyKey(name) {
|
|
1510
|
+
if (ts.isStringLiteralLike(name)) {
|
|
1511
|
+
return name.text;
|
|
1512
|
+
}
|
|
1513
|
+
if (ts.isIdentifier(name)) {
|
|
1514
|
+
return name.text;
|
|
1515
|
+
}
|
|
1516
|
+
return null;
|
|
1517
|
+
}
|
|
1518
|
+
function analyzeCJsonCall(node) {
|
|
1519
|
+
// c.json(() => import("path"))
|
|
1520
|
+
if (!ts.isCallExpression(node) || !isCJson(node.expression)) {
|
|
1521
|
+
return null;
|
|
1522
|
+
}
|
|
1523
|
+
const [thunk] = node.arguments;
|
|
1524
|
+
if (!thunk) {
|
|
1525
|
+
return null;
|
|
1526
|
+
}
|
|
1527
|
+
const importPath = getImportPathFromThunk(thunk);
|
|
1528
|
+
if (importPath === null) {
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
return {
|
|
1532
|
+
importPath
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
function isCJson(expr) {
|
|
1536
|
+
// c.json
|
|
1537
|
+
return ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === "c" && ts.isIdentifier(expr.name) && expr.name.text === "json";
|
|
1538
|
+
}
|
|
1539
|
+
function getImportPathFromThunk(thunk) {
|
|
1540
|
+
// () => import("path") (concise body) OR () => { return import("path"); }
|
|
1541
|
+
if (!ts.isArrowFunction(thunk)) {
|
|
1542
|
+
return null;
|
|
1543
|
+
}
|
|
1544
|
+
let importCall;
|
|
1545
|
+
if (ts.isCallExpression(thunk.body)) {
|
|
1546
|
+
importCall = thunk.body;
|
|
1547
|
+
} else if (ts.isBlock(thunk.body)) {
|
|
1548
|
+
for (const stmt of thunk.body.statements) {
|
|
1549
|
+
if (ts.isReturnStatement(stmt) && stmt.expression && ts.isCallExpression(stmt.expression)) {
|
|
1550
|
+
importCall = stmt.expression;
|
|
1551
|
+
break;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
if (!importCall || !ts.isCallExpression(importCall)) {
|
|
1556
|
+
return null;
|
|
1557
|
+
}
|
|
1558
|
+
const isImport = importCall.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(importCall.expression) && importCall.expression.text === "import";
|
|
1559
|
+
if (!isImport) {
|
|
1560
|
+
return null;
|
|
1561
|
+
}
|
|
1562
|
+
const arg = importCall.arguments[0];
|
|
1563
|
+
if (arg && ts.isStringLiteralLike(arg)) {
|
|
1564
|
+
return arg.text;
|
|
1565
|
+
}
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/**
|
|
1570
|
+
* Which `*.val.json` holds a `.jsonValues()` entry's content, given the module's
|
|
1571
|
+
* `.val.ts`.
|
|
1572
|
+
*
|
|
1573
|
+
* Tooling that maps a validation `sourcePath` back to a place in a file needs
|
|
1574
|
+
* this: for a jsonValues module the offending value is NOT in the `.val.ts` at
|
|
1575
|
+
* all — that file only holds `c.json(() => import("./x.val.json"))` — so a
|
|
1576
|
+
* resolver that only ever looks at the `.val.ts` can report the error but not
|
|
1577
|
+
* where it lives, which for a record with hundreds of entries is not much of a
|
|
1578
|
+
* report.
|
|
1579
|
+
*
|
|
1580
|
+
* Returns a path relative to the project root (leading slash), or undefined when
|
|
1581
|
+
* the module has no such entry (not a jsonValues module, unparseable, or the key
|
|
1582
|
+
* is not backed by a `c.json` thunk).
|
|
1583
|
+
*/
|
|
1584
|
+
function findJsonEntryFilePath(moduleFilePath, valTsSourceFile, entryKey) {
|
|
1585
|
+
let analysis;
|
|
1586
|
+
try {
|
|
1587
|
+
analysis = analyzeValModule(valTsSourceFile);
|
|
1588
|
+
} catch {
|
|
1589
|
+
return undefined;
|
|
1590
|
+
}
|
|
1591
|
+
if (result.isErr(analysis)) {
|
|
1592
|
+
return undefined;
|
|
1593
|
+
}
|
|
1594
|
+
const entry = analyzeJsonValuesEntries(analysis.value.source).get(entryKey);
|
|
1595
|
+
if (!entry) {
|
|
1596
|
+
return undefined;
|
|
1597
|
+
}
|
|
1598
|
+
return resolveExistingJsonPath(moduleFilePath, entry.importPath);
|
|
1459
1599
|
}
|
|
1460
1600
|
|
|
1601
|
+
const jsonOps$1 = new JSONOps();
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* Substitutes loaded `.jsonValues()` entry content back into a module's root
|
|
1605
|
+
* record, in place of the `c.json(...)` markers the `.val.ts` holds.
|
|
1606
|
+
*
|
|
1607
|
+
* Only entries that actually loaded are replaced: an entry whose file is missing
|
|
1608
|
+
* or unparseable keeps its marker, so the failure stays visible as the
|
|
1609
|
+
* validation error it already produced rather than becoming a silent `undefined`
|
|
1610
|
+
* further down.
|
|
1611
|
+
*/
|
|
1612
|
+
function withLoadedJsonEntries(source, loadedEntries) {
|
|
1613
|
+
if (Object.keys(loadedEntries).length === 0) {
|
|
1614
|
+
return source;
|
|
1615
|
+
}
|
|
1616
|
+
if (!isSourceObject(source)) {
|
|
1617
|
+
return source;
|
|
1618
|
+
}
|
|
1619
|
+
return {
|
|
1620
|
+
...source,
|
|
1621
|
+
...loadedEntries
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/** A non-null, non-array source object: the shape a record/router source has. */
|
|
1626
|
+
function isSourceObject(source) {
|
|
1627
|
+
return typeof source === "object" && source !== null && !Array.isArray(source);
|
|
1628
|
+
}
|
|
1461
1629
|
async function createService(projectRoot, host = {
|
|
1462
1630
|
...ts.sys,
|
|
1463
1631
|
writeFile: (fileName, data, encoding) => {
|
|
@@ -1496,13 +1664,16 @@ class Service {
|
|
|
1496
1664
|
getModuleFilePaths() {
|
|
1497
1665
|
return Object.keys(this.extracted.sources);
|
|
1498
1666
|
}
|
|
1667
|
+
serializedSchemaOf(moduleFilePath) {
|
|
1668
|
+
return this.extracted.serializedSchemas[moduleFilePath];
|
|
1669
|
+
}
|
|
1499
1670
|
async get(moduleFilePath, modulePath, options) {
|
|
1500
1671
|
const opts = options ?? {
|
|
1501
1672
|
validate: true
|
|
1502
1673
|
};
|
|
1503
1674
|
const source = this.extracted.sources[moduleFilePath];
|
|
1504
1675
|
const schema = this.extracted.schemas[moduleFilePath];
|
|
1505
|
-
const serializedSchema = this.
|
|
1676
|
+
const serializedSchema = this.serializedSchemaOf(moduleFilePath);
|
|
1506
1677
|
const moduleError = this.extracted.moduleErrors.find(e => e.path === moduleFilePath);
|
|
1507
1678
|
// A module whose `def()` threw is recorded WITHOUT a path: the import never
|
|
1508
1679
|
// got far enough to reveal one, so it cannot be matched above. Those errors
|
|
@@ -1534,6 +1705,12 @@ class Service {
|
|
|
1534
1705
|
// entry content, or an unsupported nested `.jsonValues()`, as VALID, and CI
|
|
1535
1706
|
// gates on that.
|
|
1536
1707
|
let jsonValuesModuleError;
|
|
1708
|
+
// The source as it is on disk holds a `c.json(...)` marker for every
|
|
1709
|
+
// `.jsonValues()` entry, and validation reports errors at paths INSIDE those
|
|
1710
|
+
// entries. Resolving such a path against the marker source is impossible —
|
|
1711
|
+
// it throws — so the loaded content is substituted back in before anything
|
|
1712
|
+
// downstream (the `val validate --fix` handlers, chiefly) tries.
|
|
1713
|
+
let resolvedFromSource = source;
|
|
1537
1714
|
if (opts.validate) {
|
|
1538
1715
|
const nested = findNestedJsonValuesRecords(serializedSchema);
|
|
1539
1716
|
if (nested.length > 0) {
|
|
@@ -1546,7 +1723,11 @@ class Service {
|
|
|
1546
1723
|
// accepted cost of having no revalidation token (locked decision #3):
|
|
1547
1724
|
// validation is allowed to be slower at scale, but it is not allowed to
|
|
1548
1725
|
// silently skip content.
|
|
1549
|
-
const
|
|
1726
|
+
const {
|
|
1727
|
+
errors: entryErrors,
|
|
1728
|
+
loadedEntries
|
|
1729
|
+
} = await validateJsonValuesEntries(schema, source, moduleFilePath);
|
|
1730
|
+
resolvedFromSource = withLoadedJsonEntries(source, loadedEntries);
|
|
1550
1731
|
if (Object.keys(entryErrors).length > 0) {
|
|
1551
1732
|
// Concatenate per path, never overwrite: an entry written inline is
|
|
1552
1733
|
// reported BOTH by the record-level validation (which checks the
|
|
@@ -1564,7 +1745,7 @@ class Service {
|
|
|
1564
1745
|
}
|
|
1565
1746
|
}
|
|
1566
1747
|
}
|
|
1567
|
-
const resolved = Internal.resolvePath(modulePath,
|
|
1748
|
+
const resolved = Internal.resolvePath(modulePath, resolvedFromSource, serializedSchema);
|
|
1568
1749
|
const sourcePath = resolved.path ? [moduleFilePath, resolved.path].join(".") : moduleFilePath;
|
|
1569
1750
|
if (!validation && !moduleError && !jsonValuesModuleError) {
|
|
1570
1751
|
return {
|
|
@@ -1595,8 +1776,126 @@ class Service {
|
|
|
1595
1776
|
}
|
|
1596
1777
|
};
|
|
1597
1778
|
}
|
|
1779
|
+
|
|
1780
|
+
/**
|
|
1781
|
+
* Applies a patch to a module's files.
|
|
1782
|
+
*
|
|
1783
|
+
* A `.jsonValues()` entry's content is not in the `.val.ts` — that file holds
|
|
1784
|
+
* only `c.json(() => import("./x.val.json"))` — so an op whose path descends
|
|
1785
|
+
* into an entry has to be replayed against the backing `*.val.json` instead.
|
|
1786
|
+
* That routing is the same one the Studio's commit flow does in
|
|
1787
|
+
* `ValOps.prepare`; both classify with {@link classifyJsonValuesOp} and rebase
|
|
1788
|
+
* with {@link rebaseContentOp}, so a CLI fix and a Studio publish write the
|
|
1789
|
+
* same file the same way.
|
|
1790
|
+
*/
|
|
1598
1791
|
async patch(moduleFilePath, patch) {
|
|
1599
|
-
|
|
1792
|
+
const serializedSchema = this.serializedSchemaOf(moduleFilePath);
|
|
1793
|
+
const valTsOps = [];
|
|
1794
|
+
const entryOps = new Map();
|
|
1795
|
+
const classify = opPath => serializedSchema ? classifyJsonValuesOp(serializedSchema, opPath) : {
|
|
1796
|
+
kind: "normal"
|
|
1797
|
+
};
|
|
1798
|
+
for (const op of patch) {
|
|
1799
|
+
const cls = classify(op.path);
|
|
1800
|
+
// `move` and `copy` read a SECOND path, and `rebaseContentOp` slices
|
|
1801
|
+
// `from` by the same prefix it slices `path` by. So an op whose two ends
|
|
1802
|
+
// are not in the same place is not merely unsupported, it is silently
|
|
1803
|
+
// wrong: a copy INTO an entry from anywhere else gets its `from`
|
|
1804
|
+
// reinterpreted as a path inside the destination's `*.val.json` and reads
|
|
1805
|
+
// the wrong value. `ValOps.prepare` guards this on the publish path; both
|
|
1806
|
+
// ends have to be classified here for the same reason.
|
|
1807
|
+
const fromCls = op.op === "move" || op.op === "copy" ? classify(op.from) : undefined;
|
|
1808
|
+
if (cls.kind === "normal") {
|
|
1809
|
+
// Unreachable while `.jsonValues()` is root-only -- every top-level path
|
|
1810
|
+
// in such a module IS an entry key, so a normal destination means the
|
|
1811
|
+
// module has no jsonValues record and the source cannot be in one
|
|
1812
|
+
// either. Kept because it is the half of the guard that stops being
|
|
1813
|
+
// theoretical the moment root-only is relaxed.
|
|
1814
|
+
if (fromCls && fromCls.kind !== "normal") {
|
|
1815
|
+
throw Error(`Cannot ${op.op} out of the .jsonValues() entry '${fromCls.entryKey}' of ${moduleFilePath}: the entry's content is not in the .val.ts`);
|
|
1816
|
+
}
|
|
1817
|
+
valTsOps.push(op);
|
|
1818
|
+
continue;
|
|
1819
|
+
}
|
|
1820
|
+
if (cls.recordPath.length > 0) {
|
|
1821
|
+
// Nested `.jsonValues()` is rejected as a module error before any fix
|
|
1822
|
+
// runs, so reaching here means the contract moved without this moving
|
|
1823
|
+
// with it. Refuse rather than write to the wrong file.
|
|
1824
|
+
throw Error(`Cannot patch a nested .jsonValues() record in ${moduleFilePath}: only a root record/router is supported`);
|
|
1825
|
+
}
|
|
1826
|
+
if (cls.subPath.length === 0) {
|
|
1827
|
+
// Adding, removing or replacing a whole entry means writing the
|
|
1828
|
+
// `*.val.json` AND rewriting the `c.json(...)` reference in the
|
|
1829
|
+
// `.val.ts`. Nothing that reaches `Service.patch` produces those today
|
|
1830
|
+
// (`val validate --fix` only ever corrects values inside an entry), and
|
|
1831
|
+
// silently applying it to the `.val.ts` alone would leave the module
|
|
1832
|
+
// pointing at a file that does not exist.
|
|
1833
|
+
throw Error(`Cannot ${op.op} the whole .jsonValues() entry '${cls.entryKey}' of ${moduleFilePath} through Service.patch: only edits INSIDE an entry are supported`);
|
|
1834
|
+
}
|
|
1835
|
+
if (fromCls && (fromCls.kind !== "entry" || fromCls.entryKey !== cls.entryKey)) {
|
|
1836
|
+
throw Error(`Cannot ${op.op} into the .jsonValues() entry '${cls.entryKey}' of ${moduleFilePath} from outside it: each entry is a separate file`);
|
|
1837
|
+
}
|
|
1838
|
+
const ops = entryOps.get(cls.entryKey);
|
|
1839
|
+
if (ops) {
|
|
1840
|
+
ops.push(op);
|
|
1841
|
+
} else {
|
|
1842
|
+
entryOps.set(cls.entryKey, [op]);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
// When nothing is routed away, hand the patch over untouched: an empty
|
|
1846
|
+
// `valTsOps` would otherwise rewrite the `.val.ts` for no reason.
|
|
1847
|
+
if (entryOps.size === 0) {
|
|
1848
|
+
await patchValFile(moduleFilePath, this.projectRoot, patch, this.sourceFileHandler);
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
if (valTsOps.length > 0) {
|
|
1852
|
+
await patchValFile(moduleFilePath, this.projectRoot, valTsOps, this.sourceFileHandler);
|
|
1853
|
+
}
|
|
1854
|
+
for (const [entryKey, ops] of entryOps) {
|
|
1855
|
+
this.patchJsonValuesEntry(moduleFilePath, entryKey, ops);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/** Replays content ops against one `.jsonValues()` entry's `*.val.json`. */
|
|
1860
|
+
patchJsonValuesEntry(moduleFilePath, entryKey, ops) {
|
|
1861
|
+
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")}`);
|
|
1862
|
+
const sourceFile = this.sourceFileHandler.getSourceFile(valTsPath);
|
|
1863
|
+
if (!sourceFile) {
|
|
1864
|
+
throw Error(`Source file ${valTsPath} not found`);
|
|
1865
|
+
}
|
|
1866
|
+
// The import specifier is authoritative, not the derived path: entries may
|
|
1867
|
+
// be hand-placed (hybrid authoring), so deriving would write a file the
|
|
1868
|
+
// module does not read.
|
|
1869
|
+
const jsonPath = findJsonEntryFilePath(moduleFilePath, sourceFile, entryKey);
|
|
1870
|
+
if (!jsonPath) {
|
|
1871
|
+
throw Error(`Could not find the '*.val.json' backing .jsonValues() entry '${entryKey}' of ${moduleFilePath}`);
|
|
1872
|
+
}
|
|
1873
|
+
const absoluteJsonPath = path__default.join(this.projectRoot, jsonPath);
|
|
1874
|
+
const text = this.sourceFileHandler.host.readFile(absoluteJsonPath);
|
|
1875
|
+
if (text === undefined) {
|
|
1876
|
+
throw Error(`Could not read jsonValues entry file: ${jsonPath}`);
|
|
1877
|
+
}
|
|
1878
|
+
let content;
|
|
1879
|
+
try {
|
|
1880
|
+
content = JSON.parse(text);
|
|
1881
|
+
} catch (err) {
|
|
1882
|
+
throw new Error(`Could not parse jsonValues entry file ${jsonPath}`, {
|
|
1883
|
+
cause: err
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
for (const op of ops) {
|
|
1887
|
+
// Root record / router only, so the prefix is exactly the entry key.
|
|
1888
|
+
const rebased = rebaseContentOp(op, 1);
|
|
1889
|
+
if (result.isErr(rebased)) {
|
|
1890
|
+
throw Error(`Could not apply ${op.op} to jsonValues entry '${entryKey}' of ${moduleFilePath}: ${rebased.error.message}`);
|
|
1891
|
+
}
|
|
1892
|
+
const applied = applyPatch(deepClone(content), jsonOps$1, [rebased.value]);
|
|
1893
|
+
if (result.isErr(applied)) {
|
|
1894
|
+
throw Error(`Could not apply ${op.op} to ${jsonPath}: ${applied.error.message}`);
|
|
1895
|
+
}
|
|
1896
|
+
content = applied.value;
|
|
1897
|
+
}
|
|
1898
|
+
this.sourceFileHandler.writeFile(absoluteJsonPath, JSON.stringify(content, null, 2) + "\n", "utf8");
|
|
1600
1899
|
}
|
|
1601
1900
|
dispose() {
|
|
1602
1901
|
// No-op: the vm-based loader holds no disposable resources.
|
|
@@ -1661,92 +1960,6 @@ function encodeJwt(payload, sessionKey) {
|
|
|
1661
1960
|
return `${jwtHeaderBase64}.${payloadBase64}.${crypto$1.createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
|
|
1662
1961
|
}
|
|
1663
1962
|
|
|
1664
|
-
/**
|
|
1665
|
-
* Given the `source` object-literal expression of a `.jsonValues()` record /
|
|
1666
|
-
* router (the 3rd argument to `c.define`, via {@link analyzeValModule}),
|
|
1667
|
-
* extracts each entry's `c.json(() => import("..."))` import path, keyed by
|
|
1668
|
-
* entry key. Entries whose value is not a `c.json(...)` call are skipped.
|
|
1669
|
-
*/
|
|
1670
|
-
function analyzeJsonValuesEntries(source) {
|
|
1671
|
-
const entries = new Map();
|
|
1672
|
-
if (!ts.isObjectLiteralExpression(source)) {
|
|
1673
|
-
return entries;
|
|
1674
|
-
}
|
|
1675
|
-
for (const prop of source.properties) {
|
|
1676
|
-
if (!ts.isPropertyAssignment(prop)) {
|
|
1677
|
-
continue;
|
|
1678
|
-
}
|
|
1679
|
-
const key = getPropertyKey(prop.name);
|
|
1680
|
-
if (key === null) {
|
|
1681
|
-
continue;
|
|
1682
|
-
}
|
|
1683
|
-
const entry = analyzeCJsonCall(prop.initializer);
|
|
1684
|
-
if (entry) {
|
|
1685
|
-
entries.set(key, entry);
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
return entries;
|
|
1689
|
-
}
|
|
1690
|
-
function getPropertyKey(name) {
|
|
1691
|
-
if (ts.isStringLiteralLike(name)) {
|
|
1692
|
-
return name.text;
|
|
1693
|
-
}
|
|
1694
|
-
if (ts.isIdentifier(name)) {
|
|
1695
|
-
return name.text;
|
|
1696
|
-
}
|
|
1697
|
-
return null;
|
|
1698
|
-
}
|
|
1699
|
-
function analyzeCJsonCall(node) {
|
|
1700
|
-
// c.json(() => import("path"))
|
|
1701
|
-
if (!ts.isCallExpression(node) || !isCJson(node.expression)) {
|
|
1702
|
-
return null;
|
|
1703
|
-
}
|
|
1704
|
-
const [thunk] = node.arguments;
|
|
1705
|
-
if (!thunk) {
|
|
1706
|
-
return null;
|
|
1707
|
-
}
|
|
1708
|
-
const importPath = getImportPathFromThunk(thunk);
|
|
1709
|
-
if (importPath === null) {
|
|
1710
|
-
return null;
|
|
1711
|
-
}
|
|
1712
|
-
return {
|
|
1713
|
-
importPath
|
|
1714
|
-
};
|
|
1715
|
-
}
|
|
1716
|
-
function isCJson(expr) {
|
|
1717
|
-
// c.json
|
|
1718
|
-
return ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === "c" && ts.isIdentifier(expr.name) && expr.name.text === "json";
|
|
1719
|
-
}
|
|
1720
|
-
function getImportPathFromThunk(thunk) {
|
|
1721
|
-
// () => import("path") (concise body) OR () => { return import("path"); }
|
|
1722
|
-
if (!ts.isArrowFunction(thunk)) {
|
|
1723
|
-
return null;
|
|
1724
|
-
}
|
|
1725
|
-
let importCall;
|
|
1726
|
-
if (ts.isCallExpression(thunk.body)) {
|
|
1727
|
-
importCall = thunk.body;
|
|
1728
|
-
} else if (ts.isBlock(thunk.body)) {
|
|
1729
|
-
for (const stmt of thunk.body.statements) {
|
|
1730
|
-
if (ts.isReturnStatement(stmt) && stmt.expression && ts.isCallExpression(stmt.expression)) {
|
|
1731
|
-
importCall = stmt.expression;
|
|
1732
|
-
break;
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
if (!importCall || !ts.isCallExpression(importCall)) {
|
|
1737
|
-
return null;
|
|
1738
|
-
}
|
|
1739
|
-
const isImport = importCall.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(importCall.expression) && importCall.expression.text === "import";
|
|
1740
|
-
if (!isImport) {
|
|
1741
|
-
return null;
|
|
1742
|
-
}
|
|
1743
|
-
const arg = importCall.arguments[0];
|
|
1744
|
-
if (arg && ts.isStringLiteralLike(arg)) {
|
|
1745
|
-
return arg.text;
|
|
1746
|
-
}
|
|
1747
|
-
return null;
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
1963
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
1751
1964
|
const jsonOps = new JSONOps();
|
|
1752
1965
|
const tsOps = new TSOps(document => {
|
|
@@ -2621,7 +2834,9 @@ class ValOps {
|
|
|
2621
2834
|
const res = schema["executeValidate"](path, source);
|
|
2622
2835
|
// For `.jsonValues()` records, executeValidate only checks the entry
|
|
2623
2836
|
// markers; load + validate each entry's backing `*.val.json` content here.
|
|
2624
|
-
const
|
|
2837
|
+
const {
|
|
2838
|
+
errors: jsonValuesErrors
|
|
2839
|
+
} = await validateJsonValuesEntries(schema, source, path);
|
|
2625
2840
|
for (const [sourcePathS, entryErrors] of Object.entries(jsonValuesErrors)) {
|
|
2626
2841
|
const sourcePath = sourcePathS;
|
|
2627
2842
|
if (!errors[path]) {
|
|
@@ -12486,38 +12701,6 @@ function traverseObjectLiteral(node, sourceFile) {
|
|
|
12486
12701
|
}, {});
|
|
12487
12702
|
}
|
|
12488
12703
|
|
|
12489
|
-
/**
|
|
12490
|
-
* Which `*.val.json` holds a `.jsonValues()` entry's content, given the module's
|
|
12491
|
-
* `.val.ts`.
|
|
12492
|
-
*
|
|
12493
|
-
* Tooling that maps a validation `sourcePath` back to a place in a file needs
|
|
12494
|
-
* this: for a jsonValues module the offending value is NOT in the `.val.ts` at
|
|
12495
|
-
* all — that file only holds `c.json(() => import("./x.val.json"))` — so a
|
|
12496
|
-
* resolver that only ever looks at the `.val.ts` can report the error but not
|
|
12497
|
-
* where it lives, which for a record with hundreds of entries is not much of a
|
|
12498
|
-
* report.
|
|
12499
|
-
*
|
|
12500
|
-
* Returns a path relative to the project root (leading slash), or undefined when
|
|
12501
|
-
* the module has no such entry (not a jsonValues module, unparseable, or the key
|
|
12502
|
-
* is not backed by a `c.json` thunk).
|
|
12503
|
-
*/
|
|
12504
|
-
function findJsonEntryFilePath(moduleFilePath, valTsSourceFile, entryKey) {
|
|
12505
|
-
let analysis;
|
|
12506
|
-
try {
|
|
12507
|
-
analysis = analyzeValModule(valTsSourceFile);
|
|
12508
|
-
} catch {
|
|
12509
|
-
return undefined;
|
|
12510
|
-
}
|
|
12511
|
-
if (result.isErr(analysis)) {
|
|
12512
|
-
return undefined;
|
|
12513
|
-
}
|
|
12514
|
-
const entry = analyzeJsonValuesEntries(analysis.value.source).get(entryKey);
|
|
12515
|
-
if (!entry) {
|
|
12516
|
-
return undefined;
|
|
12517
|
-
}
|
|
12518
|
-
return resolveExistingJsonPath(moduleFilePath, entry.importPath);
|
|
12519
|
-
}
|
|
12520
|
-
|
|
12521
12704
|
/**
|
|
12522
12705
|
* Replays a `val debug` snapshot: applies its patches the way /save does and
|
|
12523
12706
|
* validates the result.
|
|
@@ -12617,4 +12800,4 @@ function readCapturedReport(snapshotDir) {
|
|
|
12617
12800
|
return JSON.parse(fs.readFileSync(reportPath, "utf-8"));
|
|
12618
12801
|
}
|
|
12619
12802
|
|
|
12620
|
-
export { DEFAULT_LOGIN_HOST, DEFAULT_LOGIN_MAX_DURATION, DEFAULT_LOGIN_POLL_INTERVAL, Service, ValFSHost, ValLoginError, ValModuleLoader, ValOpsFS, ValOpsHttp, ValSourceFileHandler, analyzeValModule, awaitValLoginConfirmation, checkRemoteRef, compareWithCapturedReport, createDefaultValFSHost, createFixPatch, createJsonEntryPathMap, createModulePathMap, createService, createValApiRouter, createValModuleFileInspector, createValServer, currentFixHandlers, decodeJwt, describePatchStoreProblems, downloadFileFromRemote, encodeJwt, evalValConfigFile, extractFileMetadata, extractImageMetadata, extractJsonValuesEntry, findAndEvalValConfigFile, findJsonEntryFilePath, fixHandlers, formatPatchSourceError, formatSyntaxErrorTree, getCachedRemoteFileDir, getCachedRemoteFilePath, getCompilerOptions, getExpire, getFileExt, getModulePathRange, getPersonalAccessTokenPath, getSettings, getValidationErrorFileRef, handleCheckAllFiles, handleFileMetadata, handleJsonValuesExtractEntry, handleRemoteFileCheck, handleRemoteFileDownload, handleRemoteFileUpload, handleRemoteGalleryFileUpload, handleUniqueFolderCheck, loadValModules, parsePersonalAccessTokenFile, patchSourceFile, persistPersonalAccessToken, readCapturedReport, readPatchStore, replaySnapshot, safeReadGit, startValLogin, uploadRemoteFile, validateMetadata };
|
|
12803
|
+
export { DEFAULT_LOGIN_HOST, DEFAULT_LOGIN_MAX_DURATION, DEFAULT_LOGIN_POLL_INTERVAL, Service, ValFSHost, ValLoginError, ValModuleLoader, ValOpsFS, ValOpsHttp, ValSourceFileHandler, analyzeValModule, awaitValLoginConfirmation, checkRemoteRef, classifyJsonValuesOp, compareWithCapturedReport, createDefaultValFSHost, createFixPatch, createJsonEntryPathMap, createModulePathMap, createService, createValApiRouter, createValModuleFileInspector, createValServer, currentFixHandlers, decodeJwt, describePatchStoreProblems, downloadFileFromRemote, encodeJwt, evalValConfigFile, extractFileMetadata, extractImageMetadata, extractJsonValuesEntry, findAndEvalValConfigFile, findJsonEntryFilePath, fixHandlers, formatPatchSourceError, formatSyntaxErrorTree, getCachedRemoteFileDir, getCachedRemoteFilePath, getCompilerOptions, getExpire, getFileExt, getModulePathRange, getPersonalAccessTokenPath, getSettings, getValidationErrorFileRef, handleCheckAllFiles, handleFileMetadata, handleJsonValuesExtractEntry, handleRemoteFileCheck, handleRemoteFileDownload, handleRemoteFileUpload, handleRemoteGalleryFileUpload, handleUniqueFolderCheck, loadValModules, parsePersonalAccessTokenFile, patchSourceFile, persistPersonalAccessToken, readCapturedReport, readPatchStore, rebaseContentOp, replaySnapshot, safeReadGit, startValLogin, uploadRemoteFile, validateMetadata };
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"./package.json": "./package.json"
|
|
17
17
|
},
|
|
18
18
|
"types": "dist/valbuild-server.cjs.d.ts",
|
|
19
|
-
"version": "0.108.
|
|
19
|
+
"version": "0.108.5",
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@prettier/sync": "^0.6.1",
|
|
22
22
|
"@types/jest": "^30.0.0"
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"typescript": "^6.0.3",
|
|
30
30
|
"zod": "^4.4.3",
|
|
31
31
|
"zod-validation-error": "^5.0.0",
|
|
32
|
-
"@valbuild/core": "0.106.0",
|
|
33
32
|
"@valbuild/shared": "0.108.0",
|
|
34
|
-
"@valbuild/ui": "0.108.
|
|
33
|
+
"@valbuild/ui": "0.108.5",
|
|
34
|
+
"@valbuild/core": "0.106.0"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": "^20.19.0 || >=22"
|