smoodly 0.0.50 → 0.0.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -13
- package/dist/admin/auth-config.d.ts +6 -5
- package/dist/admin/auth.d.ts +3 -11
- package/dist/admin/auth.js +1 -30
- package/dist/admin/browser/fetch-op.js +3 -2
- package/dist/admin/browser/main.js +6 -2
- package/dist/admin/client-ops.d.ts +5 -4
- package/dist/admin/client-ops.js +14 -11
- 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 +5 -8
- package/dist/admin/index.js +3 -6
- package/dist/admin/ops.d.ts +36 -22
- package/dist/admin/page-data.d.ts +14 -6
- package/dist/admin/page-data.js +1 -1
- package/dist/admin/session-flow.d.ts +43 -0
- package/dist/admin/session-flow.js +94 -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 +5 -4
- 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 +20 -12
- package/dist/admin/shell/session.js +113 -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 +5 -8
- 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 +4 -302
- package/dist/asset-store.d.ts +28 -30
- package/dist/asset-store.js +26 -121
- 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 +54 -25
- 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 +2 -1
- package/dist/astro/runtime.js +4 -3
- package/dist/astro/server.d.ts +7 -11
- package/dist/astro/server.js +22 -28
- package/dist/astro/shell.d.ts +2 -2
- 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 +22 -23
- package/dist/content/content-api.js +46 -56
- package/dist/content/mime.d.ts +1 -0
- package/dist/content/mime.js +6 -0
- package/dist/deploy-store.d.ts +19 -0
- package/dist/deploy-store.js +1 -0
- package/dist/entry-store.d.ts +3 -89
- package/dist/entry-store.js +2 -550
- 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 +12 -22
- 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 +57 -3
- package/dist/media.js +125 -15
- package/dist/member-store.d.ts +3 -22
- package/dist/member-store.js +1 -53
- 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 +28 -15
- 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/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 +9 -8
- package/dist/reads.js +7 -3
- 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 +5 -2
- package/dist/space-transfer.js +17 -5
- 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/ops-impl.d.ts +0 -51
- package/dist/admin/ops-impl.js +0 -1400
- 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/assemble.js
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
// The assembler (spec 2026-10-07 delivery model §3): the loaded page, entry, global, tree and entry list
|
|
2
2
|
// as the site layer reads them, split out of site.ts so the live reads and the projector that fills the
|
|
3
3
|
// delivery tables share one implementation. Framework-free and React-free, like the site layer.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
4
|
+
// The assembler itself is the service's (packages/service/src/site/assemble.ts, plan 2026-10-14 the service holds
|
|
5
|
+
// the stores); this file keeps the shape and the query checks the package's statics share with it.
|
|
6
|
+
import { ARRAY_FIELD_TYPES } from "./entry-store.js";
|
|
7
7
|
import { elementSchemas } from "./richtext-walk.js";
|
|
8
|
-
import { globalNodeId, SETTINGS_KEY, STYLES_KEY, CSS_KEY } from "./global.js";
|
|
9
|
-
import { SMD_KEY } from "./smd-key.js";
|
|
10
8
|
import { resolveStyleValues } from "./styles.js";
|
|
11
9
|
import { resolveCustom } from "./custom/index.js";
|
|
12
10
|
import { addressMap, codeAddresses } from "./addresses.js";
|
|
13
|
-
import {
|
|
14
|
-
import { withDefaults } from "./field-defaults.js";
|
|
15
|
-
import { toEntryItem } from "./read-shapes.js";
|
|
16
|
-
import { entrySeo, pageSeo, SEO_KEY } from "./seo.js";
|
|
11
|
+
import { SEO_KEY } from "./seo.js";
|
|
17
12
|
import { schemaOf as registeredSchema } from "./registration.js";
|
|
18
13
|
/** Field types whose stored value is an object: not filterable (memory and SQL would disagree). */
|
|
19
14
|
const OBJECT_FIELD_TYPES = new Set(["image", "video", "file", "richtext", "object"]);
|
|
@@ -64,296 +59,3 @@ export function checkEntryQuery(schema, collection, o) {
|
|
|
64
59
|
throw new Error(`smoodly: getEntries("${collection}"): limit must be a non-negative integer.`);
|
|
65
60
|
}
|
|
66
61
|
}
|
|
67
|
-
/** The reads a batch answers once per argument set (a projection asks the same ones for every page). */
|
|
68
|
-
const MEMOIZED = { pages: ["listPages", "fieldsOf"], entries: ["getMany", "pathsIn"], assets: ["getMany"] };
|
|
69
|
-
export function createAssembler(options, shape) {
|
|
70
|
-
const { locales, pageSchemas, styles, custom, elements, pathOpts, anchors } = shape;
|
|
71
|
-
const mounted = options.collections.filter((c) => c.path !== undefined);
|
|
72
|
-
/** The resolver's fetcher, bound to a locale: the merged fields of that
|
|
73
|
-
* locale — published ones (a versioned collection's snapshot) on the
|
|
74
|
-
* public path, the live rows in draft mode. */
|
|
75
|
-
const fetcher = (draft, locale) => (collection, ids) => options.stores().entries.getMany(collection, ids, locale, draft ? undefined : { status: "published" });
|
|
76
|
-
/** The resolver's path lookup, bound to a locale: each EntryRef's address (spec 2026-09-29 content API contract §2). */
|
|
77
|
-
const pathLookup = (locale) => (collection, ids) => options.stores().entries.pathsIn(collection, ids, locale);
|
|
78
|
-
/** The media fetcher (spec 2026-09-08 §9) — absent when the site runs without an asset store. */
|
|
79
|
-
const assetFetcher = () => {
|
|
80
|
-
const assets = options.stores().assets;
|
|
81
|
-
return assets ? (ids) => assets.getMany(ids) : undefined;
|
|
82
|
-
};
|
|
83
|
-
const sectionSettings = (name) => {
|
|
84
|
-
const s = options.sections.find((x) => ("schema" in x ? x.schema : x).name === name);
|
|
85
|
-
return (s && ("schema" in s ? s.schema : s).settings) ?? {};
|
|
86
|
-
};
|
|
87
|
-
/** Visible to the public in a locale: present there, and a folder or a
|
|
88
|
-
* published page. Draft mode sees every present node. Presence is the
|
|
89
|
-
* locale row — nothing falls back (spec 2026-09-06 §3). */
|
|
90
|
-
const visible = (node, locale, draft) => {
|
|
91
|
-
const row = node.locales[locale];
|
|
92
|
-
return row !== undefined && (draft || node.template === null || row.status === "published");
|
|
93
|
-
};
|
|
94
|
-
const hasPage = (node, locale, draft) => {
|
|
95
|
-
const row = node.locales[locale];
|
|
96
|
-
return node.template !== null && row !== undefined && (draft || row.status === "published");
|
|
97
|
-
};
|
|
98
|
-
const pageFields = (template) => pageFieldsOf(shape, template);
|
|
99
|
-
/** The version whose fields a link shows: the draft's in draft mode, else the published one. */
|
|
100
|
-
const versionFor = (node, locale, draft) => {
|
|
101
|
-
const row = node.locales[locale];
|
|
102
|
-
if (!row || node.template === null)
|
|
103
|
-
return null;
|
|
104
|
-
return draft ? row.draftVersionId : row.publishedVersionId;
|
|
105
|
-
};
|
|
106
|
-
/** Raw page fields (no asset resolution) of many nodes in ONE store read
|
|
107
|
-
* (spec 2026-09-10 page fields §3), defaults applied; `{}` for a folder. */
|
|
108
|
-
async function linkFields(nodes, locale, draft) {
|
|
109
|
-
const { pages } = options.stores();
|
|
110
|
-
const wanted = new Map(); // node id → version id
|
|
111
|
-
for (const n of nodes) {
|
|
112
|
-
const v = versionFor(n, locale, draft);
|
|
113
|
-
if (v)
|
|
114
|
-
wanted.set(n.id, v);
|
|
115
|
-
}
|
|
116
|
-
const byVersion = wanted.size > 0 ? await pages.fieldsOf([...new Set(wanted.values())]) : {};
|
|
117
|
-
const out = new Map();
|
|
118
|
-
for (const n of nodes) {
|
|
119
|
-
const v = wanted.get(n.id);
|
|
120
|
-
out.set(n.id, n.template === null ? {} : withDefaults(v ? byVersion[v] : undefined, pageFields(n.template)));
|
|
121
|
-
}
|
|
122
|
-
return out;
|
|
123
|
-
}
|
|
124
|
-
/** Only ever called for a node PRESENT in the locale (visible() or an ancestor of one). */
|
|
125
|
-
const link = (node, byId, locale, draft, fields) => ({
|
|
126
|
-
title: node.locales[locale].title,
|
|
127
|
-
path: pagePath(node, byId, locale, pathOpts),
|
|
128
|
-
hasPage: hasPage(node, locale, draft),
|
|
129
|
-
fields: fields.get(node.id) ?? {},
|
|
130
|
-
});
|
|
131
|
-
async function loadPage(id, locale, path, draft, version) {
|
|
132
|
-
const { pages } = options.stores();
|
|
133
|
-
const record = await pages.getPage(id);
|
|
134
|
-
if (!record || record.template === null)
|
|
135
|
-
return null;
|
|
136
|
-
const row = record.locales[locale];
|
|
137
|
-
if (!row)
|
|
138
|
-
return null;
|
|
139
|
-
let raw;
|
|
140
|
-
if (version !== undefined) {
|
|
141
|
-
// History: a stored version of THIS page in THIS locale, and only inside draft mode.
|
|
142
|
-
const stored = draft ? await pages.getVersion(version) : null;
|
|
143
|
-
raw = stored && stored.pageId === id && stored.locale === locale ? stored.tree : null;
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
raw = draft ? await pages.getDraftTree(id, locale) : row.status === "published" ? await pages.getPublishedTree(id, locale) : null;
|
|
147
|
-
}
|
|
148
|
-
if (!raw)
|
|
149
|
-
return null;
|
|
150
|
-
const tree = await resolveTree(raw, {
|
|
151
|
-
sections: options.sections,
|
|
152
|
-
collections: options.collections,
|
|
153
|
-
globals: options.globals,
|
|
154
|
-
elements: options.elements,
|
|
155
|
-
fetch: fetcher(draft, locale), paths: pathLookup(locale),
|
|
156
|
-
assets: assetFetcher(),
|
|
157
|
-
page: { fields: pageFields(record.template) },
|
|
158
|
-
custom,
|
|
159
|
-
});
|
|
160
|
-
const all = await pages.listPages();
|
|
161
|
-
const index = new Map(all.map((n) => [n.id, n]));
|
|
162
|
-
const byId = (x) => index.get(x);
|
|
163
|
-
const chain = chainOf(record, byId);
|
|
164
|
-
const kids = all.filter((n) => n.parentId === id && visible(n, locale, draft));
|
|
165
|
-
const fields = await linkFields([...chain.slice(0, -1), ...kids], locale, draft);
|
|
166
|
-
const ancestors = chain.slice(0, -1).map((n) => link(n, byId, locale, draft, fields));
|
|
167
|
-
const children = kids.map((n) => link(n, byId, locale, draft, fields));
|
|
168
|
-
// Alternates advertise PUBLISHED locales only; the editor (draft) sees every present
|
|
169
|
-
// one. Derived from the tree already listed — no index lookup (spec 2026-09-15 §2).
|
|
170
|
-
const alternates = pageAlternates(record, byId, pathOpts, (l) => draft || record.locales[l]?.status === "published");
|
|
171
|
-
const withSeo = pageSchemas.some((x) => x.kind === "page" && x.name === record.template && x.seo === true);
|
|
172
|
-
return {
|
|
173
|
-
kind: "page",
|
|
174
|
-
locale,
|
|
175
|
-
record,
|
|
176
|
-
template: record.template,
|
|
177
|
-
tree,
|
|
178
|
-
title: row.title,
|
|
179
|
-
path,
|
|
180
|
-
fields: withDefaults(tree.fields, pageFields(record.template)),
|
|
181
|
-
ancestors,
|
|
182
|
-
children,
|
|
183
|
-
locales: alternates,
|
|
184
|
-
...(withSeo ? { seo: pageSeo(tree.meta, row.title) } : {}),
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
async function loadEntry(id, locale, path, draft) {
|
|
188
|
-
const { entries } = options.stores();
|
|
189
|
-
for (const schema of mounted) {
|
|
190
|
-
const record = await entries.get(schema.name, id);
|
|
191
|
-
if (!record)
|
|
192
|
-
continue;
|
|
193
|
-
// No row, or a draft row outside draft mode, is a 404 for THIS locale.
|
|
194
|
-
const row = record.locales[locale];
|
|
195
|
-
if (!row)
|
|
196
|
-
return null;
|
|
197
|
-
if (!draft && row.status !== "published")
|
|
198
|
-
return null;
|
|
199
|
-
const raw = draft
|
|
200
|
-
? entryFieldsIn(record, locale)
|
|
201
|
-
: (await entries.getMany(schema.name, [id], locale, { status: "published" }))[id];
|
|
202
|
-
if (!raw)
|
|
203
|
-
return null;
|
|
204
|
-
const fields = await resolveFields(raw, schema.fields, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), paths: pathLookup(locale), assets: assetFetcher() });
|
|
205
|
-
// Alternates advertise PUBLISHED locales only; the editor (draft) sees every present one.
|
|
206
|
-
// One record is in memory (a nested entry's parents are not), so the `paths` index
|
|
207
|
-
// supplies the paths — the same single lookup as before (spec 2026-09-15 §2).
|
|
208
|
-
const known = await entries.pathsOf(schema.name, id);
|
|
209
|
-
const alternates = entryAlternates(record, locales, (l) => draft || record.locales[l]?.status === "published", (l) => known[l] ?? null);
|
|
210
|
-
return {
|
|
211
|
-
kind: "entry", locale, collection: schema.name, entry: { ...record, fields }, path, locales: alternates,
|
|
212
|
-
...(schema.seo ? { seo: entrySeo(fields, schema) } : {}),
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
const localeOf = (o) => o?.locale ?? locales.default;
|
|
218
|
-
async function readEntries(collection, o = {}) {
|
|
219
|
-
const schema = schemaOf(options.collections, collection);
|
|
220
|
-
const locale = localeOf(o);
|
|
221
|
-
const draft = o.draft ?? false;
|
|
222
|
-
const { entries } = options.stores();
|
|
223
|
-
checkEntryQuery(schema, collection, o);
|
|
224
|
-
// The store filters, orders and limits over the snapshot shown (spec 2026-09-29 content API contract §5): the
|
|
225
|
-
// locale gate — presence in draft mode, published otherwise — is part of the query, so `limit` counts rows that are really there.
|
|
226
|
-
const rows = await entries.query(collection, {
|
|
227
|
-
locale, draft, where: o.where, order: o.order, limit: o.limit, ids: o.ids,
|
|
228
|
-
...(o.parent !== undefined ? { parent: o.parent } : {}),
|
|
229
|
-
});
|
|
230
|
-
// Paths need every ancestor: a tree collection reads the whole collection for that, a flat one does not.
|
|
231
|
-
const all = schema.tree ? await entries.list(collection) : rows.map((r) => r.record);
|
|
232
|
-
// References and media resolve for the whole list in one pass (§2, §6).
|
|
233
|
-
const sets = rows.map((r) => ({ values: structuredClone(r.fields), descriptors: schema.fields }));
|
|
234
|
-
await resolveFieldSets(sets, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), paths: pathLookup(locale), assets: assetFetcher() });
|
|
235
|
-
const index = new Map(all.map((e) => [e.id, e]));
|
|
236
|
-
const byId = (x) => index.get(x);
|
|
237
|
-
return rows.map(({ record }, i) => {
|
|
238
|
-
const paths = entryPathRows([record], byId, schema.name);
|
|
239
|
-
const pathIn = (l) => {
|
|
240
|
-
const row = paths.find((r) => r.locale === l);
|
|
241
|
-
return row ? fullPath(anchors, row.anchor ?? "", row.path, l) : null;
|
|
242
|
-
};
|
|
243
|
-
const item = toEntryItem(record, sets[i].values, {
|
|
244
|
-
path: pathIn(locale),
|
|
245
|
-
locale,
|
|
246
|
-
locales: entryAlternates(record, locales, (l) => draft || record.locales[l]?.status === "published", pathIn),
|
|
247
|
-
...(schema.seo ? { seo: entrySeo(sets[i].values, schema) } : {}),
|
|
248
|
-
});
|
|
249
|
-
return { record, item };
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
/** A node the public tree leaves out, with its subtree: a record no longer in code (no path, spec
|
|
253
|
-
* 2026-09-29 nested paths §5), and a page where another code address wins, which visitors never
|
|
254
|
-
* reach (anchored paths §4). */
|
|
255
|
-
const hiddenAt = (path, record, byId, locale) => path === null || servedAnchor(anchors, locale, path) !== (chainOf(record, byId)[0].fixed ?? "");
|
|
256
|
-
async function tree(locale, { draft }) {
|
|
257
|
-
const all = await options.stores().pages.listPages();
|
|
258
|
-
const shown = all.filter((n) => visible(n, locale, draft));
|
|
259
|
-
const index = new Map(shown.map((n) => [n.id, n]));
|
|
260
|
-
const fields = await linkFields(shown, locale, draft);
|
|
261
|
-
// Every node the walk reaches is present with a present chain.
|
|
262
|
-
const hidden = (node) => hiddenAt(node.path, node.record, (x) => index.get(x), locale);
|
|
263
|
-
const toItems = (nodes) => nodes.flatMap((node) => hidden(node) ? [] : [{
|
|
264
|
-
id: node.record.id,
|
|
265
|
-
title: node.record.locales[locale].title,
|
|
266
|
-
path: node.path,
|
|
267
|
-
hasPage: hasPage(node.record, locale, draft),
|
|
268
|
-
fields: fields.get(node.record.id) ?? {},
|
|
269
|
-
children: toItems(node.children),
|
|
270
|
-
}]);
|
|
271
|
-
return toItems(buildPageTree(shown, locale, pathOpts));
|
|
272
|
-
}
|
|
273
|
-
async function treeRow(record, all, locale) {
|
|
274
|
-
const index = new Map(all.map((n) => [n.id, n]));
|
|
275
|
-
const byId = (x) => index.get(x);
|
|
276
|
-
const path = pagePath(record, byId, locale, pathOpts);
|
|
277
|
-
const fields = await linkFields([record], locale, false);
|
|
278
|
-
return {
|
|
279
|
-
id: record.id,
|
|
280
|
-
locale,
|
|
281
|
-
parentId: record.parentId,
|
|
282
|
-
sort: record.sort,
|
|
283
|
-
path: hiddenAt(path, record, byId, locale) ? null : path,
|
|
284
|
-
title: record.locales[locale].title,
|
|
285
|
-
hasPage: hasPage(record, locale, false),
|
|
286
|
-
linkFields: fields.get(record.id) ?? {},
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
async function global(item, locale, { draft }) {
|
|
290
|
-
const { entries } = options.stores();
|
|
291
|
-
const [record] = await entries.list(item.name);
|
|
292
|
-
if (!record)
|
|
293
|
-
return null;
|
|
294
|
-
const row = record.locales[locale];
|
|
295
|
-
if (!row || (!draft && row.status !== "published"))
|
|
296
|
-
return null;
|
|
297
|
-
const raw = draft
|
|
298
|
-
? entryFieldsIn(record, locale)
|
|
299
|
-
: (await entries.getMany(item.name, [record.id], locale, { status: "published" }))[record.id];
|
|
300
|
-
if (!raw)
|
|
301
|
-
return null;
|
|
302
|
-
const fields = await resolveFields(raw, item.fields, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), paths: pathLookup(locale), assets: assetFetcher() });
|
|
303
|
-
if (!item.section)
|
|
304
|
-
return { locale, fields };
|
|
305
|
-
const { [SETTINGS_KEY]: settings, [STYLES_KEY]: steps, [CSS_KEY]: css, ...rest } = fields;
|
|
306
|
-
const fetch = assetFetcher();
|
|
307
|
-
const own = settings !== null && typeof settings === "object" ? settings : {};
|
|
308
|
-
if (fetch)
|
|
309
|
-
await resolveMedia(own, sectionSettings(item.section), fetch, elements);
|
|
310
|
-
// The layout node id, the css and the values ride the styles under the reserved key (spec 2026-09-13 §1, 2026-09-14 §2, §4):
|
|
311
|
-
// the wrapper strips it and emits data-smd-node, so the canvas selects the item and the css reaches it.
|
|
312
|
-
const stepsObject = steps !== null && typeof steps === "object" ? steps : {};
|
|
313
|
-
return {
|
|
314
|
-
locale,
|
|
315
|
-
fields: rest,
|
|
316
|
-
settings: own,
|
|
317
|
-
styles: { ...stepsObject, [SMD_KEY]: { node: globalNodeId(item.name), css: typeof css === "string" ? css : undefined, values: styles } },
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
/** A copy whose reads in MEMOIZED answer once per argument set — the promise is shared, so concurrent
|
|
321
|
-
* identical calls make one store call. For the lifetime of the copy only; the base assembler never caches. */
|
|
322
|
-
function batch(primed) {
|
|
323
|
-
const memo = new Map();
|
|
324
|
-
if (primed?.pages)
|
|
325
|
-
memo.set(`pages.listPages:${JSON.stringify([])}`, Promise.resolve(primed.pages));
|
|
326
|
-
const wrap = (store, group) => new Proxy(store, {
|
|
327
|
-
get(target, prop) {
|
|
328
|
-
const value = Reflect.get(target, prop, target);
|
|
329
|
-
if (typeof value !== "function")
|
|
330
|
-
return value;
|
|
331
|
-
if (!MEMOIZED[group].includes(prop))
|
|
332
|
-
return value.bind(target);
|
|
333
|
-
return (...args) => {
|
|
334
|
-
const key = `${group}.${String(prop)}:${JSON.stringify(args)}`;
|
|
335
|
-
let hit = memo.get(key);
|
|
336
|
-
if (!hit)
|
|
337
|
-
memo.set(key, (hit = value.apply(target, args)));
|
|
338
|
-
return hit;
|
|
339
|
-
};
|
|
340
|
-
},
|
|
341
|
-
});
|
|
342
|
-
return createAssembler({
|
|
343
|
-
...options,
|
|
344
|
-
stores: () => {
|
|
345
|
-
const s = options.stores();
|
|
346
|
-
return { ...s, pages: wrap(s.pages, "pages"), entries: wrap(s.entries, "entries"), ...(s.assets ? { assets: wrap(s.assets, "assets") } : {}) };
|
|
347
|
-
},
|
|
348
|
-
}, shape);
|
|
349
|
-
}
|
|
350
|
-
return {
|
|
351
|
-
page: (id, locale, path, { draft, version }) => loadPage(id, locale, path, draft, version),
|
|
352
|
-
entry: (id, locale, path, { draft }) => loadEntry(id, locale, path, draft),
|
|
353
|
-
global,
|
|
354
|
-
tree,
|
|
355
|
-
entries: readEntries,
|
|
356
|
-
treeRow,
|
|
357
|
-
batch,
|
|
358
|
-
};
|
|
359
|
-
}
|
package/dist/asset-store.d.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
1
|
+
import { type AssetKind, type AssetMeta, type AssetRecord } from "./media.ts";
|
|
2
|
+
/** A pending asset and the path its bytes go to (spec 2026-10-18 media on R2 and Images §4): the Worker streams them
|
|
3
|
+
* there, then finishes or aborts the row. */
|
|
3
4
|
export type UploadTicket = {
|
|
4
5
|
id: string;
|
|
5
|
-
|
|
6
|
-
token: string;
|
|
6
|
+
path: string;
|
|
7
7
|
};
|
|
8
|
+
/** What the browser measured, and the metadata the upload carried. */
|
|
8
9
|
export type FinishInput = {
|
|
9
10
|
width?: number;
|
|
10
11
|
height?: number;
|
|
11
12
|
duration?: number;
|
|
12
13
|
posterId?: string;
|
|
13
|
-
};
|
|
14
|
+
} & AssetMeta;
|
|
14
15
|
export type AssetQuery = {
|
|
15
16
|
kind?: AssetKind;
|
|
16
17
|
search?: string;
|
|
@@ -36,10 +37,12 @@ export type CreateAssetInput = BeginUploadInput & AssetMeta & {
|
|
|
36
37
|
};
|
|
37
38
|
export declare const PAGE_SIZE = 60;
|
|
38
39
|
export interface AssetStore {
|
|
39
|
-
/** A pending row
|
|
40
|
+
/** A pending row, which no list or read shows, at `<space>/<id>.<ext>`. */
|
|
40
41
|
beginUpload(input: BeginUploadInput): Promise<UploadTicket>;
|
|
41
|
-
/** Marks the row ready with what
|
|
42
|
+
/** Marks the row ready with what was measured. */
|
|
42
43
|
finishUpload(id: string, dims: FinishInput): Promise<AssetRecord>;
|
|
44
|
+
/** A failed upload undone: the pending row and any bytes at its path are deleted; a ready row is refused. */
|
|
45
|
+
abortUpload(id: string): Promise<void>;
|
|
43
46
|
/** Server-side: bytes in, a ready row out (the seed, scripts). */
|
|
44
47
|
create(input: CreateAssetInput): Promise<AssetRecord>;
|
|
45
48
|
/** Any status — the widget shows a pending row as "uploading". */
|
|
@@ -52,7 +55,13 @@ export interface AssetStore {
|
|
|
52
55
|
/** Refused while any refs row points at the asset; a video's poster goes with it. Idempotent. */
|
|
53
56
|
delete(id: string): Promise<void>;
|
|
54
57
|
}
|
|
58
|
+
/** A file the media rules refuse (spec 2026-10-16 the control plane §4): the prefix lets a caller across RPC tell it
|
|
59
|
+
* from a failure and answer 400 with the text after it. */
|
|
60
|
+
export declare const ASSET_REFUSED = "smoodly:refused:";
|
|
61
|
+
export declare const isAssetRefused: (e: unknown) => e is Error;
|
|
55
62
|
export declare const assetErrors: {
|
|
63
|
+
/** `reason` finishes the sentence after the file's name: `is larger than 25 MB`. */
|
|
64
|
+
refused: (filename: string, reason: string) => Error;
|
|
56
65
|
notFound: (id: string) => Error;
|
|
57
66
|
notPending: (id: string) => Error;
|
|
58
67
|
referenced: (id: string, n: number) => Error;
|
|
@@ -61,26 +70,15 @@ export declare const kindOf: (mime: string) => AssetKind;
|
|
|
61
70
|
/** The object's extension: the filename's when it has a sane one, else the mime's, else "bin". */
|
|
62
71
|
export declare function extensionFor(filename: string, mime: string): string;
|
|
63
72
|
export declare const objectPath: (space: string, id: string, filename: string, mime: string) => string;
|
|
64
|
-
/** The
|
|
65
|
-
|
|
66
|
-
export declare
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
private url;
|
|
77
|
-
private row;
|
|
78
|
-
beginUpload(input: BeginUploadInput): Promise<UploadTicket>;
|
|
79
|
-
finishUpload(id: string, dims: FinishInput): Promise<AssetRecord>;
|
|
80
|
-
create(input: CreateAssetInput): Promise<AssetRecord>;
|
|
81
|
-
get(id: string): Promise<AssetRecord | null>;
|
|
82
|
-
getMany(ids: string[]): Promise<Record<string, AssetRecord>>;
|
|
83
|
-
list(query?: AssetQuery): Promise<AssetPage>;
|
|
84
|
-
update(id: string, meta: AssetMeta): Promise<AssetRecord>;
|
|
85
|
-
delete(id: string): Promise<void>;
|
|
86
|
-
}
|
|
73
|
+
/** The media library's rules (config.media: the accepted types, the size cap) as a store applies them at beginUpload and
|
|
74
|
+
* create, so whoever writes a file meets them — the admin, a script, the service's upload routes. */
|
|
75
|
+
export declare function assertMediaRules(rules: {
|
|
76
|
+
maxSize: number;
|
|
77
|
+
accept: readonly string[];
|
|
78
|
+
}, input: {
|
|
79
|
+
filename: string;
|
|
80
|
+
mime: string;
|
|
81
|
+
size: number;
|
|
82
|
+
}): void;
|
|
83
|
+
/** A refusal for size alone, which the upload routes answer 413. */
|
|
84
|
+
export declare const isTooLarge: (e: unknown) => e is Error;
|
package/dist/asset-store.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// The media persistence contract (spec 2026-09-08 media §6, §7); both
|
|
2
|
+
// adapters live in packages/service/src/stores. An asset is one row plus one object; the store never
|
|
3
|
+
// gates on drafts or locales. The delete guard reads the refs index the
|
|
4
|
+
// same way entries do; the walk that writes asset edges lives in refs.ts.
|
|
5
|
+
import { CUT_TYPES } from "./media.js";
|
|
4
6
|
export const PAGE_SIZE = 60;
|
|
7
|
+
/** A file the media rules refuse (spec 2026-10-16 the control plane §4): the prefix lets a caller across RPC tell it
|
|
8
|
+
* from a failure and answer 400 with the text after it. */
|
|
9
|
+
export const ASSET_REFUSED = "smoodly:refused:";
|
|
10
|
+
export const isAssetRefused = (e) => e instanceof Error && e.message.startsWith(ASSET_REFUSED);
|
|
5
11
|
export const assetErrors = {
|
|
12
|
+
/** `reason` finishes the sentence after the file's name: `is larger than 25 MB`. */
|
|
13
|
+
refused: (filename, reason) => new Error(`${ASSET_REFUSED} smoodly: "${filename}" ${reason}.`),
|
|
6
14
|
notFound: (id) => new Error(`smoodly: no asset "${id}".`),
|
|
7
15
|
notPending: (id) => new Error(`smoodly: asset "${id}" is not pending.`),
|
|
8
16
|
referenced: (id, n) => new Error(`smoodly: "asset/${id}" is referenced by ${n} document(s) — remove those references first.`),
|
|
@@ -20,122 +28,19 @@ export function extensionFor(filename, mime) {
|
|
|
20
28
|
return BY_MIME[mime] ?? "bin";
|
|
21
29
|
}
|
|
22
30
|
export const objectPath = (space, id, filename, mime) => `${space}/${id}.${extensionFor(filename, mime)}`;
|
|
23
|
-
/** The
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.space = options.space ?? DEFAULT_SPACE_ID;
|
|
33
|
-
}
|
|
34
|
-
url(path, mime) {
|
|
35
|
-
return withDownload(`${this.baseUrl}/${path}`, mime);
|
|
36
|
-
}
|
|
37
|
-
row(input, status) {
|
|
38
|
-
const id = crypto.randomUUID();
|
|
39
|
-
const path = objectPath(this.space, id, input.filename, input.mime);
|
|
40
|
-
const now = Date.now();
|
|
41
|
-
return {
|
|
42
|
-
id, kind: kindOf(input.mime), status, path, mime: input.mime, size: input.size,
|
|
43
|
-
width: null, height: null, duration: null, posterId: null, filename: input.filename,
|
|
44
|
-
title: null, alt: null, caption: null, credit: null, url: this.url(path, input.mime), createdAt: now, updatedAt: now,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
async beginUpload(input) {
|
|
48
|
-
const row = this.row(input, "pending");
|
|
49
|
-
this.rows.set(row.id, row);
|
|
50
|
-
return { id: row.id, uploadUrl: `memory://upload/${row.path}`, token: "memory" };
|
|
51
|
-
}
|
|
52
|
-
async finishUpload(id, dims) {
|
|
53
|
-
const row = this.rows.get(id);
|
|
54
|
-
if (!row)
|
|
55
|
-
throw assetErrors.notFound(id);
|
|
56
|
-
if (row.status !== "pending")
|
|
57
|
-
throw assetErrors.notPending(id);
|
|
58
|
-
const next = {
|
|
59
|
-
...row, status: "ready", width: dims.width ?? null, height: dims.height ?? null,
|
|
60
|
-
duration: dims.duration ?? null, posterId: dims.posterId ?? null, updatedAt: Date.now(),
|
|
61
|
-
};
|
|
62
|
-
this.rows.set(id, next);
|
|
63
|
-
return structuredClone(next);
|
|
64
|
-
}
|
|
65
|
-
async create(input) {
|
|
66
|
-
const row = {
|
|
67
|
-
...this.row(input, "ready"),
|
|
68
|
-
width: input.width ?? null, height: input.height ?? null, duration: input.duration ?? null,
|
|
69
|
-
posterId: input.posterId ?? null, title: input.title ?? null, alt: input.alt ?? null,
|
|
70
|
-
caption: input.caption ?? null, credit: input.credit ?? null,
|
|
71
|
-
};
|
|
72
|
-
// Distinct createdAt per row keeps "newest first" and the cursor honest
|
|
73
|
-
// when two creates land in one millisecond.
|
|
74
|
-
const last = [...this.rows.values()].reduce((m, r) => Math.max(m, r.createdAt), 0);
|
|
75
|
-
if (row.createdAt <= last) {
|
|
76
|
-
row.createdAt = last + 1;
|
|
77
|
-
row.updatedAt = row.createdAt;
|
|
78
|
-
}
|
|
79
|
-
this.rows.set(row.id, row);
|
|
80
|
-
this.objects.set(row.path, input.bytes);
|
|
81
|
-
return structuredClone(row);
|
|
82
|
-
}
|
|
83
|
-
async get(id) {
|
|
84
|
-
const row = this.rows.get(id);
|
|
85
|
-
return row ? structuredClone(row) : null;
|
|
86
|
-
}
|
|
87
|
-
async getMany(ids) {
|
|
88
|
-
const out = {};
|
|
89
|
-
for (const id of ids) {
|
|
90
|
-
const row = this.rows.get(id);
|
|
91
|
-
if (row?.status === "ready")
|
|
92
|
-
out[id] = structuredClone(row);
|
|
93
|
-
}
|
|
94
|
-
return out;
|
|
95
|
-
}
|
|
96
|
-
async list(query = {}) {
|
|
97
|
-
const limit = query.limit ?? PAGE_SIZE;
|
|
98
|
-
const q = lower(query.search);
|
|
99
|
-
const cursor = query.cursor ? Number(query.cursor) : Infinity;
|
|
100
|
-
const all = [...this.rows.values()]
|
|
101
|
-
.filter((r) => r.status === "ready")
|
|
102
|
-
.filter((r) => !query.kind || r.kind === query.kind)
|
|
103
|
-
.filter((r) => !q || lower(r.title).includes(q) || lower(r.filename).includes(q) || lower(r.alt).includes(q))
|
|
104
|
-
.filter((r) => r.createdAt < cursor)
|
|
105
|
-
.sort((a, b) => b.createdAt - a.createdAt);
|
|
106
|
-
const items = all.slice(0, limit).map((r) => structuredClone(r));
|
|
107
|
-
return { items, nextCursor: all.length > limit ? String(items[items.length - 1].createdAt) : null };
|
|
108
|
-
}
|
|
109
|
-
async update(id, meta) {
|
|
110
|
-
const row = this.rows.get(id);
|
|
111
|
-
if (!row)
|
|
112
|
-
throw assetErrors.notFound(id);
|
|
113
|
-
const next = {
|
|
114
|
-
...row,
|
|
115
|
-
...(meta.title !== undefined ? { title: meta.title } : {}),
|
|
116
|
-
...(meta.alt !== undefined ? { alt: meta.alt } : {}),
|
|
117
|
-
...(meta.caption !== undefined ? { caption: meta.caption } : {}),
|
|
118
|
-
...(meta.credit !== undefined ? { credit: meta.credit } : {}),
|
|
119
|
-
updatedAt: Math.max(Date.now(), row.updatedAt),
|
|
120
|
-
};
|
|
121
|
-
this.rows.set(id, next);
|
|
122
|
-
return structuredClone(next);
|
|
123
|
-
}
|
|
124
|
-
async delete(id) {
|
|
125
|
-
const row = this.rows.get(id);
|
|
126
|
-
if (!row)
|
|
127
|
-
return;
|
|
128
|
-
const usage = this.refs.to(ASSET_COLLECTION, id);
|
|
129
|
-
if (usage.length > 0)
|
|
130
|
-
throw assetErrors.referenced(id, usage.length);
|
|
131
|
-
if (row.posterId) {
|
|
132
|
-
const poster = this.rows.get(row.posterId);
|
|
133
|
-
if (poster) {
|
|
134
|
-
this.rows.delete(poster.id);
|
|
135
|
-
this.objects.delete(poster.path);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
this.rows.delete(id);
|
|
139
|
-
this.objects.delete(row.path);
|
|
140
|
-
}
|
|
31
|
+
/** The media library's rules (config.media: the accepted types, the size cap) as a store applies them at beginUpload and
|
|
32
|
+
* create, so whoever writes a file meets them — the admin, a script, the service's upload routes. */
|
|
33
|
+
export function assertMediaRules(rules, input) {
|
|
34
|
+
if (!rules.accept.includes(input.mime))
|
|
35
|
+
throw assetErrors.refused(input.filename, `is ${input.mime}, not an accepted file type`);
|
|
36
|
+
if (input.size > rules.maxSize)
|
|
37
|
+
throw assetErrors.refused(input.filename, `${TOO_LARGE} ${Math.round(rules.maxSize / 1024 / 1024)} MB`);
|
|
38
|
+
if (CUT_TYPES.has(input.mime) && input.size > IMAGE_SIZE_CEILING)
|
|
39
|
+
throw assetErrors.refused(input.filename, `${TOO_LARGE} 20 MB, the most an image may be`);
|
|
141
40
|
}
|
|
41
|
+
/** Images' input limit (developers.cloudflare.com/images › Limits, "the maximum input size for .input() is 20 MB"):
|
|
42
|
+
* an image past it could never be cut, so it is refused whatever the space's cap. */
|
|
43
|
+
const IMAGE_SIZE_CEILING = 20 * 1024 * 1024;
|
|
44
|
+
const TOO_LARGE = "is larger than";
|
|
45
|
+
/** A refusal for size alone, which the upload routes answer 413. */
|
|
46
|
+
export const isTooLarge = (e) => isAssetRefused(e) && e.message.includes(`" ${TOO_LARGE} `);
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// link per locale the page is published in, the Open Graph values, and noindex. Next writes the same through
|
|
5
5
|
// smoodlyMetadata. Absolute addresses from `site` in astro.config, else the request's origin. A layout places
|
|
6
6
|
// it inside <head>; `fallback` titles a document Smoodly has no title for (an entry whose collection has no
|
|
7
|
-
// seo). A layout that writes its own <title> leaves `fallback` out and reads `of.title` itself.
|
|
7
|
+
// seo). A layout that writes its own <title> leaves `fallback` out and reads `of.title` itself. A page that shows an
|
|
8
|
+
// image preconnects to the media host, with no crossorigin: an <img> is fetched with credentials and would not use an
|
|
9
|
+
// anonymous connection (spec 2026-10-18 media on R2 and Images §7).
|
|
8
10
|
const { of: head, fallback } = Astro.props;
|
|
9
11
|
const origin = (Astro.site ?? Astro.url).origin;
|
|
10
12
|
const absolute = (path: string) => new URL(path, origin).href;
|
|
@@ -12,6 +14,7 @@ const title = head.title ?? fallback;
|
|
|
12
14
|
const canonical = head.canonical ? absolute(head.canonical) : undefined;
|
|
13
15
|
const image = head.image;
|
|
14
16
|
---
|
|
17
|
+
{head.mediaOrigin && <link rel="preconnect" href={head.mediaOrigin} />}
|
|
15
18
|
{title && <title>{title}</title>}
|
|
16
19
|
{head.description && <meta name="description" content={head.description} />}
|
|
17
20
|
{canonical && <link rel="canonical" href={canonical} />}
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
---
|
|
2
|
-
// <Image>:
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
2
|
+
// <Image>: the one image markup (src/image/markup.ts) written as HTML, attribute for attribute what React writes for
|
|
3
|
+
// <SmoodlyImage> (spec 2026-10-18 media on R2 and Images §7): a box, a plain <img> over the cut's widths, or a <picture>
|
|
4
|
+
// with a <source> per crop the view places in `media`. No Astro image service and no Smoodly JavaScript. Data
|
|
5
|
+
// attributes land on the box. In a prerendered page of static output the addresses become files of the site's own
|
|
6
|
+
// output, AVIF and WebP (static-media.ts, spec §8); the preview address, rendered on demand, keeps the media host's.
|
|
7
|
+
import { imageHtml, imageMarkup, staticImage } from "smoodly/astro/runtime";
|
|
8
|
+
import { output } from "smoodly:build";
|
|
9
9
|
|
|
10
|
-
const { image, aspect, sizes, priority, loading, alt,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const src = transformed?.src ?? image.url;
|
|
15
|
-
const srcset = transformed?.srcSet.attribute || undefined;
|
|
10
|
+
const { image, aspect, media, sizes, priority, loading, alt, quality, maxWidth, class: className, style, ...data } = Astro.props;
|
|
11
|
+
let markup = imageMarkup(image, { aspect, media, sizes, priority, loading, alt, quality, maxWidth });
|
|
12
|
+
if (output === "static" && Astro.isPrerendered) markup = await staticImage(markup);
|
|
13
|
+
const html = imageHtml(markup, { class: className, style: typeof style === "string" && style ? style : undefined, data });
|
|
16
14
|
---
|
|
17
|
-
<
|
|
18
|
-
<img src={src} srcset={srcset} sizes={srcset ? sizes : undefined} alt={alt ?? image.alt} width={image.width} height={image.height}
|
|
19
|
-
loading={priority ? "eager" : loading ?? "lazy"} fetchpriority={priority ? "high" : undefined} decoding="async" style={placed.frame} />
|
|
20
|
-
</span>
|
|
15
|
+
<Fragment set:html={html} />
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
// A <video>
|
|
2
|
+
// A <video> from the media host with its poster (a cut where there is one) and size, no transcoding (spec 2026-09-08
|
|
3
|
+
// media §9).
|
|
4
|
+
import { posterUrl } from "smoodly/astro/runtime";
|
|
5
|
+
|
|
3
6
|
const { video, ...rest } = Astro.props;
|
|
4
7
|
---
|
|
5
|
-
<video src={video.url} poster={video
|
|
8
|
+
<video src={video.url} poster={posterUrl(video)} width={video.width} height={video.height} playsinline {...rest}></video>
|
package/dist/astro/head.d.ts
CHANGED
|
@@ -9,5 +9,8 @@ export type PageHead = {
|
|
|
9
9
|
canonical?: string;
|
|
10
10
|
/** locale → the page's address there, for a multi-locale site; empty otherwise. */
|
|
11
11
|
languages: Record<string, string>;
|
|
12
|
+
/** The media host of the first image the page shows, for a preconnect (spec 2026-10-18 media on R2 and Images §7):
|
|
13
|
+
* the connection opens while the HTML is parsed. */
|
|
14
|
+
mediaOrigin?: string;
|
|
12
15
|
};
|
|
13
16
|
export declare function headOf(hit: SiteHit, locale: string, href: (path: string, locale: string) => string, locales: LocaleSet): PageHead;
|