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.
- package/README.md +14 -13
- package/dist/admin/auth.d.ts +3 -11
- package/dist/admin/auth.js +1 -30
- package/dist/admin/client-ops.d.ts +5 -4
- package/dist/admin/client-ops.js +15 -12
- package/dist/admin/delivery-mode.d.ts +1 -1
- package/dist/admin/delivery-mode.js +2 -2
- package/dist/admin/drift.d.ts +0 -2
- package/dist/admin/drift.js +1 -1
- package/dist/admin/editor/EditorView.d.ts +0 -3
- package/dist/admin/editor/EditorView.js +11 -16
- package/dist/admin/editor/FloatingToolbar.d.ts +0 -1
- package/dist/admin/editor/FloatingToolbar.js +1 -1
- package/dist/admin/editor/SeoSettings.d.ts +0 -2
- package/dist/admin/editor/SeoSettings.js +2 -2
- package/dist/admin/editor/canvas-address.d.ts +8 -0
- package/dist/admin/editor/canvas-address.js +18 -2
- package/dist/admin/editor/canvas-diagnosis.d.ts +2 -0
- package/dist/admin/editor/canvas-diagnosis.js +3 -0
- package/dist/admin/editor/inline-dom.d.ts +0 -4
- package/dist/admin/editor/inline-dom.js +1 -1
- package/dist/admin/editor/preview-grant.d.ts +6 -0
- package/dist/admin/editor/preview-grant.js +25 -0
- package/dist/admin/editor/useCanvasDiagnosis.js +2 -2
- package/dist/admin/files.generated.js +1 -1
- package/dist/admin/fixed-nodes.d.ts +0 -6
- package/dist/admin/fixed-nodes.js +1 -81
- package/dist/admin/forms/CropEditor.d.ts +4 -2
- package/dist/admin/forms/CropEditor.js +58 -5
- package/dist/admin/forms/ImagePreview.d.ts +3 -1
- package/dist/admin/forms/ImagePreview.js +9 -3
- package/dist/admin/forms/MediaGrid.js +2 -1
- package/dist/admin/forms/MediaPicker.js +9 -6
- package/dist/admin/forms/MediaWidget.js +11 -9
- package/dist/admin/forms/assets-context.d.ts +7 -3
- package/dist/admin/forms/assets-context.js +5 -3
- package/dist/admin/forms/crop-editor.d.ts +24 -2
- package/dist/admin/forms/crop-editor.js +36 -2
- package/dist/admin/forms/date-picker.d.ts +0 -1
- package/dist/admin/forms/date-picker.js +1 -1
- package/dist/admin/forms/tabs.d.ts +0 -1
- package/dist/admin/forms/tabs.js +1 -1
- package/dist/admin/forms/upload.d.ts +18 -11
- package/dist/admin/forms/upload.js +62 -24
- package/dist/admin/global-items.d.ts +0 -3
- package/dist/admin/global-items.js +0 -54
- package/dist/admin/handler.d.ts +22 -28
- package/dist/admin/handler.js +146 -47
- package/dist/admin/health.d.ts +0 -4
- package/dist/admin/index.d.ts +6 -9
- package/dist/admin/index.js +3 -6
- package/dist/admin/ops.d.ts +40 -32
- package/dist/admin/page-data.d.ts +14 -6
- package/dist/admin/page-data.js +1 -1
- package/dist/admin/session-flow.d.ts +65 -0
- package/dist/admin/session-flow.js +121 -0
- package/dist/admin/shell/AdminApp.d.ts +8 -3
- package/dist/admin/shell/AdminApp.js +37 -58
- package/dist/admin/shell/ContentHealthPage.js +1 -3
- package/dist/admin/shell/GlobalFields.d.ts +0 -3
- package/dist/admin/shell/GlobalFields.js +1 -1
- package/dist/admin/shell/IconRail.js +10 -8
- package/dist/admin/shell/MediaScreen.js +9 -6
- package/dist/admin/shell/MembersPage.d.ts +1 -2
- package/dist/admin/shell/MembersPage.js +19 -90
- package/dist/admin/shell/SettingsView.d.ts +1 -2
- package/dist/admin/shell/SettingsView.js +2 -2
- package/dist/admin/shell/SignedOutView.d.ts +5 -0
- package/dist/admin/shell/SignedOutView.js +9 -0
- package/dist/admin/shell/console-links.d.ts +5 -0
- package/dist/admin/shell/console-links.js +11 -0
- package/dist/admin/shell/members-list.d.ts +1 -14
- package/dist/admin/shell/members-list.js +2 -23
- package/dist/admin/shell/pages-tree.d.ts +0 -2
- package/dist/admin/shell/pages-tree.js +1 -1
- package/dist/admin/shell/presence-socket.d.ts +10 -0
- package/dist/admin/shell/presence-socket.js +191 -0
- package/dist/admin/shell/presence.d.ts +6 -18
- package/dist/admin/shell/presence.js +4 -11
- package/dist/admin/shell/session.d.ts +19 -12
- package/dist/admin/shell/session.js +114 -47
- package/dist/admin/ui/LocaleSwitcher.d.ts +0 -4
- package/dist/admin/ui/LocaleSwitcher.js +2 -2
- package/dist/admin/ui/primitives.d.ts +4 -14
- package/dist/admin/ui/primitives.js +7 -9
- package/dist/admin/ui/theme.js +13 -2
- package/dist/admin/validate.js +10 -1
- package/dist/api-client.d.ts +26 -0
- package/dist/api-client.js +107 -0
- package/dist/assemble.d.ts +4 -41
- package/dist/assemble.js +5 -303
- package/dist/asset-store.d.ts +28 -30
- package/dist/asset-store.js +26 -121
- package/dist/astro/cache.d.ts +1 -1
- package/dist/astro/components/Head.astro +4 -1
- package/dist/astro/components/Image.astro +12 -17
- package/dist/astro/components/Video.astro +5 -2
- package/dist/astro/head.d.ts +3 -0
- package/dist/astro/head.js +24 -0
- package/dist/astro/index.js +61 -27
- package/dist/astro/locale-paths.d.ts +5 -2
- package/dist/astro/locale-paths.js +10 -2
- package/dist/astro/routes/admin.js +9 -7
- package/dist/astro/runtime.d.ts +3 -2
- package/dist/astro/runtime.js +5 -4
- package/dist/astro/server.d.ts +7 -11
- package/dist/astro/server.js +22 -28
- package/dist/astro/shell.d.ts +3 -3
- package/dist/astro/shell.js +1 -1
- package/dist/astro/static-media-node.d.ts +4 -0
- package/dist/astro/static-media-node.js +25 -0
- package/dist/astro/static-media.d.ts +17 -0
- package/dist/astro/static-media.js +55 -0
- package/dist/astro/static.d.ts +5 -10
- package/dist/astro/static.js +13 -29
- package/dist/canvas-marker.d.ts +14 -0
- package/dist/canvas-marker.js +36 -0
- package/dist/cli/api.d.ts +14 -0
- package/dist/cli/api.js +48 -0
- package/dist/cli/app-env.d.ts +2 -0
- package/dist/cli/app-env.js +20 -0
- package/dist/cli/export.d.ts +16 -2
- package/dist/cli/export.js +52 -35
- package/dist/cli/import.d.ts +7 -11
- package/dist/cli/import.js +50 -54
- package/dist/cli/load-config.d.ts +0 -1
- package/dist/cli/load-config.js +1 -1
- package/dist/cli/main.d.ts +4 -12
- package/dist/cli/main.js +26 -72
- package/dist/cli/push.d.ts +6 -0
- package/dist/cli/push.js +31 -0
- package/dist/collections.d.ts +0 -1
- package/dist/collections.js +1 -1
- package/dist/config.d.ts +20 -5
- package/dist/config.js +18 -1
- package/dist/content/content-api.d.ts +23 -23
- package/dist/content/content-api.js +48 -56
- package/dist/content/mime.d.ts +1 -0
- package/dist/content/mime.js +6 -0
- package/dist/css.js +1 -1
- package/dist/deploy-store.d.ts +19 -0
- package/dist/entry-store.d.ts +8 -94
- package/dist/entry-store.js +9 -557
- package/dist/env.d.ts +27 -20
- package/dist/env.js +38 -50
- package/dist/fields.d.ts +11 -3
- package/dist/fields.js +33 -2
- package/dist/global-id.d.ts +2 -0
- package/dist/global-id.js +4 -2
- package/dist/image/SmoodlyImage.d.ts +9 -5
- package/dist/image/SmoodlyImage.js +29 -15
- package/dist/image/SmoodlyVideo.d.ts +1 -1
- package/dist/image/SmoodlyVideo.js +3 -2
- package/dist/image/index.d.ts +2 -3
- package/dist/image/index.js +0 -2
- package/dist/image/markup.d.ts +53 -0
- package/dist/image/markup.js +111 -0
- package/dist/index.d.ts +13 -23
- package/dist/index.js +7 -20
- package/dist/media-cut.d.ts +44 -0
- package/dist/media-cut.js +70 -0
- package/dist/media.d.ts +58 -4
- package/dist/media.js +126 -16
- package/dist/member-store.d.ts +2 -50
- package/dist/member-store.js +0 -56
- package/dist/next/admin-route.d.ts +1 -1
- package/dist/next/admin-route.js +5 -3
- package/dist/next/meta.js +2 -3
- package/dist/next/page-renderer.d.ts +2 -2
- package/dist/next/page-renderer.js +22 -11
- package/dist/next/queries.d.ts +2 -2
- package/dist/path-index.d.ts +0 -24
- package/dist/path-index.js +0 -98
- package/dist/paths.d.ts +1 -1
- package/dist/paths.js +1 -1
- package/dist/push.d.ts +30 -0
- package/dist/push.js +52 -0
- package/dist/read-counter.d.ts +2 -0
- package/dist/read-counter.js +6 -0
- package/dist/reads.d.ts +7 -7
- package/dist/reads.js +3 -0
- package/dist/registry-json.d.ts +11 -0
- package/dist/registry-json.js +81 -0
- package/dist/render.d.ts +0 -1
- package/dist/render.js +1 -1
- package/dist/resolve.d.ts +12 -6
- package/dist/resolve.js +11 -8
- package/dist/revalidate.d.ts +8 -1
- package/dist/revalidate.js +8 -1
- package/dist/richtext/index.d.ts +1 -1
- package/dist/section-wrapper.d.ts +1 -1
- package/dist/section-wrapper.js +0 -9
- package/dist/signed-message.d.ts +26 -0
- package/dist/signed-message.js +103 -0
- package/dist/site.d.ts +59 -18
- package/dist/site.js +21 -252
- package/dist/space-transfer.d.ts +6 -3
- package/dist/space-transfer.js +18 -6
- package/dist/store.d.ts +2 -80
- package/dist/store.js +3 -489
- package/dist/toolset.d.ts +2 -1
- package/dist/toolset.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -15
- package/dist/admin/auth-config.d.ts +0 -6
- package/dist/admin/browser/fetch-op.d.ts +0 -10
- package/dist/admin/browser/fetch-op.js +0 -44
- package/dist/admin/browser/main.d.ts +0 -1
- package/dist/admin/browser/main.js +0 -23
- package/dist/admin/ops-impl.d.ts +0 -51
- package/dist/admin/ops-impl.js +0 -1403
- package/dist/admin/shell/InviteModal.d.ts +0 -6
- package/dist/admin/shell/InviteModal.js +0 -28
- package/dist/admin/shell/LoginView.d.ts +0 -5
- package/dist/admin/shell/LoginView.js +0 -80
- package/dist/admin/shell/SchemaNotice.d.ts +0 -4
- package/dist/admin/shell/SchemaNotice.js +0 -4
- package/dist/admin/shell/SetPasswordView.d.ts +0 -7
- package/dist/admin/shell/SetPasswordView.js +0 -41
- package/dist/admin/shell/presence-realtime.d.ts +0 -3
- package/dist/admin/shell/presence-realtime.js +0 -32
- package/dist/admin/supabase-auth.d.ts +0 -7
- package/dist/admin/supabase-auth.js +0 -30
- package/dist/astro/deploy-deps.d.ts +0 -10
- package/dist/astro/deploy-deps.js +0 -8
- package/dist/astro/image.d.ts +0 -27
- package/dist/astro/image.js +0 -29
- package/dist/bind.d.ts +0 -19
- package/dist/bind.js +0 -34
- package/dist/cli/add-admin.d.ts +0 -11
- package/dist/cli/add-admin.js +0 -25
- package/dist/cli/load-collections.d.ts +0 -2
- package/dist/cli/load-collections.js +0 -27
- package/dist/cli/migrate.d.ts +0 -43
- package/dist/cli/migrate.js +0 -138
- package/dist/cli/rebuild.d.ts +0 -23
- package/dist/cli/rebuild.js +0 -22
- package/dist/cli/space-io.d.ts +0 -41
- package/dist/cli/space-io.js +0 -84
- package/dist/client.d.ts +0 -11
- package/dist/client.js +0 -54
- package/dist/current-space.d.ts +0 -4
- package/dist/current-space.js +0 -9
- package/dist/document-stamp.d.ts +0 -7
- package/dist/document-stamp.js +0 -48
- package/dist/image/loaders.d.ts +0 -15
- package/dist/image/loaders.js +0 -5
- package/dist/image/next-config.d.ts +0 -18
- package/dist/image/next-config.js +0 -32
- package/dist/image/remote-pattern.d.ts +0 -1
- package/dist/image/remote-pattern.js +0 -5
- package/dist/migrations.d.ts +0 -37
- package/dist/migrations.js +0 -73
- package/dist/project.d.ts +0 -88
- package/dist/project.js +0 -231
- package/dist/published.d.ts +0 -142
- package/dist/published.js +0 -164
- package/dist/ref-index.d.ts +0 -19
- package/dist/ref-index.js +0 -0
- package/dist/schema-read.d.ts +0 -23
- package/dist/schema-read.js +0 -50
- package/dist/schema-version.d.ts +0 -19
- package/dist/schema-version.js +0 -34
- package/dist/sql-space.d.ts +0 -44
- package/dist/sql-space.js +0 -289
- package/dist/sql.d.ts +0 -15
- package/dist/sql.js +0 -556
- package/dist/supabase-asset-store.d.ts +0 -21
- package/dist/supabase-asset-store.js +0 -170
- package/dist/supabase-entry-store.d.ts +0 -92
- package/dist/supabase-entry-store.js +0 -824
- package/dist/supabase-member-store.d.ts +0 -21
- package/dist/supabase-member-store.js +0 -96
- package/dist/supabase-path-index.d.ts +0 -30
- package/dist/supabase-path-index.js +0 -138
- package/dist/supabase-published-store.d.ts +0 -56
- package/dist/supabase-published-store.js +0 -314
- package/dist/supabase-store.d.ts +0 -103
- package/dist/supabase-store.js +0 -678
- package/migrations/20260926120000_smoodly_v1.sql +0 -535
- package/migrations/20260926130000_smoodly_v2.sql +0 -26
- package/migrations/20260926140000_smoodly_v3.sql +0 -38
- package/migrations/20260926150000_smoodly_v4.sql +0 -41
- package/migrations/20260929120000_smoodly_v5.sql +0 -56
- package/migrations/20260929135523_smoodly_v6.sql +0 -200
- package/migrations/20260930040956_smoodly_v7.sql +0 -84
- package/migrations/20261007120000_smoodly_v8.sql +0 -154
- package/migrations/20261007200000_smoodly_v9.sql +0 -30
- package/migrations/20261008120000_smoodly_v10.sql +0 -47
- package/migrations/20261009120000_smoodly_v11.sql +0 -49
- /package/dist/{admin/auth-config.js → deploy-store.js} +0 -0
package/dist/next/admin-route.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// packages/smoodly/src/next/admin-route.ts
|
|
2
2
|
// The admin in a Next app (spec 2026-10-02 astro cloud fit §4): one route handler file exports GET and
|
|
3
|
-
// POST from here, and that is the whole mount — the page, the package's files
|
|
4
|
-
//
|
|
3
|
+
// POST from here, and that is the whole mount — the page, the package's files, the revalidate route the
|
|
4
|
+
// service calls and the preview route (admin/handler.ts). This file adds only what is Next's: the cache
|
|
5
|
+
// and draft mode.
|
|
5
6
|
import { revalidateTag } from "next/cache";
|
|
6
7
|
import { draftMode } from "next/headers";
|
|
7
8
|
import { connection } from "next/server";
|
|
@@ -17,7 +18,8 @@ const effects = {
|
|
|
17
18
|
};
|
|
18
19
|
/** `deps` is read per request: the app's glue reads the environment there, so `next build` needs no keys. */
|
|
19
20
|
export function createAdminRoute(deps) {
|
|
20
|
-
|
|
21
|
+
// The canvas marks the path: Vercel keeps only a prerendered route's own params in the query, draft mode included.
|
|
22
|
+
const handle = createAdminHandler(() => ({ ...deps(), effects, canvasMarks: "path" }));
|
|
21
23
|
// A request first: the handler catches every error to answer with a named one, and Next's own
|
|
22
24
|
// "this route is dynamic" signal must not be among them.
|
|
23
25
|
const route = async (request) => {
|
package/dist/next/meta.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { imageUrl } from "../image/loaders.js";
|
|
2
1
|
import { pageSeo } from "../seo.js";
|
|
3
2
|
/** Maps the effective SEO values onto Next metadata (spec 2026-09-10 page
|
|
4
3
|
* fields §4, 2026-09-29 content API contract §7). Absent values leave the
|
|
@@ -16,8 +15,8 @@ export function metadataFromSeo(seo) {
|
|
|
16
15
|
if (description)
|
|
17
16
|
og.description = description;
|
|
18
17
|
if (image) {
|
|
19
|
-
// The
|
|
20
|
-
og.images = [{ url:
|
|
18
|
+
// The original, not a cut: a scraper asks no AVIF and some read neither AVIF nor WebP (spec 2026-10-18 media §7).
|
|
19
|
+
og.images = [{ url: image.url, width: image.width, height: image.height, ...(image.alt ? { alt: image.alt } : {}) }];
|
|
21
20
|
}
|
|
22
21
|
if (Object.keys(og).length > 0)
|
|
23
22
|
out.openGraph = og;
|
|
@@ -3,7 +3,7 @@ import type { Metadata } from "next";
|
|
|
3
3
|
import type { Descriptor } from "../fields.ts";
|
|
4
4
|
import { type PairedPage } from "../render.tsx";
|
|
5
5
|
import type { GlobalSchema } from "../global.ts";
|
|
6
|
-
import { type
|
|
6
|
+
import { type ContentReads } from "../site.ts";
|
|
7
7
|
/** Logged once per renderer when Next's cacheComponents is on (spec 2026-09-27 cache components §2). */
|
|
8
8
|
export declare const CACHE_COMPONENTS_WARNING = "smoodly: Next's cacheComponents is on, and Smoodly does not support it \u2014 a missing page answers 200 with noindex instead of 404, and a redirect answers 200 instead of 308. Turn cacheComponents off in next.config.ts; the Next.js guide's \"Cache Components\" section says why (https://smoodly.io/docs/frameworks/nextjs).";
|
|
9
9
|
/** A registered section, as renderPage's registry needs it. */
|
|
@@ -22,7 +22,7 @@ export type PageRegistration = ((props: any) => ReactElement) & {
|
|
|
22
22
|
};
|
|
23
23
|
};
|
|
24
24
|
export type PageRendererOptions = {
|
|
25
|
-
site:
|
|
25
|
+
site: ContentReads;
|
|
26
26
|
sections: SectionEntry[];
|
|
27
27
|
/** Registered Elements: what the custom section's default richtext renderer needs when no RichText is bound (spec 2026-09-14 §6). */
|
|
28
28
|
elements?: SectionEntry[];
|
|
@@ -8,6 +8,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
8
8
|
import { Suspense } from "react";
|
|
9
9
|
import { draftMode } from "next/headers";
|
|
10
10
|
import { notFound, permanentRedirect, unstable_rethrow } from "next/navigation";
|
|
11
|
+
import { markCanvasPath, readCanvasMarker } from "../canvas-marker.js";
|
|
11
12
|
import { BRIDGE_CSS } from "../marks.js";
|
|
12
13
|
import { CanvasBridgeSlot, CanvasNetSlot } from "./canvas-slot.js";
|
|
13
14
|
import { DRAFT_META_NAME } from "../admin/editor/canvas-diagnosis.js";
|
|
@@ -36,8 +37,9 @@ export function createPageRenderer(options) {
|
|
|
36
37
|
* The hit for a located target (locate is uncached: the id it returns IS the cache tag): the read
|
|
37
38
|
* layer's unit, run by Next's adapter — the draft inside draft mode, one cached unit outside it. */
|
|
38
39
|
const load = (target, locale, path, version) => nextReads.run(hitUnit(site, target, locale, path, version));
|
|
39
|
-
/** A redirect row (spec 2026-09-26 redirects §7): the node's CURRENT path in this locale, through href, the query
|
|
40
|
-
|
|
40
|
+
/** A redirect row (spec 2026-09-26 redirects §7): the node's CURRENT path in this locale, through href, the query
|
|
41
|
+
* and the canvas marker kept; no path there is a 404. */
|
|
42
|
+
async function redirectTo(target, locale, params, flags) {
|
|
41
43
|
const current = (await site.targetPaths(target))[locale];
|
|
42
44
|
if (!current)
|
|
43
45
|
notFound();
|
|
@@ -50,7 +52,8 @@ export function createPageRenderer(options) {
|
|
|
50
52
|
query.append(k, item);
|
|
51
53
|
}
|
|
52
54
|
const qs = query.toString();
|
|
53
|
-
|
|
55
|
+
const to = href(current, locale) + (qs ? `?${qs}` : "");
|
|
56
|
+
permanentRedirect(flags ? markCanvasPath(to, flags) : to);
|
|
54
57
|
}
|
|
55
58
|
function render(hit, editor, locale) {
|
|
56
59
|
if (hit.kind === "page") {
|
|
@@ -94,28 +97,32 @@ export function createPageRenderer(options) {
|
|
|
94
97
|
cacheComponentsWarned = true;
|
|
95
98
|
console.warn(CACHE_COMPONENTS_WARNING);
|
|
96
99
|
}
|
|
97
|
-
async function page(
|
|
100
|
+
async function page(address, opts, accept) {
|
|
98
101
|
warnCacheComponents(); // before buildGate(), so a build's prerender prints it too
|
|
99
102
|
// A CMS route renders on its first visit and is cached until a tag expires it (plan 2026-10-09 next
|
|
100
103
|
// cached routes): nothing here may touch a dynamic API at run time. At build, the gate keeps a route
|
|
101
104
|
// without a dynamic segment off the database (DESIGN.md §3).
|
|
102
105
|
await buildGate();
|
|
103
106
|
const locale = opts?.locale ?? site.locales.default;
|
|
104
|
-
const path = pathFor(locale);
|
|
105
107
|
const { isEnabled: draft } = await draftMode();
|
|
108
|
+
const { pathFor, marker } = address(draft);
|
|
109
|
+
const path = pathFor(locale);
|
|
106
110
|
// searchParams is a dynamic API: read inside draft mode only, where the render is dynamic anyway.
|
|
107
111
|
const params = draft ? await opts?.searchParams : undefined;
|
|
108
|
-
//
|
|
109
|
-
|
|
112
|
+
// The flags exist only inside draft mode: the public site never sees an editor bridge or an old version. The
|
|
113
|
+
// canvas marks the path (Vercel strips the query); the query still works where a host keeps it.
|
|
110
114
|
const versionParam = params?.["smoodly-version"];
|
|
111
|
-
const
|
|
115
|
+
const editor = draft && (marker?.editor === true || params?.["smoodly-editor"] === "1");
|
|
116
|
+
const version = !draft ? undefined
|
|
117
|
+
: marker?.version ?? (typeof versionParam === "string" && versionParam !== "" ? versionParam : undefined);
|
|
118
|
+
const flags = marker?.editor ? { editor: true } : marker?.version ? { version: marker.version } : null;
|
|
112
119
|
const target = await site.locate(locale, path);
|
|
113
120
|
// No document to tag: a redirect row, a miss or a code-owned page. Next caches the response like a
|
|
114
121
|
// page, so it hears the addresses tag (reads.ts missUnit, decision 4).
|
|
115
122
|
const tagAddress = () => nextReads.run(missUnit(site));
|
|
116
123
|
if (target?.redirect) {
|
|
117
124
|
await tagAddress();
|
|
118
|
-
await redirectTo(target, locale, params);
|
|
125
|
+
await redirectTo(target, locale, params, flags);
|
|
119
126
|
}
|
|
120
127
|
if (!target)
|
|
121
128
|
await tagAddress();
|
|
@@ -168,10 +175,14 @@ export function createPageRenderer(options) {
|
|
|
168
175
|
}
|
|
169
176
|
return {
|
|
170
177
|
getSmoodlyGlobal,
|
|
171
|
-
renderSmoodlyPath: (segments, opts) => page(
|
|
178
|
+
renderSmoodlyPath: (segments, opts) => page((draft) => {
|
|
179
|
+
// The catch-all's segments may end with the canvas marker; only a draft-mode render reads it.
|
|
180
|
+
const marker = draft ? readCanvasMarker(segments) : null;
|
|
181
|
+
return { pathFor: literal(marker ? marker.segments : segments), marker };
|
|
182
|
+
}, opts, any),
|
|
172
183
|
smoodlyMetadata: (segments, opts) => metadata(literal(segments), opts, any),
|
|
173
184
|
/** A fixed registration rendered from the developer's own route file. */
|
|
174
|
-
renderSmoodlyPage: (Page, opts) => page(fixedPath(Page), opts, only(Page)),
|
|
185
|
+
renderSmoodlyPage: (Page, opts) => page(() => ({ pathFor: fixedPath(Page), marker: null }), opts, only(Page)),
|
|
175
186
|
smoodlyPageMetadata: (Page, opts) => metadata(fixedPath(Page), opts, only(Page)),
|
|
176
187
|
};
|
|
177
188
|
}
|
package/dist/next/queries.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { CollectionSchema } from "../collections.ts";
|
|
2
2
|
import type { GlobalSchema } from "../global.ts";
|
|
3
3
|
import { type SmoodlyQueries } from "../reads.ts";
|
|
4
|
-
import type {
|
|
4
|
+
import type { ContentReads } from "../site.ts";
|
|
5
5
|
export type { SmoodlyQueries, QueryOptions, EntriesQuery, TypedEntryItem, GlobalValue } from "../reads.ts";
|
|
6
6
|
export declare function createSmoodlyQueries<const C extends readonly CollectionSchema<any, string>[], const S extends readonly GlobalSchema<any, string>[] = []>(options: {
|
|
7
|
-
site:
|
|
7
|
+
site: ContentReads;
|
|
8
8
|
collections: C;
|
|
9
9
|
globals?: S;
|
|
10
10
|
}): SmoodlyQueries<C, S>;
|
package/dist/path-index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { PathRow, PathTarget } from "./paths.ts";
|
|
2
|
-
import { type Anchors } from "./anchors.ts";
|
|
3
2
|
export type { PathRow, PathTarget } from "./paths.ts";
|
|
4
3
|
/** A rewrite target; `redirectLocales` names the locales whose old live rows stay as redirects (none = drop them). */
|
|
5
4
|
export type RewriteTarget = PathTarget & {
|
|
@@ -29,26 +28,3 @@ export interface PathIndex {
|
|
|
29
28
|
}
|
|
30
29
|
export declare const pathTakenError: (locale: string, path: string) => Error;
|
|
31
30
|
export declare const pathLiveError: (locale: string, path: string) => Error;
|
|
32
|
-
export declare class MemoryPathIndex implements PathIndex {
|
|
33
|
-
private anchors;
|
|
34
|
-
private rows;
|
|
35
|
-
/** Held by reference: the config's addresses, read on every call (spec 2026-09-29 anchored paths §2). */
|
|
36
|
-
constructor(anchors?: Anchors);
|
|
37
|
-
/** A stored row as callers see it: the full URL, or nothing when its anchor has no address. */
|
|
38
|
-
private shown;
|
|
39
|
-
private shownAll;
|
|
40
|
-
rewrite(targets: RewriteTarget[], rows: PathRow[]): Promise<void>;
|
|
41
|
-
remove(targets: PathTarget[]): Promise<void>;
|
|
42
|
-
private hit;
|
|
43
|
-
resolve(locale: string, path: string): Promise<PathHit | null>;
|
|
44
|
-
pathsOf(target: PathTarget): Promise<Record<string, string>>;
|
|
45
|
-
livePathsIn(kind: "page" | "entry", ids: string[], locale: string): Promise<Record<string, string>>;
|
|
46
|
-
redirectsOf(target: PathTarget): Promise<PathRow[]>;
|
|
47
|
-
/** Every live URL in the locale; an editor's row under a code address is left out, since visitors never reach it (spec §4). */
|
|
48
|
-
listLive(locale: string): Promise<PathRow[]>;
|
|
49
|
-
listRedirects(locale: string): Promise<PathRow[]>;
|
|
50
|
-
/** A redirect by hand stays absolute (anchor ''): refused where the URL resolves live; a redirect there is replaced. */
|
|
51
|
-
addRedirect(row: PathRow): Promise<void>;
|
|
52
|
-
/** Deletes the redirect rows a URL reaches, under any candidate anchor; a live row there is left alone. */
|
|
53
|
-
removeRedirect(locale: string, path: string): Promise<void>;
|
|
54
|
-
}
|
package/dist/path-index.js
CHANGED
|
@@ -1,100 +1,2 @@
|
|
|
1
|
-
import { candidates, fullPath, pickHit, sameUrlKeys } from "./anchors.js";
|
|
2
|
-
const sameTarget = (a, b) => a.kind === b.kind && a.id === b.id;
|
|
3
1
|
export const pathTakenError = (locale, path) => new Error(`smoodly: the path "${path}" (${locale}) is already taken.`);
|
|
4
2
|
export const pathLiveError = (locale, path) => new Error(`smoodly: "${path}" (${locale}) is a live address.`);
|
|
5
|
-
const key = (locale, anchor, path) => `${locale} ${anchor} ${path}`;
|
|
6
|
-
const stored = (r) => ({ kind: r.kind, id: r.id, locale: r.locale, anchor: r.anchor ?? "", path: r.path, redirect: false });
|
|
7
|
-
export class MemoryPathIndex {
|
|
8
|
-
/** Held by reference: the config's addresses, read on every call (spec 2026-09-29 anchored paths §2). */
|
|
9
|
-
constructor(anchors = { addresses: {}, homePageSlug: "home", defaultLocale: "en" }) {
|
|
10
|
-
this.anchors = anchors;
|
|
11
|
-
this.rows = [];
|
|
12
|
-
}
|
|
13
|
-
/** A stored row as callers see it: the full URL, or nothing when its anchor has no address. */
|
|
14
|
-
shown(r) {
|
|
15
|
-
const path = fullPath(this.anchors, r.anchor, r.path, r.locale);
|
|
16
|
-
return path === null ? null : { kind: r.kind, id: r.id, locale: r.locale, path };
|
|
17
|
-
}
|
|
18
|
-
shownAll(rows) {
|
|
19
|
-
return rows.map((r) => this.shown(r)).filter((r) => r !== null);
|
|
20
|
-
}
|
|
21
|
-
async rewrite(targets, rows) {
|
|
22
|
-
const incoming = rows.map(stored);
|
|
23
|
-
const ofTarget = (r) => targets.find((t) => sameTarget(t, r));
|
|
24
|
-
// Every key a new row's full URL answers to: live wins a redirect there under any anchor (a redirect by
|
|
25
|
-
// hand has none), and an old row at the same full URL is the same address, not an old one.
|
|
26
|
-
const newKeys = new Set(incoming.flatMap((r) => sameUrlKeys(this.anchors, r.locale, r.anchor, r.path).map((k) => key(r.locale, k.anchor, k.path))));
|
|
27
|
-
// 1. redirect rows at the new live addresses go, whoever they point at (live wins, rename-back reclaims)
|
|
28
|
-
let kept = this.rows.filter((r) => !(r.redirect && newKeys.has(key(r.locale, r.anchor, r.path))));
|
|
29
|
-
// 2. the targets' old live rows: flipped to redirects in the named locales unless the address stays live
|
|
30
|
-
kept = kept.map((r) => {
|
|
31
|
-
const t = !r.redirect ? ofTarget(r) : undefined;
|
|
32
|
-
return t && t.redirectLocales?.includes(r.locale) && !newKeys.has(key(r.locale, r.anchor, r.path)) ? { ...r, redirect: true } : r;
|
|
33
|
-
});
|
|
34
|
-
// 3. the targets' remaining live rows go — and so do their redirect rows in a locale the target has no new live row in
|
|
35
|
-
const liveLocales = new Set(incoming.map((r) => `${r.kind} ${r.id} ${r.locale}`));
|
|
36
|
-
kept = kept.filter((r) => !ofTarget(r) || (r.redirect && liveLocales.has(`${r.kind} ${r.id} ${r.locale}`)));
|
|
37
|
-
// 4. the new live rows: a collision now is with another target's live row under the same anchor
|
|
38
|
-
const taken = new Set(kept.map((r) => key(r.locale, r.anchor, r.path)));
|
|
39
|
-
for (const r of incoming) {
|
|
40
|
-
const k = key(r.locale, r.anchor, r.path);
|
|
41
|
-
if (taken.has(k))
|
|
42
|
-
throw pathTakenError(r.locale, fullPath(this.anchors, r.anchor, r.path, r.locale) ?? r.path);
|
|
43
|
-
taken.add(k);
|
|
44
|
-
}
|
|
45
|
-
this.rows = [...kept, ...incoming];
|
|
46
|
-
}
|
|
47
|
-
async remove(targets) {
|
|
48
|
-
this.rows = this.rows.filter((r) => !targets.some((t) => sameTarget(t, r)));
|
|
49
|
-
}
|
|
50
|
-
hit(locale, url) {
|
|
51
|
-
const { keys, coded } = candidates(this.anchors, locale, url);
|
|
52
|
-
return pickHit(this.rows.filter((r) => r.locale === locale), keys, coded);
|
|
53
|
-
}
|
|
54
|
-
async resolve(locale, path) {
|
|
55
|
-
const row = this.hit(locale, path);
|
|
56
|
-
return row ? { kind: row.kind, id: row.id, redirect: row.redirect } : null;
|
|
57
|
-
}
|
|
58
|
-
async pathsOf(target) {
|
|
59
|
-
const out = {};
|
|
60
|
-
for (const r of this.shownAll(this.rows.filter((r) => !r.redirect && sameTarget(r, target))))
|
|
61
|
-
out[r.locale] = r.path;
|
|
62
|
-
return out;
|
|
63
|
-
}
|
|
64
|
-
async livePathsIn(kind, ids, locale) {
|
|
65
|
-
const wanted = new Set(ids);
|
|
66
|
-
const out = {};
|
|
67
|
-
for (const r of this.shownAll(this.rows.filter((r) => !r.redirect && r.kind === kind && r.locale === locale && wanted.has(r.id))))
|
|
68
|
-
out[r.id] = r.path;
|
|
69
|
-
return out;
|
|
70
|
-
}
|
|
71
|
-
async redirectsOf(target) {
|
|
72
|
-
return this.shownAll(this.rows.filter((r) => r.redirect && sameTarget(r, target)));
|
|
73
|
-
}
|
|
74
|
-
/** Every live URL in the locale; an editor's row under a code address is left out, since visitors never reach it (spec §4). */
|
|
75
|
-
async listLive(locale) {
|
|
76
|
-
const served = (r) => !(r.anchor === "" && candidates(this.anchors, locale, r.path).coded);
|
|
77
|
-
return this.shownAll(this.rows.filter((r) => !r.redirect && r.locale === locale && served(r))).sort((a, b) => a.path.localeCompare(b.path));
|
|
78
|
-
}
|
|
79
|
-
async listRedirects(locale) {
|
|
80
|
-
return this.shownAll(this.rows.filter((r) => r.redirect && r.locale === locale)).sort((a, b) => a.path.localeCompare(b.path));
|
|
81
|
-
}
|
|
82
|
-
/** A redirect by hand stays absolute (anchor ''): refused where the URL resolves live; a redirect there is replaced. */
|
|
83
|
-
async addRedirect(row) {
|
|
84
|
-
const existing = this.hit(row.locale, row.path);
|
|
85
|
-
if (existing && !existing.redirect)
|
|
86
|
-
throw pathLiveError(row.locale, row.path);
|
|
87
|
-
// An editor's page hidden under a code address still holds its row: a redirect never stacks on it.
|
|
88
|
-
if (this.rows.some((r) => !r.redirect && r.locale === row.locale && r.anchor === "" && r.path === row.path))
|
|
89
|
-
throw pathLiveError(row.locale, row.path);
|
|
90
|
-
// Every redirect the URL reaches goes, under any anchor: the new one replaces them all.
|
|
91
|
-
const { keys } = candidates(this.anchors, row.locale, row.path);
|
|
92
|
-
const replaced = (r) => r.redirect && r.locale === row.locale && keys.some((k) => k.anchor === r.anchor && k.path === r.path);
|
|
93
|
-
this.rows = [...this.rows.filter((r) => !replaced(r)), { ...stored({ ...row, anchor: "" }), redirect: true }];
|
|
94
|
-
}
|
|
95
|
-
/** Deletes the redirect rows a URL reaches, under any candidate anchor; a live row there is left alone. */
|
|
96
|
-
async removeRedirect(locale, path) {
|
|
97
|
-
const { keys } = candidates(this.anchors, locale, path);
|
|
98
|
-
this.rows = this.rows.filter((r) => !(r.redirect && r.locale === locale && keys.some((k) => k.anchor === r.anchor && k.path === r.path)));
|
|
99
|
-
}
|
|
100
|
-
}
|
package/dist/paths.d.ts
CHANGED
|
@@ -102,7 +102,7 @@ export declare function collectionSegment(path: string | Record<string, string>
|
|
|
102
102
|
* it is a non-empty string — absent, null, or `""` all mean "no URL".
|
|
103
103
|
* There is no sibling-slug index: sibling uniqueness is the primary key
|
|
104
104
|
* of `paths` plus the pre-check each store makes per locale before it
|
|
105
|
-
* writes (spec 2026-09-06 §12), so
|
|
105
|
+
* writes (spec 2026-09-06 §12), so every store agrees on which
|
|
106
106
|
* entries can collide. */
|
|
107
107
|
export declare const slugOf: (fields: Record<string, unknown>) => string | null;
|
|
108
108
|
/** The entry's segment in one locale: its row's slug. An absent locale
|
package/dist/paths.js
CHANGED
|
@@ -162,7 +162,7 @@ export function collectionSegment(path, locale, set) {
|
|
|
162
162
|
* it is a non-empty string — absent, null, or `""` all mean "no URL".
|
|
163
163
|
* There is no sibling-slug index: sibling uniqueness is the primary key
|
|
164
164
|
* of `paths` plus the pre-check each store makes per locale before it
|
|
165
|
-
* writes (spec 2026-09-06 §12), so
|
|
165
|
+
* writes (spec 2026-09-06 §12), so every store agrees on which
|
|
166
166
|
* entries can collide. */
|
|
167
167
|
export const slugOf = (fields) => {
|
|
168
168
|
const slug = fields.slug;
|
package/dist/push.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "./config.ts";
|
|
2
|
+
import { type RegistryJson } from "./registry-json.ts";
|
|
3
|
+
/** The pushing site: where it lives, where its admin is mounted, how it delivers, and whether the host says this build
|
|
4
|
+
* is a preview deployment (`transient`: its admin works, but a publish never calls it). */
|
|
5
|
+
export type SiteInfo = {
|
|
6
|
+
url: string;
|
|
7
|
+
adminPath: string;
|
|
8
|
+
mode: "live" | "static";
|
|
9
|
+
deployHook?: string;
|
|
10
|
+
transient: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type PushBody = {
|
|
13
|
+
registry: RegistryJson;
|
|
14
|
+
site: SiteInfo | null;
|
|
15
|
+
package: string;
|
|
16
|
+
};
|
|
17
|
+
/** The site's address from SITE_URL; null when it is unset (the space then records no address). */
|
|
18
|
+
export declare function siteInfo(config: ResolvedConfig, env: Record<string, string | undefined>, mode: "live" | "static"): SiteInfo | null;
|
|
19
|
+
export declare function pushBody(config: ResolvedConfig, env: Record<string, string | undefined>, mode: "live" | "static"): PushBody;
|
|
20
|
+
export type PushApi = {
|
|
21
|
+
apiUrl: string;
|
|
22
|
+
spaceId: string;
|
|
23
|
+
key: string;
|
|
24
|
+
fetch?: typeof fetch;
|
|
25
|
+
};
|
|
26
|
+
/** One push: the space's version of the registry, and whether this push changed it. A refusal throws `SmoodlyApiError`. */
|
|
27
|
+
export declare function pushSchema(api: PushApi, body: PushBody): Promise<{
|
|
28
|
+
version: number;
|
|
29
|
+
changed: boolean;
|
|
30
|
+
}>;
|
package/dist/push.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// The push (spec 2026-10-16 the control plane §4): a site tells its space the registry it runs, its own address and
|
|
2
|
+
// delivery mode, and the package version, at `POST /v1/spaces/:space/schema` under the secret key. `smoodly push`, Astro's
|
|
3
|
+
// build hook, the Next templates' prebuild and the API client in development all send this one body. The space keeps
|
|
4
|
+
// the registry, versions it and purges its pages when it changed; a failed push never fails a build.
|
|
5
|
+
import { SmoodlyApiError } from "./api-client.js";
|
|
6
|
+
import { serializeRegistry } from "./registry-json.js";
|
|
7
|
+
import { SMOODLY_VERSION } from "./version.js";
|
|
8
|
+
/** A preview deployment, as the three hosts the spec names say it: Vercel, Netlify, Cloudflare Pages. */
|
|
9
|
+
function isPreview(env) {
|
|
10
|
+
if (env.VERCEL_ENV === "preview")
|
|
11
|
+
return true;
|
|
12
|
+
if (env.CONTEXT === "deploy-preview")
|
|
13
|
+
return true;
|
|
14
|
+
return !!env.CF_PAGES_BRANCH && !!env.CF_PAGES_PRODUCTION_BRANCH && env.CF_PAGES_BRANCH !== env.CF_PAGES_PRODUCTION_BRANCH;
|
|
15
|
+
}
|
|
16
|
+
/** The site's address from SITE_URL; null when it is unset (the space then records no address). */
|
|
17
|
+
export function siteInfo(config, env, mode) {
|
|
18
|
+
const url = env.SITE_URL?.replace(/\/+$/, "");
|
|
19
|
+
if (!url)
|
|
20
|
+
return null;
|
|
21
|
+
return {
|
|
22
|
+
url,
|
|
23
|
+
adminPath: config.admin.path,
|
|
24
|
+
mode,
|
|
25
|
+
...(env.SMOODLY_DEPLOY_HOOK ? { deployHook: env.SMOODLY_DEPLOY_HOOK } : {}),
|
|
26
|
+
transient: isPreview(env),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function pushBody(config, env, mode) {
|
|
30
|
+
return { registry: serializeRegistry(config), site: siteInfo(config, env, mode), package: SMOODLY_VERSION };
|
|
31
|
+
}
|
|
32
|
+
/** One push: the space's version of the registry, and whether this push changed it. A refusal throws `SmoodlyApiError`. */
|
|
33
|
+
export async function pushSchema(api, body) {
|
|
34
|
+
const apiUrl = api.apiUrl.replace(/\/+$/, "");
|
|
35
|
+
const doFetch = api.fetch ?? fetch;
|
|
36
|
+
let res;
|
|
37
|
+
try {
|
|
38
|
+
res = await doFetch(`${apiUrl}/v1/spaces/${encodeURIComponent(api.spaceId)}/schema`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { authorization: `Bearer ${api.key}`, "content-type": "application/json", accept: "application/json" },
|
|
41
|
+
body: JSON.stringify(body),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
throw new TypeError(`smoodly: can't reach SMOODLY_API_URL (${apiUrl}): ${e instanceof Error ? e.message : String(e)}`);
|
|
46
|
+
}
|
|
47
|
+
const answer = (await res.json().catch(() => null));
|
|
48
|
+
if (!answer || !answer.ok) {
|
|
49
|
+
throw new SmoodlyApiError(res.status, answer?.code ?? "internal", answer?.message ?? `smoodly: SMOODLY_API_URL (${apiUrl}) answered ${res.status} with no message.`);
|
|
50
|
+
}
|
|
51
|
+
return { version: answer.data.version, changed: answer.data.changed };
|
|
52
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Where a framework adapter hears each read (spec 2026-10-07 delivery model): the Astro request scope installs a counter, so a test can hold a live page to one lookup. A global, not an import: the client stays free of any adapter.
|
|
2
|
+
const READ_COUNTER = Symbol.for("smoodly:read-counter");
|
|
3
|
+
/** One read for the adapters' counters: a database call in the service's client, an API call in api-client.ts. */
|
|
4
|
+
export function countRead() {
|
|
5
|
+
globalThis[READ_COUNTER]?.();
|
|
6
|
+
}
|
package/dist/reads.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { GlobalSchema } from "./global.ts";
|
|
|
3
3
|
import type { PathTarget } from "./paths.ts";
|
|
4
4
|
import { type EntryItem, type PageItem } from "./read-shapes.ts";
|
|
5
5
|
import type { Materialize } from "./schema.ts";
|
|
6
|
-
import type { AddressWithGlobals, EntryQuery, PageTreeItem, SiteHit
|
|
6
|
+
import type { AddressWithGlobals, ContentReads, EntryQuery, PageTreeItem, SiteHit } from "./site.ts";
|
|
7
7
|
/** One content read, as the framework's cache needs it. */
|
|
8
8
|
export type ReadUnit<T> = {
|
|
9
9
|
/** What the published read holds: two calls with one key may share a cached `read(false)`. A draft read
|
|
@@ -56,17 +56,17 @@ export type SmoodlyQueries<C extends readonly CollectionSchema<any, string>[], S
|
|
|
56
56
|
* what the unit holds, not only where: an address that changes owner — a deploy handing it to a code
|
|
57
57
|
* record, which fires no tag — can never serve the previous occupant (spec 2026-09-29 pre-install fixes
|
|
58
58
|
* §5). `version` is History's stored version and counts inside draft mode only. */
|
|
59
|
-
export declare function hitUnit(site:
|
|
59
|
+
export declare function hitUnit(site: ContentReads, target: PathTarget, locale: string, path: string, version?: string): ReadUnit<SiteHit | null>;
|
|
60
60
|
/** An address with no document to tag — a miss, a redirect row or a code-owned page. The unit reads
|
|
61
61
|
* nothing: it exists so a cached response hears the `addresses` tag, which every address-changing
|
|
62
62
|
* operation fires (revalidate.ts). Next runs it (its route cache stores a 404 like a page); Astro never
|
|
63
63
|
* caches such a response and does not call it. One key for every address: the tag reaches the route
|
|
64
64
|
* entry before the lookup, so a per-address key would only write one data-cache row per scanned 404. */
|
|
65
|
-
export declare function missUnit(site:
|
|
65
|
+
export declare function missUnit(site: ContentReads): ReadUnit<null>;
|
|
66
66
|
/** Astro's route read of an address: one lookup outside draft mode — the tags come with the document, so no
|
|
67
67
|
* locate comes first; in draft mode, locate then load as the editing tables need (spec 2026-10-07 delivery
|
|
68
68
|
* model §3). A redirect row answers as a redirect; a code-owned page answers where no record lives. */
|
|
69
|
-
export declare function addressUnit(site:
|
|
69
|
+
export declare function addressUnit(site: ContentReads, locale: string, path: string, version?: string): ReadUnit<SiteHit | {
|
|
70
70
|
kind: "redirect";
|
|
71
71
|
target: PathTarget;
|
|
72
72
|
} | null>;
|
|
@@ -74,12 +74,12 @@ export declare function addressUnit(site: SmoodlySite, locale: string, path: str
|
|
|
74
74
|
* `lookupWithGlobals`, and the shell seeds each value into the request scope under `globalUnit`'s key, so the
|
|
75
75
|
* layout's loaders cost no call. The tags are the hit's; a global's tag joins when the layout reads it. A draft
|
|
76
76
|
* read brings no globals: a preview reads live. */
|
|
77
|
-
export declare function addressWithGlobalsUnit(site:
|
|
77
|
+
export declare function addressWithGlobalsUnit(site: ContentReads, locale: string, path: string, items: readonly GlobalSchema<any>[], version?: string): ReadUnit<AddressWithGlobals>;
|
|
78
78
|
/** A global item's unit, by schema: the one `q.getGlobal` runs for the same item and locale. */
|
|
79
|
-
export declare function globalUnit<V>(site:
|
|
79
|
+
export declare function globalUnit<V>(site: ContentReads, item: GlobalSchema<V>, locale: string): ReadUnit<V | null>;
|
|
80
80
|
/** The typed queries over a site, run by the framework's adapter. */
|
|
81
81
|
export declare function createQueries<const C extends readonly CollectionSchema<any, string>[], const S extends readonly GlobalSchema<any, string>[] = []>(options: {
|
|
82
|
-
site:
|
|
82
|
+
site: ContentReads;
|
|
83
83
|
collections: C;
|
|
84
84
|
globals?: S;
|
|
85
85
|
}, adapter: ReadAdapter): SmoodlyQueries<C, S>;
|
package/dist/reads.js
CHANGED
|
@@ -51,6 +51,9 @@ export function addressUnit(site, locale, path, version) {
|
|
|
51
51
|
function tagsOfHit(value) {
|
|
52
52
|
if (!value || value.kind === "redirect")
|
|
53
53
|
return [];
|
|
54
|
+
// A published document carries its delivery row's tags (its own and every ref it embeds).
|
|
55
|
+
if (value.tags)
|
|
56
|
+
return value.tags;
|
|
54
57
|
if (value.kind === "entry")
|
|
55
58
|
return [entryTag(value.entry.id)];
|
|
56
59
|
// A code-owned page has no row to expire it by.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ResolvedConfig } from "./config.ts";
|
|
2
|
+
export type RegistryJson = {
|
|
3
|
+
v: 1;
|
|
4
|
+
config: Record<string, unknown>;
|
|
5
|
+
};
|
|
6
|
+
/** The pushed form of a resolved config: its defineConfig input, as data. */
|
|
7
|
+
export declare function serializeRegistry(config: ResolvedConfig): RegistryJson;
|
|
8
|
+
/** A resolved config from its pushed form: the ref thunks resolve by name within the same registry. */
|
|
9
|
+
export declare function hydrateRegistry(json: RegistryJson): ResolvedConfig;
|
|
10
|
+
/** The registry's fingerprint: SHA-256 hex of its key-sorted JSON. */
|
|
11
|
+
export declare function registryHash(json: RegistryJson): Promise<string>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// The registry as JSON (spec 2026-10-16 the control plane §4): what `smoodly push` carries to the space. It is the
|
|
2
|
+
// defineConfig input, rebuilt from the resolved config, with the only functions a config holds replaced: a ref's
|
|
3
|
+
// target thunk becomes the name of the collection it returns, and any other function (a view the config does not
|
|
4
|
+
// need) is dropped. Hydrating is defineConfig over the revived input, so the derived parts (addresses, anchors, the
|
|
5
|
+
// styles descriptors) come from the one function that computes them.
|
|
6
|
+
import { defineConfig } from "./config.js";
|
|
7
|
+
const COLLECTION = "$collection";
|
|
8
|
+
const isPlain = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
/** Plain data out of a config value: a `target` thunk becomes `{ $collection: name }`, any other function goes. */
|
|
10
|
+
function toData(value, key) {
|
|
11
|
+
if (typeof value === "function") {
|
|
12
|
+
return key === "target" ? { [COLLECTION]: value().name } : undefined;
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(value))
|
|
15
|
+
return value.map((item) => toData(item));
|
|
16
|
+
if (isPlain(value)) {
|
|
17
|
+
const out = {};
|
|
18
|
+
for (const [k, v] of Object.entries(value)) {
|
|
19
|
+
const data = toData(v, k);
|
|
20
|
+
if (data !== undefined)
|
|
21
|
+
out[k] = data;
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
/** The pushed form of a resolved config: its defineConfig input, as data. */
|
|
28
|
+
export function serializeRegistry(config) {
|
|
29
|
+
const { sections, elements, collections, globals, pages } = config.registry;
|
|
30
|
+
const input = {
|
|
31
|
+
registry: { sections, elements, collections, globals, pages },
|
|
32
|
+
locales: config.locales,
|
|
33
|
+
...(config.timeZone !== undefined ? { timeZone: config.timeZone } : {}),
|
|
34
|
+
pages: { tree: { depth: config.pageDepth } },
|
|
35
|
+
styles: config.styles,
|
|
36
|
+
css: config.css,
|
|
37
|
+
custom: { breakpoint: config.custom.breakpoint, toolset: config.custom.toolset },
|
|
38
|
+
homePageSlug: config.homePageSlug,
|
|
39
|
+
...(config.database !== undefined ? { database: config.database } : {}),
|
|
40
|
+
media: config.media,
|
|
41
|
+
admin: { path: config.admin.path },
|
|
42
|
+
routing: config.routing,
|
|
43
|
+
};
|
|
44
|
+
return { v: 1, config: toData(input) };
|
|
45
|
+
}
|
|
46
|
+
/** `{ $collection: name }` back into a thunk over the hydrated registry's own collections and globals. */
|
|
47
|
+
function revive(value, byName) {
|
|
48
|
+
if (Array.isArray(value))
|
|
49
|
+
return value.map((item) => revive(item, byName));
|
|
50
|
+
if (!isPlain(value))
|
|
51
|
+
return value;
|
|
52
|
+
const keys = Object.keys(value);
|
|
53
|
+
if (keys.length === 1 && keys[0] === COLLECTION && typeof value[COLLECTION] === "string") {
|
|
54
|
+
const name = value[COLLECTION];
|
|
55
|
+
return () => {
|
|
56
|
+
const target = byName.get(name);
|
|
57
|
+
if (!target)
|
|
58
|
+
throw new Error(`smoodly: the pushed registry has a ref to "${name}", which it does not register.`);
|
|
59
|
+
return target;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, revive(v, byName)]));
|
|
63
|
+
}
|
|
64
|
+
/** A resolved config from its pushed form: the ref thunks resolve by name within the same registry. */
|
|
65
|
+
export function hydrateRegistry(json) {
|
|
66
|
+
const byName = new Map();
|
|
67
|
+
const input = revive(json.config, byName);
|
|
68
|
+
// The thunks run only after defineConfig, which keeps the collections and globals it was given.
|
|
69
|
+
for (const c of [...(input.registry.collections ?? []), ...(input.registry.globals ?? [])])
|
|
70
|
+
byName.set(c.name, c);
|
|
71
|
+
return defineConfig(input);
|
|
72
|
+
}
|
|
73
|
+
/** JSON with every object's keys sorted, so two equal registries stringify alike whatever their key order. */
|
|
74
|
+
function sortedJson(value) {
|
|
75
|
+
return JSON.stringify(value, (_key, v) => isPlain(v) ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, v[k]])) : v);
|
|
76
|
+
}
|
|
77
|
+
/** The registry's fingerprint: SHA-256 hex of its key-sorted JSON. */
|
|
78
|
+
export async function registryHash(json) {
|
|
79
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(sortedJson(json)));
|
|
80
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
81
|
+
}
|
package/dist/render.d.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { type StyleValues } from "./styles.ts";
|
|
|
5
5
|
import { type CustomOptions } from "./custom/CustomSection.tsx";
|
|
6
6
|
import { type EditorWrap, type PageContext, type PageTree, type ZoneContent } from "./page-context.ts";
|
|
7
7
|
export type { TreeNode, PageTree, PageLink, PageContextLink, PageContextAlternate, PageContext, ZoneContent, EditorWrap } from "./page-context.ts";
|
|
8
|
-
export { EMPTY_PAGE_CONTEXT } from "./page-context.ts";
|
|
9
8
|
type AnyPaired = ComponentType<any> & {
|
|
10
9
|
schema: {
|
|
11
10
|
name: string;
|
package/dist/render.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createCustomSection } from "./custom/CustomSection.js";
|
|
|
8
8
|
import { EMPTY_PAGE_CONTEXT } from "./page-context.js";
|
|
9
9
|
import { renderGuarded } from "./section-guard.js";
|
|
10
10
|
import { PLACEHOLDER_STYLE } from "./section-crash.js";
|
|
11
|
-
|
|
11
|
+
;
|
|
12
12
|
export function Zone({ of: content }) {
|
|
13
13
|
// A malformed tree (bad data, a client that skipped isPageTree) must not
|
|
14
14
|
// crash the render — treat a non-array nodes as empty rather than throw.
|
package/dist/resolve.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ import type { Descriptor } from "./fields.ts";
|
|
|
2
2
|
import type { CollectionSchema } from "./collections.ts";
|
|
3
3
|
import type { PageTree } from "./page-context.ts";
|
|
4
4
|
import { type GlobalSchema } from "./global.ts";
|
|
5
|
-
import { type AssetRecord } from "./media.ts";
|
|
5
|
+
import { type AssetRecord, type ImageDefaults } from "./media.ts";
|
|
6
|
+
import type { CutSigner } from "./media-cut.ts";
|
|
6
7
|
import { type ElementSchemas } from "./richtext-walk.ts";
|
|
7
8
|
import type { SectionLike } from "./refs.ts";
|
|
8
9
|
import type { Toolset } from "./toolset.ts";
|
|
@@ -31,6 +32,11 @@ type ResolveOptions = {
|
|
|
31
32
|
paths?: PathLookup;
|
|
32
33
|
/** Media (spec 2026-09-08 §9): one batched asset fetch per page, values become resolved objects. */
|
|
33
34
|
assets?: AssetFetcher;
|
|
35
|
+
/** Signs each image's cut where the service cuts (spec 2026-10-18 media on R2 and Images §5); absent, no cut. */
|
|
36
|
+
signer?: CutSigner;
|
|
37
|
+
/** The project's image quality and srcset cap, stamped on each image under its field's own (spec 2026-10-18 image
|
|
38
|
+
* quality and max width §6). */
|
|
39
|
+
images?: ImageDefaults;
|
|
34
40
|
/** The registered global items a placement node may name (spec 2026-09-07 §4). */
|
|
35
41
|
globals?: GlobalSchema[];
|
|
36
42
|
/** Registered Elements a richtext document may embed (spec 2026-09-08 block nodes §2). */
|
|
@@ -48,17 +54,17 @@ type ResolveOptions = {
|
|
|
48
54
|
};
|
|
49
55
|
export declare function resolveTree(tree: PageTree, options: ResolveOptions): Promise<PageTree>;
|
|
50
56
|
/** One value set's media, resolved in place. Exported for the site layer's global-item settings. */
|
|
51
|
-
export declare function resolveMedia(values: Record<string, unknown>, descriptors: Record<string, Descriptor>, fetch: AssetFetcher, elements?: ElementSchemas): Promise<void>;
|
|
52
|
-
/** Many value sets, one asset round (plus one for posters): the tree's
|
|
53
|
-
* nodes and every entry hydrated through a ref, or a list page's items.
|
|
54
|
-
* Exported for the site layer's content API. */
|
|
55
|
-
export declare function resolveMediaSets(sets: MediaSet[], fetch: AssetFetcher, elements?: ElementSchemas): Promise<void>;
|
|
57
|
+
export declare function resolveMedia(values: Record<string, unknown>, descriptors: Record<string, Descriptor>, fetch: AssetFetcher, elements?: ElementSchemas, signer?: CutSigner, defaults?: ImageDefaults): Promise<void>;
|
|
56
58
|
export type ResolveFieldsOptions = {
|
|
57
59
|
collections: CollectionSchema<any>[];
|
|
58
60
|
fetch: EntryFetcher;
|
|
59
61
|
/** Each hydrated ref's address (spec 2026-09-29 content API contract §2). */
|
|
60
62
|
paths?: PathLookup;
|
|
61
63
|
assets?: AssetFetcher;
|
|
64
|
+
/** Signs each image's cut (spec 2026-10-18 media on R2 and Images §5); absent, no cut. */
|
|
65
|
+
signer?: CutSigner;
|
|
66
|
+
/** The project's image quality and srcset cap (spec 2026-10-18 image quality and max width §6). */
|
|
67
|
+
images?: ImageDefaults;
|
|
62
68
|
/** Registered Elements a richtext document may embed (spec 2026-09-08 block nodes §2). */
|
|
63
69
|
elements?: SectionLike[];
|
|
64
70
|
maxDepth?: number;
|