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
@@ -1,25 +1,39 @@
1
1
  // packages/smoodly/src/cli/import.ts
2
- // `smoodly import <dir>` (spec 2026-09-28 export/import §9–§10): an
3
- // export folder into the key's space. Every check runs before the first
4
- // write; nothing is overwritten or deleted; a run that stops midway is
5
- // finished by running it again, since the target may hold only this
6
- // export's rows and both writes skip what already landed.
2
+ // `smoodly import <dir>` (spec 2026-09-28 export/import §9–§10; over the API
3
+ // since plan 2026-10-14 the service holds the stores): an export folder into
4
+ // the key's space. Every check runs before the first write; nothing is
5
+ // overwritten or deleted; a run that stops midway is finished by running it
6
+ // again, since the target may hold only this export's rows and both writes
7
+ // skip what already landed. The service writes each row into its own space;
8
+ // a read assembles from what landed, so nothing is rebuilt after.
7
9
  import { existsSync } from "node:fs";
8
10
  import { readFile } from "node:fs/promises";
9
11
  import { join, resolve } from "node:path";
10
- import { MEDIA_CACHE_SECONDS } from "../media.js";
11
- import { CONTENT_TABLES } from "../sql-space.js";
12
- import { CONFLICT_COLUMNS, FORMAT, ROW_KEY, describeImport, emptyTables, foreignRows, insertPlan, intoSpace, missingRows, notEmptyMessage, rowKey, versionMismatchMessage, } from "../space-transfer.js";
13
- import { MEDIA_BUCKET, connectFromEnv, isAlreadyExists, readSchemaVersion, readSpaceRows, rebuildSpace, rebuiltMessage, schemaRefusal, } from "./space-io.js";
14
- import { readCurrentSpace } from "../current-space.js";
15
- import { loadConfig } from "./load-config.js";
16
- export const BATCH_ROWS = 500;
17
- export const notAnExport = (dir) => `Refusing: ${dir} is not a Smoodly export (no manifest.json).`;
18
- export const formatMismatch = (format) => `Refusing: this export's format is ${String(format)}; this smoodly reads format ${FORMAT}. Update smoodly.`;
12
+ import { CONTENT_TABLES } from "../space-transfer.js";
13
+ import { FORMAT, describeImport, emptyTables, foreignRows, insertPlan, missingRows, notEmptyMessage, rowKey, versionMismatchMessage, } from "../space-transfer.js";
14
+ import { connectApi } from "./api.js";
15
+ import { mediaUrl, readTable, transferHead } from "./export.js";
16
+ const BATCH_ROWS = 500;
17
+ /** A batch's JSON stays under the API's 4 MB request cap with room for the envelope. */
18
+ const MAX_BATCH_BYTES = 3500000;
19
+ const notAnExport = (dir) => `Refusing: ${dir} is not a Smoodly export (no manifest.json).`;
20
+ const formatMismatch = (format) => `Refusing: this export's format is ${String(format)}; this smoodly reads format ${FORMAT}. Update smoodly.`;
19
21
  export const RUN_AGAIN = "Run the same command again to continue: rows and files that landed are skipped.";
20
- export const MEMBERS_HEADING = "Members in the source — invite them in Settings › Members:";
21
- export const nothingNew = (space) => `Nothing new: space ${space} already holds everything in this export.`;
22
+ export const MEMBERS_HEADING = "Members in the source — add them to the space's members in the project at smoodly.io:";
23
+ export const nothingNew = () => "Nothing new: the space already holds everything in this export.";
22
24
  export const CACHE_NOTE = "Restart or redeploy the app: it may have cached pages from while the space was empty.";
