smoodly 0.0.28 → 0.0.30
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/README.md +123 -17
- package/dist/admin/client-ops.js +1 -0
- package/dist/admin/drift-copy.d.ts +19 -0
- package/dist/admin/drift-copy.js +89 -0
- package/dist/admin/drift.d.ts +76 -0
- package/dist/admin/drift.js +264 -0
- package/dist/admin/editor/CanvasWarning.d.ts +5 -0
- package/dist/admin/editor/CanvasWarning.js +10 -0
- package/dist/admin/editor/EditorView.js +113 -26
- package/dist/admin/editor/InlineRichtext.js +3 -1
- package/dist/admin/editor/Outline.d.ts +7 -0
- package/dist/admin/editor/Outline.js +8 -5
- package/dist/admin/editor/PageInspector.d.ts +6 -1
- package/dist/admin/editor/PageInspector.js +4 -2
- package/dist/admin/editor/PageSettings.js +26 -16
- package/dist/admin/editor/UnknownNode.d.ts +7 -0
- package/dist/admin/editor/UnknownNode.js +8 -0
- package/dist/admin/editor/canvas-diagnosis.d.ts +49 -0
- package/dist/admin/editor/canvas-diagnosis.js +106 -0
- package/dist/admin/editor/crash-copy.d.ts +7 -0
- package/dist/admin/editor/crash-copy.js +13 -0
- package/dist/admin/editor/protocol.d.ts +2 -0
- package/dist/admin/editor/useCanvasBridge.d.ts +3 -0
- package/dist/admin/editor/useCanvasBridge.js +14 -7
- package/dist/admin/editor/useCanvasDiagnosis.d.ts +7 -0
- package/dist/admin/editor/useCanvasDiagnosis.js +24 -0
- package/dist/admin/field-paths.d.ts +3 -0
- package/dist/admin/field-paths.js +26 -0
- package/dist/admin/forms/OrphanFields.d.ts +8 -0
- package/dist/admin/forms/OrphanFields.js +11 -0
- package/dist/admin/forms/RefPicker.js +6 -2
- package/dist/admin/forms/RichtextElementCard.js +9 -3
- package/dist/admin/forms/RichtextWidget.js +3 -1
- package/dist/admin/forms/TagsField.js +1 -1
- package/dist/admin/forms/richtext-form-context.d.ts +8 -0
- package/dist/admin/forms/richtext-form-context.js +5 -0
- package/dist/admin/health.d.ts +23 -0
- package/dist/admin/health.js +42 -0
- package/dist/admin/index.d.ts +1 -0
- package/dist/admin/next/bridge.js +4 -2
- package/dist/admin/next/index.d.ts +0 -1
- package/dist/admin/next/index.js +6 -1
- package/dist/admin/ops-impl.js +40 -9
- package/dist/admin/ops.d.ts +13 -0
- package/dist/admin/shell/AddRedirectModal.js +1 -1
- package/dist/admin/shell/ContentHealthPage.d.ts +10 -0
- package/dist/admin/shell/ContentHealthPage.js +37 -0
- package/dist/admin/shell/EntriesList.js +4 -4
- package/dist/admin/shell/EntryForm.js +20 -3
- package/dist/admin/shell/ErrorPane.d.ts +4 -2
- package/dist/admin/shell/ErrorPane.js +5 -4
- package/dist/admin/shell/GlobalForm.js +22 -2
- package/dist/admin/shell/PagesList.js +3 -3
- package/dist/admin/shell/SettingsView.js +6 -2
- package/dist/admin/shell/health-list.d.ts +27 -0
- package/dist/admin/shell/health-list.js +47 -0
- package/dist/admin/shell/settings.d.ts +4 -0
- package/dist/admin/shell/settings.js +5 -2
- package/dist/admin/tree-ops.d.ts +4 -0
- package/dist/admin/tree-ops.js +15 -1
- package/dist/admin/ui/ActionMenu.d.ts +20 -0
- package/dist/admin/ui/ActionMenu.js +33 -7
- package/dist/admin/ui/Combobox.d.ts +8 -2
- package/dist/admin/ui/Combobox.js +48 -16
- package/dist/admin/ui/ConfirmDialog.js +3 -3
- package/dist/admin/ui/DriftNotice.d.ts +6 -0
- package/dist/admin/ui/DriftNotice.js +10 -0
- package/dist/admin/ui/combo-box.d.ts +13 -0
- package/dist/admin/ui/combo-box.js +8 -0
- package/dist/admin/ui/confirm.d.ts +8 -0
- package/dist/admin/ui/confirm.js +17 -4
- package/dist/admin/ui/theme.js +29 -3
- package/dist/admin/validate.js +15 -47
- package/dist/custom/CustomSection.d.ts +1 -0
- package/dist/custom/CustomSection.js +4 -1
- package/dist/entry-store.d.ts +13 -0
- package/dist/entry-store.js +26 -3
- package/dist/index.d.ts +2 -2
- package/dist/marks.d.ts +12 -0
- package/dist/marks.js +14 -0
- package/dist/next/canvas-boundary.d.ts +16 -0
- package/dist/next/canvas-boundary.js +25 -0
- package/dist/next/page-renderer.d.ts +3 -1
- package/dist/next/page-renderer.js +33 -4
- package/dist/pairing.js +10 -3
- package/dist/render.d.ts +12 -1
- package/dist/render.js +15 -4
- package/dist/section-crash.d.ts +11 -0
- package/dist/section-crash.js +16 -0
- package/dist/section-guard.d.ts +27 -0
- package/dist/section-guard.js +41 -0
- package/dist/store.d.ts +9 -0
- package/dist/store.js +8 -0
- package/dist/supabase-entry-store.d.ts +2 -1
- package/dist/supabase-entry-store.js +27 -5
- package/dist/supabase-store.d.ts +2 -1
- package/dist/supabase-store.js +27 -0
- package/package.json +1 -1
package/dist/admin/validate.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
// A field whose `.hidden()` condition holds is skipped entirely — a hidden
|
|
6
6
|
// required field is not required (spec 2026-09-14 §12); the same isHidden
|
|
7
7
|
// the form uses decides, over the same scope.
|
|
8
|
-
import { optionValues } from "../fields.js";
|
|
9
8
|
import { isHidden } from "../conditions.js";
|
|
10
9
|
import { cssProblem } from "../css.js";
|
|
11
10
|
import { normalizeTag, sameTag } from "../tags.js";
|
|
@@ -13,6 +12,7 @@ import { hasContent } from "./richtext-doc.js";
|
|
|
13
12
|
import { ELEMENT_NODE, richtextBlocks } from "../richtext-walk.js";
|
|
14
13
|
import { rowsProblems } from "../custom/rows.js";
|
|
15
14
|
import { isCalendarDay, isInstant } from "./forms/date-picker.js";
|
|
15
|
+
import { fits, misfitMessage } from "./drift.js";
|
|
16
16
|
const subfields = (d) => d.fields && typeof d.fields === "object" ? d.fields : {};
|
|
17
17
|
const itemOf = (d) => d.item && typeof d.item === "object" ? d.item : undefined;
|
|
18
18
|
const isEmpty = (v) => v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
@@ -32,6 +32,11 @@ function validateValue(path, value, d, errors, elements, scope) {
|
|
|
32
32
|
errors.push({ field: path, message: "Required." });
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
|
+
// The one type check (spec 2026-09-27 editor safety §2): drift and publish share it.
|
|
36
|
+
if (!fits(d, value)) {
|
|
37
|
+
errors.push({ field: path, message: misfitMessage(d, value) });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
35
40
|
if (d.type === "richtext") {
|
|
36
41
|
if (!hasContent(value)) {
|
|
37
42
|
if (!mayBeAbsent(d))
|
|
@@ -58,26 +63,20 @@ function validateValue(path, value, d, errors, elements, scope) {
|
|
|
58
63
|
}
|
|
59
64
|
// Dates (spec 2026-09-25 date pickers §2): the stored formats exactly, and a day the calendar has.
|
|
60
65
|
if (d.type === "date") {
|
|
61
|
-
if (
|
|
66
|
+
if (!isCalendarDay(value))
|
|
62
67
|
errors.push({ field: path, message: "Not a date (2026-09-02)." });
|
|
63
68
|
return;
|
|
64
69
|
}
|
|
65
70
|
if (d.type === "datetime") {
|
|
66
|
-
if (
|
|
71
|
+
if (!isInstant(value))
|
|
67
72
|
errors.push({ field: path, message: "Not a date and time." });
|
|
68
73
|
return;
|
|
69
74
|
}
|
|
70
|
-
if (d.type === "image" || d.type === "video" || d.type === "file")
|
|
71
|
-
if (typeof value !== "object" || Array.isArray(value) || typeof value.asset !== "string") {
|
|
72
|
-
errors.push({ field: path, message: "Must be an uploaded file." });
|
|
73
|
-
}
|
|
75
|
+
if (d.type === "image" || d.type === "video" || d.type === "file")
|
|
74
76
|
return;
|
|
75
|
-
}
|
|
76
77
|
if (d.type === "list") {
|
|
77
|
-
if (!Array.isArray(value))
|
|
78
|
-
errors.push({ field: path, message: "Must be a list." });
|
|
78
|
+
if (!Array.isArray(value))
|
|
79
79
|
return;
|
|
80
|
-
}
|
|
81
80
|
if (typeof d.min === "number" && value.length < d.min)
|
|
82
81
|
errors.push({ field: path, message: `At least ${plural(d.min, "item")}.` });
|
|
83
82
|
if (typeof d.max === "number" && value.length > d.max)
|
|
@@ -89,37 +88,19 @@ function validateValue(path, value, d, errors, elements, scope) {
|
|
|
89
88
|
return;
|
|
90
89
|
}
|
|
91
90
|
if (d.type === "object") {
|
|
92
|
-
if (typeof value !== "object" || Array.isArray(value)) {
|
|
93
|
-
errors.push({ field: path, message: "Must be an object." });
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
91
|
validateMap(path, value, subfields(d), errors, elements, { ...scope, self: value });
|
|
97
92
|
return;
|
|
98
93
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
errors.push({ field: path, message: "Must be on or off." });
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (d.type === "select" || d.type === "radio") {
|
|
105
|
-
const allowed = optionValues(d.options);
|
|
106
|
-
if (allowed.length > 0 && !allowed.includes(value)) {
|
|
107
|
-
errors.push({ field: path, message: `Must be one of: ${allowed.join(", ")}.` });
|
|
108
|
-
}
|
|
94
|
+
// The shape of these is fits' (above); what is left is the count.
|
|
95
|
+
if (d.type === "boolean" || d.type === "select" || d.type === "radio" || d.type === "checkboxes")
|
|
109
96
|
return;
|
|
110
|
-
}
|
|
111
97
|
// Refs (spec 2026-09-17 §3): the shape and the count. Whether a target exists
|
|
112
98
|
// and is published is publish integrity's question (store.ts), not this one's.
|
|
113
|
-
if (d.type === "ref")
|
|
114
|
-
if (typeof value !== "string")
|
|
115
|
-
errors.push({ field: path, message: "Must be an entry." });
|
|
99
|
+
if (d.type === "ref")
|
|
116
100
|
return;
|
|
117
|
-
}
|
|
118
101
|
if (d.type === "refList") {
|
|
119
|
-
if (!Array.isArray(value)
|
|
120
|
-
errors.push({ field: path, message: "Must be a list of entries." });
|
|
102
|
+
if (!Array.isArray(value))
|
|
121
103
|
return;
|
|
122
|
-
}
|
|
123
104
|
if (typeof d.min === "number" && value.length < d.min)
|
|
124
105
|
errors.push({ field: path, message: `At least ${plural(d.min, "item")}.` });
|
|
125
106
|
if (typeof d.max === "number" && value.length > d.max)
|
|
@@ -130,10 +111,8 @@ function validateValue(path, value, d, errors, elements, scope) {
|
|
|
130
111
|
return;
|
|
131
112
|
}
|
|
132
113
|
if (d.type === "tags") {
|
|
133
|
-
if (!Array.isArray(value))
|
|
134
|
-
errors.push({ field: path, message: "Must be a list of tags." });
|
|
114
|
+
if (!Array.isArray(value))
|
|
135
115
|
return;
|
|
136
|
-
}
|
|
137
116
|
if (typeof d.min === "number" && value.length < d.min)
|
|
138
117
|
errors.push({ field: path, message: `At least ${plural(d.min, "item")}.` });
|
|
139
118
|
if (typeof d.max === "number" && value.length > d.max)
|
|
@@ -146,17 +125,6 @@ function validateValue(path, value, d, errors, elements, scope) {
|
|
|
146
125
|
});
|
|
147
126
|
return;
|
|
148
127
|
}
|
|
149
|
-
if (d.type === "checkboxes") {
|
|
150
|
-
if (!Array.isArray(value)) {
|
|
151
|
-
errors.push({ field: path, message: "Must be a list." });
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
const allowed = optionValues(d.options);
|
|
155
|
-
if (value.some((v) => !allowed.includes(v))) {
|
|
156
|
-
errors.push({ field: path, message: `Must be one of: ${allowed.join(", ")}.` });
|
|
157
|
-
}
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
128
|
if (typeof d.max === "number" && typeof value === "string" && value.length > d.max) {
|
|
161
129
|
errors.push({ field: path, message: `Must be at most ${d.max} characters.` });
|
|
162
130
|
}
|
|
@@ -19,6 +19,7 @@ export type CustomSectionProps = {
|
|
|
19
19
|
export type CustomSectionComponent = ((props: CustomSectionProps) => ReactElement) & {
|
|
20
20
|
schema: {
|
|
21
21
|
name: string;
|
|
22
|
+
title: string;
|
|
22
23
|
};
|
|
23
24
|
};
|
|
24
25
|
export declare function createCustomSection({ RichText: Bound, breakpoint, elements }: CustomOptions & {
|
|
@@ -4,6 +4,7 @@ import { RichText } from "../richtext-render.js";
|
|
|
4
4
|
import { FIELD_ATTR, ITEM_ATTR } from "../marks.js";
|
|
5
5
|
import { CUSTOM_NODE_TYPE, DEFAULT_BREAKPOINT } from "./schema.js";
|
|
6
6
|
import { customCss } from "./css.js";
|
|
7
|
+
import { RENDER_VIEW } from "../section-guard.js";
|
|
7
8
|
const isRow = (r) => typeof r === "object" && r !== null && Array.isArray(r.columns);
|
|
8
9
|
export function createCustomSection({ RichText: Bound, breakpoint = DEFAULT_BREAKPOINT, elements = [] }) {
|
|
9
10
|
const Body = Bound ?? (({ doc }) => _jsx(RichText, { doc: doc, elements: elements }));
|
|
@@ -15,6 +16,8 @@ export function createCustomSection({ RichText: Bound, breakpoint = DEFAULT_BREA
|
|
|
15
16
|
: styles;
|
|
16
17
|
return (_jsx(Section, { styles: withLayout, "data-custom": "", children: list.map((row, i) => (_jsx("div", { className: row.reverse ? "smd-row smd-row--reverse" : "smd-row", "data-row": i, [ITEM_ATTR]: `rows.${i}`, children: row.columns.map((doc, j) => (_jsx("div", { className: "smd-col", [FIELD_ATTR]: `columns.${j}`, children: _jsx(Body, { doc: doc }) }, j))) }, i))) }));
|
|
17
18
|
}
|
|
18
|
-
CustomSection.schema = { name: CUSTOM_NODE_TYPE };
|
|
19
|
+
CustomSection.schema = { name: CUSTOM_NODE_TYPE, title: "Custom section" };
|
|
20
|
+
// Smoodly's own plain view: the guard may call it (spec 2026-09-27 section guard §1).
|
|
21
|
+
CustomSection[RENDER_VIEW] = CustomSection;
|
|
19
22
|
return CustomSection;
|
|
20
23
|
}
|
package/dist/entry-store.d.ts
CHANGED
|
@@ -40,6 +40,12 @@ export type EntryVersion = {
|
|
|
40
40
|
/** When the content last changed: the unpublished draft row is rewritten in place (spec 2026-09-13 history §1). */
|
|
41
41
|
savedAt: number;
|
|
42
42
|
};
|
|
43
|
+
/** One entry row for the drift scan (spec 2026-09-27 editor safety §3), whatever its collection:
|
|
44
|
+
* locale → the live version's merged fields, null where the locale is not published. */
|
|
45
|
+
export type EntryScan = {
|
|
46
|
+
record: EntryRecord;
|
|
47
|
+
live: Record<string, Record<string, unknown> | null>;
|
|
48
|
+
};
|
|
43
49
|
export type EntryUsage = {
|
|
44
50
|
sourceKind: string;
|
|
45
51
|
sourceId: string;
|
|
@@ -96,6 +102,8 @@ export interface EntryStore {
|
|
|
96
102
|
create(collection: string, fields: Record<string, unknown>, options: CreateEntryOptions, by?: string): Promise<EntryRecord>;
|
|
97
103
|
get(collection: string, id: string): Promise<EntryRecord | null>;
|
|
98
104
|
list(collection: string, options?: ListOptions): Promise<EntryRecord[]>;
|
|
105
|
+
/** Every entry row, registered collection or not, with each published locale's live fields (spec 2026-09-27 editor safety §3). */
|
|
106
|
+
scanEntries(): Promise<EntryScan[]>;
|
|
99
107
|
/** Batched read for the resolver: the MERGED fields in `locale`, keyed
|
|
100
108
|
* by id. An id that is missing, absent in the locale, or (with
|
|
101
109
|
* `status`) unpublished there is omitted. With `{ status: "published" }`
|
|
@@ -203,6 +211,10 @@ export type SplitFields = {
|
|
|
203
211
|
* Every key that is neither localized nor `slug` is shared — declared or
|
|
204
212
|
* not — so a stray key never disappears into a locale row. */
|
|
205
213
|
export declare function splitFields(fields: Record<string, unknown>, schema: CollectionSchema<any>): SplitFields;
|
|
214
|
+
/** A localized key's value still in the shared fields — saved while the field was shared — stays
|
|
215
|
+
* there as every locale's fallback: a write never deletes it (spec 2026-09-27 editor safety §7).
|
|
216
|
+
* Reads merge shared, then the row, so each locale shows its own value once it has one. */
|
|
217
|
+
export declare function keepLocalizedShared(before: Record<string, unknown>, shared: Record<string, unknown>, schema: CollectionSchema<any>): Record<string, unknown>;
|
|
206
218
|
export declare function entryLocaleRow(record: EntryRecord, locale: string): EntryLocale;
|
|
207
219
|
/** The merged view of one locale: shared + that locale's fields + its
|
|
208
220
|
* slug. A fresh object; throws for an absent locale. */
|
|
@@ -285,6 +297,7 @@ export declare class MemoryEntryStore implements EntryStore {
|
|
|
285
297
|
discardDraft(collection: string, id: string, locale: string, by?: string, expected?: DraftToken | null): Promise<EntryRecord>;
|
|
286
298
|
addLocale(collection: string, id: string, locale: string, options: AddEntryLocaleOptions, by?: string): Promise<EntryRecord>;
|
|
287
299
|
removeLocale(collection: string, id: string, locale: string): Promise<EntryRecord>;
|
|
300
|
+
scanEntries(): Promise<EntryScan[]>;
|
|
288
301
|
listVersions(collection: string, id: string, locale: string): Promise<EntryVersion[]>;
|
|
289
302
|
restore(collection: string, id: string, locale: string, versionId: string, by?: string, expected?: DraftToken | null): Promise<EntryRecord>;
|
|
290
303
|
getVersion(collection: string, versionId: string): Promise<EntryVersion | null>;
|
package/dist/entry-store.js
CHANGED
|
@@ -98,6 +98,17 @@ export function splitFields(fields, schema) {
|
|
|
98
98
|
}
|
|
99
99
|
return { shared: structuredClone(shared), localized: structuredClone(localized), slug: slugOf(fields) };
|
|
100
100
|
}
|
|
101
|
+
/** A localized key's value still in the shared fields — saved while the field was shared — stays
|
|
102
|
+
* there as every locale's fallback: a write never deletes it (spec 2026-09-27 editor safety §7).
|
|
103
|
+
* Reads merge shared, then the row, so each locale shows its own value once it has one. */
|
|
104
|
+
export function keepLocalizedShared(before, shared, schema) {
|
|
105
|
+
const out = { ...shared };
|
|
106
|
+
for (const key of localizedKeys(schema)) {
|
|
107
|
+
if (key in before && !(key in out))
|
|
108
|
+
out[key] = structuredClone(before[key]);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
101
112
|
export function entryLocaleRow(record, locale) {
|
|
102
113
|
const row = record.locales[locale];
|
|
103
114
|
if (!row)
|
|
@@ -408,7 +419,7 @@ export class MemoryEntryStore {
|
|
|
408
419
|
throw entryErrors.noSlug(collection);
|
|
409
420
|
this.assertSiblingSlug(collection, entry.parentId, locale, slug, id);
|
|
410
421
|
const before = { fields: entry.fields, row: { ...row } };
|
|
411
|
-
entry.fields = shared;
|
|
422
|
+
entry.fields = keepLocalizedShared(before.fields, shared, schema);
|
|
412
423
|
row.fields = localized;
|
|
413
424
|
row.slug = slug;
|
|
414
425
|
try {
|
|
@@ -449,7 +460,7 @@ export class MemoryEntryStore {
|
|
|
449
460
|
if (!snapshot)
|
|
450
461
|
throw entryErrors.nothingPublished(collection, id, locale);
|
|
451
462
|
const { shared, localized } = splitFields(snapshot.fields, schema);
|
|
452
|
-
entry.fields = shared;
|
|
463
|
+
entry.fields = keepLocalizedShared(entry.fields, shared, schema);
|
|
453
464
|
row.fields = localized;
|
|
454
465
|
const abandoned = row.draftVersionId;
|
|
455
466
|
row.draftVersionId = row.publishedVersionId;
|
|
@@ -517,6 +528,18 @@ export class MemoryEntryStore {
|
|
|
517
528
|
this.writeRefs(entry);
|
|
518
529
|
return this.snapshot(entry);
|
|
519
530
|
}
|
|
531
|
+
async scanEntries() {
|
|
532
|
+
return [...this.entries.values()].map((entry) => ({
|
|
533
|
+
record: this.snapshot(entry),
|
|
534
|
+
live: Object.fromEntries(Object.entries(entry.locales).map(([l, row]) => {
|
|
535
|
+
if (row.status !== "published")
|
|
536
|
+
return [l, null];
|
|
537
|
+
// A legacy published row without a snapshot serves its live row (spec 2026-09-14 §1).
|
|
538
|
+
const snap = row.publishedVersionId ? this.versions.get(row.publishedVersionId) : undefined;
|
|
539
|
+
return [l, snap ? structuredClone(snap.fields) : entryFieldsIn(entry, l)];
|
|
540
|
+
})),
|
|
541
|
+
}));
|
|
542
|
+
}
|
|
520
543
|
async listVersions(collection, id, locale) {
|
|
521
544
|
this.must(collection, id);
|
|
522
545
|
return [...this.versions.values()]
|
|
@@ -535,7 +558,7 @@ export class MemoryEntryStore {
|
|
|
535
558
|
if (versionId === row.publishedVersionId)
|
|
536
559
|
return this.discardDraft(collection, id, locale, by, expected);
|
|
537
560
|
const { shared, localized } = splitFields(version.fields, schema);
|
|
538
|
-
entry.fields = shared;
|
|
561
|
+
entry.fields = keepLocalizedShared(entry.fields, shared, schema);
|
|
539
562
|
row.fields = localized; // the slug is the row's own and never versioned
|
|
540
563
|
this.recordVersion(entry, locale);
|
|
541
564
|
this.bump(entry, row, by);
|
package/dist/index.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export type { PathIndex } from "./path-index.ts";
|
|
|
51
51
|
export { MemoryEntryStore } from "./entry-store.ts";
|
|
52
52
|
export { SupabaseEntryStore } from "./supabase-entry-store.ts";
|
|
53
53
|
export { SupabasePathIndex } from "./supabase-path-index.ts";
|
|
54
|
-
export type { EntryStore, EntryRecord, EntryLocale, EntryVersion, EntryUsage, ListOptions, CreateEntryOptions, AddEntryLocaleOptions, EntryStoreOptions, Where, WhereValue, } from "./entry-store.ts";
|
|
54
|
+
export type { EntryStore, EntryRecord, EntryLocale, EntryVersion, EntryScan, EntryUsage, ListOptions, CreateEntryOptions, AddEntryLocaleOptions, EntryStoreOptions, Where, WhereValue, } from "./entry-store.ts";
|
|
55
55
|
export { entryErrors, compareEntries, DEFAULT_ENTRY_LOCALES, localizedKeys, splitFields, entryLocaleRow, entryFieldsIn, entryRefEdges, entryTitleIn, matchesWhere, } from "./entry-store.ts";
|
|
56
56
|
export { affectedTargets, pageTag, entryTag, globalTag, pagesTreeTag, collectionTag } from "./revalidate.ts";
|
|
57
57
|
export type { AffectedTargets } from "./revalidate.ts";
|
|
@@ -60,7 +60,7 @@ export type { EntryFetcher, ResolveFieldsOptions, AssetFetcher } from "./resolve
|
|
|
60
60
|
export { SEGMENT_RE, isSegment, assertSegment, joinPath, splitPath, segmentFor, isHomeNode, pagePathIn, chainOf, assertDepth, pagePath, pagePathRows, entryPathRows, collectionSegment, buildPageTree, perLocaleSegments, assertSupportedLocale, unsupportedLocale, pageAlternates, entryAlternates, } from "./paths.ts";
|
|
61
61
|
export type { LocaleSet, PathTarget, PathRow, PageLike, EntryLike, PageTreeNode, PagePathOptions, LocaleAlternate, PageAlternate } from "./paths.ts";
|
|
62
62
|
export { fixedPathFor } from "./localize.ts";
|
|
63
|
-
export type { PageStore, PageRecord, PageLocale, PageVersion, PageStoreOptions, CreatePageInput, AddLocaleOptions } from "./store.ts";
|
|
63
|
+
export type { PageStore, PageRecord, PageLocale, PageVersion, PageScan, PageStoreOptions, CreatePageInput, AddLocaleOptions } from "./store.ts";
|
|
64
64
|
export { pathOptions, pageErrors, localeRow, pageRefEdges, DEFAULT_LOCALES } from "./store.ts";
|
|
65
65
|
export type { SmoodlyConfig, ResolvedConfig } from "./config.ts";
|
|
66
66
|
export type { FieldBuilder, Descriptor, ValueOf, Option, OptionInput, OptionValue } from "./fields.ts";
|
package/dist/marks.d.ts
CHANGED
|
@@ -53,4 +53,16 @@ export type Scope<T> = Marks<T> & {
|
|
|
53
53
|
export declare function marks<S extends {
|
|
54
54
|
__f?: BuilderMap;
|
|
55
55
|
}>(_schema: S): Marks<NamespaceValues<NonNullable<S["__f"]>>>;
|
|
56
|
+
/** A crashed section's placeholder (spec 2026-09-27 section guard §5): `section` — the guard caught
|
|
57
|
+
* it and the live site leaves it out; `page` — only the canvas net caught it, the live page would fail. */
|
|
58
|
+
export declare const CRASHED_ATTR = "data-smd-crashed";
|
|
59
|
+
/** One crashed section as the bridge reports it. */
|
|
60
|
+
export type CrashedSection = {
|
|
61
|
+
id: string;
|
|
62
|
+
live: "left-out" | "page-fails";
|
|
63
|
+
};
|
|
64
|
+
/** The crashed sections among the marked elements, in document order — what the bridge posts. */
|
|
65
|
+
export declare function crashedOf(els: ReadonlyArray<{
|
|
66
|
+
getAttribute(name: string): string | null;
|
|
67
|
+
}>): CrashedSection[];
|
|
56
68
|
export {};
|
package/dist/marks.js
CHANGED
|
@@ -44,3 +44,17 @@ function level() {
|
|
|
44
44
|
export function marks(_schema) {
|
|
45
45
|
return level();
|
|
46
46
|
}
|
|
47
|
+
/** A crashed section's placeholder (spec 2026-09-27 section guard §5): `section` — the guard caught
|
|
48
|
+
* it and the live site leaves it out; `page` — only the canvas net caught it, the live page would fail. */
|
|
49
|
+
export const CRASHED_ATTR = "data-smd-crashed";
|
|
50
|
+
/** The crashed sections among the marked elements, in document order — what the bridge posts. */
|
|
51
|
+
export function crashedOf(els) {
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const el of els) {
|
|
54
|
+
const id = el.getAttribute(NODE_ATTR);
|
|
55
|
+
const mark = el.getAttribute(CRASHED_ATTR);
|
|
56
|
+
if (id && (mark === "section" || mark === "page"))
|
|
57
|
+
out.push({ id, live: mark === "section" ? "left-out" : "page-fails" });
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Component, type ReactNode } from "react";
|
|
2
|
+
type Props = {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
};
|
|
7
|
+
type State = {
|
|
8
|
+
failed: boolean;
|
|
9
|
+
error: unknown;
|
|
10
|
+
};
|
|
11
|
+
export declare class CanvasBoundary extends Component<Props, State> {
|
|
12
|
+
state: State;
|
|
13
|
+
static getDerivedStateFromError(error: unknown): State;
|
|
14
|
+
render(): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
// The Next adapter's canvas net (spec 2026-09-27 section guard §5): in
|
|
4
|
+
// editor mode each section sits in <Suspense> and this boundary, so a
|
|
5
|
+
// throw the guard cannot catch — in a component the section renders, or
|
|
6
|
+
// in a 'use client' section — fails only that section: Server Components
|
|
7
|
+
// hand it to the browser to retry, and this catches it there. Never
|
|
8
|
+
// rendered outside editor mode. Imports nothing from `next`, but it is
|
|
9
|
+
// Next's mechanism, so it lives with the adapter.
|
|
10
|
+
import { Component } from "react";
|
|
11
|
+
import { CrashPlaceholder } from "../section-crash.js";
|
|
12
|
+
export class CanvasBoundary extends Component {
|
|
13
|
+
constructor() {
|
|
14
|
+
super(...arguments);
|
|
15
|
+
this.state = { failed: false, error: undefined };
|
|
16
|
+
}
|
|
17
|
+
static getDerivedStateFromError(error) {
|
|
18
|
+
return { failed: true, error };
|
|
19
|
+
}
|
|
20
|
+
render() {
|
|
21
|
+
return this.state.failed
|
|
22
|
+
? _jsx(CrashPlaceholder, { id: this.props.id, title: this.props.title, error: this.state.error, live: "page" })
|
|
23
|
+
: this.props.children;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ComponentType, type ReactElement } from "react";
|
|
2
2
|
import type { Metadata } from "next";
|
|
3
3
|
import type { Descriptor } from "../fields.ts";
|
|
4
4
|
import { type PairedPage } from "../render.tsx";
|
|
5
5
|
import type { GlobalSchema } from "../global.ts";
|
|
6
6
|
import { type SmoodlySite } from "../site.ts";
|
|
7
|
+
/** Logged once per renderer when Next's cacheComponents is on (spec 2026-09-27 cache components §2). */
|
|
8
|
+
export declare const CACHE_COMPONENTS_WARNING = "smoodly: Next's cacheComponents is on, and Smoodly does not support it \u2014 a missing page answers 200 with noindex instead of 404, and a redirect answers 200 instead of 308. Turn cacheComponents off in next.config.ts; the Next.js guide's \"Cache Components\" section says why (https://smoodly.io/docs/frameworks/nextjs).";
|
|
7
9
|
/** A registered section, as renderPage's registry needs it. */
|
|
8
10
|
type SectionEntry = ComponentType<any> & {
|
|
9
11
|
schema: {
|
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// The site-route seam (spec 2026-09-05 §5): the two route files call
|
|
3
|
+
// renderSmoodlyPath; everything else — request gating, draft mode, the
|
|
4
|
+
// cache keyed by (locale, path) and tagged by id, 404s, the editor
|
|
5
|
+
// bridge — lives here, over the framework-free site layer. Bound once by
|
|
6
|
+
// the app, exactly like createAdminOpHandler.
|
|
7
|
+
import { Suspense } from "react";
|
|
2
8
|
import { unstable_cache } from "next/cache";
|
|
3
9
|
import { draftMode } from "next/headers";
|
|
4
|
-
import { notFound, permanentRedirect } from "next/navigation";
|
|
10
|
+
import { notFound, permanentRedirect, unstable_rethrow } from "next/navigation";
|
|
5
11
|
import { connection } from "next/server";
|
|
6
12
|
import { EditorBridge } from "../admin/next/bridge.js";
|
|
13
|
+
import { CanvasBoundary } from "./canvas-boundary.js";
|
|
14
|
+
import { DRAFT_META_NAME } from "../admin/editor/canvas-diagnosis.js";
|
|
7
15
|
import { fixedPathFor } from "../localize.js";
|
|
8
16
|
import { joinPath } from "../paths.js";
|
|
9
17
|
import { renderPage } from "../render.js";
|
|
10
18
|
import { entryTag, pageTag, globalTag } from "../revalidate.js";
|
|
11
19
|
import { pageContextOf } from "../site.js";
|
|
12
20
|
import { metadataFrom } from "./meta.js";
|
|
21
|
+
/** Logged once per renderer when Next's cacheComponents is on (spec 2026-09-27 cache components §2). */
|
|
22
|
+
export const CACHE_COMPONENTS_WARNING = "smoodly: Next's cacheComponents is on, and Smoodly does not support it — a missing page answers 200 with noindex instead of 404, and a redirect answers 200 instead of 308. Turn cacheComponents off in next.config.ts; the Next.js guide's \"Cache Components\" section says why (https://smoodly.io/docs/frameworks/nextjs).";
|
|
23
|
+
/** The canvas net (spec 2026-09-27 section guard §5): Server Components hand a section that failed to the
|
|
24
|
+
* browser, where CanvasBoundary catches it. Defined here, not in the client file: a function exported
|
|
25
|
+
* from a "use client" module is a reference on the server and cannot be called. */
|
|
26
|
+
const canvasWrap = (child, section) => (_jsx(Suspense, { fallback: null, children: _jsx(CanvasBoundary, { id: section.id, title: section.title, children: child }) }));
|
|
13
27
|
export function createPageRenderer(options) {
|
|
14
28
|
const { site } = options;
|
|
15
29
|
const href = options.href ?? ((path) => path);
|
|
@@ -48,7 +62,7 @@ export function createPageRenderer(options) {
|
|
|
48
62
|
notFound();
|
|
49
63
|
}
|
|
50
64
|
const page = pageContextOf(hit, href);
|
|
51
|
-
return renderPage(Page, hit.tree, { sections: options.sections, elements: options.elements, styles: site.styles, custom: site.custom }, { editor, page });
|
|
65
|
+
return renderPage(Page, hit.tree, { sections: options.sections, elements: options.elements, styles: site.styles, custom: site.custom }, { editor, page, rethrow: unstable_rethrow, editorWrap: canvasWrap });
|
|
52
66
|
}
|
|
53
67
|
const View = entryPages.get(hit.collection);
|
|
54
68
|
if (!View) {
|
|
@@ -67,10 +81,22 @@ export function createPageRenderer(options) {
|
|
|
67
81
|
languages[locale] = href(alt.path, locale);
|
|
68
82
|
return { alternates: { languages } };
|
|
69
83
|
}
|
|
84
|
+
// Next's cacheComponents serves the catch-all from a fallback shell that
|
|
85
|
+
// commits 200 before the page renders, so 404s and 308s are lost (spec
|
|
86
|
+
// 2026-09-27 cache components). The variable is Next's own, inlined at
|
|
87
|
+
// build: a warning, never an error, and once per renderer.
|
|
88
|
+
let cacheComponentsWarned = false;
|
|
89
|
+
function warnCacheComponents() {
|
|
90
|
+
if (cacheComponentsWarned || String(process.env.__NEXT_CACHE_COMPONENTS) !== "true")
|
|
91
|
+
return;
|
|
92
|
+
cacheComponentsWarned = true;
|
|
93
|
+
console.warn(CACHE_COMPONENTS_WARNING);
|
|
94
|
+
}
|
|
70
95
|
async function page(pathFor, opts, accept) {
|
|
96
|
+
warnCacheComponents(); // before connection(), so a build's prerender prints it too
|
|
71
97
|
// A CMS route is request-rendered by construction: without this a
|
|
72
98
|
// route with no dynamic segment would be prerendered against the
|
|
73
|
-
// database at build time (DESIGN.md §3
|
|
99
|
+
// database at build time (DESIGN.md §3).
|
|
74
100
|
await connection();
|
|
75
101
|
const locale = opts?.locale ?? site.locales.default;
|
|
76
102
|
const path = pathFor(locale);
|
|
@@ -86,7 +112,10 @@ export function createPageRenderer(options) {
|
|
|
86
112
|
const hit = target ? await load(target, locale, path, draft, version) : null;
|
|
87
113
|
if (!hit || !accept(hit))
|
|
88
114
|
notFound();
|
|
89
|
-
|
|
115
|
+
// The draft marker tells the page editor's canvas diagnosis that Smoodly
|
|
116
|
+
// rendered this response (spec 2026-09-27 shadowed-page detector §3);
|
|
117
|
+
// React hoists the meta into the head. Never outside draft mode.
|
|
118
|
+
return (_jsxs(_Fragment, { children: [draft ? _jsx("meta", { name: DRAFT_META_NAME }) : null, editor ? _jsx(EditorBridge, {}) : null, render(hit, editor, locale)] }));
|
|
90
119
|
}
|
|
91
120
|
async function metadata(pathFor, opts, accept) {
|
|
92
121
|
// generateMetadata runs alongside the page render, so it gates too.
|
package/dist/pairing.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { defaultsOf } from "./field-defaults.js";
|
|
3
|
+
import { isPlainView, RENDER_VIEW } from "./section-guard.js";
|
|
3
4
|
function pair(schema, View) {
|
|
4
5
|
const defaults = defaultsOf(schema.settings);
|
|
5
|
-
const
|
|
6
|
+
const viewProps = (props) => {
|
|
6
7
|
const { settings: overrides, styles, ...fields } = props ?? {};
|
|
7
|
-
|
|
8
|
+
return { ...fields, settings: { ...defaults, ...overrides }, styles: styles ?? {} };
|
|
9
|
+
};
|
|
10
|
+
const Component = (props) => {
|
|
8
11
|
const V = View;
|
|
9
|
-
return _jsx(V, { ...
|
|
12
|
+
return _jsx(V, { ...viewProps(props) });
|
|
10
13
|
};
|
|
11
14
|
Component.schema = schema;
|
|
15
|
+
// The guard's direct call (spec 2026-09-27 section guard §1): only a plain-function view may be called.
|
|
16
|
+
if (isPlainView(View)) {
|
|
17
|
+
Component[RENDER_VIEW] = (props) => View(viewProps(props));
|
|
18
|
+
}
|
|
12
19
|
return Component;
|
|
13
20
|
}
|
|
14
21
|
export function section(schema, View) {
|
package/dist/render.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ComponentType, type ReactElement, type ReactNode } from "react";
|
|
2
2
|
import type { PageAlternate } from "./paths.ts";
|
|
3
3
|
import type { PageSchema } from "./page.ts";
|
|
4
4
|
import type { BuilderMap, NamespaceValues } from "./schema.ts";
|
|
@@ -70,8 +70,14 @@ export declare const EMPTY_PAGE_CONTEXT: PageContext;
|
|
|
70
70
|
type AnyPaired = ComponentType<any> & {
|
|
71
71
|
schema: {
|
|
72
72
|
name: string;
|
|
73
|
+
title?: string;
|
|
73
74
|
};
|
|
74
75
|
};
|
|
76
|
+
/** The adapter's canvas net (spec 2026-09-27 section guard §5): called per section, in editor mode only. */
|
|
77
|
+
export type EditorWrap = (child: ReactNode, section: {
|
|
78
|
+
id: string;
|
|
79
|
+
title: string;
|
|
80
|
+
}) => ReactNode;
|
|
75
81
|
export type ZoneContent = {
|
|
76
82
|
readonly name: string;
|
|
77
83
|
readonly nodes: TreeNode[];
|
|
@@ -79,6 +85,9 @@ export type ZoneContent = {
|
|
|
79
85
|
/** The config's CSS values behind the styles set (spec 2026-09-14 §2). */
|
|
80
86
|
readonly styleValues: StyleValues;
|
|
81
87
|
readonly editor: boolean;
|
|
88
|
+
/** Control flow the guard must let through — the adapter's (Next: unstable_rethrow; spec 2026-09-27 section guard §2). */
|
|
89
|
+
readonly rethrow?: (error: unknown) => void;
|
|
90
|
+
readonly editorWrap?: EditorWrap;
|
|
82
91
|
};
|
|
83
92
|
export declare function Zone({ of: content }: {
|
|
84
93
|
of: ZoneContent;
|
|
@@ -102,5 +111,7 @@ export type Registry = {
|
|
|
102
111
|
export declare function renderPage<Zones extends Record<string, unknown>>(Page: PairedPage<Zones>, tree: PageTree, registry: Registry, options?: {
|
|
103
112
|
editor?: boolean;
|
|
104
113
|
page?: PageContext;
|
|
114
|
+
rethrow?: (error: unknown) => void;
|
|
115
|
+
editorWrap?: EditorWrap;
|
|
105
116
|
}): ReactElement;
|
|
106
117
|
export {};
|
package/dist/render.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment } from "react";
|
|
3
|
+
import { EMPTY_ATTR, NODE_ATTR, ZONE_ATTR } from "./marks.js";
|
|
3
4
|
import { SMD_KEY } from "./section-wrapper.js";
|
|
4
5
|
import { DEFAULT_STYLE_VALUES } from "./styles.js";
|
|
5
6
|
import { CUSTOM_NODE_TYPE, createCustomSection } from "./custom/index.js";
|
|
7
|
+
import { renderGuarded } from "./section-guard.js";
|
|
8
|
+
import { PLACEHOLDER_STYLE } from "./section-crash.js";
|
|
6
9
|
/** The `page` prop of a view rendered outside the site (a test, a preview
|
|
7
10
|
* of a shape): a real object, so a view never guards an optional prop, but
|
|
8
11
|
* no position in the tree. Frozen: one singleton shared by every such
|
|
@@ -23,12 +26,20 @@ export function Zone({ of: content }) {
|
|
|
23
26
|
return (_jsx(_Fragment, { children: nodes.map((node) => {
|
|
24
27
|
const Component = content.components.get(node.type);
|
|
25
28
|
if (!Component) {
|
|
29
|
+
// Editor mode draws a box the bridge can measure, so the node is selectable in place
|
|
30
|
+
// (spec 2026-09-27 editor safety §5); the site renders nothing, as before.
|
|
31
|
+
if (content.editor) {
|
|
32
|
+
return (_jsxs("div", { [NODE_ATTR]: node.id, style: PLACEHOLDER_STYLE, children: ["Unknown section `", node.type, "`"] }, node.id));
|
|
33
|
+
}
|
|
26
34
|
console.warn(`smoodly: no registered component for node type "${node.type}" — skipping.`);
|
|
27
35
|
return null;
|
|
28
36
|
}
|
|
29
37
|
// The node id, the css and the values ride the styles under one reserved key; the wrapper reads and strips it.
|
|
30
38
|
const styles = { ...(node.styles ?? {}), [SMD_KEY]: { node: node.id, css: typeof node.css === "string" ? node.css : undefined, values: content.styleValues } };
|
|
31
|
-
|
|
39
|
+
// Through the guard (spec 2026-09-27 section guard): a throw in the section's own code leaves it out, not the page.
|
|
40
|
+
const title = Component.schema.title ?? node.type;
|
|
41
|
+
const section = renderGuarded(Component, { ...node.fields, settings: node.settings ?? {}, styles }, { id: node.id, type: node.type, title, editor: content.editor, rethrow: content.rethrow });
|
|
42
|
+
return (_jsx(Fragment, { children: content.editor && content.editorWrap ? content.editorWrap(section, { id: node.id, title }) : section }, node.id));
|
|
32
43
|
}) }));
|
|
33
44
|
}
|
|
34
45
|
export function page(schema, View) {
|
|
@@ -49,7 +60,7 @@ export function renderPage(Page, tree, registry, options) {
|
|
|
49
60
|
const styleValues = registry.styles ?? DEFAULT_STYLE_VALUES;
|
|
50
61
|
const zones = Object.fromEntries(Object.keys(Page.schema.zones).map((zone) => [
|
|
51
62
|
zone,
|
|
52
|
-
{ name: zone, nodes: tree.zones?.[zone] ?? [], components, styleValues, editor: options?.editor ?? false },
|
|
63
|
+
{ name: zone, nodes: tree.zones?.[zone] ?? [], components, styleValues, editor: options?.editor ?? false, rethrow: options?.rethrow, editorWrap: options?.editorWrap },
|
|
53
64
|
]));
|
|
54
65
|
return _jsx(Page, { zones: zones, page: (options?.page ?? EMPTY_PAGE_CONTEXT) });
|
|
55
66
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
/** The canvas placeholders' look — the unknown section's too (spec 2026-09-27 editor safety §5). */
|
|
3
|
+
export declare const PLACEHOLDER_STYLE: CSSProperties;
|
|
4
|
+
/** Why it crashed, for the placeholder: the message, or the digest when a production build stripped the message. */
|
|
5
|
+
export declare function crashMessage(error: unknown): string;
|
|
6
|
+
export declare function CrashPlaceholder({ id, title, error, live }: {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
error: unknown;
|
|
10
|
+
live: "section" | "page";
|
|
11
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { CRASHED_ATTR, NODE_ATTR } from "./marks.js";
|
|
3
|
+
/** The canvas placeholders' look — the unknown section's too (spec 2026-09-27 editor safety §5). */
|
|
4
|
+
export const PLACEHOLDER_STYLE = {
|
|
5
|
+
padding: "24px 16px", border: "1px dashed #b54708", color: "#b54708", font: "500 14px/1.4 system-ui, sans-serif", textAlign: "center",
|
|
6
|
+
};
|
|
7
|
+
/** Why it crashed, for the placeholder: the message, or the digest when a production build stripped the message. */
|
|
8
|
+
export function crashMessage(error) {
|
|
9
|
+
const digest = typeof error === "object" && error !== null ? error.digest : undefined;
|
|
10
|
+
if (typeof digest === "string")
|
|
11
|
+
return `the server log has the error (digest ${digest})`;
|
|
12
|
+
return error instanceof Error ? error.message : String(error);
|
|
13
|
+
}
|
|
14
|
+
export function CrashPlaceholder({ id, title, error, live }) {
|
|
15
|
+
return (_jsx("div", { [NODE_ATTR]: id, [CRASHED_ATTR]: live, style: PLACEHOLDER_STYLE, children: `Section “${title}” failed to render — ${crashMessage(error)}` }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from "react";
|
|
2
|
+
/** The paired component's own view call, set by pair() and the custom section — only for a plain-function view. */
|
|
3
|
+
export declare const RENDER_VIEW: unique symbol;
|
|
4
|
+
/** A view the guard may call: a plain function — not a client reference or memo/forwardRef ($$typeof), not a class. */
|
|
5
|
+
export declare function isPlainView(View: unknown): boolean;
|
|
6
|
+
export type GuardContext = {
|
|
7
|
+
id: string;
|
|
8
|
+
type: string;
|
|
9
|
+
title: string;
|
|
10
|
+
editor: boolean;
|
|
11
|
+
rethrow?: (error: unknown) => void;
|
|
12
|
+
};
|
|
13
|
+
type Settled = {
|
|
14
|
+
ok: true;
|
|
15
|
+
value: unknown;
|
|
16
|
+
} | {
|
|
17
|
+
ok: false;
|
|
18
|
+
error: unknown;
|
|
19
|
+
};
|
|
20
|
+
/** A section through the guard when its component offers the view call; any other component as an element, unguarded. */
|
|
21
|
+
export declare function renderGuarded(Component: ComponentType<any>, props: Record<string, unknown>, ctx: GuardContext): ReactNode;
|
|
22
|
+
/** An async section, awaited in place — no <Suspense>, so it renders where it always did. */
|
|
23
|
+
export declare function SectionGuard({ settled, ctx }: {
|
|
24
|
+
settled: Promise<Settled>;
|
|
25
|
+
ctx: GuardContext;
|
|
26
|
+
}): Promise<ReactNode>;
|
|
27
|
+
export {};
|