smoodly 0.0.35 → 0.0.37
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 +24 -1611
- package/dist/admin/drift.js +3 -2
- package/dist/admin/editor/SeoSettings.d.ts +7 -1
- package/dist/admin/editor/SeoSettings.js +2 -2
- package/dist/admin/field-label.d.ts +2 -0
- package/dist/admin/field-label.js +2 -0
- package/dist/admin/forms/FieldWidget.js +2 -2
- package/dist/admin/next/create-admin.d.ts +4 -4
- package/dist/admin/next/create-admin.js +5 -5
- package/dist/admin/next/index.d.ts +1 -1
- package/dist/admin/serialize.d.ts +8 -1
- package/dist/admin/serialize.js +23 -7
- package/dist/admin/shell/AdminApp.d.ts +3 -3
- package/dist/admin/shell/AdminApp.js +2 -2
- package/dist/admin/shell/EntryForm.js +8 -1
- package/dist/admin/shell/presence-realtime.d.ts +2 -2
- package/dist/cli/space-io.js +1 -1
- package/dist/collections.d.ts +4 -0
- package/dist/collections.js +9 -1
- package/dist/entry-page.d.ts +12 -10
- package/dist/entry-store.d.ts +30 -1
- package/dist/entry-store.js +54 -1
- package/dist/env.d.ts +5 -5
- package/dist/env.js +2 -2
- package/dist/fields.d.ts +13 -7
- package/dist/fields.js +2 -2
- package/dist/global.d.ts +4 -7
- package/dist/index.d.ts +10 -39
- package/dist/index.js +1 -1
- package/dist/migrations.js +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +1 -1
- package/dist/next/meta.d.ts +6 -4
- package/dist/next/meta.js +14 -17
- package/dist/next/page-renderer.js +6 -3
- package/dist/next/queries.d.ts +10 -11
- package/dist/next/queries.js +7 -2
- package/dist/page.d.ts +5 -2
- package/dist/page.js +3 -1
- package/dist/pairing.d.ts +10 -10
- package/dist/path-index.d.ts +3 -0
- package/dist/path-index.js +7 -0
- package/dist/read-shapes.d.ts +64 -0
- package/dist/read-shapes.js +52 -0
- package/dist/render.d.ts +3 -3
- package/dist/resolve.d.ts +12 -0
- package/dist/resolve.js +29 -7
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +1 -1
- package/dist/schema.d.ts +22 -1
- package/dist/seo.d.ts +18 -0
- package/dist/seo.js +53 -0
- package/dist/site.d.ts +15 -16
- package/dist/site.js +64 -58
- package/dist/sql-space.d.ts +6 -5
- package/dist/sql-space.js +11 -24
- package/dist/sql.d.ts +6 -1
- package/dist/sql.js +60 -23
- package/dist/supabase-entry-store.d.ts +3 -1
- package/dist/supabase-entry-store.js +39 -0
- package/dist/supabase-path-index.d.ts +1 -0
- package/dist/supabase-path-index.js +16 -0
- package/migrations/20260930040956_smoodly_v7.sql +84 -0
- package/package.json +1 -1
package/dist/fields.d.ts
CHANGED
|
@@ -24,6 +24,14 @@ export declare function normalizeOptions<V extends string>(options: readonly Opt
|
|
|
24
24
|
export declare function optionsOf(options: unknown): Option[];
|
|
25
25
|
/** The values a stored choice may take. */
|
|
26
26
|
export declare const optionValues: (options: unknown) => string[];
|
|
27
|
+
/** A ref field's value at the type level: the target and how deep the ref site resolves it.
|
|
28
|
+
* Never a runtime value — `Values<M>` turns it into what a read returns. */
|
|
29
|
+
export type RefMode = "shallow" | "full" | (string & {});
|
|
30
|
+
export type RefTo<T, M extends RefMode = "shallow"> = {
|
|
31
|
+
readonly __refTo: T;
|
|
32
|
+
readonly __mode: M;
|
|
33
|
+
};
|
|
34
|
+
type Rebase<V, M extends RefMode> = V extends RefTo<infer T, any> ? RefTo<T, M> : V extends RefTo<infer T, any>[] ? RefTo<T, M>[] : V;
|
|
27
35
|
export type FieldBuilder<Value = unknown, Flags extends BuilderFlags = {}> = {
|
|
28
36
|
readonly descriptor: Descriptor;
|
|
29
37
|
optional(): FieldBuilder<Value, Flags & {
|
|
@@ -43,7 +51,9 @@ export type FieldBuilder<Value = unknown, Flags extends BuilderFlags = {}> = {
|
|
|
43
51
|
* the named refs, one level; object values nest deeper
|
|
44
52
|
* - no call — hydrate the entry itself; its refs stay ids
|
|
45
53
|
*/
|
|
46
|
-
resolve(
|
|
54
|
+
resolve(): FieldBuilder<Rebase<Value, "full">, Flags>;
|
|
55
|
+
resolve<const K extends string>(...keys: K[]): FieldBuilder<Rebase<Value, K>, Flags>;
|
|
56
|
+
resolve<const P extends Record<string, unknown>>(projection: P): FieldBuilder<Rebase<Value, keyof P & string>, Flags>;
|
|
47
57
|
/** Hide the field while the condition holds (spec 2026-09-14 §12): it leaves the form and validation skips it; the stored value is kept. */
|
|
48
58
|
hidden(condition: Condition): FieldBuilder<Value, Flags>;
|
|
49
59
|
/** phantom, type-level only */
|
|
@@ -105,14 +115,10 @@ export declare const f: {
|
|
|
105
115
|
* 300px sidebar cannot show one legibly (spec 2026-09-07, list field §1).
|
|
106
116
|
* `.min`/`.max` count items. */
|
|
107
117
|
list: <B extends FieldBuilder<any, any>>(item: B & NotLocalized<B>) => FieldBuilder<ValueOf<B>[], {}>;
|
|
108
|
-
ref: <T>(target: () => T) => FieldBuilder<
|
|
109
|
-
refList: <T>(target: () => T) => FieldBuilder<
|
|
118
|
+
ref: <T>(target: () => T) => FieldBuilder<RefTo<T, "shallow">, {}>;
|
|
119
|
+
refList: <T>(target: () => T) => FieldBuilder<RefTo<T, "shallow">[], {}>;
|
|
110
120
|
};
|
|
111
121
|
export type ValueOf<B> = B extends FieldBuilder<infer V, infer Flags> ? Flags extends {
|
|
112
122
|
optional: true;
|
|
113
123
|
} ? V | undefined : V : never;
|
|
114
|
-
/** Resolved entry for a ref target — refined when collections land. */
|
|
115
|
-
export type Resolved<T> = T extends {
|
|
116
|
-
__entry?: infer E;
|
|
117
|
-
} ? E : Record<string, unknown>;
|
|
118
124
|
export {};
|
package/dist/fields.js
CHANGED
|
@@ -37,13 +37,13 @@ function builder(descriptor) {
|
|
|
37
37
|
max: (n) => chain({ max: n }),
|
|
38
38
|
min: (n) => chain({ min: n }),
|
|
39
39
|
default: (v) => chain({ default: v }),
|
|
40
|
-
resolve: (...projection) => {
|
|
40
|
+
resolve: ((...projection) => {
|
|
41
41
|
if (projection.length === 0)
|
|
42
42
|
return chain({ resolve: "full" });
|
|
43
43
|
if (typeof projection[0] === "object")
|
|
44
44
|
return chain({ resolve: projection[0] });
|
|
45
45
|
return chain({ resolve: Object.fromEntries(projection.map((k) => [k, true])) });
|
|
46
|
-
},
|
|
46
|
+
}),
|
|
47
47
|
hidden: (condition) => chain({ hidden: condition }),
|
|
48
48
|
};
|
|
49
49
|
}
|
package/dist/global.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { CollectionSchema } from "./collections.ts";
|
|
2
2
|
import type { Descriptor } from "./fields.ts";
|
|
3
3
|
import type { PairedComponent } from "./pairing.tsx";
|
|
4
|
-
import type { BuilderMap,
|
|
5
|
-
import type {
|
|
4
|
+
import type { BuilderMap, SectionSchema, Values } from "./schema.ts";
|
|
5
|
+
import type { GlobalItem, VisualGlobalItem } from "./read-shapes.ts";
|
|
6
6
|
/** The reserved tree node type of a placement. */
|
|
7
7
|
export declare const GLOBAL_NODE_TYPE = "global";
|
|
8
8
|
/** The reserved keys a visual item's settings, styles steps and css live under in its row (spec 2026-09-14 §8). */
|
|
@@ -46,12 +46,9 @@ type GlobalInput<N extends string = string> = {
|
|
|
46
46
|
export declare function globalItem<F extends BuilderMap, const N extends string = string>(input: GlobalInput<N> & {
|
|
47
47
|
fields: F;
|
|
48
48
|
section?: undefined;
|
|
49
|
-
}): GlobalSchema<
|
|
49
|
+
}): GlobalSchema<GlobalItem<Values<F>>, N>;
|
|
50
50
|
export declare function globalItem<F extends BuilderMap, St extends BuilderMap, const N extends string = string>(input: GlobalInput<N> & {
|
|
51
51
|
section: PairedComponent<SectionSchema<F, St>, F, St>;
|
|
52
52
|
fields?: undefined;
|
|
53
|
-
}): GlobalSchema<
|
|
54
|
-
settings: NamespaceValues<St>;
|
|
55
|
-
styles: StyleProps;
|
|
56
|
-
}, N>;
|
|
53
|
+
}): GlobalSchema<VisualGlobalItem<Values<F>, Values<St>>, N>;
|
|
57
54
|
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export type { PathIndex } from "./path-index.ts";
|
|
|
51
51
|
export { MemoryEntryStore } from "./entry-store.ts";
|
|
52
52
|
export { SupabaseEntryStore } from "./supabase-entry-store.ts";
|
|
53
53
|
export { SupabasePathIndex } from "./supabase-path-index.ts";
|
|
54
|
-
export type { EntryStore, EntryRecord, EntryLocale, EntryVersion, EntryScan, EntryUsage, ListOptions, CreateEntryOptions, AddEntryLocaleOptions, EntryStoreOptions, Where, WhereValue, } from "./entry-store.ts";
|
|
54
|
+
export type { EntryStore, EntryRecord, EntryLocale, EntryVersion, EntryScan, EntryUsage, ListOptions, CreateEntryOptions, AddEntryLocaleOptions, EntryStoreOptions, Where, WhereValue, SnapshotQuery, SnapshotRow, } from "./entry-store.ts";
|
|
55
55
|
export { entryErrors, compareEntries, DEFAULT_ENTRY_LOCALES, localizedKeys, splitFields, entryLocaleRow, entryFieldsIn, entryRefEdges, entryTitleIn, matchesWhere, } from "./entry-store.ts";
|
|
56
56
|
export { affectedTargets, pageTag, entryTag, globalTag, pagesTreeTag, collectionTag } from "./revalidate.ts";
|
|
57
57
|
export type { AffectedTargets } from "./revalidate.ts";
|
|
@@ -66,7 +66,10 @@ export type { PageStore, PageRecord, PageLocale, PageVersion, PageScan, PageStor
|
|
|
66
66
|
export { pathOptions, pageErrors, localeRow, pageRefEdges, DEFAULT_LOCALES } from "./store.ts";
|
|
67
67
|
export type { SmoodlyConfig, ResolvedConfig } from "./config.ts";
|
|
68
68
|
export type { FieldBuilder, Descriptor, ValueOf, Option, OptionInput, OptionValue } from "./fields.ts";
|
|
69
|
-
export type { SectionSchema, ElementSchema, BuilderMap } from "./schema.ts";
|
|
69
|
+
export type { SectionSchema, ElementSchema, BuilderMap, Values } from "./schema.ts";
|
|
70
|
+
export type { CollectionSeo } from "./seo.ts";
|
|
71
|
+
export type { EntryRef, EntryItem, PageItem, GlobalItem, VisualGlobalItem, Seo } from "./read-shapes.ts";
|
|
72
|
+
export type { RefTo } from "./fields.ts";
|
|
70
73
|
export type { Props, PairedComponent } from "./pairing.tsx";
|
|
71
74
|
export type { CollectionSchema, EntryOrder } from "./collections.ts";
|
|
72
75
|
export { DEFAULT_TOOLSET, type Toolset, type ToolsetMark, type ToolsetNode } from "./toolset.ts";
|
|
@@ -94,23 +97,7 @@ export declare const smoodly: {
|
|
|
94
97
|
omit: string[];
|
|
95
98
|
};
|
|
96
99
|
css?: false;
|
|
97
|
-
sample?: Partial<{ [
|
|
98
|
-
__flags?: infer Fl;
|
|
99
|
-
} ? Fl extends {
|
|
100
|
-
optional: true;
|
|
101
|
-
} ? K : never : never; }[keyof F]>]: import("./fields.ts").ValueOf<F[K_1]>; } & { [K_2 in { [K in keyof F]: F[K] extends {
|
|
102
|
-
__flags?: infer Fl;
|
|
103
|
-
} ? Fl extends {
|
|
104
|
-
optional: true;
|
|
105
|
-
} ? K : never : never; }[keyof F]]?: import("./fields.ts").ValueOf<F[K_2]> | undefined; } & { [K_4 in Exclude<keyof S, { [K_3 in keyof S]: S[K_3] extends {
|
|
106
|
-
__flags?: infer Fl;
|
|
107
|
-
} ? Fl extends {
|
|
108
|
-
optional: true;
|
|
109
|
-
} ? K_3 : never : never; }[keyof S]>]: import("./fields.ts").ValueOf<S[K_4]>; } & { [K_5 in { [K_3 in keyof S]: S[K_3] extends {
|
|
110
|
-
__flags?: infer Fl;
|
|
111
|
-
} ? Fl extends {
|
|
112
|
-
optional: true;
|
|
113
|
-
} ? K_3 : never : never; }[keyof S]]?: import("./fields.ts").ValueOf<S[K_5]> | undefined; }> | undefined;
|
|
100
|
+
sample?: Partial<{ [K in keyof import("./schema.ts").NamespaceValues<F>]: import("./schema.ts").NamespaceValues<F>[K] extends import("./fields.ts").RefTo<any, any> ? string : import("./schema.ts").NamespaceValues<F>[K] extends import("./fields.ts").RefTo<any, any>[] ? string[] : import("./schema.ts").NamespaceValues<F>[K]; } & { [K_1 in keyof import("./schema.ts").NamespaceValues<S>]: import("./schema.ts").NamespaceValues<S>[K_1] extends import("./fields.ts").RefTo<any, any> ? string : import("./schema.ts").NamespaceValues<S>[K_1] extends import("./fields.ts").RefTo<any, any>[] ? string[] : import("./schema.ts").NamespaceValues<S>[K_1]; }> | undefined;
|
|
114
101
|
}): import("./schema.ts").SectionSchema<F, S>;
|
|
115
102
|
element<F extends import("./schema.ts").BuilderMap = {}, S extends import("./schema.ts").BuilderMap = {}>(input: {
|
|
116
103
|
name: string;
|
|
@@ -124,23 +111,7 @@ export declare const smoodly: {
|
|
|
124
111
|
omit: string[];
|
|
125
112
|
};
|
|
126
113
|
css?: false;
|
|
127
|
-
sample?: Partial<{ [
|
|
128
|
-
__flags?: infer Fl;
|
|
129
|
-
} ? Fl extends {
|
|
130
|
-
optional: true;
|
|
131
|
-
} ? K : never : never; }[keyof F]>]: import("./fields.ts").ValueOf<F[K_1]>; } & { [K_2 in { [K in keyof F]: F[K] extends {
|
|
132
|
-
__flags?: infer Fl;
|
|
133
|
-
} ? Fl extends {
|
|
134
|
-
optional: true;
|
|
135
|
-
} ? K : never : never; }[keyof F]]?: import("./fields.ts").ValueOf<F[K_2]> | undefined; } & { [K_4 in Exclude<keyof S, { [K_3 in keyof S]: S[K_3] extends {
|
|
136
|
-
__flags?: infer Fl;
|
|
137
|
-
} ? Fl extends {
|
|
138
|
-
optional: true;
|
|
139
|
-
} ? K_3 : never : never; }[keyof S]>]: import("./fields.ts").ValueOf<S[K_4]>; } & { [K_5 in { [K_3 in keyof S]: S[K_3] extends {
|
|
140
|
-
__flags?: infer Fl;
|
|
141
|
-
} ? Fl extends {
|
|
142
|
-
optional: true;
|
|
143
|
-
} ? K_3 : never : never; }[keyof S]]?: import("./fields.ts").ValueOf<S[K_5]> | undefined; }> | undefined;
|
|
114
|
+
sample?: Partial<{ [K in keyof import("./schema.ts").NamespaceValues<F>]: import("./schema.ts").NamespaceValues<F>[K] extends import("./fields.ts").RefTo<any, any> ? string : import("./schema.ts").NamespaceValues<F>[K] extends import("./fields.ts").RefTo<any, any>[] ? string[] : import("./schema.ts").NamespaceValues<F>[K]; } & { [K_1 in keyof import("./schema.ts").NamespaceValues<S>]: import("./schema.ts").NamespaceValues<S>[K_1] extends import("./fields.ts").RefTo<any, any> ? string : import("./schema.ts").NamespaceValues<S>[K_1] extends import("./fields.ts").RefTo<any, any>[] ? string[] : import("./schema.ts").NamespaceValues<S>[K_1]; }> | undefined;
|
|
144
115
|
} & {
|
|
145
116
|
contexts: ("richtext" | "column")[];
|
|
146
117
|
}): import("./schema.ts").ElementSchema<F, S>;
|
|
@@ -157,8 +128,8 @@ export declare const smoodly: {
|
|
|
157
128
|
};
|
|
158
129
|
export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, BLANK_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.ts";
|
|
159
130
|
export type { MarkNode, MarkPath, Marks, Scope } from "./marks.ts";
|
|
160
|
-
export { resolveSmoodlyEnv, smoodlyEnv,
|
|
161
|
-
export type { SmoodlyEnv,
|
|
131
|
+
export { resolveSmoodlyEnv, smoodlyEnv, presenceConfig } from "./env.ts";
|
|
132
|
+
export type { SmoodlyEnv, PresenceConfig } from "./env.ts";
|
|
162
133
|
export { clientOptions, createSmoodlyClient } from "./client.ts";
|
|
163
134
|
export { createSmoodlySite, pageContextOf } from "./site.ts";
|
|
164
|
-
export type { SiteOptions,
|
|
135
|
+
export type { SiteOptions, SitePageSchema, SmoodlySite, LoadOptions, PageTreeItem, EntryTreeItem, EntryQuery, } from "./site.ts";
|
package/dist/index.js
CHANGED
|
@@ -66,6 +66,6 @@ export const smoodly = {
|
|
|
66
66
|
marks,
|
|
67
67
|
};
|
|
68
68
|
export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, BLANK_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.js";
|
|
69
|
-
export { resolveSmoodlyEnv, smoodlyEnv,
|
|
69
|
+
export { resolveSmoodlyEnv, smoodlyEnv, presenceConfig } from "./env.js";
|
|
70
70
|
export { clientOptions, createSmoodlyClient } from "./client.js";
|
|
71
71
|
export { createSmoodlySite, pageContextOf } from "./site.js";
|
package/dist/migrations.js
CHANGED
|
@@ -43,7 +43,7 @@ export function viewsFile(collections, existing, at) {
|
|
|
43
43
|
const changed = collections.filter((c) => existing.get(c.name) !== collectionHash(c.sql));
|
|
44
44
|
if (changed.length === 0)
|
|
45
45
|
return null;
|
|
46
|
-
const text = "-- Smoodly: read views
|
|
46
|
+
const text = "-- Smoodly: read views for this app's collections, written by `npx smoodly migrate`\n" +
|
|
47
47
|
`-- (${at.toISOString()}). One block per collection; the marker's hash is the block's SQL, compared on the next run.\n` +
|
|
48
48
|
"-- Never edit a written file: change the collection and run migrate again.\n\n" +
|
|
49
49
|
changed.map((c) => `${collectionMarker(c)}\n${c.sql.trim()}`).join("\n\n") +
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createPageRenderer } from "./page-renderer.tsx";
|
|
2
2
|
export type { PageRendererOptions, PathRenderOptions, PageRegistration } from "./page-renderer.tsx";
|
|
3
|
-
export { metadataFrom } from "./meta.ts";
|
|
3
|
+
export { metadataFrom, metadataFromSeo } from "./meta.ts";
|
|
4
4
|
export { createSmoodlyQueries } from "./queries.ts";
|
|
5
5
|
export type { SmoodlyQueries, QueryOptions, EntriesQuery, TypedEntryItem, GlobalValue } from "./queries.ts";
|
|
6
6
|
export { previewRoute } from "./preview-route.ts";
|
package/dist/next/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createPageRenderer } from "./page-renderer.js";
|
|
2
|
-
export { metadataFrom } from "./meta.js";
|
|
2
|
+
export { metadataFrom, metadataFromSeo } from "./meta.js";
|
|
3
3
|
export { createSmoodlyQueries } from "./queries.js";
|
|
4
4
|
export { previewRoute } from "./preview-route.js";
|
|
5
5
|
export { previewPill } from "./preview-pill.js";
|
package/dist/next/meta.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* layout
|
|
2
|
+
import type { Seo } from "../read-shapes.ts";
|
|
3
|
+
/** Maps the effective SEO values onto Next metadata (spec 2026-09-10 page
|
|
4
|
+
* fields §4, 2026-09-29 content API contract §7). Absent values leave the
|
|
5
|
+
* app's layout defaults untouched: no seo at all returns {}. */
|
|
6
|
+
export declare function metadataFromSeo(seo: Seo | undefined): Metadata;
|
|
7
|
+
/** A page tree's stored meta as Next metadata; the SEO title falls back to the page's own title. */
|
|
6
8
|
export declare function metadataFrom(meta: unknown, page?: {
|
|
7
9
|
title?: string;
|
|
8
10
|
}): Metadata;
|
package/dist/next/meta.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import { imageUrl } from "../image/loaders.js";
|
|
2
|
-
|
|
3
|
-
/** Maps the
|
|
4
|
-
* fields §4
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const m = typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta : {};
|
|
9
|
-
const str = (key) => (typeof m[key] === "string" && m[key] ? m[key] : undefined);
|
|
10
|
-
const title = str("title") ?? (page?.title ? page.title : undefined);
|
|
11
|
-
const description = str("description");
|
|
12
|
-
const ogImage = m.ogImage;
|
|
2
|
+
import { pageSeo } from "../seo.js";
|
|
3
|
+
/** Maps the effective SEO values onto Next metadata (spec 2026-09-10 page
|
|
4
|
+
* fields §4, 2026-09-29 content API contract §7). Absent values leave the
|
|
5
|
+
* app's layout defaults untouched: no seo at all returns {}. */
|
|
6
|
+
export function metadataFromSeo(seo) {
|
|
7
|
+
const { title, description, image, noindex } = seo ?? {};
|
|
13
8
|
const out = {};
|
|
14
9
|
if (title)
|
|
15
10
|
out.title = title;
|
|
@@ -20,16 +15,18 @@ export function metadataFrom(meta, page) {
|
|
|
20
15
|
og.title = title;
|
|
21
16
|
if (description)
|
|
22
17
|
og.description = description;
|
|
23
|
-
if (
|
|
18
|
+
if (image) {
|
|
24
19
|
// The delivery seam decides the URL; the cloud's transform loader (OQ 28) can serve a real 1200-wide render.
|
|
25
|
-
og.images = [{ url: imageUrl(
|
|
26
|
-
}
|
|
27
|
-
else if (typeof ogImage === "string" && ogImage) {
|
|
28
|
-
og.images = [ogImage];
|
|
20
|
+
og.images = [{ url: imageUrl(image, { width: 1200 }), width: image.width, height: image.height, ...(image.alt ? { alt: image.alt } : {}) }];
|
|
29
21
|
}
|
|
30
22
|
if (Object.keys(og).length > 0)
|
|
31
23
|
out.openGraph = og;
|
|
32
|
-
if (
|
|
24
|
+
if (noindex === true)
|
|
33
25
|
out.robots = { index: false, follow: false };
|
|
34
26
|
return out;
|
|
35
27
|
}
|
|
28
|
+
/** A page tree's stored meta as Next metadata; the SEO title falls back to the page's own title. */
|
|
29
|
+
export function metadataFrom(meta, page) {
|
|
30
|
+
const m = typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta : {};
|
|
31
|
+
return metadataFromSeo(pageSeo(m, page?.title ?? ""));
|
|
32
|
+
}
|
|
@@ -16,8 +16,9 @@ import { fixedPathFor } from "../localize.js";
|
|
|
16
16
|
import { decodeSegment, joinPath } from "../paths.js";
|
|
17
17
|
import { renderPage } from "../render.js";
|
|
18
18
|
import { entryTag, pageTag, globalTag } from "../revalidate.js";
|
|
19
|
+
import { toEntryItem } from "../read-shapes.js";
|
|
19
20
|
import { pageContextOf } from "../site.js";
|
|
20
|
-
import {
|
|
21
|
+
import { metadataFromSeo } from "./meta.js";
|
|
21
22
|
import { previewPill } from "./preview-pill.js";
|
|
22
23
|
/** Logged once per renderer when Next's cacheComponents is on (spec 2026-09-27 cache components §2). */
|
|
23
24
|
export const CACHE_COMPONENTS_WARNING = "smoodly: Next's cacheComponents is on, and Smoodly does not support it — a missing page answers 200 with noindex instead of 404, and a redirect answers 200 instead of 308. Turn cacheComponents off in next.config.ts; the Next.js guide's \"Cache Components\" section says why (https://smoodly.io/docs/frameworks/nextjs).";
|
|
@@ -73,7 +74,7 @@ export function createPageRenderer(options) {
|
|
|
73
74
|
console.warn(`smoodly: no entry page for collection "${hit.collection}" — the entry at ${hit.path} is a 404.`);
|
|
74
75
|
notFound();
|
|
75
76
|
}
|
|
76
|
-
return _jsx(View, { entry: hit.entry,
|
|
77
|
+
return _jsx(View, { entry: toEntryItem(hit.entry, hit.entry.fields, hit) });
|
|
77
78
|
}
|
|
78
79
|
/** hreflang from the SAME map a language switch reads (spec 2026-09-15 §3): the locales with a URL, through href. */
|
|
79
80
|
function alternates(hit) {
|
|
@@ -133,7 +134,9 @@ export function createPageRenderer(options) {
|
|
|
133
134
|
const hit = await load(target, locale, path, draft);
|
|
134
135
|
if (!hit || !accept(hit))
|
|
135
136
|
return {};
|
|
136
|
-
|
|
137
|
+
// A page type without seo: true still gets its page title as <title>; seo: true adds the panel's values on top.
|
|
138
|
+
const seo = hit.seo ?? (hit.kind === "page" ? { title: hit.title } : undefined);
|
|
139
|
+
return { ...metadataFromSeo(seo), ...alternates(hit) };
|
|
137
140
|
}
|
|
138
141
|
const any = () => true;
|
|
139
142
|
/** The escape hatch may only render ITS OWN registration's record. */
|
package/dist/next/queries.d.ts
CHANGED
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
import type { CollectionSchema } from "../collections.ts";
|
|
2
|
-
import type { EntryRecord } from "../entry-store.ts";
|
|
3
2
|
import type { GlobalSchema } from "../global.ts";
|
|
4
|
-
import
|
|
3
|
+
import { type EntryItem, type PageItem } from "../read-shapes.ts";
|
|
4
|
+
import type { Materialize } from "../schema.ts";
|
|
5
|
+
import type { EntryQuery, PageTreeItem, SmoodlySite } from "../site.ts";
|
|
5
6
|
export type QueryOptions = {
|
|
6
7
|
locale?: string;
|
|
7
8
|
};
|
|
8
|
-
export type EntriesQuery = Omit<EntryQuery, "locale" | "draft"> & QueryOptions;
|
|
9
|
+
export type EntriesQuery = Omit<EntryQuery, "locale" | "draft" | "ids"> & QueryOptions;
|
|
9
10
|
type ByName<T, N> = Extract<T, {
|
|
10
11
|
name: N;
|
|
11
12
|
}>;
|
|
12
|
-
/** Fields typed from the schema; a widened `CollectionSchema[]` array (names are `string`) falls back to the untyped record instead of `never`. */
|
|
13
|
-
type EntryFields<C extends readonly CollectionSchema<any, string>[], N> = string extends C[number]["name"] ? Record<string, unknown> :
|
|
14
|
-
|
|
15
|
-
export type TypedEntryItem<C extends readonly CollectionSchema<any, string>[], N> = Omit<SiteEntryItem, "entry"> & {
|
|
16
|
-
entry: Omit<EntryRecord, "fields"> & {
|
|
17
|
-
fields: EntryFields<C, N>;
|
|
18
|
-
};
|
|
13
|
+
/** Fields typed from the schema, references materialized as `EntryRef`s; a widened `CollectionSchema[]` array (names are `string`) falls back to the untyped record instead of `never`. */
|
|
14
|
+
type EntryFields<C extends readonly CollectionSchema<any, string>[], N> = string extends C[number]["name"] ? Record<string, unknown> : {
|
|
15
|
+
[K in keyof NonNullable<ByName<C[number], N>["__entry"]>]: Materialize<NonNullable<ByName<C[number], N>["__entry"]>[K]>;
|
|
19
16
|
};
|
|
17
|
+
/** A read item whose `fields` are the collection's typed values. */
|
|
18
|
+
export type TypedEntryItem<C extends readonly CollectionSchema<any, string>[], N> = EntryItem<EntryFields<C, N>>;
|
|
20
19
|
export type GlobalValue<S extends readonly GlobalSchema<any, string>[], N> = string extends S[number]["name"] ? Record<string, unknown> : NonNullable<ByName<S[number], N>["__value"]>;
|
|
21
20
|
export type SmoodlyQueries<C extends readonly CollectionSchema<any, string>[], S extends readonly GlobalSchema<any, string>[]> = {
|
|
22
21
|
/** The published page tree in a locale, every item with its page fields. */
|
|
23
22
|
getPageTree(options?: QueryOptions): Promise<PageTreeItem[]>;
|
|
24
23
|
/** A page by locale-relative path ("/company/about-us") or id. */
|
|
25
|
-
getPage(pathOrId: string, options?: QueryOptions): Promise<
|
|
24
|
+
getPage(pathOrId: string, options?: QueryOptions): Promise<PageItem | null>;
|
|
26
25
|
/** Published entries of a collection: `where` (shared fields), `order`, `limit`, `parent` run in the store. */
|
|
27
26
|
getEntries<N extends C[number]["name"]>(collection: N, options?: EntriesQuery): Promise<TypedEntryItem<C, N>[]>;
|
|
28
27
|
getEntry<N extends C[number]["name"]>(collection: N, idOrPath: string, options?: QueryOptions): Promise<TypedEntryItem<C, N> | null>;
|
package/dist/next/queries.js
CHANGED
|
@@ -7,6 +7,7 @@ import { unstable_cache } from "next/cache";
|
|
|
7
7
|
import { draftMode } from "next/headers";
|
|
8
8
|
import { connection } from "next/server";
|
|
9
9
|
import { collectionTag, entryTag, pageTag, pagesTreeTag, globalTag } from "../revalidate.js";
|
|
10
|
+
import { referencedCollections } from "../read-shapes.js";
|
|
10
11
|
/** JSON with object keys sorted at every level and `undefined` dropped, so two spellings of one query share a unit. */
|
|
11
12
|
function stable(value) {
|
|
12
13
|
if (Array.isArray(value))
|
|
@@ -26,6 +27,7 @@ function entriesKey(o) {
|
|
|
26
27
|
}
|
|
27
28
|
export function createSmoodlyQueries(options) {
|
|
28
29
|
const { site } = options;
|
|
30
|
+
const collectionByName = new Map(options.collections.map((c) => [c.name, c]));
|
|
29
31
|
const globalByName = new Map((options.globals ?? []).map((s) => [s.name, s]));
|
|
30
32
|
const localeOf = (o) => o?.locale ?? site.locales.default;
|
|
31
33
|
/** Every query gates on the request (never a build prerender), then reads live in draft mode or cached under `tags`. */
|
|
@@ -59,7 +61,10 @@ export function createSmoodlyQueries(options) {
|
|
|
59
61
|
getEntries: (collection, o = {}) => {
|
|
60
62
|
const locale = localeOf(o);
|
|
61
63
|
const { locale: _locale, ...query } = o;
|
|
62
|
-
|
|
64
|
+
// The unit also depends on every collection its references resolve into (spec 2026-09-29 content API contract §6).
|
|
65
|
+
const schema = collectionByName.get(collection);
|
|
66
|
+
const tags = [collection, ...(schema ? referencedCollections(schema, [...options.collections]) : [])];
|
|
67
|
+
return run(["entries", collection, locale, entriesKey(o)], [...new Set(tags)].map(collectionTag), (draft) => site.getEntries(collection, { ...query, locale, draft }));
|
|
63
68
|
},
|
|
64
69
|
async getEntry(collection, idOrPath, o) {
|
|
65
70
|
const locale = localeOf(o);
|
|
@@ -70,7 +75,7 @@ export function createSmoodlyQueries(options) {
|
|
|
70
75
|
const hit = await site.getEntry(collection, idOrPath, { locale, draft });
|
|
71
76
|
// The site layer also answers a bare root slug, but the unit would sit under
|
|
72
77
|
// a tag no op ever fires — so a slug is refused here rather than cached stale.
|
|
73
|
-
if (hit && !idOrPath.startsWith("/") && hit.
|
|
78
|
+
if (hit && !idOrPath.startsWith("/") && hit.id !== idOrPath) {
|
|
74
79
|
throw new Error(`smoodly: getEntry("${collection}", "${idOrPath}") takes an id or a path — for a slug, pass the entry's path.`);
|
|
75
80
|
}
|
|
76
81
|
return hit;
|
package/dist/page.d.ts
CHANGED
|
@@ -13,7 +13,8 @@ export type PageSchema<Zones extends Record<string, unknown> = Record<string, un
|
|
|
13
13
|
* segment; the default locale's entry is required (see `fixedSlug`). */
|
|
14
14
|
slug?: string | Record<string, string>;
|
|
15
15
|
zones: Record<keyof Zones & string, ZonePolicy>;
|
|
16
|
-
|
|
16
|
+
/** The SEO & Social tab exists for this template; reads return `seo` and the route's metadata uses it (spec 2026-09-29 content API contract §7). */
|
|
17
|
+
seo?: true;
|
|
17
18
|
/** The template's own page-level fields (spec 2026-09-10 page fields §1):
|
|
18
19
|
* stored in the tree per locale, typed on the view's `page.fields`. */
|
|
19
20
|
fields: Record<string, Descriptor>;
|
|
@@ -27,7 +28,9 @@ export declare function pageSchema<Zones extends Record<string, object>, F exten
|
|
|
27
28
|
title: string;
|
|
28
29
|
slug?: string | Record<string, string>;
|
|
29
30
|
zones: Zones;
|
|
30
|
-
|
|
31
|
+
seo?: true;
|
|
32
|
+
/** Gone: `meta: { seo: true }` is `seo: true`. */
|
|
33
|
+
meta?: never;
|
|
31
34
|
fields?: F;
|
|
32
35
|
}): PageSchema<Zones, F>;
|
|
33
36
|
/** The default-locale segment of a fixed slug — what the admin lists and
|
package/dist/page.js
CHANGED
|
@@ -44,6 +44,8 @@ function normalizePolicy(p) {
|
|
|
44
44
|
export function pageSchema(input) {
|
|
45
45
|
if (!input?.name)
|
|
46
46
|
throw new Error("smoodly: a page requires a name.");
|
|
47
|
+
if (input.meta !== undefined)
|
|
48
|
+
throw new Error(`smoodly: page "${input.name}": meta: { seo: true } is now seo: true.`);
|
|
47
49
|
// A fixed slug is decided by `slug !== undefined` at every other hop, but
|
|
48
50
|
// the renderer's path lookup is truthiness-based: an empty string would read
|
|
49
51
|
// as fixed to the admin and blow up in the route. Reject it at construction.
|
|
@@ -62,7 +64,7 @@ export function pageSchema(input) {
|
|
|
62
64
|
title: input.title,
|
|
63
65
|
...(input.slug !== undefined ? { slug: input.slug } : {}),
|
|
64
66
|
zones: Object.fromEntries(Object.entries(input.zones).map(([k, p]) => [k, normalizePolicy(p)])),
|
|
65
|
-
|
|
67
|
+
...(input.seo === true ? { seo: true } : {}),
|
|
66
68
|
fields,
|
|
67
69
|
};
|
|
68
70
|
}
|
package/dist/pairing.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComponentType, ReactElement } from "react";
|
|
2
|
-
import type { BuilderMap, ElementSchema,
|
|
2
|
+
import type { BuilderMap, ElementSchema, SectionSchema, Values } from "./schema.ts";
|
|
3
3
|
import type { PageContext, ZoneContent } from "./render.tsx";
|
|
4
4
|
import type { StyleProps } from "./styles.ts";
|
|
5
5
|
/** What the view receives. A page view gets its zones and where it sits
|
|
@@ -14,28 +14,28 @@ export type Props<S> = S extends {
|
|
|
14
14
|
__fields?: infer F extends BuilderMap;
|
|
15
15
|
} ? {
|
|
16
16
|
zones: Record<keyof Z & string, ZoneContent>;
|
|
17
|
-
page: PageContext<
|
|
17
|
+
page: PageContext<Values<F>>;
|
|
18
18
|
} : S extends {
|
|
19
19
|
__f?: infer F extends BuilderMap;
|
|
20
20
|
__s?: infer St extends BuilderMap;
|
|
21
|
-
} ?
|
|
22
|
-
settings:
|
|
21
|
+
} ? Values<F> & {
|
|
22
|
+
settings: Values<St>;
|
|
23
23
|
styles: StyleProps;
|
|
24
24
|
} : never;
|
|
25
25
|
/** What hard-coded JSX may pass: fields flat, settings optional and partial, styles optional. */
|
|
26
|
-
type ExternalProps<F extends BuilderMap, St extends BuilderMap> =
|
|
27
|
-
settings?: Partial<
|
|
26
|
+
type ExternalProps<F extends BuilderMap, St extends BuilderMap> = Values<F> & {
|
|
27
|
+
settings?: Partial<Values<St>>;
|
|
28
28
|
styles?: StyleProps;
|
|
29
29
|
};
|
|
30
30
|
export type PairedComponent<Schema, F extends BuilderMap, St extends BuilderMap> = ((props: ExternalProps<F, St>) => ReactElement) & {
|
|
31
31
|
schema: Schema;
|
|
32
32
|
};
|
|
33
|
-
export declare function section<F extends BuilderMap, St extends BuilderMap>(schema: SectionSchema<F, St>, View: ComponentType<
|
|
34
|
-
settings:
|
|
33
|
+
export declare function section<F extends BuilderMap, St extends BuilderMap>(schema: SectionSchema<F, St>, View: ComponentType<Values<F> & {
|
|
34
|
+
settings: Values<St>;
|
|
35
35
|
styles: StyleProps;
|
|
36
36
|
}>): PairedComponent<SectionSchema<F, St>, F, St>;
|
|
37
|
-
export declare function element<F extends BuilderMap, St extends BuilderMap>(schema: ElementSchema<F, St>, View: ComponentType<
|
|
38
|
-
settings:
|
|
37
|
+
export declare function element<F extends BuilderMap, St extends BuilderMap>(schema: ElementSchema<F, St>, View: ComponentType<Values<F> & {
|
|
38
|
+
settings: Values<St>;
|
|
39
39
|
styles: StyleProps;
|
|
40
40
|
}>): PairedComponent<ElementSchema<F, St>, F, St>;
|
|
41
41
|
export {};
|
package/dist/path-index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface PathIndex {
|
|
|
16
16
|
resolve(locale: string, path: string): Promise<PathHit | null>;
|
|
17
17
|
/** locale → the target's LIVE path. */
|
|
18
18
|
pathsOf(target: PathTarget): Promise<Record<string, string>>;
|
|
19
|
+
/** id → the LIVE full path in one locale, for many targets at once (the resolver's EntryRef paths). */
|
|
20
|
+
livePathsIn(kind: "page" | "entry", ids: string[], locale: string): Promise<Record<string, string>>;
|
|
19
21
|
/** The target's redirect rows. */
|
|
20
22
|
redirectsOf(target: PathTarget): Promise<PathRow[]>;
|
|
21
23
|
listLive(locale: string): Promise<PathRow[]>;
|
|
@@ -40,6 +42,7 @@ export declare class MemoryPathIndex implements PathIndex {
|
|
|
40
42
|
private hit;
|
|
41
43
|
resolve(locale: string, path: string): Promise<PathHit | null>;
|
|
42
44
|
pathsOf(target: PathTarget): Promise<Record<string, string>>;
|
|
45
|
+
livePathsIn(kind: "page" | "entry", ids: string[], locale: string): Promise<Record<string, string>>;
|
|
43
46
|
redirectsOf(target: PathTarget): Promise<PathRow[]>;
|
|
44
47
|
/** Every live URL in the locale; an editor's row under a code address is left out, since visitors never reach it (spec §4). */
|
|
45
48
|
listLive(locale: string): Promise<PathRow[]>;
|
package/dist/path-index.js
CHANGED
|
@@ -61,6 +61,13 @@ export class MemoryPathIndex {
|
|
|
61
61
|
out[r.locale] = r.path;
|
|
62
62
|
return out;
|
|
63
63
|
}
|
|
64
|
+
async livePathsIn(kind, ids, locale) {
|
|
65
|
+
const wanted = new Set(ids);
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const r of this.shownAll(this.rows.filter((r) => !r.redirect && r.kind === kind && r.locale === locale && wanted.has(r.id))))
|
|
68
|
+
out[r.id] = r.path;
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
64
71
|
async redirectsOf(target) {
|
|
65
72
|
return this.shownAll(this.rows.filter((r) => r.redirect && sameTarget(r, target)));
|
|
66
73
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { CollectionSchema } from "./collections.ts";
|
|
2
|
+
import type { EntryRecord } from "./entry-store.ts";
|
|
3
|
+
import type { LocaleAlternate, PageAlternate } from "./paths.ts";
|
|
4
|
+
import type { PageLink } from "./render.tsx";
|
|
5
|
+
import type { ResolvedImage } from "./media.ts";
|
|
6
|
+
import type { SitePage } from "./site.ts";
|
|
7
|
+
import type { StyleProps } from "./styles.ts";
|
|
8
|
+
/** A resolved reference: the target in the read's locale (§2). */
|
|
9
|
+
export type EntryRef<F = Record<string, unknown>> = {
|
|
10
|
+
id: string;
|
|
11
|
+
path: string | null;
|
|
12
|
+
fields: F;
|
|
13
|
+
};
|
|
14
|
+
/** The effective SEO values: an editor's override where set, the fallback otherwise (§3, §7). */
|
|
15
|
+
export type Seo = {
|
|
16
|
+
title?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
image?: ResolvedImage;
|
|
19
|
+
noindex?: boolean;
|
|
20
|
+
};
|
|
21
|
+
export type EntryItem<F = Record<string, unknown>> = {
|
|
22
|
+
id: string;
|
|
23
|
+
path: string | null;
|
|
24
|
+
locale: string;
|
|
25
|
+
locales: Record<string, LocaleAlternate | null>;
|
|
26
|
+
updatedAt: number;
|
|
27
|
+
fields: F;
|
|
28
|
+
seo?: Seo;
|
|
29
|
+
};
|
|
30
|
+
export type PageItem<F = Record<string, unknown>> = {
|
|
31
|
+
id: string;
|
|
32
|
+
title: string;
|
|
33
|
+
path: string;
|
|
34
|
+
locale: string;
|
|
35
|
+
locales: Record<string, PageAlternate | null>;
|
|
36
|
+
template: string;
|
|
37
|
+
fields: F;
|
|
38
|
+
seo?: Seo;
|
|
39
|
+
ancestors: PageLink[];
|
|
40
|
+
children: PageLink[];
|
|
41
|
+
};
|
|
42
|
+
export type GlobalItem<F = Record<string, unknown>, S = Record<string, unknown>> = {
|
|
43
|
+
locale: string;
|
|
44
|
+
fields: F;
|
|
45
|
+
settings?: S;
|
|
46
|
+
styles?: StyleProps;
|
|
47
|
+
};
|
|
48
|
+
/** A visual item's read: its settings and styles always present. */
|
|
49
|
+
export type VisualGlobalItem<F = Record<string, unknown>, S = Record<string, unknown>> = GlobalItem<F, S> & {
|
|
50
|
+
settings: S;
|
|
51
|
+
styles: StyleProps;
|
|
52
|
+
};
|
|
53
|
+
/** One entry as a read returns it: Smoodly's keys, the content under `fields`; the record never leaves the site layer. */
|
|
54
|
+
export declare function toEntryItem(record: EntryRecord, fields: Record<string, unknown>, o: {
|
|
55
|
+
path: string | null;
|
|
56
|
+
locale: string;
|
|
57
|
+
locales: Record<string, LocaleAlternate | null>;
|
|
58
|
+
seo?: Seo;
|
|
59
|
+
}): EntryItem;
|
|
60
|
+
/** One page as a read returns it: no record, no tree. */
|
|
61
|
+
export declare function toPageItem(hit: SitePage): PageItem;
|
|
62
|
+
/** Every collection a read of `schema` resolves into, by each ref field's own depth (the mirror of
|
|
63
|
+
* runResolution's modes) — a list unit's extra tags (spec 2026-09-29 content API contract §6). */
|
|
64
|
+
export declare function referencedCollections(schema: CollectionSchema<any>, all: CollectionSchema<any>[]): string[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { SEO_KEY } from "./seo.js";
|
|
2
|
+
/** One entry as a read returns it: Smoodly's keys, the content under `fields`; the record never leaves the site layer. */
|
|
3
|
+
export function toEntryItem(record, fields, o) {
|
|
4
|
+
// The reserved `$seo` values are Smoodly's: they leave as `seo`, never as a field.
|
|
5
|
+
const { [SEO_KEY]: _seo, ...rest } = fields;
|
|
6
|
+
return {
|
|
7
|
+
id: record.id, path: o.path, locale: o.locale, locales: o.locales,
|
|
8
|
+
updatedAt: record.locales[o.locale]?.updatedAt ?? record.updatedAt, fields: rest,
|
|
9
|
+
...(o.seo ? { seo: o.seo } : {}),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** One page as a read returns it: no record, no tree. */
|
|
13
|
+
export function toPageItem(hit) {
|
|
14
|
+
return {
|
|
15
|
+
id: hit.record.id, title: hit.title, path: hit.path, locale: hit.locale, locales: hit.locales,
|
|
16
|
+
template: hit.template, fields: hit.fields, ...(hit.seo ? { seo: hit.seo } : {}),
|
|
17
|
+
ancestors: hit.ancestors, children: hit.children,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Every collection a read of `schema` resolves into, by each ref field's own depth (the mirror of
|
|
21
|
+
* runResolution's modes) — a list unit's extra tags (spec 2026-09-29 content API contract §6). */
|
|
22
|
+
export function referencedCollections(schema, all) {
|
|
23
|
+
const byName = new Map(all.map((c) => [c.name, c]));
|
|
24
|
+
const out = new Set();
|
|
25
|
+
const walk = (s, mode, seen) => {
|
|
26
|
+
for (const [key, raw] of Object.entries(s.fields)) {
|
|
27
|
+
const d = raw;
|
|
28
|
+
if (d.type !== "ref" && d.type !== "refList")
|
|
29
|
+
continue;
|
|
30
|
+
let next;
|
|
31
|
+
if (mode === "own")
|
|
32
|
+
next = d.resolve === "full" ? "full" : d.resolve !== null && typeof d.resolve === "object" ? d.resolve : "shallow";
|
|
33
|
+
else if (mode === "full")
|
|
34
|
+
next = "full";
|
|
35
|
+
else if (mode === "shallow")
|
|
36
|
+
continue;
|
|
37
|
+
else {
|
|
38
|
+
const on = mode[key];
|
|
39
|
+
if (!on)
|
|
40
|
+
continue;
|
|
41
|
+
next = typeof on === "object" ? on : "shallow";
|
|
42
|
+
}
|
|
43
|
+
const name = d.target().name;
|
|
44
|
+
out.add(name);
|
|
45
|
+
const target = byName.get(name);
|
|
46
|
+
if (next !== "shallow" && target && !seen.has(name))
|
|
47
|
+
walk(target, next, new Set([...seen, name]));
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
walk(schema, "own", new Set([schema.name]));
|
|
51
|
+
return [...out];
|
|
52
|
+
}
|