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
@@ -0,0 +1,40 @@
1
+ import type { LiveLoader } from "astro/loaders";
2
+ import type { CollectionSchema } from "../collections.ts";
3
+ import type { GlobalSchema } from "../global.ts";
4
+ import type { PageItem } from "../read-shapes.ts";
5
+ import type { EntriesQuery, GlobalValue, ReadAdapter, SmoodlyQueries, TypedEntryItem } from "../reads.ts";
6
+ import type { PageTreeItem } from "../site.ts";
7
+ export type EntryFilter = {
8
+ id?: string;
9
+ path?: string;
10
+ locale?: string;
11
+ };
12
+ export type EntriesFilter = EntriesQuery;
13
+ export type GlobalFilter = {
14
+ locale?: string;
15
+ };
16
+ export type PageFilter = {
17
+ id?: string;
18
+ path?: string;
19
+ locale?: string;
20
+ };
21
+ export type PagesFilter = {
22
+ locale?: string;
23
+ };
24
+ export type PageTreeFilter = {
25
+ id: string;
26
+ locale?: string;
27
+ };
28
+ type AnyQueries = SmoodlyQueries<readonly CollectionSchema<any, string>[], readonly GlobalSchema<any, string>[]>;
29
+ /**
30
+ * `open` builds the queries over the adapter it is handed; `adapter` is the one that runs the units. Each load opens
31
+ * its own queries over a recorder, so the cacheHint is the tags of the units the load ran — the read layer's, one
32
+ * source of truth (a collection's tags include the collections its references resolve into).
33
+ */
34
+ export declare function loadersOver(open: (adapter: ReadAdapter) => AnyQueries, adapter: ReadAdapter): {
35
+ entries<C extends CollectionSchema<any, string>>(schema: C): LiveLoader<TypedEntryItem<[C], C["name"]>, EntryFilter, EntriesFilter>;
36
+ global<S extends GlobalSchema<any, string>>(item: S): LiveLoader<GlobalValue<[S], S["name"]>, GlobalFilter, GlobalFilter>;
37
+ page(): LiveLoader<PageItem, PageFilter, never>;
38
+ pageTree(): LiveLoader<PageTreeItem, PageTreeFilter, PagesFilter>;
39
+ };
40
+ export {};
@@ -0,0 +1,100 @@
1
+ const findInTree = (items, id) => {
2
+ for (const item of items) {
3
+ if (item.id === id)
4
+ return item;
5
+ const below = findInTree(item.children, id);
6
+ if (below)
7
+ return below;
8
+ }
9
+ return undefined;
10
+ };
11
+ /**
12
+ * `open` builds the queries over the adapter it is handed; `adapter` is the one that runs the units. Each load opens
13
+ * its own queries over a recorder, so the cacheHint is the tags of the units the load ran — the read layer's, one
14
+ * source of truth (a collection's tags include the collections its references resolve into).
15
+ */
16
+ export function loadersOver(open, adapter) {
17
+ const recorded = () => {
18
+ const tags = new Set();
19
+ const queries = open({ ...adapter, run: (unit) => { for (const t of unit.tags)
20
+ tags.add(t); return adapter.run(unit); } });
21
+ return { queries, hint: () => ({ tags: [...tags] }) };
22
+ };
23
+ return {
24
+ entries(schema) {
25
+ const name = schema.name;
26
+ return {
27
+ name: `smoodly:${name}`,
28
+ async loadCollection({ filter }) {
29
+ const { queries, hint } = recorded();
30
+ const items = (await queries.getEntries(name, filter ?? {}));
31
+ return { entries: items.map((data) => ({ id: data.id, data })), cacheHint: hint() };
32
+ },
33
+ async loadEntry({ filter }) {
34
+ // Astro passes filter undefined for getLiveEntry("articles") with no lookup.
35
+ const f = filter;
36
+ const key = f?.id ?? f?.path;
37
+ if (!key)
38
+ return { error: new Error(`smoodly: getLiveEntry("${name}") needs an id or a path.`) };
39
+ const { queries, hint } = recorded();
40
+ const data = (await queries.getEntry(name, key, { locale: f?.locale }));
41
+ return data ? { id: data.id, data, cacheHint: hint() } : undefined;
42
+ },
43
+ };
44
+ },
45
+ global(item) {
46
+ const read = async (filter) => {
47
+ const { queries, hint } = recorded();
48
+ const data = (await queries.getGlobal(item.name, { locale: filter?.locale }));
49
+ return { data, hint: hint() };
50
+ };
51
+ return {
52
+ name: `smoodly:global:${item.name}`,
53
+ async loadCollection({ filter }) {
54
+ // An absent or unpublished item is an empty list that still carries the read's tags: publishing it expires the list.
55
+ const { data, hint } = await read(filter);
56
+ return { entries: data ? [{ id: item.name, data }] : [], cacheHint: hint };
57
+ },
58
+ async loadEntry({ filter }) {
59
+ const { data, hint } = await read(filter);
60
+ return data ? { id: item.name, data, cacheHint: hint } : undefined;
61
+ },
62
+ };
63
+ },
64
+ page() {
65
+ return {
66
+ name: "smoodly:pages",
67
+ async loadCollection() {
68
+ return { error: new Error('smoodly: the "pages" collection reads one page by path or id — the tree is getLiveCollection("pageTree").') };
69
+ },
70
+ async loadEntry({ filter }) {
71
+ const f = filter;
72
+ const key = f?.id ?? f?.path;
73
+ if (!key)
74
+ return { error: new Error('smoodly: getLiveEntry("pages") needs an id or a path.') };
75
+ const { queries, hint } = recorded();
76
+ const data = await queries.getPage(key, { locale: f?.locale });
77
+ return data ? { id: data.id, data, cacheHint: hint() } : undefined;
78
+ },
79
+ };
80
+ },
81
+ pageTree() {
82
+ return {
83
+ name: "smoodly:page-tree",
84
+ async loadCollection({ filter }) {
85
+ const { queries, hint } = recorded();
86
+ const roots = await queries.getPageTree({ locale: filter?.locale });
87
+ return { entries: roots.map((data) => ({ id: data.id, data })), cacheHint: hint() };
88
+ },
89
+ async loadEntry({ filter }) {
90
+ const f = filter;
91
+ if (!f?.id)
92
+ return { error: new Error('smoodly: getLiveEntry("pageTree") needs an id.') };
93
+ const { queries, hint } = recorded();
94
+ const data = findInTree(await queries.getPageTree({ locale: f.locale }), f.id);
95
+ return data ? { id: data.id, data, cacheHint: hint() } : undefined;
96
+ },
97
+ };
98
+ },
99
+ };
100
+ }
@@ -0,0 +1,9 @@
1
+ /** A collection's entries: `getLiveCollection("articles", { locale, where, order, limit, parent })`, `getLiveEntry("articles", id | { path, locale })`. */
2
+ export declare const entriesLoader: <C extends import("../collections.ts").CollectionSchema<any, string>>(schema: C) => import("astro/loaders").LiveLoader<import("../reads.ts").TypedEntryItem<[C], C["name"]>, import("./content-loaders.ts").EntryFilter, import("./content-loaders.ts").EntriesFilter>;
3
+ /** A global item: `getLiveEntry("footer", { locale })`. */
4
+ export declare const globalLoader: <S extends import("../global.ts").GlobalSchema<any, string>>(item: S) => import("astro/loaders").LiveLoader<import("../reads.ts").GlobalValue<[S], S["name"]>, import("./content-loaders.ts").GlobalFilter, import("./content-loaders.ts").GlobalFilter>;
5
+ /** One page by path or id: `getLiveEntry("pages", { path, locale })`. */
6
+ export declare const pageLoader: () => import("astro/loaders").LiveLoader<import("../read-shapes.ts").PageItem, import("./content-loaders.ts").PageFilter, never>;
7
+ /** The page tree, roots with children nested: `getLiveCollection("pageTree", { locale })`. */
8
+ export declare const pageTreeLoader: () => import("astro/loaders").LiveLoader<import("../site.ts").PageTreeItem, import("./content-loaders.ts").PageTreeFilter, import("./content-loaders.ts").PagesFilter>;
9
+ export type { EntriesFilter, EntryFilter, GlobalFilter, PageFilter, PageTreeFilter, PagesFilter } from "./content-loaders.ts";
@@ -0,0 +1,17 @@
1
+ // packages/smoodly/src/astro/content.ts
2
+ // The project's live loaders (spec 2026-10-02 astro cloud fit §6), bound to its site and run by Astro's read
3
+ // adapter: what src/live.config.ts hands to defineLiveCollection. The queries are built per load.
4
+ import { createQueries } from "../reads.js";
5
+ import { loadersOver } from "./content-loaders.js";
6
+ import { astroReads } from "./scope.js";
7
+ import { config, site } from "./server.js";
8
+ // Queries are built per load, over a recorder of the units' tags (content-loaders.ts).
9
+ const loaders = loadersOver((adapter) => createQueries({ site: site(), collections: config.registry.collections, globals: config.registry.globals }, adapter), astroReads);
10
+ /** A collection's entries: `getLiveCollection("articles", { locale, where, order, limit, parent })`, `getLiveEntry("articles", id | { path, locale })`. */
11
+ export const entriesLoader = loaders.entries;
12
+ /** A global item: `getLiveEntry("footer", { locale })`. */
13
+ export const globalLoader = loaders.global;
14
+ /** One page by path or id: `getLiveEntry("pages", { path, locale })`. */
15
+ export const pageLoader = loaders.page;
16
+ /** The page tree, roots with children nested: `getLiveCollection("pageTree", { locale })`. */
17
+ export const pageTreeLoader = loaders.pageTree;
@@ -0,0 +1 @@
1
+ export { customLayout } from "../custom/layout.ts";
@@ -0,0 +1,3 @@
1
+ // What the custom section renders in Astro (spec 2026-09-14 §6), decided in custom/layout.ts (shared with
2
+ // the React section) so Custom.astro is only markup.
3
+ export { customLayout } from "../custom/layout.js";
@@ -0,0 +1,20 @@
1
+ import type { Descriptor } from "../fields.ts";
2
+ type WithSettings = {
3
+ name: string;
4
+ title?: string;
5
+ settings?: Record<string, Descriptor>;
6
+ };
7
+ export type SchemaInfo = {
8
+ title: string;
9
+ defaults: Record<string, unknown>;
10
+ };
11
+ export type RegistryInfo = {
12
+ section(name: string): SchemaInfo;
13
+ element(name: string): SchemaInfo;
14
+ isSection(name: string): boolean;
15
+ };
16
+ export declare function registryInfo(registry: {
17
+ sections: readonly WithSettings[];
18
+ elements: readonly WithSettings[];
19
+ }): RegistryInfo;
20
+ export {};
@@ -0,0 +1,21 @@
1
+ // What a view found by file name is not given by a pairing (plan 2026-10-06 astro adapter 2): React's pair()
2
+ // (pairing.tsx) fills a section's and an element's settings defaults and the guard knows the schema's title;
3
+ // in Astro the zone and the richtext renderer read both from here, computed once per registry.
4
+ import { CUSTOM_NODE_TYPE } from "../custom/schema.js";
5
+ import { defaultsOf } from "../field-defaults.js";
6
+ export function registryInfo(registry) {
7
+ const table = (list) => new Map(list.map((s) => [s.name, { title: s.title ?? s.name, defaults: defaultsOf(s.settings ?? {}) }]));
8
+ const sections = table(registry.sections);
9
+ const elements = table(registry.elements);
10
+ // A fresh object each time: a view that writes into its settings must not change the next render's.
11
+ const of = (map, name, fallbackTitle) => {
12
+ const found = map.get(name);
13
+ return { title: found?.title ?? fallbackTitle, defaults: { ...(found?.defaults ?? {}) } };
14
+ };
15
+ return {
16
+ section: (name) => of(sections, name, name === CUSTOM_NODE_TYPE ? "Custom section" : name),
17
+ element: (name) => of(elements, name, name),
18
+ // The custom section is Smoodly's own and always registered.
19
+ isSection: (name) => name === CUSTOM_NODE_TYPE || sections.has(name),
20
+ };
21
+ }
@@ -0,0 +1 @@
1
+ export declare function loadSmoodlyEnv(root: string, mode: string, env?: Record<string, string | undefined>): void;
@@ -0,0 +1,21 @@
1
+ // The project's env files, for the build tool (spec 2026-10-02 astro cloud fit §1): Astro reads .env into
2
+ // import.meta.env, and Smoodly's server code reads process.env (env.ts), so the integration copies the
3
+ // SMOODLY_ values across when Astro starts. SITE_URL comes along too: Astro evaluates astro.config before
4
+ // it loads any .env, so the integration sets `site` from it when astro.config leaves it unset. Vite's
5
+ // order, later files winning; a value the host already set is never replaced. A deployed server gets its
6
+ // values from the host and never runs this.
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { parseEnv } from "node:util";
10
+ export function loadSmoodlyEnv(root, mode, env = process.env) {
11
+ const found = {};
12
+ for (const name of [".env", ".env.local", `.env.${mode}`, `.env.${mode}.local`]) {
13
+ const file = join(root, name);
14
+ if (existsSync(file))
15
+ Object.assign(found, parseEnv(readFileSync(file, "utf8")));
16
+ }
17
+ for (const [key, value] of Object.entries(found)) {
18
+ if ((key.startsWith("SMOODLY_") || key === "SITE_URL") && value !== undefined && env[key] === undefined)
19
+ env[key] = value;
20
+ }
21
+ }
@@ -0,0 +1,13 @@
1
+ import type { ResolvedImage } from "../media.ts";
2
+ import type { LocaleSet } from "../paths.ts";
3
+ import type { SiteHit } from "../site.ts";
4
+ export type PageHead = {
5
+ title?: string;
6
+ description?: string;
7
+ image?: ResolvedImage;
8
+ noindex: boolean;
9
+ canonical?: string;
10
+ /** locale → the page's address there, for a multi-locale site; empty otherwise. */
11
+ languages: Record<string, string>;
12
+ };
13
+ export declare function headOf(hit: SiteHit, locale: string, href: (path: string, locale: string) => string, locales: LocaleSet): PageHead;
@@ -0,0 +1,19 @@
1
+ export function headOf(hit, locale, href, locales) {
2
+ // A page type without seo: true still titles the document with its page title; an entry without seo leaves the title to the layout.
3
+ const seo = hit.seo ?? (hit.kind === "page" ? { title: hit.title } : {});
4
+ const own = hit.locales[locale]?.path;
5
+ const languages = {};
6
+ if (locales.supported.length > 1) {
7
+ for (const [l, alt] of Object.entries(hit.locales))
8
+ if (alt?.path)
9
+ languages[l] = href(alt.path, l);
10
+ }
11
+ return {
12
+ ...(seo.title ? { title: seo.title } : {}),
13
+ ...(seo.description ? { description: seo.description } : {}),
14
+ ...(seo.image ? { image: seo.image } : {}),
15
+ noindex: seo.noindex === true,
16
+ ...(own ? { canonical: href(own, locale) } : {}),
17
+ languages,
18
+ };
19
+ }
@@ -0,0 +1,27 @@
1
+ import type { ResolvedImage } from "../media.ts";
2
+ /** The srcset candidates, up to the original's width: Astro ignores any wider than the image anyway. */
3
+ export declare const IMAGE_WIDTHS: number[];
4
+ export declare function imageWidths(width: number): number[];
5
+ export type ImagePlacement = {
6
+ box: string;
7
+ frame: string;
8
+ svg: boolean;
9
+ };
10
+ export declare function placeImage(image: ResolvedImage, options?: {
11
+ aspect?: string;
12
+ handlesCrop?: boolean;
13
+ }): ImagePlacement;
14
+ export type ImageRequest = {
15
+ src: string;
16
+ width: number;
17
+ height: number;
18
+ format: "webp";
19
+ widths?: number[];
20
+ sizes?: string;
21
+ };
22
+ /** What to ask Astro's image service for, or null when the original URL is served as it is: an SVG (a vector
23
+ * has no sizes) or `unoptimized`. Widths, and so a srcset, only when the page says its `sizes`. */
24
+ export declare function imageRequest(image: ResolvedImage, options?: {
25
+ sizes?: string;
26
+ unoptimized?: boolean;
27
+ }): ImageRequest | null;
@@ -0,0 +1,29 @@
1
+ // What <Image> renders in Astro (spec 2026-09-08 media §9; the React one is image/SmoodlyImage.tsx): a box at
2
+ // the aspect, the picture inside placed by crop and hotspot, as inline style strings; and the candidate widths
3
+ // Astro's image service is asked for. Pure, so Image.astro is only markup around `getImage`.
4
+ import { boxAspectOf, frameFor } from "../image/crop-math.js";
5
+ /** The srcset candidates, up to the original's width: Astro ignores any wider than the image anyway. */
6
+ export const IMAGE_WIDTHS = [320, 640, 960, 1280, 1920, 2560];
7
+ export function imageWidths(width) {
8
+ return [...IMAGE_WIDTHS.filter((w) => w < width), width];
9
+ }
10
+ const pct = (n) => `${Math.round(n * 1000) / 1000}%`;
11
+ export function placeImage(image, options = {}) {
12
+ const box = boxAspectOf(image, options.aspect);
13
+ const frame = options.handlesCrop ? { left: 0, top: 0, width: 100, height: 100 } : frameFor(image, box);
14
+ return {
15
+ box: `display:block;position:relative;overflow:hidden;aspect-ratio:${box}`,
16
+ frame: `position:absolute;left:${pct(frame.left)};top:${pct(frame.top)};width:${pct(frame.width)};height:${pct(frame.height)};max-width:none`,
17
+ svg: image.mime === "image/svg+xml",
18
+ };
19
+ }
20
+ /** What to ask Astro's image service for, or null when the original URL is served as it is: an SVG (a vector
21
+ * has no sizes) or `unoptimized`. Widths, and so a srcset, only when the page says its `sizes`. */
22
+ export function imageRequest(image, options = {}) {
23
+ if (options.unoptimized || image.mime === "image/svg+xml")
24
+ return null;
25
+ return {
26
+ src: image.url, width: image.width, height: image.height, format: "webp",
27
+ ...(options.sizes ? { widths: imageWidths(image.width), sizes: options.sizes } : {}),
28
+ };
29
+ }
@@ -0,0 +1,43 @@
1
+ import type { AstroIntegration } from "astro";
2
+ import type { ResolvedConfig } from "../config.ts";
3
+ import { type SmoodlyContent } from "../content/content-api.ts";
4
+ import { type AstroI18n } from "./locale-paths.ts";
5
+ export type SmoodlyOptions = {
6
+ /** Where the admin is mounted: the same value as `admin.path` in smoodly.config.ts. Default "/admin". */
7
+ adminPath?: string;
8
+ /** The config file, from the project root. Default "smoodly.config.ts". */
9
+ config?: string;
10
+ };
11
+ /** The three routes, most specific first; a page file of the project's own wins over the shell for its address. */
12
+ export declare function routes(adminPath: string): {
13
+ pattern: string;
14
+ entrypoint: string;
15
+ }[];
16
+ /** The views by file name, under the project's source dir (root-relative, "/src" by default): a section's
17
+ * is sections/<section name>.astro, an element's elements/<element name>.astro, a page type's
18
+ * page-types/<page type name>.astro, an entry page's entry-pages/<collection name>.astro. */
19
+ export declare function viewsModule(srcDir: string): string;
20
+ /** `root` is the project's directory; `srcDir` its source dir relative to the root, with a leading slash. */
21
+ export declare function virtualModules(root: string, configFile: string, srcDir?: string): {
22
+ name: string;
23
+ resolveId(id: string): string | undefined;
24
+ load(id: string): string | undefined;
25
+ };
26
+ export default function smoodly(options?: SmoodlyOptions): AstroIntegration;
27
+ export type CreateContentOptions = {
28
+ /** astro.config's `i18n`, when the project sets one; the adapter's default routing otherwise (the default locale at the root). */
29
+ i18n?: AstroI18n;
30
+ /** The project root whose env files are read. Default: the working directory. */
31
+ root?: string;
32
+ /** The environment to read SMOODLY_ values from. Default: process.env, after the project's env files are loaded into it. */
33
+ env?: Record<string, string | undefined>;
34
+ };
35
+ /** Write content from code in an Astro project — a seed, a migration script (spec 2026-09-30 tester findings §1):
36
+ * the admin's own operations with no signed-in editor, so validation and the publish checks apply and a failure
37
+ * throws `SmoodlyContentError`. `const { content, href } = createContent(config)`; then `content.pages.create(…)`,
38
+ * `content.entries.create(…)`, `content.globals.set(…)`, `content.media.upload(…)`; tree nodes with `node(schema, fields)`
39
+ * from "smoodly". Reads the project's .env files like the integration does, so a script needs no env flag. */
40
+ export declare function createContent(config: ResolvedConfig, options?: CreateContentOptions): {
41
+ content: SmoodlyContent;
42
+ href: (path: string, locale: string) => string;
43
+ };
@@ -0,0 +1,111 @@
1
+ // Smoodly as an Astro integration (spec 2026-10-02 astro cloud fit §1): `integrations: [smoodly()]` in
2
+ // astro.config mounts the admin, the preview address and the page shell, adds the middleware that
3
+ // collects cache tags, and supplies the project's config and its views as virtual modules. The project
4
+ // holds no glue file. This module runs in the build tool, and in a script through createContent; never on the server.
5
+ import { join, relative } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { DEFAULT_ADMIN_PATH } from "../admin-path.js";
8
+ import { createSupabaseStores, siteOptions } from "../bind.js";
9
+ import { createSmoodlyClient } from "../client.js";
10
+ import { createSmoodlyContent } from "../content/content-api.js";
11
+ import { resolveSmoodlyEnv, siteUrl } from "../env.js";
12
+ import { mediaRemotePattern } from "../image/remote-pattern.js";
13
+ import { loadSmoodlyEnv } from "./env-files.js";
14
+ import { astroRouting, localeHref } from "./locale-paths.js";
15
+ import { PREVIEW_BASE } from "./preview.js";
16
+ import { createSmoodlySite } from "../site.js";
17
+ // In the repository this file is TypeScript; in the published package tsc made it JavaScript, and the
18
+ // .astro files were copied beside it unchanged.
19
+ const ext = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
20
+ const here = (path) => fileURLToPath(new URL(path, import.meta.url));
21
+ /** The three routes, most specific first; a page file of the project's own wins over the shell for its address. */
22
+ export function routes(adminPath) {
23
+ return [
24
+ { pattern: `${adminPath}/[...path]`, entrypoint: here(`./routes/admin${ext}`) },
25
+ { pattern: `${PREVIEW_BASE}/[...path]`, entrypoint: here("./routes/preview.astro") },
26
+ { pattern: "/[...path]", entrypoint: here("./routes/page.astro") },
27
+ ];
28
+ }
29
+ /** The views by file name, under the project's source dir (root-relative, "/src" by default): a section's
30
+ * is sections/<section name>.astro, an element's elements/<element name>.astro, a page type's
31
+ * page-types/<page type name>.astro, an entry page's entry-pages/<collection name>.astro. */
32
+ export function viewsModule(srcDir) {
33
+ const glob = (dir) => `named(import.meta.glob("${srcDir}/${dir}/*.astro", { eager: true }))`;
34
+ return `const named = (modules) => Object.fromEntries(Object.entries(modules).map(([file, module]) => [file.split("/").pop().replace(/\\.astro$/, ""), module.default]));
35
+ export const views = { sections: ${glob("sections")}, elements: ${glob("elements")}, pages: ${glob("page-types")}, entries: ${glob("entry-pages")} };
36
+ `;
37
+ }
38
+ const VIEWS_ID = "\0smoodly:views";
39
+ /** `root` is the project's directory; `srcDir` its source dir relative to the root, with a leading slash. */
40
+ export function virtualModules(root, configFile, srcDir = "/src") {
41
+ return {
42
+ name: "smoodly:virtual",
43
+ resolveId(id) {
44
+ if (id === "smoodly:config")
45
+ return join(root, configFile);
46
+ if (id === "smoodly:views")
47
+ return VIEWS_ID;
48
+ return undefined;
49
+ },
50
+ load(id) {
51
+ return id === VIEWS_ID ? viewsModule(srcDir) : undefined;
52
+ },
53
+ };
54
+ }
55
+ export default function smoodly(options = {}) {
56
+ const adminPath = options.adminPath ?? DEFAULT_ADMIN_PATH;
57
+ return {
58
+ name: "smoodly",
59
+ hooks: {
60
+ "astro:config:setup": ({ config, command, updateConfig, injectRoute, addMiddleware, logger }) => {
61
+ const root = fileURLToPath(config.root);
62
+ // Root-relative with a leading slash, as Vite's import.meta.glob reads it; a source dir that is the root itself is "".
63
+ const rel = relative(root, fileURLToPath(config.srcDir)).split("\\").join("/");
64
+ const srcDir = rel === "" ? "" : "/" + rel;
65
+ loadSmoodlyEnv(root, command === "dev" ? "development" : "production");
66
+ // The media host, for Astro's image service: <Image> is served resized through /_image only from an allowed host.
67
+ const mediaUrl = process.env.SMOODLY_URL;
68
+ if (!mediaUrl)
69
+ logger.warn("smoodly: SMOODLY_URL is not set at build — every image will fail: Astro's /_image serves only hosts in image.remotePatterns, and the media host is taken from SMOODLY_URL here. Set it where `astro build` runs.");
70
+ updateConfig({
71
+ image: { remotePatterns: mediaUrl ? [mediaRemotePattern(mediaUrl)] : [] },
72
+ vite: { plugins: [virtualModules(root, options.config ?? "smoodly.config.ts", srcDir)] },
73
+ });
74
+ for (const route of routes(adminPath))
75
+ injectRoute({ ...route, prerender: false });
76
+ addMiddleware({ entrypoint: here(`./middleware${ext}`), order: "pre" });
77
+ if (!config.cache?.provider) {
78
+ logger.warn("smoodly: no cache provider in astro.config — every page renders from the database. Add `cache: { provider: memoryCache() }` (from \"astro/config\").");
79
+ }
80
+ // astro.config is evaluated before Astro loads .env, so `site: process.env.SITE_URL` there sees only the
81
+ // shell's value; the env files were read above, and SITE_URL from them sets `site` here. A value
82
+ // without a scheme is named by siteUrl, not by Astro's bare "Invalid URL".
83
+ if (!config.site && process.env.SITE_URL) {
84
+ siteUrl(process.env);
85
+ updateConfig({ site: process.env.SITE_URL });
86
+ }
87
+ else if (!config.site) {
88
+ logger.warn("smoodly: `site` is not set — canonical, hreflang and og:url links are built from each request's own origin, which behind a proxy can be the internal host, and a live page is cached for a year. Set SITE_URL in .env or where `astro build` runs, or `site` in astro.config.");
89
+ }
90
+ },
91
+ },
92
+ };
93
+ }
94
+ /** Write content from code in an Astro project — a seed, a migration script (spec 2026-09-30 tester findings §1):
95
+ * the admin's own operations with no signed-in editor, so validation and the publish checks apply and a failure
96
+ * throws `SmoodlyContentError`. `const { content, href } = createContent(config)`; then `content.pages.create(…)`,
97
+ * `content.entries.create(…)`, `content.globals.set(…)`, `content.media.upload(…)`; tree nodes with `node(schema, fields)`
98
+ * from "smoodly". Reads the project's .env files like the integration does, so a script needs no env flag. */
99
+ export function createContent(config, options = {}) {
100
+ let env = options.env;
101
+ if (!env) {
102
+ loadSmoodlyEnv(options.root ?? process.cwd(), process.env.NODE_ENV === "production" ? "production" : "development");
103
+ env = process.env;
104
+ }
105
+ const db = createSmoodlyClient(resolveSmoodlyEnv(env));
106
+ const { pages, entries, assets, members, published } = createSupabaseStores(db, config);
107
+ const href = localeHref(config.locales, astroRouting(options.i18n, config.locales));
108
+ // The site's projector: a publish from a script writes the delivery rows as the admin's does.
109
+ const { projector } = createSmoodlySite(siteOptions(config, () => ({ pages, entries, assets, published })));
110
+ return { content: createSmoodlyContent({ config, pages, entries, assets, members, href, projector }), href };
111
+ }
@@ -0,0 +1,30 @@
1
+ import { type LocaleSet } from "../paths.ts";
2
+ /** `redirectToDefault`: Astro itself sends "/" to the default locale's home (its redirectToDefaultLocale). */
3
+ export type LocaleRouting = {
4
+ prefixDefault: boolean;
5
+ redirectToDefault: boolean;
6
+ };
7
+ export declare const DEFAULT_ROUTING: LocaleRouting;
8
+ /** Prefix-always without Astro's root redirect lets "/" through to the catch-all, so the adapter sends it where Astro would. */
9
+ export declare const redirectsRoot: (pathname: string, routing: LocaleRouting) => boolean;
10
+ /** `astro:config/server`'s `i18n`, the part read here. */
11
+ export type AstroI18n = {
12
+ defaultLocale: string;
13
+ locales: (string | {
14
+ path: string;
15
+ codes: string[];
16
+ })[];
17
+ routing?: string | {
18
+ prefixDefaultLocale?: boolean;
19
+ redirectToDefaultLocale?: boolean;
20
+ };
21
+ domains?: Record<string, string>;
22
+ };
23
+ export declare function splitLocale(pathname: string, locales: LocaleSet, routing?: LocaleRouting): {
24
+ locale: string;
25
+ path: string;
26
+ } | null;
27
+ /** Locale-relative CMS path → site URL: what the operations and the `page` prop link with. */
28
+ export declare const localeHref: (locales: LocaleSet, routing?: LocaleRouting) => (path: string, locale: string) => string;
29
+ /** The rule astro.config's i18n asks for; its locales must be the config's, or links and reads would disagree. */
30
+ export declare function astroRouting(i18n: AstroI18n | undefined, locales: LocaleSet): LocaleRouting;
@@ -0,0 +1,38 @@
1
+ // Which locale an address is in, and the address a locale-relative path gets (plan 2026-10-05 astro adapter
2
+ // 1; spec 2026-10-02 astro cloud fit, "languages on Astro's own routing", plan 2026-10-06 astro adapter 2):
3
+ // the default locale at the root and every other under its code, unless astro.config's i18n prefixes the
4
+ // default too. The same rule both ways, so the admin's links land where the shell reads. Astro's own i18n
5
+ // router answers its 404 for a prefixed default locale; the root under prefix-always it redirects only with
6
+ // redirectToDefaultLocale, else it lets "/" through and the shell redirects it. The adapter agrees with it.
7
+ import { decodeSegment, joinPath } from "../paths.js";
8
+ export const DEFAULT_ROUTING = { prefixDefault: false, redirectToDefault: false };
9
+ /** Prefix-always without Astro's root redirect lets "/" through to the catch-all, so the adapter sends it where Astro would. */
10
+ export const redirectsRoot = (pathname, routing) => routing.prefixDefault && !routing.redirectToDefault && pathname.split("/").filter(Boolean).length === 0;
11
+ export function splitLocale(pathname, locales, routing = DEFAULT_ROUTING) {
12
+ const segments = pathname.split("/").filter(Boolean).map(decodeSegment);
13
+ const first = segments[0];
14
+ if (first !== undefined && locales.supported.includes(first) && (routing.prefixDefault || first !== locales.default)) {
15
+ return { locale: first, path: joinPath(segments.slice(1)) };
16
+ }
17
+ // Every locale is prefixed: an unprefixed address is nobody's.
18
+ if (routing.prefixDefault)
19
+ return null;
20
+ return { locale: locales.default, path: joinPath(segments) };
21
+ }
22
+ /** Locale-relative CMS path → site URL: what the operations and the `page` prop link with. */
23
+ export const localeHref = (locales, routing = DEFAULT_ROUTING) => (path, locale) => locale === locales.default && !routing.prefixDefault ? path : path === "/" ? `/${locale}` : `/${locale}${path}`;
24
+ /** The rule astro.config's i18n asks for; its locales must be the config's, or links and reads would disagree. */
25
+ export function astroRouting(i18n, locales) {
26
+ if (!i18n)
27
+ return DEFAULT_ROUTING;
28
+ if (i18n.domains && Object.keys(i18n.domains).length > 0) {
29
+ throw new Error("smoodly: i18n.domains is not supported yet — locales on their own hosts need the address rule to know the host. Remove `domains` from astro.config's i18n, or keep locales under path prefixes.");
30
+ }
31
+ const codes = i18n.locales.map((l) => (typeof l === "string" ? l : l.path));
32
+ const same = i18n.defaultLocale === locales.default && codes.every((c) => locales.supported.includes(c)) && locales.supported.every((c) => codes.includes(c));
33
+ if (!same) {
34
+ throw new Error(`smoodly: astro.config's i18n (default "${i18n.defaultLocale}", locales ${codes.join(", ")}) and smoodly.config.ts (default "${locales.default}", locales ${locales.supported.join(", ")}) disagree — give both the same locales.`);
35
+ }
36
+ const r = typeof i18n.routing === "object" && i18n.routing !== null ? i18n.routing : {};
37
+ return { prefixDefault: r.prefixDefaultLocale === true, redirectToDefault: r.redirectToDefaultLocale === true };
38
+ }
@@ -0,0 +1 @@
1
+ export declare const onRequest: import("astro").MiddlewareHandler;