smoodly 0.0.51 → 0.0.53

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 (292) hide show
  1. package/README.md +14 -13
  2. package/dist/admin/auth.d.ts +3 -11
  3. package/dist/admin/auth.js +1 -30
  4. package/dist/admin/client-ops.d.ts +5 -4
  5. package/dist/admin/client-ops.js +15 -12
  6. package/dist/admin/delivery-mode.d.ts +1 -1
  7. package/dist/admin/delivery-mode.js +2 -2
  8. package/dist/admin/drift.d.ts +0 -2
  9. package/dist/admin/drift.js +1 -1
  10. package/dist/admin/editor/EditorView.d.ts +0 -3
  11. package/dist/admin/editor/EditorView.js +11 -16
  12. package/dist/admin/editor/FloatingToolbar.d.ts +0 -1
  13. package/dist/admin/editor/FloatingToolbar.js +1 -1
  14. package/dist/admin/editor/SeoSettings.d.ts +0 -2
  15. package/dist/admin/editor/SeoSettings.js +2 -2
  16. package/dist/admin/editor/canvas-address.d.ts +8 -0
  17. package/dist/admin/editor/canvas-address.js +18 -2
  18. package/dist/admin/editor/canvas-diagnosis.d.ts +2 -0
  19. package/dist/admin/editor/canvas-diagnosis.js +3 -0
  20. package/dist/admin/editor/inline-dom.d.ts +0 -4
  21. package/dist/admin/editor/inline-dom.js +1 -1
  22. package/dist/admin/editor/preview-grant.d.ts +6 -0
  23. package/dist/admin/editor/preview-grant.js +25 -0
  24. package/dist/admin/editor/useCanvasDiagnosis.js +2 -2
  25. package/dist/admin/files.generated.js +1 -1
  26. package/dist/admin/fixed-nodes.d.ts +0 -6
  27. package/dist/admin/fixed-nodes.js +1 -81
  28. package/dist/admin/forms/CropEditor.d.ts +4 -2
  29. package/dist/admin/forms/CropEditor.js +58 -5
  30. package/dist/admin/forms/ImagePreview.d.ts +3 -1
  31. package/dist/admin/forms/ImagePreview.js +9 -3
  32. package/dist/admin/forms/MediaGrid.js +2 -1
  33. package/dist/admin/forms/MediaPicker.js +9 -6
  34. package/dist/admin/forms/MediaWidget.js +11 -9
  35. package/dist/admin/forms/assets-context.d.ts +7 -3
  36. package/dist/admin/forms/assets-context.js +5 -3
  37. package/dist/admin/forms/crop-editor.d.ts +24 -2
  38. package/dist/admin/forms/crop-editor.js +36 -2
  39. package/dist/admin/forms/date-picker.d.ts +0 -1
  40. package/dist/admin/forms/date-picker.js +1 -1
  41. package/dist/admin/forms/tabs.d.ts +0 -1
  42. package/dist/admin/forms/tabs.js +1 -1
  43. package/dist/admin/forms/upload.d.ts +18 -11
  44. package/dist/admin/forms/upload.js +62 -24
  45. package/dist/admin/global-items.d.ts +0 -3
  46. package/dist/admin/global-items.js +0 -54
  47. package/dist/admin/handler.d.ts +22 -28
  48. package/dist/admin/handler.js +146 -47
  49. package/dist/admin/health.d.ts +0 -4
  50. package/dist/admin/index.d.ts +6 -9
  51. package/dist/admin/index.js +3 -6
  52. package/dist/admin/ops.d.ts +40 -32
  53. package/dist/admin/page-data.d.ts +14 -6
  54. package/dist/admin/page-data.js +1 -1
  55. package/dist/admin/session-flow.d.ts +65 -0
  56. package/dist/admin/session-flow.js +121 -0
  57. package/dist/admin/shell/AdminApp.d.ts +8 -3
  58. package/dist/admin/shell/AdminApp.js +37 -58
  59. package/dist/admin/shell/ContentHealthPage.js +1 -3
  60. package/dist/admin/shell/GlobalFields.d.ts +0 -3
  61. package/dist/admin/shell/GlobalFields.js +1 -1
  62. package/dist/admin/shell/IconRail.js +10 -8
  63. package/dist/admin/shell/MediaScreen.js +9 -6
  64. package/dist/admin/shell/MembersPage.d.ts +1 -2
  65. package/dist/admin/shell/MembersPage.js +19 -90
  66. package/dist/admin/shell/SettingsView.d.ts +1 -2
  67. package/dist/admin/shell/SettingsView.js +2 -2
  68. package/dist/admin/shell/SignedOutView.d.ts +5 -0
  69. package/dist/admin/shell/SignedOutView.js +9 -0
  70. package/dist/admin/shell/console-links.d.ts +5 -0
  71. package/dist/admin/shell/console-links.js +11 -0
  72. package/dist/admin/shell/members-list.d.ts +1 -14
  73. package/dist/admin/shell/members-list.js +2 -23
  74. package/dist/admin/shell/pages-tree.d.ts +0 -2
  75. package/dist/admin/shell/pages-tree.js +1 -1
  76. package/dist/admin/shell/presence-socket.d.ts +10 -0
  77. package/dist/admin/shell/presence-socket.js +191 -0
  78. package/dist/admin/shell/presence.d.ts +6 -18
  79. package/dist/admin/shell/presence.js +4 -11
  80. package/dist/admin/shell/session.d.ts +19 -12
  81. package/dist/admin/shell/session.js +114 -47
  82. package/dist/admin/ui/LocaleSwitcher.d.ts +0 -4
  83. package/dist/admin/ui/LocaleSwitcher.js +2 -2
  84. package/dist/admin/ui/primitives.d.ts +4 -14
  85. package/dist/admin/ui/primitives.js +7 -9
  86. package/dist/admin/ui/theme.js +13 -2
  87. package/dist/admin/validate.js +10 -1
  88. package/dist/api-client.d.ts +26 -0
  89. package/dist/api-client.js +107 -0
  90. package/dist/assemble.d.ts +4 -41
  91. package/dist/assemble.js +5 -303
  92. package/dist/asset-store.d.ts +28 -30
  93. package/dist/asset-store.js +26 -121
  94. package/dist/astro/cache.d.ts +1 -1
  95. package/dist/astro/components/Head.astro +4 -1
  96. package/dist/astro/components/Image.astro +12 -17
  97. package/dist/astro/components/Video.astro +5 -2
  98. package/dist/astro/head.d.ts +3 -0
  99. package/dist/astro/head.js +24 -0
  100. package/dist/astro/index.js +61 -27
  101. package/dist/astro/locale-paths.d.ts +5 -2
  102. package/dist/astro/locale-paths.js +10 -2
  103. package/dist/astro/routes/admin.js +9 -7
  104. package/dist/astro/runtime.d.ts +3 -2
  105. package/dist/astro/runtime.js +5 -4
  106. package/dist/astro/server.d.ts +7 -11
  107. package/dist/astro/server.js +22 -28
  108. package/dist/astro/shell.d.ts +3 -3
  109. package/dist/astro/shell.js +1 -1
  110. package/dist/astro/static-media-node.d.ts +4 -0
  111. package/dist/astro/static-media-node.js +25 -0
  112. package/dist/astro/static-media.d.ts +17 -0
  113. package/dist/astro/static-media.js +55 -0
  114. package/dist/astro/static.d.ts +5 -10
  115. package/dist/astro/static.js +13 -29
  116. package/dist/canvas-marker.d.ts +14 -0
  117. package/dist/canvas-marker.js +36 -0
  118. package/dist/cli/api.d.ts +14 -0
  119. package/dist/cli/api.js +48 -0
  120. package/dist/cli/app-env.d.ts +2 -0
  121. package/dist/cli/app-env.js +20 -0
  122. package/dist/cli/export.d.ts +16 -2
  123. package/dist/cli/export.js +52 -35
  124. package/dist/cli/import.d.ts +7 -11
  125. package/dist/cli/import.js +50 -54
  126. package/dist/cli/load-config.d.ts +0 -1
  127. package/dist/cli/load-config.js +1 -1
  128. package/dist/cli/main.d.ts +4 -12
  129. package/dist/cli/main.js +26 -72
  130. package/dist/cli/push.d.ts +6 -0
  131. package/dist/cli/push.js +31 -0
  132. package/dist/collections.d.ts +0 -1
  133. package/dist/collections.js +1 -1
  134. package/dist/config.d.ts +20 -5
  135. package/dist/config.js +18 -1
  136. package/dist/content/content-api.d.ts +23 -23
  137. package/dist/content/content-api.js +48 -56
  138. package/dist/content/mime.d.ts +1 -0
  139. package/dist/content/mime.js +6 -0
  140. package/dist/css.js +1 -1
  141. package/dist/deploy-store.d.ts +19 -0
  142. package/dist/entry-store.d.ts +8 -94
  143. package/dist/entry-store.js +9 -557
  144. package/dist/env.d.ts +27 -20
  145. package/dist/env.js +38 -50
  146. package/dist/fields.d.ts +11 -3
  147. package/dist/fields.js +33 -2
  148. package/dist/global-id.d.ts +2 -0
  149. package/dist/global-id.js +4 -2
  150. package/dist/image/SmoodlyImage.d.ts +9 -5
  151. package/dist/image/SmoodlyImage.js +29 -15
  152. package/dist/image/SmoodlyVideo.d.ts +1 -1
  153. package/dist/image/SmoodlyVideo.js +3 -2
  154. package/dist/image/index.d.ts +2 -3
  155. package/dist/image/index.js +0 -2
  156. package/dist/image/markup.d.ts +53 -0
  157. package/dist/image/markup.js +111 -0
  158. package/dist/index.d.ts +13 -23
  159. package/dist/index.js +7 -20
  160. package/dist/media-cut.d.ts +44 -0
  161. package/dist/media-cut.js +70 -0
  162. package/dist/media.d.ts +58 -4
  163. package/dist/media.js +126 -16
  164. package/dist/member-store.d.ts +2 -50
  165. package/dist/member-store.js +0 -56
  166. package/dist/next/admin-route.d.ts +1 -1
  167. package/dist/next/admin-route.js +5 -3
  168. package/dist/next/meta.js +2 -3
  169. package/dist/next/page-renderer.d.ts +2 -2
  170. package/dist/next/page-renderer.js +22 -11
  171. package/dist/next/queries.d.ts +2 -2
  172. package/dist/path-index.d.ts +0 -24
  173. package/dist/path-index.js +0 -98
  174. package/dist/paths.d.ts +1 -1
  175. package/dist/paths.js +1 -1
  176. package/dist/push.d.ts +30 -0
  177. package/dist/push.js +52 -0
  178. package/dist/read-counter.d.ts +2 -0
  179. package/dist/read-counter.js +6 -0
  180. package/dist/reads.d.ts +7 -7
  181. package/dist/reads.js +3 -0
  182. package/dist/registry-json.d.ts +11 -0
  183. package/dist/registry-json.js +81 -0
  184. package/dist/render.d.ts +0 -1
  185. package/dist/render.js +1 -1
  186. package/dist/resolve.d.ts +12 -6
  187. package/dist/resolve.js +11 -8
  188. package/dist/revalidate.d.ts +8 -1
  189. package/dist/revalidate.js +8 -1
  190. package/dist/richtext/index.d.ts +1 -1
  191. package/dist/section-wrapper.d.ts +1 -1
  192. package/dist/section-wrapper.js +0 -9
  193. package/dist/signed-message.d.ts +26 -0
  194. package/dist/signed-message.js +103 -0
  195. package/dist/site.d.ts +59 -18
  196. package/dist/site.js +21 -252
  197. package/dist/space-transfer.d.ts +6 -3
  198. package/dist/space-transfer.js +18 -6
  199. package/dist/store.d.ts +2 -80
  200. package/dist/store.js +3 -489
  201. package/dist/toolset.d.ts +2 -1
  202. package/dist/toolset.js +1 -1
  203. package/dist/version.d.ts +1 -1
  204. package/dist/version.js +1 -1
  205. package/package.json +4 -15
  206. package/dist/admin/auth-config.d.ts +0 -6
  207. package/dist/admin/browser/fetch-op.d.ts +0 -10
  208. package/dist/admin/browser/fetch-op.js +0 -44
  209. package/dist/admin/browser/main.d.ts +0 -1
  210. package/dist/admin/browser/main.js +0 -23
  211. package/dist/admin/ops-impl.d.ts +0 -51
  212. package/dist/admin/ops-impl.js +0 -1403
  213. package/dist/admin/shell/InviteModal.d.ts +0 -6
  214. package/dist/admin/shell/InviteModal.js +0 -28
  215. package/dist/admin/shell/LoginView.d.ts +0 -5
  216. package/dist/admin/shell/LoginView.js +0 -80
  217. package/dist/admin/shell/SchemaNotice.d.ts +0 -4
  218. package/dist/admin/shell/SchemaNotice.js +0 -4
  219. package/dist/admin/shell/SetPasswordView.d.ts +0 -7
  220. package/dist/admin/shell/SetPasswordView.js +0 -41
  221. package/dist/admin/shell/presence-realtime.d.ts +0 -3
  222. package/dist/admin/shell/presence-realtime.js +0 -32
  223. package/dist/admin/supabase-auth.d.ts +0 -7
  224. package/dist/admin/supabase-auth.js +0 -30
  225. package/dist/astro/deploy-deps.d.ts +0 -10
  226. package/dist/astro/deploy-deps.js +0 -8
  227. package/dist/astro/image.d.ts +0 -27
  228. package/dist/astro/image.js +0 -29
  229. package/dist/bind.d.ts +0 -19
  230. package/dist/bind.js +0 -34
  231. package/dist/cli/add-admin.d.ts +0 -11
  232. package/dist/cli/add-admin.js +0 -25
  233. package/dist/cli/load-collections.d.ts +0 -2
  234. package/dist/cli/load-collections.js +0 -27
  235. package/dist/cli/migrate.d.ts +0 -43
  236. package/dist/cli/migrate.js +0 -138
  237. package/dist/cli/rebuild.d.ts +0 -23
  238. package/dist/cli/rebuild.js +0 -22
  239. package/dist/cli/space-io.d.ts +0 -41
  240. package/dist/cli/space-io.js +0 -84
  241. package/dist/client.d.ts +0 -11
  242. package/dist/client.js +0 -54
  243. package/dist/current-space.d.ts +0 -4
  244. package/dist/current-space.js +0 -9
  245. package/dist/document-stamp.d.ts +0 -7
  246. package/dist/document-stamp.js +0 -48
  247. package/dist/image/loaders.d.ts +0 -15
  248. package/dist/image/loaders.js +0 -5
  249. package/dist/image/next-config.d.ts +0 -18
  250. package/dist/image/next-config.js +0 -32
  251. package/dist/image/remote-pattern.d.ts +0 -1
  252. package/dist/image/remote-pattern.js +0 -5
  253. package/dist/migrations.d.ts +0 -37
  254. package/dist/migrations.js +0 -73
  255. package/dist/project.d.ts +0 -88
  256. package/dist/project.js +0 -231
  257. package/dist/published.d.ts +0 -142
  258. package/dist/published.js +0 -164
  259. package/dist/ref-index.d.ts +0 -19
  260. package/dist/ref-index.js +0 -0
  261. package/dist/schema-read.d.ts +0 -23
  262. package/dist/schema-read.js +0 -50
  263. package/dist/schema-version.d.ts +0 -19
  264. package/dist/schema-version.js +0 -34
  265. package/dist/sql-space.d.ts +0 -44
  266. package/dist/sql-space.js +0 -289
  267. package/dist/sql.d.ts +0 -15
  268. package/dist/sql.js +0 -556
  269. package/dist/supabase-asset-store.d.ts +0 -21
  270. package/dist/supabase-asset-store.js +0 -170
  271. package/dist/supabase-entry-store.d.ts +0 -92
  272. package/dist/supabase-entry-store.js +0 -824
  273. package/dist/supabase-member-store.d.ts +0 -21
  274. package/dist/supabase-member-store.js +0 -96
  275. package/dist/supabase-path-index.d.ts +0 -30
  276. package/dist/supabase-path-index.js +0 -138
  277. package/dist/supabase-published-store.d.ts +0 -56
  278. package/dist/supabase-published-store.js +0 -314
  279. package/dist/supabase-store.d.ts +0 -103
  280. package/dist/supabase-store.js +0 -678
  281. package/migrations/20260926120000_smoodly_v1.sql +0 -535
  282. package/migrations/20260926130000_smoodly_v2.sql +0 -26
  283. package/migrations/20260926140000_smoodly_v3.sql +0 -38
  284. package/migrations/20260926150000_smoodly_v4.sql +0 -41
  285. package/migrations/20260929120000_smoodly_v5.sql +0 -56
  286. package/migrations/20260929135523_smoodly_v6.sql +0 -200
  287. package/migrations/20260930040956_smoodly_v7.sql +0 -84
  288. package/migrations/20261007120000_smoodly_v8.sql +0 -154
  289. package/migrations/20261007200000_smoodly_v9.sql +0 -30
  290. package/migrations/20261008120000_smoodly_v10.sql +0 -47
  291. package/migrations/20261009120000_smoodly_v11.sql +0 -49
  292. /package/dist/{admin/auth-config.js → deploy-store.js} +0 -0
