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
package/dist/store.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // The page persistence contract. Writes always go through this API —
2
2
  // never raw table access — so validation, versioning and publish hooks
3
- // can't be bypassed (DESIGN.md §1). MemoryPageStore is the reference
4
- // implementation and the contract-test subject; the Supabase adapter
3
+ // can't be bypassed (DESIGN.md §1). MemoryPageStore (packages/service/src/stores)
4
+ // is the reference implementation and the contract-test subject; the space object's SQLite store
5
5
  // implements the same interface over pages/page_locales/page_versions.
6
6
  //
7
7
  // The model (spec 2026-09-06): a page is ONE node with ONE row per
@@ -12,10 +12,7 @@
12
12
  import { ASSET_COLLECTION } from "./media.js";
13
13
  import { collectTreeRefs } from "./refs.js";
14
14
  import { customToolset, withCustomSchema } from "./custom/index.js";
15
- import { MemoryPathIndex } from "./path-index.js";
16
- import { assertFresh } from "./draft-token.js";
17
- import { SCHEMA_VERSION } from "./schema-version.js";
18
- import { assertDepth, assertSegment, assertSupportedLocale, chainOf, isHomeNode, pagePathRows, unsupportedLocale, rewriteTargets, } from "./paths.js";
15
+ import { unsupportedLocale, } from "./paths.js";
19
16
  export const DEFAULT_LOCALES = { default: "en", supported: ["en"] };
20
17
  /** The section list every adapter walks refs with: the registered ones plus the custom section's schema (spec 2026-09-14 §6). */
21
18
  /** Group an index's redirect rows by locale, paths sorted. */
@@ -65,7 +62,6 @@ export const pageErrors = {
65
62
  fixedTaken: (key) => new Error(`smoodly: a page owned by "${key}" already exists.`),
66
63
  fixedAtRoot: () => new Error("smoodly: a code-owned page is a root record."),
67
64
  };
68
- export { assertSupportedLocale } from "./paths.js";
69
65
  /** The page's row for a locale — the "does not exist in locale" guard every per-locale write runs. */
70
66
  export function localeRow(page, locale) {
71
67
  const row = page.locales[locale];
@@ -107,485 +103,3 @@ export async function assertPublishable(label, tree, sections, entries, locale)
107
103
  throw new Error(`smoodly: cannot publish "${label}" — it references entries that are not published: ${offenders.join(", ")}.`);
108
104
  }
109
105
  }
