@glw907/cairn-cms 0.68.0 → 0.76.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/CHANGELOG.md +82 -0
- package/dist/ambient.d.ts +2 -0
- package/dist/components/CairnAdmin.svelte.d.ts +2 -7
- package/dist/components/ComponentForm.svelte +44 -27
- package/dist/components/ComponentInsertDialog.svelte +5 -5
- package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
- package/dist/components/EditPage.svelte +29 -107
- package/dist/components/EditPage.svelte.d.ts +2 -7
- package/dist/components/EntryPicker.svelte +117 -0
- package/dist/components/EntryPicker.svelte.d.ts +35 -0
- package/dist/components/FieldInput.svelte +218 -0
- package/dist/components/FieldInput.svelte.d.ts +51 -0
- package/dist/components/IconPicker.svelte +2 -2
- package/dist/components/IconPicker.svelte.d.ts +2 -0
- package/dist/components/LinkPicker.svelte +8 -75
- package/dist/components/LinkPicker.svelte.d.ts +4 -5
- package/dist/components/MediaHeroField.svelte +8 -5
- package/dist/components/MediaHeroField.svelte.d.ts +4 -0
- package/dist/components/ObjectGroupField.svelte +54 -0
- package/dist/components/ObjectGroupField.svelte.d.ts +47 -0
- package/dist/components/ReferenceField.svelte +94 -0
- package/dist/components/ReferenceField.svelte.d.ts +27 -0
- package/dist/components/RepeatableField.svelte +221 -0
- package/dist/components/RepeatableField.svelte.d.ts +53 -0
- package/dist/components/cairn-admin.css +4 -0
- package/dist/components/preview-doc.js +5 -1
- package/dist/components/tidy-validate.js +1 -1
- package/dist/content/adapter.js +18 -0
- package/dist/content/advisories.d.ts +2 -2
- package/dist/content/advisories.js +3 -5
- package/dist/content/compose.d.ts +7 -6
- package/dist/content/compose.js +26 -20
- package/dist/content/concepts.d.ts +21 -15
- package/dist/content/concepts.js +55 -32
- package/dist/content/field-rules.js +3 -4
- package/dist/content/fields.d.ts +49 -1
- package/dist/content/fields.js +11 -0
- package/dist/content/fieldset.d.ts +31 -10
- package/dist/content/fieldset.js +262 -109
- package/dist/content/frontmatter-region.d.ts +38 -0
- package/dist/content/frontmatter-region.js +75 -0
- package/dist/content/frontmatter.d.ts +35 -2
- package/dist/content/frontmatter.js +232 -11
- package/dist/content/manifest.d.ts +34 -0
- package/dist/content/manifest.js +80 -4
- package/dist/content/media-refs.d.ts +2 -2
- package/dist/content/media-rewrite.js +1 -69
- package/dist/content/reference-index.d.ts +56 -0
- package/dist/content/reference-index.js +95 -0
- package/dist/content/references.d.ts +40 -0
- package/dist/content/references.js +0 -0
- package/dist/content/standard-schema.d.ts +30 -0
- package/dist/content/standard-schema.js +4 -0
- package/dist/content/types.d.ts +127 -178
- package/dist/delivery/data.d.ts +2 -2
- package/dist/delivery/data.js +1 -1
- package/dist/delivery/public-routes.d.ts +2 -5
- package/dist/delivery/public-routes.js +15 -1
- package/dist/delivery/site-descriptors.d.ts +5 -1
- package/dist/delivery/site-descriptors.js +8 -3
- package/dist/delivery/site-indexes.d.ts +2 -2
- package/dist/delivery/site-resolver.d.ts +25 -0
- package/dist/delivery/site-resolver.js +49 -0
- package/dist/doctor/checks-local.js +6 -11
- package/dist/github/backend.d.ts +83 -0
- package/dist/github/backend.js +76 -0
- package/dist/github/credentials.d.ts +11 -5
- package/dist/github/credentials.js +3 -3
- package/dist/github/repo.d.ts +8 -19
- package/dist/github/repo.js +69 -80
- package/dist/github/types.d.ts +1 -1
- package/dist/github/types.js +4 -4
- package/dist/index.d.ts +16 -12
- package/dist/index.js +7 -8
- package/dist/islands/index.d.ts +12 -0
- package/dist/islands/index.js +83 -0
- package/dist/islands/types.d.ts +7 -0
- package/dist/islands/types.js +1 -0
- package/dist/media/rewrite-plan.d.ts +2 -3
- package/dist/media/rewrite-plan.js +2 -3
- package/dist/media/usage.d.ts +2 -2
- package/dist/media/usage.js +3 -5
- package/dist/nav/site-config.d.ts +0 -6
- package/dist/nav/site-config.js +6 -4
- package/dist/render/component-grammar.js +11 -11
- package/dist/render/component-reference.js +5 -3
- package/dist/render/component-validate.d.ts +4 -1
- package/dist/render/component-validate.js +10 -35
- package/dist/render/pipeline.d.ts +0 -6
- package/dist/render/pipeline.js +1 -1
- package/dist/render/registry.d.ts +34 -34
- package/dist/render/registry.js +26 -5
- package/dist/render/rehype-dispatch.d.ts +4 -4
- package/dist/render/rehype-dispatch.js +36 -11
- package/dist/render/remark-directives.js +4 -5
- package/dist/render/sanitize-schema.js +1 -1
- package/dist/sveltekit/cairn-admin.d.ts +5 -5
- package/dist/sveltekit/cairn-admin.js +3 -4
- package/dist/sveltekit/content-routes.d.ts +10 -8
- package/dist/sveltekit/content-routes.js +269 -181
- package/dist/sveltekit/health.d.ts +7 -3
- package/dist/sveltekit/health.js +9 -3
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/nav-routes.d.ts +6 -5
- package/dist/sveltekit/nav-routes.js +22 -20
- package/dist/sveltekit/types.d.ts +2 -0
- package/dist/vite/index.d.ts +3 -3
- package/dist/vite/index.js +17 -8
- package/package.json +5 -1
- package/src/lib/ambient.ts +7 -0
- package/src/lib/components/CairnAdmin.svelte +2 -6
- package/src/lib/components/ComponentForm.svelte +48 -27
- package/src/lib/components/ComponentInsertDialog.svelte +9 -8
- package/src/lib/components/EditPage.svelte +43 -119
- package/src/lib/components/EntryPicker.svelte +154 -0
- package/src/lib/components/FieldInput.svelte +262 -0
- package/src/lib/components/IconPicker.svelte +4 -2
- package/src/lib/components/LinkPicker.svelte +10 -81
- package/src/lib/components/MediaHeroField.svelte +12 -5
- package/src/lib/components/ObjectGroupField.svelte +97 -0
- package/src/lib/components/ReferenceField.svelte +126 -0
- package/src/lib/components/RepeatableField.svelte +310 -0
- package/src/lib/components/preview-doc.ts +5 -1
- package/src/lib/components/tidy-validate.ts +1 -1
- package/src/lib/content/adapter.ts +21 -0
- package/src/lib/content/advisories.ts +4 -7
- package/src/lib/content/compose.ts +30 -23
- package/src/lib/content/concepts.ts +68 -40
- package/src/lib/content/field-rules.ts +3 -4
- package/src/lib/content/fields.ts +52 -1
- package/src/lib/content/fieldset.ts +291 -128
- package/src/lib/content/frontmatter-region.ts +90 -0
- package/src/lib/content/frontmatter.ts +231 -15
- package/src/lib/content/manifest.ts +101 -4
- package/src/lib/content/media-refs.ts +2 -2
- package/src/lib/content/media-rewrite.ts +7 -80
- package/src/lib/content/reference-index.ts +159 -0
- package/src/lib/content/references.ts +0 -0
- package/src/lib/content/standard-schema.ts +25 -0
- package/src/lib/content/types.ts +128 -195
- package/src/lib/delivery/data.ts +2 -2
- package/src/lib/delivery/public-routes.ts +17 -3
- package/src/lib/delivery/site-descriptors.ts +8 -3
- package/src/lib/delivery/site-indexes.ts +2 -2
- package/src/lib/delivery/site-resolver.ts +64 -0
- package/src/lib/doctor/checks-local.ts +6 -14
- package/src/lib/github/backend.ts +161 -0
- package/src/lib/github/credentials.ts +10 -7
- package/src/lib/github/repo.ts +79 -83
- package/src/lib/github/types.ts +5 -5
- package/src/lib/index.ts +38 -23
- package/src/lib/islands/index.ts +84 -0
- package/src/lib/islands/types.ts +11 -0
- package/src/lib/media/rewrite-plan.ts +4 -6
- package/src/lib/media/usage.ts +4 -7
- package/src/lib/nav/site-config.ts +8 -9
- package/src/lib/render/component-grammar.ts +10 -10
- package/src/lib/render/component-reference.ts +4 -3
- package/src/lib/render/component-validate.ts +10 -35
- package/src/lib/render/pipeline.ts +1 -7
- package/src/lib/render/registry.ts +58 -39
- package/src/lib/render/rehype-dispatch.ts +45 -10
- package/src/lib/render/remark-directives.ts +4 -5
- package/src/lib/render/sanitize-schema.ts +1 -1
- package/src/lib/sveltekit/cairn-admin.ts +8 -9
- package/src/lib/sveltekit/content-routes.ts +330 -221
- package/src/lib/sveltekit/health.ts +13 -6
- package/src/lib/sveltekit/index.ts +2 -2
- package/src/lib/sveltekit/nav-routes.ts +33 -29
- package/src/lib/sveltekit/types.ts +5 -1
- package/src/lib/vite/index.ts +20 -11
- package/dist/content/schema.d.ts +0 -87
- package/dist/content/schema.js +0 -85
- package/dist/content/validate.d.ts +0 -17
- package/dist/content/validate.js +0 -93
- package/src/lib/content/schema.ts +0 -163
- package/src/lib/content/validate.ts +0 -90
package/src/lib/media/usage.ts
CHANGED
|
@@ -19,10 +19,8 @@
|
|
|
19
19
|
// is therefore "found in N entries" / "no references found", never a bare "unused": absence of a row
|
|
20
20
|
// means no reference was found, not a proof that none exists.
|
|
21
21
|
import type { ConceptDescriptor } from '../content/types.js';
|
|
22
|
-
import type {
|
|
22
|
+
import type { Backend } from '../github/backend.js';
|
|
23
23
|
import type { Manifest } from '../content/manifest.js';
|
|
24
|
-
import { listBranches } from '../github/branches.js';
|
|
25
|
-
import { readRaw } from '../github/repo.js';
|
|
26
24
|
import { PENDING_PREFIX, parsePendingBranch } from '../content/pending.js';
|
|
27
25
|
import { findConcept } from '../content/concepts.js';
|
|
28
26
|
import { isValidId, filenameFromId } from '../content/ids.js';
|
|
@@ -84,8 +82,7 @@ function push(index: UsageIndex, hash: string, entry: UsageEntry): void {
|
|
|
84
82
|
* (the load path lists once for the media-union) rather than listing them a second time.
|
|
85
83
|
*/
|
|
86
84
|
export async function buildUsageIndex(
|
|
87
|
-
|
|
88
|
-
token: string,
|
|
85
|
+
backend: Backend,
|
|
89
86
|
concepts: ConceptDescriptor[],
|
|
90
87
|
manifest: Manifest,
|
|
91
88
|
opts: BuildUsageOptions = {},
|
|
@@ -108,7 +105,7 @@ export async function buildUsageIndex(
|
|
|
108
105
|
|
|
109
106
|
// The branch arm: read each open cairn/* branch's one edited file. The path is derivable from the
|
|
110
107
|
// branch name, so no tree-listing is needed. The branch list is reused when the caller passes it.
|
|
111
|
-
const names = opts.branches ?? (await listBranches(
|
|
108
|
+
const names = opts.branches ?? (await backend.listBranches(PENDING_PREFIX));
|
|
112
109
|
// Read the branches in parallel rather than one at a time, so the latency floor is one round trip
|
|
113
110
|
// instead of N. workerd self-throttles to 6 simultaneous outbound connections, so this batch and
|
|
114
111
|
// the load path's media-union batch each stay under the limit; do NOT merge the two into one
|
|
@@ -125,7 +122,7 @@ export async function buildUsageIndex(
|
|
|
125
122
|
|
|
126
123
|
const path = `${concept.dir}/${filenameFromId(ref.id)}`;
|
|
127
124
|
try {
|
|
128
|
-
const raw = await
|
|
125
|
+
const raw = await backend.readFile(path, name);
|
|
129
126
|
if (raw === null) return []; // The file is absent on the branch: nothing to extract.
|
|
130
127
|
const { frontmatter, body } = parseMarkdown(raw);
|
|
131
128
|
const fmTitle = frontmatter.title;
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// commits the file back through the GitHub-App pipeline. This module is pure: parse, validate, and
|
|
4
4
|
// rewrite only. The engine returns data; each site renders the tree with its own markup.
|
|
5
5
|
import { parse as parseYaml, parseDocument } from 'yaml';
|
|
6
|
-
import type { ConceptUrlPolicy } from '../content/types.js';
|
|
7
6
|
|
|
8
7
|
/** One navigation node. An omitted or empty `url` is a label-only grouping header; no `children` is a leaf. */
|
|
9
8
|
export interface NavNode {
|
|
@@ -75,12 +74,9 @@ export interface SiteConfig {
|
|
|
75
74
|
siteName: string;
|
|
76
75
|
description?: string;
|
|
77
76
|
author?: string;
|
|
78
|
-
url?: string;
|
|
79
77
|
locale?: string;
|
|
80
78
|
/** Named navigation menus, each a NavNode[] (normalized by extractMenu). */
|
|
81
79
|
menus?: Record<string, unknown>;
|
|
82
|
-
/** Per-concept URL policy: the permalink pattern and date-prefix granularity, keyed by concept id. */
|
|
83
|
-
content?: Record<string, ConceptUrlPolicy>;
|
|
84
80
|
/**
|
|
85
81
|
* The editor spellcheck settings. The dialect is declared once per site (spec 1.2), so a British
|
|
86
82
|
* site loads the British word list and "colour" reads as correct. Today only US English ships, so an
|
|
@@ -285,6 +281,14 @@ export function parseSiteConfig(raw: string): SiteConfig {
|
|
|
285
281
|
if (typeof siteName !== 'string' || !siteName.trim()) {
|
|
286
282
|
throw new SiteConfigError('Site config needs a siteName');
|
|
287
283
|
}
|
|
284
|
+
// Contract v2 moved per-concept URL policy out of the YAML and onto defineConcept. A leftover
|
|
285
|
+
// `content:` block here would silently do nothing while the concept defaulted its permalink, so a
|
|
286
|
+
// half-migrated site (one carrying a non-default datePrefix) would rewrite every post URL. Fail loud.
|
|
287
|
+
if ((parsed as SiteConfig).content !== undefined) {
|
|
288
|
+
throw new SiteConfigError(
|
|
289
|
+
'cairn: site config no longer carries per-concept URL policy; move permalink/datePrefix into defineConcept (Contract v2)',
|
|
290
|
+
);
|
|
291
|
+
}
|
|
288
292
|
return parsed as SiteConfig;
|
|
289
293
|
}
|
|
290
294
|
|
|
@@ -295,11 +299,6 @@ export function extractMenu(config: SiteConfig, name: string, maxDepth: number):
|
|
|
295
299
|
return validateNavTree(menu, maxDepth);
|
|
296
300
|
}
|
|
297
301
|
|
|
298
|
-
/** The per-concept URL policy from a parsed config, or an empty policy when the `content` key is absent. */
|
|
299
|
-
export function urlPolicyFrom(config: SiteConfig): Record<string, ConceptUrlPolicy> {
|
|
300
|
-
return config.content ?? {};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
302
|
/**
|
|
304
303
|
* Replace one named menu in the YAML site-config text and reserialize, preserving every other
|
|
305
304
|
* top-level key (siteName, other menus, settings). Parses into a Document so the rest of the file
|
|
@@ -9,17 +9,17 @@ const COLON = ':';
|
|
|
9
9
|
|
|
10
10
|
function attrBlock(def: ComponentDef, values: ComponentValues): string {
|
|
11
11
|
const parts: string[] = [];
|
|
12
|
-
for (const field of def.attributes ??
|
|
13
|
-
const v = values.attributes[
|
|
12
|
+
for (const [name, field] of Object.entries(def.attributes ?? {})) {
|
|
13
|
+
const v = values.attributes[name];
|
|
14
14
|
if (field.type === 'boolean') {
|
|
15
|
-
if (v === true) parts.push(`${
|
|
15
|
+
if (v === true) parts.push(`${name}="true"`);
|
|
16
16
|
} else if (typeof v === 'string' && v !== '') {
|
|
17
17
|
// The directive attribute grammar (mdast-util-directive) treats a literal `"` as the value
|
|
18
18
|
// terminator and decodes HTML entities, so a backslash escape does not survive a round-trip.
|
|
19
19
|
// Encode `&` first (so existing entities are not double-decoded) then `"`; the parser decodes
|
|
20
20
|
// both back. A backslash is literal in this grammar and needs no escaping.
|
|
21
21
|
const escaped = v.replace(/&/g, '&').replace(/"/g, '"');
|
|
22
|
-
parts.push(`${
|
|
22
|
+
parts.push(`${name}="${escaped}"`);
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
return parts.length ? `{${parts.join(' ')}}` : '';
|
|
@@ -103,10 +103,10 @@ function valuesFromRoot(root: (RootContent & DirectiveNode) | undefined, def: Co
|
|
|
103
103
|
const values = emptyComponentValues(def);
|
|
104
104
|
if (!root) return values;
|
|
105
105
|
|
|
106
|
-
for (const field of def.attributes ??
|
|
107
|
-
const raw = root.attributes?.[
|
|
108
|
-
if (field.type === 'boolean') values.attributes[
|
|
109
|
-
else if (typeof raw === 'string') values.attributes[
|
|
106
|
+
for (const [name, field] of Object.entries(def.attributes ?? {})) {
|
|
107
|
+
const raw = root.attributes?.[name];
|
|
108
|
+
if (field.type === 'boolean') values.attributes[name] = raw === 'true';
|
|
109
|
+
else if (typeof raw === 'string') values.attributes[name] = raw;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
const titleSlot = slotByName(def, 'title');
|
|
@@ -181,7 +181,7 @@ export async function componentRoundTripSafety(markdown: string, def: ComponentD
|
|
|
181
181
|
const root = findComponentRoot(markdown, def);
|
|
182
182
|
if (!root) return { safe: false, reason: 'not-a-component' };
|
|
183
183
|
|
|
184
|
-
const declaredKeys = new Set((def.attributes ??
|
|
184
|
+
const declaredKeys = new Set(Object.keys(def.attributes ?? {}));
|
|
185
185
|
for (const key of parseRawAttributeKeys(markdown, def)) {
|
|
186
186
|
if (!declaredKeys.has(key)) return { safe: false, reason: 'unknown-attribute' };
|
|
187
187
|
}
|
|
@@ -220,7 +220,7 @@ export async function parseComponentWithRawKeys(
|
|
|
220
220
|
// here; the parse must overwrite only the fields actually present in the markdown.
|
|
221
221
|
function emptyComponentValues(def: ComponentDef): ComponentValues {
|
|
222
222
|
const attributes: Record<string, string | boolean> = {};
|
|
223
|
-
for (const
|
|
223
|
+
for (const [name, field] of Object.entries(def.attributes ?? {})) attributes[name] = field.type === 'boolean' ? false : '';
|
|
224
224
|
const slots: Record<string, string | string[]> = {};
|
|
225
225
|
for (const s of def.slots ?? []) slots[s.name] = s.kind === 'repeatable' ? [] : '';
|
|
226
226
|
return { attributes, slots };
|
|
@@ -27,9 +27,10 @@ function componentSection(def: ComponentDef): string {
|
|
|
27
27
|
/** Seed example values that show every declared field: an ellipsis for strings, one sample list item. */
|
|
28
28
|
function exampleValues(def: ComponentDef): ComponentValues {
|
|
29
29
|
const values = emptyValues(def);
|
|
30
|
-
for (const field of def.attributes ??
|
|
31
|
-
if (field.type === 'boolean') values.attributes[
|
|
32
|
-
else values.attributes[
|
|
30
|
+
for (const [name, field] of Object.entries(def.attributes ?? {})) {
|
|
31
|
+
if (field.type === 'boolean') values.attributes[name] = false;
|
|
32
|
+
else if (field.type === 'select') values.attributes[name] = field.options[0] ?? '…';
|
|
33
|
+
else values.attributes[name] = '…';
|
|
33
34
|
}
|
|
34
35
|
for (const slot of def.slots ?? []) {
|
|
35
36
|
if (slot.kind === 'repeatable') values.slots[slot.name] = ['…'];
|
|
@@ -1,38 +1,25 @@
|
|
|
1
1
|
import { parseComponentWithRawKeys } from './component-grammar.js';
|
|
2
|
-
import
|
|
2
|
+
import { fieldset } from '../content/fieldset.js';
|
|
3
|
+
import type { ComponentDef } from './registry.js';
|
|
3
4
|
|
|
4
5
|
/** A validation verdict: ok, or field-keyed error messages. */
|
|
5
6
|
export type ComponentValidation = { ok: true } | { ok: false; errors: Record<string, string> };
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
+
* Validate a serialized component directive against its definition: the attributes through the same
|
|
10
|
+
* `fieldset` validator a concept field uses (coercion, constraints, required, select domain, pattern,
|
|
11
|
+
* and any per-attribute `behavior.validate`), then the two component-only checks, an unknown attribute
|
|
12
|
+
* key and an unfilled required slot.
|
|
9
13
|
*/
|
|
10
14
|
export async function validateComponent(markdown: string, def: ComponentDef): Promise<ComponentValidation> {
|
|
11
15
|
const { values, rawKeys } = await parseComponentWithRawKeys(markdown, def);
|
|
12
16
|
const errors: Record<string, string> = {};
|
|
13
|
-
const declared = new Set((def.attributes ?? []).map((f) => f.key));
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (field.required && !filled) {
|
|
19
|
-
errors[field.key] = `${field.label} is required.`;
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
if (field.type === 'select' && typeof v === 'string' && v !== '' && !(field.options ?? []).includes(v)) {
|
|
23
|
-
errors[field.key] = `${field.label} must be one of: ${(field.options ?? []).join(', ')}.`;
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
if (field.pattern && typeof v === 'string' && v !== '' && !new RegExp(field.pattern.source).test(v)) {
|
|
27
|
-
errors[field.key] = field.pattern.message;
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (field.validate) {
|
|
31
|
-
const message = runFieldValidator(def, field.key, () => field.validate!(v, values));
|
|
32
|
-
if (typeof message === 'string') errors[field.key] = message;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
18
|
+
const schema = def.attributeSchema ?? fieldset(def.attributes ?? {}, { behavior: def.behavior });
|
|
19
|
+
const result = schema.validate(values.attributes, '');
|
|
20
|
+
if (!result.ok) Object.assign(errors, result.errors);
|
|
35
21
|
|
|
22
|
+
const declared = new Set(Object.keys(def.attributes ?? {}));
|
|
36
23
|
for (const key of rawKeys) {
|
|
37
24
|
if (!declared.has(key)) errors[key] = `Unknown attribute "${key}".`;
|
|
38
25
|
}
|
|
@@ -46,15 +33,3 @@ export async function validateComponent(markdown: string, def: ComponentDef): Pr
|
|
|
46
33
|
|
|
47
34
|
return Object.keys(errors).length ? { ok: false, errors } : { ok: true };
|
|
48
35
|
}
|
|
49
|
-
|
|
50
|
-
// Run a site-supplied attribute validator. The validator is author code, so a throw is contained:
|
|
51
|
-
// the field is treated as valid and a dev-time warning names the component and field so the author
|
|
52
|
-
// can find the bug. A returned string is the field error; anything else (null) is clean.
|
|
53
|
-
function runFieldValidator(def: ComponentDef, key: string, call: () => string | null): string | null {
|
|
54
|
-
try {
|
|
55
|
-
return call();
|
|
56
|
-
} catch (err) {
|
|
57
|
-
console.warn(`cairn: validate() for component "${def.name}" field "${key}" threw; treating the field as valid.`, err);
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
@@ -20,12 +20,6 @@ import { defineRegistry, type ComponentRegistry } from './registry.js';
|
|
|
20
20
|
import type { LinkResolve } from '../content/links.js';
|
|
21
21
|
|
|
22
22
|
export interface RendererOptions {
|
|
23
|
-
/**
|
|
24
|
-
* Stamp a `data-rise` ordinal (0, 1, 2, …) on each top-level component so a site's
|
|
25
|
-
* CSS can drive an entrance-cascade delay off it. Omit for no stagger. The ordinal
|
|
26
|
-
* is inert, so a consumer's sanitize floor can keep `data-rise` and drop `style`.
|
|
27
|
-
*/
|
|
28
|
-
stagger?: boolean;
|
|
29
23
|
/**
|
|
30
24
|
* Extend the sanitize allowlist. Receives cairn's default schema (defaultSchema plus the
|
|
31
25
|
* directive markers and the common benign tags) and returns the schema to use. Add to the
|
|
@@ -73,7 +67,7 @@ export function createRenderer(
|
|
|
73
67
|
const rehypePlugins: PluggableList = [
|
|
74
68
|
rehypeRaw,
|
|
75
69
|
...floor,
|
|
76
|
-
[rehypeDispatch, registry
|
|
70
|
+
[rehypeDispatch, registry],
|
|
77
71
|
rehypeSlug,
|
|
78
72
|
// Name each GFM task-list checkbox from its item text. It runs after the sanitize floor (which
|
|
79
73
|
// does not allow aria-label) so the added attribute survives, and is content-not-sink, so it is
|
|
@@ -4,33 +4,9 @@
|
|
|
4
4
|
// parser, the render dispatch, and the editor never drift apart. The adapter references
|
|
5
5
|
// `ComponentRegistry` from here.
|
|
6
6
|
import type { Element, ElementContent } from 'hast';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/** One `{key="value"}` attribute on a component directive, or one field of a repeatable item. */
|
|
12
|
-
export interface AttributeField {
|
|
13
|
-
/** The attribute name as it appears in the directive, e.g. `icon`. */
|
|
14
|
-
key: string;
|
|
15
|
-
/** The form label. */
|
|
16
|
-
label: string;
|
|
17
|
-
type: FieldType;
|
|
18
|
-
required?: boolean;
|
|
19
|
-
/** Initial value; a string for text/select/icon, a boolean for boolean. */
|
|
20
|
-
default?: string | boolean;
|
|
21
|
-
/** Allowed values for `type: 'select'`. */
|
|
22
|
-
options?: readonly string[];
|
|
23
|
-
/** Helper text shown under the field. */
|
|
24
|
-
help?: string;
|
|
25
|
-
/** A RegExp `source` to validate the value against, plus the message to show on a mismatch. */
|
|
26
|
-
pattern?: { source: string; message: string };
|
|
27
|
-
/**
|
|
28
|
-
* A pure, browser-safe cross-field validator. Returns an error string, or null when valid.
|
|
29
|
-
* Receives the field's value and the full {@link ComponentValues} so a rule can read sibling
|
|
30
|
-
* fields. The picker wraps the call in try/catch so an author's throw never crashes the form.
|
|
31
|
-
*/
|
|
32
|
-
validate?: (value: string | boolean, all: ComponentValues) => string | null;
|
|
33
|
-
}
|
|
7
|
+
import type { FieldDescriptor } from '../content/fields.js';
|
|
8
|
+
import type { BehaviorTable, Fieldset } from '../content/fieldset.js';
|
|
9
|
+
import { fieldset } from '../content/fieldset.js';
|
|
34
10
|
|
|
35
11
|
export type SlotKind = 'markdown' | 'inline' | 'repeatable';
|
|
36
12
|
|
|
@@ -45,7 +21,7 @@ export interface SlotDef {
|
|
|
45
21
|
required?: boolean;
|
|
46
22
|
help?: string;
|
|
47
23
|
/** For `kind: 'repeatable'`: the fields composing each list item (v1 uses the first field). */
|
|
48
|
-
itemFields?:
|
|
24
|
+
itemFields?: Record<string, FieldDescriptor>;
|
|
49
25
|
/**
|
|
50
26
|
* For `kind: 'repeatable'`: derives a row's label from its item values and zero-based index.
|
|
51
27
|
* When it returns nothing, the picker falls back to `${label} ${index + 1}`.
|
|
@@ -82,10 +58,18 @@ export interface ComponentDef {
|
|
|
82
58
|
insertTemplate?: string;
|
|
83
59
|
/**
|
|
84
60
|
* Build the final hast element from the component context (attributes plus partitioned
|
|
85
|
-
* slots). The engine stamps the entrance
|
|
61
|
+
* slots). The engine stamps the entrance ordinal (`data-rise`) on the top-level
|
|
86
62
|
* result, so a build fn stays free of any motion concern.
|
|
87
63
|
*/
|
|
88
64
|
build: (ctx: ComponentContext) => Element;
|
|
65
|
+
/**
|
|
66
|
+
* Opt this directive into client hydration (phase 4b islands). `true` mounts the island eagerly on
|
|
67
|
+
* first load and after client-side navigation; `'visible'` defers the mount to first intersection.
|
|
68
|
+
* The engine wraps {@link ComponentDef.build}'s output in an island boundary, and the site registers
|
|
69
|
+
* the live Svelte component under the same name on `rendering.islands`. Absent leaves the directive a
|
|
70
|
+
* static, server-only component.
|
|
71
|
+
*/
|
|
72
|
+
hydrate?: boolean | 'visible';
|
|
89
73
|
/**
|
|
90
74
|
* Optional role-to-default-icon, e.g. `{ caution: 'warning' }`. Maps a free-string role to a
|
|
91
75
|
* glyph key in the site IconSet; choose a logically representative glyph and prefer glyphs
|
|
@@ -95,8 +79,18 @@ export interface ComponentDef {
|
|
|
95
79
|
defaultIconByRole?: Record<string, string>;
|
|
96
80
|
/** One line on when to reach for this component; feeds the picker and the reference file. */
|
|
97
81
|
use?: string;
|
|
98
|
-
/** The `{key="value"}` attributes this component accepts. */
|
|
99
|
-
attributes?:
|
|
82
|
+
/** The `{key="value"}` attributes this component accepts, keyed by attribute name. */
|
|
83
|
+
attributes?: Record<string, FieldDescriptor>;
|
|
84
|
+
/**
|
|
85
|
+
* Per-attribute function-valued behavior (a cross-field `validate`), keyed by attribute name.
|
|
86
|
+
* {@link defineComponent} bundles it into the attribute {@link Fieldset}.
|
|
87
|
+
*/
|
|
88
|
+
behavior?: BehaviorTable;
|
|
89
|
+
/**
|
|
90
|
+
* The attribute validator {@link defineComponent} builds from `attributes` and `behavior`.
|
|
91
|
+
* Engine-internal: the constructor sets it, and {@link validateComponent} runs it.
|
|
92
|
+
*/
|
|
93
|
+
attributeSchema?: Fieldset;
|
|
100
94
|
/** The named content regions this component accepts. */
|
|
101
95
|
slots?: SlotDef[];
|
|
102
96
|
/**
|
|
@@ -123,8 +117,8 @@ export interface ComponentRegistry {
|
|
|
123
117
|
names: string[];
|
|
124
118
|
get(name: string): ComponentDef | undefined;
|
|
125
119
|
defaultIcon(name: string, role?: string): string | undefined;
|
|
126
|
-
/** The component's first `type:'icon'` attribute, or undefined when it declares none. */
|
|
127
|
-
iconField(name: string):
|
|
120
|
+
/** The name of the component's first `type:'icon'` attribute, or undefined when it declares none. */
|
|
121
|
+
iconField(name: string): string | undefined;
|
|
128
122
|
}
|
|
129
123
|
|
|
130
124
|
/**
|
|
@@ -137,12 +131,12 @@ export function dataAttrProp(key: string): string {
|
|
|
137
131
|
}
|
|
138
132
|
|
|
139
133
|
/**
|
|
140
|
-
*
|
|
141
|
-
* construction-time guard and the registry's `iconField` derive the icon field from this one
|
|
134
|
+
* The name of a component's first `type:'icon'` attribute, or undefined when it declares none. Both
|
|
135
|
+
* the construction-time guard and the registry's `iconField` derive the icon field from this one
|
|
142
136
|
* predicate rather than spelling the `type === 'icon'` find twice.
|
|
143
137
|
*/
|
|
144
|
-
function findIconField(def: ComponentDef):
|
|
145
|
-
return def.attributes
|
|
138
|
+
function findIconField(def: ComponentDef): string | undefined {
|
|
139
|
+
return Object.entries(def.attributes ?? {}).find(([, field]) => field.type === 'icon')?.[0];
|
|
146
140
|
}
|
|
147
141
|
|
|
148
142
|
/**
|
|
@@ -209,8 +203,8 @@ export interface ComponentValues {
|
|
|
209
203
|
*/
|
|
210
204
|
export function emptyValues(def: ComponentDef): ComponentValues {
|
|
211
205
|
const attributes: Record<string, string | boolean> = {};
|
|
212
|
-
for (const field of def.attributes ??
|
|
213
|
-
attributes[
|
|
206
|
+
for (const [name, field] of Object.entries(def.attributes ?? {})) {
|
|
207
|
+
attributes[name] = field.default ?? (field.type === 'boolean' ? false : '');
|
|
214
208
|
}
|
|
215
209
|
const slots: Record<string, string | string[]> = {};
|
|
216
210
|
for (const slot of def.slots ?? []) {
|
|
@@ -232,3 +226,28 @@ export function previewValues(def: ComponentDef): ComponentValues {
|
|
|
232
226
|
slots: { ...base.slots, ...def.preview.slots },
|
|
233
227
|
};
|
|
234
228
|
}
|
|
229
|
+
|
|
230
|
+
/** The descriptor types that serialize to a single directive-attribute string (decision 2). */
|
|
231
|
+
const ATTRIBUTE_TYPES = new Set(['text', 'textarea', 'number', 'select', 'url', 'email', 'date', 'datetime', 'boolean', 'icon']);
|
|
232
|
+
|
|
233
|
+
/** Reject an attribute type that cannot serialize to a single directive-attribute string (decision 2). */
|
|
234
|
+
function checkComponentAttributes(name: string, attributes: Record<string, FieldDescriptor>): void {
|
|
235
|
+
for (const [key, field] of Object.entries(attributes)) {
|
|
236
|
+
if (!ATTRIBUTE_TYPES.has(field.type)) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`cairn: component "${name}" attribute "${key}" is type "${field.type}"; a directive attribute must be a single-value scalar (text, textarea, number, select, url, email, date, datetime, boolean, or icon).`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Declare a site component, building its attribute validator from the `fields.*` descriptors and
|
|
246
|
+
* validating the component at declaration. Mirrors {@link defineConcept}: a malformed attribute type
|
|
247
|
+
* or pattern fails at module load. The built `attributeSchema` is what {@link validateComponent} runs.
|
|
248
|
+
*/
|
|
249
|
+
export function defineComponent<const D extends ComponentDef>(def: D): D & { attributeSchema: Fieldset } {
|
|
250
|
+
const attributes = def.attributes ?? {};
|
|
251
|
+
checkComponentAttributes(def.name, attributes);
|
|
252
|
+
return { ...def, attributeSchema: fieldset(attributes, { behavior: def.behavior }) };
|
|
253
|
+
}
|
|
@@ -69,7 +69,7 @@ export function markFirstList(children: ElementContent[]): Element | undefined {
|
|
|
69
69
|
|
|
70
70
|
// Recurse into a node's children, transforming any nested primitive sections
|
|
71
71
|
// (a grid inside a card, panels inside a split). Nested primitives never carry the
|
|
72
|
-
// entrance
|
|
72
|
+
// entrance ordinal; only top-level ones do (stamped in the transformer below).
|
|
73
73
|
function transformChildren(children: ElementContent[], registry: ComponentRegistry): ElementContent[] {
|
|
74
74
|
return children.map((c) => {
|
|
75
75
|
if (isElement(c) && c.properties?.dataPrimitive) return transformNode(c, registry);
|
|
@@ -82,10 +82,10 @@ function transformChildren(children: ElementContent[], registry: ComponentRegist
|
|
|
82
82
|
// 'true'/'false'; everything else is the literal string the author wrote.
|
|
83
83
|
function readAttributes(node: Element, def: ComponentDef): Record<string, string | boolean> {
|
|
84
84
|
const out: Record<string, string | boolean> = {};
|
|
85
|
-
for (const field of def.attributes ??
|
|
86
|
-
const value = strProp(node, dataAttrProp(
|
|
85
|
+
for (const [name, field] of Object.entries(def.attributes ?? {})) {
|
|
86
|
+
const value = strProp(node, dataAttrProp(name));
|
|
87
87
|
if (value == null) continue;
|
|
88
|
-
out[
|
|
88
|
+
out[name] = field.type === 'boolean' ? value === 'true' : value;
|
|
89
89
|
}
|
|
90
90
|
return out;
|
|
91
91
|
}
|
|
@@ -135,6 +135,40 @@ function partitionSlots(node: Element): {
|
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
// Serialize a hydrate component's declared attributes into the island prop payload. A `number` field is
|
|
139
|
+
// coerced from its stamped string to a JSON number here; a `boolean` already arrived as a real boolean
|
|
140
|
+
// from readAttributes (which coerces 'true'/'false' upstream), and every other field stays the literal
|
|
141
|
+
// string the author wrote. The result is JSON.stringify-ed into data-cairn-props and parsed on the client.
|
|
142
|
+
function serializeIslandProps(
|
|
143
|
+
def: ComponentDef,
|
|
144
|
+
attributes: Record<string, string | boolean>,
|
|
145
|
+
): Record<string, string | number | boolean> {
|
|
146
|
+
const out: Record<string, string | number | boolean> = {};
|
|
147
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
148
|
+
const type = def.attributes?.[key]?.type;
|
|
149
|
+
out[key] = type === 'number' && typeof value === 'string' ? Number(value) : value;
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Wrap a hydrate component's static fallback in its island boundary. The boundary carries the directive
|
|
155
|
+
// name and the JSON prop payload; a 'visible' island also carries data-cairn-hydrate="visible". The
|
|
156
|
+
// boundary attributes are inert data-* and survive both the sanitize floor (this runs after it) and the
|
|
157
|
+
// sink guard (which strips only style/on*). The fallback is build()'s no-JS, first-paint representation.
|
|
158
|
+
function islandBoundary(
|
|
159
|
+
name: string,
|
|
160
|
+
def: ComponentDef,
|
|
161
|
+
attributes: Record<string, string | boolean>,
|
|
162
|
+
fallback: Element,
|
|
163
|
+
): Element {
|
|
164
|
+
const properties: Record<string, string> = {
|
|
165
|
+
dataCairnIsland: name,
|
|
166
|
+
dataCairnProps: JSON.stringify(serializeIslandProps(def, attributes)),
|
|
167
|
+
};
|
|
168
|
+
if (def.hydrate === 'visible') properties.dataCairnHydrate = 'visible';
|
|
169
|
+
return { type: 'element', tagName: 'div', properties, children: [fallback] };
|
|
170
|
+
}
|
|
171
|
+
|
|
138
172
|
function transformNode(node: Element, registry: ComponentRegistry): Element {
|
|
139
173
|
node.children = transformChildren(node.children as ElementContent[], registry);
|
|
140
174
|
const name = strProp(node, 'dataPrimitive');
|
|
@@ -147,24 +181,25 @@ function transformNode(node: Element, registry: ComponentRegistry): Element {
|
|
|
147
181
|
items: parts.items,
|
|
148
182
|
node,
|
|
149
183
|
};
|
|
150
|
-
|
|
184
|
+
const built = def.build(ctx);
|
|
185
|
+
return def.hydrate ? islandBoundary(name!, def, ctx.attributes, built) : built;
|
|
151
186
|
}
|
|
152
187
|
|
|
153
188
|
/**
|
|
154
189
|
* Rehype transformer: dispatch each stamped element through its registry `build`
|
|
155
|
-
* fn.
|
|
156
|
-
*
|
|
157
|
-
*
|
|
190
|
+
* fn. Each top-level primitive gets a `data-rise` attribute carrying its
|
|
191
|
+
* document-order index (0, 1, 2, …); the site's CSS maps that ordinal to an
|
|
192
|
+
* entrance delay. The index is inert, so a consumer's sanitize floor can keep
|
|
158
193
|
* `data-rise` while dropping `style`. Nested primitives never get it. Non-primitive
|
|
159
194
|
* content (lede, intro paragraphs, the page-toc nav) passes through untouched.
|
|
160
195
|
*/
|
|
161
|
-
export function rehypeDispatch(registry: ComponentRegistry
|
|
196
|
+
export function rehypeDispatch(registry: ComponentRegistry) {
|
|
162
197
|
return (tree: Root) => {
|
|
163
198
|
let idx = 0;
|
|
164
199
|
tree.children = (tree.children as ElementContent[]).map((child) => {
|
|
165
200
|
if (isElement(child) && child.properties?.dataPrimitive) {
|
|
166
201
|
const el = transformNode(child, registry);
|
|
167
|
-
|
|
202
|
+
el.properties = { ...el.properties, dataRise: String(idx++) };
|
|
168
203
|
return el;
|
|
169
204
|
}
|
|
170
205
|
if (isElement(child)) child.children = transformChildren(child.children as ElementContent[], registry);
|
|
@@ -62,8 +62,7 @@ export function remarkDirectiveStamp(registry: ComponentRegistry) {
|
|
|
62
62
|
const def = registry.get(node.name);
|
|
63
63
|
const attrs = node.attributes ?? {};
|
|
64
64
|
const role = attrs.role || undefined;
|
|
65
|
-
const
|
|
66
|
-
const iconKey = iconField?.key ?? 'icon';
|
|
65
|
+
const iconKey = registry.iconField(node.name) ?? 'icon';
|
|
67
66
|
let icon = attrs[iconKey] || undefined;
|
|
68
67
|
if (!icon && role) icon = registry.defaultIcon(node.name, role);
|
|
69
68
|
|
|
@@ -76,9 +75,9 @@ export function remarkDirectiveStamp(registry: ComponentRegistry) {
|
|
|
76
75
|
// back to that default the same way a missing one does. data-attr-<key> survives to the
|
|
77
76
|
// element; build() consumes it and returns a fresh element, so the marker never reaches the
|
|
78
77
|
// published DOM.
|
|
79
|
-
for (const
|
|
80
|
-
const raw =
|
|
81
|
-
if (raw != null) properties[dataAttrProp(
|
|
78
|
+
for (const name of Object.keys(def?.attributes ?? {})) {
|
|
79
|
+
const raw = name === iconKey ? icon : attrs[name];
|
|
80
|
+
if (raw != null) properties[dataAttrProp(name)] = raw;
|
|
82
81
|
}
|
|
83
82
|
|
|
84
83
|
const data = node.data ?? (node.data = {});
|
|
@@ -23,7 +23,7 @@ export function buildSanitizeSchema(
|
|
|
23
23
|
registry: ComponentRegistry,
|
|
24
24
|
extend?: (defaults: Schema) => Schema,
|
|
25
25
|
): Schema {
|
|
26
|
-
const attrMarkers = registry.defs.flatMap((d) => (d.attributes ??
|
|
26
|
+
const attrMarkers = registry.defs.flatMap((d) => Object.keys(d.attributes ?? {}).map((key) => dataAttrProp(key)));
|
|
27
27
|
const markers = [...FIXED_MARKERS, ...attrMarkers];
|
|
28
28
|
const attributes = defaultSchema.attributes ?? {};
|
|
29
29
|
// defaultSchema's `a` entry carries a className tuple (`['className', 'data-footnote-backref']`)
|
|
@@ -22,7 +22,7 @@ import { createEditorRoutes } from './editors-routes.js';
|
|
|
22
22
|
import { createNavRoutes, type NavLoadData } from './nav-routes.js';
|
|
23
23
|
import type { AuthBranding, SendMagicLink } from '../email.js';
|
|
24
24
|
import type { AuthEnv, Editor } from '../auth/types.js';
|
|
25
|
-
import type {
|
|
25
|
+
import type { BackendEnv } from '../github/credentials.js';
|
|
26
26
|
import type { CairnRuntime } from '../content/types.js';
|
|
27
27
|
import type { CookieJar, EventBase } from './types.js';
|
|
28
28
|
|
|
@@ -31,20 +31,20 @@ import type { CookieJar, EventBase } from './types.js';
|
|
|
31
31
|
* (ContentEvent minus params, which the dispatcher synthesizes, plus RequestContext's cookies
|
|
32
32
|
* and setHeaders). A real SvelteKit RequestEvent satisfies it.
|
|
33
33
|
*/
|
|
34
|
-
export interface AdminEvent extends EventBase<
|
|
34
|
+
export interface AdminEvent extends EventBase<BackendEnv & AuthEnv> {
|
|
35
35
|
cookies: CookieJar;
|
|
36
36
|
setHeaders(headers: Record<string, string>): void;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Injectable dependencies. Branding defaults from the runtime's siteName and sender, so a
|
|
41
|
-
* site overrides it only to change the magic-link email identity; `send`
|
|
42
|
-
*
|
|
41
|
+
* site overrides it only to change the magic-link email identity; `send` is the same seam the
|
|
42
|
+
* underlying auth factory takes. The content backend rides `event.locals.backend` (the dev double)
|
|
43
|
+
* or the adapter's provider, so it is not a dep here.
|
|
43
44
|
*/
|
|
44
45
|
export interface CairnAdminDeps {
|
|
45
46
|
branding?: AuthBranding;
|
|
46
47
|
send?: SendMagicLink;
|
|
47
|
-
mintToken?: ContentRoutesDeps['mintToken'];
|
|
48
48
|
/**
|
|
49
49
|
* Build the Anthropic client for the tidy action. Forwarded to the content routes; a site that
|
|
50
50
|
* enables tidy injects a stub here to avoid a real network call. Defaults to the real SDK client.
|
|
@@ -83,13 +83,12 @@ export function createCairnAdmin(runtime: CairnRuntime, deps: CairnAdminDeps = {
|
|
|
83
83
|
};
|
|
84
84
|
const auth = createAuthRoutes({ branding, send: deps.send });
|
|
85
85
|
const content = createContentRoutes(runtime, {
|
|
86
|
-
mintToken: deps.mintToken,
|
|
87
86
|
anthropic: deps.anthropic,
|
|
88
87
|
tidyTimeoutMs: deps.tidyTimeoutMs,
|
|
89
88
|
});
|
|
90
89
|
const editors = createEditorRoutes();
|
|
91
90
|
// The nav surface exists only when the site configures a menu; without one its view is a 404.
|
|
92
|
-
const nav = runtime.navMenu ? createNavRoutes(runtime
|
|
91
|
+
const nav = runtime.navMenu ? createNavRoutes(runtime) : null;
|
|
93
92
|
|
|
94
93
|
/**
|
|
95
94
|
* Build the event a wrapped content load reads. The catch-all route carries only a rest
|
|
@@ -111,8 +110,8 @@ export function createCairnAdmin(runtime: CairnRuntime, deps: CairnAdminDeps = {
|
|
|
111
110
|
|
|
112
111
|
/**
|
|
113
112
|
* Serve the admin view the pathname names, or a 404 for any shape the parser refuses.
|
|
114
|
-
* The authed views run the layout load and the view load concurrently; both
|
|
115
|
-
*
|
|
113
|
+
* The authed views run the layout load and the view load concurrently; both resolve the same
|
|
114
|
+
* backend, and the installation-token cache coalesces their lazy mints into one signing.
|
|
116
115
|
*/
|
|
117
116
|
async function load(event: AdminEvent): Promise<AdminData> {
|
|
118
117
|
const view = parseAdminPath(event.url.pathname, runtime.concepts);
|