25
+ /** `list` cut into batches of at most `size` rows, each halved while its JSON is over MAX_BATCH_BYTES. */
26
+ function batches(list, size) {
27
+ const out = [];
28
+ for (let i = 0; i < list.length;) {
29
+ let take = Math.min(size, list.length - i);
30
+ while (take > 1 && Buffer.byteLength(JSON.stringify({ rows: list.slice(i, i + take) })) > MAX_BATCH_BYTES)
31
+ take = Math.ceil(take / 2);
32
+ out.push(list.slice(i, i + take));
33
+ i += take;
34
+ }
35
+ return out;
36
+ }
23
37
  export async function runImport(args, out, deps = {}) {
24
38
  const cwd = deps.cwd ?? process.cwd();
25
39
  const dir = resolve(cwd, args.dir);
@@ -33,64 +47,48 @@ export async function runImport(args, out, deps = {}) {
33
47
  out(formatMismatch(manifest.format));
34
48
  return 1;
35
49
  }
36
- const { db } = deps.connection ?? connectFromEnv(cwd);
37
- const schema = await readSchemaVersion(db);
38
- if ("missing" in schema) {
39
- out(schemaRefusal(schema.missing, "Import"));
50
+ const env = deps.env ?? process.env;
51
+ const api = deps.api ?? connectApi(cwd, env);
52
+ // Read after connectApi: it loads the app's .env files.
53
+ const space = env.SMOODLY_SPACE_ID ?? "";
54
+ const head = await transferHead(api, out);
55
+ if (!head)
40
56
  return 1;
41
- }
42
- const space = await readCurrentSpace(db);
43
- if (schema.version !== manifest.schemaVersion) {
44
- out(versionMismatchMessage(schema.version, manifest.schemaVersion));
57
+ if (head.schemaVersion !== manifest.schemaVersion) {
58
+ out(versionMismatchMessage(head.schemaVersion, manifest.schemaVersion));
45
59
  return 1;
46
60
  }
47
61
  const rows = emptyTables();
48
62
  for (const table of CONTENT_TABLES)
49
63
  rows[table] = JSON.parse(await readFile(join(dir, "rows", `${table}.json`), "utf8"));
50
64
  const target = {};
51
- for (const table of CONTENT_TABLES) {
52
- target[table] = (await readSpaceRows(db, table, space, {}, ROW_KEY[table].join(","))).map((row) => rowKey(table, row));
53
- }
65
+ for (const table of CONTENT_TABLES)
66
+ target[table] = (await readTable(api, table, true)).map((row) => rowKey(table, row));
54
67
  const foreign = foreignRows(target, rows);
55
68
  if (CONTENT_TABLES.some((t) => foreign[t] > 0)) {
56
69
  out(notEmptyMessage(foreign));
57
70
  return 1;
58
71
  }
72
+ // The files that travel (a file missing in the source as well is in manifest.media.missing), each sent whole with its
73
+ // length: the service streams it into the bucket (spec 2026-10-18 media on R2 and Images §4).
74
+ const files = rows.assets.map((asset) => ({ path: String(asset.path), mime: String(asset.mime), file: join(dir, "media", String(asset.path)) }))
75
+ .filter((m) => existsSync(m.file));
59
76
  const newRows = missingRows(target, rows);
60
77
  let newFiles = 0;
61
- // The rebuild after the writes assembles with the app's config: a folder without one is refused before them.
62
- const config = deps.config ?? (await loadConfig(cwd));
63
- let rebuilt = { pages: 0, entries: 0 };
64
78
  try {
65
79
  // Media first, so no ready asset row ever points at a missing file.
66
- for (const asset of rows.assets) {
67
- const path = String(asset.path);
68
- const file = join(dir, "media", path);
69
- if (!existsSync(file))
70
- continue; // missing in the source as well (manifest.media.missing)
71
- const up = await db.storage.from(MEDIA_BUCKET).upload(`${space}/${path}`, await readFile(file), {
72
- contentType: String(asset.mime),
73
- upsert: false,
74
- cacheControl: String(MEDIA_CACHE_SECONDS),
75
- });
76
- if (up.error && !isAlreadyExists(up.error))
77
- throw new Error(`uploading ${path}: ${up.error.message}`);
78
- if (!up.error)
80
+ for (const m of files) {
81
+ const up = await api.postBytes(mediaUrl(m.path), new Uint8Array(await readFile(m.file)), m.mime);
82
+ if (up.created)
79
83
  newFiles++;
80
84
  }
81
85
  const size = deps.batchSize ?? BATCH_ROWS;
82
86
  for (const { table, rows: list } of insertPlan(rows)) {
83
- for (let i = 0; i * size < list.length; i++) {
87
+ for (const [i, batch] of batches(list, size).entries()) {
84
88
  deps.beforeBatch?.(table, i);
85
- const batch = list.slice(i * size, (i + 1) * size).map((row) => intoSpace(table, row, space));
86
- const res = await db.from(table).upsert(batch, { onConflict: CONFLICT_COLUMNS[table], ignoreDuplicates: true });
87
- if (res.error)
88
- throw new Error(`inserting ${table}: ${res.error.message}`);
89
+ await api.post(`/transfer/rows/${table}`, { rows: batch });
89
90
  }
90
91
  }
91
- // The delivery tables are derived, never exported: rebuilt from what landed (spec 2026-10-07 delivery model §2).
92
- // Also on a run that landed nothing new, so a run stopped here is finished by running it again.
93
- rebuilt = await rebuildSpace(db, config);
94
92
  }
95
93
  catch (e) {
96
94
  out(`Import stopped: ${e instanceof Error ? e.message : String(e)}`);
@@ -98,12 +96,10 @@ export async function runImport(args, out, deps = {}) {
98
96
  return 1;
99
97
  }
100
98
  if (newRows === 0 && newFiles === 0) {
101
- out(nothingNew(space));
102
- out(rebuiltMessage(rebuilt));
99
+ out(nothingNew());
103
100
  return 0;
104
101
  }
105
102
  out(`Imported ${describeImport(rows)} into space ${space}.`);
106
- out(rebuiltMessage(rebuilt));
107
103
  if (manifest.members.length > 0) {
108
104
  out(MEMBERS_HEADING);
109
105
  out(` ${manifest.members.map((m) => `${m.email ?? "(no email)"} (${m.role})`).join(", ")}`);
@@ -1,5 +1,4 @@
1
1
  import type { ResolvedConfig } from "../config.ts";
2
- export declare const CONFIG_FILES: string[];
3
2
  export declare const noConfig: (cwd: string) => string;
4
3
  export declare function loadConfig(cwd: string): Promise<ResolvedConfig>;
5
4
  /** One named config file (the Astro integration's `config` option names it): the project's tsconfig beside `cwd`
@@ -8,7 +8,7 @@ import { existsSync } from "node:fs";
8
8
  import { dirname, join } from "node:path";
9
9
  import { pathToFileURL } from "node:url";
10
10
  import { tsImport } from "tsx/esm/api";
11
- export const CONFIG_FILES = ["smoodly.config.ts", "smoodly/config.ts", "src/smoodly/config.ts"];
11
+ const CONFIG_FILES = ["smoodly.config.ts", "smoodly/config.ts", "src/smoodly/config.ts"];
12
12
  export const noConfig = (cwd) => `smoodly: no ${CONFIG_FILES.join(", ")} in ${cwd} — run this in your app's folder.`;
13
13
  export async function loadConfig(cwd) {
14
14
  const file = CONFIG_FILES.map((name) => join(cwd, name)).find((path) => existsSync(path));
@@ -1,24 +1,16 @@
1
- import { type MigrateArgs } from "./migrate.ts";
2
1
  import { type ExportArgs } from "./export.ts";
3
2
  import { type ImportArgs } from "./import.ts";
4
- import { type AddAdminArgs } from "./add-admin.ts";
5
- import { type RebuildArgs } from "./rebuild.ts";
6
- export declare const USAGE = "usage: smoodly <command> (smoodly --version prints the version)\n\n migrate [--check] [--collections <file>] [--migrations <dir>]\n Copies the core schema files this package ships that supabase/migrations/\n lacks, and writes one views file for the collections whose view is new or\n changed. Prints the push or reset command; never runs it.\n\n --check write nothing; exit 1 when anything is pending, or when\n SMOODLY_SECRET_KEY (if set) cannot read the database\n --collections <file> the module exporting `collections` (default\n smoodly/collections.ts, or src/smoodly/collections.ts\n when only that one exists)\n --migrations <dir> the app's migration folder (default supabase/migrations)\n\n export [--out <dir>]\n Writes the space SMOODLY_SECRET_KEY names \u2014 every content row, history\n included, and the media files \u2014 into a folder (default\n smoodly-export-<date>). Reads only.\n\n import <dir>\n Writes an export folder into the key's space, which must be empty or hold\n only this export's rows (an earlier run to continue). Never deletes. Ends\n by rebuilding the delivery tables with the app's config (smoodly.config.ts,\n smoodly/config.ts or src/smoodly/config.ts), so run it in the app's folder.\n\n rebuild\n Rewrites the delivery tables of the key's space: the finished document of\n every published page, entry and global, assembled with the app's config.\n A publish keeps them current and a document behind the content model is\n rebuilt by its first visitor; run this after a deploy for a warm start.\n\n add-admin <email> [--admin-url <url>]\n Makes <email> an admin of the key's space: with SMOODLY_ADMIN_PASSWORD\n set, creates the account with that password; without, mails an invite\n whose link lands on --admin-url (default: SITE_URL + /admin).";
3
+ import { type PushArgs } from "./push.ts";
4
+ export declare const USAGE = "usage: smoodly <command> (smoodly --version prints the version)\nEvery command reads SMOODLY_API_URL, SMOODLY_SPACE_ID and SMOODLY_SECRET_KEY from the app's .env.local or .env.\n\n push\n Sends the app's registry (its smoodly config), its address (SITE_URL) and\n the package version to the space. A Next site's prebuild runs it, Astro's\n build and both dev servers push on their own; run it from CI or a script.\n SMOODLY_STATIC=1 marks a static site. As a prebuild it never fails the build.\n\n export [--out <dir>]\n Writes the space SMOODLY_SECRET_KEY names \u2014 every content row, history\n included, and the media files \u2014 into a folder (default\n smoodly-export-<date>). Reads only.\n\n import <dir>\n Writes an export folder into the key's space, which must be empty or hold\n only this export's rows (an earlier run to continue). Never deletes.";
7
5
  export type Parsed = {
8
- command: "migrate";
9
- args: MigrateArgs;
10
- } | {
11
6
  command: "export";
12
7
  args: ExportArgs;
13
8
  } | {
14
9
  command: "import";
15
10
  args: ImportArgs;
16
11
  } | {
17
- command: "add-admin";
18
- args: AddAdminArgs;
19
- } | {
20
- command: "rebuild";
21
- args: RebuildArgs;
12
+ command: "push";
13
+ args: PushArgs;
22
14
  } | {
23
15
  command: "help";
24
16
  } | {
package/dist/cli/main.js CHANGED
@@ -1,29 +1,25 @@
1
1
  // packages/smoodly/src/cli/main.ts
2
- // `smoodly` — the package's command line. Five subcommands: migrate
3
- // (spec 2026-09-26 §2), export and import (spec 2026-09-28), add-admin
4
- // (spec 2026-09-30 tester findings §1), rebuild (spec 2026-10-07 delivery model §4). Exit codes:
5
- // 0 done or nothing to do, 1 pending (--check), refused or failed, 2 usage.
2
+ // `smoodly` — the package's command line. Three subcommands: push (spec
3
+ // 2026-10-16 the control plane §4), export and import (spec 2026-09-28). A
4
+ // space's schema lives in its object, sent there by the push, so there is
5
+ // nothing to migrate or rebuild;
6
+ // its members are set by smoodly.io (spec 2026-10-16 the control plane §5),
7
+ // so there is no add-admin. Exit codes: 0 done or nothing to do,
8
+ // 1 refused or failed, 2 usage.
6
9
  import { resolve } from "node:path";
7
10
  import { fileURLToPath } from "node:url";
8
- import { DEFAULT_COLLECTIONS, runMigrate } from "./migrate.js";
9
11
  import { runExport } from "./export.js";
10
12
  import { runImport } from "./import.js";
11
- import { runAddAdmin } from "./add-admin.js";
12
- import { runRebuild } from "./rebuild.js";
13
+ import { runPush } from "./push.js";
13
14
  import { SMOODLY_VERSION } from "../version.js";
14
15
  export const USAGE = `usage: smoodly <command> (smoodly --version prints the version)
16
+ Every command reads SMOODLY_API_URL, SMOODLY_SPACE_ID and SMOODLY_SECRET_KEY from the app's .env.local or .env.
15
17
 
16
- migrate [--check] [--collections <file>] [--migrations <dir>]
17
- Copies the core schema files this package ships that supabase/migrations/
18
- lacks, and writes one views file for the collections whose view is new or
19
- changed. Prints the push or reset command; never runs it.
20
-
21
- --check write nothing; exit 1 when anything is pending, or when
22
- SMOODLY_SECRET_KEY (if set) cannot read the database
23
- --collections <file> the module exporting \`collections\` (default
24
- smoodly/collections.ts, or src/smoodly/collections.ts
25
- when only that one exists)
26
- --migrations <dir> the app's migration folder (default supabase/migrations)
18
+ push
19
+ Sends the app's registry (its smoodly config), its address (SITE_URL) and
20
+ the package version to the space. A Next site's prebuild runs it, Astro's
21
+ build and both dev servers push on their own; run it from CI or a script.
22
+ SMOODLY_STATIC=1 marks a static site. As a prebuild it never fails the build.
27
23
 
28
24
  export [--out <dir>]
29
25
  Writes the space SMOODLY_SECRET_KEY names — every content row, history
@@ -32,20 +28,7 @@ export const USAGE = `usage: smoodly <command> (smoodly --version prints the
32
28
 
33
29
  import <dir>
34
30
  Writes an export folder into the key's space, which must be empty or hold
35
- only this export's rows (an earlier run to continue). Never deletes. Ends
36
- by rebuilding the delivery tables with the app's config (smoodly.config.ts,
37
- smoodly/config.ts or src/smoodly/config.ts), so run it in the app's folder.
38
-
39
- rebuild
40
- Rewrites the delivery tables of the key's space: the finished document of
41
- every published page, entry and global, assembled with the app's config.
42
- A publish keeps them current and a document behind the content model is
43
- rebuilt by its first visitor; run this after a deploy for a warm start.
44
-
45
- add-admin <email> [--admin-url <url>]
46
- Makes <email> an admin of the key's space: with SMOODLY_ADMIN_PASSWORD
47
- set, creates the account with that password; without, mails an invite
48
- whose link lands on --admin-url (default: SITE_URL + /admin).`;
31
+ only this export's rows (an earlier run to continue). Never deletes.`;
49
32
  export function parseArgs(argv) {
50
33
  const [command, ...rest] = argv;
51
34
  if (!command || command === "help" || argv.some((a) => a === "--help" || a === "-h"))
@@ -63,42 +46,17 @@ export function parseArgs(argv) {
63
46
  }
64
47
  return { command: "export", args };
65
48
  }
49
+ if (command === "push") {
50
+ if (rest.length > 0)
51
+ return { command: "error", message: `unexpected argument "${rest[0]}"` };
52
+ return { command: "push", args: {} };
53
+ }
66
54
  if (command === "import") {
67
55
  if (rest.length !== 1 || rest[0].startsWith("-"))
68
56
  return { command: "error", message: "import needs one export folder: smoodly import <dir>" };
69
57
  return { command: "import", args: { dir: rest[0] } };
70
58
  }
71
- if (command === "rebuild") {
72
- if (rest.length > 0)
73
- return { command: "error", message: `unexpected argument "${rest[0]}"` };
74
- return { command: "rebuild", args: {} };
75
- }
76
- if (command === "add-admin") {
77
- const usage = { command: "error", message: "add-admin needs one email: smoodly add-admin <email> [--admin-url <url>]" };
78
- const [email, ...flags] = rest;
79
- if (!email || email.startsWith("-"))
80
- return usage;
81
- if (flags.length === 0)
82
- return { command: "add-admin", args: { email } };
83
- if (flags.length !== 2 || flags[0] !== "--admin-url" || flags[1].startsWith("-"))
84
- return usage;
85
- return { command: "add-admin", args: { email, adminUrl: flags[1] } };
86
- }
87
- if (command !== "migrate")
88
- return { command: "error", message: `unknown command "${command}"` };
89
- const args = { check: false, collections: DEFAULT_COLLECTIONS, migrations: "supabase/migrations" };
90
- for (let i = 0; i < rest.length; i++) {
91
- const a = rest[i];
92
- if (a === "--check")
93
- args.check = true;
94
- else if (a === "--collections" && rest[i + 1] !== undefined)
95
- args.collections = rest[++i];
96
- else if (a === "--migrations" && rest[i + 1] !== undefined)
97
- args.migrations = rest[++i];
98
- else
99
- return { command: "error", message: `unexpected argument "${a}"` };
100
- }
101
- return { command: "migrate", args };
59
+ return { command: "error", message: `unknown command "${command}"` };
102
60
  }
103
61
  export async function main(argv, out = console.log) {
104
62
  const parsed = parseArgs(argv);
@@ -115,23 +73,19 @@ export async function main(argv, out = console.log) {
115
73
  return 2;
116
74
  }
117
75
  try {
76
+ if (parsed.command === "push")
77
+ return await runPush(parsed.args, out);
118
78
  if (parsed.command === "import")
119
79
  return await runImport(parsed.args, out);
120
- if (parsed.command === "export")
121
- return await runExport(parsed.args, out);
122
- if (parsed.command === "add-admin")
123
- return await runAddAdmin(parsed.args, out);
124
- if (parsed.command === "rebuild")
125
- return await runRebuild(parsed.args, out);
126
- return await runMigrate(parsed.args, out);
80
+ return await runExport(parsed.args, out);
127
81
  }
128
82
  catch (e) {
129
83
  out(e instanceof Error ? e.message : String(e));
130
84
  return 1;
131
85
  }
132
86
  }
133
- // Run when this file is the entry (`tsx src/cli/main.ts …`, the repo's
134
- // `npm run db:migrate`); the published bin imports `main` and runs it itself.
87
+ // Run when this file is the entry (`tsx src/cli/main.ts …`); the published bin
88
+ // imports `main` and runs it itself.
135
89
  if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(new URL(import.meta.url))) {
136
90
  void main(process.argv.slice(2)).then((code) => {
137
91
  process.exitCode = code;
@@ -0,0 +1,6 @@
1
+ export type PushArgs = Record<string, never>;
2
+ export declare function runPush(_args: PushArgs, out: (line: string) => void, deps?: {
3
+ cwd?: string;
4
+ env?: Record<string, string | undefined>;
5
+ fetch?: typeof fetch;
6
+ }): Promise<number>;
@@ -0,0 +1,31 @@
1
+ // `smoodly push` (spec 2026-10-16 the control plane §4): the app's registry, its address (SITE_URL) and the package
2
+ // version to the space SMOODLY_SECRET_KEY names. The Next templates' prebuild runs it; Astro's build hook and the API
3
+ // client in development push the same body themselves. Static mode when SMOODLY_STATIC=1, else live. A refusal exits 1,
4
+ // except when npm runs the command as a build's `prebuild`: a push never fails a build (the spec's rule), so there it
5
+ // says what failed and exits 0, and the site runs against the schema the space has.
6
+ import { apiEnv } from "../env.js";
7
+ import { pushBody, pushSchema } from "../push.js";
8
+ import { loadAppEnv } from "./app-env.js";
9
+ import { loadConfig } from "./load-config.js";
10
+ export async function runPush(_args, out, deps = {}) {
11
+ const cwd = deps.cwd ?? process.cwd();
12
+ const env = deps.env ?? process.env;
13
+ loadAppEnv(cwd, env);
14
+ try {
15
+ const config = await loadConfig(cwd);
16
+ const { apiUrl, spaceId, key } = apiEnv(env);
17
+ const body = pushBody(config, env, env.SMOODLY_STATIC === "1" ? "static" : "live");
18
+ const { version, changed } = await pushSchema({ apiUrl, spaceId, key, fetch: deps.fetch }, body);
19
+ out(changed ? `Pushed schema version ${version} to ${spaceId}.` : `Schema unchanged (version ${version}).`);
20
+ return 0;
21
+ }
22
+ catch (e) {
23
+ const message = e instanceof Error ? e.message : String(e);
24
+ if (env.npm_lifecycle_event === "prebuild") {
25
+ out(`smoodly: the schema push failed — ${message}; the site runs against the schema the space has.`);
26
+ return 0;
27
+ }
28
+ out(message);
29
+ return 1;
30
+ }
31
+ }
@@ -8,7 +8,6 @@ export type EntryOrder = "manual" | {
8
8
  direction: "asc" | "desc";
9
9
  };
10
10
  export declare const DEFAULT_ORDER: EntryOrder;
11
- export declare const BUILT_IN_ORDER_FIELDS: readonly ["createdAt", "updatedAt"];
12
11
  export type CollectionSchema<F extends BuilderMap = BuilderMap, N extends string = string> = {
13
12
  /** "global" marks a code-declared singleton (spec 2026-09-07 decision
14
13
  * 4): a global item IS a collection with one entry, so both entry
@@ -1,6 +1,6 @@
1
1
  import { SEO_DESCRIPTOR, SEO_KEY, validateCollectionSeo } from "./seo.js";
2
2
  export const DEFAULT_ORDER = { by: "createdAt", direction: "desc" };
3
- export const BUILT_IN_ORDER_FIELDS = ["createdAt", "updatedAt"];
3
+ const BUILT_IN_ORDER_FIELDS = ["createdAt", "updatedAt"];
4
4
  export const collectionOrder = (c) => c.order ?? DEFAULT_ORDER;
5
5
  export const collectionDepth = (c) => c.tree?.depth ?? 1;
6
6
  export function collection(input) {
package/dist/config.d.ts CHANGED
@@ -3,8 +3,10 @@ import type { CollectionSchema } from "./collections.ts";
3
3
  import type { GlobalSchema } from "./global.ts";
4
4
  import { type Addresses } from "./addresses.ts";
5
5
  import type { Anchors } from "./anchors.ts";
6
+ import { type ImageQuality } from "./media-cut.ts";
6
7
  import { type StyleValues, type StyleValuesInput } from "./styles.ts";
7
8
  import { type CustomConfigInput, type ResolvedCustom } from "./custom/index.ts";
9
+ import type { LocaleRouting } from "./astro/locale-paths.ts";
8
10
  /** A section, element or page schema as the config reads it. */
9
11
  export type RegisteredSchema = {
10
12
  kind: string;
@@ -58,16 +60,24 @@ export type SmoodlyConfig = {
58
60
  database?: {
59
61
  provider: string;
60
62
  };
61
- /** Upload limits (spec 2026-09-08 media §10): bytes per file and the accepted mime types. */
63
+ /** Upload limits (spec 2026-09-08 media §10): bytes per file and the accepted mime types. The project's image
64
+ * quality and srcset cap (spec 2026-10-18 image quality and max width §1), under a field's and a view's own. */
62
65
  media?: {
63
66
  maxSize?: number;
64
67
  accept?: string[];
68
+ quality?: ImageQuality;
69
+ maxWidth?: number;
65
70
  };
66
71
  /** Where the admin is mounted (spec 2026-09-17): must match the route
67
72
  * folder under app/(smoodly)/. Default "/admin". */
68
73
  admin?: {
69
74
  path?: string;
70
75
  };
76
+ /** Where the default locale's pages live (spec 2026-10-16 the control plane §4): at the root (default), or under
77
+ * its code like every other locale. Astro's integration holds it equal to astro.config's i18n. */
78
+ routing?: {
79
+ prefixDefaultLocale: boolean;
80
+ };
71
81
  };
72
82
  /** The registry as defineConfig resolves it: schemas, and page types only under `pages`. */
73
83
  export type Registry = {
@@ -77,7 +87,7 @@ export type Registry = {
77
87
  globals: GlobalSchema[];
78
88
  pages: RegisteredSchema[];
79
89
  };
80
- export type ResolvedConfig = Omit<SmoodlyConfig, "styles" | "custom" | "admin" | "registry"> & {
90
+ export type ResolvedConfig = Omit<SmoodlyConfig, "styles" | "custom" | "admin" | "registry" | "routing"> & {
81
91
  registry: Registry;
82
92
  homePageSlug: string;
83
93
  /** How deep editors may nest pages in the tree. Default 1 (flat). */
@@ -93,17 +103,22 @@ export type ResolvedConfig = Omit<SmoodlyConfig, "styles" | "custom" | "admin" |
93
103
  media: {
94
104
  maxSize: number;
95
105
  accept: string[];
106
+ quality?: ImageQuality;
107
+ maxWidth?: number;
96
108
  };
97
109
  /** The admin's mount path, always present. */
98
110
  admin: {
99
111
  path: string;
100
112
  };
113
+ /** Where the default locale's pages live; `prefixDefaultLocale` is false unless the config says otherwise. */
114
+ routing: {
115
+ prefixDefaultLocale: boolean;
116
+ };
101
117
  /** Code-owned addresses by owner key (spec 2026-09-29 nested paths §3). */
102
118
  addresses: Addresses;
103
119
  /** The path index's view of the code-owned addresses (spec 2026-09-29 anchored paths §2): pass it to the index. */
104
120
  anchors: Anchors;
105
121
  };
106
- /** `global`: the placement node type and an admin route; `asset`: the refs
107
- * target of every media edge; `media`: the admin's Media screen route. */
108
- export declare const RESERVED_NAMES: readonly string[];
109
122
  export declare function defineConfig(config: SmoodlyConfig): ResolvedConfig;
123
+ /** The address rule the config's routing asks for: a prefixed default locale, whose bare root goes to its home. */
124
+ export declare const configRouting: (config: Pick<ResolvedConfig, "routing">) => LocaleRouting;
package/dist/config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { assertSegment, fixedPageSegments } from "./paths.js";
2
2
  import { addressMap, assertAddress, codeAddresses, lastPart } from "./addresses.js";
3
3
  import { DEFAULT_MEDIA } from "./media.js";
4
+ import { isImageQuality, isMaxWidth, qualityError } from "./media-cut.js";
4
5
  import { DEFAULT_ADMIN_PATH, assertAdminPath, isAdminSlug } from "./admin-path.js";
5
6
  import { resolveStyleValues, styleDescriptors } from "./styles.js";
6
7
  import { resolveCustom } from "./custom/index.js";
@@ -8,7 +9,7 @@ import { isValidZone } from "./time-zone.js";
8
9
  import { schemaOnly } from "./registration.js";
9
10
  /** `global`: the placement node type and an admin route; `asset`: the refs
10
11
  * target of every media edge; `media`: the admin's Media screen route. */
11
- export const RESERVED_NAMES = ["global", "asset", "media"];
12
+ const RESERVED_NAMES = ["global", "asset", "media"];
12
13
  const isReserved = (name) => RESERVED_NAMES.includes(name);
13
14
  function normalizeRegistry(config) {
14
15
  const collections = config.registry.collections ?? [];
@@ -234,13 +235,23 @@ export function defineConfig(config) {
234
235
  const styles = resolveStyleValues(config.styles);
235
236
  const custom = resolveCustom(config.custom);
236
237
  const byName = new Map(registry.sections.map((s) => [s.name, s]));
238
+ const quality = config.media?.quality, maxWidth = config.media?.maxWidth;
237
239
  const media = {
238
240
  maxSize: config.media?.maxSize ?? DEFAULT_MEDIA.maxSize,
239
241
  accept: config.media?.accept ? [...config.media.accept] : [...DEFAULT_MEDIA.accept],
242
+ ...(quality !== undefined ? { quality } : {}),
243
+ ...(maxWidth !== undefined ? { maxWidth } : {}),
240
244
  };
241
245
  if (!Number.isInteger(media.maxSize) || media.maxSize <= 0) {
242
246
  throw new Error("smoodly: media.maxSize must be a positive integer of bytes.");
243
247
  }
248
+ if (quality !== undefined && !isImageQuality(quality))
249
+ throw new Error(qualityError(quality, "media.quality"));
250
+ if (maxWidth !== undefined && !isMaxWidth(maxWidth))
251
+ throw new Error("smoodly: media.maxWidth must be a positive whole number of pixels.");
252
+ const prefixDefaultLocale = config.routing?.prefixDefaultLocale ?? false;
253
+ if (typeof prefixDefaultLocale !== "boolean")
254
+ throw new Error("smoodly: routing.prefixDefaultLocale must be true or false.");
244
255
  const addresses = addressMap(codeAddresses({ locales: config.locales, pages: registry.pages, collections: registry.collections }));
245
256
  return {
246
257
  ...config,
@@ -249,6 +260,7 @@ export function defineConfig(config) {
249
260
  pageDepth: depth,
250
261
  media,
251
262
  admin: { path: adminPath },
263
+ routing: { prefixDefaultLocale },
252
264
  addresses,
253
265
  anchors: { addresses, homePageSlug, defaultLocale: config.locales.default },
254
266
  styles,
@@ -264,3 +276,8 @@ export function defineConfig(config) {
264
276
  },
265
277
  };
266
278
  }
279
+ /** The address rule the config's routing asks for: a prefixed default locale, whose bare root goes to its home. */
280
+ export const configRouting = (config) => ({
281
+ prefixDefault: config.routing.prefixDefaultLocale,
282
+ redirectToDefault: config.routing.prefixDefaultLocale,
283
+ });
@@ -1,11 +1,11 @@
1
- import { createAdminOps } from "../admin/ops-impl.ts";
2
- import type { EntryCreateInput, OpError, PageCreateInput } from "../admin/ops.ts";
1
+ import type { AdminOpCall, EntryCreateInput, OpError, OpResult, PageCreateInput } from "../admin/ops.ts";
2
+ import type { PathHit } from "../path-index.ts";
3
3
  import type { FieldError } from "../admin/validate.ts";
4
4
  import type { EntryRecord } from "../entry-store.ts";
5
5
  import type { PageRecord } from "../store.ts";
6
6
  import type { PageTree } from "../page-context.ts";
7
7
  import type { ImageValue } from "../media.ts";
8
- import type { MemberStore } from "../member-store.ts";
8
+ import type { ResolvedConfig } from "../config.ts";
9
9
  export type UploadMeta = {
10
10
  filename?: string;
11
11
  mime?: string;
@@ -19,17 +19,26 @@ export declare class SmoodlyContentError extends Error {
19
19
  readonly fields: FieldError[];
20
20
  constructor(error: Pick<OpError, "code" | "message" | "fields">);
21
21
  }
22
- export type ContentDeps = Omit<Parameters<typeof createAdminOps>[0], "actor">;
23
- /** With a password: the account is created (or an existing one kept) and made an admin. Without: an invite mail,
24
- * whose link goes to `redirectTo` or the Auth project's Site URL. Running it again changes nothing.
25
- * Its own function because `smoodly add-admin` has a member store and no config. */
26
- export declare function addAdmin(members: MemberStore | undefined, email: string, options?: {
27
- password?: string;
28
- redirectTo?: string;
29
- }): Promise<{
30
- email: string;
31
- invited: boolean;
32
- }>;
22
+ /** What content() writes through: the operations, the paths index's lookup, an upload. */
23
+ export type ContentTransport = {
24
+ op(call: AdminOpCall): Promise<OpResult<unknown>>;
25
+ /** The target at an address, as a draft read: what the script is writing, never an edge copy. */
26
+ locate(locale: string, path: string): Promise<PathHit | null>;
27
+ upload(bytes: Uint8Array, filename: string, mime: string | undefined, meta: UploadMeta): Promise<ImageValue>;
28
+ };
29
+ export type ContentDeps = {
30
+ config: ResolvedConfig;
31
+ href?: (path: string, locale: string) => string;
32
+ transport: ContentTransport;
33
+ };
34
+ /** Over the content API with the secret key (a seed, a script, the installer). A refusal throws SmoodlyContentError
35
+ * with the service's code and message; a network failure names SMOODLY_API_URL. */
36
+ export declare function apiTransport(options: {
37
+ apiUrl: string;
38
+ spaceId: string;
39
+ key: string;
40
+ fetch?: typeof fetch;
41
+ }): ContentTransport;
33
42
  export declare function createSmoodlyContent(deps: ContentDeps): {
34
43
  pages: {
35
44
  create: (input: PageCreateInput) => Promise<PageRecord>;
@@ -77,14 +86,5 @@ export declare function createSmoodlyContent(deps: ContentDeps): {
77
86
  /** A file path or bytes in, a ready asset and the value an image field stores out. */
78
87
  upload(file: string | Uint8Array, meta?: UploadMeta): Promise<ImageValue>;
79
88
  };
80
- members: {
81
- addAdmin: (email: string, options?: {
82
- password?: string;
83
- redirectTo?: string;
84
- }) => Promise<{
85
- email: string;
86
- invited: boolean;
87
- }>;
88
- };
89
89
  };
90
90
  export type SmoodlyContent = ReturnType<typeof createSmoodlyContent>;