110
- let counter = 0;
111
- const uid = () => `id-${++counter}-${Date.now().toString(36)}`;
112
- /** A version's saved time: wall-clock, but strictly increasing, so two saves in one
113
- * millisecond still move the draft token (spec 2026-09-26 concurrency §1) the way
114
- * Postgres's microsecond `now()` does. */
115
- let lastSavedAt = 0;
116
- const savedClock = () => (lastSavedAt = Math.max(Date.now(), lastSavedAt + 1));
117
- /** The row's audit pair: every write that touches a locale row sets both (spec 2026-09-13 history §2). */
118
- function touchRow(row, by) {
119
- row.updatedAt = Date.now();
120
- row.updatedBy = by ?? null;
121
- }
122
- export class MemoryPageStore {
123
- constructor(options = {}) {
124
- this.options = options;
125
- this.pages = new Map();
126
- this.versions = new Map();
127
- /** Append-only creation-order tie-break; `size + 1` stays fresh only because entries are never deleted. */
128
- this.seq = new Map();
129
- this.byId = (id) => this.pages.get(id);
130
- this.options = { ...options, sections: refSections(options) };
131
- this.opts = pathOptions(options);
132
- // Alone, the store's own index reads the addresses it was given (spec 2026-09-29 anchored paths §2).
133
- this.paths = options.paths ?? new MemoryPathIndex(storeAnchors(options));
134
- }
135
- must(id) {
136
- const page = this.pages.get(id);
137
- if (!page)
138
- throw pageErrors.notFound(id);
139
- return page;
140
- }
141
- isHome(page) {
142
- return isHomeNode(page, this.opts);
143
- }
144
- snapshot(page) {
145
- return { ...page, locales: structuredClone(page.locales) };
146
- }
147
- children(id) {
148
- return [...this.pages.values()].filter((p) => p.parentId === id);
149
- }
150
- hasChildren(id) {
151
- return this.children(id).length > 0;
152
- }
153
- siblings(parentId, except) {
154
- return [...this.pages.values()]
155
- .filter((p) => p.parentId === parentId && p.id !== except)
156
- // null sorts last; the SQL equivalent is ORDER BY sort ASC NULLS LAST, created_at ASC.
157
- .sort((a, b) => (a.sort ?? Infinity) - (b.sort ?? Infinity) || this.seq.get(a.id) - this.seq.get(b.id));
158
- }
159
- /** Sibling slugs are unique per parent PER LOCALE — a friendlier error
160
- * than the path index's, which stays the real invariant. Code-owned
161
- * roots are left out: their path is their address, and the path index
162
- * guards it (spec 2026-09-29 nested paths §3). */
163
- assertSlugFree(parentId, locale, slug, except) {
164
- if (this.siblings(parentId, except).some((p) => !p.fixed && p.locales[locale]?.slug === slug))
165
- throw pageErrors.slugTaken(slug);
166
- }
167
- subtree(id) {
168
- const out = [];
169
- const walk = (parentId) => {
170
- for (const p of this.pages.values())
171
- if (p.parentId === parentId) {
172
- out.push(p);
173
- walk(p.id);
174
- }
175
- };
176
- const root = this.must(id);
177
- out.push(root);
178
- walk(id);
179
- return out;
180
- }
181
- async writePaths(nodes, redirect = true) {
182
- await this.paths.rewrite(rewriteTargets("page", nodes, redirect), pagePathRows(nodes, this.byId, this.opts));
183
- }
184
- /** Every locale's draft and published trees, deduped by version id. */
185
- liveTrees(page) {
186
- const ids = new Set();
187
- for (const row of Object.values(page.locales)) {
188
- if (row.draftVersionId)
189
- ids.add(row.draftVersionId);
190
- if (row.publishedVersionId)
191
- ids.add(row.publishedVersionId);
192
- }
193
- const trees = [];
194
- for (const id of ids) {
195
- const tree = this.versions.get(id)?.tree;
196
- if (tree)
197
- trees.push(tree);
198
- }
199
- return trees;
200
- }
201
- writeRefs(page) {
202
- const { sections, elements, refs } = this.options;
203
- if (!sections || !refs)
204
- return;
205
- refs.rewrite({ sourceKind: "page", sourceId: page.id }, pageRefEdges(this.liveTrees(page), sections, elements));
206
- }
207
- newVersion(pageId, locale, tree) {
208
- const version = { id: uid(), pageId, locale, tree: structuredClone(tree), savedAt: savedClock() };
209
- this.versions.set(version.id, version);
210
- return version;
211
- }
212
- /** The draft rule (spec 2026-09-13 history §1): the unpublished draft row is
213
- * rewritten in place; a new row starts only when the draft IS the published one. */
214
- writeDraftVersion(page, locale, tree, by) {
215
- const row = localeRow(page, locale);
216
- const current = row.draftVersionId ? this.versions.get(row.draftVersionId) : undefined;
217
- let version;
218
- if (current && row.draftVersionId !== row.publishedVersionId) {
219
- current.tree = structuredClone(tree);
220
- current.savedAt = savedClock();
221
- row.draftSavedAt = String(current.savedAt);
222
- version = current;
223
- }
224
- else {
225
- version = this.newVersion(page.id, locale, tree);
226
- row.draftVersionId = version.id;
227
- row.draftSavedAt = String(version.savedAt);
228
- }
229
- touchRow(row, by);
230
- return version;
231
- }
232
- dropVersions(pageId, locale) {
233
- for (const [vid, v] of this.versions) {
234
- if (v.pageId === pageId && (locale === undefined || v.locale === locale))
235
- this.versions.delete(vid);
236
- }
237
- }
238
- async createPage(input, by) {
239
- assertSupportedLocale(input.locale, this.opts.locales);
240
- assertSegment(input.slug, "page slug");
241
- const parentId = input.parentId ?? null;
242
- if (input.fixed !== undefined) {
243
- if (parentId !== null)
244
- throw pageErrors.fixedAtRoot();
245
- if ([...this.pages.values()].some((p) => p.fixed === input.fixed))
246
- throw pageErrors.fixedTaken(input.fixed);
247
- }
248
- if (parentId !== null) {
249
- const parent = this.must(parentId);
250
- if (this.isHome(parent))
251
- throw pageErrors.homeChildren();
252
- if (!parent.locales[input.locale])
253
- throw pageErrors.parentNotInLocale(input.locale);
254
- assertDepth(chainOf(parent, this.byId).length + 1, this.opts.depth, "the page");
255
- }
256
- if (input.fixed === undefined)
257
- this.assertSlugFree(parentId, input.locale, input.slug);
258
- const id = uid();
259
- const version = input.template === null ? null : this.newVersion(id, input.locale, { zones: {} });
260
- const page = {
261
- id,
262
- parentId,
263
- template: input.template,
264
- // An ordering hint, not a unique key: duplicates are possible after
265
- // moves/deletes, and the creation-order tie-break settles them.
266
- sort: this.siblings(parentId).length + 1,
267
- fixed: input.fixed ?? null,
268
- locales: {
269
- [input.locale]: {
270
- slug: input.slug,
271
- title: input.title ?? input.slug,
272
- status: "draft",
273
- draftVersionId: version?.id ?? null,
274
- draftSavedAt: version ? String(version.savedAt) : null,
275
- publishedVersionId: null,
276
- updatedAt: Date.now(),
277
- updatedBy: by ?? null,
278
- },
279
- },
280
- };
281
- this.pages.set(id, page);
282
- this.seq.set(id, this.seq.size + 1);
283
- try {
284
- await this.writePaths([page]);
285
- }
286
- catch (e) {
287
- this.pages.delete(id);
288
- if (version)
289
- this.versions.delete(version.id);
290
- throw e;
291
- }
292
- return this.snapshot(page);
293
- }
294
- async getPage(id) {
295
- const page = this.pages.get(id);
296
- return page ? this.snapshot(page) : null;
297
- }
298
- async scanTrees() {
299
- const tree = (id) => (id ? structuredClone(this.versions.get(id)?.tree ?? null) : null);
300
- return (await this.listPages()).map((record) => ({
301
- record,
302
- draft: Object.fromEntries(Object.entries(record.locales).map(([l, row]) => [l, tree(row.draftVersionId)])),
303
- live: Object.fromEntries(Object.entries(record.locales).map(([l, row]) => [l, tree(row.publishedVersionId)])),
304
- }));
305
- }
306
- async listPages() {
307
- return [...this.pages.values()]
308
- // null sorts last; the SQL equivalent is ORDER BY sort ASC NULLS LAST, created_at ASC.
309
- .sort((a, b) => (a.sort ?? Infinity) - (b.sort ?? Infinity) || this.seq.get(a.id) - this.seq.get(b.id))
310
- .map((p) => this.snapshot(p));
311
- }
312
- resolvePath(locale, path) {
313
- return this.paths.resolve(locale, path);
314
- }
315
- schemaVersion() {
316
- return Promise.resolve(SCHEMA_VERSION);
317
- }
318
- pathsOf(id) {
319
- return this.paths.pathsOf({ kind: "page", id });
320
- }
321
- targetPaths(target) { return this.paths.pathsOf(target); }
322
- async redirectsOf(id) { return groupRedirects(await this.paths.redirectsOf({ kind: "page", id })); }
323
- listLive(locale) { return this.paths.listLive(locale); }
324
- listRedirects(locale) { return this.paths.listRedirects(locale); }
325
- addRedirect(row) { return this.paths.addRedirect(row); }
326
- removeRedirect(locale, path) { return this.paths.removeRedirect(locale, path); }
327
- async addLocale(id, locale, options, by) {
328
- assertSupportedLocale(locale, this.opts.locales);
329
- const page = this.must(id);
330
- if (page.locales[locale])
331
- throw pageErrors.alreadyInLocale(id, locale);
332
- const source = localeRow(page, options.from);
333
- const slug = options.slug ?? source.slug;
334
- if (options.slug !== undefined)
335
- assertSegment(slug, `page slug (${locale})`);
336
- if (page.parentId !== null && !this.must(page.parentId).locales[locale])
337
- throw pageErrors.parentNotInLocale(locale);
338
- if (!page.fixed)
339
- this.assertSlugFree(page.parentId, locale, slug, id);
340
- // The SOURCE'S DRAFT is the copy: what the editor sees on screen when
341
- // they add the locale (spec §3). A folder has no tree to copy.
342
- const version = page.template === null
343
- ? null
344
- : this.newVersion(id, locale, (source.draftVersionId && this.versions.get(source.draftVersionId)?.tree) || { zones: {} });
345
- page.locales[locale] = {
346
- slug,
347
- title: options.title ?? source.title,
348
- status: "draft",
349
- draftVersionId: version?.id ?? null,
350
- draftSavedAt: version ? String(version.savedAt) : null,
351
- publishedVersionId: null,
352
- updatedAt: Date.now(),
353
- updatedBy: by ?? null,
354
- };
355
- try {
356
- await this.writePaths([page]);
357
- }
358
- catch (e) {
359
- delete page.locales[locale];
360
- if (version)
361
- this.versions.delete(version.id);
362
- throw e;
363
- }
364
- this.writeRefs(page);
365
- return this.snapshot(page);
366
- }
367
- async removeLocale(id, locale) {
368
- const page = this.must(id);
369
- const removed = localeRow(page, locale);
370
- if (Object.keys(page.locales).length === 1)
371
- throw pageErrors.lastLocale(id);
372
- // Under the parent rule only direct children can hold the locale, but
373
- // the walk is cheap and the message must name every blocker.
374
- const blockers = this.subtree(id).filter((n) => n.id !== id && n.locales[locale]);
375
- if (blockers.length > 0)
376
- throw pageErrors.localeInUse(locale, blockers.map((b) => b.locales[locale].title));
377
- delete page.locales[locale];
378
- try {
379
- await this.writePaths([page], false);
380
- }
381
- catch (e) {
382
- page.locales[locale] = removed;
383
- throw e;
384
- }
385
- this.dropVersions(id, locale);
386
- this.writeRefs(page);
387
- return this.snapshot(page);
388
- }
389
- async rename(id, patch, by, options) {
390
- const page = this.must(id);
391
- const row = localeRow(page, patch.locale);
392
- if (patch.slug !== undefined)
393
- assertSegment(patch.slug, `page slug (${patch.locale})`);
394
- // A rename that turns this node INTO the home node (root, default-locale
395
- // slug === homePageSlug) is subject to the same invariant as createPage:
396
- // the home page cannot have child pages.
397
- if (patch.locale === this.opts.locales.default &&
398
- patch.slug !== undefined &&
399
- page.parentId === null &&
400
- patch.slug === this.opts.homePageSlug &&
401
- this.hasChildren(id)) {
402
- throw pageErrors.homeChildren();
403
- }
404
- if (patch.slug !== undefined && !page.fixed)
405
- this.assertSlugFree(page.parentId, patch.locale, patch.slug, id);
406
- const before = { ...row };
407
- if (patch.slug !== undefined)
408
- row.slug = patch.slug;
409
- if (patch.title !== undefined)
410
- row.title = patch.title;
411
- try {
412
- await this.writePaths(this.subtree(id), options?.redirect ?? true);
413
- }
414
- catch (e) {
415
- Object.assign(row, before);
416
- throw e;
417
- }
418
- touchRow(row, by);
419
- return this.snapshot(page);
420
- }
421
- async move(id, to, options) {
422
- const page = this.must(id);
423
- if (page.fixed && to.parentId !== null)
424
- throw pageErrors.fixedAtRoot();
425
- if (to.parentId !== null) {
426
- const parent = this.must(to.parentId);
427
- if (to.parentId === id || chainOf(parent, this.byId).some((n) => n.id === id))
428
- throw pageErrors.cycle();
429
- if (this.isHome(parent))
430
- throw pageErrors.homeChildren();
431
- // The parent rule, on the move: every locale the page has must be
432
- // present at the destination, or its chain would break there.
433
- for (const locale of Object.keys(page.locales))
434
- if (!parent.locales[locale])
435
- throw pageErrors.moveLocales(locale);
436
- const subtreeDepth = Math.max(...this.subtree(id).map((n) => chainOf(n, this.byId).length)) - chainOf(page, this.byId).length + 1;
437
- assertDepth(chainOf(parent, this.byId).length + subtreeDepth, this.opts.depth, "the page");
438
- }
439
- else if (page.locales[this.opts.locales.default]?.slug === this.opts.homePageSlug && this.hasChildren(id)) {
440
- // Moving a home-slugged node TO the root is the mirror case of the
441
- // rename guard above: it would make it the home node while it still
442
- // has children.
443
- throw pageErrors.homeChildren();
444
- }
445
- if (to.parentId !== page.parentId) {
446
- for (const [locale, row] of Object.entries(page.locales))
447
- this.assertSlugFree(to.parentId, locale, row.slug, id);
448
- }
449
- const before = { parentId: page.parentId, sort: page.sort };
450
- page.parentId = to.parentId;
451
- const siblings = this.siblings(to.parentId, id);
452
- const index = Math.min(Math.max(to.index ?? siblings.length, 0), siblings.length);
453
- siblings.splice(index, 0, page);
454
- const previousSort = new Map(siblings.map((s) => [s.id, s.sort]));
455
- siblings.forEach((s, i) => (s.sort = i + 1));
456
- try {
457
- await this.writePaths(this.subtree(id), options?.redirect ?? true);
458
- }
459
- catch (e) {
460
- Object.assign(page, before);
461
- for (const s of siblings)
462
- s.sort = previousSort.get(s.id) ?? null;
463
- throw e;
464
- }
465
- return this.snapshot(page);
466
- }
467
- async setTemplate(id, template, by) {
468
- const page = this.must(id);
469
- if (page.template !== null)
470
- throw pageErrors.hasTemplate(id);
471
- page.template = template;
472
- for (const [locale, row] of Object.entries(page.locales)) {
473
- const version = this.newVersion(id, locale, { zones: {} });
474
- row.draftVersionId = version.id;
475
- row.draftSavedAt = String(version.savedAt);
476
- touchRow(row, by);
477
- }
478
- return this.snapshot(page);
479
- }
480
- async saveDraft(id, locale, tree, by, expected) {
481
- const page = this.must(id);
482
- if (page.template === null)
483
- throw pageErrors.folder(id);
484
- assertFresh(localeRow(page, locale), expected);
485
- const version = this.writeDraftVersion(page, locale, tree, by);
486
- this.writeRefs(page);
487
- return { ...version, tree: structuredClone(version.tree) };
488
- }
489
- async publish(id, locale, by, expected) {
490
- const page = this.must(id);
491
- if (page.template === null)
492
- throw pageErrors.folder(id);
493
- const row = localeRow(page, locale);
494
- assertFresh(row, expected);
495
- if (!row.draftVersionId)
496
- throw pageErrors.folder(id);
497
- const { sections, entries } = this.options;
498
- const draft = this.versions.get(row.draftVersionId)?.tree ?? { zones: {} };
499
- if (sections && entries)
500
- await assertPublishable(row.slug, draft, sections, entries, locale);
501
- row.publishedVersionId = row.draftVersionId;
502
- row.status = "published";
503
- touchRow(row, by);
504
- this.writeRefs(page);
505
- return this.snapshot(page);
506
- }
507
- async discardDraft(id, locale, by, expected) {
508
- const page = this.must(id);
509
- const row = localeRow(page, locale);
510
- assertFresh(row, expected);
511
- if (!row.publishedVersionId)
512
- throw pageErrors.nothingPublished(id, locale);
513
- const abandoned = row.draftVersionId;
514
- row.draftVersionId = row.publishedVersionId;
515
- row.draftSavedAt = String(this.versions.get(row.publishedVersionId).savedAt);
516
- if (abandoned && abandoned !== row.publishedVersionId)
517
- this.versions.delete(abandoned); // never shown as a stray draft
518
- touchRow(row, by);
519
- this.writeRefs(page); // the refs union (draft ∪ published) just shrank to one tree
520
- return this.snapshot(page);
521
- }
522
- async unpublish(id, locale, by) {
523
- const page = this.must(id);
524
- const row = localeRow(page, locale);
525
- row.status = "draft";
526
- touchRow(row, by);
527
- this.writeRefs(page);
528
- return this.snapshot(page);
529
- }
530
- async restore(id, locale, versionId, by, expected) {
531
- const page = this.must(id);
532
- if (page.template === null)
533
- throw pageErrors.folder(id);
534
- const row = localeRow(page, locale);
535
- const version = this.versions.get(versionId);
536
- if (!version || version.pageId !== id || version.locale !== locale)
537
- throw pageErrors.foreignVersion(id, locale, versionId);
538
- // The memory store is single-threaded: the read-side check IS the atomic check.
539
- assertFresh(row, expected);
540
- if (versionId === row.publishedVersionId)
541
- return this.discardDraft(id, locale, by, expected);
542
- this.writeDraftVersion(page, locale, version.tree, by);
543
- this.writeRefs(page);
544
- return this.snapshot(page);
545
- }
546
- async getVersion(versionId) {
547
- const v = this.versions.get(versionId);
548
- return v ? { ...v, tree: structuredClone(v.tree) } : null;
549
- }
550
- async getDraftTree(id, locale) {
551
- const row = this.pages.get(id)?.locales[locale];
552
- if (!row?.draftVersionId)
553
- return null;
554
- return structuredClone(this.versions.get(row.draftVersionId)?.tree ?? null);
555
- }
556
- async fieldsOf(versionIds) {
557
- const out = {};
558
- for (const id of versionIds) {
559
- const v = this.versions.get(id);
560
- if (v)
561
- out[id] = structuredClone(v.tree.fields ?? {});
562
- }
563
- return out;
564
- }
565
- async getPublishedTree(id, locale) {
566
- const row = this.pages.get(id)?.locales[locale];
567
- if (!row?.publishedVersionId)
568
- return null;
569
- return structuredClone(this.versions.get(row.publishedVersionId)?.tree ?? null);
570
- }
571
- async listVersions(id, locale) {
572
- return [...this.versions.values()]
573
- .filter((v) => v.pageId === id && v.locale === locale)
574
- .sort((a, b) => b.savedAt - a.savedAt || b.id.localeCompare(a.id))
575
- .map((v) => ({ ...v, tree: structuredClone(v.tree) }));
576
- }
577
- async deletePage(id) {
578
- const page = this.pages.get(id);
579
- if (!page)
580
- return;
581
- if (this.hasChildren(id))
582
- throw pageErrors.hasChildren(id);
583
- // The index removal goes first: in the Supabase adapter it is a network
584
- // call that can fail, and a failed rewrite must leave this store's own
585
- // state (versions, refs, the page map) untouched.
586
- await this.paths.remove([{ kind: "page", id }]);
587
- this.dropVersions(id);
588
- this.options.refs?.remove({ sourceKind: "page", sourceId: id });
589
- this.pages.delete(id);
590
- }
591
- }
package/dist/toolset.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export declare const TOOLSET_MARKS: readonly ["bold", "italic", "link"];
2
- export declare const TOOLSET_NODES: readonly ["h1", "h2", "h3", "h4", "h5", "h6", "bulletList", "orderedList", "blockquote", "image"];
2
+ declare const TOOLSET_NODES: readonly ["h1", "h2", "h3", "h4", "h5", "h6", "bulletList", "orderedList", "blockquote", "image"];
3
3
  export type ToolsetMark = (typeof TOOLSET_MARKS)[number];
