smoodly 0.0.51 → 0.0.52

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 (285) hide show
  1. package/README.md +14 -13
  2. package/dist/admin/auth-config.d.ts +6 -5
  3. package/dist/admin/auth.d.ts +3 -11
  4. package/dist/admin/auth.js +1 -30
  5. package/dist/admin/browser/fetch-op.js +3 -2
  6. package/dist/admin/browser/main.js +6 -2
  7. package/dist/admin/client-ops.d.ts +5 -4
  8. package/dist/admin/client-ops.js +14 -11
  9. package/dist/admin/delivery-mode.d.ts +1 -1
  10. package/dist/admin/delivery-mode.js +2 -2
  11. package/dist/admin/drift.d.ts +0 -2
  12. package/dist/admin/drift.js +1 -1
  13. package/dist/admin/editor/EditorView.d.ts +0 -3
  14. package/dist/admin/editor/EditorView.js +11 -16
  15. package/dist/admin/editor/FloatingToolbar.d.ts +0 -1
  16. package/dist/admin/editor/FloatingToolbar.js +1 -1
  17. package/dist/admin/editor/SeoSettings.d.ts +0 -2
  18. package/dist/admin/editor/SeoSettings.js +2 -2
  19. package/dist/admin/editor/canvas-address.d.ts +8 -0
  20. package/dist/admin/editor/canvas-address.js +18 -2
  21. package/dist/admin/editor/canvas-diagnosis.d.ts +2 -0
  22. package/dist/admin/editor/canvas-diagnosis.js +3 -0
  23. package/dist/admin/editor/inline-dom.d.ts +0 -4
  24. package/dist/admin/editor/inline-dom.js +1 -1
  25. package/dist/admin/editor/preview-grant.d.ts +6 -0
  26. package/dist/admin/editor/preview-grant.js +25 -0
  27. package/dist/admin/editor/useCanvasDiagnosis.js +2 -2
  28. package/dist/admin/files.generated.js +1 -1
  29. package/dist/admin/fixed-nodes.d.ts +0 -6
  30. package/dist/admin/fixed-nodes.js +1 -81
  31. package/dist/admin/forms/CropEditor.d.ts +4 -2
  32. package/dist/admin/forms/CropEditor.js +58 -5
  33. package/dist/admin/forms/ImagePreview.d.ts +3 -1
  34. package/dist/admin/forms/ImagePreview.js +9 -3
  35. package/dist/admin/forms/MediaGrid.js +2 -1
  36. package/dist/admin/forms/MediaPicker.js +9 -6
  37. package/dist/admin/forms/MediaWidget.js +11 -9
  38. package/dist/admin/forms/assets-context.d.ts +7 -3
  39. package/dist/admin/forms/assets-context.js +5 -3
  40. package/dist/admin/forms/crop-editor.d.ts +24 -2
  41. package/dist/admin/forms/crop-editor.js +36 -2
  42. package/dist/admin/forms/date-picker.d.ts +0 -1
  43. package/dist/admin/forms/date-picker.js +1 -1
  44. package/dist/admin/forms/tabs.d.ts +0 -1
  45. package/dist/admin/forms/tabs.js +1 -1
  46. package/dist/admin/forms/upload.d.ts +18 -11
  47. package/dist/admin/forms/upload.js +62 -24
  48. package/dist/admin/global-items.d.ts +0 -3
  49. package/dist/admin/global-items.js +0 -54
  50. package/dist/admin/handler.d.ts +22 -28
  51. package/dist/admin/handler.js +146 -47
  52. package/dist/admin/health.d.ts +0 -4
  53. package/dist/admin/index.d.ts +5 -8
  54. package/dist/admin/index.js +3 -6
  55. package/dist/admin/ops.d.ts +36 -22
  56. package/dist/admin/page-data.d.ts +14 -6
  57. package/dist/admin/page-data.js +1 -1
  58. package/dist/admin/session-flow.d.ts +43 -0
  59. package/dist/admin/session-flow.js +94 -0
  60. package/dist/admin/shell/AdminApp.d.ts +8 -3
  61. package/dist/admin/shell/AdminApp.js +37 -58
  62. package/dist/admin/shell/ContentHealthPage.js +1 -3
  63. package/dist/admin/shell/GlobalFields.d.ts +0 -3
  64. package/dist/admin/shell/GlobalFields.js +1 -1
  65. package/dist/admin/shell/IconRail.js +5 -4
  66. package/dist/admin/shell/MediaScreen.js +9 -6
  67. package/dist/admin/shell/MembersPage.d.ts +1 -2
  68. package/dist/admin/shell/MembersPage.js +19 -90
  69. package/dist/admin/shell/SettingsView.d.ts +1 -2
  70. package/dist/admin/shell/SettingsView.js +2 -2
  71. package/dist/admin/shell/SignedOutView.d.ts +5 -0
  72. package/dist/admin/shell/SignedOutView.js +9 -0
  73. package/dist/admin/shell/console-links.d.ts +5 -0
  74. package/dist/admin/shell/console-links.js +11 -0
  75. package/dist/admin/shell/members-list.d.ts +1 -14
  76. package/dist/admin/shell/members-list.js +2 -23
  77. package/dist/admin/shell/pages-tree.d.ts +0 -2
  78. package/dist/admin/shell/pages-tree.js +1 -1
  79. package/dist/admin/shell/presence-socket.d.ts +10 -0
  80. package/dist/admin/shell/presence-socket.js +191 -0
  81. package/dist/admin/shell/presence.d.ts +6 -18
  82. package/dist/admin/shell/presence.js +4 -11
  83. package/dist/admin/shell/session.d.ts +20 -12
  84. package/dist/admin/shell/session.js +113 -47
  85. package/dist/admin/ui/LocaleSwitcher.d.ts +0 -4
  86. package/dist/admin/ui/LocaleSwitcher.js +2 -2
  87. package/dist/admin/ui/primitives.d.ts +4 -14
  88. package/dist/admin/ui/primitives.js +5 -8
  89. package/dist/admin/ui/theme.js +13 -2
  90. package/dist/admin/validate.js +10 -1
  91. package/dist/api-client.d.ts +26 -0
  92. package/dist/api-client.js +107 -0
  93. package/dist/assemble.d.ts +4 -41
  94. package/dist/assemble.js +4 -302
  95. package/dist/asset-store.d.ts +28 -30
  96. package/dist/asset-store.js +26 -121
  97. package/dist/astro/components/Head.astro +4 -1
  98. package/dist/astro/components/Image.astro +12 -17
  99. package/dist/astro/components/Video.astro +5 -2
  100. package/dist/astro/head.d.ts +3 -0
  101. package/dist/astro/head.js +24 -0
  102. package/dist/astro/index.js +54 -25
  103. package/dist/astro/locale-paths.d.ts +5 -2
  104. package/dist/astro/locale-paths.js +10 -2
  105. package/dist/astro/routes/admin.js +9 -7
  106. package/dist/astro/runtime.d.ts +2 -1
  107. package/dist/astro/runtime.js +4 -3
  108. package/dist/astro/server.d.ts +7 -11
  109. package/dist/astro/server.js +22 -28
  110. package/dist/astro/shell.d.ts +2 -2
  111. package/dist/astro/static-media-node.d.ts +4 -0
  112. package/dist/astro/static-media-node.js +25 -0
  113. package/dist/astro/static-media.d.ts +17 -0
  114. package/dist/astro/static-media.js +55 -0
  115. package/dist/astro/static.d.ts +5 -10
  116. package/dist/astro/static.js +13 -29
  117. package/dist/canvas-marker.d.ts +14 -0
  118. package/dist/canvas-marker.js +36 -0
  119. package/dist/cli/api.d.ts +14 -0
  120. package/dist/cli/api.js +48 -0
  121. package/dist/cli/app-env.d.ts +2 -0
  122. package/dist/cli/app-env.js +20 -0
  123. package/dist/cli/export.d.ts +16 -2
  124. package/dist/cli/export.js +52 -35
  125. package/dist/cli/import.d.ts +7 -11
  126. package/dist/cli/import.js +50 -54
  127. package/dist/cli/load-config.d.ts +0 -1
  128. package/dist/cli/load-config.js +1 -1
  129. package/dist/cli/main.d.ts +4 -12
  130. package/dist/cli/main.js +26 -72
  131. package/dist/cli/push.d.ts +6 -0
  132. package/dist/cli/push.js +31 -0
  133. package/dist/collections.d.ts +0 -1
  134. package/dist/collections.js +1 -1
  135. package/dist/config.d.ts +20 -5
  136. package/dist/config.js +18 -1
  137. package/dist/content/content-api.d.ts +22 -23
  138. package/dist/content/content-api.js +46 -56
  139. package/dist/content/mime.d.ts +1 -0
  140. package/dist/content/mime.js +6 -0
  141. package/dist/deploy-store.d.ts +19 -0
  142. package/dist/deploy-store.js +1 -0
  143. package/dist/entry-store.d.ts +3 -89
  144. package/dist/entry-store.js +2 -550
  145. package/dist/env.d.ts +27 -20
  146. package/dist/env.js +38 -50
  147. package/dist/fields.d.ts +11 -3
  148. package/dist/fields.js +33 -2
  149. package/dist/global-id.d.ts +2 -0
  150. package/dist/global-id.js +4 -2
  151. package/dist/image/SmoodlyImage.d.ts +9 -5
  152. package/dist/image/SmoodlyImage.js +29 -15
  153. package/dist/image/SmoodlyVideo.d.ts +1 -1
  154. package/dist/image/SmoodlyVideo.js +3 -2
  155. package/dist/image/index.d.ts +2 -3
  156. package/dist/image/index.js +0 -2
  157. package/dist/image/markup.d.ts +53 -0
  158. package/dist/image/markup.js +111 -0
  159. package/dist/index.d.ts +12 -22
  160. package/dist/index.js +7 -20
  161. package/dist/media-cut.d.ts +44 -0
  162. package/dist/media-cut.js +70 -0
  163. package/dist/media.d.ts +57 -3
  164. package/dist/media.js +125 -15
  165. package/dist/member-store.d.ts +3 -22
  166. package/dist/member-store.js +1 -53
  167. package/dist/next/admin-route.d.ts +1 -1
  168. package/dist/next/admin-route.js +5 -3
  169. package/dist/next/meta.js +2 -3
  170. package/dist/next/page-renderer.d.ts +2 -2
  171. package/dist/next/page-renderer.js +22 -11
  172. package/dist/next/queries.d.ts +2 -2
  173. package/dist/path-index.d.ts +0 -24
  174. package/dist/path-index.js +0 -98
  175. package/dist/push.d.ts +30 -0
  176. package/dist/push.js +52 -0
  177. package/dist/read-counter.d.ts +2 -0
  178. package/dist/read-counter.js +6 -0
  179. package/dist/reads.d.ts +7 -7
  180. package/dist/reads.js +3 -0
  181. package/dist/registry-json.d.ts +11 -0
  182. package/dist/registry-json.js +81 -0
  183. package/dist/render.d.ts +0 -1
  184. package/dist/render.js +1 -1
  185. package/dist/resolve.d.ts +12 -6
  186. package/dist/resolve.js +11 -8
  187. package/dist/revalidate.d.ts +8 -1
  188. package/dist/revalidate.js +8 -1
  189. package/dist/richtext/index.d.ts +1 -1
  190. package/dist/section-wrapper.d.ts +1 -1
  191. package/dist/section-wrapper.js +0 -9
  192. package/dist/signed-message.d.ts +26 -0
  193. package/dist/signed-message.js +103 -0
  194. package/dist/site.d.ts +59 -18
  195. package/dist/site.js +21 -252
  196. package/dist/space-transfer.d.ts +5 -2
  197. package/dist/space-transfer.js +17 -5
  198. package/dist/store.d.ts +2 -80
  199. package/dist/store.js +3 -489
  200. package/dist/toolset.d.ts +2 -1
  201. package/dist/toolset.js +1 -1
  202. package/dist/version.d.ts +1 -1
  203. package/dist/version.js +1 -1
  204. package/package.json +4 -15
  205. package/dist/admin/ops-impl.d.ts +0 -51
  206. package/dist/admin/ops-impl.js +0 -1403
  207. package/dist/admin/shell/InviteModal.d.ts +0 -6
  208. package/dist/admin/shell/InviteModal.js +0 -28
  209. package/dist/admin/shell/LoginView.d.ts +0 -5
  210. package/dist/admin/shell/LoginView.js +0 -80
  211. package/dist/admin/shell/SchemaNotice.d.ts +0 -4
  212. package/dist/admin/shell/SchemaNotice.js +0 -4
  213. package/dist/admin/shell/SetPasswordView.d.ts +0 -7
  214. package/dist/admin/shell/SetPasswordView.js +0 -41
  215. package/dist/admin/shell/presence-realtime.d.ts +0 -3
  216. package/dist/admin/shell/presence-realtime.js +0 -32
  217. package/dist/admin/supabase-auth.d.ts +0 -7
  218. package/dist/admin/supabase-auth.js +0 -30
  219. package/dist/astro/deploy-deps.d.ts +0 -10
  220. package/dist/astro/deploy-deps.js +0 -8
  221. package/dist/astro/image.d.ts +0 -27
  222. package/dist/astro/image.js +0 -29
  223. package/dist/bind.d.ts +0 -19
  224. package/dist/bind.js +0 -34
  225. package/dist/cli/add-admin.d.ts +0 -11
  226. package/dist/cli/add-admin.js +0 -25
  227. package/dist/cli/load-collections.d.ts +0 -2
  228. package/dist/cli/load-collections.js +0 -27
  229. package/dist/cli/migrate.d.ts +0 -43
  230. package/dist/cli/migrate.js +0 -138
  231. package/dist/cli/rebuild.d.ts +0 -23
  232. package/dist/cli/rebuild.js +0 -22
  233. package/dist/cli/space-io.d.ts +0 -41
  234. package/dist/cli/space-io.js +0 -84
  235. package/dist/client.d.ts +0 -11
  236. package/dist/client.js +0 -54
  237. package/dist/current-space.d.ts +0 -4
  238. package/dist/current-space.js +0 -9
  239. package/dist/document-stamp.d.ts +0 -7
  240. package/dist/document-stamp.js +0 -48
  241. package/dist/image/loaders.d.ts +0 -15
  242. package/dist/image/loaders.js +0 -5
  243. package/dist/image/next-config.d.ts +0 -18
  244. package/dist/image/next-config.js +0 -32
  245. package/dist/image/remote-pattern.d.ts +0 -1
  246. package/dist/image/remote-pattern.js +0 -5
  247. package/dist/migrations.d.ts +0 -37
  248. package/dist/migrations.js +0 -73
  249. package/dist/project.d.ts +0 -88
  250. package/dist/project.js +0 -231
  251. package/dist/published.d.ts +0 -142
  252. package/dist/published.js +0 -164
  253. package/dist/ref-index.d.ts +0 -19
  254. package/dist/ref-index.js +0 -0
  255. package/dist/schema-read.d.ts +0 -23
  256. package/dist/schema-read.js +0 -50
  257. package/dist/schema-version.d.ts +0 -19
  258. package/dist/schema-version.js +0 -34
  259. package/dist/sql-space.d.ts +0 -44
  260. package/dist/sql-space.js +0 -289
  261. package/dist/sql.d.ts +0 -15
  262. package/dist/sql.js +0 -556
  263. package/dist/supabase-asset-store.d.ts +0 -21
  264. package/dist/supabase-asset-store.js +0 -170
  265. package/dist/supabase-entry-store.d.ts +0 -92
  266. package/dist/supabase-entry-store.js +0 -824
  267. package/dist/supabase-member-store.d.ts +0 -21
  268. package/dist/supabase-member-store.js +0 -96
  269. package/dist/supabase-path-index.d.ts +0 -30
  270. package/dist/supabase-path-index.js +0 -138
  271. package/dist/supabase-published-store.d.ts +0 -56
  272. package/dist/supabase-published-store.js +0 -314
  273. package/dist/supabase-store.d.ts +0 -103
  274. package/dist/supabase-store.js +0 -678
  275. package/migrations/20260926120000_smoodly_v1.sql +0 -535
  276. package/migrations/20260926130000_smoodly_v2.sql +0 -26
  277. package/migrations/20260926140000_smoodly_v3.sql +0 -38
  278. package/migrations/20260926150000_smoodly_v4.sql +0 -41
  279. package/migrations/20260929120000_smoodly_v5.sql +0 -56
  280. package/migrations/20260929135523_smoodly_v6.sql +0 -200
  281. package/migrations/20260930040956_smoodly_v7.sql +0 -84
  282. package/migrations/20261007120000_smoodly_v8.sql +0 -154
  283. package/migrations/20261007200000_smoodly_v9.sql +0 -30
  284. package/migrations/20261008120000_smoodly_v10.sql +0 -47
  285. package/migrations/20261009120000_smoodly_v11.sql +0 -49
