@glw907/cairn-cms 0.50.0 → 0.52.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/components/EditPage.svelte +125 -16
  3. package/dist/components/EditPage.svelte.d.ts +4 -1
  4. package/dist/components/EditorToolbar.svelte +135 -10
  5. package/dist/components/EditorToolbar.svelte.d.ts +19 -2
  6. package/dist/components/MarkdownEditor.svelte +112 -6
  7. package/dist/components/MarkdownEditor.svelte.d.ts +4 -0
  8. package/dist/components/cairn-admin.css +69 -9
  9. package/dist/components/editor-highlight.d.ts +2 -0
  10. package/dist/components/editor-highlight.js +79 -15
  11. package/dist/components/editor-modes.d.ts +26 -0
  12. package/dist/components/editor-modes.js +92 -0
  13. package/dist/components/fonts/iAWriterMono-OFL.txt +100 -0
  14. package/dist/components/fonts/ia-writer-mono-latin-400-italic.woff2 +0 -0
  15. package/dist/components/fonts/ia-writer-mono-latin-400-normal.woff2 +0 -0
  16. package/dist/components/fonts/ia-writer-mono-latin-700-italic.woff2 +0 -0
  17. package/dist/components/fonts/ia-writer-mono-latin-700-normal.woff2 +0 -0
  18. package/dist/components/markdown-directives.d.ts +51 -0
  19. package/dist/components/markdown-directives.js +130 -1
  20. package/dist/components/preview-doc.d.ts +27 -0
  21. package/dist/components/preview-doc.js +64 -0
  22. package/dist/content/compose.js +1 -0
  23. package/dist/content/types.d.ts +33 -0
  24. package/dist/diagnostics/conditions.js +24 -0
  25. package/dist/doctor/bin.js +30 -12
  26. package/dist/doctor/check-floors.d.ts +15 -0
  27. package/dist/doctor/check-floors.js +107 -0
  28. package/dist/doctor/check-probe.d.ts +3 -0
  29. package/dist/doctor/check-probe.js +123 -0
  30. package/dist/doctor/checks-github.js +1 -1
  31. package/dist/doctor/checks-local.d.ts +1 -0
  32. package/dist/doctor/checks-local.js +28 -2
  33. package/dist/doctor/cloudflare-api.js +2 -2
  34. package/dist/doctor/index.d.ts +28 -3
  35. package/dist/doctor/index.js +47 -6
  36. package/dist/doctor/types.d.ts +2 -0
  37. package/dist/doctor/wrangler-config.d.ts +4 -0
  38. package/dist/doctor/wrangler-config.js +11 -0
  39. package/dist/env.d.ts +2 -1
  40. package/dist/env.js +9 -4
  41. package/dist/index.d.ts +1 -1
  42. package/dist/sveltekit/content-routes.d.ts +5 -1
  43. package/dist/sveltekit/content-routes.js +25 -17
  44. package/dist/sveltekit/guard.d.ts +8 -2
  45. package/dist/sveltekit/guard.js +3 -1
  46. package/dist/sveltekit/nav-routes.js +3 -9
  47. package/dist/vite/index.d.ts +16 -0
  48. package/dist/vite/index.js +57 -13
  49. package/package.json +2 -2
  50. package/src/lib/components/EditPage.svelte +125 -16
  51. package/src/lib/components/EditorToolbar.svelte +135 -10
  52. package/src/lib/components/MarkdownEditor.svelte +112 -6
  53. package/src/lib/components/cairn-admin.css +95 -5
  54. package/src/lib/components/editor-highlight.ts +91 -14
  55. package/src/lib/components/editor-modes.ts +106 -0
  56. package/src/lib/components/fonts/iAWriterMono-OFL.txt +100 -0
  57. package/src/lib/components/fonts/ia-writer-mono-latin-400-italic.woff2 +0 -0
  58. package/src/lib/components/fonts/ia-writer-mono-latin-400-normal.woff2 +0 -0
  59. package/src/lib/components/fonts/ia-writer-mono-latin-700-italic.woff2 +0 -0
  60. package/src/lib/components/fonts/ia-writer-mono-latin-700-normal.woff2 +0 -0
  61. package/src/lib/components/markdown-directives.ts +151 -1
  62. package/src/lib/components/preview-doc.ts +82 -0
  63. package/src/lib/content/compose.ts +1 -0
  64. package/src/lib/content/types.ts +32 -0
  65. package/src/lib/diagnostics/conditions.ts +24 -0
  66. package/src/lib/doctor/bin.ts +35 -10
  67. package/src/lib/doctor/check-floors.ts +124 -0
  68. package/src/lib/doctor/check-probe.ts +138 -0
  69. package/src/lib/doctor/checks-github.ts +3 -1
  70. package/src/lib/doctor/checks-local.ts +28 -2
  71. package/src/lib/doctor/cloudflare-api.ts +4 -2
  72. package/src/lib/doctor/index.ts +67 -6
  73. package/src/lib/doctor/types.ts +2 -0
  74. package/src/lib/doctor/wrangler-config.ts +11 -0
  75. package/src/lib/env.ts +9 -4
  76. package/src/lib/index.ts +2 -0
  77. package/src/lib/sveltekit/content-routes.ts +29 -17
  78. package/src/lib/sveltekit/guard.ts +4 -2
  79. package/src/lib/sveltekit/nav-routes.ts +3 -10
  80. package/src/lib/vite/index.ts +71 -17