4
4
  export type ToolsetNode = (typeof TOOLSET_NODES)[number];
5
5
  export type Toolset = {
@@ -32,3 +32,4 @@ export type Allowed = {
32
32
  elements: string[];
33
33
  };
34
34
  export declare function allowedOf(input: unknown): Allowed;
35
+ export {};
package/dist/toolset.js CHANGED
@@ -5,7 +5,7 @@ import { schemaOnly } from "./registration.js";
5
5
  // registration; a field without a toolset carries the default. `Allowed`
6
6
  // is the resolved form the widget reads (spec 2026-09-08 §1).
7
7
  export const TOOLSET_MARKS = ["bold", "italic", "link"];
8
- export const TOOLSET_NODES = ["h1", "h2", "h3", "h4", "h5", "h6", "bulletList", "orderedList", "blockquote", "image"];
8
+ const TOOLSET_NODES = ["h1", "h2", "h3", "h4", "h5", "h6", "bulletList", "orderedList", "blockquote", "image"];
9
9
  /** What a richtext field gets when it declares nothing. `h1` is left out:
10
10
  * the section or page shape owns the page's first heading. */
11
11
  export const DEFAULT_TOOLSET = {
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const SMOODLY_VERSION = "0.0.51";
1
+ export declare const SMOODLY_VERSION = "0.0.52";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/write-version.mjs from package.json — do not edit.
2
- export const SMOODLY_VERSION = "0.0.51";
2
+ export const SMOODLY_VERSION = "0.0.52";
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "smoodly",
3
- "version": "0.0.51",
4
- "description": "Open-source, code-first CMS for Astro and Next.js on Supabase: define a component once, it becomes a typed, editable building block.",
3
+ "version": "0.0.52",
4
+ "description": "Open-source, code-first CMS for Astro and Next.js: define a component once, it becomes a typed, editable building block.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/laniakea-studio/smoodly-cms.git",
8
+ "url": "git+https://github.com/smoodly-io/smoodly.git",
9
9
  "directory": "packages/smoodly"
10
10
  },
11
11
  "type": "module",
@@ -47,10 +47,6 @@
47
47
  "types": "./dist/image/index.d.ts",
48
48
  "default": "./dist/image/index.js"
49
49
  },