package/dist/site.js CHANGED
@@ -1,15 +1,9 @@
1
- import { schemaOf } from "./entry-store.js";
2
- import { entrySegment } from "./paths.js";
3
1
  import { lastPart } from "./addresses.js";
4
2
  import { withDefaults } from "./field-defaults.js";
5
3
  import { modelKey } from "./model-key.js";
6
- import { documentStamp } from "./document-stamp.js";
7
4
  import { SMOODLY_VERSION } from "./version.js";
8
- import { toEntryItem, toPageItem } from "./read-shapes.js";
9
5
  import { pageSeo } from "./seo.js";
10
- import { checkEntryQuery, createAssembler, pageFieldsOf, siteShape } from "./assemble.js";
11
- import { collectionOrder } from "./collections.js";
12
- import { createProjector } from "./project.js";
6
+ import { pageFieldsOf, siteShape } from "./assemble.js";
13
7
  /** The `page` prop for a loaded page. `href` is the app's seam from a
14
8
  * locale-relative path to a site URL; the default keeps CMS paths as URLs. */
15
9
  export function pageContextOf(hit, href = (p) => p) {
@@ -26,10 +20,11 @@ export function pageContextOf(hit, href = (p) => p) {
26
20
  locales: Object.fromEntries(Object.entries(hit.locales).map(([code, alt]) => [code, alt && { ...alt, url: href(alt.path, code) }])),
27
21
  };
28
22
  }
