smoodly 0.0.44 → 0.0.46

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 (241) hide show
  1. package/README.md +3 -3
  2. package/dist/addresses.d.ts +6 -7
  3. package/dist/addresses.js +6 -4
  4. package/dist/admin/PublicSans-LICENSE.txt +93 -0
  5. package/dist/admin/auth-config.d.ts +6 -0
  6. package/dist/admin/auth-config.js +1 -0
  7. package/dist/admin/browser/fetch-op.d.ts +10 -0
  8. package/dist/admin/browser/fetch-op.js +44 -0
  9. package/dist/admin/browser/main.d.ts +1 -0
  10. package/dist/admin/browser/main.js +21 -0
  11. package/dist/admin/custom-section.d.ts +1 -1
  12. package/dist/admin/drift.d.ts +1 -1
  13. package/dist/admin/editor/EditorView.js +14 -14
  14. package/dist/admin/editor/autosave.js +1 -1
  15. package/dist/admin/editor/canvas-address.d.ts +2 -0
  16. package/dist/admin/editor/canvas-address.js +8 -0
  17. package/dist/admin/editor/overlay-geometry.d.ts +1 -1
  18. package/dist/admin/editor/protocol.d.ts +18 -4
  19. package/dist/admin/editor/protocol.js +15 -1
  20. package/dist/admin/editor/useCanvasBridge.d.ts +1 -1
  21. package/dist/admin/editor/useCanvasBridge.js +5 -6
  22. package/dist/admin/editor/useInlineSession.js +4 -2
  23. package/dist/admin/files.d.ts +11 -0
  24. package/dist/admin/files.generated.js +2 -0
  25. package/dist/admin/files.js +8 -0
  26. package/dist/admin/forms/useRefOptions.d.ts +2 -2
  27. package/dist/admin/forms/useRefOptions.js +2 -2
  28. package/dist/admin/forms/useRichtextEditor.js +4 -4
  29. package/dist/admin/handler.d.ts +40 -0
  30. package/dist/admin/handler.js +151 -0
  31. package/dist/admin/index.d.ts +1 -1
  32. package/dist/admin/ops-impl.d.ts +7 -1
  33. package/dist/admin/ops-impl.js +110 -33
  34. package/dist/admin/ops.d.ts +3 -2
  35. package/dist/admin/page-data.d.ts +21 -0
  36. package/dist/admin/page-data.js +4 -0
  37. package/dist/admin/page.d.ts +6 -0
  38. package/dist/admin/page.js +35 -0
  39. package/dist/admin/serialize.js +7 -7
  40. package/dist/admin/shell/AdminApp.js +2 -2
  41. package/dist/admin/shell/EntryForm.js +1 -1
  42. package/dist/admin/shell/router.d.ts +3 -16
  43. package/dist/admin/shell/router.js +3 -39
  44. package/dist/admin/shell/session.d.ts +2 -5
  45. package/dist/admin/supabase-auth.js +1 -1
  46. package/dist/admin/tree-ops.d.ts +1 -1
  47. package/dist/admin/ui/fonts.d.ts +12 -0
  48. package/dist/admin/ui/fonts.js +22 -0
  49. package/dist/admin/ui/theme.d.ts +13 -4
  50. package/dist/admin/ui/theme.js +28 -157
  51. package/dist/assemble.d.ts +60 -0
  52. package/dist/assemble.js +359 -0
  53. package/dist/astro/cache.d.ts +34 -0
  54. package/dist/astro/cache.js +69 -0
  55. package/dist/astro/components/Custom.astro +21 -0
  56. package/dist/astro/components/Guarded.astro +24 -0
  57. package/dist/astro/components/Head.astro +26 -0
  58. package/dist/astro/components/Image.astro +20 -0
  59. package/dist/astro/components/RichText.astro +23 -0
  60. package/dist/astro/components/Section.astro +20 -0
  61. package/dist/astro/components/Video.astro +5 -0
  62. package/dist/astro/components/Zone.astro +19 -0
  63. package/dist/astro/components.d.ts +6 -0
  64. package/dist/astro/components.js +7 -0
  65. package/dist/astro/content-loaders.d.ts +40 -0
  66. package/dist/astro/content-loaders.js +100 -0
  67. package/dist/astro/content.d.ts +9 -0
  68. package/dist/astro/content.js +17 -0
  69. package/dist/astro/custom.d.ts +1 -0
  70. package/dist/astro/custom.js +3 -0
  71. package/dist/astro/defaults.d.ts +20 -0
  72. package/dist/astro/defaults.js +21 -0
  73. package/dist/astro/env-files.d.ts +1 -0
  74. package/dist/astro/env-files.js +21 -0
  75. package/dist/astro/head.d.ts +13 -0
  76. package/dist/astro/head.js +19 -0
  77. package/dist/astro/image.d.ts +27 -0
  78. package/dist/astro/image.js +29 -0
  79. package/dist/astro/index.d.ts +43 -0
  80. package/dist/astro/index.js +111 -0
  81. package/dist/astro/locale-paths.d.ts +30 -0
  82. package/dist/astro/locale-paths.js +38 -0
  83. package/dist/astro/middleware.d.ts +1 -0
  84. package/dist/astro/middleware.js +83 -0
  85. package/dist/astro/preview-document.d.ts +1 -0
  86. package/dist/astro/preview-document.js +18 -0
  87. package/dist/astro/preview.d.ts +10 -0
  88. package/dist/astro/preview.js +72 -0
  89. package/dist/astro/richtext.d.ts +15 -0
  90. package/dist/astro/richtext.js +78 -0
  91. package/dist/astro/routes/admin.d.ts +3 -0
  92. package/dist/astro/routes/admin.js +41 -0
  93. package/dist/astro/routes/page.astro +17 -0
  94. package/dist/astro/routes/preview.astro +11 -0
  95. package/dist/astro/runtime.d.ts +32 -0
  96. package/dist/astro/runtime.js +46 -0
  97. package/dist/astro/scope.d.ts +45 -0
  98. package/dist/astro/scope.js +103 -0
  99. package/dist/astro/server.d.ts +14 -0
  100. package/dist/astro/server.js +45 -0
  101. package/dist/astro/shell.d.ts +67 -0
  102. package/dist/astro/shell.js +63 -0
  103. package/dist/astro/style.d.ts +1 -0
  104. package/dist/astro/style.js +11 -0
  105. package/dist/astro/views-check.d.ts +3 -0
  106. package/dist/astro/views-check.js +19 -0
  107. package/dist/astro/zone.d.ts +39 -0
  108. package/dist/astro/zone.js +33 -0
  109. package/dist/bind.d.ts +19 -0
  110. package/dist/bind.js +34 -0
  111. package/dist/bridge/bridge.d.ts +9 -0
  112. package/dist/bridge/bridge.js +107 -0
  113. package/dist/bridge/entry.d.ts +1 -0
  114. package/dist/bridge/entry.js +5 -0
  115. package/dist/bridge/preview-pill.d.ts +18 -0
  116. package/dist/bridge/preview-pill.js +115 -0
  117. package/dist/cli/import.d.ts +3 -0
  118. package/dist/cli/import.js +10 -1
  119. package/dist/cli/load-collections.js +4 -3
  120. package/dist/cli/load-config.d.ts +4 -0
  121. package/dist/cli/load-config.js +26 -0
  122. package/dist/cli/main.d.ts +7 -1
  123. package/dist/cli/main.js +32 -7
  124. package/dist/cli/migrate.d.ts +3 -0
  125. package/dist/cli/migrate.js +8 -1
  126. package/dist/cli/rebuild.d.ts +23 -0
  127. package/dist/cli/rebuild.js +22 -0
  128. package/dist/cli/space-io.d.ts +15 -1
  129. package/dist/cli/space-io.js +14 -0
  130. package/dist/client.js +5 -0
  131. package/dist/config.d.ts +28 -20
  132. package/dist/config.js +71 -75
  133. package/dist/content/content-api.d.ts +1 -1
  134. package/dist/content/node.d.ts +2 -3
  135. package/dist/content/node.js +2 -1
  136. package/dist/crash-message.d.ts +1 -0
  137. package/dist/crash-message.js +8 -0
  138. package/dist/css.d.ts +4 -0
  139. package/dist/css.js +5 -2
  140. package/dist/custom/CustomSection.d.ts +1 -1
  141. package/dist/custom/CustomSection.js +4 -8
  142. package/dist/custom/css.js +2 -2
  143. package/dist/custom/index.d.ts +6 -10
  144. package/dist/custom/index.js +1 -2
  145. package/dist/custom/layout.d.ts +5 -0
  146. package/dist/custom/layout.js +14 -0
  147. package/dist/entry-page.d.ts +3 -18
  148. package/dist/env.js +1 -1
  149. package/dist/fields.js +2 -1
  150. package/dist/global-id.js +2 -2
  151. package/dist/global.d.ts +5 -2
  152. package/dist/global.js +8 -5
  153. package/dist/image/next-config.d.ts +3 -0
  154. package/dist/image/next-config.js +12 -7
  155. package/dist/image/remote-pattern.d.ts +1 -0
  156. package/dist/image/remote-pattern.js +5 -0
  157. package/dist/index.d.ts +15 -17
  158. package/dist/index.js +10 -19
  159. package/dist/marks.d.ts +7 -0
  160. package/dist/marks.js +7 -0
  161. package/dist/next/admin-route.d.ts +7 -0
  162. package/dist/next/admin-route.js +28 -0
  163. package/dist/next/bridge.d.ts +1 -0
  164. package/dist/next/bridge.js +26 -0
  165. package/dist/next/canvas-boundary.d.ts +1 -1
  166. package/dist/next/canvas-slot.d.ts +1 -0
  167. package/dist/next/canvas-slot.js +4 -3
  168. package/dist/next/index.d.ts +2 -0
  169. package/dist/next/index.js +1 -0
  170. package/dist/next/page-renderer.d.ts +5 -1
  171. package/dist/next/page-renderer.js +19 -33
  172. package/dist/next/preview-pill-define.d.ts +1 -0
  173. package/dist/next/preview-pill-define.js +10 -0
  174. package/dist/next/preview-pill.d.ts +0 -3
  175. package/dist/next/preview-pill.js +7 -50
  176. package/dist/next/queries.d.ts +3 -28
  177. package/dist/next/queries.js +3 -90
  178. package/dist/next/reads.d.ts +2 -0
  179. package/dist/next/reads.js +18 -0
  180. package/dist/page-context.d.ts +89 -0
  181. package/dist/page-context.js +8 -0
  182. package/dist/pairing.d.ts +2 -22
  183. package/dist/project.d.ts +69 -0
  184. package/dist/project.js +210 -0
  185. package/dist/published.d.ts +96 -0
  186. package/dist/published.js +125 -0
  187. package/dist/react.d.ts +61 -0
  188. package/dist/react.js +17 -0
  189. package/dist/read-shapes.d.ts +1 -1
  190. package/dist/reads.d.ts +80 -0
  191. package/dist/reads.js +142 -0
  192. package/dist/refs.d.ts +1 -1
  193. package/dist/registration.d.ts +8 -0
  194. package/dist/registration.js +17 -0
  195. package/dist/render.d.ts +5 -83
  196. package/dist/render.js +5 -10
  197. package/dist/resolve.d.ts +1 -1
  198. package/dist/richtext/index.d.ts +6 -2
  199. package/dist/richtext/index.js +3 -2
  200. package/dist/richtext-href.d.ts +4 -0
  201. package/dist/richtext-href.js +17 -0
  202. package/dist/richtext-render.js +1 -14
  203. package/dist/schema-version.d.ts +1 -1
  204. package/dist/schema-version.js +1 -1
  205. package/dist/section-crash.d.ts +2 -2
  206. package/dist/section-crash.js +2 -7
  207. package/dist/section-wrapper.d.ts +3 -10
  208. package/dist/section-wrapper.js +3 -2
  209. package/dist/site.d.ts +38 -14
  210. package/dist/site.js +159 -286
  211. package/dist/smd-key.d.ts +9 -0
  212. package/dist/smd-key.js +1 -0
  213. package/dist/sql-space.d.ts +4 -0
  214. package/dist/sql-space.js +10 -5
  215. package/dist/sql.d.ts +2 -0
  216. package/dist/sql.js +117 -0
  217. package/dist/store.d.ts +1 -1
  218. package/dist/supabase-published-store.d.ts +35 -0
  219. package/dist/supabase-published-store.js +215 -0
  220. package/dist/supabase-store.d.ts +1 -1
  221. package/dist/toolset.d.ts +2 -4
  222. package/dist/toolset.js +6 -4
  223. package/dist/version.d.ts +1 -1
  224. package/dist/version.js +1 -1
  225. package/dist/view-props.d.ts +40 -0
  226. package/dist/view-props.js +1 -0
  227. package/dist/views.d.ts +36 -0
  228. package/dist/views.js +52 -0
  229. package/dist/zones.d.ts +7 -7
  230. package/dist/zones.js +7 -6
  231. package/migrations/20261007120000_smoodly_v8.sql +154 -0
  232. package/migrations/20261007200000_smoodly_v9.sql +30 -0
  233. package/package.json +46 -19
  234. package/dist/admin/next/bridge.d.ts +0 -2
  235. package/dist/admin/next/bridge.js +0 -129
  236. package/dist/admin/next/create-admin.d.ts +0 -19
  237. package/dist/admin/next/create-admin.js +0 -44
  238. package/dist/admin/next/index.d.ts +0 -8
  239. package/dist/admin/next/index.js +0 -8
  240. package/dist/admin/next/op-handler.d.ts +0 -18
  241. package/dist/admin/next/op-handler.js +0 -31