@@ -0,0 +1,100 @@
1
+ # iA Writer Typeface
2
+
3
+ Copyright © 2018 Information Architects Inc. with Reserved Font Name "iA Writer"
4
+
5
+ # Based on IBM Plex Typeface
6
+
7
+ Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
8
+
9
+ # License
10
+
11
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
12
+ This license is copied below, and is also available with a FAQ at:
13
+ http://scripts.sil.org/OFL
14
+
15
+ -----------------------------------------------------------
16
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
17
+ -----------------------------------------------------------
18
+
19
+ PREAMBLE
20
+ The goals of the Open Font License (OFL) are to stimulate worldwide
21
+ development of collaborative font projects, to support the font creation
22
+ efforts of academic and linguistic communities, and to provide a free and
23
+ open framework in which fonts may be shared and improved in partnership
24
+ with others.
25
+
26
+ The OFL allows the licensed fonts to be used, studied, modified and
27
+ redistributed freely as long as they are not sold by themselves. The
28
+ fonts, including any derivative works, can be bundled, embedded,
29
+ redistributed and/or sold with any software provided that any reserved
30
+ names are not used by derivative works. The fonts and derivatives,
31
+ however, cannot be released under any other type of license. The
32
+ requirement for fonts to remain under this license does not apply
33
+ to any document created using the fonts or their derivatives.
34
+
35
+ DEFINITIONS
36
+ "Font Software" refers to the set of files released by the Copyright
37
+ Holder(s) under this license and clearly marked as such. This may
38
+ include source files, build scripts and documentation.
39
+
40
+ "Reserved Font Name" refers to any names specified as such after the
41
+ copyright statement(s).
42
+
43
+ "Original Version" refers to the collection of Font Software components as
44
+ distributed by the Copyright Holder(s).
45
+
46
+ "Modified Version" refers to any derivative made by adding to, deleting,
47
+ or substituting -- in part or in whole -- any of the components of the
48
+ Original Version, by changing formats or by porting the Font Software to a
49
+ new environment.
50
+
51
+ "Author" refers to any designer, engineer, programmer, technical
52
+ writer or other person who contributed to the Font Software.
53
+
54
+ PERMISSION & CONDITIONS
55
+ Permission is hereby granted, free of charge, to any person obtaining
56
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
57
+ redistribute, and sell modified and unmodified copies of the Font
58
+ Software, subject to the following conditions:
59
+
60
+ 1) Neither the Font Software nor any of its individual components,
61
+ in Original or Modified Versions, may be sold by itself.
62
+
63
+ 2) Original or Modified Versions of the Font Software may be bundled,
64
+ redistributed and/or sold with any software, provided that each copy
65
+ contains the above copyright notice and this license. These can be
66
+ included either as stand-alone text files, human-readable headers or
67
+ in the appropriate machine-readable metadata fields within text or
68
+ binary files as long as those fields can be easily viewed by the user.
69
+
70
+ 3) No Modified Version of the Font Software may use the Reserved Font
71
+ Name(s) unless explicit written permission is granted by the corresponding
72
+ Copyright Holder. This restriction only applies to the primary font name as
73
+ presented to the users.
74
+
75
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
76
+ Software shall not be used to promote, endorse or advertise any
77
+ Modified Version, except to acknowledge the contribution(s) of the
78
+ Copyright Holder(s) and the Author(s) or with their explicit written
79
+ permission.
80
+
81
+ 5) The Font Software, modified or unmodified, in part or in whole,
82
+ must be distributed entirely under this license, and must not be
83
+ distributed under any other license. The requirement for fonts to
84
+ remain under this license does not apply to any document created
85
+ using the Font Software.
86
+
87
+ TERMINATION
88
+ This license becomes null and void if any of the above conditions are
89
+ not met.
90
+
91
+ DISCLAIMER
92
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
93
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
94
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
95
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
96
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
97
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
98
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
99
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
100
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -1,5 +1,56 @@
1
1
  /** Classify a whole line as a container fence, a leaf directive, or neither. */