50
- "./image/config": {
51
- "types": "./dist/image/next-config.d.ts",
52
- "default": "./dist/image/next-config.js"
53
- },
54
50
  "./richtext": {
55
51
  "types": "./dist/richtext/index.d.ts",
56
52
  "default": "./dist/richtext/index.js"
@@ -58,15 +54,10 @@
58
54
  "./admin": {
59
55
  "types": "./dist/admin/index.d.ts",
60
56
  "default": "./dist/admin/index.js"
61
- },
62
- "./migrations": {
63
- "types": "./dist/migrations.d.ts",
64
- "default": "./dist/migrations.js"
65
57
  }
66
58
  },
67
59
  "files": [
68
60
  "dist",
69
- "migrations",
70
61
  "bin"
71
62
  ],
72
63
  "scripts": {
@@ -76,8 +67,7 @@
76
67
  "pretest": "node scripts/write-version.mjs && npm run -s build:admin",
77
68
  "test": "vitest run",
78
69
  "test:watch": "vitest",
79
- "typecheck": "tsc --noEmit",
80
- "test:supabase": "bash scripts/test-supabase.sh"
70
+ "typecheck": "tsc --noEmit"
81
71
  },
82
72
  "peerDependencies": {
83
73
  "astro": "^7.3.5",
@@ -123,7 +113,6 @@
123
113
  "vitest": "^3"
124
114
  },