@@ -10,8 +10,9 @@ import { join, resolve } from "node:path";
10
10
  import { MEDIA_CACHE_SECONDS } from "../media.js";
11
11
  import { CONTENT_TABLES } from "../sql-space.js";
12
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, schemaRefusal, } from "./space-io.js";
13
+ import { MEDIA_BUCKET, connectFromEnv, isAlreadyExists, readSchemaVersion, readSpaceRows, rebuildSpace, rebuiltMessage, schemaRefusal, } from "./space-io.js";
14
14
  import { readCurrentSpace } from "../current-space.js";
15
+ import { loadConfig } from "./load-config.js";
15
16
  export const BATCH_ROWS = 500;
16
17
  export const notAnExport = (dir) => `Refusing: ${dir} is not a Smoodly export (no manifest.json).`;
17
18
  export const formatMismatch = (format) => `Refusing: this export's format is ${String(format)}; this smoodly reads format ${FORMAT}. Update smoodly.`;
@@ -57,6 +58,9 @@ export async function runImport(args, out, deps = {}) {
57
58
  }
58
59
  const newRows = missingRows(target, rows);
59
60
  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 };
60
64
  try {
61
65
  // Media first, so no ready asset row ever points at a missing file.
62
66
  for (const asset of rows.assets) {
@@ -84,6 +88,9 @@ export async function runImport(args, out, deps = {}) {
84
88
  throw new Error(`inserting ${table}: ${res.error.message}`);
85
89
  }
86
90
  }
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);
87
94
  }
