@caelo-cms/shared 0.10.67 → 0.10.69
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/ai-tools.d.ts +30 -14
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +33 -14
- package/dist/ai-tools.js.map +1 -1
- package/dist/build-page.d.ts +7 -6
- package/dist/build-page.d.ts.map +1 -1
- package/dist/build-page.js +17 -13
- package/dist/build-page.js.map +1 -1
- package/dist/html-blank.d.ts +11 -0
- package/dist/html-blank.d.ts.map +1 -0
- package/dist/html-blank.js +17 -0
- package/dist/html-blank.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/link-canonicalise.d.ts +29 -4
- package/dist/link-canonicalise.d.ts.map +1 -1
- package/dist/link-canonicalise.js +96 -8
- package/dist/link-canonicalise.js.map +1 -1
- package/dist/markup-structure.d.ts +24 -0
- package/dist/markup-structure.d.ts.map +1 -1
- package/dist/markup-structure.js +52 -0
- package/dist/markup-structure.js.map +1 -1
- package/dist/media.d.ts +2 -2
- package/dist/phase-timer.d.ts +36 -0
- package/dist/phase-timer.d.ts.map +1 -0
- package/dist/phase-timer.js +54 -0
- package/dist/phase-timer.js.map +1 -0
- package/dist/run-batched.d.ts +12 -0
- package/dist/run-batched.d.ts.map +1 -0
- package/dist/run-batched.js +37 -0
- package/dist/run-batched.js.map +1 -0
- package/dist/seo.d.ts +12 -5
- package/dist/seo.d.ts.map +1 -1
- package/dist/seo.js +40 -8
- package/dist/seo.js.map +1 -1
- package/dist/strip-cdata.d.ts +23 -0
- package/dist/strip-cdata.d.ts.map +1 -1
- package/dist/strip-cdata.js +31 -0
- package/dist/strip-cdata.js.map +1 -1
- package/dist/template-engine.d.ts.map +1 -1
- package/dist/template-engine.js +54 -22
- package/dist/template-engine.js.map +1 -1
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +29 -4
- package/dist/themes.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/ai-tools.ts +34 -14
- package/src/build-page.test.ts +5 -4
- package/src/build-page.ts +17 -14
- package/src/html-blank.ts +21 -0
- package/src/index.ts +2 -0
- package/src/link-canonicalise.test.ts +27 -0
- package/src/link-canonicalise.ts +100 -5
- package/src/markup-structure.test.ts +21 -0
- package/src/markup-structure.ts +54 -0
- package/src/nested-module-render.test.ts +77 -0
- package/src/phase-timer.test.ts +48 -0
- package/src/phase-timer.ts +62 -0
- package/src/run-batched.test.ts +40 -0
- package/src/run-batched.ts +39 -0
- package/src/seo.test.ts +50 -0
- package/src/seo.ts +40 -8
- package/src/strip-cdata.test.ts +30 -1
- package/src/strip-cdata.ts +31 -0
- package/src/template-engine.test.ts +84 -0
- package/src/template-engine.ts +62 -17
- package/src/theme-dimension.test.ts +61 -0
- package/src/themes.ts +30 -5
- package/src/version.ts +1 -1
package/src/template-engine.ts
CHANGED
|
@@ -187,6 +187,12 @@ const SECTION_RE =
|
|
|
187
187
|
/\{\{\s*#\s*([a-z][a-z0-9_]*)\s*\}\}((?:(?!\{\{\s*\/\s*\1\s*\}\})[\s\S])*)\{\{\s*\/\s*\1\s*\}\}/g;
|
|
188
188
|
const PARTIAL_RE = /\{\{\s*>\s*([a-z][a-z0-9_]*)\s*\}\}/g;
|
|
189
189
|
const PRIMITIVE_RE = /\{\{\s*([a-zA-Z][a-zA-Z0-9_]*)\s*\}\}/g;
|
|
190
|
+
// A plain tag (no Mustache sigil) of any name, and the identifier test
|
|
191
|
+
// that tells a field placeholder from literal text such as `{{Page URL}}`.
|
|
192
|
+
// No `\s*` before the close: it would overlap `[^{}]*` (polynomial
|
|
193
|
+
// backtracking); the callback trims instead.
|
|
194
|
+
const NON_IDENTIFIER_TAG_RE = /\{\{\s*([^#^/>!&={}\s][^{}]*)\}\}/g;
|
|
195
|
+
const IDENTIFIER_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
190
196
|
|
|
191
197
|
// Section-dispatch kinds (the engine's `{{#name}}` operand). Imported
|
|
192
198
|
// from content.ts so new list-shaped kinds wire through automatically
|
|
@@ -284,6 +290,14 @@ export function renderTemplate(input: RenderTemplateInput): RenderTemplateOutput
|
|
|
284
290
|
// pattern into their HTML can't break loud-raw restore by
|
|
285
291
|
// intercepting the sentinel for a known-key sentinel — they would
|
|
286
292
|
// have to guess the UUID minted at render time.
|
|
293
|
+
//
|
|
294
|
+
// The same sentinels also carry DATA through the render (#1053): every
|
|
295
|
+
// value the engine pastes into the template before Mustache runs — a
|
|
296
|
+
// list item, a link label/href, a conditional's `{{.}}`, a plugin list
|
|
297
|
+
// value, a rendered nested module — is wrapped, so literal `{{…}}` in
|
|
298
|
+
// content (a GTM `{{Page URL}}` in a code example) is never parsed as a
|
|
299
|
+
// tag a second time. Values Mustache substitutes itself are never
|
|
300
|
+
// re-parsed, so only these pre-pasted ones need it.
|
|
287
301
|
const sentinels = new Map<string, string>();
|
|
288
302
|
const sentinelPrefix = `__CAELO_TPL_${globalThis.crypto.randomUUID()}_`;
|
|
289
303
|
const mkSentinel = (original: string): string => {
|
|
@@ -380,15 +394,28 @@ export function renderTemplate(input: RenderTemplateInput): RenderTemplateOutput
|
|
|
380
394
|
return mkSentinel(match);
|
|
381
395
|
});
|
|
382
396
|
|
|
397
|
+
// 3b. A tag whose name is not a field identifier (`{{Page URL}}`, a
|
|
398
|
+
// Google Tag Manager variable in a snippet) can never be a
|
|
399
|
+
// placeholder — field names are snake_case. Mustache would look it
|
|
400
|
+
// up and silently render "", so it stays literal (#1053).
|
|
401
|
+
html = html.replace(NON_IDENTIFIER_TAG_RE, (match, name: string) =>
|
|
402
|
+
IDENTIFIER_RE.test(name.trim()) ? match : mkSentinel(match),
|
|
403
|
+
);
|
|
404
|
+
|
|
383
405
|
// 4. Render. The view holds only lowercase keys; sentinels survive
|
|
384
406
|
// untouched; sections + partials are already substituted.
|
|
385
407
|
const rendered = Mustache.render(html, view);
|
|
386
408
|
|
|
387
|
-
// 5. Restore
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
409
|
+
// 5. Restore sentinels in one pass. A single replace never re-scans
|
|
410
|
+
// the text it inserts, so restored content cannot be mistaken for a
|
|
411
|
+
// sentinel (and a list of N items costs one scan, not N).
|
|
412
|
+
let final =
|
|
413
|
+
sentinels.size === 0
|
|
414
|
+
? rendered
|
|
415
|
+
: rendered.replace(
|
|
416
|
+
new RegExp(`${sentinelPrefix}\\d+__`, "g"),
|
|
417
|
+
(key) => sentinels.get(key) ?? key,
|
|
418
|
+
);
|
|
392
419
|
|
|
393
420
|
// 6. Resolve internal references (epic #724). Last, so values,
|
|
394
421
|
// defaults and list items substituted above are covered; nested
|
|
@@ -417,7 +444,7 @@ function renderSection(
|
|
|
417
444
|
// fields are looked up FIRST by construction, so a plugin can never
|
|
418
445
|
// shadow something the module's author declared.
|
|
419
446
|
const items = dataLists[name];
|
|
420
|
-
if (items) return renderDataList(inner, items);
|
|
447
|
+
if (items) return renderDataList(inner, items, mkSentinel);
|
|
421
448
|
// Declared by an installed plugin that is NOT running. Naming the
|
|
422
449
|
// plugin turns "unknown field" (hunt for a typo) into "switch that
|
|
423
450
|
// plugin back on", which is the actual fix.
|
|
@@ -430,13 +457,13 @@ function renderSection(
|
|
|
430
457
|
return mkSentinel(match);
|
|
431
458
|
}
|
|
432
459
|
if (field.kind === "module-list") {
|
|
433
|
-
return renderModuleList(name, field, cvs, partials, missing);
|
|
460
|
+
return renderModuleList(name, field, cvs, partials, missing, mkSentinel);
|
|
434
461
|
}
|
|
435
462
|
if (field.kind === "text-list") {
|
|
436
|
-
return renderTextList(name, inner, field, cvs, missing);
|
|
463
|
+
return renderTextList(name, inner, field, cvs, missing, mkSentinel);
|
|
437
464
|
}
|
|
438
465
|
if (field.kind === "link-list") {
|
|
439
|
-
return renderLinkList(name, inner, field, cvs, missing);
|
|
466
|
+
return renderLinkList(name, inner, field, cvs, missing, mkSentinel);
|
|
440
467
|
}
|
|
441
468
|
// Any NON-list field used as a `{{#name}}…{{/name}}` section is a
|
|
442
469
|
// CONDITIONAL (Mustache semantics): render the inner block once when the
|
|
@@ -449,7 +476,7 @@ function renderSection(
|
|
|
449
476
|
// partial pass); `{{.}}`/`{{item}}` are substituted here for parity with
|
|
450
477
|
// the *-list renderers. An empty conditional is intentional, not
|
|
451
478
|
// "missing" — nothing is pushed to `missing`.
|
|
452
|
-
return renderConditionalSection(name, inner, field, cvs);
|
|
479
|
+
return renderConditionalSection(name, inner, field, cvs, mkSentinel);
|
|
453
480
|
}
|
|
454
481
|
|
|
455
482
|
/**
|
|
@@ -461,11 +488,14 @@ function renderConditionalSection(
|
|
|
461
488
|
inner: string,
|
|
462
489
|
field: TemplateField,
|
|
463
490
|
cvs: Readonly<Record<string, unknown>>,
|
|
491
|
+
mkSentinel: (original: string) => string,
|
|
464
492
|
): string {
|
|
465
493
|
const raw = Object.hasOwn(cvs, name) ? cvs[name] : field.default;
|
|
466
494
|
if (!isPresentSectionValue(raw)) return "";
|
|
467
495
|
const value = scalarSectionString(raw);
|
|
468
|
-
return value.length > 0
|
|
496
|
+
return value.length > 0
|
|
497
|
+
? inner.replace(/\{\{\s*(?:\.|item)\s*\}\}/g, () => mkSentinel(value))
|
|
498
|
+
: inner;
|
|
469
499
|
}
|
|
470
500
|
|
|
471
501
|
/**
|
|
@@ -498,6 +528,7 @@ function renderTextList(
|
|
|
498
528
|
field: TemplateField,
|
|
499
529
|
cvs: Readonly<Record<string, unknown>>,
|
|
500
530
|
missing: string[],
|
|
531
|
+
mkSentinel: (original: string) => string,
|
|
501
532
|
): string {
|
|
502
533
|
const raw = Object.hasOwn(cvs, name) ? cvs[name] : field.default;
|
|
503
534
|
if (!Array.isArray(raw)) return "";
|
|
@@ -510,7 +541,7 @@ function renderTextList(
|
|
|
510
541
|
continue;
|
|
511
542
|
}
|
|
512
543
|
const value = String(el);
|
|
513
|
-
parts.push(inner.replace(/\{\{\s*(?:\.|item)\s*\}\}/g, () => value));
|
|
544
|
+
parts.push(inner.replace(/\{\{\s*(?:\.|item)\s*\}\}/g, () => mkSentinel(value)));
|
|
514
545
|
}
|
|
515
546
|
return parts.join("");
|
|
516
547
|
}
|
|
@@ -529,13 +560,16 @@ function renderTextList(
|
|
|
529
560
|
function renderDataList(
|
|
530
561
|
inner: string,
|
|
531
562
|
items: ReadonlyArray<Readonly<Record<string, string>>>,
|
|
563
|
+
mkSentinel: (original: string) => string,
|
|
532
564
|
): string {
|
|
533
565
|
const parts: string[] = [];
|
|
534
566
|
for (const item of items) {
|
|
535
567
|
let rendered = inner;
|
|
536
568
|
for (const [key, value] of Object.entries(item)) {
|
|
537
569
|
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
538
|
-
rendered = rendered.replace(new RegExp(`\\{\\{\\s*${escaped}\\s*\\}\\}`, "g"), () =>
|
|
570
|
+
rendered = rendered.replace(new RegExp(`\\{\\{\\s*${escaped}\\s*\\}\\}`, "g"), () =>
|
|
571
|
+
mkSentinel(value),
|
|
572
|
+
);
|
|
539
573
|
}
|
|
540
574
|
parts.push(rendered);
|
|
541
575
|
}
|
|
@@ -548,6 +582,7 @@ function renderLinkList(
|
|
|
548
582
|
field: TemplateField,
|
|
549
583
|
cvs: Readonly<Record<string, unknown>>,
|
|
550
584
|
missing: string[],
|
|
585
|
+
mkSentinel: (original: string) => string,
|
|
551
586
|
): string {
|
|
552
587
|
const raw = Object.hasOwn(cvs, name) ? cvs[name] : field.default;
|
|
553
588
|
if (!Array.isArray(raw)) return "";
|
|
@@ -566,7 +601,9 @@ function renderLinkList(
|
|
|
566
601
|
}
|
|
567
602
|
const { label, href } = el as { label: string; href: string };
|
|
568
603
|
parts.push(
|
|
569
|
-
inner
|
|
604
|
+
inner
|
|
605
|
+
.replace(/\{\{\s*label\s*\}\}/g, () => mkSentinel(label))
|
|
606
|
+
.replace(/\{\{\s*href\s*\}\}/g, () => mkSentinel(href)),
|
|
570
607
|
);
|
|
571
608
|
}
|
|
572
609
|
return parts.join("");
|
|
@@ -578,6 +615,7 @@ function renderModuleList(
|
|
|
578
615
|
cvs: Readonly<Record<string, unknown>>,
|
|
579
616
|
partials: PartialResolver,
|
|
580
617
|
missing: string[],
|
|
618
|
+
mkSentinel: (original: string) => string,
|
|
581
619
|
): string {
|
|
582
620
|
const raw = Object.hasOwn(cvs, name) ? cvs[name] : field.default;
|
|
583
621
|
if (!Array.isArray(raw)) return "";
|
|
@@ -601,7 +639,7 @@ function renderModuleList(
|
|
|
601
639
|
parts.push(`<!-- caelo:module-list ${name} needs recursive renderer (compose path) -->`);
|
|
602
640
|
continue;
|
|
603
641
|
}
|
|
604
|
-
parts.push(partialHtml);
|
|
642
|
+
parts.push(mkSentinel(partialHtml));
|
|
605
643
|
}
|
|
606
644
|
return parts.join("");
|
|
607
645
|
}
|
|
@@ -625,7 +663,14 @@ function renderPartialRef(
|
|
|
625
663
|
missing.push(reason);
|
|
626
664
|
return comment(reason);
|
|
627
665
|
}
|
|
628
|
-
const ref = cvs[name];
|
|
666
|
+
const ref = Object.hasOwn(cvs, name) ? cvs[name] : undefined;
|
|
667
|
+
// An unset or explicitly null single `module` field is an EMPTY slot,
|
|
668
|
+
// the same as an empty module-list / text-list: it renders nothing and
|
|
669
|
+
// is not a failure (#1041). The static generator refuses a Stage on any
|
|
670
|
+
// marker, so flagging "empty" here made an optional nested slot
|
|
671
|
+
// impossible to leave blank. A present value that is not a ref is still
|
|
672
|
+
// malformed and stays loud.
|
|
673
|
+
if (ref === undefined || ref === null) return "";
|
|
629
674
|
if (!isNestedRef(ref)) {
|
|
630
675
|
missing.push(`module-ref-malformed:${name}`);
|
|
631
676
|
return comment(`module-ref-malformed ${name}`);
|
|
@@ -636,5 +681,5 @@ function renderPartialRef(
|
|
|
636
681
|
missing.push(`${NESTED_RENDERER_UNAVAILABLE}:${name}`);
|
|
637
682
|
return `<!-- caelo:module ${name} needs recursive renderer (compose path) -->`;
|
|
638
683
|
}
|
|
639
|
-
return partialHtml;
|
|
684
|
+
return mkSentinel(partialHtml);
|
|
640
685
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dimension values accept CSS math functions (e2e-livedit run 40018361466:
|
|
5
|
+
* set_theme_tokens refused `typography.display` with a fluid
|
|
6
|
+
* `clamp(2.5rem, 2rem + 2.5vw, 4rem)` font size, and the error printed the
|
|
7
|
+
* truncated composite value, which hid the field it was about).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from "bun:test";
|
|
11
|
+
import { themeDocument } from "./themes.js";
|
|
12
|
+
|
|
13
|
+
const display = (fontSize: string) => ({
|
|
14
|
+
typography: {
|
|
15
|
+
display: {
|
|
16
|
+
$type: "typography",
|
|
17
|
+
$value: {
|
|
18
|
+
fontFamily: "Inter, system-ui, -apple-system, 'Segoe UI', sans-serif",
|
|
19
|
+
fontSize,
|
|
20
|
+
fontWeight: 800,
|
|
21
|
+
lineHeight: 1.05,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("theme dimension values", () => {
|
|
28
|
+
it("accept clamp() / calc() / min() / max() font sizes and spacing", () => {
|
|
29
|
+
for (const v of [
|
|
30
|
+
"clamp(2.5rem, 2rem + 2.5vw, 4rem)",
|
|
31
|
+
"calc(1rem + 0.5vw)",
|
|
32
|
+
"min(4rem, 10vw)",
|
|
33
|
+
"max(1rem, calc(0.75rem + 1vw))",
|
|
34
|
+
]) {
|
|
35
|
+
expect(themeDocument.safeParse(display(v)).success).toBe(true);
|
|
36
|
+
}
|
|
37
|
+
const spacing = {
|
|
38
|
+
spacing: { section: { $type: "dimension", $value: "clamp(2rem, 5vw, 5rem)" } },
|
|
39
|
+
};
|
|
40
|
+
expect(themeDocument.safeParse(spacing).success).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("still refuse values that are not a length, and math functions that could close the declaration", () => {
|
|
44
|
+
for (const v of [
|
|
45
|
+
"large",
|
|
46
|
+
"clamp(1rem; } body { display:none }",
|
|
47
|
+
"calc(1rem)</style>",
|
|
48
|
+
"url(x)",
|
|
49
|
+
]) {
|
|
50
|
+
expect(themeDocument.safeParse(display(v)).success).toBe(false);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("name the offending field's value, not the truncated composite", () => {
|
|
55
|
+
const r = themeDocument.safeParse(display("large"));
|
|
56
|
+
expect(r.success).toBe(false);
|
|
57
|
+
if (r.success) return;
|
|
58
|
+
const message = r.error.issues.map((i) => i.message).join(" ");
|
|
59
|
+
expect(message).toContain('(got "large")');
|
|
60
|
+
});
|
|
61
|
+
});
|
package/src/themes.ts
CHANGED
|
@@ -51,14 +51,26 @@ const colorValueString = z
|
|
|
51
51
|
"color value must be #hex, oklch(...), rgb(...), or a CSS named color",
|
|
52
52
|
);
|
|
53
53
|
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Dimension: a CSS length / percentage / numeric value, or a CSS math
|
|
56
|
+
* function over them (`clamp()`, `calc()`, `min()`, `max()`).
|
|
57
|
+
*
|
|
58
|
+
* The math functions are accepted because fluid type is the idiomatic way to
|
|
59
|
+
* size a display heading — `clamp(2.5rem, 2rem + 2.5vw, 4rem)` — and token
|
|
60
|
+
* values are emitted verbatim as CSS custom properties, where a browser
|
|
61
|
+
* resolves them like any length. Refusing them (e2e-livedit run 40018361466,
|
|
62
|
+
* set_theme_tokens `typography.display`) cost a retry and the AI downgraded to
|
|
63
|
+
* a fixed size it had not chosen. The arguments are limited to numbers,
|
|
64
|
+
* units, operators, commas and nested parentheses, so a value cannot close
|
|
65
|
+
* the declaration (`;`, `{`, `}`) or carry markup.
|
|
66
|
+
*/
|
|
55
67
|
const dimensionValueString = z
|
|
56
68
|
.string()
|
|
57
69
|
.min(1)
|
|
58
|
-
.max(
|
|
70
|
+
.max(120)
|
|
59
71
|
.regex(
|
|
60
|
-
/^-?\d+(\.\d+)?(rem|em|px|%|vh|vw|vmin|vmax|pt|pc|ch|ex|fr|deg|rad|turn|s|ms)?$|^auto$|^0$/,
|
|
61
|
-
"dimension must be a CSS length / number / percentage",
|
|
72
|
+
/^-?\d+(\.\d+)?(rem|em|px|%|vh|vw|vmin|vmax|pt|pc|ch|ex|fr|deg|rad|turn|s|ms)?$|^auto$|^0$|^(calc|clamp|min|max)\([0-9a-zA-Z.%+\-*/,\s()]+\)$/,
|
|
73
|
+
"dimension must be a CSS length / number / percentage, or clamp()/calc()/min()/max() over them",
|
|
62
74
|
);
|
|
63
75
|
|
|
64
76
|
/** Font-weight: keyword or 1-1000 number. */
|
|
@@ -422,6 +434,16 @@ const CATEGORY_EXPECTATIONS: Record<string, string> = {
|
|
|
422
434
|
};
|
|
423
435
|
|
|
424
436
|
/** Compact, human-readable render of a rejected `$value` for the error tail. */
|
|
437
|
+
/** The value `path` points at inside `root`; `root` itself for an empty path. */
|
|
438
|
+
function valueAtPath(root: unknown, path: readonly PropertyKey[]): unknown {
|
|
439
|
+
let node = root;
|
|
440
|
+
for (const key of path) {
|
|
441
|
+
if (node === null || typeof node !== "object") return node;
|
|
442
|
+
node = (node as Record<PropertyKey, unknown>)[key];
|
|
443
|
+
}
|
|
444
|
+
return node;
|
|
445
|
+
}
|
|
446
|
+
|
|
425
447
|
function describeValue(value: unknown): string {
|
|
426
448
|
if (typeof value === "string") return JSON.stringify(value);
|
|
427
449
|
if (value === undefined) return "undefined";
|
|
@@ -488,10 +510,13 @@ function refineKnownCategoryLeaves(
|
|
|
488
510
|
});
|
|
489
511
|
} else {
|
|
490
512
|
for (const issue of sub.error.issues) {
|
|
513
|
+
// Name the offending LEAF, not the whole composite `$value`: a
|
|
514
|
+
// typography value prints `fontFamily` first, so the truncated
|
|
515
|
+
// object hid the `fontSize` the issue was about.
|
|
491
516
|
ctx.addIssue({
|
|
492
517
|
code: "custom",
|
|
493
518
|
path: [...path, ...issue.path],
|
|
494
|
-
message: `${category} token "${String(name)}" invalid: ${issue.message} (got ${describeValue(obj
|
|
519
|
+
message: `${category} token "${String(name)}" invalid: ${issue.message} (got ${describeValue(valueAtPath(obj, issue.path))})`,
|
|
495
520
|
});
|
|
496
521
|
}
|
|
497
522
|
}
|