125
115
  "dependencies": {
126
- "@supabase/supabase-js": "^2.113.0",
127
116
  "tsx": "^4.23.15"
128
117
  }
129
118
  }
@@ -1,51 +0,0 @@
1
- import type { ResolvedConfig } from "../config.ts";
2
- import type { EntryStore } from "../entry-store.ts";
3
- import type { AssetStore } from "../asset-store.ts";
4
- import type { MemberStore } from "../member-store.ts";
5
- import type { PageStore } from "../store.ts";
6
- import type { AdminOpCall, AdminOps, OpResult } from "./ops.ts";
7
- import type { PublishedStore } from "../published.ts";
8
- import type { AdminSession } from "./auth.ts";
9
- import type { Projector } from "../project.ts";
10
- export type AdminEffects = {
11
- /** Expire one cache tag (`pageTag(id)` / `entryTag(id)`). */
12
- revalidate?(tag: string): void;
13
- draft?: {
14
- enable(): void | Promise<void>;
15
- disable(): void | Promise<void>;
16
- };
17
- };
18
- export declare function createAdminOps(deps: {
19
- config: ResolvedConfig;
20
- pages: PageStore;
21
- entries: EntryStore;
22
- effects?: AdminEffects;
23
- /** Locale-relative CMS path → site URL (the renderer's seam, applied here for the admin). Default: identity. */
24
- href?: (path: string, locale: string) => string;
25
- /** The media library; absent = every assets op answers "no media store". */
26
- assets?: AssetStore;
27
- /** The admission table (spec 2026-09-25 §4); absent = members.list answers "no member store". */
28
- members?: MemberStore;
29
- /** The verified editor; their email is every write's `by`. Absent for the seed and tests (spec 2026-09-13 history §2). */
30
- actor?: AdminSession;
31
- /** Writes the delivery rows (spec 2026-10-07 delivery model §2): the site's `site.projector`. Every op that
32
- * expires a page's or an entry's tag projects that page or entry first, so the first read after it finds
33
- * the new document — a draft save excepted (R17). The truth is written before; a projection that throws is
34
- * logged, its targets marked stale to heal on read, and the op succeeds. */
35
- projector: Projector;
36
- /** Static mode's Deploy tile (spec 2026-10-09 static mode §5–§7); absent on a live site, where the group answers not_found. */
37
- deploy?: DeployDeps;
38
- }): AdminOps;
39
- /** What the deploy operations need (spec 2026-10-09 static mode §7): the live site's origin, where the marker is
40
- * read from; the host's hook from SMOODLY_DEPLOY_HOOK (absent = Deploy disabled); the delivery store's reads. */
41
- export type DeployDeps = {
42
- siteUrl: string;
43
- hook?: string;
44
- store: Pick<PublishedStore, "newestPublishedAt" | "countPublishedAfter" | "deployRequest" | "requestDeploy" | "lastRemovalAt">;
45
- /** Test seams. */
46
- fetch?: typeof fetch;
47
- now?: () => number;
48
- };
49
- /** Path-dispatch for the one-endpoint seam. Own-property checks only —
50
- * "constructor.constructor" and prototype paths must not resolve. */
51
- export declare function runAdminOp(ops: AdminOps, call: AdminOpCall): Promise<OpResult<unknown>>;