2
2
  export declare function directiveLineKind(line: string): 'fence' | 'leaf' | null;
3
+ /** One pass over the document: each line's container depth alongside its fence role. */
4
+ export interface FenceScan {
5
+ /** The 1-based container depth per line, or null outside any container. */
6
+ depths: (number | null)[];
7
+ /** Whether a line opened or closed a container, or null for everything else. A fence-shaped
8
+ * line the code-block tracking disowned is null too, so the role array is the one source of
9
+ * truth for pairing and no caller re-parses a line the scan already judged. */
10
+ roles: ('opener' | 'closer' | null)[];
11
+ }
12
+ /**
13
+ * Scan the document's container structure in one pass. A named fence opens a container; a bare
14
+ * fence closes the most recent one (colon counts are not trusted for pairing, since authors
15
+ * vary them). An opener and its closer share the opener's depth, and a line between them
16
+ * carries the depth of its innermost container. Lines inside a fenced code block are plain
17
+ * content, so a documented ::: example cannot open a phantom container running to end of
18
+ * document. Author errors are tolerated: an unmatched closer reads as depth 1 and the count
19
+ * never goes below zero.
20
+ */
21
+ export declare function fenceScan(lines: string[]): FenceScan;
22
+ /** The depth half of {@link fenceScan}, for callers that need no roles. */
23
+ export declare function fenceDepths(lines: string[]): (number | null)[];
24
+ /** The inclusive line span of one directive container. */
25
+ export interface ContainerRange {
26
+ fromLine: number;
27
+ toLine: number;
28
+ depth: number;
29
+ }
30
+ /**
31
+ * The innermost container around a caret line, as an inclusive line range, or null outside any
32
+ * container. Works from the cached scan without re-parsing: the caret line's own depth names
33
+ * the container (fence rows carry the depth they delimit, so a caret on a fence belongs to
34
+ * that fence's container), and within a container the only same-depth real fences are its
35
+ * opener and closer (nested containers sit deeper, siblings sit outside), so the nearest
36
+ * opener above and the nearest closer below bound the range. The scan's roles already disown a
37
+ * fence-shaped line inside a code block, so a documented example can never clip the range. An
38
+ * unclosed container runs to the document end.
39
+ */
40
+ export declare function caretContainerRange(scan: FenceScan, caretLine: number): ContainerRange | null;
41
+ /** One span of a fence line, in line-local offsets: machinery (`mark`) or meaning (`label`). */
42
+ export interface FenceToken {
43
+ from: number;
44
+ to: number;
45
+ kind: 'mark' | 'label';
46
+ }
47
+ /**
48
+ * Split a fence line into machinery and meaning. The colon run, the label's brackets, and the
49
+ * whole {attrs} group are machinery; the directive name and the label text are meaning, the
50
+ * parts an editor reads. A bare closer is a single machinery span, and a non-fence line yields
51
+ * no spans at all.
52
+ */
53
+ export declare function fenceTokens(line: string): FenceToken[];
3
54
  /** Inline directive ranges (`:name[...]{...}`) within a line of text. */
4
55
  export declare function findInlineDirectives(text: string): {
5
56
  from: number;
@@ -1,9 +1,19 @@
1
1
  // Remark-directive detection for the editor's machinery highlighting (spec: directive syntax is
2
2
  // styled distinctly so an editor can tell component scaffolding from prose). Pure functions; the
3
3
  // CodeMirror decoration plugin wraps them.
4
- const FENCE = /^\s{0,3}:::+\s*[\w-]*\s*(\{[^}]*\})?\s*$/;
4
+ // A container fence: three or more colons, then an optional name, an optional [label], and
5
+ // optional {attrs}, in remark-directive order. The name is captured so the depth scan below can
6
+ // tell an opener (named) from a closer (bare colons), and the d flag records each group's span
7
+ // so fenceTokens can split the line without re-parsing. Matching is tolerant of stray
8
+ // whitespace, the same posture as the leaf form: a slightly off fence should still read as
9
+ // machinery.
10
+ const FENCE = /^\s{0,3}(:{3,})\s*([\w-]*)\s*(\[[^\]]*\])?\s*(\{[^}]*\})?\s*$/d;
5
11
  const LEAF = /^\s{0,3}::[\w-]+(\[[^\]]*\])?(\{[^}]*\})?\s*$/;
