@glw907/cairn-cms 0.40.0 → 0.50.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 +76 -0
- package/README.md +3 -3
- package/dist/ambient.d.ts +9 -0
- package/dist/ambient.js +1 -0
- package/dist/components/AdminLayout.svelte +6 -8
- package/dist/components/CairnAdmin.svelte +67 -0
- package/dist/components/CairnAdmin.svelte.d.ts +35 -0
- package/dist/components/ConceptList.svelte +18 -10
- package/dist/components/ConceptList.svelte.d.ts +4 -8
- package/dist/components/ConfirmPage.svelte +1 -1
- package/dist/components/EditPage.svelte +47 -19
- package/dist/components/EditPage.svelte.d.ts +4 -9
- package/dist/components/EditorToolbar.svelte +4 -0
- package/dist/components/LoginPage.svelte +2 -2
- package/dist/components/LoginPage.svelte.d.ts +1 -1
- package/dist/components/ManageEditors.svelte +4 -3
- package/dist/components/ManageEditors.svelte.d.ts +2 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/link-completion.js +10 -3
- package/dist/components/markdown-format.d.ts +0 -8
- package/dist/components/markdown-format.js +0 -28
- package/dist/content/links.d.ts +8 -0
- package/dist/content/links.js +28 -0
- package/dist/content/types.d.ts +2 -2
- package/dist/delivery/data.d.ts +3 -5
- package/dist/delivery/data.js +2 -3
- package/dist/delivery/feeds.js +1 -7
- package/dist/delivery/index.d.ts +2 -2
- package/dist/delivery/index.js +1 -1
- package/dist/delivery/manifest.d.ts +0 -5
- package/dist/delivery/manifest.js +5 -16
- package/dist/{sveltekit → delivery}/public-routes.d.ts +4 -4
- package/dist/{sveltekit → delivery}/public-routes.js +7 -7
- package/dist/delivery/site-indexes.d.ts +3 -3
- package/dist/delivery/site-indexes.js +3 -3
- package/dist/delivery/{site-index.d.ts → site-resolver.d.ts} +7 -3
- package/dist/delivery/{site-index.js → site-resolver.js} +13 -3
- package/dist/delivery/sitemap.js +1 -3
- package/dist/delivery/xml.d.ts +2 -0
- package/dist/delivery/xml.js +11 -0
- package/dist/diagnostics/conditions.d.ts +8 -1
- package/dist/diagnostics/conditions.js +68 -1
- package/dist/doctor/bin.d.ts +2 -0
- package/dist/doctor/bin.js +44 -0
- package/dist/doctor/check-send.d.ts +3 -0
- package/dist/doctor/check-send.js +43 -0
- package/dist/doctor/checks-cloudflare.d.ts +5 -0
- package/dist/doctor/checks-cloudflare.js +200 -0
- package/dist/doctor/checks-github.d.ts +2 -0
- package/dist/doctor/checks-github.js +57 -0
- package/dist/doctor/checks-local.d.ts +5 -0
- package/dist/doctor/checks-local.js +112 -0
- package/dist/doctor/cloudflare-api.d.ts +7 -0
- package/dist/doctor/cloudflare-api.js +24 -0
- package/dist/doctor/index.d.ts +23 -0
- package/dist/doctor/index.js +68 -0
- package/dist/doctor/report.d.ts +5 -0
- package/dist/doctor/report.js +21 -0
- package/dist/doctor/run.d.ts +8 -0
- package/dist/doctor/run.js +20 -0
- package/dist/doctor/types.d.ts +41 -0
- package/dist/doctor/types.js +10 -0
- package/dist/doctor/wrangler-config.d.ts +12 -0
- package/dist/doctor/wrangler-config.js +125 -0
- package/dist/email.js +4 -11
- package/dist/env.d.ts +1 -1
- package/dist/env.js +3 -2
- package/dist/escape.d.ts +2 -0
- package/dist/escape.js +11 -0
- package/dist/github/credentials.d.ts +2 -1
- package/dist/github/credentials.js +10 -2
- package/dist/github/signing.d.ts +3 -1
- package/dist/github/signing.js +13 -5
- package/dist/github/types.d.ts +2 -0
- package/dist/github/types.js +4 -0
- package/dist/log/events.d.ts +1 -1
- package/dist/nav/site-config.d.ts +2 -0
- package/dist/nav/site-config.js +2 -0
- package/dist/sveltekit/admin-dispatch.d.ts +28 -0
- package/dist/sveltekit/admin-dispatch.js +62 -0
- package/dist/sveltekit/cairn-admin.d.ts +94 -0
- package/dist/sveltekit/cairn-admin.js +126 -0
- package/dist/sveltekit/condition-response.d.ts +1 -0
- package/dist/sveltekit/condition-response.js +25 -0
- package/dist/sveltekit/content-routes.d.ts +34 -14
- package/dist/sveltekit/content-routes.js +78 -44
- package/dist/sveltekit/guard.js +15 -3
- package/dist/sveltekit/https-required-page.js +2 -1
- package/dist/sveltekit/index.d.ts +3 -1
- package/dist/sveltekit/index.js +2 -0
- package/dist/sveltekit/nav-routes.d.ts +3 -1
- package/dist/sveltekit/nav-routes.js +19 -10
- package/dist/sveltekit/static-admin-page.d.ts +0 -2
- package/dist/sveltekit/static-admin-page.js +1 -8
- package/dist/sveltekit/types.d.ts +18 -11
- package/package.json +10 -4
- package/src/lib/ambient.ts +19 -0
- package/src/lib/components/AdminLayout.svelte +6 -8
- package/src/lib/components/CairnAdmin.svelte +67 -0
- package/src/lib/components/ConceptList.svelte +18 -10
- package/src/lib/components/ConfirmPage.svelte +1 -1
- package/src/lib/components/EditPage.svelte +47 -19
- package/src/lib/components/EditorToolbar.svelte +4 -0
- package/src/lib/components/LoginPage.svelte +2 -2
- package/src/lib/components/ManageEditors.svelte +4 -3
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/link-completion.ts +10 -3
- package/src/lib/components/markdown-format.ts +0 -27
- package/src/lib/content/links.ts +28 -0
- package/src/lib/content/types.ts +2 -2
- package/src/lib/delivery/data.ts +3 -5
- package/src/lib/delivery/feeds.ts +1 -8
- package/src/lib/delivery/index.ts +2 -2
- package/src/lib/delivery/manifest.ts +5 -18
- package/src/lib/{sveltekit → delivery}/public-routes.ts +11 -11
- package/src/lib/delivery/site-indexes.ts +6 -6
- package/src/lib/delivery/{site-index.ts → site-resolver.ts} +20 -8
- package/src/lib/delivery/sitemap.ts +1 -4
- package/src/lib/delivery/xml.ts +12 -0
- package/src/lib/diagnostics/conditions.ts +75 -2
- package/src/lib/doctor/bin.ts +45 -0
- package/src/lib/doctor/check-send.ts +43 -0
- package/src/lib/doctor/checks-cloudflare.ts +222 -0
- package/src/lib/doctor/checks-github.ts +63 -0
- package/src/lib/doctor/checks-local.ts +119 -0
- package/src/lib/doctor/cloudflare-api.ts +33 -0
- package/src/lib/doctor/index.ts +93 -0
- package/src/lib/doctor/report.ts +30 -0
- package/src/lib/doctor/run.ts +23 -0
- package/src/lib/doctor/types.ts +52 -0
- package/src/lib/doctor/wrangler-config.ts +142 -0
- package/src/lib/email.ts +4 -11
- package/src/lib/env.ts +3 -2
- package/src/lib/escape.ts +12 -0
- package/src/lib/github/credentials.ts +6 -2
- package/src/lib/github/signing.ts +13 -6
- package/src/lib/github/types.ts +5 -0
- package/src/lib/log/events.ts +2 -0
- package/src/lib/nav/site-config.ts +3 -0
- package/src/lib/sveltekit/admin-dispatch.ts +75 -0
- package/src/lib/sveltekit/cairn-admin.ts +177 -0
- package/src/lib/sveltekit/condition-response.ts +27 -1
- package/src/lib/sveltekit/content-routes.ts +121 -55
- package/src/lib/sveltekit/guard.ts +16 -3
- package/src/lib/sveltekit/https-required-page.ts +2 -1
- package/src/lib/sveltekit/index.ts +6 -0
- package/src/lib/sveltekit/nav-routes.ts +21 -11
- package/src/lib/sveltekit/static-admin-page.ts +1 -9
- package/src/lib/sveltekit/types.ts +16 -7
- package/dist/delivery/paginate.d.ts +0 -12
- package/dist/delivery/paginate.js +0 -20
- package/dist/render/index.d.ts +0 -5
- package/dist/render/index.js +0 -8
- package/src/lib/delivery/paginate.ts +0 -32
- package/src/lib/render/index.ts +0 -8
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { syntaxTree } from '@codemirror/language';
|
|
2
1
|
import { formatCairnToken, escapeLinkText } from '../content/links.js';
|
|
2
|
+
// EditPage imports this module statically, so a static @codemirror value import here would pull
|
|
3
|
+
// CodeMirror into a consumer's server bundle. syntaxTree resolves lazily inside the source
|
|
4
|
+
// instead (a CompletionSource may return a Promise), cached after the first completion.
|
|
5
|
+
let langMod = null;
|
|
3
6
|
/** The known concepts in display order; an unlisted concept sorts after these under its own name. */
|
|
4
7
|
const CONCEPT_SECTIONS = {
|
|
5
8
|
pages: { name: 'Pages', rank: 0 },
|
|
@@ -30,7 +33,7 @@ export function linkCompletions(targets, query) {
|
|
|
30
33
|
* whole `[[query` with the chosen link, and sets filter:false because linkCompletions already
|
|
31
34
|
* filtered by the query (CodeMirror would otherwise re-filter against the literal `[[query`). */
|
|
32
35
|
export function cairnLinkCompletionSource(targets) {
|
|
33
|
-
return (context) => {
|
|
36
|
+
return async (context) => {
|
|
34
37
|
const line = context.state.doc.lineAt(context.pos);
|
|
35
38
|
const before = context.state.sliceDoc(line.from, context.pos);
|
|
36
39
|
const trigger = matchCairnTrigger(before);
|
|
@@ -38,7 +41,11 @@ export function cairnLinkCompletionSource(targets) {
|
|
|
38
41
|
return null;
|
|
39
42
|
// Skip a [[ inside a fenced or inline code node: a cairn link there would be literal text, and
|
|
40
43
|
// the build resolver does not look inside code. The node name carries "Code" for both forms.
|
|
41
|
-
|
|
44
|
+
langMod ??= await import('@codemirror/language');
|
|
45
|
+
// The first completion awaits the import above, so the request may already be stale here.
|
|
46
|
+
if (context.aborted)
|
|
47
|
+
return null;
|
|
48
|
+
const node = langMod.syntaxTree(context.state).resolveInner(context.pos, -1);
|
|
42
49
|
for (let n = node; n; n = n.parent) {
|
|
43
50
|
if (/Code/.test(n.name))
|
|
44
51
|
return null;
|
|
@@ -22,11 +22,3 @@ export declare function insertInlineLink(doc: string, from: number, to: number,
|
|
|
22
22
|
* is left in place.
|
|
23
23
|
*/
|
|
24
24
|
export declare function unwrapCairnLink(doc: string, href: string): string;
|
|
25
|
-
/**
|
|
26
|
-
* Rewrite every cairn: link whose href is exactly `oldHref` so its href becomes `newHref`, keeping
|
|
27
|
-
* the display text and any link title byte-for-byte. Rename calls this to repoint a renamed entry's
|
|
28
|
-
* inbound tokens. Parsed with the same remark pipeline as extractCairnLinks, so a token inside a code
|
|
29
|
-
* span is not a link node and is never touched. Each matching node's source span is rewritten from
|
|
30
|
-
* last to first, replacing only the `](oldHref` run so the label and title stay exact.
|
|
31
|
-
*/
|
|
32
|
-
export declare function rewriteCairnLink(doc: string, oldHref: string, newHref: string): string;
|
|
@@ -157,31 +157,3 @@ export function unwrapCairnLink(doc, href) {
|
|
|
157
157
|
}
|
|
158
158
|
return out;
|
|
159
159
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Rewrite every cairn: link whose href is exactly `oldHref` so its href becomes `newHref`, keeping
|
|
162
|
-
* the display text and any link title byte-for-byte. Rename calls this to repoint a renamed entry's
|
|
163
|
-
* inbound tokens. Parsed with the same remark pipeline as extractCairnLinks, so a token inside a code
|
|
164
|
-
* span is not a link node and is never touched. Each matching node's source span is rewritten from
|
|
165
|
-
* last to first, replacing only the `](oldHref` run so the label and title stay exact.
|
|
166
|
-
*/
|
|
167
|
-
export function rewriteCairnLink(doc, oldHref, newHref) {
|
|
168
|
-
const tree = unified().use(remarkParse).use(remarkGfm).parse(doc);
|
|
169
|
-
const spans = [];
|
|
170
|
-
visit(tree, 'link', (node) => {
|
|
171
|
-
if (node.url !== oldHref)
|
|
172
|
-
return;
|
|
173
|
-
const start = node.position?.start?.offset;
|
|
174
|
-
const end = node.position?.end?.offset;
|
|
175
|
-
if (start == null || end == null)
|
|
176
|
-
return;
|
|
177
|
-
spans.push({ start, end });
|
|
178
|
-
});
|
|
179
|
-
spans.sort((a, b) => b.start - a.start);
|
|
180
|
-
let out = doc;
|
|
181
|
-
for (const span of spans) {
|
|
182
|
-
const src = out.slice(span.start, span.end);
|
|
183
|
-
const rewritten = src.replace(`](${oldHref}`, `](${newHref}`);
|
|
184
|
-
out = out.slice(0, span.start) + rewritten + out.slice(span.end);
|
|
185
|
-
}
|
|
186
|
-
return out;
|
|
187
|
-
}
|
package/dist/content/links.d.ts
CHANGED
|
@@ -18,3 +18,11 @@ export declare function escapeLinkText(text: string): string;
|
|
|
18
18
|
/** The cairn links a markdown body points at, in first-occurrence order, deduped by concept/id.
|
|
19
19
|
* Parses the body as mdast, so a token inside a code span or fence is never matched. */
|
|
20
20
|
export declare function extractCairnLinks(body: string): CairnRef[];
|
|
21
|
+
/**
|
|
22
|
+
* Rewrite every cairn: link whose href is exactly `oldHref` so its href becomes `newHref`, keeping
|
|
23
|
+
* the display text and any link title byte-for-byte. Rename calls this to repoint a renamed entry's
|
|
24
|
+
* inbound tokens. Parsed with the same remark pipeline as extractCairnLinks, so a token inside a code
|
|
25
|
+
* span is not a link node and is never touched. Each matching node's source span is rewritten from
|
|
26
|
+
* last to first, replacing only the `](oldHref` run so the label and title stay exact.
|
|
27
|
+
*/
|
|
28
|
+
export declare function rewriteCairnLink(doc: string, oldHref: string, newHref: string): string;
|
package/dist/content/links.js
CHANGED
|
@@ -50,3 +50,31 @@ export function extractCairnLinks(body) {
|
|
|
50
50
|
});
|
|
51
51
|
return refs;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Rewrite every cairn: link whose href is exactly `oldHref` so its href becomes `newHref`, keeping
|
|
55
|
+
* the display text and any link title byte-for-byte. Rename calls this to repoint a renamed entry's
|
|
56
|
+
* inbound tokens. Parsed with the same remark pipeline as extractCairnLinks, so a token inside a code
|
|
57
|
+
* span is not a link node and is never touched. Each matching node's source span is rewritten from
|
|
58
|
+
* last to first, replacing only the `](oldHref` run so the label and title stay exact.
|
|
59
|
+
*/
|
|
60
|
+
export function rewriteCairnLink(doc, oldHref, newHref) {
|
|
61
|
+
const tree = unified().use(remarkParse).use(remarkGfm).parse(doc);
|
|
62
|
+
const spans = [];
|
|
63
|
+
visit(tree, 'link', (node) => {
|
|
64
|
+
if (node.url !== oldHref)
|
|
65
|
+
return;
|
|
66
|
+
const start = node.position?.start?.offset;
|
|
67
|
+
const end = node.position?.end?.offset;
|
|
68
|
+
if (start == null || end == null)
|
|
69
|
+
return;
|
|
70
|
+
spans.push({ start, end });
|
|
71
|
+
});
|
|
72
|
+
spans.sort((a, b) => b.start - a.start);
|
|
73
|
+
let out = doc;
|
|
74
|
+
for (const span of spans) {
|
|
75
|
+
const src = out.slice(span.start, span.end);
|
|
76
|
+
const rewritten = src.replace(`](${oldHref}`, `](${newHref}`);
|
|
77
|
+
out = out.slice(0, span.start) + rewritten + out.slice(span.end);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
package/dist/content/types.d.ts
CHANGED
|
@@ -154,8 +154,8 @@ export interface CairnAdapter {
|
|
|
154
154
|
backend: BackendConfig;
|
|
155
155
|
sender: SenderConfig;
|
|
156
156
|
/** The site's one renderer: the editor preview and every public page call it (design decision 4).
|
|
157
|
-
* `resolve` rewrites cairn: links to live permalinks; the build passes a site-
|
|
158
|
-
* preview a manifest one. */
|
|
157
|
+
* `resolve` rewrites cairn: links to live permalinks; the build passes a site-resolver-backed
|
|
158
|
+
* one, the preview a manifest one. */
|
|
159
159
|
render(md: string, opts?: {
|
|
160
160
|
stagger?: boolean;
|
|
161
161
|
resolve?: LinkResolve;
|
package/dist/delivery/data.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createContentIndex, fromGlob } from './content-index.js';
|
|
2
2
|
export type { RawFile, ContentSummary, ContentEntry, ContentIndex, ContentProblem } from './content-index.js';
|
|
3
|
-
export {
|
|
4
|
-
export type {
|
|
3
|
+
export { createSiteResolver, buildLinkResolver } from './site-resolver.js';
|
|
4
|
+
export type { SiteResolver, ConceptIndex } from './site-resolver.js';
|
|
5
5
|
export { createSiteIndexes } from './site-indexes.js';
|
|
6
6
|
export type { SiteIndexes, SiteGlobs } from './site-indexes.js';
|
|
7
7
|
export { siteDescriptors } from './site-descriptors.js';
|
|
@@ -15,9 +15,7 @@ export { buildSeoMeta } from './seo.js';
|
|
|
15
15
|
export type { SeoInput, SeoMeta } from './seo.js';
|
|
16
16
|
export { readSeoFields, resolveImageUrl } from './seo-fields.js';
|
|
17
17
|
export type { SeoFields } from './seo-fields.js';
|
|
18
|
-
export { paginate } from './paginate.js';
|
|
19
|
-
export type { Page } from './paginate.js';
|
|
20
18
|
export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
|
|
21
19
|
export { jsonLdScript } from './json-ld.js';
|
|
22
20
|
export { permalink } from '../content/permalink.js';
|
|
23
|
-
export { buildSiteManifest
|
|
21
|
+
export { buildSiteManifest } from './manifest.js';
|
package/dist/delivery/data.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// projections a SvelteKit site or a plain-Node tool reads, with no @sveltejs/kit and no .svelte in
|
|
3
3
|
// the graph. The full ./delivery barrel re-exports this and adds the route loaders.
|
|
4
4
|
export { createContentIndex, fromGlob } from './content-index.js';
|
|
5
|
-
export {
|
|
5
|
+
export { createSiteResolver, buildLinkResolver } from './site-resolver.js';
|
|
6
6
|
export { createSiteIndexes } from './site-indexes.js';
|
|
7
7
|
export { siteDescriptors } from './site-descriptors.js';
|
|
8
8
|
export { deriveExcerpt, wordCount } from './excerpt.js';
|
|
@@ -11,8 +11,7 @@ export { buildSitemap } from './sitemap.js';
|
|
|
11
11
|
export { buildRobots } from './robots.js';
|
|
12
12
|
export { buildSeoMeta } from './seo.js';
|
|
13
13
|
export { readSeoFields, resolveImageUrl } from './seo-fields.js';
|
|
14
|
-
export { paginate } from './paginate.js';
|
|
15
14
|
export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
|
|
16
15
|
export { jsonLdScript } from './json-ld.js';
|
|
17
16
|
export { permalink } from '../content/permalink.js';
|
|
18
|
-
export { buildSiteManifest
|
|
17
|
+
export { buildSiteManifest } from './manifest.js';
|
package/dist/delivery/feeds.js
CHANGED
|
@@ -2,13 +2,7 @@
|
|
|
2
2
|
// channel and a list of items, so they unit-test without a render or a network. The caller
|
|
3
3
|
// (a template +server.ts shim) assembles items from the content index and passes absolute
|
|
4
4
|
// URLs built from PUBLIC_ORIGIN.
|
|
5
|
-
|
|
6
|
-
return value
|
|
7
|
-
.replace(/&/g, '&')
|
|
8
|
-
.replace(/</g, '<')
|
|
9
|
-
.replace(/>/g, '>')
|
|
10
|
-
.replace(/"/g, '"');
|
|
11
|
-
}
|
|
5
|
+
import { escapeXml } from './xml.js';
|
|
12
6
|
/** Make a string safe inside a CDATA section by splitting any `]]>` across two sections. */
|
|
13
7
|
function cdataSafe(value) {
|
|
14
8
|
return value.replace(/]]>/g, ']]]]><![CDATA[>');
|
package/dist/delivery/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export * from './data.js';
|
|
2
|
-
export { createPublicRoutes } from '
|
|
3
|
-
export type { PublicRoutesDeps, ListData, TagData, TagIndexData, EntryData, } from '
|
|
2
|
+
export { createPublicRoutes } from './public-routes.js';
|
|
3
|
+
export type { PublicRoutesDeps, ListData, TagData, TagIndexData, EntryData, } from './public-routes.js';
|
package/dist/delivery/index.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
// lives at ./delivery/head. Importing this pulls @sveltejs/kit through the route loaders, so a
|
|
4
4
|
// plain-Node tool imports from ./delivery/data instead.
|
|
5
5
|
export * from './data.js';
|
|
6
|
-
export { createPublicRoutes } from '
|
|
6
|
+
export { createPublicRoutes } from './public-routes.js';
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import type { Manifest } from '../content/manifest.js';
|
|
2
|
-
import type { LinkResolve } from '../content/links.js';
|
|
3
|
-
import type { SiteIndex } from './site-index.js';
|
|
4
2
|
import type { SiteConfig } from '../nav/site-config.js';
|
|
5
3
|
import type { CairnAdapter } from '../content/types.js';
|
|
6
4
|
import type { SiteGlobs } from './site-indexes.js';
|
|
7
5
|
/** Build the whole-corpus manifest from a site's adapter, config, and per-concept globs. Drafts are
|
|
8
6
|
* included and flagged, so the admin picker and the guards see the full graph. */
|
|
9
7
|
export declare function buildSiteManifest<A extends CairnAdapter>(adapter: A, config: SiteConfig, globs: SiteGlobs<A>): Manifest;
|
|
10
|
-
/** A resolver backed by the site index, for the build. A miss throws, so a dangling cairn: token
|
|
11
|
-
* fails the prerender (the build backstop). The preview uses manifestLinkResolver, which marks. */
|
|
12
|
-
export declare function buildLinkResolver(site: SiteIndex): LinkResolve;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
// cairn-cms: the build-side manifest builder
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// the build (the backstop). The admin preview uses manifestLinkResolver instead.
|
|
1
|
+
// cairn-cms: the build-side manifest builder (content-graph design). buildSiteManifest mirrors
|
|
2
|
+
// createSiteIndexes: it maps the site descriptors over the per-concept globs and projects each
|
|
3
|
+
// file to a manifest row. The build-time cairn: link resolver lives beside the site resolver in
|
|
4
|
+
// site-resolver.ts; the admin preview uses manifestLinkResolver instead.
|
|
6
5
|
import { siteDescriptors } from './site-descriptors.js';
|
|
7
6
|
import { fromGlob } from './content-index.js';
|
|
8
7
|
import { parseMarkdown } from '../content/frontmatter.js';
|
|
@@ -15,7 +14,7 @@ export function buildSiteManifest(adapter, config, globs) {
|
|
|
15
14
|
for (const descriptor of siteDescriptors(adapter, config)) {
|
|
16
15
|
const record = globRecord[descriptor.id] ?? {};
|
|
17
16
|
for (const file of fromGlob(record)) {
|
|
18
|
-
// Validate the same way createContentIndex does, so the manifest and the site
|
|
17
|
+
// Validate the same way createContentIndex does, so the manifest and the site resolver agree on
|
|
19
18
|
// which entries exist. A validation failure is excluded from both; otherwise the preview would
|
|
20
19
|
// resolve a link the build then rejects as a missing target.
|
|
21
20
|
const { frontmatter, body } = parseMarkdown(file.raw);
|
|
@@ -26,13 +25,3 @@ export function buildSiteManifest(adapter, config, globs) {
|
|
|
26
25
|
}
|
|
27
26
|
return manifest;
|
|
28
27
|
}
|
|
29
|
-
/** A resolver backed by the site index, for the build. A miss throws, so a dangling cairn: token
|
|
30
|
-
* fails the prerender (the build backstop). The preview uses manifestLinkResolver, which marks. */
|
|
31
|
-
export function buildLinkResolver(site) {
|
|
32
|
-
return (ref) => {
|
|
33
|
-
const url = site.concept(ref.concept)?.byId(ref.id)?.permalink;
|
|
34
|
-
if (!url)
|
|
35
|
-
throw new Error(`cairn link target not found: cairn:${ref.concept}/${ref.id}`);
|
|
36
|
-
return url;
|
|
37
|
-
};
|
|
38
|
-
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { ContentSummary, ContentEntry } from '
|
|
2
|
-
import type {
|
|
3
|
-
import type { SeoMeta } from '
|
|
1
|
+
import type { ContentSummary, ContentEntry } from './content-index.js';
|
|
2
|
+
import type { SiteResolver } from './site-resolver.js';
|
|
3
|
+
import type { SeoMeta } from './seo.js';
|
|
4
4
|
import type { LinkResolve } from '../content/links.js';
|
|
5
5
|
/** Injected dependencies for the public loaders. */
|
|
6
6
|
export interface PublicRoutesDeps {
|
|
7
|
-
site:
|
|
7
|
+
site: SiteResolver;
|
|
8
8
|
render: (md: string, opts?: {
|
|
9
9
|
stagger?: boolean;
|
|
10
10
|
resolve?: LinkResolve;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
// cairn-cms: public route loaders (dated-slug design). The factory closes over the site
|
|
2
|
-
//
|
|
1
|
+
// cairn-cms: public route loaders (dated-slug design). The factory closes over the site
|
|
2
|
+
// resolver, the runtime render, and the origin. entryLoad and entries are site-wide: one catch-all
|
|
3
3
|
// `[...path]` route resolves any concept by request path through `byPermalink`. The archive, tag,
|
|
4
|
-
// and tag-index loaders stay concept-scoped, keyed by concept id. The
|
|
5
|
-
// from globs, so it stays in the prerender graph and out of the runtime Worker.
|
|
4
|
+
// and tag-index loaders stay concept-scoped, keyed by concept id. The resolver is built in site
|
|
5
|
+
// code from globs, so it stays in the prerender graph and out of the runtime Worker.
|
|
6
6
|
import { error } from '@sveltejs/kit';
|
|
7
|
-
import { buildSeoMeta } from '
|
|
8
|
-
import { readSeoFields, resolveImageUrl } from '
|
|
9
|
-
import { buildLinkResolver } from '
|
|
7
|
+
import { buildSeoMeta } from './seo.js';
|
|
8
|
+
import { readSeoFields, resolveImageUrl } from './seo-fields.js';
|
|
9
|
+
import { buildLinkResolver } from './site-resolver.js';
|
|
10
10
|
/** Build the public loaders for a site's unified index. */
|
|
11
11
|
export function createPublicRoutes(deps) {
|
|
12
12
|
const { site, render, origin, siteName, description, feeds, defaultImage } = deps;
|
|
@@ -2,7 +2,7 @@ import type { CairnAdapter, ConceptConfig } from '../content/types.js';
|
|
|
2
2
|
import type { Infer } from '../content/schema.js';
|
|
3
3
|
import type { SiteConfig } from '../nav/site-config.js';
|
|
4
4
|
import type { ContentIndex } from './content-index.js';
|
|
5
|
-
import type {
|
|
5
|
+
import type { SiteResolver } from './site-resolver.js';
|
|
6
6
|
/** A per-concept raw glob record (`{ path: raw }`) keyed by concept id, from `import.meta.glob`. */
|
|
7
7
|
export type SiteGlobs<A extends CairnAdapter> = {
|
|
8
8
|
[K in keyof A['content']]?: Record<string, string>;
|
|
@@ -12,13 +12,13 @@ export type SiteGlobs<A extends CairnAdapter> = {
|
|
|
12
12
|
export type SiteIndexes<A extends CairnAdapter> = {
|
|
13
13
|
[K in keyof A['content']]: ContentIndex<NonNullable<A['content'][K]> extends ConceptConfig<infer S> ? Infer<S> : Record<string, unknown>>;
|
|
14
14
|
} & {
|
|
15
|
-
readonly site:
|
|
15
|
+
readonly site: SiteResolver;
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
18
18
|
* Build typed per-concept indexes and a site resolver from one adapter. Pass the per-concept raw
|
|
19
19
|
* globs as `{ posts: import.meta.glob('...?raw', { eager: true }), ... }`; Vite needs the literal
|
|
20
20
|
* glob at the call site, so the engine cannot glob on the site's behalf. `validate: false` opts out
|
|
21
|
-
* of the build gate, exactly as on `
|
|
21
|
+
* of the build gate, exactly as on `createSiteResolver`.
|
|
22
22
|
*/
|
|
23
23
|
export declare function createSiteIndexes<const A extends CairnAdapter>(adapter: A, config: SiteConfig, globs: SiteGlobs<A>, opts?: {
|
|
24
24
|
validate?: boolean;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { siteDescriptors } from './site-descriptors.js';
|
|
2
2
|
import { createContentIndex, fromGlob } from './content-index.js';
|
|
3
|
-
import {
|
|
3
|
+
import { createSiteResolver } from './site-resolver.js';
|
|
4
4
|
/**
|
|
5
5
|
* Build typed per-concept indexes and a site resolver from one adapter. Pass the per-concept raw
|
|
6
6
|
* globs as `{ posts: import.meta.glob('...?raw', { eager: true }), ... }`; Vite needs the literal
|
|
7
7
|
* glob at the call site, so the engine cannot glob on the site's behalf. `validate: false` opts out
|
|
8
|
-
* of the build gate, exactly as on `
|
|
8
|
+
* of the build gate, exactly as on `createSiteResolver`.
|
|
9
9
|
*/
|
|
10
10
|
export function createSiteIndexes(adapter, config, globs, opts = {}) {
|
|
11
11
|
const descriptors = siteDescriptors(adapter, config);
|
|
@@ -25,6 +25,6 @@ export function createSiteIndexes(adapter, config, globs, opts = {}) {
|
|
|
25
25
|
byConcept[descriptor.id] = index;
|
|
26
26
|
conceptIndexes.push({ descriptor, index });
|
|
27
27
|
}
|
|
28
|
-
const site =
|
|
28
|
+
const site = createSiteResolver(conceptIndexes, opts);
|
|
29
29
|
return { ...byConcept, site };
|
|
30
30
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { ConceptDescriptor } from '../content/types.js';
|
|
2
2
|
import type { ContentEntry, ContentIndex, ContentSummary } from './content-index.js';
|
|
3
|
+
import type { LinkResolve } from '../content/links.js';
|
|
3
4
|
/** One concept's descriptor paired with its built index. */
|
|
4
5
|
export interface ConceptIndex {
|
|
5
6
|
descriptor: ConceptDescriptor;
|
|
6
7
|
index: ContentIndex;
|
|
7
8
|
}
|
|
8
9
|
/** The cross-concept query surface a catch-all route and the sitemap read. */
|
|
9
|
-
export interface
|
|
10
|
+
export interface SiteResolver {
|
|
10
11
|
/** Resolve a request path (with or without a trailing slash) to its entry, or undefined. */
|
|
11
12
|
byPermalink(path: string): ContentEntry | undefined;
|
|
12
13
|
/** Newer/older neighbors within the entry's own concept, for prev/next links. */
|
|
@@ -28,6 +29,9 @@ export interface SiteIndex {
|
|
|
28
29
|
* unless `validate` is `false`, on any non-draft entry whose frontmatter fails its concept's
|
|
29
30
|
* validator, so malformed content fails the build instead of shipping.
|
|
30
31
|
*/
|
|
31
|
-
export declare function
|
|
32
|
+
export declare function createSiteResolver(concepts: ConceptIndex[], opts?: {
|
|
32
33
|
validate?: boolean;
|
|
33
|
-
}):
|
|
34
|
+
}): SiteResolver;
|
|
35
|
+
/** A resolver backed by the site resolver, for the build. A miss throws, so a dangling cairn: token
|
|
36
|
+
* fails the prerender (the build backstop). The preview uses manifestLinkResolver, which marks. */
|
|
37
|
+
export declare function buildLinkResolver(site: SiteResolver): LinkResolve;
|
|
@@ -21,11 +21,11 @@ function siteProblems(concepts) {
|
|
|
21
21
|
* unless `validate` is `false`, on any non-draft entry whose frontmatter fails its concept's
|
|
22
22
|
* validator, so malformed content fails the build instead of shipping.
|
|
23
23
|
*/
|
|
24
|
-
export function
|
|
24
|
+
export function createSiteResolver(concepts, opts = {}) {
|
|
25
25
|
if (opts.validate !== false) {
|
|
26
26
|
const problems = siteProblems(concepts);
|
|
27
27
|
if (problems.length > 0) {
|
|
28
|
-
throw new Error(`site
|
|
28
|
+
throw new Error(`site resolver: ${problems.length} invalid frontmatter field(s):\n ${problems.join('\n ')}`);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
const byPath = new Map();
|
|
@@ -35,7 +35,7 @@ export function createSiteIndex(concepts, opts = {}) {
|
|
|
35
35
|
for (const summary of index.all()) {
|
|
36
36
|
const existing = byPath.get(summary.permalink);
|
|
37
37
|
if (existing) {
|
|
38
|
-
throw new Error(`site
|
|
38
|
+
throw new Error(`site resolver: "${existing.id}" and "${summary.id}" both resolve to "${summary.permalink}"`);
|
|
39
39
|
}
|
|
40
40
|
byPath.set(summary.permalink, { index, id: summary.id });
|
|
41
41
|
}
|
|
@@ -60,3 +60,13 @@ export function createSiteIndex(concepts, opts = {}) {
|
|
|
60
60
|
},
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
/** A resolver backed by the site resolver, for the build. A miss throws, so a dangling cairn: token
|
|
64
|
+
* fails the prerender (the build backstop). The preview uses manifestLinkResolver, which marks. */
|
|
65
|
+
export function buildLinkResolver(site) {
|
|
66
|
+
return (ref) => {
|
|
67
|
+
const url = site.concept(ref.concept)?.byId(ref.id)?.permalink;
|
|
68
|
+
if (!url)
|
|
69
|
+
throw new Error(`cairn link target not found: cairn:${ref.concept}/${ref.id}`);
|
|
70
|
+
return url;
|
|
71
|
+
};
|
|
72
|
+
}
|
package/dist/delivery/sitemap.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// cairn-cms: sitemap builder (public-delivery design). Pure over a URL list; the caller
|
|
2
2
|
// derives the list from the content index and the routable concepts.
|
|
3
|
-
|
|
4
|
-
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
5
|
-
}
|
|
3
|
+
import { escapeXml } from './xml.js';
|
|
6
4
|
/** Build a sitemap XML document from a list of URLs. */
|
|
7
5
|
export function buildSitemap(urls) {
|
|
8
6
|
const entries = urls
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// cairn-cms: the one XML text escape the feed and sitemap builders share. The strongest of the
|
|
2
|
+
// two copies it replaced (the old sitemap copy skipped quotes), so both documents stay safe in
|
|
3
|
+
// element text and double-quoted attributes.
|
|
4
|
+
/** Escape the XML-significant characters for element text and double-quoted attribute values. */
|
|
5
|
+
export function escapeXml(value) {
|
|
6
|
+
return value
|
|
7
|
+
.replaceAll('&', '&')
|
|
8
|
+
.replaceAll('<', '<')
|
|
9
|
+
.replaceAll('>', '>')
|
|
10
|
+
.replaceAll('"', '"');
|
|
11
|
+
}
|
|
@@ -10,11 +10,18 @@ export interface CairnCondition {
|
|
|
10
10
|
why: string;
|
|
11
11
|
/** The fix, often a command. */
|
|
12
12
|
remediation: string;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The condition's section in the readiness checklist, written as
|
|
15
|
+
* 'cloudflare-readiness.md#<heading-slug>' so a doc can link it relative to docs/guides/.
|
|
16
|
+
* The check:readiness gate parses the part after '#' and asserts the heading exists; two
|
|
17
|
+
* conditions may share a section. Every entry carries one unless the gate's allowlist
|
|
18
|
+
* excuses it.
|
|
19
|
+
*/
|
|
14
20
|
docsAnchor?: string;
|
|
15
21
|
/** The log vocabulary event this condition correlates with, if any. */
|
|
16
22
|
logEvent?: CairnLogEvent;
|
|
17
23
|
}
|
|
24
|
+
export declare const REGISTRY: Record<string, CairnCondition>;
|
|
18
25
|
/** Resolve a condition by id. Throws on an unknown id, since ids are compile-time constants. */
|
|
19
26
|
export declare function condition(id: string): CairnCondition;
|
|
20
27
|
/** Every registered condition, for the checklist generator and coverage tests. */
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
// Exported for the freeze test only; resolve entries through condition() everywhere else.
|
|
2
|
+
export const REGISTRY = {
|
|
2
3
|
'edge.https-not-forced': {
|
|
3
4
|
id: 'edge.https-not-forced',
|
|
4
5
|
severity: 'blocker',
|
|
5
6
|
title: 'Always Use HTTPS is off',
|
|
6
7
|
why: 'The JS-free admin sign-in posts a form, and the framework CSRF guard rejects a form POST whose origin scheme does not match, so an admin reached over http hits an opaque 403.',
|
|
7
8
|
remediation: 'Turn on Always Use HTTPS for the zone under SSL/TLS, Edge Certificates, and keep HSTS on.',
|
|
9
|
+
docsAnchor: 'cloudflare-readiness.md#force-https-at-the-edge',
|
|
8
10
|
logEvent: 'guard.rejected',
|
|
9
11
|
},
|
|
10
12
|
'auth.csrf-token-invalid': {
|
|
@@ -13,6 +15,7 @@ const REGISTRY = {
|
|
|
13
15
|
title: 'Admin CSRF token check failed',
|
|
14
16
|
why: 'An admin form POST carried no valid __Host-cairn_csrf double-submit token, usually a stale tab or blocked cookies.',
|
|
15
17
|
remediation: 'Open the sign-in page fresh, allow cookies for the site, and request a new link.',
|
|
18
|
+
docsAnchor: 'cloudflare-readiness.md#admin-csrf-token-rejected',
|
|
16
19
|
logEvent: 'guard.rejected',
|
|
17
20
|
},
|
|
18
21
|
'auth.csrf-origin-mismatch': {
|
|
@@ -21,6 +24,7 @@ const REGISTRY = {
|
|
|
21
24
|
title: 'Non-admin form Origin rejected',
|
|
22
25
|
why: "A non-admin unsafe form POST carried an Origin that did not match the site, so cairn's restored framework Origin check rejected it.",
|
|
23
26
|
remediation: 'Post the form from the same origin, or check a proxy that strips or rewrites the Origin header.',
|
|
27
|
+
docsAnchor: 'cloudflare-readiness.md#non-admin-origin-rejected',
|
|
24
28
|
logEvent: 'guard.rejected',
|
|
25
29
|
},
|
|
26
30
|
'email.sender-not-onboarded': {
|
|
@@ -29,6 +33,7 @@ const REGISTRY = {
|
|
|
29
33
|
title: 'Email sending domain is not onboarded',
|
|
30
34
|
why: 'The from-address domain has no enabled Cloudflare sending subdomain, so env.EMAIL.send has no aligned sender and the magic-link send throws E_SENDER_NOT_VERIFIED. No editor can sign in.',
|
|
31
35
|
remediation: 'Onboard the sending domain with `wrangler email sending enable <domain>`, then re-deploy. The domain must match branding.from.',
|
|
36
|
+
docsAnchor: 'cloudflare-readiness.md#onboard-the-sending-domain',
|
|
32
37
|
logEvent: 'auth.link.send_failed',
|
|
33
38
|
},
|
|
34
39
|
'email.send-failed': {
|
|
@@ -37,9 +42,71 @@ const REGISTRY = {
|
|
|
37
42
|
title: 'Magic-link email send failed',
|
|
38
43
|
why: 'The magic-link send threw for a reason other than a missing sender onboarding (a delivery error, a binding misconfiguration, or a custom sender failure), so the editor never received a link.',
|
|
39
44
|
remediation: 'Read the auth.link.send_failed log record (the code and error fields) in Workers Logs, and check the EMAIL binding and the sender configuration.',
|
|
45
|
+
docsAnchor: 'cloudflare-readiness.md#onboard-the-sending-domain',
|
|
40
46
|
logEvent: 'auth.link.send_failed',
|
|
41
47
|
},
|
|
48
|
+
'config.bindings-missing': {
|
|
49
|
+
id: 'config.bindings-missing',
|
|
50
|
+
severity: 'blocker',
|
|
51
|
+
title: 'Wrangler bindings are missing',
|
|
52
|
+
why: 'The wrangler config declares no send_email binding named EMAIL or no D1 binding named AUTH_DB, so the magic-link send or the session store has nothing to call and no editor can sign in.',
|
|
53
|
+
remediation: 'Declare the send_email binding as EMAIL and the d1_databases binding as AUTH_DB in wrangler.jsonc (or wrangler.toml), then re-deploy.',
|
|
54
|
+
docsAnchor: 'cloudflare-readiness.md#deploy-the-worker-with-its-bindings',
|
|
55
|
+
},
|
|
56
|
+
'config.observability-off': {
|
|
57
|
+
id: 'config.observability-off',
|
|
58
|
+
severity: 'warning',
|
|
59
|
+
title: 'Workers Logs has no sink',
|
|
60
|
+
why: 'observability.enabled is not true in the wrangler config, so the structured log records go nowhere and a runtime failure leaves nothing to read.',
|
|
61
|
+
remediation: 'Set observability.enabled to true in wrangler.jsonc, then re-deploy.',
|
|
62
|
+
docsAnchor: 'cloudflare-readiness.md#turn-on-observability',
|
|
63
|
+
},
|
|
64
|
+
'config.csrf-disable-missing': {
|
|
65
|
+
id: 'config.csrf-disable-missing',
|
|
66
|
+
severity: 'warning',
|
|
67
|
+
title: 'Framework CSRF check is not handed off',
|
|
68
|
+
why: "The CSRF authority is not handed to cairn cleanly. Either svelte.config.js does not carry csrf: { checkOrigin: false }, so SvelteKit's own Origin check runs ahead of cairn's guard and rejects an admin form POST that arrives without an Origin header, or the disable is present with no cairn guard wired in src/hooks.server.ts, which leaves the site with no CSRF protection at all.",
|
|
69
|
+
remediation: "Set csrf: { checkOrigin: false } in svelte.config.js and wire createAuthGuard into src/hooks.server.ts; cairn's guard owns the Origin and double-submit token checks.",
|
|
70
|
+
docsAnchor: 'cloudflare-readiness.md#hand-cairn-the-csrf-authority',
|
|
71
|
+
},
|
|
72
|
+
'config.site-config-invalid': {
|
|
73
|
+
id: 'config.site-config-invalid',
|
|
74
|
+
severity: 'blocker',
|
|
75
|
+
title: 'Site config does not validate',
|
|
76
|
+
why: 'site.config.yaml fails to parse or fails the URL-policy validation, so the build and the admin cannot resolve the content concepts.',
|
|
77
|
+
remediation: 'Correct site.config.yaml; the parse or validation error names the failing field or URL-policy rule.',
|
|
78
|
+
docsAnchor: 'cloudflare-readiness.md#validate-the-site-config',
|
|
79
|
+
},
|
|
80
|
+
'edge.hsts-off': {
|
|
81
|
+
id: 'edge.hsts-off',
|
|
82
|
+
severity: 'warning',
|
|
83
|
+
title: 'HSTS is off',
|
|
84
|
+
why: 'The zone sends no Strict-Transport-Security header with a meaningful max-age, so browsers do not pin https and a later http visit can still hit the admin guard rejection.',
|
|
85
|
+
remediation: 'Turn on HSTS for the zone under SSL/TLS, Edge Certificates, with a max-age of at least six months.',
|
|
86
|
+
docsAnchor: 'cloudflare-readiness.md#turn-on-hsts',
|
|
87
|
+
},
|
|
88
|
+
'auth.store-unreachable': {
|
|
89
|
+
id: 'auth.store-unreachable',
|
|
90
|
+
severity: 'blocker',
|
|
91
|
+
title: 'Auth store is unreachable',
|
|
92
|
+
why: 'The AUTH_DB D1 database is missing, lacks the auth schema, or holds no owner row, so no magic-link token can be minted and nobody can sign in.',
|
|
93
|
+
remediation: 'Create the database, apply the auth schema with `wrangler d1 execute <db> --remote --file ./migrations/0000_auth.sql`, seed the owner row, and check the AUTH_DB binding id in wrangler.jsonc.',
|
|
94
|
+
docsAnchor: 'cloudflare-readiness.md#provision-the-auth-store',
|
|
95
|
+
},
|
|
96
|
+
'github.app-unreachable': {
|
|
97
|
+
id: 'github.app-unreachable',
|
|
98
|
+
severity: 'blocker',
|
|
99
|
+
title: 'GitHub App is unreachable',
|
|
100
|
+
why: 'The App key fails to parse, the App fails to authenticate, the installation token fails to mint, or the repository refuses a read, so saves and publishes cannot commit.',
|
|
101
|
+
remediation: 'Check GITHUB_APP_ID, GITHUB_APP_INSTALLATION_ID, and GITHUB_APP_PRIVATE_KEY_B64 against the App settings, and confirm the App is installed on the repository.',
|
|
102
|
+
docsAnchor: 'cloudflare-readiness.md#install-the-github-app',
|
|
103
|
+
logEvent: 'github.unreachable',
|
|
104
|
+
},
|
|
42
105
|
};
|
|
106
|
+
// The registry is shared identity, never working state; freeze every entry and the map itself.
|
|
107
|
+
for (const entry of Object.values(REGISTRY))
|
|
108
|
+
Object.freeze(entry);
|
|
109
|
+
Object.freeze(REGISTRY);
|
|
43
110
|
/** Resolve a condition by id. Throws on an unknown id, since ids are compile-time constants. */
|
|
44
111
|
export function condition(id) {
|
|
45
112
|
const found = REGISTRY[id];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cairn-doctor: the environment preflight. A thin shell over index.ts (where the unit tests
|
|
3
|
+
// reach the logic): parse the flags, assemble the context with the real fetch and filesystem,
|
|
4
|
+
// run the default registry plus the opt-in live send, print the report. Bad flags go to
|
|
5
|
+
// stderr with exit 2; a failed check exits 1; a clean or all-skip run exits 0. The codes go
|
|
6
|
+
// through process.exitCode, never process.exit, so a piped stdout flushes the whole report
|
|
7
|
+
// before the process ends.
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
import { liveSendCheck } from './check-send.js';
|
|
11
|
+
import { contextFromEnv, defaultChecks, formatReport, parseArgs, runDoctor } from './index.js';
|
|
12
|
+
async function main() {
|
|
13
|
+
let args;
|
|
14
|
+
try {
|
|
15
|
+
args = parseArgs(process.argv.slice(2));
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
19
|
+
process.exitCode = 2;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const cwd = process.cwd();
|
|
23
|
+
const ctx = {
|
|
24
|
+
...contextFromEnv(process.env, args, cwd),
|
|
25
|
+
fetch: globalThis.fetch,
|
|
26
|
+
readFile: async (relPath) => {
|
|
27
|
+
try {
|
|
28
|
+
return await readFile(resolve(cwd, relPath), 'utf8');
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
if (err.code === 'ENOENT')
|
|
32
|
+
return null;
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
const checks = defaultChecks();
|
|
38
|
+
if (args.sendTest)
|
|
39
|
+
checks.push(liveSendCheck(args.sendTest));
|
|
40
|
+
const { results, failed } = await runDoctor(checks, ctx);
|
|
41
|
+
console.log(formatReport(results));
|
|
42
|
+
process.exitCode = failed > 0 ? 1 : 0;
|
|
43
|
+
}
|
|
44
|
+
await main();
|