29
- export function createSmoodlySite(options) {
23
+ /** The members of a site that need no store (the API client computes them from the same config, so a cache key or a
24
+ * code page never disagrees across the wire). */
25
+ export function siteStatics(options) {
30
26
  const shape = siteShape(options);
31
27
  const { locales, pageSchemas, homePageSlug, styles, custom, codeOwned } = shape;
32
- const assembler = createAssembler(options, shape);
33
28
  // Every cached read keys on it (spec 2026-09-30 tester findings §3): an upgrade or a model change starts a fresh cache.
34
29
  const cacheKey = `${SMOODLY_VERSION}:${modelKey({
35
30
  // Whole schemas: a collection's order, tree or title field changes a read as its fields do (functions drop out in the hash).
@@ -39,206 +34,9 @@ export function createSmoodlySite(options) {
39
34
  elements: (options.elements ?? []).map((x) => ("schema" in x ? x.schema : x)),
40
35
  pages: pageSchemas,
41
36
  locales, homePageSlug, styles, custom,
37
+ // The project's image quality and cap are stamped on every image a read resolves.
38
+ images: options.images ?? {},
42
39
  })}`;
43
- // What a delivery row is stamped with (DESIGN §3, the document stamp): the document format and the schema parts
44
- // that reach a document, never the package version, so a release leaves current documents current.
45
- const stamp = documentStamp(options);
46
- const localeOf = (o) => o?.locale ?? locales.default;
47
- const published = () => options.stores().published;
48
- const projector = createProjector({
49
- assembler,
50
- stores: () => { const s = options.stores(); return { pages: s.pages, entries: s.entries, published: s.published }; },
51
- collections: options.collections, globals: options.globals ?? [], sections: options.sections, elements: options.elements,
52
- locales, model: stamp,
53
- });
54
- // Delivery (spec 2026-10-07 delivery model §3–§4): outside draft mode a read takes the stored document when it is
55
- // current — built by this model and not marked stale — and otherwise assembles that one document, stores it and serves it.
56
- const current = (row) => row.model === stamp && !row.stale;
57
- /** One writer of record (DESIGN §3, the document stamp): only the deployment whose stamp the space recorded rewrites
58
- * rows; the first writer into an empty record records itself; every other deployment serves assembled documents
59
- * and writes nothing. Read only when a heal is about to write, so a current page stays one call. A "no" is kept for
60
- * the process (a later rebuild at this stamp is seen by the next process, which costs assembled reads, never wrong
61
- * ones); a "yes" is read again on each heal, so a rebuild elsewhere stops this writer at its next heal. */
62
- let yields = false;
63
- async function mayWrite() {
64
- if (yields)
65
- return false;
66
- // The record is derived like the rows: a read or write of it that fails is logged, and the page is served
67
- // assembled with nothing written (spec 2026-10-07 §2 Failure), never failed on it.
68
- try {
69
- const recorded = await published().recordedModel();
70
- if (recorded === null) {
71
- await published().recordModel(stamp);
72
- return true;
73
- }
74
- yields = recorded !== stamp;
75
- return !yields;
76
- }
77
- catch (e) {
78
- console.error("smoodly: reading the space's recorded document stamp failed; served assembled, nothing written.", e);
79
- return false;
80
- }
81
- }
82
- const fresh = (row) => current(row) && row.document != null;
83
- const itemOf = (doc) => toEntryItem(doc.entry, doc.entry.fields, { path: doc.path, locale: doc.locale, locales: doc.locales, ...(doc.seo ? { seo: doc.seo } : {}) });
84
- /** A page row's answer: its document when current (null for a folder or a hidden node), else the healed one. */
85
- const pageOf = async (row, id, locale) => row && current(row) ? row.document : projector.healPage(id, locale, { seen: row !== null, write: await mayWrite() });
86
- /** An entry document is served at an address only when its collection has addresses. */
87
- const served = (doc) => (doc && doc.path !== null ? doc : null);
88
- const entryOf = async (row, id, locale) => row && fresh(row) ? row.document : projector.healEntry(id, locale, { seen: row !== null, write: await mayWrite() });
89
- /** An entry document as the projector would write it, from an assembled read (the address-less shape, path from the item). */
90
- const assembledEntry = (collection, locale) => (read) => ({
91
- kind: "entry", locale, collection, entry: { ...read.record, fields: read.item.fields },
92
- path: read.item.path, locales: read.item.locales, ...(read.item.seo ? { seo: read.item.seo } : {}),
93
- });
94
- /** The published documents of a collection's query. Stale rows are rebuilt in one batched projection and the
95
- * query read once more (spec §4): a rebuild that finds an entry unpublished removes its row, so it drops out. */
96
- async function deliveredEntries(collection, o) {
97
- const schema = schemaOf(options.collections, collection);
98
- checkEntryQuery(schema, collection, o);
99
- const locale = localeOf(o);
100
- const read = () => published().entries(collection, {
101
- locale, where: o.where, order: o.order ?? collectionOrder(schema), limit: o.limit, ids: o.ids,
102
- ...(o.parent !== undefined ? { parent: o.parent } : {}),
103
- });
104
- let rows = await read();
105
- const behind = rows.filter((r) => !fresh(r)).map((r) => r.id);
106
- if (behind.length > 0) {
107
- // Not the writer of record: the list assembled now, nothing written (DESIGN §3).
108
- if (!(await mayWrite()))
109
- return (await assembler.entries(collection, { ...o, locale, draft: false })).map(assembledEntry(collection, locale));
110
- // A failed rebuild never fails the read: the rows as read are served, and the next read tries again.
111
- try {
112
- await projector.entries(behind, { cap: Infinity, stamp: false });
113
- rows = await read();
114
- }
115
- catch (e) {
116
- console.error(`smoodly: rebuilding ${behind.length} ${collection} documents in the delivery tables failed; served as read.`, e);
117
- }
118
- }
119
- return rows.flatMap((r) => (r.document != null ? [r.document] : []));
120
- }
121
- const getEntries = async (collection, o = {}) => o.draft ? (await assembler.entries(collection, o)).map((r) => r.item) : (await deliveredEntries(collection, o)).map(itemOf);
122
- /** Every entry of the collection in the locale, with the record a tree and a bare slug read `parentId` from. */
123
- const readAll = async (collection, locale, draft) => draft
124
- ? assembler.entries(collection, { locale, draft })
125
- : (await deliveredEntries(collection, { locale })).map((doc) => ({ record: doc.entry, item: itemOf(doc) }));
126
- const api = {
127
- async getPagesTree(o) {
128
- const locale = localeOf(o);
129
- if (o?.draft)
130
- return assembler.tree(locale, { draft: true });
131
- // One light read. A row behind the model or marked stale is rebuilt before answering (spec §4), and the tree read
132
- // again; a rebuild that finds the node gone or unpublished removes its row.
133
- let rows = await published().tree(locale);
134
- const behind = rows.filter((r) => !current(r)).map((r) => r.id);
135
- if (behind.length > 0) {
136
- // Not the writer of record: the tree assembled now, nothing written (DESIGN §3).
137
- if (!(await mayWrite()))
138
- return assembler.tree(locale, { draft: false });
139
- // One batched projection for every row behind (one tree read in the projector), then one more tree read.
140
- // A failed rebuild never fails the read: the rows as read are served, and the next read tries again.
141
- try {
142
- await projector.pages(behind, { cap: Infinity, stamp: false });
143
- rows = await published().tree(locale);
144
- }
145
- catch (e) {
146
- console.error(`smoodly: rebuilding ${behind.length} page documents in the delivery tables failed; served as read.`, e);
147
- }
148
- }
149
- // A node the tree hides (path null) leaves with its subtree, as does one whose parent has no row.
150
- const build = (parentId) => rows.filter((r) => r.parentId === parentId && r.path !== null).map((r) => ({
151
- id: r.id, title: r.title, path: r.path, hasPage: r.hasPage, fields: r.linkFields, children: build(r.id),
152
- }));
153
- return build(null);
154
- },
155
- async getPage(pathOrId, o) {
156
- const locale = localeOf(o);
157
- if (!o?.draft) {
158
- // The content API stays strict (§7): a redirect row, and a code-owned page with no record, are not pages.
159
- const hit = pathOrId.startsWith("/")
160
- ? (await delivered(locale, pathOrId, false)).hit
161
- : await pageOf(await published().page(pathOrId, locale), pathOrId, locale);
162
- return hit?.kind === "page" ? toPageItem(hit) : null;
163
- }
164
- const path = pathOrId.startsWith("/") ? pathOrId : (await options.stores().pages.pathsOf(pathOrId))[locale];
165
- if (!path)
166
- return null;
167
- const hit = await site.resolve(locale, path, { draft: true });
168
- return hit?.kind === "page" ? toPageItem(hit) : null;
169
- },
170
- getEntries,
171
- async getEntry(collection, idOrPath, o) {
172
- schemaOf(options.collections, collection); // an unknown collection is a wiring error on every branch
173
- const locale = localeOf(o);
174
- const draft = o?.draft ?? false;
175
- if (idOrPath.startsWith("/")) {
176
- const hit = draft ? await site.resolve(locale, idOrPath, { draft }) : (await delivered(locale, idOrPath, false)).hit;
177
- return hit?.kind === "entry" && hit.collection === collection ? toEntryItem(hit.entry, hit.entry.fields, hit) : null;
178
- }
179
- if (draft) {
180
- const [byId] = await assembler.entries(collection, { locale, draft, ids: [idOrPath] });
181
- if (byId)
182
- return byId.item;
183
- }
184
- else {
185
- const row = await published().entry(idOrPath, locale);
186
- if (row && row.collection === collection && fresh(row))
187
- return itemOf(row.document);
188
- // A miss is a record no projection wrote yet, or a slug: one `get` tells them apart.
189
- if (row?.collection === collection || (await options.stores().entries.get(collection, idOrPath))) {
190
- const doc = await projector.healEntry(idOrPath, locale, { seen: row !== null, write: await mayWrite() });
191
- if (doc)
192
- return itemOf(doc);
193
- }
194
- }
195
- // A bare slug names a ROOT entry only (sibling-scoped slugs, spec §4):
196
- // match the localized slug directly (paths.ts owns slug derivation),
197
- // regardless of whether the collection even has a `path`.
198
- const all = await readAll(collection, locale, draft);
199
- return all.find((r) => r.record.parentId === null && entrySegment(r.record, locale) === idOrPath)?.item ?? null;
200
- },
201
- async getEntriesTree(collection, o) {
202
- const rows = await readAll(collection, localeOf(o), o?.draft ?? false);
203
- const build = (parentId) => rows.filter((r) => r.record.parentId === parentId).map((r) => ({ ...r.item, children: build(r.record.id) }));
204
- return build(null);
205
- },
206
- async getGlobal(item, o) {
207
- const locale = localeOf(o);
208
- if (o?.draft)
209
- return assembler.global(item, locale, { draft: true });
210
- // The item's row by its collection, in one read: a global is a one-record collection.
211
- const [row] = await published().entries(item.name, { locale, limit: 1 });
212
- return row && fresh(row) ? row.document : projector.healGlobal(item, locale, { seen: row !== undefined, write: await mayWrite() });
213
- },
214
- };
215
- /** The published read of an address; `code`: the code-owned page answers where the index has no row. */
216
- /** `globals`: the names of global items to bring along (schema v9); `rows` is what the route carried, when it had any. */
217
- async function delivered(locale, path, code, globals) {
218
- const route = await published().byAddress(locale, path, globals);
219
- const rows = route?.globals;
220
- if (route && fresh(route.row))
221
- return { hit: route.row.document, rows };
222
- let healed = null;
223
- if (route) {
224
- // Stale: rebuilt by id. Unpublished or moved since the row was written, the address is answered as a miss.
225
- const write = await mayWrite();
226
- healed = route.kind === "page" ? await projector.healPage(route.row.id, locale, { write }) : served(await projector.healEntry(route.row.id, locale, { write }));
227
- if (healed && healed.path === path)
228
- return { hit: healed, rows };
229
- }
230
- // A miss: the paths index doubles as the existence check — a record to assemble, a redirect row, or nothing.
231
- // A record wins over the code-owned page at its address, as the renderer has it.
232
- const hit = await site.locate(locale, path);
233
- if (!hit)
234
- return { hit: code ? codePage(locale, path) : null };
235
- if (hit.redirect)
236
- return { hit: { kind: "redirect", target: { kind: hit.kind, id: hit.id } } };
237
- if (route && hit.id === route.row.id)
238
- return { hit: healed, rows }; // just rebuilt
239
- // By id: a page the tree hides has a current row the address lookup cannot reach; anything else heals there.
240
- return { hit: await site.load(hit, locale, path), rows };
241
- }
242
40
  const codePath = (address) => (address === homePageSlug ? "/" : `/${address}`);
243
41
  function codePage(locale, path) {
244
42
  const spec = codeOwned.find((a) => a.template !== null && a.segments[locale] !== undefined && codePath(a.segments[locale]) === path);
@@ -262,50 +60,21 @@ export function createSmoodlySite(options) {
262
60
  ...(withSeo ? { seo: pageSeo(undefined, spec.title) } : {}),
263
61
  };
264
62
  }
265
- const site = {
266
- locales,
267
- homePageSlug,
268
- cacheKey,
269
- stamp,
270
- styles,
271
- custom,
272
- locate: (locale, path) => options.stores().pages.resolvePath(locale, path),
273
- targetPaths: (target) => options.stores().pages.targetPaths(target),
274
- async load(target, locale, path, opts = {}) {
275
- const draft = opts.draft ?? false;
276
- // Draft mode assembles from the editing tables; so does a stored version, which outside draft mode is nothing.
277
- if (draft || opts.version !== undefined) {
278
- return target.kind === "page" ? assembler.page(target.id, locale, path, { draft, version: opts.version }) : assembler.entry(target.id, locale, path, { draft });
279
- }
280
- return target.kind === "page"
281
- ? pageOf(await published().page(target.id, locale), target.id, locale)
282
- : served(await entryOf(await published().entry(target.id, locale), target.id, locale));
283
- },
284
- lookup: async (locale, path) => (await delivered(locale, path, true)).hit,
285
- async lookupWithGlobals(locale, path, items) {
286
- const { hit, rows } = await delivered(locale, path, true, items.map((i) => i.name));
287
- const globals = new Map();
288
- if (rows) {
289
- for (const item of items) {
290
- const row = rows.find((r) => r.collection === item.name);
291
- // No row: unpublished here, or never projected — left out rather than healed on every read, because a
292
- // heal of an unpublished item writes nothing and the next read would heal again; the layout's own read
293
- // pays the call where a page renders it. A stale row heals once and is written.
294
- if (!row)
295
- continue;
296
- globals.set(item.name, fresh(row) ? row.document : await projector.healGlobal(item, locale, { seen: true, write: await mayWrite() }));
297
- }
298
- }
299
- return { hit, globals };
300
- },
301
- async resolve(locale, path, opts) {
302
- // The content API stays strict (§7): a content address is not a redirect.
303
- const target = await site.locate(locale, path);
304
- return target && !target.redirect ? site.load(target, locale, path, opts) : null;
63
+ return { shape, locales, homePageSlug, styles, custom, cacheKey, codePage };
64
+ }
65
+ /** What createSmoodlySite takes, read off the config. */
66
+ export function siteOptions(config, stores) {
67
+ const { sections, elements, collections, globals, pages } = config.registry;
68
+ return {
69
+ // Absent for a site that reads over the API (api-client.ts): its statics need no store, and it never calls this.
70
+ stores: stores ?? (() => { throw new Error("smoodly: this site reads over the API — no store is bound."); }),
71
+ collections, globals,
72
+ // defineConfig's checks guarantee a section and an element carry `fields`.
73
+ sections: sections, elements: elements,
74
+ locales: config.locales, homePageSlug: config.homePageSlug, styles: config.styles, custom: config.custom, pages,
75
+ images: {
76
+ ...(config.media.quality !== undefined ? { quality: config.media.quality } : {}),
77
+ ...(config.media.maxWidth !== undefined ? { maxWidth: config.media.maxWidth } : {}),
305
78
  },
306
- codePage,
307
- projector,
308
- ...api,
309
79
  };
310
- return site;
311
80
  }
@@ -1,4 +1,6 @@
1
- import { type ContentTable } from "./sql-space.ts";
1
+ /** The tables a space's content lives in, the ones export and import carry (members are listed, never copied). */
2
+ export declare const CONTENT_TABLES: readonly ["pages", "page_locales", "page_versions", "entries", "entry_locales", "entry_versions", "refs", "paths", "assets"];
3
+ export type ContentTable = (typeof CONTENT_TABLES)[number];
2
4
  /** The folder layout's own version, apart from the schema (spec §7). */
3
5
  export declare const FORMAT = 1;
4
6
  export type Row = Record<string, unknown>;
@@ -25,7 +27,7 @@ export type Manifest = {
25
27
  export declare const INSERT_ORDER: readonly ContentTable[];
26
28
  /** A row's identity inside one space: its primary key without `space_id`. */
27
29
  export declare const ROW_KEY: Record<ContentTable, readonly string[]>;
28
- /** The primary key as Postgres declares it — what "on conflict do nothing" names. */
30
+ /** The primary key, in column order — what the import's skip-duplicates names. */
29
31
  export declare const CONFLICT_COLUMNS: Record<ContentTable, string>;
30
32
  export declare const rowKey: (table: ContentTable, row: Row) => string;
31
33
  /** Sorted by key, so two exports of an unchanged space diff cleanly (spec §7). */
@@ -56,5 +58,6 @@ export declare function describeExport(rows: TableRows, bytes: number): string;
56
58
  export declare function describeImport(rows: TableRows): string;
57
59
  /** The target check's refusal (spec §9.1), naming only the tables that hold foreign rows. */
58
60
  export declare function notEmptyMessage(foreign: Record<ContentTable, number>): string;
59
- /** Spec §6: the target database's version must equal the export's. */
61
+ /** Spec §6: the target space's schema version must equal the export's. A space's schema is its object's step list,
62
+ * brought forward by its service (spec 2026-10-15 the space object), so the fix is the other side's service. */
60
63
  export declare function versionMismatchMessage(target: number, exported: number): string;
@@ -5,7 +5,18 @@
5
5
  // mirror of the target's foreign keys and the import's "only this
6
6
  // export's rows" test. No I/O; the CLI halves are cli/export.ts and
7
7
  // cli/import.ts.
8
- import { CONTENT_TABLES } from "./sql-space.js";
8
+ /** The tables a space's content lives in, the ones export and import carry (members are listed, never copied). */
9
+ export const CONTENT_TABLES = [
10
+ "pages",
11
+ "page_locales",
12
+ "page_versions",
13
+ "entries",
14
+ "entry_locales",
15
+ "entry_versions",
16
+ "refs",
17
+ "paths",
18
+ "assets",
19
+ ];
9
20
  /** The folder layout's own version, apart from the schema (spec §7). */
10
21
  export const FORMAT = 1;
11
22
  /** Every foreign key's target lands before the row that names it (spec §9.3). */
@@ -26,7 +37,7 @@ export const ROW_KEY = {
26
37
  paths: ["locale", "anchor", "path"],
27
38
  assets: ["id"],
28
39
  };
29
- /** The primary key as Postgres declares it — what "on conflict do nothing" names. */
40
+ /** The primary key, in column order — what the import's skip-duplicates names. */
30
41
  export const CONFLICT_COLUMNS = Object.fromEntries(CONTENT_TABLES.map((t) => [t, (t === "paths" ? ["space_id", ...ROW_KEY[t]] : ROW_KEY[t]).join(",")]));
31
42
  export const rowKey = (table, row) => JSON.stringify(ROW_KEY[table].map((c) => row[c]));
32
43
  /** Sorted by key, so two exports of an unchanged space diff cleanly (spec §7). */
@@ -171,11 +182,12 @@ const NOUN = {
171
182
  /** The target check's refusal (spec §9.1), naming only the tables that hold foreign rows. */
172
183
  export function notEmptyMessage(foreign) {
173
184
  const parts = INSERT_ORDER.filter((t) => foreign[t] > 0).map((t) => plural(foreign[t], ...NOUN[t]));
174
- return `The target space holds ${listJoin(parts)} that this export does not. Import needs an empty space: a new Supabase project, or \`supabase db reset\` on a local stack.`;
185
+ return `The target space holds ${listJoin(parts)} that this export does not. Import needs an empty space: import into a new one.`;
175
186
  }
176
- /** Spec §6: the target database's version must equal the export's. */
187
+ /** Spec §6: the target space's schema version must equal the export's. A space's schema is its object's step list,
188
+ * brought forward by its service (spec 2026-10-15 the space object), so the fix is the other side's service. */
177
189
  export function versionMismatchMessage(target, exported) {
178
190
  return target < exported
179
- ? `The target database is at schema v${target} and this export is v${exported}: update smoodly to a version that ships v${exported}, run \`npx smoodly migrate\`, then \`supabase db push\`.`
180
- : `The target database is at schema v${target}, newer than this export (v${exported}): migrate the source and export again.`;
191
+ ? `The target space is at schema v${target} and this export is v${exported}: a newer service wrote it. Import it into a space whose service has reached v${exported}.`
192
+ : `The target space is at schema v${target}, newer than this export (v${exported}): export the source again once its service has reached v${target}.`;
181
193
  }
package/dist/store.d.ts CHANGED
@@ -4,9 +4,8 @@ import type { Addresses } from "./addresses.ts";
4
4
  import type { Anchors } from "./anchors.ts";
5
5
  import { type RefEdge, type SectionLike } from "./refs.ts";
6
6
  import type { Toolset } from "./toolset.ts";
7
- import type { MemoryRefIndex } from "./ref-index.ts";
8
- import { type PathHit, type PathIndex, type PathRow } from "./path-index.ts";
9
- import { type DraftToken } from "./draft-token.ts";
7
+ import type { PathHit, PathIndex, PathRow } from "./path-index.ts";
8
+ import type { DraftToken } from "./draft-token.ts";
10
9
  import { type LocaleSet, type PagePathOptions, type PathTarget } from "./paths.ts";
11
10
  export type PageLocale = {
12
11
  /** The node's OWN segment in this locale; the URL is derived (paths.ts). */
@@ -71,8 +70,6 @@ export interface PageStore {
71
70
  /** Every page with each locale's draft and live tree, in few reads (spec 2026-09-27 editor safety §3). */
72
71
  scanTrees(): Promise<PageScan[]>;
73
72
  resolvePath(locale: string, path: string): Promise<PathHit | null>;
74
- /** The database's schema version from `smoodly_meta`; null when the table is missing (older than v1). Memory: always current (spec 2026-09-26 §5). */
75
- schemaVersion(): Promise<number | null>;
76
73
  /** locale → path, for the locales the page exists in. */
77
74
  pathsOf(id: string): Promise<Record<string, string>>;
78
75
  /** locale → the target's live path, for any page or entry — the renderer's redirect branch (spec 2026-09-26 redirects §7). */
@@ -187,7 +184,6 @@ export declare const pageErrors: {
187
184
  fixedTaken: (key: string) => Error;
188
185
  fixedAtRoot: () => Error;
189
186
  };
190
- export { assertSupportedLocale } from "./paths.ts";
191
187
  /** The page's row for a locale — the "does not exist in locale" guard every per-locale write runs. */
192
188
  export declare function localeRow(page: PageRecord, locale: string): PageLocale;
193
189
  /** A page's refs rows cover EVERY live tree — each locale's draft and
@@ -199,77 +195,3 @@ export declare function pageRefEdges(trees: PageTree[], sections: SectionLike[],
199
195
  * THERE — every referenced entry must exist, have a row in that locale,
200
196
  * and be published in it. Throws naming the offenders. */
201
197
  export declare function assertPublishable(label: string, tree: PageTree, sections: SectionLike[], entries: EntryStore, locale: string): Promise<void>;
202
- export declare class MemoryPageStore implements PageStore {
203
- private options;
204
- private pages;
205
- private versions;
206
- /** Append-only creation-order tie-break; `size + 1` stays fresh only because entries are never deleted. */
207
- private seq;
208
- private paths;
209
- private opts;
210
- constructor(options?: PageStoreOptions & {
211
- refs?: MemoryRefIndex;
212
- });
213
- private byId;
214
- private must;
215
- private isHome;
216
- private snapshot;
217
- private children;
218
- private hasChildren;
219
- private siblings;
220
- /** Sibling slugs are unique per parent PER LOCALE — a friendlier error
221
- * than the path index's, which stays the real invariant. Code-owned
222
- * roots are left out: their path is their address, and the path index
223
- * guards it (spec 2026-09-29 nested paths §3). */
224
- private assertSlugFree;
225
- private subtree;
226
- private writePaths;
227
- /** Every locale's draft and published trees, deduped by version id. */
228
- private liveTrees;
229
- private writeRefs;
230
- private newVersion;
231
- /** The draft rule (spec 2026-09-13 history §1): the unpublished draft row is
232
- * rewritten in place; a new row starts only when the draft IS the published one. */
233
- private writeDraftVersion;
234
- private dropVersions;
235
- createPage(input: CreatePageInput, by?: string): Promise<PageRecord>;
236
- getPage(id: string): Promise<PageRecord | null>;
237
- scanTrees(): Promise<PageScan[]>;
238
- listPages(): Promise<PageRecord[]>;
239
- resolvePath(locale: string, path: string): Promise<PathHit | null>;
240
- schemaVersion(): Promise<number>;
241
- pathsOf(id: string): Promise<Record<string, string>>;
242
- targetPaths(target: PathTarget): Promise<Record<string, string>>;
243
- redirectsOf(id: string): Promise<Record<string, string[]>>;
244
- listLive(locale: string): Promise<PathRow[]>;
245
- listRedirects(locale: string): Promise<PathRow[]>;
246
- addRedirect(row: PathRow): Promise<void>;
247
- removeRedirect(locale: string, path: string): Promise<void>;
248
- addLocale(id: string, locale: string, options: AddLocaleOptions, by?: string): Promise<PageRecord>;
249
- removeLocale(id: string, locale: string): Promise<PageRecord>;
250
- rename(id: string, patch: {
251
- locale: string;
252
- slug?: string;
253
- title?: string;
254
- }, by?: string, options?: {
255
- redirect?: boolean;
256
- }): Promise<PageRecord>;
257
- move(id: string, to: {
258
- parentId: string | null;
259
- index?: number;
260
- }, options?: {
261
- redirect?: boolean;
262
- }): Promise<PageRecord>;
263
- setTemplate(id: string, template: string, by?: string): Promise<PageRecord>;
264
- saveDraft(id: string, locale: string, tree: PageTree, by?: string, expected?: DraftToken | null): Promise<PageVersion>;
265
- publish(id: string, locale: string, by?: string, expected?: DraftToken | null): Promise<PageRecord>;
266
- discardDraft(id: string, locale: string, by?: string, expected?: DraftToken | null): Promise<PageRecord>;
267
- unpublish(id: string, locale: string, by?: string): Promise<PageRecord>;
268
- restore(id: string, locale: string, versionId: string, by?: string, expected?: DraftToken | null): Promise<PageRecord>;
269
- getVersion(versionId: string): Promise<PageVersion | null>;
270
- getDraftTree(id: string, locale: string): Promise<PageTree | null>;
271
- fieldsOf(versionIds: string[]): Promise<Record<string, Record<string, unknown>>>;
272
- getPublishedTree(id: string, locale: string): Promise<PageTree | null>;
273
- listVersions(id: string, locale: string): Promise<PageVersion[]>;
274
- deletePage(id: string): Promise<void>;
275
- }