@hollware/core 0.6.0 → 0.9.0
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/index.d.mts +561 -7
- package/dist/index.mjs +992 -117
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
2
|
import * as z from "zod";
|
|
3
|
+
import { Validator } from "@cfworker/json-schema";
|
|
3
4
|
//#region src/config/define.ts
|
|
4
5
|
/**
|
|
5
6
|
* The `define*` helpers only give the config its types: the frontmatter type of a collection
|
|
@@ -124,7 +125,7 @@ function runtimeOf(contractId, context) {
|
|
|
124
125
|
*/
|
|
125
126
|
/** The most characters of the entry's own text that a message repeats. */
|
|
126
127
|
const MAX_ECHO_LENGTH = 80;
|
|
127
|
-
function isRecord$
|
|
128
|
+
function isRecord$3(value) {
|
|
128
129
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
129
130
|
}
|
|
130
131
|
/** Orders two texts by UTF-16 code units: the same in every runtime, unlike `localeCompare`. */
|
|
@@ -177,11 +178,11 @@ function fragmentAt(schema, segments) {
|
|
|
177
178
|
let node = schema;
|
|
178
179
|
for (const segment of segments) {
|
|
179
180
|
const properties = node.properties;
|
|
180
|
-
if (isRecord$
|
|
181
|
+
if (isRecord$3(properties) && Object.hasOwn(properties, segment)) {
|
|
181
182
|
const next = properties[segment];
|
|
182
|
-
if (!isRecord$
|
|
183
|
+
if (!isRecord$3(next)) return void 0;
|
|
183
184
|
node = next;
|
|
184
|
-
} else if (/^\d+$/.test(segment) && isRecord$
|
|
185
|
+
} else if (/^\d+$/.test(segment) && isRecord$3(node.items)) node = node.items;
|
|
185
186
|
else return;
|
|
186
187
|
}
|
|
187
188
|
return node;
|
|
@@ -189,7 +190,7 @@ function fragmentAt(schema, segments) {
|
|
|
189
190
|
function valueAt(input, segments) {
|
|
190
191
|
let node = input;
|
|
191
192
|
for (const segment of segments) if (Array.isArray(node) && /^\d+$/.test(segment)) node = node[Number(segment)];
|
|
192
|
-
else if (isRecord$
|
|
193
|
+
else if (isRecord$3(node) && Object.hasOwn(node, segment)) node = node[segment];
|
|
193
194
|
else return void 0;
|
|
194
195
|
return node;
|
|
195
196
|
}
|
|
@@ -225,11 +226,11 @@ function numberExpectation(type, schema) {
|
|
|
225
226
|
}
|
|
226
227
|
function arrayExpectation(schema) {
|
|
227
228
|
const size = range(schema.minItems, schema.maxItems, "item");
|
|
228
|
-
const items = isRecord$
|
|
229
|
+
const items = isRecord$3(schema.items) ? expectation(schema.items) : void 0;
|
|
229
230
|
return `an array${size === void 0 ? "" : ` of ${size}`}${items === void 0 ? "" : `, each ${items}`}`;
|
|
230
231
|
}
|
|
231
232
|
function objectExpectation(schema) {
|
|
232
|
-
const keys = isRecord$
|
|
233
|
+
const keys = isRecord$3(schema.properties) ? Object.keys(schema.properties) : [];
|
|
233
234
|
return keys.length === 0 ? "an object" : `an object with the keys ${keys.join(", ")}`;
|
|
234
235
|
}
|
|
235
236
|
function typeExpectation(type, schema) {
|
|
@@ -252,13 +253,13 @@ function expectation(schema) {
|
|
|
252
253
|
if ("const" in schema) return `exactly ${JSON.stringify(schema.const)}`;
|
|
253
254
|
if (Array.isArray(schema.enum)) return `one of ${listValues(schema.enum)}`;
|
|
254
255
|
const alternatives = schema.anyOf ?? schema.oneOf;
|
|
255
|
-
const parts = Array.isArray(alternatives) ? alternatives.map((option) => isRecord$
|
|
256
|
+
const parts = Array.isArray(alternatives) ? alternatives.map((option) => isRecord$3(option) ? expectation(option) : void 0) : (Array.isArray(schema.type) ? schema.type : [schema.type]).map((type) => typeExpectation(type, schema));
|
|
256
257
|
return parts.every((part) => part !== void 0) ? parts.join(" or ") : void 0;
|
|
257
258
|
}
|
|
258
259
|
/** The keys of `value` that an object schema closed to other keys does not list. */
|
|
259
260
|
function unknownKeys(fragment, value) {
|
|
260
261
|
const { additionalProperties, properties } = fragment;
|
|
261
|
-
if (additionalProperties !== false || !isRecord$
|
|
262
|
+
if (additionalProperties !== false || !isRecord$3(properties) || !isRecord$3(value)) return [];
|
|
262
263
|
return Object.keys(value).filter((key) => !Object.hasOwn(properties, key));
|
|
263
264
|
}
|
|
264
265
|
/** The fix for one issue: imperative, naming the field and what the schema accepts there. */
|
|
@@ -268,7 +269,7 @@ function fixForIssue({ subject, schema, input, segments, message }) {
|
|
|
268
269
|
if (fragment !== void 0) {
|
|
269
270
|
const unknown = unknownKeys(fragment, valueAt(input, segments));
|
|
270
271
|
if (unknown.length > 0) {
|
|
271
|
-
const allowed = isRecord$
|
|
272
|
+
const allowed = isRecord$3(fragment.properties) ? Object.keys(fragment.properties) : [];
|
|
272
273
|
const nouns = unknown.length === 1 ? subject.noun : `${subject.noun}s`;
|
|
273
274
|
const where = allowed.length === 0 ? `no ${subject.noun}s are allowed there` : `the ${subject.noun}s allowed there are: ${allowed.join(", ")}`;
|
|
274
275
|
return `Remove the ${nouns} ${unknown.map((key) => quote$1(key)).join(", ")} from ${target}; ${where}.`;
|
|
@@ -280,7 +281,7 @@ function fixForIssue({ subject, schema, input, segments, message }) {
|
|
|
280
281
|
//#endregion
|
|
281
282
|
//#region src/contracts/schema.ts
|
|
282
283
|
const SNAPSHOTS = /* @__PURE__ */ new WeakMap();
|
|
283
|
-
function toJsonSchema$
|
|
284
|
+
function toJsonSchema$2(schema) {
|
|
284
285
|
try {
|
|
285
286
|
return schema["~standard"].jsonSchema.input({ target: "draft-2020-12" });
|
|
286
287
|
} catch {
|
|
@@ -291,8 +292,8 @@ function schemaSnapshot(collection) {
|
|
|
291
292
|
let snapshot = SNAPSHOTS.get(collection);
|
|
292
293
|
if (snapshot === void 0) {
|
|
293
294
|
snapshot = {
|
|
294
|
-
frontmatter: toJsonSchema$
|
|
295
|
-
components: new Map([...collection.components].map(([name, component]) => [name, toJsonSchema$
|
|
295
|
+
frontmatter: toJsonSchema$2(collection.schema),
|
|
296
|
+
components: new Map([...collection.components].map(([name, component]) => [name, toJsonSchema$2(component.props)]))
|
|
296
297
|
};
|
|
297
298
|
SNAPSHOTS.set(collection, snapshot);
|
|
298
299
|
}
|
|
@@ -540,6 +541,211 @@ function describeBuiltInContracts() {
|
|
|
540
541
|
].map((contract) => describeContract(contract));
|
|
541
542
|
}
|
|
542
543
|
//#endregion
|
|
544
|
+
//#region src/search/chunks.ts
|
|
545
|
+
/**
|
|
546
|
+
* The pieces of an entry that semantic search embeds (v0.5): one summary, and one piece for each
|
|
547
|
+
* section the body has under an `h2`. Vectors are derived data, like the Comark document: a change
|
|
548
|
+
* of this version makes every vector one to make again.
|
|
549
|
+
*/
|
|
550
|
+
const CHUNKER_VERSION = "1";
|
|
551
|
+
/** How long a piece may grow, in characters, before it is cut at a word. */
|
|
552
|
+
const CHUNK_LIMITS = {
|
|
553
|
+
summary: 1200,
|
|
554
|
+
section: 2e3
|
|
555
|
+
};
|
|
556
|
+
/** The text a reader sees in a document, its blocks on lines of their own. */
|
|
557
|
+
function documentText(document) {
|
|
558
|
+
return document.nodes.map((node) => textOf(node)).join("\n");
|
|
559
|
+
}
|
|
560
|
+
/** The text a reader sees in a node: its strings, never its tag, attributes or comments. */
|
|
561
|
+
function textOf(node) {
|
|
562
|
+
if (typeof node === "string") return node;
|
|
563
|
+
if (!Array.isArray(node) || node[0] === null) return "";
|
|
564
|
+
return node.slice(2).map((child) => textOf(child)).join("");
|
|
565
|
+
}
|
|
566
|
+
function tagOf(node) {
|
|
567
|
+
return Array.isArray(node) && typeof node[0] === "string" ? node[0] : null;
|
|
568
|
+
}
|
|
569
|
+
/** The words of a text in one line, cut at the last word that fits in `limit`. */
|
|
570
|
+
function fit(text, limit) {
|
|
571
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
572
|
+
if (flat.length <= limit) return flat;
|
|
573
|
+
const cut = flat.slice(0, limit);
|
|
574
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
575
|
+
return (lastSpace > 0 ? cut.slice(0, lastSpace) : cut).trim();
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* The pieces of an entry: the summary (its title, its description and the text before the first
|
|
579
|
+
* `h2`), then each section (its heading and its text up to the next `h2`). An `h1` adds nothing:
|
|
580
|
+
* it repeats the title. A section with no text is left out.
|
|
581
|
+
*/
|
|
582
|
+
function chunksOf(entry) {
|
|
583
|
+
const intro = [];
|
|
584
|
+
const sections = [];
|
|
585
|
+
for (const node of entry.document.nodes) {
|
|
586
|
+
const tag = tagOf(node);
|
|
587
|
+
if (tag === "h1") continue;
|
|
588
|
+
if (tag === "h2") {
|
|
589
|
+
sections.push({
|
|
590
|
+
heading: textOf(node).trim(),
|
|
591
|
+
parts: []
|
|
592
|
+
});
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const text = textOf(node);
|
|
596
|
+
const current = sections.at(-1);
|
|
597
|
+
if (current === void 0) intro.push(text);
|
|
598
|
+
else current.parts.push(text);
|
|
599
|
+
}
|
|
600
|
+
const chunks = [{
|
|
601
|
+
index: 0,
|
|
602
|
+
kind: "summary",
|
|
603
|
+
heading: null,
|
|
604
|
+
text: fit([
|
|
605
|
+
entry.title,
|
|
606
|
+
entry.description ?? "",
|
|
607
|
+
intro.join(" ")
|
|
608
|
+
].filter((part) => part !== "").join("\n"), CHUNK_LIMITS.summary)
|
|
609
|
+
}];
|
|
610
|
+
for (const section of sections) {
|
|
611
|
+
const body = section.parts.join(" ").trim();
|
|
612
|
+
if (body === "") continue;
|
|
613
|
+
chunks.push({
|
|
614
|
+
index: chunks.length,
|
|
615
|
+
kind: "section",
|
|
616
|
+
heading: section.heading,
|
|
617
|
+
text: fit(`${section.heading}\n${body}`, CHUNK_LIMITS.section)
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
return chunks;
|
|
621
|
+
}
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region src/contracts/rules.ts
|
|
624
|
+
function present(frontmatter, field) {
|
|
625
|
+
return isRecord$3(frontmatter) ? frontmatter[field] : void 0;
|
|
626
|
+
}
|
|
627
|
+
const GRAPHEMES = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
628
|
+
/** The length of a value as a reader counts it: characters of a text (an emoji is one), items of a list. */
|
|
629
|
+
function lengthOf(value) {
|
|
630
|
+
if (typeof value === "string") return [...GRAPHEMES.segment(value)].length;
|
|
631
|
+
return Array.isArray(value) ? value.length : void 0;
|
|
632
|
+
}
|
|
633
|
+
/** A text, or a list, of a field has between `min` and `max` characters or items. */
|
|
634
|
+
function fieldLength(params, options = {}) {
|
|
635
|
+
const bounds = [params.min === void 0 ? null : `at least ${params.min}`, params.max === void 0 ? null : `at most ${params.max}`].filter((bound) => bound !== null);
|
|
636
|
+
const unless = params.unless === void 0 ? "" : `, unless ${quote$1(params.unless)} is given`;
|
|
637
|
+
return registerBuiltIn({
|
|
638
|
+
id: options.id ?? "field-length",
|
|
639
|
+
severity: options.severity ?? "error",
|
|
640
|
+
description: `${quote$1(params.field)} has ${bounds.join(" and ")} characters or items${unless}.`,
|
|
641
|
+
rule: {
|
|
642
|
+
name: "field-length",
|
|
643
|
+
params: { ...params }
|
|
644
|
+
},
|
|
645
|
+
check: ({ frontmatter }) => {
|
|
646
|
+
const value = present(frontmatter, params.field);
|
|
647
|
+
const length = lengthOf(value);
|
|
648
|
+
if (length === void 0) return [];
|
|
649
|
+
const unit = typeof value === "string" ? "character" : "item";
|
|
650
|
+
if (params.unless !== void 0 && present(frontmatter, params.unless) !== void 0) return [];
|
|
651
|
+
const path = `/frontmatter/${params.field}`;
|
|
652
|
+
if (params.max !== void 0 && length > params.max) return [{
|
|
653
|
+
path,
|
|
654
|
+
message: `${quote$1(params.field)} has ${count(length, unit)}; the most is ${params.max}.`,
|
|
655
|
+
fix: `Shorten ${quote$1(params.field)} to ${count(params.max, unit)} or fewer${params.unless === void 0 ? "" : `, or add ${quote$1(params.unless)}`}.`
|
|
656
|
+
}];
|
|
657
|
+
if (params.min !== void 0 && length < params.min) return [{
|
|
658
|
+
path,
|
|
659
|
+
message: `${quote$1(params.field)} has ${count(length, unit)}; the least is ${params.min}.`,
|
|
660
|
+
fix: `Lengthen ${quote$1(params.field)} to ${count(params.min, unit)} or more.`
|
|
661
|
+
}];
|
|
662
|
+
return [];
|
|
663
|
+
}
|
|
664
|
+
}, { readsFrontmatter: true });
|
|
665
|
+
}
|
|
666
|
+
/** A text field matches a pattern. */
|
|
667
|
+
function fieldPattern(params, options = {}) {
|
|
668
|
+
const pattern = new RegExp(`^(?:${params.pattern})$`, "u");
|
|
669
|
+
return registerBuiltIn({
|
|
670
|
+
id: options.id ?? "field-pattern",
|
|
671
|
+
severity: options.severity ?? "error",
|
|
672
|
+
description: `${quote$1(params.field)} is ${params.description}.`,
|
|
673
|
+
rule: {
|
|
674
|
+
name: "field-pattern",
|
|
675
|
+
params: { ...params }
|
|
676
|
+
},
|
|
677
|
+
check: ({ frontmatter }) => {
|
|
678
|
+
const value = present(frontmatter, params.field);
|
|
679
|
+
if (typeof value !== "string" || pattern.test(value)) return [];
|
|
680
|
+
return [{
|
|
681
|
+
path: `/frontmatter/${params.field}`,
|
|
682
|
+
message: `${quote$1(params.field)} is not ${params.description}.`,
|
|
683
|
+
fix: `Write ${quote$1(params.field)} as ${params.description}.`
|
|
684
|
+
}];
|
|
685
|
+
}
|
|
686
|
+
}, { readsFrontmatter: true });
|
|
687
|
+
}
|
|
688
|
+
/** Fields a draft may lack and a published entry must have, with something in them. */
|
|
689
|
+
function requiredToPublish(params, options = {}) {
|
|
690
|
+
const empty = (value) => value === void 0 || value === null || typeof value === "string" && value.trim() === "" || Array.isArray(value) && value.length === 0;
|
|
691
|
+
return registerBuiltIn({
|
|
692
|
+
id: options.id ?? "required-to-publish",
|
|
693
|
+
severity: options.severity ?? "error",
|
|
694
|
+
phases: ["publish"],
|
|
695
|
+
description: `To publish, ${params.fields.map(quote$1).join(", ")} must have a value.`,
|
|
696
|
+
rule: {
|
|
697
|
+
name: "required-to-publish",
|
|
698
|
+
params: { fields: [...params.fields] }
|
|
699
|
+
},
|
|
700
|
+
check: ({ frontmatter }) => params.fields.filter((field) => empty(present(frontmatter, field))).map((field) => ({
|
|
701
|
+
path: `/frontmatter/${field}`,
|
|
702
|
+
message: `${quote$1(field)} is empty, and the entry cannot be published without it.`,
|
|
703
|
+
fix: `Give ${quote$1(field)} a value, then publish.`
|
|
704
|
+
}))
|
|
705
|
+
}, { readsFrontmatter: true });
|
|
706
|
+
}
|
|
707
|
+
function escape(text) {
|
|
708
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
709
|
+
}
|
|
710
|
+
/** Words that may appear neither in a text field of the frontmatter nor in the body. */
|
|
711
|
+
function bannedWords(params, options = {}) {
|
|
712
|
+
const words = params.words.filter((word) => word.trim() !== "");
|
|
713
|
+
const pattern = words.length === 0 ? null : new RegExp(`(?<![\\p{L}\\p{N}])(?:${words.map(escape).join("|")})(?![\\p{L}\\p{N}])`, "giu");
|
|
714
|
+
const because = params.reason === void 0 ? "" : ` (${params.reason})`;
|
|
715
|
+
const found = (text) => [...new Set([...text.matchAll(pattern ?? /$^/g)].map((m) => m[0]))];
|
|
716
|
+
return registerBuiltIn({
|
|
717
|
+
id: options.id ?? "banned-words",
|
|
718
|
+
severity: options.severity ?? "error",
|
|
719
|
+
description: `None of these words: ${words.map(quote$1).join(", ")}${because}.`,
|
|
720
|
+
rule: {
|
|
721
|
+
name: "banned-words",
|
|
722
|
+
params: {
|
|
723
|
+
...params,
|
|
724
|
+
words: [...params.words]
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
check: (context) => {
|
|
728
|
+
if (pattern === null) return [];
|
|
729
|
+
const findings = [];
|
|
730
|
+
const frontmatter = isRecord$3(context.frontmatter) ? context.frontmatter : {};
|
|
731
|
+
for (const [field, value] of Object.entries(frontmatter)) {
|
|
732
|
+
if (typeof value !== "string") continue;
|
|
733
|
+
for (const word of found(value)) findings.push({
|
|
734
|
+
path: `/frontmatter/${field}`,
|
|
735
|
+
message: `${quote$1(field)} uses ${quote$1(word)}.`,
|
|
736
|
+
fix: `Write ${quote$1(field)} without ${quote$1(word)}${because}.`
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
for (const word of found(documentText(context.markdown.document))) findings.push({
|
|
740
|
+
path: "/body",
|
|
741
|
+
message: `The body uses ${quote$1(word)}.`,
|
|
742
|
+
fix: `Rewrite the body without ${quote$1(word)}${because}.`
|
|
743
|
+
});
|
|
744
|
+
return findings;
|
|
745
|
+
}
|
|
746
|
+
}, { readsFrontmatter: false });
|
|
747
|
+
}
|
|
748
|
+
//#endregion
|
|
543
749
|
//#region src/contracts/opt-in.ts
|
|
544
750
|
/**
|
|
545
751
|
* Every locale of the site has an entry in the translation group of this one. It reads the group
|
|
@@ -549,7 +755,11 @@ function describeBuiltInContracts() {
|
|
|
549
755
|
*/
|
|
550
756
|
function translationPair(options = {}) {
|
|
551
757
|
return registerBuiltIn({
|
|
552
|
-
id: "translation-pair",
|
|
758
|
+
id: options.id ?? "translation-pair",
|
|
759
|
+
rule: {
|
|
760
|
+
name: "translation-pair",
|
|
761
|
+
params: {}
|
|
762
|
+
},
|
|
553
763
|
severity: options.severity ?? "error",
|
|
554
764
|
phases: ["publish"],
|
|
555
765
|
description: "Every locale of the site has an entry in the translation group of this entry. Checked when publishing.",
|
|
@@ -558,7 +768,7 @@ function translationPair(options = {}) {
|
|
|
558
768
|
const field = collection.translationGroupField;
|
|
559
769
|
if (field === null) return [];
|
|
560
770
|
const path = `/frontmatter${pointer([field])}`;
|
|
561
|
-
const group = isRecord$
|
|
771
|
+
const group = isRecord$3(context.frontmatter) ? context.frontmatter[field] : void 0;
|
|
562
772
|
if (typeof group !== "string" || group === "") return [{
|
|
563
773
|
path,
|
|
564
774
|
message: `The entry has no translation group: its frontmatter field ${field} is empty.`,
|
|
@@ -581,7 +791,11 @@ function translationPair(options = {}) {
|
|
|
581
791
|
*/
|
|
582
792
|
function internalLinksResolve(options = {}) {
|
|
583
793
|
return registerBuiltIn({
|
|
584
|
-
id: "internal-links-resolve",
|
|
794
|
+
id: options.id ?? "internal-links-resolve",
|
|
795
|
+
rule: {
|
|
796
|
+
name: "internal-links-resolve",
|
|
797
|
+
params: {}
|
|
798
|
+
},
|
|
585
799
|
severity: options.severity ?? "error",
|
|
586
800
|
phases: ["publish"],
|
|
587
801
|
description: "Every link to another entry of the site points at a published entry. Checked when publishing.",
|
|
@@ -635,7 +849,11 @@ function isThisEntry(target, context) {
|
|
|
635
849
|
/** Every image has alt text. An alt of only spaces counts as none. Checked on every write. */
|
|
636
850
|
function imagesHaveAlt(options = {}) {
|
|
637
851
|
return registerBuiltIn({
|
|
638
|
-
id: "images-have-alt",
|
|
852
|
+
id: options.id ?? "images-have-alt",
|
|
853
|
+
rule: {
|
|
854
|
+
name: "images-have-alt",
|
|
855
|
+
params: {}
|
|
856
|
+
},
|
|
639
857
|
severity: options.severity ?? "error",
|
|
640
858
|
phases: ["write", "publish"],
|
|
641
859
|
description: "Every image has alt text that describes it.",
|
|
@@ -673,12 +891,81 @@ function custom(options) {
|
|
|
673
891
|
};
|
|
674
892
|
}
|
|
675
893
|
const contracts = {
|
|
894
|
+
bannedWords,
|
|
676
895
|
custom,
|
|
896
|
+
fieldLength,
|
|
897
|
+
fieldPattern,
|
|
677
898
|
imagesHaveAlt,
|
|
678
899
|
internalLinksResolve,
|
|
900
|
+
requiredToPublish,
|
|
679
901
|
translationPair
|
|
680
902
|
};
|
|
681
903
|
//#endregion
|
|
904
|
+
//#region src/contracts/catalog.ts
|
|
905
|
+
/**
|
|
906
|
+
* The catalog of rules (ADR-033): every rule a collection may list in its definition, by name,
|
|
907
|
+
* with the JSON Schema of its parameters. A site's definition stores `{ rule, severity, params }`
|
|
908
|
+
* and the Worker makes the contract from it here; `describe_site` shows the catalog, so an agent
|
|
909
|
+
* or the admin knows what each rule takes.
|
|
910
|
+
*/
|
|
911
|
+
const fieldName = z.string().min(1).describe("A field of the frontmatter, by its name.");
|
|
912
|
+
const NO_PARAMS = z.strictObject({});
|
|
913
|
+
function isValidPattern(pattern) {
|
|
914
|
+
try {
|
|
915
|
+
new RegExp(pattern, "u");
|
|
916
|
+
return true;
|
|
917
|
+
} catch {
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
function rule(name, description, params, make) {
|
|
922
|
+
return {
|
|
923
|
+
name,
|
|
924
|
+
description,
|
|
925
|
+
params,
|
|
926
|
+
make: (raw, severity, id) => make(params.parse(raw), {
|
|
927
|
+
severity,
|
|
928
|
+
...id === void 0 ? {} : { id }
|
|
929
|
+
})
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
const CATALOG = [
|
|
933
|
+
rule("banned-words", "Words that may appear neither in a text field of the frontmatter nor in the body.", z.strictObject({
|
|
934
|
+
words: z.array(z.string().min(1)).min(1).max(200),
|
|
935
|
+
reason: z.string().min(1).max(200).optional().describe("Why, for the fix.")
|
|
936
|
+
}), (params, options) => bannedWords(params, options)),
|
|
937
|
+
rule("field-length", "A text field has between min and max characters, or a list between min and max items; unless names a field that lifts the rule when given.", z.strictObject({
|
|
938
|
+
field: fieldName,
|
|
939
|
+
min: z.number().int().min(0).optional(),
|
|
940
|
+
max: z.number().int().min(0).optional(),
|
|
941
|
+
unless: fieldName.optional()
|
|
942
|
+
}).refine((params) => params.min !== void 0 || params.max !== void 0, { message: "Give min, max or both." }).refine((params) => params.min === void 0 || params.max === void 0 || params.min <= params.max, { message: "min is more than max." }), (params, options) => fieldLength(params, options)),
|
|
943
|
+
rule("field-pattern", "A text field matches a regular expression, as a whole.", z.strictObject({
|
|
944
|
+
field: fieldName,
|
|
945
|
+
pattern: z.string().min(1).max(500).refine(isValidPattern, { message: "Not a regular expression." }),
|
|
946
|
+
description: z.string().min(1).max(200).describe("What the pattern asks for, in words.")
|
|
947
|
+
}), (params, options) => fieldPattern(params, options)),
|
|
948
|
+
rule("images-have-alt", "Every image has alt text that describes it.", NO_PARAMS, (_params, options) => contracts.imagesHaveAlt(options)),
|
|
949
|
+
rule("internal-links-resolve", "Every link to another entry of the site points to one that exists, and is published when this one is.", NO_PARAMS, (_params, options) => contracts.internalLinksResolve(options)),
|
|
950
|
+
rule("required-to-publish", "Fields a draft may leave empty and a published entry must fill.", z.strictObject({ fields: z.array(fieldName).min(1) }), (params, options) => requiredToPublish(params, options)),
|
|
951
|
+
rule("translation-pair", "Every locale of the site has an entry in the translation group of this one, to publish.", NO_PARAMS, (_params, options) => contracts.translationPair(options))
|
|
952
|
+
];
|
|
953
|
+
/** A rule of the catalog by its name, or `undefined`. */
|
|
954
|
+
function catalogRule(name) {
|
|
955
|
+
return CATALOG.find((candidate) => candidate.name === name);
|
|
956
|
+
}
|
|
957
|
+
/** The catalog as `describe_site` shows it: each rule with the JSON Schema of its parameters. */
|
|
958
|
+
function describeCatalog() {
|
|
959
|
+
return CATALOG.map((entry) => ({
|
|
960
|
+
name: entry.name,
|
|
961
|
+
description: entry.description,
|
|
962
|
+
params: z.toJSONSchema(entry.params, {
|
|
963
|
+
target: "draft-2020-12",
|
|
964
|
+
io: "input"
|
|
965
|
+
})
|
|
966
|
+
}));
|
|
967
|
+
}
|
|
968
|
+
//#endregion
|
|
682
969
|
//#region src/contracts/hash.ts
|
|
683
970
|
/**
|
|
684
971
|
* `contractsHash` (ADR-004): what a revision was checked against, in a short string. A stored
|
|
@@ -708,10 +995,11 @@ async function sha256Hex(text) {
|
|
|
708
995
|
function hashContracts(snapshot, contracts) {
|
|
709
996
|
return sha256Hex(canonicalJson({
|
|
710
997
|
components: Object.fromEntries(snapshot.components),
|
|
711
|
-
contracts: contracts.toSorted((left, right) => compareText(left.id, right.id)).map(({ id, phases, severity }) => ({
|
|
998
|
+
contracts: contracts.toSorted((left, right) => compareText(left.id, right.id)).map(({ id, phases, severity, params }) => ({
|
|
712
999
|
id,
|
|
713
1000
|
phases,
|
|
714
|
-
severity
|
|
1001
|
+
severity,
|
|
1002
|
+
params: params === void 0 || Object.keys(params).length === 0 ? void 0 : params
|
|
715
1003
|
})),
|
|
716
1004
|
frontmatter: snapshot.frontmatter
|
|
717
1005
|
}));
|
|
@@ -830,7 +1118,10 @@ async function runContracts(input) {
|
|
|
830
1118
|
phase,
|
|
831
1119
|
errors,
|
|
832
1120
|
warnings,
|
|
833
|
-
contractsHash: await hashContracts(schemaSnapshot(collection), [SCHEMA_CONTRACT, ...contracts].map((contract) =>
|
|
1121
|
+
contractsHash: await hashContracts(schemaSnapshot(collection), [SCHEMA_CONTRACT, ...contracts].map((contract) => ({
|
|
1122
|
+
...describeContract(contract),
|
|
1123
|
+
params: contract.rule?.params
|
|
1124
|
+
})))
|
|
834
1125
|
};
|
|
835
1126
|
}
|
|
836
1127
|
//#endregion
|
|
@@ -884,7 +1175,7 @@ function uniqueTranslationViolation(holder, field) {
|
|
|
884
1175
|
//#endregion
|
|
885
1176
|
//#region src/config/values.ts
|
|
886
1177
|
/** A value with keys: not null, not an array. */
|
|
887
|
-
function isRecord$
|
|
1178
|
+
function isRecord$2(value) {
|
|
888
1179
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
889
1180
|
}
|
|
890
1181
|
/** A string as the messages show it: quoted, with what could break a line escaped. */
|
|
@@ -940,9 +1231,9 @@ const TARGET = "draft-2020-12";
|
|
|
940
1231
|
* The JSON Schema that a schema accepts as input, as plain JSON data. It throws what the library
|
|
941
1232
|
* throws when the schema has no JSON Schema (Zod: `z.date()`, `z.custom()`).
|
|
942
1233
|
*/
|
|
943
|
-
function toJsonSchema(schema) {
|
|
1234
|
+
function toJsonSchema$1(schema) {
|
|
944
1235
|
const converted = schema["~standard"].jsonSchema.input({ target: TARGET });
|
|
945
|
-
if (!isRecord$
|
|
1236
|
+
if (!isRecord$2(converted)) throw new TypeError(`the library ${wrongType(converted, "a JSON Schema object")}`);
|
|
946
1237
|
return JSON.parse(JSON.stringify(converted));
|
|
947
1238
|
}
|
|
948
1239
|
/** Reference chains longer than this are cycles. */
|
|
@@ -954,7 +1245,7 @@ function pointTo(root, ref) {
|
|
|
954
1245
|
let node = root;
|
|
955
1246
|
for (const part of ref.slice(2).split("/")) {
|
|
956
1247
|
const key = part.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
957
|
-
if (!isRecord$
|
|
1248
|
+
if (!isRecord$2(node) || !Object.hasOwn(node, key)) return void 0;
|
|
958
1249
|
node = node[key];
|
|
959
1250
|
}
|
|
960
1251
|
return node;
|
|
@@ -966,7 +1257,7 @@ function pointTo(root, ref) {
|
|
|
966
1257
|
function resolveSchema(root, schema) {
|
|
967
1258
|
let node = schema;
|
|
968
1259
|
for (let hop = 0; hop <= MAX_REF_HOPS; hop += 1) {
|
|
969
|
-
if (!isRecord$
|
|
1260
|
+
if (!isRecord$2(node)) return null;
|
|
970
1261
|
const ref = node["$ref"];
|
|
971
1262
|
if (typeof ref !== "string") return node;
|
|
972
1263
|
node = pointTo(root, ref);
|
|
@@ -1015,7 +1306,7 @@ const NO_BUILT_IN = [];
|
|
|
1015
1306
|
function describeCollection(collection, builtIn) {
|
|
1016
1307
|
return {
|
|
1017
1308
|
name: collection.name,
|
|
1018
|
-
schema: toJsonSchema(collection.schema),
|
|
1309
|
+
schema: toJsonSchema$1(collection.schema),
|
|
1019
1310
|
routes: { ...collection.routes },
|
|
1020
1311
|
translationGroupField: collection.translationGroupField,
|
|
1021
1312
|
tagsField: collection.tagsField,
|
|
@@ -1024,7 +1315,7 @@ function describeCollection(collection, builtIn) {
|
|
|
1024
1315
|
name,
|
|
1025
1316
|
description: component.description ?? null,
|
|
1026
1317
|
example: component.example ?? null,
|
|
1027
|
-
props: toJsonSchema(component.props)
|
|
1318
|
+
props: toJsonSchema$1(component.props)
|
|
1028
1319
|
})),
|
|
1029
1320
|
contracts: [...builtIn, ...collection.contracts].map((contract) => describeContract(contract)),
|
|
1030
1321
|
maxMarkdownBytes: collection.maxMarkdownBytes,
|
|
@@ -1065,6 +1356,7 @@ function describeResolvedSite(site, options = {}) {
|
|
|
1065
1356
|
key: site.key,
|
|
1066
1357
|
locales: [...site.locales],
|
|
1067
1358
|
defaultLocale: site.defaultLocale,
|
|
1359
|
+
url: site.url,
|
|
1068
1360
|
styleGuide: site.styleGuide,
|
|
1069
1361
|
collections: [...site.collections.values()].map((collection) => describeCollection(collection, builtIn))
|
|
1070
1362
|
});
|
|
@@ -1082,14 +1374,14 @@ function standardProps(value) {
|
|
|
1082
1374
|
/** A Standard Schema that also converts to JSON Schema, which is what a collection needs. */
|
|
1083
1375
|
function isDescribedSchema(value) {
|
|
1084
1376
|
const standard = standardProps(value);
|
|
1085
|
-
if (!isRecord$
|
|
1377
|
+
if (!isRecord$2(standard) || typeof standard["validate"] !== "function") return false;
|
|
1086
1378
|
const converter = standard["jsonSchema"];
|
|
1087
|
-
return isRecord$
|
|
1379
|
+
return isRecord$2(converter) && typeof converter["input"] === "function";
|
|
1088
1380
|
}
|
|
1089
1381
|
/** What `isDescribedSchema` found missing, for the message. */
|
|
1090
1382
|
function whyNotDescribed(value) {
|
|
1091
1383
|
const standard = standardProps(value);
|
|
1092
|
-
if (!isRecord$
|
|
1384
|
+
if (!isRecord$2(standard)) return wrongType(value, "a Standard Schema with JSON Schema");
|
|
1093
1385
|
if (typeof standard["validate"] !== "function") return "has no ~standard.validate function, so it is no Standard Schema";
|
|
1094
1386
|
return "has no ~standard.jsonSchema.input function, so describe_site could not show it to agents";
|
|
1095
1387
|
}
|
|
@@ -1107,7 +1399,7 @@ function readSchema(path, raw, kind, problems) {
|
|
|
1107
1399
|
}
|
|
1108
1400
|
let document;
|
|
1109
1401
|
try {
|
|
1110
|
-
document = toJsonSchema(raw);
|
|
1402
|
+
document = toJsonSchema$1(raw);
|
|
1111
1403
|
} catch (error) {
|
|
1112
1404
|
problems.add(path, `cannot be converted to JSON Schema (${oneLine(errorMessage(error))})`, "Remove what JSON Schema cannot express, such as z.date() (use z.iso.date() for dates), z.custom() or z.map(), so that agents can read the schema.");
|
|
1113
1405
|
return null;
|
|
@@ -1159,7 +1451,7 @@ function claimRoute(route, path, site, problems) {
|
|
|
1159
1451
|
}
|
|
1160
1452
|
/** The route of each locale, in the order of the site's locales. */
|
|
1161
1453
|
function readRoutes(path, raw, collection, site, problems) {
|
|
1162
|
-
if (!isRecord$
|
|
1454
|
+
if (!isRecord$2(raw)) {
|
|
1163
1455
|
problems.add(path, wrongType(raw, "an object with one path per locale"), ROUTES_FIX);
|
|
1164
1456
|
return {};
|
|
1165
1457
|
}
|
|
@@ -1207,7 +1499,7 @@ function readField(path, raw, schema, kind, problems) {
|
|
|
1207
1499
|
return null;
|
|
1208
1500
|
}
|
|
1209
1501
|
if (schema === null) return raw;
|
|
1210
|
-
const properties = isRecord$
|
|
1502
|
+
const properties = isRecord$2(schema.object["properties"]) ? schema.object["properties"] : {};
|
|
1211
1503
|
if (!Object.hasOwn(properties, raw)) {
|
|
1212
1504
|
const names = Object.keys(properties);
|
|
1213
1505
|
problems.add(path, `${quote(raw)} is not a property of the schema${names.length === 0 ? ", which declares none" : ` (its properties are ${names.map(quote).join(", ")})`}`, `Name a field that the schema declares, or add ${quote(raw)} to the schema.`);
|
|
@@ -1216,7 +1508,7 @@ function readField(path, raw, schema, kind, problems) {
|
|
|
1216
1508
|
}
|
|
1217
1509
|
function readTranslations(path, raw, schema, problems) {
|
|
1218
1510
|
if (raw === void 0) return null;
|
|
1219
|
-
if (!isRecord$
|
|
1511
|
+
if (!isRecord$2(raw)) {
|
|
1220
1512
|
problems.add(path, wrongType(raw, "an object like { groupBy: \"translationGroup\" }"), "Set it to { groupBy: \"<field>\" }, the field of the frontmatter that holds the key the translations of an entry share, or remove it.");
|
|
1221
1513
|
return null;
|
|
1222
1514
|
}
|
|
@@ -1225,7 +1517,7 @@ function readTranslations(path, raw, schema, problems) {
|
|
|
1225
1517
|
const COMPONENT_FIX = "Declare it with defineComponent({ props: z.strictObject({ … }) }).";
|
|
1226
1518
|
function readComponent(path, name, raw, problems) {
|
|
1227
1519
|
if (!NAME$1.test(name)) problems.add(path, `${quote(name)} is not a valid component name`, nameFix("callout"));
|
|
1228
|
-
if (!isRecord$
|
|
1520
|
+
if (!isRecord$2(raw)) {
|
|
1229
1521
|
problems.add(path, wrongType(raw, "a component"), COMPONENT_FIX);
|
|
1230
1522
|
return null;
|
|
1231
1523
|
}
|
|
@@ -1243,7 +1535,7 @@ function readComponent(path, name, raw, problems) {
|
|
|
1243
1535
|
function readComponents(path, raw, problems) {
|
|
1244
1536
|
const components = /* @__PURE__ */ new Map();
|
|
1245
1537
|
if (raw === void 0) return components;
|
|
1246
|
-
if (!isRecord$
|
|
1538
|
+
if (!isRecord$2(raw)) {
|
|
1247
1539
|
problems.add(path, wrongType(raw, "an object that maps each component name to its definition"), COMPONENT_FIX);
|
|
1248
1540
|
return components;
|
|
1249
1541
|
}
|
|
@@ -1284,7 +1576,7 @@ function checkPhases(path, raw, problems) {
|
|
|
1284
1576
|
* what makes it a `Contract`. Whether it may have its id is a question for `checkIdentity`.
|
|
1285
1577
|
*/
|
|
1286
1578
|
function isContract(item, path, problems) {
|
|
1287
|
-
if (!isRecord$
|
|
1579
|
+
if (!isRecord$2(item)) {
|
|
1288
1580
|
problems.add(path, wrongType(item, "a contract"), "Write it as { id, severity, description, check }.");
|
|
1289
1581
|
return false;
|
|
1290
1582
|
}
|
|
@@ -1332,7 +1624,7 @@ function readContracts(path, raw, problems) {
|
|
|
1332
1624
|
if (isContract(item, itemAt, problems)) {
|
|
1333
1625
|
checkIdentity(item.id, isBuiltInContract(item), itemAt, ids, problems);
|
|
1334
1626
|
contracts.push(item);
|
|
1335
|
-
} else if (isRecord$
|
|
1627
|
+
} else if (isRecord$2(item) && typeof item["id"] === "string" && CONTRACT_ID.test(item["id"])) checkIdentity(item["id"], false, itemAt, ids, problems);
|
|
1336
1628
|
}
|
|
1337
1629
|
return contracts;
|
|
1338
1630
|
}
|
|
@@ -1348,7 +1640,7 @@ function readMaxMarkdownBytes(path, raw, problems) {
|
|
|
1348
1640
|
*/
|
|
1349
1641
|
function readCollection(path, name, raw, site, problems) {
|
|
1350
1642
|
if (!NAME$1.test(name)) problems.add(path, `${quote(name)} is not a valid collection name`, nameFix("blog"));
|
|
1351
|
-
if (!isRecord$
|
|
1643
|
+
if (!isRecord$2(raw)) {
|
|
1352
1644
|
problems.add(path, wrongType(raw, "a collection"), "Declare it with defineCollection({ schema, routes, … }).");
|
|
1353
1645
|
return null;
|
|
1354
1646
|
}
|
|
@@ -1399,7 +1691,7 @@ function readSiteUrl(path, raw, problems) {
|
|
|
1399
1691
|
/** The origins of a site's pages that may read its published content from the browser. */
|
|
1400
1692
|
function readCors(path, raw, problems) {
|
|
1401
1693
|
if (raw === void 0) return { origins: [] };
|
|
1402
|
-
if (!isRecord$
|
|
1694
|
+
if (!isRecord$2(raw) || !Array.isArray(raw["origins"])) {
|
|
1403
1695
|
problems.add(path, wrongType(raw, "an object with origins"), "Declare it as { origins: [\"https://example.com\"] }, or remove it.");
|
|
1404
1696
|
return { origins: [] };
|
|
1405
1697
|
}
|
|
@@ -1416,7 +1708,7 @@ function readBindingName(path, raw, problems) {
|
|
|
1416
1708
|
return null;
|
|
1417
1709
|
}
|
|
1418
1710
|
function readWebhook(path, raw, problems) {
|
|
1419
|
-
if (!isRecord$
|
|
1711
|
+
if (!isRecord$2(raw)) {
|
|
1420
1712
|
problems.add(path, wrongType(raw, "a webhook"), "Declare it as { name: \"newsletter\", url: \"https://…\", secret: \"WEBHOOK_SECRET\" }.");
|
|
1421
1713
|
return null;
|
|
1422
1714
|
}
|
|
@@ -1425,8 +1717,9 @@ function readWebhook(path, raw, problems) {
|
|
|
1425
1717
|
const url = raw["url"];
|
|
1426
1718
|
const urlOk = typeof url === "string" && URL.canParse(url) && new URL(url).protocol === "https:";
|
|
1427
1719
|
if (!urlOk) problems.add(keyPath(path, "url"), wrongType(url, "an https URL"), "Give the https URL that receives the events.");
|
|
1428
|
-
const secret = readBindingName(keyPath(path, "secret"), raw["secret"], problems);
|
|
1429
|
-
if (typeof name !== "string" || !NAME.test(name) || !urlOk
|
|
1720
|
+
const secret = raw["secret"] === void 0 ? null : readBindingName(keyPath(path, "secret"), raw["secret"], problems);
|
|
1721
|
+
if (typeof name !== "string" || !NAME.test(name) || !urlOk) return null;
|
|
1722
|
+
if (raw["secret"] !== void 0 && secret === null) return null;
|
|
1430
1723
|
return {
|
|
1431
1724
|
name,
|
|
1432
1725
|
url: String(url),
|
|
@@ -1440,7 +1733,7 @@ function readEvents(path, raw, siteUrl, problems) {
|
|
|
1440
1733
|
indexNow: null
|
|
1441
1734
|
};
|
|
1442
1735
|
if (raw === void 0) return events;
|
|
1443
|
-
if (!isRecord$
|
|
1736
|
+
if (!isRecord$2(raw)) {
|
|
1444
1737
|
problems.add(path, wrongType(raw, "an object"), "Declare { webhooks: [ … ], indexNow: { … } }.");
|
|
1445
1738
|
return events;
|
|
1446
1739
|
}
|
|
@@ -1464,7 +1757,7 @@ function readEvents(path, raw, siteUrl, problems) {
|
|
|
1464
1757
|
const indexNow = raw["indexNow"];
|
|
1465
1758
|
if (indexNow !== void 0) {
|
|
1466
1759
|
const indexPath = keyPath(path, "indexNow");
|
|
1467
|
-
if (!isRecord$
|
|
1760
|
+
if (!isRecord$2(indexNow)) problems.add(indexPath, wrongType(indexNow, "an object"), "Declare { key: \"INDEXNOW_KEY\" }.");
|
|
1468
1761
|
else {
|
|
1469
1762
|
const key = readBindingName(keyPath(indexPath, "key"), indexNow["key"], problems);
|
|
1470
1763
|
if (siteUrl === null) problems.add(indexPath, "needs the URL of the site", "Set url on the site: IndexNow is told the public URLs of the entries.");
|
|
@@ -1550,7 +1843,7 @@ function readDefaultLocale(path, raw, locales, problems) {
|
|
|
1550
1843
|
const COLLECTIONS_FIX = "Declare at least one: collections: { blog: defineCollection({ schema, routes }) }.";
|
|
1551
1844
|
function readCollections(path, raw, site, problems) {
|
|
1552
1845
|
const collections = /* @__PURE__ */ new Map();
|
|
1553
|
-
if (!isRecord$
|
|
1846
|
+
if (!isRecord$2(raw)) {
|
|
1554
1847
|
problems.add(path, wrongType(raw, "an object that maps each collection name to its collection"), COLLECTIONS_FIX);
|
|
1555
1848
|
return collections;
|
|
1556
1849
|
}
|
|
@@ -1566,7 +1859,7 @@ function readCollections(path, raw, site, problems) {
|
|
|
1566
1859
|
function readSite(key, raw, problems) {
|
|
1567
1860
|
const path = keyPath("sites", key);
|
|
1568
1861
|
if (!isSiteKey(key)) problems.add(path, `${quote(key)} is not a valid site key`, SITE_KEY_FIX);
|
|
1569
|
-
if (!isRecord$
|
|
1862
|
+
if (!isRecord$2(raw)) {
|
|
1570
1863
|
problems.add(path, wrongType(raw, "a site"), "Declare it as { locales: [\"en\"], defaultLocale: \"en\", collections: { … } }.");
|
|
1571
1864
|
return null;
|
|
1572
1865
|
}
|
|
@@ -1593,7 +1886,7 @@ function readSite(key, raw, problems) {
|
|
|
1593
1886
|
}
|
|
1594
1887
|
function readSites(raw, problems) {
|
|
1595
1888
|
const sites = /* @__PURE__ */ new Map();
|
|
1596
|
-
if (!isRecord$
|
|
1889
|
+
if (!isRecord$2(raw)) {
|
|
1597
1890
|
problems.add("sites", wrongType(raw, "an object that maps the site key to the site"), SITE_FIX);
|
|
1598
1891
|
return sites;
|
|
1599
1892
|
}
|
|
@@ -1608,7 +1901,7 @@ function readSites(raw, problems) {
|
|
|
1608
1901
|
/** `auth` of the config: absent, or an object whose `dynamicClientRegistration` is a boolean. */
|
|
1609
1902
|
function readAuth(raw, problems) {
|
|
1610
1903
|
if (raw === void 0) return { dynamicClientRegistration: false };
|
|
1611
|
-
if (!isRecord$
|
|
1904
|
+
if (!isRecord$2(raw)) {
|
|
1612
1905
|
problems.add("auth", wrongType(raw, "an object"), "Write auth: { dynamicClientRegistration: true }, or leave auth out.");
|
|
1613
1906
|
return { dynamicClientRegistration: false };
|
|
1614
1907
|
}
|
|
@@ -1650,7 +1943,7 @@ function resolveConfig(config) {
|
|
|
1650
1943
|
const problems = new Problems();
|
|
1651
1944
|
let sites = /* @__PURE__ */ new Map();
|
|
1652
1945
|
let auth = { dynamicClientRegistration: false };
|
|
1653
|
-
if (isRecord$
|
|
1946
|
+
if (isRecord$2(config)) {
|
|
1654
1947
|
sites = readSites(config["sites"], problems);
|
|
1655
1948
|
auth = readAuth(config["auth"], problems);
|
|
1656
1949
|
} else problems.add("config", wrongType(config, "an object"), "Export the result of defineConfig({ sites: { … } }) from hollware.config.ts.");
|
|
@@ -1661,6 +1954,426 @@ function resolveConfig(config) {
|
|
|
1661
1954
|
};
|
|
1662
1955
|
}
|
|
1663
1956
|
//#endregion
|
|
1957
|
+
//#region src/structure/definition.ts
|
|
1958
|
+
/**
|
|
1959
|
+
* The definition of a site as data (ADR-033): what D1 stores for it and what the operations that
|
|
1960
|
+
* change the structure take. Its shape is checked here; what only makes sense of the whole (a
|
|
1961
|
+
* route with `:slug`, a default locale among the locales, a rule's parameters) is checked when it
|
|
1962
|
+
* is compiled, with one problem for each thing wrong.
|
|
1963
|
+
*/
|
|
1964
|
+
const jsonSchema = z.record(z.string(), z.unknown()).describe("A JSON Schema, draft 2020-12, of an object.");
|
|
1965
|
+
const severity$1 = z.enum(["error", "warning"]);
|
|
1966
|
+
const ruleDefinition = z.strictObject({
|
|
1967
|
+
rule: z.string().min(1).describe("The name of a rule of the catalog that describe_site lists."),
|
|
1968
|
+
severity: severity$1.default("error"),
|
|
1969
|
+
params: z.record(z.string(), z.unknown()).default({}).describe("The parameters of the rule, as the catalog describes them."),
|
|
1970
|
+
id: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).optional().describe("The id of the contract, when a collection lists one rule twice; its name otherwise.")
|
|
1971
|
+
});
|
|
1972
|
+
const componentDefinitionData = z.strictObject({
|
|
1973
|
+
props: jsonSchema.describe("The JSON Schema of the props."),
|
|
1974
|
+
description: z.string().min(1).optional(),
|
|
1975
|
+
example: z.string().min(1).optional()
|
|
1976
|
+
});
|
|
1977
|
+
const collectionDefinitionData = z.strictObject({
|
|
1978
|
+
schema: jsonSchema.describe("The JSON Schema of the frontmatter."),
|
|
1979
|
+
routes: z.record(z.string(), z.string()).describe("The public path of an entry in each locale, with :slug: { \"en\": \"/blog/:slug/\" }."),
|
|
1980
|
+
translationGroupField: z.string().min(1).nullable().default(null),
|
|
1981
|
+
tagsField: z.string().min(1).nullable().default(null),
|
|
1982
|
+
sortByField: z.string().min(1).nullable().default(null),
|
|
1983
|
+
components: z.record(z.string(), componentDefinitionData).default({}),
|
|
1984
|
+
rules: z.array(ruleDefinition).default([]),
|
|
1985
|
+
maxMarkdownBytes: z.number().int().positive().nullable().default(null),
|
|
1986
|
+
styleGuide: z.string().nullable().default(null)
|
|
1987
|
+
});
|
|
1988
|
+
const webhookDefinition = z.strictObject({
|
|
1989
|
+
name: z.string().min(1),
|
|
1990
|
+
url: z.string().min(1).describe("The https URL that receives each event, signed."),
|
|
1991
|
+
secret: z.string().min(1).optional().describe("The Worker secret that signs the deliveries. Leave it out for Hollware to make one with make_webhook_secret and keep it sealed.")
|
|
1992
|
+
});
|
|
1993
|
+
const siteDefinitionData = z.strictObject({
|
|
1994
|
+
locales: z.array(z.string().min(1)).min(1),
|
|
1995
|
+
defaultLocale: z.string().min(1),
|
|
1996
|
+
url: z.string().min(1).nullable().default(null),
|
|
1997
|
+
styleGuide: z.string().nullable().default(null),
|
|
1998
|
+
cors: z.strictObject({ origins: z.array(z.string().min(1)) }).default({ origins: [] }),
|
|
1999
|
+
events: z.strictObject({
|
|
2000
|
+
webhooks: z.array(webhookDefinition).default([]),
|
|
2001
|
+
indexNow: z.strictObject({ key: z.string().min(1) }).nullable().default(null)
|
|
2002
|
+
}).default({
|
|
2003
|
+
webhooks: [],
|
|
2004
|
+
indexNow: null
|
|
2005
|
+
}),
|
|
2006
|
+
collections: z.record(z.string(), collectionDefinitionData)
|
|
2007
|
+
});
|
|
2008
|
+
//#endregion
|
|
2009
|
+
//#region src/structure/json-schema.ts
|
|
2010
|
+
/** The keywords that only say that a part below them failed; that part says what. */
|
|
2011
|
+
const SUMMARIES = /* @__PURE__ */ new Set([
|
|
2012
|
+
"properties",
|
|
2013
|
+
"items",
|
|
2014
|
+
"prefixItems",
|
|
2015
|
+
"additionalProperties",
|
|
2016
|
+
"unevaluatedProperties",
|
|
2017
|
+
"unevaluatedItems",
|
|
2018
|
+
"allOf",
|
|
2019
|
+
"patternProperties",
|
|
2020
|
+
"contains",
|
|
2021
|
+
"dependentSchemas",
|
|
2022
|
+
"$ref"
|
|
2023
|
+
]);
|
|
2024
|
+
/** `#/tags/0` as the path of an issue: `['tags', '0']`. */
|
|
2025
|
+
function segmentsOf$1(location) {
|
|
2026
|
+
return location.replace(/^#\/?/, "").split("/").filter((segment) => segment !== "").map((segment) => decodeURIComponent(segment).replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
2027
|
+
}
|
|
2028
|
+
function isRecord$1(value) {
|
|
2029
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2030
|
+
}
|
|
2031
|
+
/** The schema of the object at `segments`, to tell a declared key from an unknown one. */
|
|
2032
|
+
function declaredKeys(schema, segments) {
|
|
2033
|
+
let node = schema;
|
|
2034
|
+
for (const segment of segments) {
|
|
2035
|
+
if (!isRecord$1(node)) return /* @__PURE__ */ new Set();
|
|
2036
|
+
const properties = node["properties"];
|
|
2037
|
+
node = isRecord$1(properties) && Object.hasOwn(properties, segment) ? properties[segment] : node["items"];
|
|
2038
|
+
}
|
|
2039
|
+
const properties = isRecord$1(node) ? node["properties"] : void 0;
|
|
2040
|
+
return new Set(isRecord$1(properties) ? Object.keys(properties) : []);
|
|
2041
|
+
}
|
|
2042
|
+
/** The issues of a validation: one for each part that failed, at the path of the value. */
|
|
2043
|
+
function issuesOf(schema, errors) {
|
|
2044
|
+
const issues = [];
|
|
2045
|
+
for (const error of errors) {
|
|
2046
|
+
if (SUMMARIES.has(error.keyword)) continue;
|
|
2047
|
+
const path = segmentsOf$1(error.instanceLocation);
|
|
2048
|
+
if (error.keyword === "required") {
|
|
2049
|
+
const missing = /required property "(.*)"/.exec(error.error)?.[1];
|
|
2050
|
+
issues.push({
|
|
2051
|
+
message: missing === void 0 ? error.error : `Missing required field "${missing}".`,
|
|
2052
|
+
path: missing === void 0 ? path : [...path, missing]
|
|
2053
|
+
});
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
if (error.keyword === "false") {
|
|
2057
|
+
const key = path.at(-1);
|
|
2058
|
+
if (key === void 0 || declaredKeys(schema, path.slice(0, -1)).has(key)) continue;
|
|
2059
|
+
issues.push({
|
|
2060
|
+
message: `Unrecognized key "${key}".`,
|
|
2061
|
+
path
|
|
2062
|
+
});
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
issues.push({
|
|
2066
|
+
message: error.error,
|
|
2067
|
+
path
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
return issues;
|
|
2071
|
+
}
|
|
2072
|
+
/** A JSON Schema (draft 2020-12) as a schema of Hollware. */
|
|
2073
|
+
function describedSchemaOf(schema) {
|
|
2074
|
+
const validator = new Validator(schema, "2020-12", false);
|
|
2075
|
+
return { "~standard": {
|
|
2076
|
+
version: 1,
|
|
2077
|
+
vendor: "hollware-json-schema",
|
|
2078
|
+
validate: (value) => {
|
|
2079
|
+
const result = validator.validate(value);
|
|
2080
|
+
return result.valid ? { value } : { issues: issuesOf(schema, result.errors) };
|
|
2081
|
+
},
|
|
2082
|
+
jsonSchema: {
|
|
2083
|
+
input: () => schema,
|
|
2084
|
+
output: () => schema
|
|
2085
|
+
}
|
|
2086
|
+
} };
|
|
2087
|
+
}
|
|
2088
|
+
//#endregion
|
|
2089
|
+
//#region src/structure/compile.ts
|
|
2090
|
+
/**
|
|
2091
|
+
* The sites of a deployment from their definitions in D1 (ADR-033), resolved as a config is: the
|
|
2092
|
+
* same checks, the same defaults, the same `ResolvedSite` the rest of the server reads. It throws
|
|
2093
|
+
* one `ConfigError` with every problem, each at its path (`sites.blog.collections.posts.rules[0]`)
|
|
2094
|
+
* and with its fix, so that an operation that changes the structure answers what to change.
|
|
2095
|
+
*/
|
|
2096
|
+
function isObjectSchema(schema) {
|
|
2097
|
+
return schema["type"] === "object";
|
|
2098
|
+
}
|
|
2099
|
+
function compileCollection(path, collection, problems) {
|
|
2100
|
+
if (!isObjectSchema(collection.schema)) problems.add(keyPath(path, "schema"), "the schema of the frontmatter is not of an object", "Give a JSON Schema with \"type\": \"object\" and the fields under \"properties\".");
|
|
2101
|
+
const components = {};
|
|
2102
|
+
for (const [name, component] of Object.entries(collection.components)) components[name] = {
|
|
2103
|
+
props: describedSchemaOf(component.props),
|
|
2104
|
+
...component.description === void 0 ? {} : { description: component.description },
|
|
2105
|
+
...component.example === void 0 ? {} : { example: component.example }
|
|
2106
|
+
};
|
|
2107
|
+
const contracts = [];
|
|
2108
|
+
collection.rules.forEach((rule, index) => {
|
|
2109
|
+
const at = itemPath(keyPath(path, "rules"), index);
|
|
2110
|
+
const entry = catalogRule(rule.rule);
|
|
2111
|
+
if (entry === void 0) {
|
|
2112
|
+
problems.add(keyPath(at, "rule"), `there is no rule "${rule.rule}" in the catalog`, "Use the name of a rule that describe_site lists in its catalog.");
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
const params = entry.params.safeParse(rule.params);
|
|
2116
|
+
if (!params.success) {
|
|
2117
|
+
problems.add(keyPath(at, "params"), `the parameters of "${rule.rule}" are not valid: ${z.prettifyError(params.error).replaceAll("\n", " ")}`, "Give the parameters as the catalog describes them for this rule.");
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
contracts.push(entry.make(params.data, rule.severity, rule.id));
|
|
2121
|
+
});
|
|
2122
|
+
return {
|
|
2123
|
+
schema: describedSchemaOf(collection.schema),
|
|
2124
|
+
routes: collection.routes,
|
|
2125
|
+
...collection.translationGroupField === null ? {} : { translations: { groupBy: collection.translationGroupField } },
|
|
2126
|
+
...collection.tagsField === null ? {} : { tags: collection.tagsField },
|
|
2127
|
+
...collection.sortByField === null ? {} : { sortBy: collection.sortByField },
|
|
2128
|
+
components,
|
|
2129
|
+
contracts,
|
|
2130
|
+
...collection.maxMarkdownBytes === null ? {} : { maxMarkdownBytes: collection.maxMarkdownBytes },
|
|
2131
|
+
...collection.styleGuide === null ? {} : { styleGuide: collection.styleGuide }
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
function compileSite(path, site, problems) {
|
|
2135
|
+
const collections = {};
|
|
2136
|
+
for (const [name, collection] of Object.entries(site.collections)) collections[name] = compileCollection(keyPath(keyPath(path, "collections"), name), collection, problems);
|
|
2137
|
+
const [first, ...rest] = site.locales;
|
|
2138
|
+
return {
|
|
2139
|
+
locales: [first ?? "", ...rest],
|
|
2140
|
+
defaultLocale: site.defaultLocale,
|
|
2141
|
+
collections,
|
|
2142
|
+
...site.url === null ? {} : { url: site.url },
|
|
2143
|
+
...site.styleGuide === null ? {} : { styleGuide: site.styleGuide },
|
|
2144
|
+
cors: { origins: site.cors.origins },
|
|
2145
|
+
events: {
|
|
2146
|
+
webhooks: site.events.webhooks.map((webhook) => ({
|
|
2147
|
+
name: webhook.name,
|
|
2148
|
+
url: webhook.url,
|
|
2149
|
+
...webhook.secret === void 0 ? {} : { secret: webhook.secret }
|
|
2150
|
+
})),
|
|
2151
|
+
...site.events.indexNow === null ? {} : { indexNow: site.events.indexNow }
|
|
2152
|
+
}
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
/** The definitions of the sites, read from D1 as JSON, checked and resolved. */
|
|
2156
|
+
function compileSites(definitions) {
|
|
2157
|
+
const problems = new Problems();
|
|
2158
|
+
const sites = {};
|
|
2159
|
+
for (const [key, raw] of Object.entries(definitions)) {
|
|
2160
|
+
const path = keyPath("sites", key);
|
|
2161
|
+
const parsed = siteDefinitionData.safeParse(raw);
|
|
2162
|
+
if (!parsed.success) {
|
|
2163
|
+
for (const issue of parsed.error.issues) problems.add([path, ...issue.path.map(String)].join("."), issue.message.replace(/\.$/, ""), "Write the definition as describe_site shows one.");
|
|
2164
|
+
continue;
|
|
2165
|
+
}
|
|
2166
|
+
sites[key] = compileSite(path, parsed.data, problems);
|
|
2167
|
+
}
|
|
2168
|
+
if (problems.list.length > 0) throw new ConfigError(problems.list);
|
|
2169
|
+
if (Object.keys(sites).length === 0) return {
|
|
2170
|
+
sites: /* @__PURE__ */ new Map(),
|
|
2171
|
+
auth: { dynamicClientRegistration: false }
|
|
2172
|
+
};
|
|
2173
|
+
return resolveConfig({ sites });
|
|
2174
|
+
}
|
|
2175
|
+
//#endregion
|
|
2176
|
+
//#region src/structure/from-config.ts
|
|
2177
|
+
function toJsonSchema(schema) {
|
|
2178
|
+
return schema["~standard"].jsonSchema.input({ target: "draft-2020-12" });
|
|
2179
|
+
}
|
|
2180
|
+
function collectionOf(path, collection, problems) {
|
|
2181
|
+
const rules = [];
|
|
2182
|
+
(collection.contracts ?? []).forEach((contract, index) => {
|
|
2183
|
+
if (contract.rule === void 0) {
|
|
2184
|
+
problems.add(itemPath(keyPath(path, "contracts"), index), `the contract "${contract.id}" is code of the config's own, which a definition cannot hold`, "Replace it with a rule of the catalog (contracts.fieldLength, contracts.fieldPattern, contracts.requiredToPublish, contracts.bannedWords, or the built-in ones), then deploy again.");
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
rules.push({
|
|
2188
|
+
rule: contract.rule.name,
|
|
2189
|
+
severity: contract.severity,
|
|
2190
|
+
params: { ...contract.rule.params },
|
|
2191
|
+
...contract.id === contract.rule.name ? {} : { id: contract.id }
|
|
2192
|
+
});
|
|
2193
|
+
});
|
|
2194
|
+
return {
|
|
2195
|
+
schema: toJsonSchema(collection.schema),
|
|
2196
|
+
routes: { ...collection.routes },
|
|
2197
|
+
translationGroupField: collection.translations?.groupBy ?? null,
|
|
2198
|
+
tagsField: collection.tags ?? null,
|
|
2199
|
+
sortByField: collection.sortBy ?? null,
|
|
2200
|
+
components: Object.fromEntries(Object.entries(collection.components ?? {}).map(([name, component]) => [name, {
|
|
2201
|
+
props: toJsonSchema(component.props),
|
|
2202
|
+
...component.description === void 0 ? {} : { description: component.description },
|
|
2203
|
+
...component.example === void 0 ? {} : { example: component.example }
|
|
2204
|
+
}])),
|
|
2205
|
+
rules,
|
|
2206
|
+
maxMarkdownBytes: collection.maxMarkdownBytes ?? null,
|
|
2207
|
+
styleGuide: collection.styleGuide ?? null
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
/** Every site of the config as a definition, or a `ConfigError` with what keeps one from moving. */
|
|
2211
|
+
function definitionsFromConfig(config) {
|
|
2212
|
+
const problems = new Problems();
|
|
2213
|
+
const definitions = {};
|
|
2214
|
+
for (const [key, site] of Object.entries(config.sites ?? {})) {
|
|
2215
|
+
const path = keyPath("sites", key);
|
|
2216
|
+
definitions[key] = {
|
|
2217
|
+
locales: [...site.locales],
|
|
2218
|
+
defaultLocale: site.defaultLocale,
|
|
2219
|
+
url: site.url ?? null,
|
|
2220
|
+
styleGuide: site.styleGuide ?? null,
|
|
2221
|
+
cors: { origins: [...site.cors?.origins ?? []] },
|
|
2222
|
+
events: {
|
|
2223
|
+
webhooks: (site.events?.webhooks ?? []).map((webhook) => ({ ...webhook })),
|
|
2224
|
+
indexNow: site.events?.indexNow ?? null
|
|
2225
|
+
},
|
|
2226
|
+
collections: Object.fromEntries(Object.entries(site.collections).map(([name, collection]) => [name, collectionOf(keyPath(keyPath(path, "collections"), name), collection, problems)]))
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
if (problems.list.length > 0) throw new ConfigError(problems.list);
|
|
2230
|
+
return definitions;
|
|
2231
|
+
}
|
|
2232
|
+
//#endregion
|
|
2233
|
+
//#region src/structure/presets.ts
|
|
2234
|
+
const PRESETS = [
|
|
2235
|
+
"blog",
|
|
2236
|
+
"docs",
|
|
2237
|
+
"empty"
|
|
2238
|
+
];
|
|
2239
|
+
/** The path of an entry in each locale: the default one at the root, the others under theirs. */
|
|
2240
|
+
function routesFor(name, locales, defaultLocale) {
|
|
2241
|
+
return Object.fromEntries(locales.map((locale) => [locale, locale === defaultLocale ? `/${name}/:slug/` : `/${locale}/${name}/:slug/`]));
|
|
2242
|
+
}
|
|
2243
|
+
function collectionPreset(preset, name, locales, defaultLocale) {
|
|
2244
|
+
const translated = locales.length > 1;
|
|
2245
|
+
const base = {
|
|
2246
|
+
routes: routesFor(name, locales, defaultLocale),
|
|
2247
|
+
translationGroupField: null,
|
|
2248
|
+
tagsField: null,
|
|
2249
|
+
sortByField: null,
|
|
2250
|
+
components: {},
|
|
2251
|
+
rules: [],
|
|
2252
|
+
maxMarkdownBytes: null,
|
|
2253
|
+
styleGuide: null
|
|
2254
|
+
};
|
|
2255
|
+
switch (preset) {
|
|
2256
|
+
case "empty": return {
|
|
2257
|
+
...base,
|
|
2258
|
+
schema: {
|
|
2259
|
+
type: "object",
|
|
2260
|
+
properties: { title: {
|
|
2261
|
+
type: "string",
|
|
2262
|
+
minLength: 1
|
|
2263
|
+
} },
|
|
2264
|
+
required: ["title"]
|
|
2265
|
+
}
|
|
2266
|
+
};
|
|
2267
|
+
case "blog": return {
|
|
2268
|
+
...base,
|
|
2269
|
+
schema: {
|
|
2270
|
+
type: "object",
|
|
2271
|
+
properties: {
|
|
2272
|
+
title: {
|
|
2273
|
+
type: "string",
|
|
2274
|
+
minLength: 1
|
|
2275
|
+
},
|
|
2276
|
+
description: {
|
|
2277
|
+
type: "string",
|
|
2278
|
+
minLength: 1,
|
|
2279
|
+
maxLength: 160
|
|
2280
|
+
},
|
|
2281
|
+
date: {
|
|
2282
|
+
type: "string",
|
|
2283
|
+
format: "date"
|
|
2284
|
+
},
|
|
2285
|
+
tags: {
|
|
2286
|
+
type: "array",
|
|
2287
|
+
items: {
|
|
2288
|
+
type: "string",
|
|
2289
|
+
minLength: 1
|
|
2290
|
+
}
|
|
2291
|
+
},
|
|
2292
|
+
...translated ? { translationGroup: {
|
|
2293
|
+
type: "string",
|
|
2294
|
+
minLength: 1
|
|
2295
|
+
} } : {}
|
|
2296
|
+
},
|
|
2297
|
+
required: [
|
|
2298
|
+
"title",
|
|
2299
|
+
"description",
|
|
2300
|
+
"date",
|
|
2301
|
+
...translated ? ["translationGroup"] : []
|
|
2302
|
+
]
|
|
2303
|
+
},
|
|
2304
|
+
translationGroupField: translated ? "translationGroup" : null,
|
|
2305
|
+
tagsField: "tags",
|
|
2306
|
+
sortByField: "date",
|
|
2307
|
+
rules: [
|
|
2308
|
+
{
|
|
2309
|
+
rule: "images-have-alt",
|
|
2310
|
+
severity: "error",
|
|
2311
|
+
params: {}
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
rule: "internal-links-resolve",
|
|
2315
|
+
severity: "warning",
|
|
2316
|
+
params: {}
|
|
2317
|
+
},
|
|
2318
|
+
...translated ? [{
|
|
2319
|
+
rule: "translation-pair",
|
|
2320
|
+
severity: "warning",
|
|
2321
|
+
params: {}
|
|
2322
|
+
}] : []
|
|
2323
|
+
]
|
|
2324
|
+
};
|
|
2325
|
+
case "docs": return {
|
|
2326
|
+
...base,
|
|
2327
|
+
schema: {
|
|
2328
|
+
type: "object",
|
|
2329
|
+
properties: {
|
|
2330
|
+
title: {
|
|
2331
|
+
type: "string",
|
|
2332
|
+
minLength: 1,
|
|
2333
|
+
maxLength: 70
|
|
2334
|
+
},
|
|
2335
|
+
description: {
|
|
2336
|
+
type: "string",
|
|
2337
|
+
minLength: 1,
|
|
2338
|
+
maxLength: 160
|
|
2339
|
+
},
|
|
2340
|
+
order: {
|
|
2341
|
+
type: "integer",
|
|
2342
|
+
minimum: 0
|
|
2343
|
+
}
|
|
2344
|
+
},
|
|
2345
|
+
required: ["title", "description"]
|
|
2346
|
+
},
|
|
2347
|
+
rules: [{
|
|
2348
|
+
rule: "images-have-alt",
|
|
2349
|
+
severity: "error",
|
|
2350
|
+
params: {}
|
|
2351
|
+
}, {
|
|
2352
|
+
rule: "internal-links-resolve",
|
|
2353
|
+
severity: "warning",
|
|
2354
|
+
params: {}
|
|
2355
|
+
}]
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
/** A new site: its locales, and one collection from the preset. */
|
|
2360
|
+
function sitePreset(preset, locales, defaultLocale) {
|
|
2361
|
+
const name = preset === "empty" ? "pages" : preset;
|
|
2362
|
+
const collections = { [name]: collectionPreset(preset, name, locales, defaultLocale) };
|
|
2363
|
+
return {
|
|
2364
|
+
locales: [...locales],
|
|
2365
|
+
defaultLocale,
|
|
2366
|
+
url: null,
|
|
2367
|
+
styleGuide: null,
|
|
2368
|
+
cors: { origins: [] },
|
|
2369
|
+
events: {
|
|
2370
|
+
webhooks: [],
|
|
2371
|
+
indexNow: null
|
|
2372
|
+
},
|
|
2373
|
+
collections
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
//#endregion
|
|
1664
2377
|
//#region src/errors.ts
|
|
1665
2378
|
/**
|
|
1666
2379
|
* The errors of an operation that are none of its outcomes: the caller asked for something it
|
|
@@ -1715,6 +2428,8 @@ const ID_PREFIXES = {
|
|
|
1715
2428
|
reviewComment: "rvc",
|
|
1716
2429
|
schedule: "sch",
|
|
1717
2430
|
session: "ses",
|
|
2431
|
+
siteChange: "sit",
|
|
2432
|
+
siteSecret: "sec",
|
|
1718
2433
|
token: "tok",
|
|
1719
2434
|
user: "usr"
|
|
1720
2435
|
};
|
|
@@ -3112,6 +3827,7 @@ const entrySummary = z.object({
|
|
|
3112
3827
|
tags: z.array(z.string()),
|
|
3113
3828
|
hasUnpublishedChanges: z.boolean().describe("Whether the latest revision differs from the published one."),
|
|
3114
3829
|
publishedAt: z.string().nullable().describe("When the entry was first published, or null."),
|
|
3830
|
+
scheduledAt: z.string().nullable().describe("When a scheduled entry will be published, or null for one that is not scheduled."),
|
|
3115
3831
|
updatedAt: z.string()
|
|
3116
3832
|
});
|
|
3117
3833
|
const publication = z.object({
|
|
@@ -3209,6 +3925,7 @@ const siteDescription = z.object({
|
|
|
3209
3925
|
key: z.string(),
|
|
3210
3926
|
locales: z.array(z.string()),
|
|
3211
3927
|
defaultLocale: z.string(),
|
|
3928
|
+
url: z.string().nullable().describe("The public origin of the site, which starts the URL of each entry, or null."),
|
|
3212
3929
|
styleGuide: z.string().nullable(),
|
|
3213
3930
|
collections: z.array(collectionDescription)
|
|
3214
3931
|
});
|
|
@@ -3608,23 +4325,205 @@ const contentStats = defineOperation({
|
|
|
3608
4325
|
unavailable: { isError: true }
|
|
3609
4326
|
}
|
|
3610
4327
|
});
|
|
4328
|
+
const siteKey = z.string().refine(isSiteKey, { message: "Not a site key: lowercase words joined by hyphens, such as my-site." }).describe("The key of a site: lowercase letters and digits joined by hyphens, such as my-site.");
|
|
4329
|
+
const structureVersion = z.number().int().min(0).describe("The version of the structure you read with get_structure. Another one answers conflict.");
|
|
4330
|
+
const confirm = z.boolean().default(false).describe("true to go ahead when needs_confirmation listed what the change does to existing entries.");
|
|
4331
|
+
const problem = z.object({
|
|
4332
|
+
path: z.string().describe("Where, from the root: sites.<key>.collections.<name>.schema."),
|
|
4333
|
+
message: z.string(),
|
|
4334
|
+
fix: z.string()
|
|
4335
|
+
});
|
|
4336
|
+
/** What a change of the structure would do to the entries a site already has. */
|
|
4337
|
+
const impact = z.object({
|
|
4338
|
+
removedCollections: z.array(z.object({
|
|
4339
|
+
name: z.string(),
|
|
4340
|
+
entries: z.number().int()
|
|
4341
|
+
})).describe("Collections the change removes, with how many entries each holds."),
|
|
4342
|
+
removedLocales: z.array(z.object({
|
|
4343
|
+
locale: z.string(),
|
|
4344
|
+
entries: z.number().int()
|
|
4345
|
+
})).describe("Locales the change removes, with how many entries each holds."),
|
|
4346
|
+
refused: z.array(z.object({
|
|
4347
|
+
collection: z.string(),
|
|
4348
|
+
entries: z.number().int().describe("Entries whose latest frontmatter the new schema refuses."),
|
|
4349
|
+
examples: z.array(z.string()).describe("A few of their slugs.")
|
|
4350
|
+
})).describe("Collections whose new schema refuses entries they hold; they stay, and their next write is checked against it.")
|
|
4351
|
+
});
|
|
4352
|
+
const structureChange = z.object({
|
|
4353
|
+
outcome: z.enum([
|
|
4354
|
+
"updated",
|
|
4355
|
+
"unchanged",
|
|
4356
|
+
"invalid",
|
|
4357
|
+
"needs_confirmation",
|
|
4358
|
+
"conflict"
|
|
4359
|
+
]),
|
|
4360
|
+
version: z.number().int().optional().describe("The version of the structure now."),
|
|
4361
|
+
problems: z.array(problem).optional().describe("With invalid: every problem, each with its fix."),
|
|
4362
|
+
impact: impact.optional().describe("With needs_confirmation: what the change would do."),
|
|
4363
|
+
currentVersion: z.number().int().optional().describe("With conflict: the version to read again.")
|
|
4364
|
+
});
|
|
4365
|
+
const STRUCTURE_OUTCOMES = {
|
|
4366
|
+
updated: { isError: false },
|
|
4367
|
+
unchanged: { isError: false },
|
|
4368
|
+
invalid: { isError: true },
|
|
4369
|
+
needs_confirmation: { isError: false },
|
|
4370
|
+
conflict: { isError: true }
|
|
4371
|
+
};
|
|
4372
|
+
const getStructure = defineOperation({
|
|
4373
|
+
name: "get_structure",
|
|
4374
|
+
title: "Get the structure of the site",
|
|
4375
|
+
description: "Gets the definition of the site as update_site and set_collection take it: its locales, URL, CORS origins, events, style guide, and each collection with the JSON Schema of its frontmatter, its routes, components and rules; with its version, and the catalog of rules a collection may use, each with the JSON Schema of its parameters.",
|
|
4376
|
+
input: z.strictObject({}),
|
|
4377
|
+
output: z.object({
|
|
4378
|
+
outcome: z.enum(["ok"]),
|
|
4379
|
+
site: z.string(),
|
|
4380
|
+
version: z.number().int(),
|
|
4381
|
+
definition: siteDefinitionData,
|
|
4382
|
+
catalog: z.array(z.object({
|
|
4383
|
+
name: z.string(),
|
|
4384
|
+
description: z.string(),
|
|
4385
|
+
params: z.record(z.string(), z.unknown()).describe("The JSON Schema of its parameters.")
|
|
4386
|
+
}))
|
|
4387
|
+
}),
|
|
4388
|
+
scopes: ["content:read"],
|
|
4389
|
+
effect: "read",
|
|
4390
|
+
outcomes: { ok: { isError: false } }
|
|
4391
|
+
});
|
|
4392
|
+
const updateSite = defineOperation({
|
|
4393
|
+
name: "update_site",
|
|
4394
|
+
title: "Change the structure of the site",
|
|
4395
|
+
description: "Replaces the definition of the site with another, at once and with no deploy: read it with get_structure, change it, and send it with the version you read. Every problem is answered at its path with its fix, and nothing changes. A change that removes a collection or a locale with entries, or a schema that refuses entries the site holds, answers needs_confirmation with what it would do; send it again with confirm to go ahead. Entries are never deleted: a removed collection keeps them out of reach until it comes back.",
|
|
4396
|
+
input: z.strictObject({
|
|
4397
|
+
version: structureVersion,
|
|
4398
|
+
definition: siteDefinitionData,
|
|
4399
|
+
confirm
|
|
4400
|
+
}),
|
|
4401
|
+
output: structureChange.describe("What became of the definition."),
|
|
4402
|
+
scopes: ["schema:admin"],
|
|
4403
|
+
effect: "destructive",
|
|
4404
|
+
outcomes: STRUCTURE_OUTCOMES
|
|
4405
|
+
});
|
|
4406
|
+
const setCollection = defineOperation({
|
|
4407
|
+
name: "set_collection",
|
|
4408
|
+
title: "Add, change or remove a collection",
|
|
4409
|
+
description: "Adds the collection name to the site, replaces it, or removes it with collection null, leaving the rest of the definition as it is; with the version of the structure you read. Answers as update_site does.",
|
|
4410
|
+
input: z.strictObject({
|
|
4411
|
+
version: structureVersion,
|
|
4412
|
+
name: z.string().regex(/^[a-z][a-z0-9-]*$/).describe("The name of the collection: lowercase letters, digits and hyphens, such as blog."),
|
|
4413
|
+
collection: collectionDefinitionData.nullable().describe("The collection, or null to remove it."),
|
|
4414
|
+
confirm
|
|
4415
|
+
}),
|
|
4416
|
+
output: structureChange.describe("What became of the collection."),
|
|
4417
|
+
scopes: ["schema:admin"],
|
|
4418
|
+
effect: "destructive",
|
|
4419
|
+
outcomes: STRUCTURE_OUTCOMES
|
|
4420
|
+
});
|
|
4421
|
+
const createSite = defineOperation({
|
|
4422
|
+
name: "create_site",
|
|
4423
|
+
title: "Create a site",
|
|
4424
|
+
description: "Creates a site in this deployment, with its definition, and makes you its owner. It is served at once; to work in it, get a token of that site (the admin switches to it).",
|
|
4425
|
+
input: z.strictObject({
|
|
4426
|
+
key: siteKey,
|
|
4427
|
+
definition: siteDefinitionData
|
|
4428
|
+
}),
|
|
4429
|
+
output: z.object({
|
|
4430
|
+
outcome: z.enum([
|
|
4431
|
+
"created",
|
|
4432
|
+
"taken",
|
|
4433
|
+
"invalid"
|
|
4434
|
+
]),
|
|
4435
|
+
version: z.number().int().optional(),
|
|
4436
|
+
problems: z.array(problem).optional().describe("With invalid: every problem, each with its fix.")
|
|
4437
|
+
}),
|
|
4438
|
+
scopes: ["schema:admin"],
|
|
4439
|
+
effect: "write",
|
|
4440
|
+
outcomes: {
|
|
4441
|
+
created: { isError: false },
|
|
4442
|
+
taken: { isError: true },
|
|
4443
|
+
invalid: { isError: true }
|
|
4444
|
+
}
|
|
4445
|
+
});
|
|
4446
|
+
const deleteSite = defineOperation({
|
|
4447
|
+
name: "delete_site",
|
|
4448
|
+
title: "Put a site in the trash",
|
|
4449
|
+
description: "Puts a site you own in the trash: it is served no more, and its tokens stop working. Nothing is deleted; restore_site brings it back, from a token of another site you own. The first call answers needs_confirmation with how many entries the site holds; call again with confirm.",
|
|
4450
|
+
input: z.strictObject({
|
|
4451
|
+
key: siteKey,
|
|
4452
|
+
confirm
|
|
4453
|
+
}),
|
|
4454
|
+
output: z.object({
|
|
4455
|
+
outcome: z.enum([
|
|
4456
|
+
"trashed",
|
|
4457
|
+
"needs_confirmation",
|
|
4458
|
+
"not_found"
|
|
4459
|
+
]),
|
|
4460
|
+
entries: z.number().int().optional().describe("With needs_confirmation: the entries the site holds.")
|
|
4461
|
+
}),
|
|
4462
|
+
scopes: ["schema:admin"],
|
|
4463
|
+
effect: "destructive",
|
|
4464
|
+
outcomes: {
|
|
4465
|
+
trashed: { isError: false },
|
|
4466
|
+
needs_confirmation: { isError: false },
|
|
4467
|
+
not_found: { isError: true }
|
|
4468
|
+
}
|
|
4469
|
+
});
|
|
4470
|
+
const restoreSite = defineOperation({
|
|
4471
|
+
name: "restore_site",
|
|
4472
|
+
title: "Bring a site back from the trash",
|
|
4473
|
+
description: "Brings back a site you own from the trash, as it was: served again, with its entries and tokens.",
|
|
4474
|
+
input: z.strictObject({ key: siteKey }),
|
|
4475
|
+
output: z.object({ outcome: z.enum(["restored", "not_found"]) }),
|
|
4476
|
+
scopes: ["schema:admin"],
|
|
4477
|
+
effect: "write",
|
|
4478
|
+
outcomes: {
|
|
4479
|
+
restored: { isError: false },
|
|
4480
|
+
not_found: { isError: true }
|
|
4481
|
+
}
|
|
4482
|
+
});
|
|
3611
4483
|
/** Every operation, sorted by name: the order of `tools/list`. */
|
|
3612
4484
|
const OPERATIONS = [
|
|
3613
4485
|
approveReview,
|
|
3614
4486
|
commentReview,
|
|
3615
4487
|
contentStats,
|
|
3616
4488
|
createDraft,
|
|
4489
|
+
createSite,
|
|
4490
|
+
deleteSite,
|
|
3617
4491
|
describeSite,
|
|
3618
4492
|
getEntry,
|
|
4493
|
+
getStructure,
|
|
3619
4494
|
getTranslations,
|
|
3620
4495
|
listEntries,
|
|
3621
4496
|
listReviews,
|
|
4497
|
+
defineOperation({
|
|
4498
|
+
name: "make_webhook_secret",
|
|
4499
|
+
title: "Make the secret of a webhook",
|
|
4500
|
+
description: "Makes a new secret for a webhook of the site that names no Worker secret of its own, keeps it sealed, and answers it once: give it to the receiver, which checks each delivery's signature with it. A new one replaces the old at once. unavailable when the deployment has no HOLLWARE_SECRET to seal it with.",
|
|
4501
|
+
input: z.strictObject({ webhook: z.string().min(1).describe("The name of the webhook, as the definition of the site has it.") }),
|
|
4502
|
+
output: z.object({
|
|
4503
|
+
outcome: z.enum([
|
|
4504
|
+
"made",
|
|
4505
|
+
"not_found",
|
|
4506
|
+
"unavailable"
|
|
4507
|
+
]),
|
|
4508
|
+
secret: z.string().optional().describe("With made: the secret, shown this once.")
|
|
4509
|
+
}),
|
|
4510
|
+
scopes: ["schema:admin"],
|
|
4511
|
+
effect: "write",
|
|
4512
|
+
outcomes: {
|
|
4513
|
+
made: { isError: false },
|
|
4514
|
+
not_found: { isError: true },
|
|
4515
|
+
unavailable: { isError: true }
|
|
4516
|
+
}
|
|
4517
|
+
}),
|
|
3622
4518
|
publish,
|
|
3623
4519
|
requestChanges,
|
|
3624
4520
|
requestReview,
|
|
4521
|
+
restoreSite,
|
|
3625
4522
|
search,
|
|
4523
|
+
setCollection,
|
|
3626
4524
|
unpublish,
|
|
3627
4525
|
updateDraft,
|
|
4526
|
+
updateSite,
|
|
3628
4527
|
validate
|
|
3629
4528
|
];
|
|
3630
4529
|
//#endregion
|
|
@@ -3739,82 +4638,58 @@ async function verifyWebhook(check) {
|
|
|
3739
4638
|
return sameText(signature, await signWebhook(check.secret, timestamp, check.body));
|
|
3740
4639
|
}
|
|
3741
4640
|
//#endregion
|
|
3742
|
-
//#region src/
|
|
4641
|
+
//#region src/cache.ts
|
|
3743
4642
|
/**
|
|
3744
|
-
* The
|
|
3745
|
-
*
|
|
3746
|
-
*
|
|
4643
|
+
* The cache tags of Hollware's content (S8): one name for each thing a page can show, so that a
|
|
4644
|
+
* site tags each page with what it read and purges, when an event says what changed, the pages
|
|
4645
|
+
* that showed it. Hollware puts the tags an event invalidates in its payload; `@hollware/nuxt`
|
|
4646
|
+
* tags its pages with the same functions. A tag is at most 1,024 characters of printable ASCII
|
|
4647
|
+
* with no space or comma, which every cache that purges by tag takes.
|
|
4648
|
+
*
|
|
4649
|
+
* - `entry:<id>`: a page that shows the entry, its own or one that embeds it;
|
|
4650
|
+
* - `list:<collection>:<locale>`: a page that lists the entries of a collection in a locale, and
|
|
4651
|
+
* `list:<collection>:*` one that lists them in every locale;
|
|
4652
|
+
* - `group:<collection>:<group>`: a page that links to the translations of a group;
|
|
4653
|
+
* - `site`: a page that shows the structure of the site, such as its navigation.
|
|
3747
4654
|
*/
|
|
3748
|
-
const
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
};
|
|
3754
|
-
/** The text a reader sees in a node: its strings, never its tag, attributes or comments. */
|
|
3755
|
-
function textOf(node) {
|
|
3756
|
-
if (typeof node === "string") return node;
|
|
3757
|
-
if (!Array.isArray(node) || node[0] === null) return "";
|
|
3758
|
-
return node.slice(2).map((child) => textOf(child)).join("");
|
|
4655
|
+
const SITE_TAG = "site";
|
|
4656
|
+
const MAX = 1024;
|
|
4657
|
+
/** A part of a tag in the characters a tag may hold: percent-encoded, commas and all. */
|
|
4658
|
+
function part(value) {
|
|
4659
|
+
return encodeURIComponent(value).replaceAll(",", "%2C").replaceAll("'", "%27");
|
|
3759
4660
|
}
|
|
3760
|
-
function
|
|
3761
|
-
return
|
|
4661
|
+
function cut(tag) {
|
|
4662
|
+
return tag.length <= MAX ? tag : tag.slice(0, MAX);
|
|
3762
4663
|
}
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
4664
|
+
function entryTag(id) {
|
|
4665
|
+
return cut(`entry:${part(id)}`);
|
|
4666
|
+
}
|
|
4667
|
+
function listTag(collection, locale) {
|
|
4668
|
+
return cut(`list:${part(collection)}:${part(locale)}`);
|
|
4669
|
+
}
|
|
4670
|
+
function groupTag(collection, group) {
|
|
4671
|
+
return cut(`group:${part(collection)}:${part(group)}`);
|
|
4672
|
+
}
|
|
4673
|
+
/** The locale of a list of every locale. */
|
|
4674
|
+
const EVERY_LOCALE = "*";
|
|
4675
|
+
/** The tags of a page that shows these entries, each its own. */
|
|
4676
|
+
function tagsOfEntries(entries) {
|
|
4677
|
+
return [...new Set(entries.map((entry) => entryTag(entry.id)))];
|
|
3770
4678
|
}
|
|
3771
4679
|
/**
|
|
3772
|
-
* The
|
|
3773
|
-
*
|
|
3774
|
-
* it repeats the title. A section with no text is left out.
|
|
4680
|
+
* The tags an event about an entry invalidates: the pages that show it, the lists of its
|
|
4681
|
+
* collection in its locale and in every locale, and the pages that link to its translations.
|
|
3775
4682
|
*/
|
|
3776
|
-
function
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
sections.push({
|
|
3784
|
-
heading: textOf(node).trim(),
|
|
3785
|
-
parts: []
|
|
3786
|
-
});
|
|
3787
|
-
continue;
|
|
3788
|
-
}
|
|
3789
|
-
const text = textOf(node);
|
|
3790
|
-
const current = sections.at(-1);
|
|
3791
|
-
if (current === void 0) intro.push(text);
|
|
3792
|
-
else current.parts.push(text);
|
|
3793
|
-
}
|
|
3794
|
-
const chunks = [{
|
|
3795
|
-
index: 0,
|
|
3796
|
-
kind: "summary",
|
|
3797
|
-
heading: null,
|
|
3798
|
-
text: fit([
|
|
3799
|
-
entry.title,
|
|
3800
|
-
entry.description ?? "",
|
|
3801
|
-
intro.join(" ")
|
|
3802
|
-
].filter((part) => part !== "").join("\n"), CHUNK_LIMITS.summary)
|
|
3803
|
-
}];
|
|
3804
|
-
for (const section of sections) {
|
|
3805
|
-
const body = section.parts.join(" ").trim();
|
|
3806
|
-
if (body === "") continue;
|
|
3807
|
-
chunks.push({
|
|
3808
|
-
index: chunks.length,
|
|
3809
|
-
kind: "section",
|
|
3810
|
-
heading: section.heading,
|
|
3811
|
-
text: fit(`${section.heading}\n${body}`, CHUNK_LIMITS.section)
|
|
3812
|
-
});
|
|
3813
|
-
}
|
|
3814
|
-
return chunks;
|
|
4683
|
+
function tagsOfChange(entry) {
|
|
4684
|
+
return [
|
|
4685
|
+
entryTag(entry.id),
|
|
4686
|
+
listTag(entry.collection, entry.locale),
|
|
4687
|
+
listTag(entry.collection, "*"),
|
|
4688
|
+
...entry.translationGroup === null ? [] : [groupTag(entry.collection, entry.translationGroup)]
|
|
4689
|
+
];
|
|
3815
4690
|
}
|
|
3816
4691
|
//#endregion
|
|
3817
4692
|
//#region src/index.ts
|
|
3818
4693
|
const name = "@hollware/core";
|
|
3819
4694
|
//#endregion
|
|
3820
|
-
export { BUILT_IN_CONTRACT_IDS, CHUNKER_VERSION, CHUNK_LIMITS, COMARK_VERSION, ConfigError, DEFAULT_MAX_MARKDOWN_BYTES, ERROR_CODES, EVENT_ID_HEADER, HollwareError, ID_PREFIXES, MAX_MARKDOWN_BYTES_CEILING, OPERATIONS, PERSONAL_TOKEN_PREFIX, PERSONAL_TOKEN_TTL_DAYS, PIPELINE_VERSION, READ_TOKEN_PREFIX, REFUSAL_CONTRACT_IDS, SCOPES, SIGNATURE_HEADER, SITE_KEY_MAX_LENGTH, SITE_KEY_PATTERN, TIMESTAMP_HEADER, approveReview, chunksOf, commentReview, contentStats, contracts, createDraft, defineCollection, defineComponent, defineConfig, defineContract, defineOperation, describeBuiltInContracts, describeContract, describeResolvedSite, describeSite, generatePersonalToken, generateReadToken, getEntry, getTranslations, hashPersonalToken, isBuiltInContract, isId, isPersonalToken, isReadToken, isScope, isSiteKey, listEntries, listReviews, name, newId, processMarkdown, publish, renderHtml, requestChanges, requestReview, resolveConfig, runContracts, schemas_exports as schemas, search, signWebhook, sizeLimitViolation, ulid, uniqueSlugViolation, uniqueTranslationViolation, unpublish, updateDraft, validate, verifyWebhook };
|
|
4695
|
+
export { BUILT_IN_CONTRACT_IDS, CATALOG, CHUNKER_VERSION, CHUNK_LIMITS, COMARK_VERSION, ConfigError, DEFAULT_MAX_MARKDOWN_BYTES, ERROR_CODES, EVENT_ID_HEADER, EVERY_LOCALE, HollwareError, ID_PREFIXES, MAX_MARKDOWN_BYTES_CEILING, OPERATIONS, PERSONAL_TOKEN_PREFIX, PERSONAL_TOKEN_TTL_DAYS, PIPELINE_VERSION, PRESETS, READ_TOKEN_PREFIX, REFUSAL_CONTRACT_IDS, SCOPES, SIGNATURE_HEADER, SITE_KEY_MAX_LENGTH, SITE_KEY_PATTERN, SITE_TAG, TIMESTAMP_HEADER, approveReview, canonicalJson, catalogRule, chunksOf, collectionDefinitionData, collectionPreset, commentReview, compileSites, componentDefinitionData, contentStats, contracts, createDraft, defineCollection, defineComponent, defineConfig, defineContract, defineOperation, definitionsFromConfig, describeBuiltInContracts, describeCatalog, describeContract, describeResolvedSite, describeSite, describedSchemaOf, entryTag, generatePersonalToken, generateReadToken, getEntry, getTranslations, groupTag, hashPersonalToken, isBuiltInContract, isId, isPersonalToken, isReadToken, isScope, isSiteKey, listEntries, listReviews, listTag, name, newId, processMarkdown, publish, renderHtml, requestChanges, requestReview, resolveConfig, routesFor, ruleDefinition, runContracts, schemas_exports as schemas, search, signWebhook, siteDefinitionData, sitePreset, sizeLimitViolation, tagsOfChange, tagsOfEntries, ulid, uniqueSlugViolation, uniqueTranslationViolation, unpublish, updateDraft, validate, verifyWebhook, webhookDefinition };
|