6
12
  const INLINE = /(?<![:\w]):[\w-]+\[[^\]]*\](\{[^}]*\})?/g;
13
+ // A fenced code block's delimiter: three or more backticks or tildes, indent-tolerant like the
14
+ // directive forms. The depth scan tracks these so a documented ::: example inside a code block
15
+ // never opens a real container.
16
+ const CODE_FENCE = /^\s{0,3}(`{3,}|~{3,})/;
7
17
  /** Classify a whole line as a container fence, a leaf directive, or neither. */
8
18
  export function directiveLineKind(line) {
9
19
  if (FENCE.test(line))
@@ -12,6 +22,125 @@ export function directiveLineKind(line) {
12
22
  return 'leaf';
13
23
  return null;
14
24
  }
25
+ /**
26
+ * Scan the document's container structure in one pass. A named fence opens a container; a bare
27
+ * fence closes the most recent one (colon counts are not trusted for pairing, since authors
28
+ * vary them). An opener and its closer share the opener's depth, and a line between them
29
+ * carries the depth of its innermost container. Lines inside a fenced code block are plain
30
+ * content, so a documented ::: example cannot open a phantom container running to end of
31
+ * document. Author errors are tolerated: an unmatched closer reads as depth 1 and the count
32
+ * never goes below zero.
33
+ */
34
+ export function fenceScan(lines) {
35
+ const depths = [];
36
+ const roles = [];
37
+ let open = 0;
38
+ // The marker character that opened the current code block, or null outside one. Only a line
39
+ // opening with the same character closes it, so tildes inside a backtick block stay literal.
40
+ let codeMarker = null;
41
+ for (const line of lines) {
42
+ const code = CODE_FENCE.exec(line);
43
+ if (code) {
44
+ if (codeMarker === null)
45
+ codeMarker = code[1][0];
46
+ else if (code[1][0] === codeMarker)
47
+ codeMarker = null;
48
+ depths.push(open > 0 ? open : null);
49
+ roles.push(null);
50
+ continue;
51
+ }
52
+ if (codeMarker !== null) {
53
+ depths.push(open > 0 ? open : null);
54
+ roles.push(null);
55
+ continue;
56
+ }
57
+ const fence = FENCE.exec(line);
58
+ if (!fence) {
59
+ depths.push(open > 0 ? open : null);
60
+ roles.push(null);
61
+ }
62
+ else if (fence[2]) {
63
+ open += 1;
64
+ depths.push(open);
65
+ roles.push('opener');
66
+ }
67
+ else {
68
+ depths.push(Math.max(open, 1));
69
+ roles.push('closer');
70
+ if (open > 0)
71
+ open -= 1;
72
+ }
73
+ }
74
+ return { depths, roles };
75
+ }
76
+ /** The depth half of {@link fenceScan}, for callers that need no roles. */
77
+ export function fenceDepths(lines) {
78
+ return fenceScan(lines).depths;
79
+ }
80
+ /**
81
+ * The innermost container around a caret line, as an inclusive line range, or null outside any
82
+ * container. Works from the cached scan without re-parsing: the caret line's own depth names
83
+ * the container (fence rows carry the depth they delimit, so a caret on a fence belongs to
84
+ * that fence's container), and within a container the only same-depth real fences are its
85
+ * opener and closer (nested containers sit deeper, siblings sit outside), so the nearest
86
+ * opener above and the nearest closer below bound the range. The scan's roles already disown a
87
+ * fence-shaped line inside a code block, so a documented example can never clip the range. An
88
+ * unclosed container runs to the document end.
89
+ */
90
+ export function caretContainerRange(scan, caretLine) {
91
+ const { depths, roles } = scan;
92
+ const depth = depths[caretLine] ?? null;
93
+ if (depth === null)
94
+ return null;
95
+ let fromLine = caretLine;
96
+ for (let i = caretLine; i >= 0; i--) {
97
+ if (depths[i] === depth && roles[i] === 'opener') {
98
+ fromLine = i;
99
+ break;
100
+ }
101
+ }
102
+ let toLine = depths.length - 1;
103
+ for (let i = caretLine; i < depths.length; i++) {
104
+ if (depths[i] === depth && roles[i] === 'closer') {
105
+ toLine = i;
106
+ break;
107
+ }
108
+ }
109
+ return { fromLine, toLine, depth };
110
+ }
111
+ /**
112
+ * Split a fence line into machinery and meaning. The colon run, the label's brackets, and the
113
+ * whole {attrs} group are machinery; the directive name and the label text are meaning, the
114
+ * parts an editor reads. A bare closer is a single machinery span, and a non-fence line yields
115
+ * no spans at all.
116
+ */
117
+ export function fenceTokens(line) {
118
+ const m = FENCE.exec(line);
119
+ if (!m?.indices)
120
+ return [];
121
+ // A group's span exists whenever the group matched: group 1 (the colons) always does on a
122
+ // fence, and the optional groups are read only behind their own m[n] guard.
123
+ const indices = m.indices;
124
+ const out = [];
125
+ const [colonFrom, colonTo] = indices[1];
126
+ out.push({ from: colonFrom, to: colonTo, kind: 'mark' });
127
+ if (m[2]) {
128
+ const [from, to] = indices[2];
129
+ out.push({ from, to, kind: 'label' });
130
+ }
131
+ if (m[3]) {
132
+ const [from, to] = indices[3];
133
+ out.push({ from, to: from + 1, kind: 'mark' });
134
+ if (to - from > 2)
135
+ out.push({ from: from + 1, to: to - 1, kind: 'label' });
136
+ out.push({ from: to - 1, to, kind: 'mark' });
137
+ }
138
+ if (m[4]) {
139
+ const [from, to] = indices[4];
140
+ out.push({ from, to, kind: 'mark' });
141
+ }
142
+ return out;
143
+ }
15
144
  /** Inline directive ranges (`:name[...]{...}`) within a line of text. */
16
145
  export function findInlineDirectives(text) {
17
146
  const out = [];
@@ -0,0 +1,27 @@
1
+ import type { ResolvedPreview } from '../content/types.js';
2
+ /** One width the preview frame can take. */
3
+ export interface PreviewDevice {
4
+ id: 'desktop' | 'tablet' | 'phone' | 'small';
5
+ /** The device menu label, also the frame caption's first half. */
6
+ label: string;
7
+ /** Frame width in CSS pixels; null fills the pane (Desktop). */
8
+ width: number | null;
9
+ }
10
+ /** A preview device's id, the value the page persists. */
11
+ export type PreviewDeviceId = PreviewDevice['id'];
12
+ /** The four widths the device menu offers, in menu order. Desktop leads as the default. */
13
+ export declare const previewDevices: PreviewDevice[];
14
+ /** The table row for a device id. The id type makes a miss impossible; the fallback satisfies find. */
15
+ export declare function previewDevice(id: PreviewDeviceId): PreviewDevice;
16
+ /** A device's user-facing text, shared by the toolbar's menu items and the frame caption: the
17
+ * label with its width when one is fixed, so the value reaches assistive tech at pick time. */
18
+ export declare function deviceLabel(d: PreviewDevice): string;
19
+ /**
20
+ * Build the preview iframe's srcdoc: a complete document linking the site's stylesheets around
21
+ * the rendered entry html. The html comes from the site's floored render pipeline, which already
22
+ * stripped scripts and event handlers, so it embeds unescaped; the frame's empty `sandbox` is
23
+ * belt and braces over that floor. The parameter is the flat `ResolvedPreview` shape `editLoad`
24
+ * ships, so the per-concept map can never reach the frame document by construction.
25
+ * `preview` null (a site without the adapter knob) yields a styleless but complete document.
26
+ */
27
+ export declare function buildPreviewDoc(html: string, preview: ResolvedPreview | null): string;
@@ -0,0 +1,64 @@
1
+ // cairn-cms: the edit page's preview-frame document. The admin's chrome isolation keeps the
2
+ // site's CSS out of the admin document, so EditPage renders the preview inside a sandboxed
3
+ // iframe whose document links the site's own stylesheets from the adapter's preview knob. This
4
+ // module builds that iframe's srcdoc as one pure string, so its shape is unit-testable, and it
5
+ // carries the device table the frame's width control offers.
6
+ import { escapeHtml } from '../escape.js';
7
+ /** The four widths the device menu offers, in menu order. Desktop leads as the default. */
8
+ export const previewDevices = [
9
+ { id: 'desktop', label: 'Desktop', width: null },
10
+ { id: 'tablet', label: 'Tablet', width: 768 },
11
+ { id: 'phone', label: 'Phone', width: 390 },
12
+ { id: 'small', label: 'Small phone', width: 320 },
13
+ ];
14
+ /** The table row for a device id. The id type makes a miss impossible; the fallback satisfies find. */
15
+ export function previewDevice(id) {
16
+ return previewDevices.find((d) => d.id === id) ?? previewDevices[0];
17
+ }
18
+ /** A device's user-facing text, shared by the toolbar's menu items and the frame caption: the
19
+ * label with its width when one is fixed, so the value reaches assistive tech at pick time. */
20
+ export function deviceLabel(d) {
21
+ return d.width === null ? d.label : `${d.label} · ${d.width} px`;
22
+ }
23
+ /**
24
+ * Build the preview iframe's srcdoc: a complete document linking the site's stylesheets around
25
+ * the rendered entry html. The html comes from the site's floored render pipeline, which already
26
+ * stripped scripts and event handlers, so it embeds unescaped; the frame's empty `sandbox` is
27
+ * belt and braces over that floor. The parameter is the flat `ResolvedPreview` shape `editLoad`
28
+ * ships, so the per-concept map can never reach the frame document by construction.
29
+ * `preview` null (a site without the adapter knob) yields a styleless but complete document.
30
+ */
31
+ export function buildPreviewDoc(html, preview) {
32
+ const links = (preview?.stylesheets ?? [])
33
+ .map((href) => `<link rel="stylesheet" href="${escapeHtml(href)}">`)
34
+ .join('\n');
35
+ const bodyAttrs = preview?.bodyClass ? ` class="${escapeHtml(preview.bodyClass)}"` : '';
36
+ const content = preview?.containerClass
37
+ ? `<div class="${escapeHtml(preview.containerClass)}">${html}</div>`
38
+ : html;
39
+ // The reset sits BEFORE the site links so the site's CSS wins every collision: it only clears
40
+ // the default body margin and pins a white ground for sheets that assume one.
41
+ //
42
+ // The base tag is what makes links inert. The empty sandbox alone does not: a sandboxed
43
+ // context may still navigate itself, and a srcdoc document resolves relative hrefs against the
44
+ // parent's base URL, so a clicked fragment or root link could render the admin login inside
45
+ // the frame. Targeting every link at a new tab turns each click into a popup, and the sandbox
46
+ // (which grants no allow-popups) blocks it, so a proofing click goes nowhere.
47
+ return [
48
+ '<!doctype html>',
49
+ '<html>',
50
+ '<head>',
51
+ '<meta charset="utf-8">',
52
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
53
+ '<base target="_blank">',
54
+ '<style>body{margin:0;background:#fff}</style>',
55
+ links,
56
+ '</head>',
57
+ `<body${bodyAttrs}>`,
58
+ content,
59
+ '</body>',
60
+ '</html>',
61
+ ]
62
+ .filter((line) => line !== '')
63
+ .join('\n');
64
+ }
@@ -31,6 +31,7 @@ export function composeRuntime({ adapter, siteConfig, extensions = [] }) {
31
31
  registry: adapter.registry,
32
32
  icons: adapter.icons,
33
33
  navMenu: adapter.navMenu,
34
+ preview: adapter.preview,
34
35
  assets: adapter.assets,
35
36
  adminPanels,
36
37
  fieldTypes,
@@ -133,6 +133,34 @@ export interface NavMenuConfig {
133
133
  /** Max nesting depth allowed in the editor; defaults to 2. */
134
134
  maxDepth?: number;
135
135
  }
136
+ /**
137
+ * How the edit page's preview frame reproduces the live site's content styling. The admin
138
+ * deliberately never loads the site's CSS (chrome isolation), so a design-accurate preview needs
139
+ * the site to name its stylesheets for the preview frame; without this knob the preview renders
140
+ * unstyled markup. The frame's srcdoc pins a white body background as a deliberately overridable
141
+ * default, so a site whose ground is not white should state its body background in its own
142
+ * stylesheet.
143
+ */
144
+ export interface PreviewConfig {
145
+ /** Absolute or root-relative URLs of the site's compiled stylesheets, linked inside the
146
+ * preview document. A Vite `?url` import of the site's CSS resolves the hashed asset URL. */
147
+ stylesheets: string[];
148
+ /** Class list applied to the preview document's body, for theme or typography roots. */
149
+ bodyClass?: string;
150
+ /** Class list for a wrapper element around the rendered content, reproducing the site's
151
+ * content container (a prose or measure class). Omitted renders the content bare. */
152
+ containerClass?: string;
153
+ /** Per-concept overrides of bodyClass and containerClass, keyed by concept id. An entry's
154
+ * preview resolves the override for its concept over the top-level values; stylesheets are
155
+ * always shared. */
156
+ byConcept?: Record<string, {
157
+ bodyClass?: string;
158
+ containerClass?: string;
159
+ }>;
160
+ }
161
+ /** The flat preview shape `editLoad` ships to the edit page: the top-level `PreviewConfig`
162
+ * values with the entry's concept override applied, and no `byConcept` map. */
163
+ export type ResolvedPreview = Omit<PreviewConfig, 'byConcept'>;
136
164
  /** Reserved asset slot (seam 4). Typed and unused in the rebuild; R7/R9 read it later with no contract change. */
137
165
  export interface AssetConfig {
138
166
  /** Repo-relative asset roots, e.g. ["static/images"]. */
@@ -168,6 +196,9 @@ export interface CairnAdapter {
168
196
  /** The site's glyph name to SVG path-data map, for the admin icon picker and the renderer. */
169
197
  icons?: IconSet;
170
198
  navMenu?: NavMenuConfig;
199
+ /** The live site's content styling for the preview frame. The admin's chrome isolation keeps
200
+ * the site's CSS out of the admin document, so the preview frame links these instead. */
201
+ preview?: PreviewConfig;
171
202
  assets?: AssetConfig;
172
203
  }
173
204
  /**
@@ -263,6 +294,8 @@ export interface CairnRuntime {
263
294
  /** The site's glyph name to SVG path-data map, for the admin icon picker and the renderer. */
264
295
  icons?: IconSet;
265
296
  navMenu?: NavMenuConfig;
297
+ /** The live site's content styling for the preview frame; passed through from the adapter. */
298
+ preview?: PreviewConfig;
266
299
  assets?: AssetConfig;
267
300
  /** Admin panels contributed by extensions (Mode 2). Empty until Plan 09 wires the dispatch route. */
268
301
  adminPanels?: AdminPanel[];
@@ -69,6 +69,14 @@ export const REGISTRY = {
69
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
70
  docsAnchor: 'cloudflare-readiness.md#hand-cairn-the-csrf-authority',
71
71
  },
72
+ 'config.public-origin-invalid': {
73
+ id: 'config.public-origin-invalid',
74
+ severity: 'blocker',
75
+ title: 'PUBLIC_ORIGIN is missing or invalid',
76
+ why: 'PUBLIC_ORIGIN is unset, does not parse as a URL, or uses http on a non-local host. The magic-link confirmation links and the absolute feed URLs derive from it, config-only so a forged Host header cannot redirect a link, and sign-in cannot mint a usable link without it.',
77
+ remediation: "Set PUBLIC_ORIGIN to the site's canonical https origin in the wrangler config vars (with .dev.vars carrying the local http override), then re-deploy; http passes only on localhost or 127.0.0.1.",
78
+ docsAnchor: 'cloudflare-readiness.md#set-the-public-origin',
79
+ },
72
80
  'config.site-config-invalid': {
73
81
  id: 'config.site-config-invalid',
74
82
  severity: 'blocker',
@@ -77,6 +85,14 @@ export const REGISTRY = {
77
85
  remediation: 'Correct site.config.yaml; the parse or validation error names the failing field or URL-policy rule.',
78
86
  docsAnchor: 'cloudflare-readiness.md#validate-the-site-config',
79
87
  },
88
+ 'config.dependency-floors-unmet': {
89
+ id: 'config.dependency-floors-unmet',
90
+ severity: 'blocker',
91
+ title: 'A framework dependency sits below the engine floor',
92
+ why: 'The lockfile resolves svelte or @sveltejs/kit below the range the engine declares as a peer. Consumer sites compile the shipped .svelte sources, so a below-floor compiler bites silently at build time; svelte 5.56.1 miscompiles parenthesized boolean groupings, which is why the svelte floor is ^5.56.3.',
93
+ remediation: "Raise the devDependency range in the site's package.json to the engine peer range and reinstall so the lockfile re-resolves, for example `npm install --save-dev svelte@^5.56.3`.",
94
+ docsAnchor: 'cloudflare-readiness.md#meet-the-dependency-floors',
95
+ },
80
96
  'edge.hsts-off': {
81
97
  id: 'edge.hsts-off',
82
98
  severity: 'warning',
@@ -102,6 +118,14 @@ export const REGISTRY = {
102
118
  docsAnchor: 'cloudflare-readiness.md#install-the-github-app',
103
119
  logEvent: 'github.unreachable',
104
120
  },
121
+ 'admin.login-probe-failed': {
122
+ id: 'admin.login-probe-failed',
123
+ severity: 'blocker',
124
+ title: 'Live admin login probe failed',
125
+ why: 'A live request to the deployed admin did not answer with the working sign-in envelope (the login page, its CSRF cookie and hidden field, and the request action), so a real editor cannot sign in either. A probe failure has many possible causes; the detail line names the assertion that failed.',
126
+ remediation: 'Read the failed assertion in the detail line, run the full doctor against the same site, and work through the deploy guide; the other checks narrow the cause.',
127
+ docsAnchor: 'cloudflare-readiness.md#probe-the-deployed-admin',
128
+ },
105
129
  };
106
130
  // The registry is shared identity, never working state; freeze every entry and the map itself.
107
131
  for (const entry of Object.values(REGISTRY))
@@ -7,8 +7,10 @@
7
7
  // before the process ends.
8
8
  import { readFile } from 'node:fs/promises';
9
9
  import { resolve } from 'node:path';
10
+ import { liveProbeCheck } from './check-probe.js';
10
11
  import { liveSendCheck } from './check-send.js';
11
- import { contextFromEnv, defaultChecks, formatReport, parseArgs, runDoctor } from './index.js';
12
+ import { readWranglerConfig } from './wrangler-config.js';
13
+ import { contextFromEnv, defaultChecks, deriveMissingInputs, formatReport, parseArgs, runDoctor, } from './index.js';
12
14
  async function main() {
13
15
  let args;
14
16
  try {
@@ -20,23 +22,39 @@ async function main() {
20
22
  return;
21
23
  }
22
24
  const cwd = process.cwd();
25
+ const readFileUnderCwd = async (relPath) => {
26
+ try {
27
+ return await readFile(resolve(cwd, relPath), 'utf8');
28
+ }
29
+ catch (err) {
30
+ if (err.code === 'ENOENT')
31
+ return null;
32
+ throw err;
33
+ }
34
+ };
35
+ // Fill inputs the flags and env left missing from the repo itself: from and repo off the
36
+ // adapter (through the vite arm, which exists only on this bin path, never in a Worker)
37
+ // and the account id off the wrangler config. The API token stays env-only.
38
+ const derived = await deriveMissingInputs(contextFromEnv(process.env, args, cwd), {
39
+ adapterFacts: async () => {
40
+ const { readAdapterFacts } = await import('../vite/index.js');
41
+ return readAdapterFacts(cwd);
42
+ },
43
+ wranglerAccountId: async () => (await readWranglerConfig(readFileUnderCwd))?.accountId,
44
+ });
23
45
  const ctx = {
24
- ...contextFromEnv(process.env, args, cwd),
46
+ ...derived,
25
47
  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
- },
48
+ readFile: readFileUnderCwd,
36
49
  };
37
50
  const checks = defaultChecks();
38
51
  if (args.sendTest)
39
52
  checks.push(liveSendCheck(args.sendTest));
53
+ // The probe is an opt-in network POST against a live site, so it joins only on --probe;
54
+ // the bare flag hands the URL resolution (the PUBLIC_ORIGIN input) to the check itself.
55
+ if (args.probe !== undefined) {
56
+ checks.push(liveProbeCheck(args.probe === true ? undefined : args.probe));
57
+ }
40
58
  const { results, failed } = await runDoctor(checks, ctx);
41
59
  console.log(formatReport(results));
42
60
  process.exitCode = failed > 0 ? 1 : 0;
@@ -0,0 +1,15 @@
1
+ import type { CheckResult, DoctorCheck } from './types.js';
2
+ /**
3
+ * Judge a lockfile's resolved framework versions against the engine's peer ranges. Pure, so the
4
+ * tests drive it table-style; the check object wires in the real lockfile and the real peers.
5
+ * A below-range version fails; a lockfile or entry the check cannot read skips, since a pnpm or
6
+ * yarn consumer carries no package-lock.json at all.
7
+ */
8
+ export declare function dependencyFloorsResult(lockText: string | null, peers: Record<string, string>): CheckResult;
9
+ /**
10
+ * The engine's own declared peer ranges, read from the installed package.json at runtime so the
11
+ * floors are declared exactly once. The self-reference resolves through the consumer's
12
+ * node_modules in a real install and through the repo root during development.
13
+ */
14
+ export declare function readEnginePeers(): Record<string, string>;
15
+ export declare const configDependencyFloors: DoctorCheck;