@@ -3,7 +3,7 @@ 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
- import { type SmoodlySite } from "../site.ts";
6
+ import { type ContentReads } from "../site.ts";
7
7
  /** Logged once per renderer when Next's cacheComponents is on (spec 2026-09-27 cache components §2). */
8
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).";
9
9
  /** A registered section, as renderPage's registry needs it. */
@@ -22,7 +22,7 @@ export type PageRegistration = ((props: any) => ReactElement) & {
22
22
  };
23
23
  };
24
24
  export type PageRendererOptions = {
25
- site: SmoodlySite;
25
+ site: ContentReads;
26
26
  sections: SectionEntry[];
27
27
  /** Registered Elements: what the custom section's default richtext renderer needs when no RichText is bound (spec 2026-09-14 §6). */
28
28
  elements?: SectionEntry[];
@@ -8,6 +8,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
8
8
  import { Suspense } from "react";
9
9
  import { draftMode } from "next/headers";
10
10
  import { notFound, permanentRedirect, unstable_rethrow } from "next/navigation";
11
+ import { markCanvasPath, readCanvasMarker } from "../canvas-marker.js";
11
12
  import { BRIDGE_CSS } from "../marks.js";
12
13
  import { CanvasBridgeSlot, CanvasNetSlot } from "./canvas-slot.js";
13
14
  import { DRAFT_META_NAME } from "../admin/editor/canvas-diagnosis.js";
@@ -36,8 +37,9 @@ export function createPageRenderer(options) {
36
37
  * The hit for a located target (locate is uncached: the id it returns IS the cache tag): the read
37
38
  * layer's unit, run by Next's adapter — the draft inside draft mode, one cached unit outside it. */
38
39
  const load = (target, locale, path, version) => nextReads.run(hitUnit(site, target, locale, path, version));
39
- /** A redirect row (spec 2026-09-26 redirects §7): the node's CURRENT path in this locale, through href, the query kept; no path there is a 404. */
40
- async function redirectTo(target, locale, params) {
40
+ /** A redirect row (spec 2026-09-26 redirects §7): the node's CURRENT path in this locale, through href, the query
41
+ * and the canvas marker kept; no path there is a 404. */
42
+ async function redirectTo(target, locale, params, flags) {
41
43
  const current = (await site.targetPaths(target))[locale];
42
44
  if (!current)
43
45
  notFound();
@@ -50,7 +52,8 @@ export function createPageRenderer(options) {
50
52
  query.append(k, item);
51
53
  }
52
54
  const qs = query.toString();
53
- permanentRedirect(href(current, locale) + (qs ? `?${qs}` : ""));
55
+ const to = href(current, locale) + (qs ? `?${qs}` : "");
56
+ permanentRedirect(flags ? markCanvasPath(to, flags) : to);
54
57
  }
55
58
  function render(hit, editor, locale) {
56
59
  if (hit.kind === "page") {
@@ -94,28 +97,32 @@ export function createPageRenderer(options) {
94
97
  cacheComponentsWarned = true;
95
98
  console.warn(CACHE_COMPONENTS_WARNING);
96
99
  }
97
- async function page(pathFor, opts, accept) {
100
+ async function page(address, opts, accept) {
98
101
  warnCacheComponents(); // before buildGate(), so a build's prerender prints it too
99
102
  // A CMS route renders on its first visit and is cached until a tag expires it (plan 2026-10-09 next
100
103
  // cached routes): nothing here may touch a dynamic API at run time. At build, the gate keeps a route
101
104
  // without a dynamic segment off the database (DESIGN.md §3).
102
105
  await buildGate();
103
106
  const locale = opts?.locale ?? site.locales.default;
104
- const path = pathFor(locale);
105
107
  const { isEnabled: draft } = await draftMode();
108
+ const { pathFor, marker } = address(draft);
109
+ const path = pathFor(locale);
106
110
  // searchParams is a dynamic API: read inside draft mode only, where the render is dynamic anyway.
107
111
  const params = draft ? await opts?.searchParams : undefined;
108
- // Both parameters exist only inside draft mode: the public site never sees an editor bridge or an old version.
109
- const editor = draft && params?.["smoodly-editor"] === "1";
112
+ // The flags exist only inside draft mode: the public site never sees an editor bridge or an old version. The
113
+ // canvas marks the path (Vercel strips the query); the query still works where a host keeps it.
110
114
  const versionParam = params?.["smoodly-version"];
111
- const version = draft && typeof versionParam === "string" && versionParam !== "" ? versionParam : undefined;
115
+ const editor = draft && (marker?.editor === true || params?.["smoodly-editor"] === "1");
116
+ const version = !draft ? undefined
117
+ : marker?.version ?? (typeof versionParam === "string" && versionParam !== "" ? versionParam : undefined);
118
+ const flags = marker?.editor ? { editor: true } : marker?.version ? { version: marker.version } : null;
112
119
  const target = await site.locate(locale, path);
113
120
  // No document to tag: a redirect row, a miss or a code-owned page. Next caches the response like a
114
121
  // page, so it hears the addresses tag (reads.ts missUnit, decision 4).
115
122
  const tagAddress = () => nextReads.run(missUnit(site));
116
123
  if (target?.redirect) {
117
124
  await tagAddress();
118
- await redirectTo(target, locale, params);
125
+ await redirectTo(target, locale, params, flags);
119
126
  }
120
127
  if (!target)
121
128
  await tagAddress();
@@ -168,10 +175,14 @@ export function createPageRenderer(options) {
168
175
  }
169
176
  return {
170
177
  getSmoodlyGlobal,
171
- renderSmoodlyPath: (segments, opts) => page(literal(segments), opts, any),
178
+ renderSmoodlyPath: (segments, opts) => page((draft) => {
179
+ // The catch-all's segments may end with the canvas marker; only a draft-mode render reads it.
180
+ const marker = draft ? readCanvasMarker(segments) : null;
181
+ return { pathFor: literal(marker ? marker.segments : segments), marker };
182
+ }, opts, any),
172
183
  smoodlyMetadata: (segments, opts) => metadata(literal(segments), opts, any),
173
184
  /** A fixed registration rendered from the developer's own route file. */
174
- renderSmoodlyPage: (Page, opts) => page(fixedPath(Page), opts, only(Page)),
185
+ renderSmoodlyPage: (Page, opts) => page(() => ({ pathFor: fixedPath(Page), marker: null }), opts, only(Page)),
175
186
  smoodlyPageMetadata: (Page, opts) => metadata(fixedPath(Page), opts, only(Page)),
176
187
  };
177
188
  }
@@ -1,10 +1,10 @@
1
1
  import type { CollectionSchema } from "../collections.ts";
2
2
  import type { GlobalSchema } from "../global.ts";
3
3
  import { type SmoodlyQueries } from "../reads.ts";
4
- import type { SmoodlySite } from "../site.ts";
4
+ import type { ContentReads } from "../site.ts";
5
5
  export type { SmoodlyQueries, QueryOptions, EntriesQuery, TypedEntryItem, GlobalValue } from "../reads.ts";
6
6
  export declare function createSmoodlyQueries<const C extends readonly CollectionSchema<any, string>[], const S extends readonly GlobalSchema<any, string>[] = []>(options: {
7
- site: SmoodlySite;
7
+ site: ContentReads;
8
8
  collections: C;
9
9
  globals?: S;
10
10
  }): SmoodlyQueries<C, S>;
@@ -1,5 +1,4 @@
1
1
  import type { PathRow, PathTarget } from "./paths.ts";
2
- import { type Anchors } from "./anchors.ts";
3
2
  export type { PathRow, PathTarget } from "./paths.ts";
4
3
  /** A rewrite target; `redirectLocales` names the locales whose old live rows stay as redirects (none = drop them). */
5
4
  export type RewriteTarget = PathTarget & {
@@ -29,26 +28,3 @@ export interface PathIndex {
29
28
  }
30
29
  export declare const pathTakenError: (locale: string, path: string) => Error;
31
30
  export declare const pathLiveError: (locale: string, path: string) => Error;
32
- export declare class MemoryPathIndex implements PathIndex {
33
- private anchors;
34
- private rows;
35
- /** Held by reference: the config's addresses, read on every call (spec 2026-09-29 anchored paths §2). */
36
- constructor(anchors?: Anchors);
37
- /** A stored row as callers see it: the full URL, or nothing when its anchor has no address. */
38
- private shown;
39
- private shownAll;
40
- rewrite(targets: RewriteTarget[], rows: PathRow[]): Promise<void>;
41
- remove(targets: PathTarget[]): Promise<void>;
42
- private hit;
43
- resolve(locale: string, path: string): Promise<PathHit | null>;
44
- pathsOf(target: PathTarget): Promise<Record<string, string>>;
45
- livePathsIn(kind: "page" | "entry", ids: string[], locale: string): Promise<Record<string, string>>;
46
- redirectsOf(target: PathTarget): Promise<PathRow[]>;
47
- /** Every live URL in the locale; an editor's row under a code address is left out, since visitors never reach it (spec §4). */
48
- listLive(locale: string): Promise<PathRow[]>;
49
- listRedirects(locale: string): Promise<PathRow[]>;
50
- /** A redirect by hand stays absolute (anchor ''): refused where the URL resolves live; a redirect there is replaced. */
51
- addRedirect(row: PathRow): Promise<void>;
52
- /** Deletes the redirect rows a URL reaches, under any candidate anchor; a live row there is left alone. */
53
- removeRedirect(locale: string, path: string): Promise<void>;
54
- }
@@ -1,100 +1,2 @@
1
- import { candidates, fullPath, pickHit, sameUrlKeys } from "./anchors.js";
2
- const sameTarget = (a, b) => a.kind === b.kind && a.id === b.id;
3
1
  export const pathTakenError = (locale, path) => new Error(`smoodly: the path "${path}" (${locale}) is already taken.`);
4
2
  export const pathLiveError = (locale, path) => new Error(`smoodly: "${path}" (${locale}) is a live address.`);
5
- const key = (locale, anchor, path) => `${locale} ${anchor} ${path}`;
6
- const stored = (r) => ({ kind: r.kind, id: r.id, locale: r.locale, anchor: r.anchor ?? "", path: r.path, redirect: false });
7
- export class MemoryPathIndex {
8
- /** Held by reference: the config's addresses, read on every call (spec 2026-09-29 anchored paths §2). */
9
- constructor(anchors = { addresses: {}, homePageSlug: "home", defaultLocale: "en" }) {
10
- this.anchors = anchors;
11
- this.rows = [];
12
- }
13
- /** A stored row as callers see it: the full URL, or nothing when its anchor has no address. */
14
- shown(r) {
15
- const path = fullPath(this.anchors, r.anchor, r.path, r.locale);
16
- return path === null ? null : { kind: r.kind, id: r.id, locale: r.locale, path };
17
- }
18
- shownAll(rows) {
19
- return rows.map((r) => this.shown(r)).filter((r) => r !== null);
20
- }
21
- async rewrite(targets, rows) {
22
- const incoming = rows.map(stored);
23
- const ofTarget = (r) => targets.find((t) => sameTarget(t, r));
24
- // Every key a new row's full URL answers to: live wins a redirect there under any anchor (a redirect by
25
- // hand has none), and an old row at the same full URL is the same address, not an old one.
26
- const newKeys = new Set(incoming.flatMap((r) => sameUrlKeys(this.anchors, r.locale, r.anchor, r.path).map((k) => key(r.locale, k.anchor, k.path))));
27
- // 1. redirect rows at the new live addresses go, whoever they point at (live wins, rename-back reclaims)
28
- let kept = this.rows.filter((r) => !(r.redirect && newKeys.has(key(r.locale, r.anchor, r.path))));
29
- // 2. the targets' old live rows: flipped to redirects in the named locales unless the address stays live
30
- kept = kept.map((r) => {
31
- const t = !r.redirect ? ofTarget(r) : undefined;
32
- return t && t.redirectLocales?.includes(r.locale) && !newKeys.has(key(r.locale, r.anchor, r.path)) ? { ...r, redirect: true } : r;
33
- });
34
- // 3. the targets' remaining live rows go — and so do their redirect rows in a locale the target has no new live row in
35
- const liveLocales = new Set(incoming.map((r) => `${r.kind} ${r.id} ${r.locale}`));
36
- kept = kept.filter((r) => !ofTarget(r) || (r.redirect && liveLocales.has(`${r.kind} ${r.id} ${r.locale}`)));
37
- // 4. the new live rows: a collision now is with another target's live row under the same anchor
38
- const taken = new Set(kept.map((r) => key(r.locale, r.anchor, r.path)));
39
- for (const r of incoming) {
40
- const k = key(r.locale, r.anchor, r.path);
41
- if (taken.has(k))
42
- throw pathTakenError(r.locale, fullPath(this.anchors, r.anchor, r.path, r.locale) ?? r.path);
43
- taken.add(k);
44
- }
45
- this.rows = [...kept, ...incoming];
46
- }
47
- async remove(targets) {
48
- this.rows = this.rows.filter((r) => !targets.some((t) => sameTarget(t, r)));
49
- }
50
- hit(locale, url) {
51
- const { keys, coded } = candidates(this.anchors, locale, url);
52
- return pickHit(this.rows.filter((r) => r.locale === locale), keys, coded);
53
- }
54
- async resolve(locale, path) {
55
- const row = this.hit(locale, path);
56
- return row ? { kind: row.kind, id: row.id, redirect: row.redirect } : null;
57
- }
58
- async pathsOf(target) {
59
- const out = {};
60
- for (const r of this.shownAll(this.rows.filter((r) => !r.redirect && sameTarget(r, target))))
61
- out[r.locale] = r.path;
62
- return out;
63
- }
64
- async livePathsIn(kind, ids, locale) {
65
- const wanted = new Set(ids);
66
- const out = {};
67
- for (const r of this.shownAll(this.rows.filter((r) => !r.redirect && r.kind === kind && r.locale === locale && wanted.has(r.id))))
68
- out[r.id] = r.path;
69
- return out;
70
- }
71
- async redirectsOf(target) {
72
- return this.shownAll(this.rows.filter((r) => r.redirect && sameTarget(r, target)));
73
- }
74
- /** Every live URL in the locale; an editor's row under a code address is left out, since visitors never reach it (spec §4). */
75
- async listLive(locale) {
76
- const served = (r) => !(r.anchor === "" && candidates(this.anchors, locale, r.path).coded);
77
- return this.shownAll(this.rows.filter((r) => !r.redirect && r.locale === locale && served(r))).sort((a, b) => a.path.localeCompare(b.path));
78
- }
79
- async listRedirects(locale) {
80
- return this.shownAll(this.rows.filter((r) => r.redirect && r.locale === locale)).sort((a, b) => a.path.localeCompare(b.path));
81
- }
82
- /** A redirect by hand stays absolute (anchor ''): refused where the URL resolves live; a redirect there is replaced. */
83
- async addRedirect(row) {
84
- const existing = this.hit(row.locale, row.path);
85
- if (existing && !existing.redirect)
86
- throw pathLiveError(row.locale, row.path);
87
- // An editor's page hidden under a code address still holds its row: a redirect never stacks on it.
88
- if (this.rows.some((r) => !r.redirect && r.locale === row.locale && r.anchor === "" && r.path === row.path))
89
- throw pathLiveError(row.locale, row.path);
90
- // Every redirect the URL reaches goes, under any anchor: the new one replaces them all.
91
- const { keys } = candidates(this.anchors, row.locale, row.path);
92
- const replaced = (r) => r.redirect && r.locale === row.locale && keys.some((k) => k.anchor === r.anchor && k.path === r.path);
93
- this.rows = [...this.rows.filter((r) => !replaced(r)), { ...stored({ ...row, anchor: "" }), redirect: true }];
94
- }
95
- /** Deletes the redirect rows a URL reaches, under any candidate anchor; a live row there is left alone. */
96
- async removeRedirect(locale, path) {
97
- const { keys } = candidates(this.anchors, locale, path);
98
- this.rows = this.rows.filter((r) => !(r.redirect && r.locale === locale && keys.some((k) => k.anchor === r.anchor && k.path === r.path)));
99
- }
100
- }
package/dist/push.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { ResolvedConfig } from "./config.ts";
2
+ import { type RegistryJson } from "./registry-json.ts";
3
+ /** The pushing site: where it lives, where its admin is mounted, how it delivers, and whether the host says this build
4
+ * is a preview deployment (`transient`: its admin works, but a publish never calls it). */
5
+ export type SiteInfo = {
6
+ url: string;
7
+ adminPath: string;
8
+ mode: "live" | "static";
9
+ deployHook?: string;
10
+ transient: boolean;
11
+ };
12
+ export type PushBody = {
13
+ registry: RegistryJson;
14
+ site: SiteInfo | null;
15
+ package: string;
16
+ };
17
+ /** The site's address from SITE_URL; null when it is unset (the space then records no address). */
18
+ export declare function siteInfo(config: ResolvedConfig, env: Record<string, string | undefined>, mode: "live" | "static"): SiteInfo | null;
19
+ export declare function pushBody(config: ResolvedConfig, env: Record<string, string | undefined>, mode: "live" | "static"): PushBody;
20
+ export type PushApi = {
21
+ apiUrl: string;
22
+ spaceId: string;
23
+ key: string;
24
+ fetch?: typeof fetch;
25
+ };
26
+ /** One push: the space's version of the registry, and whether this push changed it. A refusal throws `SmoodlyApiError`. */
27
+ export declare function pushSchema(api: PushApi, body: PushBody): Promise<{
28
+ version: number;
29
+ changed: boolean;
30
+ }>;
package/dist/push.js ADDED
@@ -0,0 +1,52 @@
1
+ // The push (spec 2026-10-16 the control plane §4): a site tells its space the registry it runs, its own address and
2
+ // delivery mode, and the package version, at `POST /v1/spaces/:space/schema` under the secret key. `smoodly push`, Astro's
3
+ // build hook, the Next templates' prebuild and the API client in development all send this one body. The space keeps
4
+ // the registry, versions it and purges its pages when it changed; a failed push never fails a build.
5
+ import { SmoodlyApiError } from "./api-client.js";
6
+ import { serializeRegistry } from "./registry-json.js";
7
+ import { SMOODLY_VERSION } from "./version.js";
8
+ /** A preview deployment, as the three hosts the spec names say it: Vercel, Netlify, Cloudflare Pages. */
9
+ function isPreview(env) {
10
+ if (env.VERCEL_ENV === "preview")
11
+ return true;
12
+ if (env.CONTEXT === "deploy-preview")
13
+ return true;
14
+ return !!env.CF_PAGES_BRANCH && !!env.CF_PAGES_PRODUCTION_BRANCH && env.CF_PAGES_BRANCH !== env.CF_PAGES_PRODUCTION_BRANCH;
15
+ }
16
+ /** The site's address from SITE_URL; null when it is unset (the space then records no address). */
17
+ export function siteInfo(config, env, mode) {
18
+ const url = env.SITE_URL?.replace(/\/+$/, "");
19
+ if (!url)
20
+ return null;
21
+ return {
22
+ url,
23
+ adminPath: config.admin.path,
24
+ mode,
25
+ ...(env.SMOODLY_DEPLOY_HOOK ? { deployHook: env.SMOODLY_DEPLOY_HOOK } : {}),
26
+ transient: isPreview(env),
27
+ };
28
+ }
29
+ export function pushBody(config, env, mode) {
30
+ return { registry: serializeRegistry(config), site: siteInfo(config, env, mode), package: SMOODLY_VERSION };
31
+ }
32
+ /** One push: the space's version of the registry, and whether this push changed it. A refusal throws `SmoodlyApiError`. */
33
+ export async function pushSchema(api, body) {
34
+ const apiUrl = api.apiUrl.replace(/\/+$/, "");
35
+ const doFetch = api.fetch ?? fetch;
36
+ let res;
37
+ try {
38
+ res = await doFetch(`${apiUrl}/v1/spaces/${encodeURIComponent(api.spaceId)}/schema`, {
39
+ method: "POST",
40
+ headers: { authorization: `Bearer ${api.key}`, "content-type": "application/json", accept: "application/json" },
41
+ body: JSON.stringify(body),
42
+ });
43
+ }
44
+ catch (e) {
45
+ throw new TypeError(`smoodly: can't reach SMOODLY_API_URL (${apiUrl}): ${e instanceof Error ? e.message : String(e)}`);
46
+ }
47
+ const answer = (await res.json().catch(() => null));
48
+ if (!answer || !answer.ok) {
49
+ throw new SmoodlyApiError(res.status, answer?.code ?? "internal", answer?.message ?? `smoodly: SMOODLY_API_URL (${apiUrl}) answered ${res.status} with no message.`);
50
+ }
51
+ return { version: answer.data.version, changed: answer.data.changed };
52
+ }
@@ -0,0 +1,2 @@
1
+ /** One read for the adapters' counters: a database call in the service's client, an API call in api-client.ts. */
2
+ export declare function countRead(): void;
@@ -0,0 +1,6 @@
1
+ // Where a framework adapter hears each read (spec 2026-10-07 delivery model): the Astro request scope installs a counter, so a test can hold a live page to one lookup. A global, not an import: the client stays free of any adapter.
2
+ const READ_COUNTER = Symbol.for("smoodly:read-counter");
3
+ /** One read for the adapters' counters: a database call in the service's client, an API call in api-client.ts. */
4
+ export function countRead() {
5
+ globalThis[READ_COUNTER]?.();
6
+ }
package/dist/reads.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { GlobalSchema } from "./global.ts";
3
3
  import type { PathTarget } from "./paths.ts";
4
4
  import { type EntryItem, type PageItem } from "./read-shapes.ts";
5
5
  import type { Materialize } from "./schema.ts";
6
- import type { AddressWithGlobals, EntryQuery, PageTreeItem, SiteHit, SmoodlySite } from "./site.ts";
6
+ import type { AddressWithGlobals, ContentReads, EntryQuery, PageTreeItem, SiteHit } from "./site.ts";
7
7
  /** One content read, as the framework's cache needs it. */
8
8
  export type ReadUnit<T> = {
9
9
  /** What the published read holds: two calls with one key may share a cached `read(false)`. A draft read
@@ -56,17 +56,17 @@ export type SmoodlyQueries<C extends readonly CollectionSchema<any, string>[], S
56
56
  * what the unit holds, not only where: an address that changes owner — a deploy handing it to a code
57
57
  * record, which fires no tag — can never serve the previous occupant (spec 2026-09-29 pre-install fixes
58
58
  * §5). `version` is History's stored version and counts inside draft mode only. */
59
- export declare function hitUnit(site: SmoodlySite, target: PathTarget, locale: string, path: string, version?: string): ReadUnit<SiteHit | null>;
59
+ export declare function hitUnit(site: ContentReads, target: PathTarget, locale: string, path: string, version?: string): ReadUnit<SiteHit | null>;
60
60
  /** An address with no document to tag — a miss, a redirect row or a code-owned page. The unit reads
61
61
  * nothing: it exists so a cached response hears the `addresses` tag, which every address-changing
62
62
  * operation fires (revalidate.ts). Next runs it (its route cache stores a 404 like a page); Astro never
63
63
  * caches such a response and does not call it. One key for every address: the tag reaches the route
64
64
  * entry before the lookup, so a per-address key would only write one data-cache row per scanned 404. */
65
- export declare function missUnit(site: SmoodlySite): ReadUnit<null>;
65
+ export declare function missUnit(site: ContentReads): ReadUnit<null>;
66
66
  /** Astro's route read of an address: one lookup outside draft mode — the tags come with the document, so no
67
67
  * locate comes first; in draft mode, locate then load as the editing tables need (spec 2026-10-07 delivery
68
68
  * model §3). A redirect row answers as a redirect; a code-owned page answers where no record lives. */
69
- export declare function addressUnit(site: SmoodlySite, locale: string, path: string, version?: string): ReadUnit<SiteHit | {
69
+ export declare function addressUnit(site: ContentReads, locale: string, path: string, version?: string): ReadUnit<SiteHit | {
70
70
  kind: "redirect";
71
71
  target: PathTarget;
72
72
  } | null>;
@@ -74,12 +74,12 @@ export declare function addressUnit(site: SmoodlySite, locale: string, path: str
74
74
  * `lookupWithGlobals`, and the shell seeds each value into the request scope under `globalUnit`'s key, so the
75
75
  * layout's loaders cost no call. The tags are the hit's; a global's tag joins when the layout reads it. A draft
76
76
  * read brings no globals: a preview reads live. */
77
- export declare function addressWithGlobalsUnit(site: SmoodlySite, locale: string, path: string, items: readonly GlobalSchema<any>[], version?: string): ReadUnit<AddressWithGlobals>;
77
+ export declare function addressWithGlobalsUnit(site: ContentReads, locale: string, path: string, items: readonly GlobalSchema<any>[], version?: string): ReadUnit<AddressWithGlobals>;
78
78
  /** A global item's unit, by schema: the one `q.getGlobal` runs for the same item and locale. */
79
- export declare function globalUnit<V>(site: SmoodlySite, item: GlobalSchema<V>, locale: string): ReadUnit<V | null>;
79
+ export declare function globalUnit<V>(site: ContentReads, item: GlobalSchema<V>, locale: string): ReadUnit<V | null>;
80
80
  /** The typed queries over a site, run by the framework's adapter. */
81
81
  export declare function createQueries<const C extends readonly CollectionSchema<any, string>[], const S extends readonly GlobalSchema<any, string>[] = []>(options: {
82
- site: SmoodlySite;
82
+ site: ContentReads;
83
83
  collections: C;
84
84
  globals?: S;
85
85
  }, adapter: ReadAdapter): SmoodlyQueries<C, S>;
package/dist/reads.js CHANGED
@@ -51,6 +51,9 @@ export function addressUnit(site, locale, path, version) {
51
51
  function tagsOfHit(value) {
52
52
  if (!value || value.kind === "redirect")
53
53
  return [];
54
+ // A published document carries its delivery row's tags (its own and every ref it embeds).
55
+ if (value.tags)
56
+ return value.tags;
54
57
  if (value.kind === "entry")
55
58
  return [entryTag(value.entry.id)];
56
59
  // A code-owned page has no row to expire it by.
@@ -0,0 +1,11 @@
1
+ import { type ResolvedConfig } from "./config.ts";
2
+ export type RegistryJson = {
3
+ v: 1;
4
+ config: Record<string, unknown>;
5
+ };
6
+ /** The pushed form of a resolved config: its defineConfig input, as data. */
7
+ export declare function serializeRegistry(config: ResolvedConfig): RegistryJson;
8
+ /** A resolved config from its pushed form: the ref thunks resolve by name within the same registry. */
9
+ export declare function hydrateRegistry(json: RegistryJson): ResolvedConfig;
10
+ /** The registry's fingerprint: SHA-256 hex of its key-sorted JSON. */
11
+ export declare function registryHash(json: RegistryJson): Promise<string>;
@@ -0,0 +1,81 @@
1
+ // The registry as JSON (spec 2026-10-16 the control plane §4): what `smoodly push` carries to the space. It is the
2
+ // defineConfig input, rebuilt from the resolved config, with the only functions a config holds replaced: a ref's
3
+ // target thunk becomes the name of the collection it returns, and any other function (a view the config does not
4
+ // need) is dropped. Hydrating is defineConfig over the revived input, so the derived parts (addresses, anchors, the
5
+ // styles descriptors) come from the one function that computes them.
6
+ import { defineConfig } from "./config.js";
7
+ const COLLECTION = "$collection";
8
+ const isPlain = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
9
+ /** Plain data out of a config value: a `target` thunk becomes `{ $collection: name }`, any other function goes. */
10
+ function toData(value, key) {
11
+ if (typeof value === "function") {
12
+ return key === "target" ? { [COLLECTION]: value().name } : undefined;
13
+ }
14
+ if (Array.isArray(value))
15
+ return value.map((item) => toData(item));
16
+ if (isPlain(value)) {
17
+ const out = {};
18
+ for (const [k, v] of Object.entries(value)) {
19
+ const data = toData(v, k);
20
+ if (data !== undefined)
21
+ out[k] = data;
22
+ }
23
+ return out;
24
+ }
25
+ return value;
26
+ }
27
+ /** The pushed form of a resolved config: its defineConfig input, as data. */
28
+ export function serializeRegistry(config) {
29
+ const { sections, elements, collections, globals, pages } = config.registry;
30
+ const input = {
31
+ registry: { sections, elements, collections, globals, pages },
32
+ locales: config.locales,
33
+ ...(config.timeZone !== undefined ? { timeZone: config.timeZone } : {}),
34
+ pages: { tree: { depth: config.pageDepth } },
35
+ styles: config.styles,
36
+ css: config.css,
37
+ custom: { breakpoint: config.custom.breakpoint, toolset: config.custom.toolset },
38
+ homePageSlug: config.homePageSlug,
39
+ ...(config.database !== undefined ? { database: config.database } : {}),
40
+ media: config.media,
41
+ admin: { path: config.admin.path },
42
+ routing: config.routing,
43
+ };
44
+ return { v: 1, config: toData(input) };
45
+ }
46
+ /** `{ $collection: name }` back into a thunk over the hydrated registry's own collections and globals. */
47
+ function revive(value, byName) {
48
+ if (Array.isArray(value))
49
+ return value.map((item) => revive(item, byName));
50
+ if (!isPlain(value))
51
+ return value;
52
+ const keys = Object.keys(value);
53
+ if (keys.length === 1 && keys[0] === COLLECTION && typeof value[COLLECTION] === "string") {
54
+ const name = value[COLLECTION];
55
+ return () => {
56
+ const target = byName.get(name);
57
+ if (!target)
58
+ throw new Error(`smoodly: the pushed registry has a ref to "${name}", which it does not register.`);
59
+ return target;
60
+ };
61
+ }
62
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, revive(v, byName)]));
63
+ }
64
+ /** A resolved config from its pushed form: the ref thunks resolve by name within the same registry. */
65
+ export function hydrateRegistry(json) {
66
+ const byName = new Map();
67
+ const input = revive(json.config, byName);
68
+ // The thunks run only after defineConfig, which keeps the collections and globals it was given.
69
+ for (const c of [...(input.registry.collections ?? []), ...(input.registry.globals ?? [])])
70
+ byName.set(c.name, c);
71
+ return defineConfig(input);
72
+ }
73
+ /** JSON with every object's keys sorted, so two equal registries stringify alike whatever their key order. */
74
+ function sortedJson(value) {
75
+ return JSON.stringify(value, (_key, v) => isPlain(v) ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, v[k]])) : v);
76
+ }
77
+ /** The registry's fingerprint: SHA-256 hex of its key-sorted JSON. */
78
+ export async function registryHash(json) {
79
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(sortedJson(json)));
80
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
81
+ }
package/dist/render.d.ts CHANGED
@@ -5,7 +5,6 @@ import { type StyleValues } from "./styles.ts";
5
5
  import { type CustomOptions } from "./custom/CustomSection.tsx";
6
6
  import { type EditorWrap, type PageContext, type PageTree, type ZoneContent } from "./page-context.ts";
7
7
  export type { TreeNode, PageTree, PageLink, PageContextLink, PageContextAlternate, PageContext, ZoneContent, EditorWrap } from "./page-context.ts";
8
- export { EMPTY_PAGE_CONTEXT } from "./page-context.ts";
9
8
  type AnyPaired = ComponentType<any> & {
10
9
  schema: {
11
10
  name: string;
package/dist/render.js CHANGED
@@ -8,7 +8,7 @@ import { createCustomSection } from "./custom/CustomSection.js";
8
8
  import { EMPTY_PAGE_CONTEXT } from "./page-context.js";
9
9
  import { renderGuarded } from "./section-guard.js";
10
10
  import { PLACEHOLDER_STYLE } from "./section-crash.js";
11
- export { EMPTY_PAGE_CONTEXT } from "./page-context.js";
11
+ ;
12
12
  export function Zone({ of: content }) {
13
13
  // A malformed tree (bad data, a client that skipped isPageTree) must not
14
14
  // crash the render — treat a non-array nodes as empty rather than throw.
package/dist/resolve.d.ts CHANGED
@@ -2,7 +2,8 @@ import type { Descriptor } from "./fields.ts";
2
2
  import type { CollectionSchema } from "./collections.ts";
3
3
  import type { PageTree } from "./page-context.ts";
4
4
  import { type GlobalSchema } from "./global.ts";
5
- import { type AssetRecord } from "./media.ts";
5
+ import { type AssetRecord, type ImageDefaults } from "./media.ts";
6
+ import type { CutSigner } from "./media-cut.ts";
6
7
  import { type ElementSchemas } from "./richtext-walk.ts";
7
8
  import type { SectionLike } from "./refs.ts";
8
9
  import type { Toolset } from "./toolset.ts";
@@ -31,6 +32,11 @@ type ResolveOptions = {
31
32
  paths?: PathLookup;
32
33
  /** Media (spec 2026-09-08 §9): one batched asset fetch per page, values become resolved objects. */
33
34
  assets?: AssetFetcher;
35
+ /** Signs each image's cut where the service cuts (spec 2026-10-18 media on R2 and Images §5); absent, no cut. */
36
+ signer?: CutSigner;
37
+ /** The project's image quality and srcset cap, stamped on each image under its field's own (spec 2026-10-18 image
38
+ * quality and max width §6). */
39
+ images?: ImageDefaults;
34
40
  /** The registered global items a placement node may name (spec 2026-09-07 §4). */
35
41
  globals?: GlobalSchema[];
36
42
  /** Registered Elements a richtext document may embed (spec 2026-09-08 block nodes §2). */
@@ -48,17 +54,17 @@ type ResolveOptions = {
48
54
  };
49
55
  export declare function resolveTree(tree: PageTree, options: ResolveOptions): Promise<PageTree>;
50
56
  /** One value set's media, resolved in place. Exported for the site layer's global-item settings. */
51
- export declare function resolveMedia(values: Record<string, unknown>, descriptors: Record<string, Descriptor>, fetch: AssetFetcher, elements?: ElementSchemas): Promise<void>;
52
- /** Many value sets, one asset round (plus one for posters): the tree's
53
- * nodes and every entry hydrated through a ref, or a list page's items.
54
- * Exported for the site layer's content API. */
55
- export declare function resolveMediaSets(sets: MediaSet[], fetch: AssetFetcher, elements?: ElementSchemas): Promise<void>;
57
+ export declare function resolveMedia(values: Record<string, unknown>, descriptors: Record<string, Descriptor>, fetch: AssetFetcher, elements?: ElementSchemas, signer?: CutSigner, defaults?: ImageDefaults): Promise<void>;
56
58
  export type ResolveFieldsOptions = {
57
59
  collections: CollectionSchema<any>[];
58
60
  fetch: EntryFetcher;
59
61
  /** Each hydrated ref's address (spec 2026-09-29 content API contract §2). */
60
62
  paths?: PathLookup;
61
63
  assets?: AssetFetcher;
64
+ /** Signs each image's cut (spec 2026-10-18 media on R2 and Images §5); absent, no cut. */
65
+ signer?: CutSigner;
66
+ /** The project's image quality and srcset cap (spec 2026-10-18 image quality and max width §6). */
67
+ images?: ImageDefaults;
62
68
  /** Registered Elements a richtext document may embed (spec 2026-09-08 block nodes §2). */
63
69
  elements?: SectionLike[];
64
70
  maxDepth?: number;
package/dist/resolve.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // chain gets a shallow copy (refs as ids), so traversal terminates by
8
8
  // construction and the output stays JSON-serializable.
9
9
  import { globalLink, GLOBAL_NODE_TYPE, SETTINGS_KEY, STYLES_KEY, CSS_KEY } from "./global.js";
10
- import { applyAssets, collectAssetIds } from "./media.js";
10
+ import { applyAssets, collectAssetIds, signCuts } from "./media.js";
11
11
  import { elementSchemas } from "./richtext-walk.js";
12
12
  import { SEO_KEY } from "./seo.js";
13
13
  import { CUSTOM_NODE_TYPE, customSchema, customToolset } from "./custom/index.js";
@@ -62,7 +62,7 @@ export async function resolveTree(tree, options) {
62
62
  sets.push({ values: resolved.fields, descriptors: options.page.fields });
63
63
  if (resolved.meta)
64
64
  sets.push({ values: resolved.meta, descriptors: META_DESCRIPTORS });
65
- await resolveMediaSets([...sets, ...hydrated], options.assets, elementSchemas(options.elements));
65
+ await resolveMediaSets([...sets, ...hydrated], options.assets, elementSchemas(options.elements), options.signer, options.images);
66
66
  }
67
67
  return resolved;
68
68
  }
@@ -83,19 +83,22 @@ async function fetchAssets(fetch, ids) {
83
83
  return out;
84
84
  }
85
85
  /** One value set's media, resolved in place. Exported for the site layer's global-item settings. */
86
- export async function resolveMedia(values, descriptors, fetch, elements = {}) {
87
- await resolveMediaSets([{ values, descriptors }], fetch, elements);
86
+ export async function resolveMedia(values, descriptors, fetch, elements = {}, signer, defaults) {
87
+ await resolveMediaSets([{ values, descriptors }], fetch, elements, signer, defaults);
88
88
  }
89
89
  /** Many value sets, one asset round (plus one for posters): the tree's
90
90
  * nodes and every entry hydrated through a ref, or a list page's items.
91
91
  * Exported for the site layer's content API. */
92
- export async function resolveMediaSets(sets, fetch, elements = {}) {
92
+ async function resolveMediaSets(sets, fetch, elements = {}, signer, defaults) {
93
93
  const ids = new Set();
94
94
  for (const { values, descriptors } of sets)
95
95
  collectAssetIds(values, descriptors, ids, elements);
96
96
  const assets = await fetchAssets(fetch, ids);
97
+ const cuts = signer ? [] : undefined;
97
98
  for (const { values, descriptors } of sets)
98
- applyAssets(values, descriptors, assets, elements);
99
+ applyAssets(values, descriptors, assets, elements, cuts, defaults);
100
+ if (signer && cuts)
101
+ await signCuts(cuts, signer);
99
102
  }
100
103
  /** Placements first (spec 2026-09-07 §4): fetch every linked item in one
101
104
  * call per item, rewrite each node into the item's section node — the
@@ -148,7 +151,7 @@ export async function resolveFields(fields, descriptors, options) {
148
151
  collectTasks(resolved, descriptors, tasks);
149
152
  const hydrated = await runResolution(tasks, { ...options, maxDepth: options.maxDepth ?? Infinity });
150
153
  if (options.assets)
151
- await resolveMediaSets([{ values: resolved, descriptors }, ...hydrated], options.assets, elementSchemas(options.elements));
154
+ await resolveMediaSets([{ values: resolved, descriptors }, ...hydrated], options.assets, elementSchemas(options.elements), options.signer, options.images);
152
155
  return resolved;
153
156
  }
154
157
  /** Many value sets (a list's entries), one resolution: every ref batched per
@@ -159,7 +162,7 @@ export async function resolveFieldSets(sets, options) {
159
162
  collectTasks(values, descriptors, tasks);
160
163
  const hydrated = await runResolution(tasks, { ...options, maxDepth: options.maxDepth ?? Infinity });
161
164
  if (options.assets)
162
- await resolveMediaSets([...sets, ...hydrated], options.assets, elementSchemas(options.elements));
165
+ await resolveMediaSets([...sets, ...hydrated], options.assets, elementSchemas(options.elements), options.signer, options.images);
163
166
  }
164
167
  /** Hydrates every task's refs in place and returns each hydrated entry
165
168
  * copy with its collection's descriptors — the media pass walks those