@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CairnAdapter, ConceptConfig } from '../content/types.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { InferFieldset } from '../content/fieldset.js';
|
|
3
3
|
import type { SiteConfig } from '../nav/site-config.js';
|
|
4
4
|
import type { ContentIndex } from './content-index.js';
|
|
5
5
|
import type { SiteResolver } from './site-resolver.js';
|
|
@@ -12,7 +12,7 @@ export type SiteGlobs<A extends CairnAdapter> = {
|
|
|
12
12
|
* `site` is not supported, since `site` is the reserved resolver key.
|
|
13
13
|
*/
|
|
14
14
|
export type SiteIndexes<A extends CairnAdapter> = {
|
|
15
|
-
[K in keyof A['content']]: ContentIndex<NonNullable<A['content'][K]> extends ConceptConfig<infer S> ?
|
|
15
|
+
[K in keyof A['content']]: ContentIndex<NonNullable<A['content'][K]> extends ConceptConfig<infer S> ? InferFieldset<S> : Record<string, unknown>>;
|
|
16
16
|
} & {
|
|
17
17
|
readonly site: SiteResolver;
|
|
18
18
|
};
|
|
@@ -32,6 +32,31 @@ export interface SiteResolver {
|
|
|
32
32
|
export declare function createSiteResolver(concepts: ConceptIndex[], opts?: {
|
|
33
33
|
validate?: boolean;
|
|
34
34
|
}): SiteResolver;
|
|
35
|
+
/**
|
|
36
|
+
* A reference edge resolved to its target's identity, for a public route to render a linked target.
|
|
37
|
+
* It reuses the target entry's own summary fields rather than re-deriving them, so a linked author
|
|
38
|
+
* card reads the same title and permalink the target's own page does. `summary` is the target's
|
|
39
|
+
* excerpt when present.
|
|
40
|
+
*/
|
|
41
|
+
export interface ResolvedReference {
|
|
42
|
+
id: string;
|
|
43
|
+
concept: string;
|
|
44
|
+
title: string;
|
|
45
|
+
permalink: string;
|
|
46
|
+
summary?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve a concept's `reference` and `array(reference)` frontmatter edges to their target identities,
|
|
50
|
+
* keyed by the field name, so a public route renders a reference as a link to its target's page. The
|
|
51
|
+
* resolution lives here because only the cross-concept resolver reaches a different concept's entries:
|
|
52
|
+
* a posts entry's `author` edge targets a pages entry, which the posts index alone cannot read. A
|
|
53
|
+
* single `reference` field resolves to one `ResolvedReference`, an `array(reference)` to a
|
|
54
|
+
* `ResolvedReference[]` in edge order. An id with no live target is dropped rather than thrown: the
|
|
55
|
+
* build's `verifyReferences` gate already fails a true dangling edge, so an unresolved id at request
|
|
56
|
+
* time is a mid-flight or draft target, not a hard error. Resolve per call, since the target entries
|
|
57
|
+
* exist only after every per-concept index is unioned into the resolver.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveReferences(site: SiteResolver, descriptor: ConceptDescriptor, frontmatter: Record<string, unknown>): Record<string, ResolvedReference | ResolvedReference[]>;
|
|
35
60
|
/**
|
|
36
61
|
* A resolver backed by the site resolver, for the build. A miss throws, so a dangling cairn: token
|
|
37
62
|
* fails the prerender (the build backstop). The preview uses manifestLinkResolver, which marks.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { extractReferenceEdges } from '../content/references.js';
|
|
1
2
|
/** Strip a trailing slash from a path, keeping the root "/" intact. */
|
|
2
3
|
function normalizePath(path) {
|
|
3
4
|
return path.length > 1 ? path.replace(/\/+$/, '') : path;
|
|
@@ -60,6 +61,54 @@ export function createSiteResolver(concepts, opts = {}) {
|
|
|
60
61
|
},
|
|
61
62
|
};
|
|
62
63
|
}
|
|
64
|
+
/** Project a resolved target entry into the identity a public route renders for a reference. */
|
|
65
|
+
function projectReference(edge, target) {
|
|
66
|
+
const resolved = {
|
|
67
|
+
id: edge.id,
|
|
68
|
+
concept: edge.concept,
|
|
69
|
+
title: target.title,
|
|
70
|
+
permalink: target.permalink,
|
|
71
|
+
};
|
|
72
|
+
if (target.excerpt)
|
|
73
|
+
resolved.summary = target.excerpt;
|
|
74
|
+
return resolved;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a concept's `reference` and `array(reference)` frontmatter edges to their target identities,
|
|
78
|
+
* keyed by the field name, so a public route renders a reference as a link to its target's page. The
|
|
79
|
+
* resolution lives here because only the cross-concept resolver reaches a different concept's entries:
|
|
80
|
+
* a posts entry's `author` edge targets a pages entry, which the posts index alone cannot read. A
|
|
81
|
+
* single `reference` field resolves to one `ResolvedReference`, an `array(reference)` to a
|
|
82
|
+
* `ResolvedReference[]` in edge order. An id with no live target is dropped rather than thrown: the
|
|
83
|
+
* build's `verifyReferences` gate already fails a true dangling edge, so an unresolved id at request
|
|
84
|
+
* time is a mid-flight or draft target, not a hard error. Resolve per call, since the target entries
|
|
85
|
+
* exist only after every per-concept index is unioned into the resolver.
|
|
86
|
+
*/
|
|
87
|
+
export function resolveReferences(site, descriptor, frontmatter) {
|
|
88
|
+
const edges = extractReferenceEdges(frontmatter, descriptor.fields);
|
|
89
|
+
const resolved = {};
|
|
90
|
+
for (const field of descriptor.fields) {
|
|
91
|
+
const isSingle = field.type === 'reference';
|
|
92
|
+
const isArray = field.type === 'array' && field.item.type === 'reference';
|
|
93
|
+
if (!isSingle && !isArray)
|
|
94
|
+
continue;
|
|
95
|
+
const fieldEdges = edges.filter((edge) => edge.field === field.name);
|
|
96
|
+
const hits = [];
|
|
97
|
+
for (const edge of fieldEdges) {
|
|
98
|
+
const target = site.concept(edge.concept)?.byId(edge.id);
|
|
99
|
+
if (target)
|
|
100
|
+
hits.push(projectReference(edge, target));
|
|
101
|
+
}
|
|
102
|
+
if (isSingle) {
|
|
103
|
+
if (hits.length > 0)
|
|
104
|
+
resolved[field.name] = hits[0];
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
resolved[field.name] = hits;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return resolved;
|
|
111
|
+
}
|
|
63
112
|
/**
|
|
64
113
|
* A resolver backed by the site resolver, for the build. A miss throws, so a dangling cairn: token
|
|
65
114
|
* fails the prerender (the build backstop). The preview uses manifestLinkResolver, which marks.
|
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
import { fail, pass, skip } from './types.js';
|
|
5
5
|
import { readWranglerConfig } from './wrangler-config.js';
|
|
6
6
|
import { requireOrigin } from '../env.js';
|
|
7
|
-
import { parseSiteConfig
|
|
8
|
-
import { normalizeConcepts } from '../content/concepts.js';
|
|
9
|
-
import { defineFields } from '../content/schema.js';
|
|
7
|
+
import { parseSiteConfig } from '../nav/site-config.js';
|
|
10
8
|
const NO_WRANGLER = skip('no wrangler.jsonc or wrangler.toml found');
|
|
11
9
|
export const configBindings = {
|
|
12
10
|
id: 'config.bindings',
|
|
@@ -147,14 +145,11 @@ export const configSiteConfig = {
|
|
|
147
145
|
if (text === null)
|
|
148
146
|
return skip(`no site.config.yaml found (looked in ${SITE_CONFIG_PATHS.join(', ')})`);
|
|
149
147
|
try {
|
|
150
|
-
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const synthetic = Object.fromEntries(Object.keys(policy).map((id) => [id, { dir: '', schema: defineFields([]) }]));
|
|
156
|
-
normalizeConcepts(synthetic, policy);
|
|
157
|
-
return pass('parsed and URL policy validated (the adapter concept set is not checkable from the CLI)');
|
|
148
|
+
// Parse-only. parseSiteConfig validates the root shape and, since Contract v2, hard-errors on a
|
|
149
|
+
// stale per-concept `content:` block (URL policy moved onto defineConcept). The per-concept URL
|
|
150
|
+
// policy is now validated at the concept declaration, which a CLI cannot reach without the adapter.
|
|
151
|
+
parseSiteConfig(text);
|
|
152
|
+
return pass('parsed (per-concept URL policy lives on the adapter concepts, not checkable from the CLI)');
|
|
158
153
|
}
|
|
159
154
|
catch (err) {
|
|
160
155
|
return fail(err instanceof Error ? err.message : String(err));
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { FileChange } from './repo.js';
|
|
2
|
+
import type { BackendEnv } from './credentials.js';
|
|
3
|
+
import type { CommitAuthor, RepoFile } from './types.js';
|
|
4
|
+
export type { BackendEnv };
|
|
5
|
+
/**
|
|
6
|
+
* A live, connected content store pinned to a default branch. The GitHub implementation already
|
|
7
|
+
* holds a minted token behind it; the engine resolves one per request. Read, commit, and branch
|
|
8
|
+
* over files only: this interface never grows a query() method.
|
|
9
|
+
*/
|
|
10
|
+
export interface Backend {
|
|
11
|
+
/** The site's default branch, for example "main". Callers reading published state pass it as the ref. */
|
|
12
|
+
readonly defaultBranch: string;
|
|
13
|
+
/** Raw file contents at a ref, or null when the path does not exist. */
|
|
14
|
+
readFile(path: string, ref: string): Promise<string | null>;
|
|
15
|
+
/** The markdown entries directly in a concept directory at a ref, newest id first. */
|
|
16
|
+
readEntries(dir: string, ref: string): Promise<RepoFile[]>;
|
|
17
|
+
/** A branch's head commit sha, or null when the branch does not exist. */
|
|
18
|
+
branchHead(branch: string): Promise<string | null>;
|
|
19
|
+
/** Branch names under a prefix, sorted. */
|
|
20
|
+
listBranches(prefix: string): Promise<string[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Commit a set of path changes atomically on a branch; returns the new commit sha. When
|
|
23
|
+
* `expectedHead` is given the commit is fail-closed: it makes one attempt and throws
|
|
24
|
+
* CommitConflictError if the branch head is not `expectedHead`. Omitting it keeps the head-merge
|
|
25
|
+
* retry the entry and publish paths rely on.
|
|
26
|
+
*/
|
|
27
|
+
commit(branch: string, changes: FileChange[], author: CommitAuthor, message: string, expectedHead?: string): Promise<string>;
|
|
28
|
+
/** Create a branch at another branch's current head. */
|
|
29
|
+
createBranch(name: string, fromBranch: string): Promise<void>;
|
|
30
|
+
/** Delete a branch; a missing branch is success. */
|
|
31
|
+
deleteBranch(name: string): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
/** The adapter's backend value: a provider that connect()s to a live Backend given the Worker env. */
|
|
34
|
+
export interface BackendProvider {
|
|
35
|
+
/** A stable tag for the implementation, for example "github-app". The non-request readers narrow on it. */
|
|
36
|
+
readonly kind: string;
|
|
37
|
+
/** The default branch, surfaced before connect() so compose-time code can read it. */
|
|
38
|
+
readonly branch: string;
|
|
39
|
+
/** Connect to a live Backend; the GitHub implementation mints and caches its token lazily. */
|
|
40
|
+
connect(env: BackendEnv): Backend;
|
|
41
|
+
}
|
|
42
|
+
/** What githubApp() returns: the generic provider plus the GitHub App's non-secret identity facts. */
|
|
43
|
+
export interface GithubAppProvider extends BackendProvider {
|
|
44
|
+
readonly kind: 'github-app';
|
|
45
|
+
readonly owner: string;
|
|
46
|
+
readonly repo: string;
|
|
47
|
+
readonly appId: string;
|
|
48
|
+
readonly installationId: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Narrow a provider to the GitHub App provider on its `kind` tag. The non-request readers (the
|
|
52
|
+
* health self-test, the build-time facts) call this before reading the GitHub-specific identity,
|
|
53
|
+
* since `BackendProvider.kind` is a bare string the compiler cannot narrow on its own.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isGithubApp(provider: BackendProvider): provider is GithubAppProvider;
|
|
56
|
+
/** The non-secret GitHub App identity an adapter carries in source; the private key stays a Worker secret. */
|
|
57
|
+
interface GithubAppConfig {
|
|
58
|
+
owner: string;
|
|
59
|
+
repo: string;
|
|
60
|
+
branch: string;
|
|
61
|
+
appId: string;
|
|
62
|
+
installationId: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The live GitHub Backend over the existing repo.ts and branches.ts transports. The token getter
|
|
66
|
+
* is injected rather than the env: connect() wires the production getter that mints and caches the
|
|
67
|
+
* installation token, and a unit test wires a literal so the in-memory fetch double intercepts the
|
|
68
|
+
* same GitHub URLs. Not barrel-exported; it is the internal test seam imported by path.
|
|
69
|
+
*/
|
|
70
|
+
export declare function makeGithubBackend(config: GithubAppConfig, getToken: () => string | Promise<string>): Backend;
|
|
71
|
+
/**
|
|
72
|
+
* The default content backend: a GitHub App over a repo branch. Returns a provider carrying the
|
|
73
|
+
* App's non-secret identity (so the build-time, health, and doctor readers narrow on kind and read
|
|
74
|
+
* it) whose connect(env) mints and caches the installation token from the Worker's private-key
|
|
75
|
+
* secret. The missing-secret CairnError stays on first token use, inside connect.
|
|
76
|
+
*/
|
|
77
|
+
export declare function githubApp(config: {
|
|
78
|
+
owner: string;
|
|
79
|
+
repo: string;
|
|
80
|
+
branch: string;
|
|
81
|
+
appId: string;
|
|
82
|
+
installationId: string;
|
|
83
|
+
}): GithubAppProvider;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// cairn-cms: the Backend seam. A Backend is read, commit, and branch operations over files,
|
|
2
|
+
// never a query(): that line is the constraint that keeps a store swappable and a database out.
|
|
3
|
+
// The adapter holds a BackendProvider from githubApp(...); the engine resolves one live Backend
|
|
4
|
+
// per request via connect(env), with the GitHub App installation token minted and cached behind
|
|
5
|
+
// the seam. makeGithubBackend takes an injectable token getter so a test wires a literal token and
|
|
6
|
+
// the in-memory fetch double intercepts the same GitHub URLs the production getter would reach.
|
|
7
|
+
import { readRaw, listMarkdown, commitFiles } from './repo.js';
|
|
8
|
+
import { branchHeadSha, createBranch as createBranchRef, deleteBranch, listBranches } from './branches.js';
|
|
9
|
+
import { appCredentials } from './credentials.js';
|
|
10
|
+
import { cachedInstallationToken } from './signing.js';
|
|
11
|
+
import { CommitConflictError } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Narrow a provider to the GitHub App provider on its `kind` tag. The non-request readers (the
|
|
14
|
+
* health self-test, the build-time facts) call this before reading the GitHub-specific identity,
|
|
15
|
+
* since `BackendProvider.kind` is a bare string the compiler cannot narrow on its own.
|
|
16
|
+
*/
|
|
17
|
+
export function isGithubApp(provider) {
|
|
18
|
+
return provider.kind === 'github-app';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The live GitHub Backend over the existing repo.ts and branches.ts transports. The token getter
|
|
22
|
+
* is injected rather than the env: connect() wires the production getter that mints and caches the
|
|
23
|
+
* installation token, and a unit test wires a literal so the in-memory fetch double intercepts the
|
|
24
|
+
* same GitHub URLs. Not barrel-exported; it is the internal test seam imported by path.
|
|
25
|
+
*/
|
|
26
|
+
export function makeGithubBackend(config, getToken) {
|
|
27
|
+
return {
|
|
28
|
+
defaultBranch: config.branch,
|
|
29
|
+
async readFile(path, ref) {
|
|
30
|
+
return readRaw({ ...config, branch: ref }, path, await getToken());
|
|
31
|
+
},
|
|
32
|
+
async readEntries(dir, ref) {
|
|
33
|
+
return listMarkdown({ ...config, branch: ref }, dir, await getToken());
|
|
34
|
+
},
|
|
35
|
+
async branchHead(branch) {
|
|
36
|
+
return branchHeadSha(config, branch, await getToken());
|
|
37
|
+
},
|
|
38
|
+
async listBranches(prefix) {
|
|
39
|
+
return listBranches(config, prefix, await getToken());
|
|
40
|
+
},
|
|
41
|
+
async commit(branch, changes, author, message, expectedHead) {
|
|
42
|
+
return commitFiles({ ...config, branch }, changes, { message, author }, await getToken(), expectedHead);
|
|
43
|
+
},
|
|
44
|
+
async createBranch(name, fromBranch) {
|
|
45
|
+
const tok = await getToken();
|
|
46
|
+
const head = await branchHeadSha(config, fromBranch, tok);
|
|
47
|
+
// A null head means the source branch is gone or unreadable. Throw a defined, catchable
|
|
48
|
+
// error the save path maps to its 500 rather than letting the createBranchRef POST fail raw.
|
|
49
|
+
if (head === null)
|
|
50
|
+
throw new CommitConflictError(`${fromBranch} (unreadable source)`);
|
|
51
|
+
await createBranchRef(config, name, head, tok);
|
|
52
|
+
},
|
|
53
|
+
async deleteBranch(name) {
|
|
54
|
+
await deleteBranch(config, name, await getToken());
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The default content backend: a GitHub App over a repo branch. Returns a provider carrying the
|
|
60
|
+
* App's non-secret identity (so the build-time, health, and doctor readers narrow on kind and read
|
|
61
|
+
* it) whose connect(env) mints and caches the installation token from the Worker's private-key
|
|
62
|
+
* secret. The missing-secret CairnError stays on first token use, inside connect.
|
|
63
|
+
*/
|
|
64
|
+
export function githubApp(config) {
|
|
65
|
+
return {
|
|
66
|
+
kind: 'github-app',
|
|
67
|
+
branch: config.branch,
|
|
68
|
+
owner: config.owner,
|
|
69
|
+
repo: config.repo,
|
|
70
|
+
appId: config.appId,
|
|
71
|
+
installationId: config.installationId,
|
|
72
|
+
connect(env) {
|
|
73
|
+
return makeGithubBackend(config, () => cachedInstallationToken(appCredentials(config, env)));
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import type { BackendConfig } from '../content/types.js';
|
|
2
1
|
import type { AppCredentials } from './types.js';
|
|
3
|
-
/**
|
|
4
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The Worker secret carrier `Backend.connect` reads: the GitHub App private key as base64 of the
|
|
4
|
+
* PEM, single line. A consumer's `App.Platform.env` block names it. Aliased as the engine's
|
|
5
|
+
* `BackendEnv` since the backend seam owns the secret channel.
|
|
6
|
+
*/
|
|
7
|
+
export interface BackendEnv {
|
|
5
8
|
GITHUB_APP_PRIVATE_KEY_B64?: string;
|
|
6
9
|
}
|
|
7
10
|
/**
|
|
8
|
-
* Assemble the `AppCredentials` the signer needs from the
|
|
11
|
+
* Assemble the `AppCredentials` the signer needs from the GitHub App's identity (app id,
|
|
9
12
|
* installation) and the Worker's private-key secret. Throws a CairnError naming
|
|
10
13
|
* `github.app-unreachable` when the secret is unset, since the App cannot authenticate without it.
|
|
11
14
|
*/
|
|
12
|
-
export declare function appCredentials(
|
|
15
|
+
export declare function appCredentials(identity: {
|
|
16
|
+
appId: string;
|
|
17
|
+
installationId: string;
|
|
18
|
+
}, env: BackendEnv): AppCredentials;
|
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
// TypeError. Mirrors requireDb/requireOrigin in env.ts.
|
|
5
5
|
import { CairnError } from '../diagnostics/index.js';
|
|
6
6
|
/**
|
|
7
|
-
* Assemble the `AppCredentials` the signer needs from the
|
|
7
|
+
* Assemble the `AppCredentials` the signer needs from the GitHub App's identity (app id,
|
|
8
8
|
* installation) and the Worker's private-key secret. Throws a CairnError naming
|
|
9
9
|
* `github.app-unreachable` when the secret is unset, since the App cannot authenticate without it.
|
|
10
10
|
*/
|
|
11
|
-
export function appCredentials(
|
|
11
|
+
export function appCredentials(identity, env) {
|
|
12
12
|
const privateKeyB64 = env.GITHUB_APP_PRIVATE_KEY_B64;
|
|
13
13
|
if (!privateKeyB64) {
|
|
14
14
|
throw new CairnError('github.app-unreachable', {
|
|
15
15
|
message: 'GITHUB_APP_PRIVATE_KEY_B64 is not configured',
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
-
return { appId:
|
|
18
|
+
return { appId: identity.appId, installationId: identity.installationId, privateKeyB64 };
|
|
19
19
|
}
|
package/dist/github/repo.d.ts
CHANGED
|
@@ -27,24 +27,6 @@ export declare function contentsUrl(repo: RepoRef, path: string): string;
|
|
|
27
27
|
* approaches it (spec §7.3).
|
|
28
28
|
*/
|
|
29
29
|
export declare function readRaw(repo: RepoRef, path: string, token?: string): Promise<string | null>;
|
|
30
|
-
/** The current blob sha for a path, or null if the file does not yet exist. */
|
|
31
|
-
export declare function fileSha(repo: RepoRef, path: string, token: string): Promise<string | null>;
|
|
32
|
-
/**
|
|
33
|
-
* Commit `content` to `path` on the configured branch through the contents API. The author is
|
|
34
|
-
* the editor; the committer is omitted, so GitHub attributes it to the App (`cairn-cms[bot]`).
|
|
35
|
-
* Updates the file in place when it exists (passing its sha), creates it otherwise. Returns the
|
|
36
|
-
* commit sha. A stale-sha 409 (someone committed in between) becomes a `CommitConflictError`,
|
|
37
|
-
* so the save fails safe: re-fetch and ask the editor to reapply, never a merge.
|
|
38
|
-
*
|
|
39
|
-
* Caller preconditions this layer cannot enforce, and the save action (Plan 05) must:
|
|
40
|
-
* `path` is confined to the concept's configured directory (the App token can write anywhere
|
|
41
|
-
* in the repo, so an unvalidated path could overwrite CI config or source), and `author` is
|
|
42
|
-
* derived from the verified server-side session, never from request input.
|
|
43
|
-
*/
|
|
44
|
-
export declare function commitFile(repo: RepoRef, path: string, content: string, opts: {
|
|
45
|
-
message: string;
|
|
46
|
-
author: CommitAuthor;
|
|
47
|
-
}, token: string): Promise<string>;
|
|
48
30
|
/** A path change for an atomic commit: write `content`, or delete the path when `content` is null. */
|
|
49
31
|
export interface FileChange {
|
|
50
32
|
path: string;
|
|
@@ -61,9 +43,16 @@ export interface FileChange {
|
|
|
61
43
|
*
|
|
62
44
|
* An empty change set is rejected, since it would otherwise push an empty commit that triggers a
|
|
63
45
|
* site redeploy for no content change.
|
|
46
|
+
*
|
|
47
|
+
* When `expectedHead` is supplied the commit is fail-closed: it makes a single attempt with no
|
|
48
|
+
* retry, throws a `CommitConflictError` when the branch head is not `expectedHead` (a concurrent
|
|
49
|
+
* commit landed), and otherwise commits onto that head. The nav and settings writes, which land on
|
|
50
|
+
* the default branch and trigger a deploy, pass it so a same-branch race surfaces the editor's
|
|
51
|
+
* reload-and-reapply prompt rather than a silent last-writer-wins. Omitting it keeps the
|
|
52
|
+
* head-merge retry the entry and publish paths rely on.
|
|
64
53
|
*/
|
|
65
54
|
export declare function commitFiles(repo: RepoRef, changes: FileChange[], opts: {
|
|
66
55
|
message: string;
|
|
67
56
|
author: CommitAuthor;
|
|
68
|
-
}, token: string): Promise<string>;
|
|
57
|
+
}, token: string, expectedHead?: string): Promise<string>;
|
|
69
58
|
export {};
|
package/dist/github/repo.js
CHANGED
|
@@ -75,51 +75,6 @@ export async function readRaw(repo, path, token) {
|
|
|
75
75
|
throw new Error(`GitHub read ${path} failed: ${res.status}`);
|
|
76
76
|
return res.text();
|
|
77
77
|
}
|
|
78
|
-
/** Standard (padded) base64 of UTF-8 text, the form the contents API expects. */
|
|
79
|
-
function toBase64(text) {
|
|
80
|
-
return btoa(Array.from(new TextEncoder().encode(text), (b) => String.fromCharCode(b)).join(''));
|
|
81
|
-
}
|
|
82
|
-
/** The current blob sha for a path, or null if the file does not yet exist. */
|
|
83
|
-
export async function fileSha(repo, path, token) {
|
|
84
|
-
const res = await fetch(contentsUrl(repo, path), { headers: ghHeaders('application/vnd.github+json', token) });
|
|
85
|
-
if (res.status === 404)
|
|
86
|
-
return null;
|
|
87
|
-
if (!res.ok)
|
|
88
|
-
throw new Error(`GitHub stat ${path} failed: ${res.status}`);
|
|
89
|
-
return (await res.json()).sha;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Commit `content` to `path` on the configured branch through the contents API. The author is
|
|
93
|
-
* the editor; the committer is omitted, so GitHub attributes it to the App (`cairn-cms[bot]`).
|
|
94
|
-
* Updates the file in place when it exists (passing its sha), creates it otherwise. Returns the
|
|
95
|
-
* commit sha. A stale-sha 409 (someone committed in between) becomes a `CommitConflictError`,
|
|
96
|
-
* so the save fails safe: re-fetch and ask the editor to reapply, never a merge.
|
|
97
|
-
*
|
|
98
|
-
* Caller preconditions this layer cannot enforce, and the save action (Plan 05) must:
|
|
99
|
-
* `path` is confined to the concept's configured directory (the App token can write anywhere
|
|
100
|
-
* in the repo, so an unvalidated path could overwrite CI config or source), and `author` is
|
|
101
|
-
* derived from the verified server-side session, never from request input.
|
|
102
|
-
*/
|
|
103
|
-
export async function commitFile(repo, path, content, opts, token) {
|
|
104
|
-
const sha = await fileSha(repo, path, token);
|
|
105
|
-
const url = `${API}/repos/${repo.owner}/${repo.repo}/contents/${path.replace(/^\/+|\/+$/g, '')}`;
|
|
106
|
-
const res = await fetch(url, {
|
|
107
|
-
method: 'PUT',
|
|
108
|
-
headers: { ...ghHeaders('application/vnd.github+json', token), 'Content-Type': 'application/json' },
|
|
109
|
-
body: JSON.stringify({
|
|
110
|
-
message: opts.message,
|
|
111
|
-
content: toBase64(content),
|
|
112
|
-
branch: repo.branch,
|
|
113
|
-
author: opts.author,
|
|
114
|
-
...(sha ? { sha } : {}),
|
|
115
|
-
}),
|
|
116
|
-
});
|
|
117
|
-
if (res.status === 409)
|
|
118
|
-
throw new CommitConflictError(path);
|
|
119
|
-
if (!res.ok)
|
|
120
|
-
throw new Error(`GitHub commit ${path} failed: ${res.status} ${await res.text()}`);
|
|
121
|
-
return (await res.json()).commit.sha;
|
|
122
|
-
}
|
|
123
78
|
/** A Git Data API URL under the repo's `git/` namespace. */
|
|
124
79
|
function gitUrl(repo, suffix) {
|
|
125
80
|
return `${API}/repos/${repo.owner}/${repo.repo}/git/${suffix}`;
|
|
@@ -150,6 +105,57 @@ function treeChanges(changes) {
|
|
|
150
105
|
}
|
|
151
106
|
/** Retries after the initial attempt when the branch moves under an atomic commit. */
|
|
152
107
|
const COMMIT_RETRIES = 3;
|
|
108
|
+
/**
|
|
109
|
+
* Build a tree on `parent`'s tree, create the commit, and PATCH the branch ref to it. Returns the
|
|
110
|
+
* new commit sha, or null when the ref PATCH is a non-fast-forward (the head moved). A tree-create
|
|
111
|
+
* 422 (an unprocessable delete) becomes a `CommitConflictError`, and any other non-fast-forward
|
|
112
|
+
* detail is left to the caller to map.
|
|
113
|
+
*/
|
|
114
|
+
async function commitOnTree(repo, parent, tree, opts, token) {
|
|
115
|
+
const baseTree = await commitTreeSha(repo, parent, token);
|
|
116
|
+
const treeRes = await fetch(gitUrl(repo, 'trees'), {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: { ...ghHeaders('application/vnd.github+json', token), 'Content-Type': 'application/json' },
|
|
119
|
+
body: JSON.stringify({ base_tree: baseTree, tree }),
|
|
120
|
+
});
|
|
121
|
+
if (!treeRes.ok) {
|
|
122
|
+
// A 422 means an entry is unprocessable against the base tree, which a delete of an
|
|
123
|
+
// already-removed path produces (a concurrent delete or rename got there first). Treat it as
|
|
124
|
+
// the same non-fast-forward conflict the ref PATCH surfaces, so the caller fails safe with the
|
|
125
|
+
// reload-and-retry path instead of a raw 500.
|
|
126
|
+
if (treeRes.status === 422)
|
|
127
|
+
throw new CommitConflictError(`${repo.branch} (tree create)`);
|
|
128
|
+
throw new Error(`GitHub tree create failed: ${treeRes.status} ${await treeRes.text()}`);
|
|
129
|
+
}
|
|
130
|
+
const newTree = (await treeRes.json()).sha;
|
|
131
|
+
const commitRes = await fetch(gitUrl(repo, 'commits'), {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { ...ghHeaders('application/vnd.github+json', token), 'Content-Type': 'application/json' },
|
|
134
|
+
body: JSON.stringify({ message: opts.message, tree: newTree, parents: [parent], author: opts.author }),
|
|
135
|
+
});
|
|
136
|
+
if (!commitRes.ok)
|
|
137
|
+
throw new Error(`GitHub commit create failed: ${commitRes.status} ${await commitRes.text()}`);
|
|
138
|
+
const newCommit = (await commitRes.json()).sha;
|
|
139
|
+
const refRes = await fetch(gitUrl(repo, `refs/heads/${encodeURIComponent(repo.branch)}`), {
|
|
140
|
+
method: 'PATCH',
|
|
141
|
+
headers: { ...ghHeaders('application/vnd.github+json', token), 'Content-Type': 'application/json' },
|
|
142
|
+
body: JSON.stringify({ sha: newCommit, force: false }),
|
|
143
|
+
});
|
|
144
|
+
if (refRes.ok)
|
|
145
|
+
return { sha: newCommit };
|
|
146
|
+
// A non-fast-forward means the branch moved; the caller decides whether to retry or fail closed.
|
|
147
|
+
// Any other failure is not a race, so surface it.
|
|
148
|
+
if (refRes.status !== 422)
|
|
149
|
+
throw new Error(`GitHub ref update failed: ${refRes.status} ${await refRes.text()}`);
|
|
150
|
+
return { conflict: true };
|
|
151
|
+
}
|
|
152
|
+
/** Fail-closed commit on a known head: a non-fast-forward becomes a `CommitConflictError`. */
|
|
153
|
+
async function commitOnHead(repo, head, tree, opts, token) {
|
|
154
|
+
const result = await commitOnTree(repo, head, tree, opts, token);
|
|
155
|
+
if ('conflict' in result)
|
|
156
|
+
throw new CommitConflictError(`${repo.branch} (head moved)`);
|
|
157
|
+
return result.sha;
|
|
158
|
+
}
|
|
153
159
|
/**
|
|
154
160
|
* Commit several path changes in one commit over the Git Data API. The author is the editor; the
|
|
155
161
|
* committer is omitted, so GitHub attributes the commit to the App. Returns the new commit sha.
|
|
@@ -161,48 +167,31 @@ const COMMIT_RETRIES = 3;
|
|
|
161
167
|
*
|
|
162
168
|
* An empty change set is rejected, since it would otherwise push an empty commit that triggers a
|
|
163
169
|
* site redeploy for no content change.
|
|
170
|
+
*
|
|
171
|
+
* When `expectedHead` is supplied the commit is fail-closed: it makes a single attempt with no
|
|
172
|
+
* retry, throws a `CommitConflictError` when the branch head is not `expectedHead` (a concurrent
|
|
173
|
+
* commit landed), and otherwise commits onto that head. The nav and settings writes, which land on
|
|
174
|
+
* the default branch and trigger a deploy, pass it so a same-branch race surfaces the editor's
|
|
175
|
+
* reload-and-reapply prompt rather than a silent last-writer-wins. Omitting it keeps the
|
|
176
|
+
* head-merge retry the entry and publish paths rely on.
|
|
164
177
|
*/
|
|
165
|
-
export async function commitFiles(repo, changes, opts, token) {
|
|
178
|
+
export async function commitFiles(repo, changes, opts, token, expectedHead) {
|
|
166
179
|
if (changes.length === 0)
|
|
167
180
|
throw new Error('commitFiles: no changes to commit');
|
|
168
181
|
const tree = treeChanges(changes);
|
|
182
|
+
if (expectedHead !== undefined) {
|
|
183
|
+
const head = await headCommitSha(repo, token);
|
|
184
|
+
if (head !== expectedHead)
|
|
185
|
+
throw new CommitConflictError(`${repo.branch} (head moved)`);
|
|
186
|
+
return commitOnHead(repo, head, tree, opts, token);
|
|
187
|
+
}
|
|
169
188
|
for (let attempt = 0; attempt <= COMMIT_RETRIES; attempt++) {
|
|
170
189
|
const parent = await headCommitSha(repo, token);
|
|
171
|
-
const
|
|
172
|
-
const treeRes = await fetch(gitUrl(repo, 'trees'), {
|
|
173
|
-
method: 'POST',
|
|
174
|
-
headers: { ...ghHeaders('application/vnd.github+json', token), 'Content-Type': 'application/json' },
|
|
175
|
-
body: JSON.stringify({ base_tree: baseTree, tree }),
|
|
176
|
-
});
|
|
177
|
-
if (!treeRes.ok) {
|
|
178
|
-
// A 422 means an entry is unprocessable against the base tree, which a delete of an
|
|
179
|
-
// already-removed path produces (a concurrent delete or rename got there first). Treat it as
|
|
180
|
-
// the same non-fast-forward conflict the ref PATCH surfaces, so the caller fails safe with the
|
|
181
|
-
// reload-and-retry path instead of a raw 500.
|
|
182
|
-
if (treeRes.status === 422)
|
|
183
|
-
throw new CommitConflictError(`${repo.branch} (tree create)`);
|
|
184
|
-
throw new Error(`GitHub tree create failed: ${treeRes.status} ${await treeRes.text()}`);
|
|
185
|
-
}
|
|
186
|
-
const newTree = (await treeRes.json()).sha;
|
|
187
|
-
const commitRes = await fetch(gitUrl(repo, 'commits'), {
|
|
188
|
-
method: 'POST',
|
|
189
|
-
headers: { ...ghHeaders('application/vnd.github+json', token), 'Content-Type': 'application/json' },
|
|
190
|
-
body: JSON.stringify({ message: opts.message, tree: newTree, parents: [parent], author: opts.author }),
|
|
191
|
-
});
|
|
192
|
-
if (!commitRes.ok)
|
|
193
|
-
throw new Error(`GitHub commit create failed: ${commitRes.status} ${await commitRes.text()}`);
|
|
194
|
-
const newCommit = (await commitRes.json()).sha;
|
|
195
|
-
const refRes = await fetch(gitUrl(repo, `refs/heads/${encodeURIComponent(repo.branch)}`), {
|
|
196
|
-
method: 'PATCH',
|
|
197
|
-
headers: { ...ghHeaders('application/vnd.github+json', token), 'Content-Type': 'application/json' },
|
|
198
|
-
body: JSON.stringify({ sha: newCommit, force: false }),
|
|
199
|
-
});
|
|
200
|
-
if (refRes.ok)
|
|
201
|
-
return newCommit;
|
|
190
|
+
const result = await commitOnTree(repo, parent, tree, opts, token);
|
|
202
191
|
// A non-fast-forward means the branch moved; retry on the new head so a concurrent commit
|
|
203
|
-
// is preserved.
|
|
204
|
-
if (
|
|
205
|
-
|
|
192
|
+
// is preserved.
|
|
193
|
+
if ('sha' in result)
|
|
194
|
+
return result.sha;
|
|
206
195
|
}
|
|
207
196
|
throw new CommitConflictError(`${repo.branch} (atomic commit)`);
|
|
208
197
|
}
|
package/dist/github/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Repo coordinates pinned to a branch: the
|
|
1
|
+
/** Repo coordinates pinned to a branch: the `{ owner, repo, branch }` subset the read and commit paths need. */
|
|
2
2
|
export interface RepoRef {
|
|
3
3
|
owner: string;
|
|
4
4
|
repo: string;
|
package/dist/github/types.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// cairn-cms: the GitHub backend's plain data types and its one typed error. The
|
|
2
|
-
//
|
|
3
|
-
// `{ owner, repo, branch }` subset
|
|
4
|
-
// wanted with no conversion.
|
|
1
|
+
// cairn-cms: the GitHub backend's plain data types and its one typed error. The GitHub App
|
|
2
|
+
// provider config (`githubApp`'s input) carries these repo coordinates; `RepoRef` is the
|
|
3
|
+
// `{ owner, repo, branch }` subset the read and commit transports take, so the provider config
|
|
4
|
+
// is assignable wherever a `RepoRef` is wanted with no conversion.
|
|
5
5
|
/**
|
|
6
6
|
* A concurrent edit lost the SHA race: the file changed between the read and the PUT, from
|
|
7
7
|
* another editor or the site's own CI. Thrown so the save fails safe (re-fetch and ask the
|
package/dist/index.d.ts
CHANGED
|
@@ -2,26 +2,27 @@ export { requireOrigin } from './env.js';
|
|
|
2
2
|
export type { Role, Editor, AuthEnv } from './auth/types.js';
|
|
3
3
|
export type { AuthBranding, MagicLinkMessage, SendMagicLink } from './email.js';
|
|
4
4
|
export { buildMagicLinkMessage, cloudflareSend } from './email.js';
|
|
5
|
-
export type { CairnAdapter, ConceptConfig,
|
|
6
|
-
export {
|
|
5
|
+
export type { CairnAdapter, ConceptConfig, NamedField, ImageValue, ValidationResult, ValidationIssue, SenderConfig, NavMenuConfig, PreviewConfig, ResolvedPreview, AssetConfig, RoutingRule, ConceptDescriptor, ConceptUrlPolicy, CairnExtension, CairnRuntime, SiteRender, AdminPanel, FieldTypeDef, } from './content/types.js';
|
|
6
|
+
export { normalizeConcepts, findConcept, defineConcept } from './content/concepts.js';
|
|
7
7
|
export { composeRuntime } from './content/compose.js';
|
|
8
8
|
export type { ComposeInput } from './content/compose.js';
|
|
9
9
|
export { frontmatterFromForm, dateInputValue, serializeMarkdown, parseMarkdown, } from './content/frontmatter.js';
|
|
10
|
-
export { defineFields } from './content/schema.js';
|
|
11
10
|
export { defineAdapter } from './content/adapter.js';
|
|
12
|
-
export type {
|
|
11
|
+
export type { StandardInput, StandardSchemaV1 } from './content/standard-schema.js';
|
|
13
12
|
export { fields } from './content/fields.js';
|
|
14
|
-
export type { FieldDescriptor } from './content/fields.js';
|
|
13
|
+
export type { FieldDescriptor, TextField, TextareaField, NumberField, SelectField, MultiselectField, UrlField, EmailField, DateField, DatetimeField, BooleanField, IconField, ImageField, ObjectField, ReferenceField, ArrayField, } from './content/fields.js';
|
|
15
14
|
export { fieldset, initialValues } from './content/fieldset.js';
|
|
16
|
-
export type { Fieldset, InferFieldset, FieldsetOptions, BehaviorTable } from './content/fieldset.js';
|
|
15
|
+
export type { Fieldset, InferFieldset, FieldsetOptions, BehaviorTable, FieldBehavior } from './content/fieldset.js';
|
|
17
16
|
export { isValidId, idFromFilename, filenameFromId, slugify, slugFromId, composeDatedId, } from './content/ids.js';
|
|
18
17
|
export type { DatePrefix } from './content/ids.js';
|
|
19
18
|
export { parseCairnToken, extractCairnLinks, formatCairnToken, escapeLinkText } from './content/links.js';
|
|
20
19
|
export type { CairnRef, LinkResolve } from './content/links.js';
|
|
21
|
-
export { serializeManifest, parseManifest, emptyManifest, verifyManifest, diffManifests, upsertEntry, removeEntry, manifestEntryFromFile, manifestLinkResolver, inboundLinks, } from './content/manifest.js';
|
|
22
|
-
export type { Manifest, ManifestEntry, ManifestDiff, ManifestEntryDiff, LinkTarget, InboundLink } from './content/manifest.js';
|
|
23
|
-
export {
|
|
24
|
-
export type {
|
|
20
|
+
export { serializeManifest, parseManifest, emptyManifest, verifyManifest, verifyReferences, diffManifests, upsertEntry, removeEntry, manifestEntryFromFile, manifestLinkResolver, inboundLinks, } from './content/manifest.js';
|
|
21
|
+
export type { Manifest, ManifestEntry, ManifestDiff, ManifestEntryDiff, LinkTarget, InboundLink, InboundReference } from './content/manifest.js';
|
|
22
|
+
export type { ReferenceEdge } from './content/references.js';
|
|
23
|
+
export type { ResolvedReference } from './delivery/site-resolver.js';
|
|
24
|
+
export { defineRegistry, defineComponent, emptyValues } from './render/registry.js';
|
|
25
|
+
export type { ComponentDef, ComponentRegistry, SlotKind, SlotDef, ComponentValues, } from './render/registry.js';
|
|
25
26
|
export { serializeComponent, parseComponent } from './render/component-grammar.js';
|
|
26
27
|
export { validateComponent } from './render/component-validate.js';
|
|
27
28
|
export type { ComponentValidation } from './render/component-validate.js';
|
|
@@ -33,7 +34,10 @@ export type { IconSet } from './render/glyph.js';
|
|
|
33
34
|
export { remarkDirectiveStamp } from './render/remark-directives.js';
|
|
34
35
|
export { createRenderer } from './render/pipeline.js';
|
|
35
36
|
export type { RendererOptions } from './render/pipeline.js';
|
|
36
|
-
export type {
|
|
37
|
+
export type { RepoFile, CommitAuthor } from './github/types.js';
|
|
37
38
|
export { CommitConflictError } from './github/types.js';
|
|
38
|
-
export {
|
|
39
|
+
export { githubApp } from './github/backend.js';
|
|
40
|
+
export type { Backend, BackendProvider, GithubAppProvider, BackendEnv } from './github/backend.js';
|
|
41
|
+
export type { FileChange } from './github/repo.js';
|
|
42
|
+
export { parseSiteConfig, extractMenu, setMenu, validateNavTree, MAX_NAV_NODES, NavValidationError, SiteConfigError, } from './nav/site-config.js';
|
|
39
43
|
export type { NavNode, SiteConfig } from './nav/site-config.js';
|