88
95
  catch (e) {
89
96
  out(`Import stopped: ${e instanceof Error ? e.message : String(e)}`);
@@ -92,9 +99,11 @@ export async function runImport(args, out, deps = {}) {
92
99
  }
93
100
  if (newRows === 0 && newFiles === 0) {
94
101
  out(nothingNew(space));
102
+ out(rebuiltMessage(rebuilt));
95
103
  return 0;
96
104
  }
97
105
  out(`Imported ${describeImport(rows)} into space ${space}.`);
106
+ out(rebuiltMessage(rebuilt));
98
107
  if (manifest.members.length > 0) {
99
108
  out(MEMBERS_HEADING);
100
109
  out(` ${manifest.members.map((m) => `${m.email ?? "(no email)"} (${m.role})`).join(", ")}`);
@@ -1,9 +1,10 @@
1
1
  // packages/smoodly/src/cli/load-collections.ts
2
2
  // Loads the app's `smoodly/collections.ts` — TypeScript, through tsx's
3
3
  // import API, under the app's own tsconfig so its `@/` alias resolves.
4
- // The barrel and not `smoodly/config.ts`: the config imports the page
5
- // shapes and with them React and Next, which a command line has no use
6
- // for; the barrel is what the config registers in both starters.
4
+ // The barrel and not `smoodly/config.ts`: the collections are all a
5
+ // migration needs, and the barrel is what the config registers in both
6
+ // starters. (Since the registration split the config imports no
7
+ // component either; a command that needs the whole registry may load it.)
7
8
  import { existsSync } from "node:fs";
8
9
  import { join } from "node:path";
9
10
  import { pathToFileURL } from "node:url";
@@ -0,0 +1,4 @@
1
+ import type { ResolvedConfig } from "../config.ts";
2
+ export declare const CONFIG_FILES: string[];
3
+ export declare const noConfig: (cwd: string) => string;
4
+ export declare function loadConfig(cwd: string): Promise<ResolvedConfig>;
@@ -0,0 +1,26 @@
1
+ // packages/smoodly/src/cli/load-config.ts
2
+ // Loads the app's whole config — TypeScript, through tsx's import API,
3
+ // under the app's own tsconfig so its `@/` alias resolves — for a command
4
+ // that assembles documents (import's rebuild, spec 2026-10-07 delivery
5
+ // model §2). The config imports no component (the registration split), so
6
+ // it loads outside the app. Astro keeps it at the root, Next under smoodly/.
7
+ import { existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { pathToFileURL } from "node:url";
10
+ import { tsImport } from "tsx/esm/api";
11
+ export const CONFIG_FILES = ["smoodly.config.ts", "smoodly/config.ts", "src/smoodly/config.ts"];
12
+ export const noConfig = (cwd) => `smoodly: no ${CONFIG_FILES.join(", ")} in ${cwd} — run this in your app's folder.`;
13
+ export async function loadConfig(cwd) {
14
+ const file = CONFIG_FILES.map((name) => join(cwd, name)).find((path) => existsSync(path));
15
+ if (!file)
16
+ throw new Error(noConfig(cwd));
17
+ const tsconfig = join(cwd, "tsconfig.json");
18
+ const mod = (await tsImport(pathToFileURL(file).href, {
19
+ parentURL: import.meta.url,
20
+ ...(existsSync(tsconfig) ? { tsconfig } : {}),
21
+ }));
22
+ if (!mod.config?.registry) {
23
+ throw new Error(`smoodly: ${file} must export \`config\`, the result of defineConfig(…).`);
24
+ }
25
+ return mod.config;
26
+ }
@@ -2,7 +2,8 @@ import { type MigrateArgs } from "./migrate.ts";
2
2
  import { type ExportArgs } from "./export.ts";
3
3
  import { type ImportArgs } from "./import.ts";
4
4
  import { type AddAdminArgs } from "./add-admin.ts";
5
- export declare const USAGE = "usage: smoodly <command>\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 smoodly/collections.ts)\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.\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).";
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).";
6
7
  export type Parsed = {
7
8
  command: "migrate";
8
9
  args: MigrateArgs;
@@ -15,8 +16,13 @@ export type Parsed = {
15
16
  } | {
16
17
  command: "add-admin";
17
18
  args: AddAdminArgs;
19
+ } | {
20
+ command: "rebuild";
21
+ args: RebuildArgs;
18
22
  } | {
19
23
  command: "help";
24
+ } | {
25
+ command: "version";
20
26
  } | {
21
27
  command: "error";
22
28
  message: string;
package/dist/cli/main.js CHANGED
@@ -1,15 +1,17 @@
1
1
  // packages/smoodly/src/cli/main.ts
2
- // `smoodly` — the package's command line. Four subcommands: migrate
2
+ // `smoodly` — the package's command line. Five subcommands: migrate
3
3
  // (spec 2026-09-26 §2), export and import (spec 2026-09-28), add-admin
4
- // (spec 2026-09-30 tester findings §1). Exit codes:
4
+ // (spec 2026-09-30 tester findings §1), rebuild (spec 2026-10-07 delivery model §4). Exit codes:
5
5
  // 0 done or nothing to do, 1 pending (--check), refused or failed, 2 usage.
6
6
  import { resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
- import { runMigrate } from "./migrate.js";
8
+ import { DEFAULT_COLLECTIONS, runMigrate } from "./migrate.js";
9
9
  import { runExport } from "./export.js";
10
10
  import { runImport } from "./import.js";
11
11
  import { runAddAdmin } from "./add-admin.js";
12
- export const USAGE = `usage: smoodly <command>
12
+ import { runRebuild } from "./rebuild.js";
13
+ import { SMOODLY_VERSION } from "../version.js";
14
+ export const USAGE = `usage: smoodly <command> (smoodly --version prints the version)
13
15
 
14
16
  migrate [--check] [--collections <file>] [--migrations <dir>]
15
17
  Copies the core schema files this package ships that supabase/migrations/
@@ -18,7 +20,9 @@ export const USAGE = `usage: smoodly <command>
18
20
 
19
21
  --check write nothing; exit 1 when anything is pending, or when
20
22
  SMOODLY_SECRET_KEY (if set) cannot read the database
21
- --collections <file> the module exporting \`collections\` (default smoodly/collections.ts)
23
+ --collections <file> the module exporting \`collections\` (default
24
+ smoodly/collections.ts, or src/smoodly/collections.ts
25
+ when only that one exists)
22
26
  --migrations <dir> the app's migration folder (default supabase/migrations)
23
27
 
24
28
  export [--out <dir>]
@@ -28,7 +32,15 @@ export const USAGE = `usage: smoodly <command>
28
32
 
29
33
  import <dir>
30
34
  Writes an export folder into the key's space, which must be empty or hold
31
- only this export's rows (an earlier run to continue). Never deletes.
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.
32
44
 
33
45
  add-admin <email> [--admin-url <url>]
34
46
  Makes <email> an admin of the key's space: with SMOODLY_ADMIN_PASSWORD
@@ -38,6 +50,8 @@ export function parseArgs(argv) {
38
50
  const [command, ...rest] = argv;
39
51
  if (!command || command === "help" || argv.some((a) => a === "--help" || a === "-h"))
40
52
  return { command: "help" };
53
+ if (command === "--version" || command === "-v")
54
+ return { command: "version" };
41
55
  if (command === "export") {
42
56
  const args = { out: null };
43
57
  for (let i = 0; i < rest.length; i++) {
@@ -54,6 +68,11 @@ export function parseArgs(argv) {
54
68
  return { command: "error", message: "import needs one export folder: smoodly import <dir>" };
55
69
  return { command: "import", args: { dir: rest[0] } };
56
70
  }
71
+ if (command === "rebuild") {
72
+ if (rest.length > 0)
73
+ return { command: "error", message: `unexpected argument "${rest[0]}"` };
74
+ return { command: "rebuild", args: {} };
75
+ }
57
76
  if (command === "add-admin") {
58
77
  const usage = { command: "error", message: "add-admin needs one email: smoodly add-admin <email> [--admin-url <url>]" };
59
78
  const [email, ...flags] = rest;
@@ -67,7 +86,7 @@ export function parseArgs(argv) {
67
86
  }
68
87
  if (command !== "migrate")
69
88
  return { command: "error", message: `unknown command "${command}"` };
70
- const args = { check: false, collections: "smoodly/collections.ts", migrations: "supabase/migrations" };
89
+ const args = { check: false, collections: DEFAULT_COLLECTIONS, migrations: "supabase/migrations" };
71
90
  for (let i = 0; i < rest.length; i++) {
72
91
  const a = rest[i];
73
92
  if (a === "--check")
@@ -87,6 +106,10 @@ export async function main(argv, out = console.log) {
87
106
  out(USAGE);
88
107
  return 0;
89
108
  }
109
+ if (parsed.command === "version") {
110
+ out(SMOODLY_VERSION);
111
+ return 0;
112
+ }
90
113
  if (parsed.command === "error") {
91
114
  out(`smoodly: ${parsed.message}\n\n${USAGE}`);
92
115
  return 2;
@@ -98,6 +121,8 @@ export async function main(argv, out = console.log) {
98
121
  return await runExport(parsed.args, out);
99
122
  if (parsed.command === "add-admin")
100
123
  return await runAddAdmin(parsed.args, out);
124
+ if (parsed.command === "rebuild")
125
+ return await runRebuild(parsed.args, out);
101
126
  return await runMigrate(parsed.args, out);
102
127
  }
103
128
  catch (e) {
@@ -6,6 +6,9 @@ export type MigrateArgs = {
6
6
  collections: string;
7
7
  migrations: string;
8
8
  };
9
+ export declare const DEFAULT_COLLECTIONS = "smoodly/collections.ts";
10
+ /** The Astro starter keeps the setup under src/ (plan 2026-10-06 astro adapter 3); the default follows it when the Next path is absent. */
11
+ export declare function collectionsFile(cwd: string, given: string, exists?: (p: string) => boolean): string;
9
12
  /** The folder shipped in the package: `../../migrations/` from dist/cli/ and from src/cli/ alike. */
10
13
  export declare const PACKAGE_MIGRATIONS_DIR: string;
11
14
  export declare const NO_FOLDER: (dir: string) => string;
@@ -15,6 +15,13 @@ import { resolveSmoodlyEnv } from "../env.js";
15
15
  import { loadAppEnv } from "./space-io.js";
16
16
  import { planIsEmpty, planMigrate } from "../migrations.js";
17
17
  import { loadCollections } from "./load-collections.js";
18
+ export const DEFAULT_COLLECTIONS = "smoodly/collections.ts";
19
+ /** The Astro starter keeps the setup under src/ (plan 2026-10-06 astro adapter 3); the default follows it when the Next path is absent. */
20
+ export function collectionsFile(cwd, given, exists = existsSync) {
21
+ if (given !== DEFAULT_COLLECTIONS || exists(resolve(cwd, given)))
22
+ return given;
23
+ return exists(resolve(cwd, "src/smoodly/collections.ts")) ? "src/smoodly/collections.ts" : given;
24
+ }
18
25
  /** The folder shipped in the package: `../../migrations/` from dist/cli/ and from src/cli/ alike. */
19
26
  export const PACKAGE_MIGRATIONS_DIR = fileURLToPath(new URL("../../migrations/", import.meta.url));
20
27
  export const NO_FOLDER = (dir) => `No supabase/migrations folder here (${dir}) — nothing to do. Run it in your app's folder, or pass --migrations. (Smoodly Cloud keeps the schema current; a self-host app gets the folder from \`supabase init\`.)`;
@@ -87,7 +94,7 @@ export async function runMigrate(args, out, deps = {}) {
87
94
  out(NO_FOLDER(appDir));
88
95
  return 0;
89
96
  }
90
- const collections = await (deps.load ?? loadCollections)(resolve(cwd, args.collections), cwd);
97
+ const collections = await (deps.load ?? loadCollections)(resolve(cwd, collectionsFile(cwd, args.collections)), cwd);
91
98
  const plan = planMigrate({
92
99
  packageFiles: await readSqlFiles(PACKAGE_MIGRATIONS_DIR),
93
100
  appFiles: await readSqlFiles(appDir),
@@ -0,0 +1,23 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ import type { ResolvedConfig } from "../config.ts";
3
+ import { type SpaceConnection } from "./space-io.ts";
4
+ export type RebuildArgs = Record<string, never>;
5
+ export type RebuildDeps = {
6
+ cwd?: string;
7
+ connection?: SpaceConnection;
8
+ /** The app's config; default: loaded from the folder (load-config.ts). */
9
+ config?: ResolvedConfig;
10
+ /** Test seam: the projection; default rebuildSpace. */
11
+ rebuild?: (db: SupabaseClient, config: ResolvedConfig) => Promise<{
12
+ pages: number;
13
+ entries: number;
14
+ model: string;
15
+ }>;
16
+ };
17
+ /** The line a rebuild ends with: import's counts, and the model the rows now carry. */
18
+ export declare const rebuildMessage: (r: {
19
+ pages: number;
20
+ entries: number;
21
+ model: string;
22
+ }) => string;
23
+ export declare function runRebuild(_args: RebuildArgs, out: (line: string) => void, deps?: RebuildDeps): Promise<number>;
@@ -0,0 +1,22 @@
1
+ import { schemaBehind, schemaBehindMessage } from "../schema-version.js";
2
+ import { loadConfig } from "./load-config.js";
3
+ import { connectFromEnv, readSchemaVersion, rebuildSpace, rebuiltMessage, schemaRefusal } from "./space-io.js";
4
+ /** The line a rebuild ends with: import's counts, and the model the rows now carry. */
5
+ export const rebuildMessage = (r) => `${rebuiltMessage(r).replace(/\.$/, "")} (model ${r.model}).`;
6
+ export async function runRebuild(_args, out, deps = {}) {
7
+ const cwd = deps.cwd ?? process.cwd();
8
+ const { db } = deps.connection ?? connectFromEnv(cwd);
9
+ const schema = await readSchemaVersion(db);
10
+ if ("missing" in schema) {
11
+ out(schemaRefusal(schema.missing, "Rebuild"));
12
+ return 1;
13
+ }
14
+ // A behind schema has no delivery tables (version 8) or reads columns it lacks: refused before any write.
15
+ if (schemaBehind(schema.version)) {
16
+ out(schemaBehindMessage(schema.version));
17
+ return 1;
18
+ }
19
+ const config = deps.config ?? (await loadConfig(cwd));
20
+ out(rebuildMessage(await (deps.rebuild ?? rebuildSpace)(db, config)));
21
+ return 0;
22
+ }
@@ -1,4 +1,5 @@
1
1
  import type { SupabaseClient } from "@supabase/supabase-js";
2
+ import type { ResolvedConfig } from "../config.ts";
2
3
  import type { ContentTable } from "../sql-space.ts";
3
4
  import { type Row } from "../space-transfer.ts";
4
5
  export type SpaceConnection = {
@@ -13,7 +14,20 @@ export declare function loadAppEnv(cwd: string, env?: Record<string, string | un
13
14
  * folder, so the error names the folder it looked in. */
14
15
  export declare function connectFromEnv(cwd: string, env?: Record<string, string | undefined>): SpaceConnection;
15
16
  export { readSchemaVersion } from "../schema-read.ts";
16
- export declare function schemaRefusal(missing: "key" | "schema", command: "Export" | "Import"): string;
17
+ /** The key's space projected whole into the delivery tables (spec 2026-10-07 delivery model §2): the stores and
18
+ * the site built from the config as the app's glue builds them, then every page and entry. */
19
+ export declare function rebuildSpace(db: SupabaseClient, config: ResolvedConfig): Promise<{
20
+ model: string;
21
+ pages: number;
22
+ entries: number;
23
+ stale: number;
24
+ }>;
25
+ /** The line a rebuild ends with. */
26
+ export declare const rebuiltMessage: (counts: {
27
+ pages: number;
28
+ entries: number;
29
+ }) => string;
30
+ export declare function schemaRefusal(missing: "key" | "schema", command: "Export" | "Import" | "Rebuild"): string;
17
31
  /** Every row of one table in one space, paged in primary-key order. The space filter is explicit: a service-role key sees every space. */
18
32
  export declare function readSpaceRows(db: SupabaseClient, table: ContentTable, space: string, where?: Record<string, string>, columns?: string): Promise<Row[]>;
19
33
  type StorageErrorLike = {
@@ -7,7 +7,10 @@
7
7
  import { existsSync, readFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { parseEnv } from "node:util";
10
+ import { createSupabaseStores, siteOptions } from "../bind.js";
10
11
  import { createSmoodlyClient } from "../client.js";
12
+ import { projectAll } from "../project.js";
13
+ import { createSmoodlySite } from "../site.js";
11
14
  import { resolveSmoodlyEnv } from "../env.js";
12
15
  import { ROW_KEY } from "../space-transfer.js";
13
16
  export const MEDIA_BUCKET = "media";
@@ -38,6 +41,17 @@ export function connectFromEnv(cwd, env = process.env) {
38
41
  return { db: createSmoodlyClient(resolveSmoodlyEnv(env)) };
39
42
  }
40
43
  export { readSchemaVersion } from "../schema-read.js";
44
+ /** The key's space projected whole into the delivery tables (spec 2026-10-07 delivery model §2): the stores and
45
+ * the site built from the config as the app's glue builds them, then every page and entry. */
46
+ export async function rebuildSpace(db, config) {
47
+ const { pages, entries, assets, published } = createSupabaseStores(db, config);
48
+ const { projector, cacheKey } = createSmoodlySite(siteOptions(config, () => ({ pages, entries, assets, published })));
49
+ const names = [...(config.registry.collections ?? []), ...(config.registry.globals ?? [])].map((c) => c.name);
50
+ // The model fingerprint every row now carries: what a rebuild after a deploy wrote.
51
+ return { ...(await projectAll({ stores: { pages, entries }, projector, names })), model: cacheKey };
52
+ }
53
+ /** The line a rebuild ends with. */
54
+ export const rebuiltMessage = (counts) => `Rebuilt the delivery tables: ${counts.pages} ${counts.pages === 1 ? "page" : "pages"}, ${counts.entries} ${counts.entries === 1 ? "entry" : "entries"}.`;
41
55
  export function schemaRefusal(missing, command) {
42
56
  return missing === "key"
43
57
  ? `${command} needs SMOODLY_SECRET_KEY — the service-role key, or the cloud's write key. The publishable key reads no content.`
package/dist/client.js CHANGED
@@ -11,10 +11,15 @@ export function clientOptions(env) {
11
11
  // without one (spec 2026-09-30 tester findings §4): a self-host service role is a valid apikey.
12
12
  return { url: env.url, key: env.publishableKey || env.secretKey, headers: { Authorization: `Bearer ${env.secretKey}` } };
13
13
  }
14
+ /** Where a framework adapter hears each database call (spec 2026-10-07 delivery model): the Astro request
15
+ * scope installs it and counts, so a test can hold a live page to one lookup. A global, not an import:
16
+ * the client stays free of any adapter. */
17
+ const READ_COUNTER = Symbol.for("smoodly:read-counter");
14
18
  /** fetch that names the variable behind two failures supabase-js reports bare: the network
15
19
  * ("TypeError: fetch failed") and PostgREST refusing the key. */
16
20
  function namedFetch(url) {
17
21
  return async (input, init) => {
22
+ globalThis[READ_COUNTER]?.();
18
23
  let res;
19
24
  try {
20
25
  res = await fetch(input, init);
package/dist/config.d.ts CHANGED
@@ -5,28 +5,28 @@ import { type Addresses } from "./addresses.ts";
5
5
  import type { Anchors } from "./anchors.ts";
6
6
  import { type StyleValues, type StyleValuesInput } from "./styles.ts";
7
7
  import { type CustomConfigInput, type ResolvedCustom } from "./custom/index.ts";
8
- type Registered = {
9
- schema: {
10
- kind: string;
11
- name: string;
12
- slug?: string | Record<string, string>;
13
- collection?: string;
14
- fields?: Record<string, Descriptor>;
15
- settings?: Record<string, Descriptor>;
16
- /** The section's toggles on Smoodly's styles set and the CSS box (spec 2026-09-14 §2, §4). */
17
- styles?: false | {
18
- omit: string[];
19
- };
20
- css?: false;
8
+ /** A section, element or page schema as the config reads it. */
9
+ export type RegisteredSchema = {
10
+ kind: string;
11
+ name: string;
12
+ title?: string;
13
+ slug?: string | Record<string, string>;
14
+ collection?: string;
15
+ fields?: Record<string, Descriptor>;
16
+ settings?: Record<string, Descriptor>;
17
+ /** The section's toggles on Smoodly's styles set and the CSS box (spec 2026-09-14 §2, §4). */
18
+ styles?: false | {
19
+ omit: string[];
21
20
  };
21
+ css?: false;
22
22
  };
23
23
  export type SmoodlyConfig = {
24
24
  registry: {
25
- sections?: Registered[];
26
- elements?: Registered[];
25
+ sections?: RegisteredSchema[];
26
+ elements?: RegisteredSchema[];
27
27
  collections?: CollectionSchema<any>[];
28
- /** Page shapes and entry pages alike — everything that renders a URL. */
29
- pages?: Registered[];
28
+ /** Page types. Entry pages are views: they go to defineViews. */
29
+ pages?: RegisteredSchema[];
30
30
  /** Global content: code-declared singletons (spec 2026-09-07). */
31
31
  globals?: GlobalSchema[];
32
32
  };
@@ -49,7 +49,7 @@ export type SmoodlyConfig = {
49
49
  styles?: StyleValuesInput;
50
50
  /** The custom-CSS box (spec 2026-09-14 §4): off unless true. */
51
51
  css?: boolean;
52
- /** The custom section (spec 2026-09-14 §6): the columns' toolset, the stacking breakpoint, the project's bound RichText. */
52
+ /** The custom section (spec 2026-09-14 §6): the columns' toolset and the stacking breakpoint. The bound RichText is a view: defineViews takes it. */
53
53
  custom?: CustomConfigInput;
54
54
  /** Slug of the page record the site root serves. The admin maps this
55
55
  * slug to "/" (canvas URL, Pages list) and every other slug to
@@ -69,7 +69,16 @@ export type SmoodlyConfig = {
69
69
  path?: string;
70
70
  };
71
71
  };
72
- export type ResolvedConfig = Omit<SmoodlyConfig, "styles" | "custom" | "admin"> & {
72
+ /** The registry as defineConfig resolves it: schemas, and page types only under `pages`. */
73
+ export type Registry = {
74
+ sections: RegisteredSchema[];
75
+ elements: RegisteredSchema[];
76
+ collections: CollectionSchema<any>[];
77
+ globals: GlobalSchema[];
78
+ pages: RegisteredSchema[];
79
+ };
80
+ export type ResolvedConfig = Omit<SmoodlyConfig, "styles" | "custom" | "admin" | "registry"> & {
81
+ registry: Registry;
73
82
  homePageSlug: string;
74
83
  /** How deep editors may nest pages in the tree. Default 1 (flat). */
75
84
  pageDepth: number;
@@ -98,4 +107,3 @@ export type ResolvedConfig = Omit<SmoodlyConfig, "styles" | "custom" | "admin">
98
107
  * target of every media edge; `media`: the admin's Media screen route. */
99
108
  export declare const RESERVED_NAMES: readonly string[];
100
109
  export declare function defineConfig(config: SmoodlyConfig): ResolvedConfig;
101
- export {};