smoodly 0.0.45 → 0.0.47
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 +3 -3
- package/dist/addresses.d.ts +6 -7
- package/dist/addresses.js +6 -4
- package/dist/admin/PublicSans-LICENSE.txt +93 -0
- package/dist/admin/auth-config.d.ts +6 -0
- package/dist/admin/auth-config.js +1 -0
- package/dist/admin/browser/fetch-op.d.ts +10 -0
- package/dist/admin/browser/fetch-op.js +44 -0
- package/dist/admin/browser/main.d.ts +1 -0
- package/dist/admin/browser/main.js +21 -0
- package/dist/admin/custom-section.d.ts +1 -1
- package/dist/admin/drift.d.ts +1 -1
- package/dist/admin/editor/EditorView.js +14 -14
- package/dist/admin/editor/autosave.js +1 -1
- package/dist/admin/editor/canvas-address.d.ts +2 -0
- package/dist/admin/editor/canvas-address.js +8 -0
- package/dist/admin/editor/overlay-geometry.d.ts +1 -1
- package/dist/admin/editor/protocol.d.ts +18 -4
- package/dist/admin/editor/protocol.js +15 -1
- package/dist/admin/editor/useCanvasBridge.d.ts +1 -1
- package/dist/admin/editor/useCanvasBridge.js +5 -6
- package/dist/admin/editor/useInlineSession.js +4 -2
- package/dist/admin/files.d.ts +11 -0
- package/dist/admin/files.generated.js +2 -0
- package/dist/admin/files.js +8 -0
- package/dist/admin/forms/useRefOptions.d.ts +2 -2
- package/dist/admin/forms/useRefOptions.js +2 -2
- package/dist/admin/forms/useRichtextEditor.js +4 -4
- package/dist/admin/handler.d.ts +40 -0
- package/dist/admin/handler.js +151 -0
- package/dist/admin/health.d.ts +4 -0
- package/dist/admin/index.d.ts +1 -1
- package/dist/admin/ops-impl.d.ts +7 -1
- package/dist/admin/ops-impl.js +112 -35
- package/dist/admin/ops.d.ts +3 -2
- package/dist/admin/page-data.d.ts +21 -0
- package/dist/admin/page-data.js +4 -0
- package/dist/admin/page.d.ts +6 -0
- package/dist/admin/page.js +35 -0
- package/dist/admin/serialize.js +7 -7
- package/dist/admin/shell/AdminApp.js +2 -2
- package/dist/admin/shell/ContentHealthPage.js +3 -1
- package/dist/admin/shell/EntryForm.js +1 -1
- package/dist/admin/shell/router.d.ts +3 -16
- package/dist/admin/shell/router.js +3 -39
- package/dist/admin/shell/session.d.ts +2 -5
- package/dist/admin/supabase-auth.js +1 -1
- package/dist/admin/tree-ops.d.ts +1 -1
- package/dist/admin/ui/fonts.d.ts +12 -0
- package/dist/admin/ui/fonts.js +22 -0
- package/dist/admin/ui/theme.d.ts +13 -4
- package/dist/admin/ui/theme.js +28 -157
- package/dist/assemble.d.ts +60 -0
- package/dist/assemble.js +359 -0
- package/dist/astro/cache.d.ts +34 -0
- package/dist/astro/cache.js +69 -0
- package/dist/astro/components/Custom.astro +21 -0
- package/dist/astro/components/Guarded.astro +24 -0
- package/dist/astro/components/Head.astro +26 -0
- package/dist/astro/components/Image.astro +20 -0
- package/dist/astro/components/RichText.astro +23 -0
- package/dist/astro/components/Section.astro +20 -0
- package/dist/astro/components/Video.astro +5 -0
- package/dist/astro/components/Zone.astro +19 -0
- package/dist/astro/components.d.ts +6 -0
- package/dist/astro/components.js +7 -0
- package/dist/astro/content-loaders.d.ts +40 -0
- package/dist/astro/content-loaders.js +100 -0
- package/dist/astro/content.d.ts +9 -0
- package/dist/astro/content.js +17 -0
- package/dist/astro/custom.d.ts +1 -0
- package/dist/astro/custom.js +3 -0
- package/dist/astro/defaults.d.ts +20 -0
- package/dist/astro/defaults.js +21 -0
- package/dist/astro/env-files.d.ts +1 -0
- package/dist/astro/env-files.js +21 -0
- package/dist/astro/head.d.ts +13 -0
- package/dist/astro/head.js +19 -0
- package/dist/astro/image.d.ts +27 -0
- package/dist/astro/image.js +29 -0
- package/dist/astro/index.d.ts +43 -0
- package/dist/astro/index.js +111 -0
- package/dist/astro/locale-paths.d.ts +30 -0
- package/dist/astro/locale-paths.js +38 -0
- package/dist/astro/middleware.d.ts +1 -0
- package/dist/astro/middleware.js +83 -0
- package/dist/astro/preview-document.d.ts +1 -0
- package/dist/astro/preview-document.js +18 -0
- package/dist/astro/preview.d.ts +10 -0
- package/dist/astro/preview.js +72 -0
- package/dist/astro/richtext.d.ts +15 -0
- package/dist/astro/richtext.js +78 -0
- package/dist/astro/routes/admin.d.ts +3 -0
- package/dist/astro/routes/admin.js +41 -0
- package/dist/astro/routes/page.astro +17 -0
- package/dist/astro/routes/preview.astro +11 -0
- package/dist/astro/runtime.d.ts +32 -0
- package/dist/astro/runtime.js +46 -0
- package/dist/astro/scope.d.ts +45 -0
- package/dist/astro/scope.js +103 -0
- package/dist/astro/server.d.ts +14 -0
- package/dist/astro/server.js +45 -0
- package/dist/astro/shell.d.ts +67 -0
- package/dist/astro/shell.js +63 -0
- package/dist/astro/style.d.ts +1 -0
- package/dist/astro/style.js +11 -0
- package/dist/astro/views-check.d.ts +3 -0
- package/dist/astro/views-check.js +19 -0
- package/dist/astro/zone.d.ts +39 -0
- package/dist/astro/zone.js +33 -0
- package/dist/bind.d.ts +19 -0
- package/dist/bind.js +34 -0
- package/dist/bridge/bridge.d.ts +9 -0
- package/dist/bridge/bridge.js +107 -0
- package/dist/bridge/entry.d.ts +1 -0
- package/dist/bridge/entry.js +5 -0
- package/dist/bridge/preview-pill.d.ts +18 -0
- package/dist/bridge/preview-pill.js +115 -0
- package/dist/cli/import.d.ts +3 -0
- package/dist/cli/import.js +10 -1
- package/dist/cli/load-collections.js +4 -3
- package/dist/cli/load-config.d.ts +4 -0
- package/dist/cli/load-config.js +26 -0
- package/dist/cli/main.d.ts +5 -1
- package/dist/cli/main.js +24 -6
- package/dist/cli/migrate.d.ts +13 -0
- package/dist/cli/migrate.js +30 -2
- package/dist/cli/rebuild.d.ts +23 -0
- package/dist/cli/rebuild.js +22 -0
- package/dist/cli/space-io.d.ts +15 -1
- package/dist/cli/space-io.js +14 -0
- package/dist/client.js +5 -0
- package/dist/config.d.ts +28 -20
- package/dist/config.js +71 -75
- package/dist/content/content-api.d.ts +1 -1
- package/dist/content/node.d.ts +2 -3
- package/dist/content/node.js +2 -1
- package/dist/crash-message.d.ts +1 -0
- package/dist/crash-message.js +8 -0
- package/dist/css.d.ts +4 -0
- package/dist/css.js +5 -2
- package/dist/custom/CustomSection.d.ts +1 -1
- package/dist/custom/CustomSection.js +4 -8
- package/dist/custom/css.js +2 -2
- package/dist/custom/index.d.ts +6 -10
- package/dist/custom/index.js +1 -2
- package/dist/custom/layout.d.ts +5 -0
- package/dist/custom/layout.js +14 -0
- package/dist/document-stamp.d.ts +7 -0
- package/dist/document-stamp.js +48 -0
- package/dist/entry-page.d.ts +3 -18
- package/dist/env.js +1 -1
- package/dist/global-id.js +2 -2
- package/dist/global.d.ts +5 -2
- package/dist/global.js +8 -5
- package/dist/image/next-config.d.ts +3 -0
- package/dist/image/next-config.js +12 -7
- package/dist/image/remote-pattern.d.ts +1 -0
- package/dist/image/remote-pattern.js +5 -0
- package/dist/index.d.ts +15 -17
- package/dist/index.js +10 -19
- package/dist/marks.d.ts +7 -0
- package/dist/marks.js +7 -0
- package/dist/next/admin-route.d.ts +7 -0
- package/dist/next/admin-route.js +28 -0
- package/dist/next/bridge.d.ts +1 -0
- package/dist/next/bridge.js +26 -0
- package/dist/next/canvas-boundary.d.ts +1 -1
- package/dist/next/canvas-slot.d.ts +1 -0
- package/dist/next/canvas-slot.js +4 -3
- package/dist/next/index.d.ts +2 -0
- package/dist/next/index.js +1 -0
- package/dist/next/page-renderer.d.ts +5 -1
- package/dist/next/page-renderer.js +19 -33
- package/dist/next/preview-pill-define.d.ts +1 -0
- package/dist/next/preview-pill-define.js +10 -0
- package/dist/next/preview-pill.d.ts +0 -3
- package/dist/next/preview-pill.js +7 -50
- package/dist/next/queries.d.ts +3 -28
- package/dist/next/queries.js +3 -90
- package/dist/next/reads.d.ts +2 -0
- package/dist/next/reads.js +18 -0
- package/dist/page-context.d.ts +89 -0
- package/dist/page-context.js +8 -0
- package/dist/pairing.d.ts +2 -22
- package/dist/project.d.ts +85 -0
- package/dist/project.js +228 -0
- package/dist/published.d.ts +115 -0
- package/dist/published.js +136 -0
- package/dist/react.d.ts +61 -0
- package/dist/react.js +17 -0
- package/dist/read-shapes.d.ts +1 -1
- package/dist/reads.d.ts +80 -0
- package/dist/reads.js +142 -0
- package/dist/refs.d.ts +1 -1
- package/dist/registration.d.ts +8 -0
- package/dist/registration.js +17 -0
- package/dist/render.d.ts +5 -83
- package/dist/render.js +5 -10
- package/dist/resolve.d.ts +1 -1
- package/dist/richtext/index.d.ts +6 -2
- package/dist/richtext/index.js +3 -2
- package/dist/richtext-href.d.ts +4 -0
- package/dist/richtext-href.js +17 -0
- package/dist/richtext-render.js +1 -14
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +1 -1
- package/dist/section-crash.d.ts +2 -2
- package/dist/section-crash.js +2 -7
- package/dist/section-wrapper.d.ts +3 -10
- package/dist/section-wrapper.js +3 -2
- package/dist/site.d.ts +40 -14
- package/dist/site.js +199 -284
- package/dist/smd-key.d.ts +9 -0
- package/dist/smd-key.js +1 -0
- package/dist/sql-space.d.ts +5 -0
- package/dist/sql-space.js +11 -5
- package/dist/sql.d.ts +2 -0
- package/dist/sql.js +127 -1
- package/dist/store.d.ts +1 -1
- package/dist/supabase-published-store.d.ts +45 -0
- package/dist/supabase-published-store.js +247 -0
- package/dist/supabase-store.d.ts +1 -1
- package/dist/toolset.d.ts +2 -4
- package/dist/toolset.js +6 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/view-props.d.ts +40 -0
- package/dist/view-props.js +1 -0
- package/dist/views.d.ts +36 -0
- package/dist/views.js +52 -0
- package/dist/zones.d.ts +7 -7
- package/dist/zones.js +7 -6
- package/migrations/20261007120000_smoodly_v8.sql +154 -0
- package/migrations/20261007200000_smoodly_v9.sql +30 -0
- package/migrations/20261008120000_smoodly_v10.sql +47 -0
- package/package.json +46 -19
- package/dist/admin/next/bridge.d.ts +0 -2
- package/dist/admin/next/bridge.js +0 -129
- package/dist/admin/next/create-admin.d.ts +0 -19
- package/dist/admin/next/create-admin.js +0 -44
- package/dist/admin/next/index.d.ts +0 -8
- package/dist/admin/next/index.js +0 -8
- package/dist/admin/next/op-handler.d.ts +0 -18
- package/dist/admin/next/op-handler.js +0 -31
package/dist/site.js
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { elementSchemas } from "./richtext-walk.js";
|
|
5
|
-
import { globalNodeId, SETTINGS_KEY, STYLES_KEY, CSS_KEY } from "./global.js";
|
|
6
|
-
import { SMD_KEY } from "./section-wrapper.js";
|
|
7
|
-
import { resolveStyleValues } from "./styles.js";
|
|
8
|
-
import { resolveCustom } from "./custom/index.js";
|
|
9
|
-
import { addressMap, codeAddresses, lastPart } from "./addresses.js";
|
|
10
|
-
import { fullPath, servedAnchor } from "./anchors.js";
|
|
1
|
+
import { schemaOf } from "./entry-store.js";
|
|
2
|
+
import { entrySegment } from "./paths.js";
|
|
3
|
+
import { lastPart } from "./addresses.js";
|
|
11
4
|
import { withDefaults } from "./field-defaults.js";
|
|
12
5
|
import { modelKey } from "./model-key.js";
|
|
6
|
+
import { documentStamp } from "./document-stamp.js";
|
|
13
7
|
import { SMOODLY_VERSION } from "./version.js";
|
|
14
8
|
import { toEntryItem, toPageItem } from "./read-shapes.js";
|
|
15
|
-
import {
|
|
9
|
+
import { pageSeo } from "./seo.js";
|
|
10
|
+
import { checkEntryQuery, createAssembler, pageFieldsOf, siteShape } from "./assemble.js";
|
|
11
|
+
import { collectionOrder } from "./collections.js";
|
|
12
|
+
import { createProjector } from "./project.js";
|
|
16
13
|
/** The `page` prop for a loaded page. `href` is the app's seam from a
|
|
17
14
|
* locale-relative path to a site URL; the default keeps CMS paths as URLs. */
|
|
18
|
-
/** Field types whose stored value is an object: not filterable (memory and SQL would disagree). */
|
|
19
|
-
const OBJECT_FIELD_TYPES = new Set(["image", "video", "file", "richtext", "object"]);
|
|
20
15
|
export function pageContextOf(hit, href = (p) => p) {
|
|
21
16
|
const withUrl = (l) => ({ ...l, url: href(l.path, hit.locale) });
|
|
22
17
|
return {
|
|
@@ -32,264 +27,144 @@ export function pageContextOf(hit, href = (p) => p) {
|
|
|
32
27
|
};
|
|
33
28
|
}
|
|
34
29
|
export function createSmoodlySite(options) {
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const custom = resolveCustom(options.custom);
|
|
39
|
-
const elements = elementSchemas(options.elements);
|
|
40
|
-
// Code-owned addresses from the site's own registry (spec 2026-09-29 nested paths §3): a code-owned root's path.
|
|
41
|
-
const codeOwned = codeAddresses({ locales, pages: options.pages, collections: options.collections });
|
|
42
|
-
const addresses = addressMap(codeOwned);
|
|
43
|
-
const pathOpts = { locales, homePageSlug, addresses };
|
|
44
|
-
/** The same addresses as the path index sees them: an entry's stored part joined to its collection's (spec 2026-09-29 anchored paths §2). */
|
|
45
|
-
const anchors = { addresses, homePageSlug, defaultLocale: locales.default };
|
|
46
|
-
const mounted = options.collections.filter((c) => c.path !== undefined);
|
|
30
|
+
const shape = siteShape(options);
|
|
31
|
+
const { locales, pageSchemas, homePageSlug, styles, custom, codeOwned } = shape;
|
|
32
|
+
const assembler = createAssembler(options, shape);
|
|
47
33
|
// Every cached read keys on it (spec 2026-09-30 tester findings §3): an upgrade or a model change starts a fresh cache.
|
|
48
34
|
const cacheKey = `${SMOODLY_VERSION}:${modelKey({
|
|
49
35
|
// Whole schemas: a collection's order, tree or title field changes a read as its fields do (functions drop out in the hash).
|
|
50
36
|
collections: options.collections,
|
|
51
|
-
globals: (options.globals ?? []).map((g) => ({ name: g.name, fields: g.fields, section: g.section })),
|
|
37
|
+
globals: (options.globals ?? []).map((g) => ({ name: g.name, fields: g.fields, settings: g.settings, section: g.section })),
|
|
52
38
|
sections: options.sections.map((x) => ("schema" in x ? x.schema : x)),
|
|
53
39
|
elements: (options.elements ?? []).map((x) => ("schema" in x ? x.schema : x)),
|
|
54
|
-
pages:
|
|
40
|
+
pages: pageSchemas,
|
|
55
41
|
locales, homePageSlug, styles, custom,
|
|
56
42
|
})}`;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
};
|
|
68
|
-
const sectionSettings = (name) => {
|
|
69
|
-
const s = options.sections.find((x) => ("schema" in x ? x.schema : x).name === name);
|
|
70
|
-
return (s && ("schema" in s ? s.schema : s).settings) ?? {};
|
|
71
|
-
};
|
|
72
|
-
/** Visible to the public in a locale: present there, and a folder or a
|
|
73
|
-
* published page. Draft mode sees every present node. Presence is the
|
|
74
|
-
* locale row — nothing falls back (spec 2026-09-06 §3). */
|
|
75
|
-
const visible = (node, locale, draft) => {
|
|
76
|
-
const row = node.locales[locale];
|
|
77
|
-
return row !== undefined && (draft || node.template === null || row.status === "published");
|
|
78
|
-
};
|
|
79
|
-
const hasPage = (node, locale, draft) => {
|
|
80
|
-
const row = node.locales[locale];
|
|
81
|
-
return node.template !== null && row !== undefined && (draft || row.status === "published");
|
|
82
|
-
};
|
|
83
|
-
const pageFields = (template) => {
|
|
84
|
-
if (template === null)
|
|
85
|
-
return {};
|
|
86
|
-
const p = (options.pages ?? []).find((x) => x.schema.kind === "page" && x.schema.name === template);
|
|
87
|
-
return p?.schema.fields ?? {};
|
|
88
|
-
};
|
|
89
|
-
/** The version whose fields a link shows: the draft's in draft mode, else the published one. */
|
|
90
|
-
const versionFor = (node, locale, draft) => {
|
|
91
|
-
const row = node.locales[locale];
|
|
92
|
-
if (!row || node.template === null)
|
|
93
|
-
return null;
|
|
94
|
-
return draft ? row.draftVersionId : row.publishedVersionId;
|
|
95
|
-
};
|
|
96
|
-
/** Raw page fields (no asset resolution) of many nodes in ONE store read
|
|
97
|
-
* (spec 2026-09-10 page fields §3), defaults applied; `{}` for a folder. */
|
|
98
|
-
async function linkFields(nodes, locale, draft) {
|
|
99
|
-
const { pages } = options.stores();
|
|
100
|
-
const wanted = new Map(); // node id → version id
|
|
101
|
-
for (const n of nodes) {
|
|
102
|
-
const v = versionFor(n, locale, draft);
|
|
103
|
-
if (v)
|
|
104
|
-
wanted.set(n.id, v);
|
|
105
|
-
}
|
|
106
|
-
const byVersion = wanted.size > 0 ? await pages.fieldsOf([...new Set(wanted.values())]) : {};
|
|
107
|
-
const out = new Map();
|
|
108
|
-
for (const n of nodes) {
|
|
109
|
-
const v = wanted.get(n.id);
|
|
110
|
-
out.set(n.id, n.template === null ? {} : withDefaults(v ? byVersion[v] : undefined, pageFields(n.template)));
|
|
111
|
-
}
|
|
112
|
-
return out;
|
|
113
|
-
}
|
|
114
|
-
/** Only ever called for a node PRESENT in the locale (visible() or an ancestor of one). */
|
|
115
|
-
const link = (node, byId, locale, draft, fields) => ({
|
|
116
|
-
title: node.locales[locale].title,
|
|
117
|
-
path: pagePath(node, byId, locale, pathOpts),
|
|
118
|
-
hasPage: hasPage(node, locale, draft),
|
|
119
|
-
fields: fields.get(node.id) ?? {},
|
|
43
|
+
// What a delivery row is stamped with (DESIGN §3, the document stamp): the document format and the schema parts
|
|
44
|
+
// that reach a document, never the package version, so a release leaves current documents current.
|
|
45
|
+
const stamp = documentStamp(options);
|
|
46
|
+
const localeOf = (o) => o?.locale ?? locales.default;
|
|
47
|
+
const published = () => options.stores().published;
|
|
48
|
+
const projector = createProjector({
|
|
49
|
+
assembler,
|
|
50
|
+
stores: () => { const s = options.stores(); return { pages: s.pages, entries: s.entries, published: s.published }; },
|
|
51
|
+
collections: options.collections, globals: options.globals ?? [], sections: options.sections, elements: options.elements,
|
|
52
|
+
locales, model: stamp,
|
|
120
53
|
});
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
54
|
+
// Delivery (spec 2026-10-07 delivery model §3–§4): outside draft mode a read takes the stored document when it is
|
|
55
|
+
// current — built by this model and not marked stale — and otherwise assembles that one document, stores it and serves it.
|
|
56
|
+
const current = (row) => row.model === stamp && !row.stale;
|
|
57
|
+
/** One writer of record (DESIGN §3, the document stamp): only the deployment whose stamp the space recorded rewrites
|
|
58
|
+
* rows; the first writer into an empty record records itself; every other deployment serves assembled documents
|
|
59
|
+
* and writes nothing. Read only when a heal is about to write, so a current page stays one call. A "no" is kept for
|
|
60
|
+
* the process (a later rebuild at this stamp is seen by the next process, which costs assembled reads, never wrong
|
|
61
|
+
* ones); a "yes" is read again on each heal, so a rebuild elsewhere stops this writer at its next heal. */
|
|
62
|
+
let yields = false;
|
|
63
|
+
async function mayWrite() {
|
|
64
|
+
if (yields)
|
|
65
|
+
return false;
|
|
66
|
+
// The record is derived like the rows: a read or write of it that fails is logged, and the page is served
|
|
67
|
+
// assembled with nothing written (spec 2026-10-07 §2 Failure), never failed on it.
|
|
68
|
+
try {
|
|
69
|
+
const recorded = await published().recordedModel();
|
|
70
|
+
if (recorded === null) {
|
|
71
|
+
await published().recordModel(stamp);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
yields = recorded !== stamp;
|
|
75
|
+
return !yields;
|
|
137
76
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
sections: options.sections,
|
|
142
|
-
collections: options.collections,
|
|
143
|
-
globals: options.globals,
|
|
144
|
-
elements: options.elements,
|
|
145
|
-
fetch: fetcher(draft, locale), paths: pathLookup(locale),
|
|
146
|
-
assets: assetFetcher(),
|
|
147
|
-
page: { fields: pageFields(record.template) },
|
|
148
|
-
custom,
|
|
149
|
-
});
|
|
150
|
-
const all = await pages.listPages();
|
|
151
|
-
const index = new Map(all.map((n) => [n.id, n]));
|
|
152
|
-
const byId = (x) => index.get(x);
|
|
153
|
-
const chain = chainOf(record, byId);
|
|
154
|
-
const kids = all.filter((n) => n.parentId === id && visible(n, locale, draft));
|
|
155
|
-
const fields = await linkFields([...chain.slice(0, -1), ...kids], locale, draft);
|
|
156
|
-
const ancestors = chain.slice(0, -1).map((n) => link(n, byId, locale, draft, fields));
|
|
157
|
-
const children = kids.map((n) => link(n, byId, locale, draft, fields));
|
|
158
|
-
// Alternates advertise PUBLISHED locales only; the editor (draft) sees every present
|
|
159
|
-
// one. Derived from the tree already listed — no index lookup (spec 2026-09-15 §2).
|
|
160
|
-
const alternates = pageAlternates(record, byId, pathOpts, (l) => draft || record.locales[l]?.status === "published");
|
|
161
|
-
const withSeo = (options.pages ?? []).some((x) => x.schema.kind === "page" && x.schema.name === record.template && x.schema.seo === true);
|
|
162
|
-
return {
|
|
163
|
-
kind: "page",
|
|
164
|
-
locale,
|
|
165
|
-
record,
|
|
166
|
-
template: record.template,
|
|
167
|
-
tree,
|
|
168
|
-
title: row.title,
|
|
169
|
-
path,
|
|
170
|
-
fields: withDefaults(tree.fields, pageFields(record.template)),
|
|
171
|
-
ancestors,
|
|
172
|
-
children,
|
|
173
|
-
locales: alternates,
|
|
174
|
-
...(withSeo ? { seo: pageSeo(tree.meta, row.title) } : {}),
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
async function loadEntry(id, locale, path, draft) {
|
|
178
|
-
const { entries } = options.stores();
|
|
179
|
-
for (const schema of mounted) {
|
|
180
|
-
const record = await entries.get(schema.name, id);
|
|
181
|
-
if (!record)
|
|
182
|
-
continue;
|
|
183
|
-
// No row, or a draft row outside draft mode, is a 404 for THIS locale.
|
|
184
|
-
const row = record.locales[locale];
|
|
185
|
-
if (!row)
|
|
186
|
-
return null;
|
|
187
|
-
if (!draft && row.status !== "published")
|
|
188
|
-
return null;
|
|
189
|
-
const raw = draft
|
|
190
|
-
? entryFieldsIn(record, locale)
|
|
191
|
-
: (await entries.getMany(schema.name, [id], locale, { status: "published" }))[id];
|
|
192
|
-
if (!raw)
|
|
193
|
-
return null;
|
|
194
|
-
const fields = await resolveFields(raw, schema.fields, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), paths: pathLookup(locale), assets: assetFetcher() });
|
|
195
|
-
// Alternates advertise PUBLISHED locales only; the editor (draft) sees every present one.
|
|
196
|
-
// One record is in memory (a nested entry's parents are not), so the `paths` index
|
|
197
|
-
// supplies the paths — the same single lookup as before (spec 2026-09-15 §2).
|
|
198
|
-
const known = await entries.pathsOf(schema.name, id);
|
|
199
|
-
const alternates = entryAlternates(record, locales, (l) => draft || record.locales[l]?.status === "published", (l) => known[l] ?? null);
|
|
200
|
-
return {
|
|
201
|
-
kind: "entry", locale, collection: schema.name, entry: { ...record, fields }, path, locales: alternates,
|
|
202
|
-
...(schema.seo ? { seo: entrySeo(fields, schema) } : {}),
|
|
203
|
-
};
|
|
77
|
+
catch (e) {
|
|
78
|
+
console.error("smoodly: reading the space's recorded document stamp failed; served assembled, nothing written.", e);
|
|
79
|
+
return false;
|
|
204
80
|
}
|
|
205
|
-
return null;
|
|
206
81
|
}
|
|
207
|
-
const
|
|
208
|
-
|
|
82
|
+
const fresh = (row) => current(row) && row.document != null;
|
|
83
|
+
const itemOf = (doc) => toEntryItem(doc.entry, doc.entry.fields, { path: doc.path, locale: doc.locale, locales: doc.locales, ...(doc.seo ? { seo: doc.seo } : {}) });
|
|
84
|
+
/** A page row's answer: its document when current (null for a folder or a hidden node), else the healed one. */
|
|
85
|
+
const pageOf = async (row, id, locale) => row && current(row) ? row.document : projector.healPage(id, locale, { seen: row !== null, write: await mayWrite() });
|
|
86
|
+
/** An entry document is served at an address only when its collection has addresses. */
|
|
87
|
+
const served = (doc) => (doc && doc.path !== null ? doc : null);
|
|
88
|
+
const entryOf = async (row, id, locale) => row && fresh(row) ? row.document : projector.healEntry(id, locale, { seen: row !== null, write: await mayWrite() });
|
|
89
|
+
/** An entry document as the projector would write it, from an assembled read (the address-less shape, path from the item). */
|
|
90
|
+
const assembledEntry = (collection, locale) => (read) => ({
|
|
91
|
+
kind: "entry", locale, collection, entry: { ...read.record, fields: read.item.fields },
|
|
92
|
+
path: read.item.path, locales: read.item.locales, ...(read.item.seo ? { seo: read.item.seo } : {}),
|
|
93
|
+
});
|
|
94
|
+
/** The published documents of a collection's query. Stale rows are rebuilt in one batched projection and the
|
|
95
|
+
* query read once more (spec §4): a rebuild that finds an entry unpublished removes its row, so it drops out. */
|
|
96
|
+
async function deliveredEntries(collection, o) {
|
|
209
97
|
const schema = schemaOf(options.collections, collection);
|
|
98
|
+
checkEntryQuery(schema, collection, o);
|
|
210
99
|
const locale = localeOf(o);
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
100
|
+
const read = () => published().entries(collection, {
|
|
101
|
+
locale, where: o.where, order: o.order ?? collectionOrder(schema), limit: o.limit, ids: o.ids,
|
|
102
|
+
...(o.parent !== undefined ? { parent: o.parent } : {}),
|
|
103
|
+
});
|
|
104
|
+
let rows = await read();
|
|
105
|
+
const behind = rows.filter((r) => !fresh(r)).map((r) => r.id);
|
|
106
|
+
if (behind.length > 0) {
|
|
107
|
+
// Not the writer of record: the list assembled now, nothing written (DESIGN §3).
|
|
108
|
+
if (!(await mayWrite()))
|
|
109
|
+
return (await assembler.entries(collection, { ...o, locale, draft: false })).map(assembledEntry(collection, locale));
|
|
110
|
+
// A failed rebuild never fails the read: the rows as read are served, and the next read tries again.
|
|
111
|
+
try {
|
|
112
|
+
await projector.entries(behind, { cap: Infinity });
|
|
113
|
+
rows = await read();
|
|
225
114
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (Array.isArray(o.where[key]) && ARRAY_FIELD_TYPES.has(schema.fields[key].type ?? "")) {
|
|
229
|
-
throw new Error(`smoodly: getEntries("${collection}"): "${key}" is array-valued — any-of is for scalar fields; pass one value to match entries containing it.`);
|
|
115
|
+
catch (e) {
|
|
116
|
+
console.error(`smoodly: rebuilding ${behind.length} ${collection} documents in the delivery tables failed; served as read.`, e);
|
|
230
117
|
}
|
|
231
118
|
}
|
|
232
|
-
|
|
233
|
-
throw new Error(`smoodly: getEntries("${collection}"): limit must be a non-negative integer.`);
|
|
234
|
-
}
|
|
235
|
-
// The store filters, orders and limits over the snapshot shown (spec 2026-09-29 content API contract §5): the
|
|
236
|
-
// locale gate — presence in draft mode, published otherwise — is part of the query, so `limit` counts rows that are really there.
|
|
237
|
-
const rows = await entries.query(collection, {
|
|
238
|
-
locale, draft, where: o.where, order: o.order, limit: o.limit, ids: o.ids,
|
|
239
|
-
...(o.parent !== undefined ? { parent: o.parent } : {}),
|
|
240
|
-
});
|
|
241
|
-
// Paths need every ancestor: a tree collection reads the whole collection for that, a flat one does not.
|
|
242
|
-
const all = schema.tree ? await entries.list(collection) : rows.map((r) => r.record);
|
|
243
|
-
// References and media resolve for the whole list in one pass (§2, §6).
|
|
244
|
-
const sets = rows.map((r) => ({ values: structuredClone(r.fields), descriptors: schema.fields }));
|
|
245
|
-
await resolveFieldSets(sets, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), paths: pathLookup(locale), assets: assetFetcher() });
|
|
246
|
-
const index = new Map(all.map((e) => [e.id, e]));
|
|
247
|
-
const byId = (x) => index.get(x);
|
|
248
|
-
return rows.map(({ record }, i) => {
|
|
249
|
-
const paths = entryPathRows([record], byId, schema.name);
|
|
250
|
-
const pathIn = (l) => {
|
|
251
|
-
const row = paths.find((r) => r.locale === l);
|
|
252
|
-
return row ? fullPath(anchors, row.anchor ?? "", row.path, l) : null;
|
|
253
|
-
};
|
|
254
|
-
const item = toEntryItem(record, sets[i].values, {
|
|
255
|
-
path: pathIn(locale),
|
|
256
|
-
locale,
|
|
257
|
-
locales: entryAlternates(record, locales, (l) => draft || record.locales[l]?.status === "published", pathIn),
|
|
258
|
-
...(schema.seo ? { seo: entrySeo(sets[i].values, schema) } : {}),
|
|
259
|
-
});
|
|
260
|
-
return { record, item };
|
|
261
|
-
});
|
|
119
|
+
return rows.flatMap((r) => (r.document != null ? [r.document] : []));
|
|
262
120
|
}
|
|
263
|
-
const getEntries = async (collection, o = {}) => (await
|
|
121
|
+
const getEntries = async (collection, o = {}) => o.draft ? (await assembler.entries(collection, o)).map((r) => r.item) : (await deliveredEntries(collection, o)).map(itemOf);
|
|
122
|
+
/** Every entry of the collection in the locale, with the record a tree and a bare slug read `parentId` from. */
|
|
123
|
+
const readAll = async (collection, locale, draft) => draft
|
|
124
|
+
? assembler.entries(collection, { locale, draft })
|
|
125
|
+
: (await deliveredEntries(collection, { locale })).map((doc) => ({ record: doc.entry, item: itemOf(doc) }));
|
|
264
126
|
const api = {
|
|
265
127
|
async getPagesTree(o) {
|
|
266
128
|
const locale = localeOf(o);
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
129
|
+
if (o?.draft)
|
|
130
|
+
return assembler.tree(locale, { draft: true });
|
|
131
|
+
// One light read. A row behind the model or marked stale is rebuilt before answering (spec §4), and the tree read
|
|
132
|
+
// again; a rebuild that finds the node gone or unpublished removes its row.
|
|
133
|
+
let rows = await published().tree(locale);
|
|
134
|
+
const behind = rows.filter((r) => !current(r)).map((r) => r.id);
|
|
135
|
+
if (behind.length > 0) {
|
|
136
|
+
// Not the writer of record: the tree assembled now, nothing written (DESIGN §3).
|
|
137
|
+
if (!(await mayWrite()))
|
|
138
|
+
return assembler.tree(locale, { draft: false });
|
|
139
|
+
// One batched projection for every row behind (one tree read in the projector), then one more tree read.
|
|
140
|
+
// A failed rebuild never fails the read: the rows as read are served, and the next read tries again.
|
|
141
|
+
try {
|
|
142
|
+
await projector.pages(behind, { cap: Infinity });
|
|
143
|
+
rows = await published().tree(locale);
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
console.error(`smoodly: rebuilding ${behind.length} page documents in the delivery tables failed; served as read.`, e);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// A node the tree hides (path null) leaves with its subtree, as does one whose parent has no row.
|
|
150
|
+
const build = (parentId) => rows.filter((r) => r.parentId === parentId && r.path !== null).map((r) => ({
|
|
151
|
+
id: r.id, title: r.title, path: r.path, hasPage: r.hasPage, fields: r.linkFields, children: build(r.id),
|
|
152
|
+
}));
|
|
153
|
+
return build(null);
|
|
286
154
|
},
|
|
287
155
|
async getPage(pathOrId, o) {
|
|
288
156
|
const locale = localeOf(o);
|
|
157
|
+
if (!o?.draft) {
|
|
158
|
+
// The content API stays strict (§7): a redirect row, and a code-owned page with no record, are not pages.
|
|
159
|
+
const hit = pathOrId.startsWith("/")
|
|
160
|
+
? (await delivered(locale, pathOrId, false)).hit
|
|
161
|
+
: await pageOf(await published().page(pathOrId, locale), pathOrId, locale);
|
|
162
|
+
return hit?.kind === "page" ? toPageItem(hit) : null;
|
|
163
|
+
}
|
|
289
164
|
const path = pathOrId.startsWith("/") ? pathOrId : (await options.stores().pages.pathsOf(pathOrId))[locale];
|
|
290
165
|
if (!path)
|
|
291
166
|
return null;
|
|
292
|
-
const hit = await site.resolve(locale, path, { draft:
|
|
167
|
+
const hit = await site.resolve(locale, path, { draft: true });
|
|
293
168
|
return hit?.kind === "page" ? toPageItem(hit) : null;
|
|
294
169
|
},
|
|
295
170
|
getEntries,
|
|
@@ -298,64 +173,79 @@ export function createSmoodlySite(options) {
|
|
|
298
173
|
const locale = localeOf(o);
|
|
299
174
|
const draft = o?.draft ?? false;
|
|
300
175
|
if (idOrPath.startsWith("/")) {
|
|
301
|
-
const hit = await site.resolve(locale, idOrPath, { draft });
|
|
176
|
+
const hit = draft ? await site.resolve(locale, idOrPath, { draft }) : (await delivered(locale, idOrPath, false)).hit;
|
|
302
177
|
return hit?.kind === "entry" && hit.collection === collection ? toEntryItem(hit.entry, hit.entry.fields, hit) : null;
|
|
303
178
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
179
|
+
if (draft) {
|
|
180
|
+
const [byId] = await assembler.entries(collection, { locale, draft, ids: [idOrPath] });
|
|
181
|
+
if (byId)
|
|
182
|
+
return byId.item;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const row = await published().entry(idOrPath, locale);
|
|
186
|
+
if (row && row.collection === collection && fresh(row))
|
|
187
|
+
return itemOf(row.document);
|
|
188
|
+
// A miss is a record no projection wrote yet, or a slug: one `get` tells them apart.
|
|
189
|
+
if (row?.collection === collection || (await options.stores().entries.get(collection, idOrPath))) {
|
|
190
|
+
const doc = await projector.healEntry(idOrPath, locale, { seen: row !== null, write: await mayWrite() });
|
|
191
|
+
if (doc)
|
|
192
|
+
return itemOf(doc);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
307
195
|
// A bare slug names a ROOT entry only (sibling-scoped slugs, spec §4):
|
|
308
196
|
// match the localized slug directly (paths.ts owns slug derivation),
|
|
309
197
|
// regardless of whether the collection even has a `path`.
|
|
310
|
-
const all = await
|
|
198
|
+
const all = await readAll(collection, locale, draft);
|
|
311
199
|
return all.find((r) => r.record.parentId === null && entrySegment(r.record, locale) === idOrPath)?.item ?? null;
|
|
312
200
|
},
|
|
313
201
|
async getEntriesTree(collection, o) {
|
|
314
|
-
const rows = await
|
|
202
|
+
const rows = await readAll(collection, localeOf(o), o?.draft ?? false);
|
|
315
203
|
const build = (parentId) => rows.filter((r) => r.record.parentId === parentId).map((r) => ({ ...r.item, children: build(r.record.id) }));
|
|
316
204
|
return build(null);
|
|
317
205
|
},
|
|
318
206
|
async getGlobal(item, o) {
|
|
319
207
|
const locale = localeOf(o);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const row = record.locales[locale];
|
|
326
|
-
if (!row || (!draft && row.status !== "published"))
|
|
327
|
-
return null;
|
|
328
|
-
const raw = draft
|
|
329
|
-
? entryFieldsIn(record, locale)
|
|
330
|
-
: (await entries.getMany(item.name, [record.id], locale, { status: "published" }))[record.id];
|
|
331
|
-
if (!raw)
|
|
332
|
-
return null;
|
|
333
|
-
const fields = await resolveFields(raw, item.fields, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), paths: pathLookup(locale), assets: assetFetcher() });
|
|
334
|
-
if (!item.section)
|
|
335
|
-
return { locale, fields };
|
|
336
|
-
const { [SETTINGS_KEY]: settings, [STYLES_KEY]: steps, [CSS_KEY]: css, ...rest } = fields;
|
|
337
|
-
const fetch = assetFetcher();
|
|
338
|
-
const own = settings !== null && typeof settings === "object" ? settings : {};
|
|
339
|
-
if (fetch)
|
|
340
|
-
await resolveMedia(own, sectionSettings(item.section), fetch, elements);
|
|
341
|
-
// 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):
|
|
342
|
-
// the wrapper strips it and emits data-smd-node, so the canvas selects the item and the css reaches it.
|
|
343
|
-
const stepsObject = steps !== null && typeof steps === "object" ? steps : {};
|
|
344
|
-
return {
|
|
345
|
-
locale,
|
|
346
|
-
fields: rest,
|
|
347
|
-
settings: own,
|
|
348
|
-
styles: { ...stepsObject, [SMD_KEY]: { node: globalNodeId(item.name), css: typeof css === "string" ? css : undefined, values: styles } },
|
|
349
|
-
};
|
|
208
|
+
if (o?.draft)
|
|
209
|
+
return assembler.global(item, locale, { draft: true });
|
|
210
|
+
// The item's row by its collection, in one read: a global is a one-record collection.
|
|
211
|
+
const [row] = await published().entries(item.name, { locale, limit: 1 });
|
|
212
|
+
return row && fresh(row) ? row.document : projector.healGlobal(item, locale, { seen: row !== undefined, write: await mayWrite() });
|
|
350
213
|
},
|
|
351
214
|
};
|
|
215
|
+
/** The published read of an address; `code`: the code-owned page answers where the index has no row. */
|
|
216
|
+
/** `globals`: the names of global items to bring along (schema v9); `rows` is what the route carried, when it had any. */
|
|
217
|
+
async function delivered(locale, path, code, globals) {
|
|
218
|
+
const route = await published().byAddress(locale, path, globals);
|
|
219
|
+
const rows = route?.globals;
|
|
220
|
+
if (route && fresh(route.row))
|
|
221
|
+
return { hit: route.row.document, rows };
|
|
222
|
+
let healed = null;
|
|
223
|
+
if (route) {
|
|
224
|
+
// Stale: rebuilt by id. Unpublished or moved since the row was written, the address is answered as a miss.
|
|
225
|
+
const write = await mayWrite();
|
|
226
|
+
healed = route.kind === "page" ? await projector.healPage(route.row.id, locale, { write }) : served(await projector.healEntry(route.row.id, locale, { write }));
|
|
227
|
+
if (healed && healed.path === path)
|
|
228
|
+
return { hit: healed, rows };
|
|
229
|
+
}
|
|
230
|
+
// A miss: the paths index doubles as the existence check — a record to assemble, a redirect row, or nothing.
|
|
231
|
+
// A record wins over the code-owned page at its address, as the renderer has it.
|
|
232
|
+
const hit = await site.locate(locale, path);
|
|
233
|
+
if (!hit)
|
|
234
|
+
return { hit: code ? codePage(locale, path) : null };
|
|
235
|
+
if (hit.redirect)
|
|
236
|
+
return { hit: { kind: "redirect", target: { kind: hit.kind, id: hit.id } } };
|
|
237
|
+
if (route && hit.id === route.row.id)
|
|
238
|
+
return { hit: healed, rows }; // just rebuilt
|
|
239
|
+
// By id: a page the tree hides has a current row the address lookup cannot reach; anything else heals there.
|
|
240
|
+
return { hit: await site.load(hit, locale, path), rows };
|
|
241
|
+
}
|
|
352
242
|
const codePath = (address) => (address === homePageSlug ? "/" : `/${address}`);
|
|
353
243
|
function codePage(locale, path) {
|
|
354
244
|
const spec = codeOwned.find((a) => a.template !== null && a.segments[locale] !== undefined && codePath(a.segments[locale]) === path);
|
|
355
245
|
if (!spec || !locales.supported.includes(locale))
|
|
356
246
|
return null;
|
|
357
247
|
const template = spec.template;
|
|
358
|
-
const withSeo =
|
|
248
|
+
const withSeo = pageSchemas.some((x) => x.kind === "page" && x.name === template && x.seo === true);
|
|
359
249
|
return {
|
|
360
250
|
kind: "page",
|
|
361
251
|
locale,
|
|
@@ -365,7 +255,7 @@ export function createSmoodlySite(options) {
|
|
|
365
255
|
tree: { zones: {} },
|
|
366
256
|
title: spec.title,
|
|
367
257
|
path,
|
|
368
|
-
fields: withDefaults(undefined,
|
|
258
|
+
fields: withDefaults(undefined, pageFieldsOf(shape, template)),
|
|
369
259
|
ancestors: [],
|
|
370
260
|
children: [],
|
|
371
261
|
locales: Object.fromEntries(Object.entries(spec.segments).map(([l, address]) => [l, { slug: lastPart(address), path: codePath(address) }])),
|
|
@@ -376,13 +266,37 @@ export function createSmoodlySite(options) {
|
|
|
376
266
|
locales,
|
|
377
267
|
homePageSlug,
|
|
378
268
|
cacheKey,
|
|
269
|
+
stamp,
|
|
379
270
|
styles,
|
|
380
271
|
custom,
|
|
381
272
|
locate: (locale, path) => options.stores().pages.resolvePath(locale, path),
|
|
382
273
|
targetPaths: (target) => options.stores().pages.targetPaths(target),
|
|
383
|
-
load(target, locale, path, opts = {}) {
|
|
274
|
+
async load(target, locale, path, opts = {}) {
|
|
384
275
|
const draft = opts.draft ?? false;
|
|
385
|
-
|
|
276
|
+
// Draft mode assembles from the editing tables; so does a stored version, which outside draft mode is nothing.
|
|
277
|
+
if (draft || opts.version !== undefined) {
|
|
278
|
+
return target.kind === "page" ? assembler.page(target.id, locale, path, { draft, version: opts.version }) : assembler.entry(target.id, locale, path, { draft });
|
|
279
|
+
}
|
|
280
|
+
return target.kind === "page"
|
|
281
|
+
? pageOf(await published().page(target.id, locale), target.id, locale)
|
|
282
|
+
: served(await entryOf(await published().entry(target.id, locale), target.id, locale));
|
|
283
|
+
},
|
|
284
|
+
lookup: async (locale, path) => (await delivered(locale, path, true)).hit,
|
|
285
|
+
async lookupWithGlobals(locale, path, items) {
|
|
286
|
+
const { hit, rows } = await delivered(locale, path, true, items.map((i) => i.name));
|
|
287
|
+
const globals = new Map();
|
|
288
|
+
if (rows) {
|
|
289
|
+
for (const item of items) {
|
|
290
|
+
const row = rows.find((r) => r.collection === item.name);
|
|
291
|
+
// No row: unpublished here, or never projected — left out rather than healed on every read, because a
|
|
292
|
+
// heal of an unpublished item writes nothing and the next read would heal again; the layout's own read
|
|
293
|
+
// pays the call where a page renders it. A stale row heals once and is written.
|
|
294
|
+
if (!row)
|
|
295
|
+
continue;
|
|
296
|
+
globals.set(item.name, fresh(row) ? row.document : await projector.healGlobal(item, locale, { seen: true, write: await mayWrite() }));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return { hit, globals };
|
|
386
300
|
},
|
|
387
301
|
async resolve(locale, path, opts) {
|
|
388
302
|
// The content API stays strict (§7): a content address is not a redirect.
|
|
@@ -390,6 +304,7 @@ export function createSmoodlySite(options) {
|
|
|
390
304
|
return target && !target.redirect ? site.load(target, locale, path, opts) : null;
|
|
391
305
|
},
|
|
392
306
|
codePage,
|
|
307
|
+
projector,
|
|
393
308
|
...api,
|
|
394
309
|
};
|
|
395
310
|
return site;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { StyleValues } from "./styles.ts";
|
|
2
|
+
export declare const SMD_KEY = "__smd";
|
|
3
|
+
export type SmdInternal = {
|
|
4
|
+
node?: string;
|
|
5
|
+
css?: string;
|
|
6
|
+
values?: StyleValues;
|
|
7
|
+
/** Pre-scoped layout CSS (the custom section, spec 2026-09-14 §6), emitted before the editor's css so the box wins. */
|
|
8
|
+
layoutCss?: string;
|
|
9
|
+
};
|
package/dist/smd-key.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SMD_KEY = "__smd";
|
package/dist/sql-space.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ export declare const DEFAULT_SPACE = "'00000000-0000-0000-0000-000000000000'";
|
|
|
6
6
|
export declare const SPACE_COL = "\"space_id\" uuid not null default '00000000-0000-0000-0000-000000000000'";
|
|
7
7
|
export declare const CONTENT_TABLES: readonly ["pages", "page_locales", "page_versions", "entries", "entry_locales", "entry_versions", "refs", "paths", "assets"];
|
|
8
8
|
export type ContentTable = (typeof CONTENT_TABLES)[number];
|
|
9
|
+
/** The delivery model's derived tables (spec 2026-10-07 §1): policy-covered and
|
|
10
|
+
* space-stamped like content, but not transferred (export skips them, import
|
|
11
|
+
* rebuilds them) and not counted toward the row cap, since they are derived.
|
|
12
|
+
* `published_models` (schema v10) holds one row per space: the document stamp the space recorded as current. */
|
|
13
|
+
export declare const DELIVERY_TABLES: readonly ["published_pages", "published_entries", "published_models"];
|
|
9
14
|
export declare function currentSpaceSQL(): string;
|
|
10
15
|
/** One permissive policy per action (spec 2026-09-29 pre-install fixes §3):
|
|
11
16
|
* the write key's insert, update and delete, each alone for its action; its
|