fimo 0.11.0 → 0.12.0-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +7 -1
  2. package/assets/content-templates/hooks-template.eta +8 -8
  3. package/assets/content-templates/next-client-template.eta +102 -0
  4. package/assets/content-templates/next-server-template.eta +75 -0
  5. package/assets/content-templates/types-template.eta +2 -2
  6. package/assets/skills/fimo-cli/SKILL.md +2 -0
  7. package/assets/skills/fimo-cli/references/deploy.md +22 -1
  8. package/assets/skills/fimo-cli/references/preview.md +4 -3
  9. package/assets/skills/fimo-code/references/setup-plain-vite.md +4 -2
  10. package/assets/skills/fimo-code/references/setup-react-router.md +4 -2
  11. package/dist/cli/bundle.json +1 -1
  12. package/dist/cli/index.js +49 -47
  13. package/dist/runtime/content/client/collection-core.d.ts +37 -0
  14. package/dist/runtime/content/client/collection-core.js +149 -0
  15. package/dist/runtime/content/client/createContentClient.d.ts +3 -16
  16. package/dist/runtime/content/client/createContentClient.js +4 -321
  17. package/dist/runtime/content/client/createSingletonClient.js +3 -108
  18. package/dist/runtime/content/client/internal.d.ts +1 -3
  19. package/dist/runtime/content/client/internal.js +3 -21
  20. package/dist/runtime/content/client/react-content-client.d.ts +10 -0
  21. package/dist/runtime/content/client/react-content-client.js +165 -0
  22. package/dist/runtime/content/client/react-singleton-client.d.ts +7 -0
  23. package/dist/runtime/content/client/react-singleton-client.js +38 -0
  24. package/dist/runtime/content/client/runtime.d.ts +8 -0
  25. package/dist/runtime/content/client/runtime.js +7 -0
  26. package/dist/runtime/content/client/server-content-client.d.ts +9 -0
  27. package/dist/runtime/content/client/server-content-client.js +35 -0
  28. package/dist/runtime/content/client/singleton-core.d.ts +26 -0
  29. package/dist/runtime/content/client/singleton-core.js +73 -0
  30. package/dist/runtime/content/client/useQueryLocale.d.ts +1 -1
  31. package/dist/runtime/content/client/useQueryLocale.js +2 -3
  32. package/dist/runtime/content/client/vite-runtime.d.ts +3 -0
  33. package/dist/runtime/content/client/vite-runtime.js +23 -0
  34. package/dist/runtime/content/index.d.ts +16 -0
  35. package/dist/runtime/content/index.js +10 -0
  36. package/dist/runtime/content/serialization.d.ts +22 -0
  37. package/dist/runtime/content/serialization.js +42 -0
  38. package/dist/runtime/next/Text.d.ts +10 -0
  39. package/dist/runtime/next/Text.js +18 -0
  40. package/dist/runtime/next/client-runtime.d.ts +3 -0
  41. package/dist/runtime/next/client-runtime.js +20 -0
  42. package/dist/runtime/next/client.d.ts +16 -0
  43. package/dist/runtime/next/client.js +15 -0
  44. package/dist/runtime/next/provider.d.ts +10 -0
  45. package/dist/runtime/next/provider.js +12 -0
  46. package/dist/runtime/next/server-runtime.d.ts +3 -0
  47. package/dist/runtime/next/server-runtime.js +20 -0
  48. package/dist/runtime/next/server.d.ts +11 -0
  49. package/dist/runtime/next/server.js +10 -0
  50. package/dist/runtime/primitives/components/Text.d.ts +1 -2
  51. package/dist/runtime/primitives/lib/query.d.ts +1 -1
  52. package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +4 -6
  53. package/dist/runtime/primitives/lib/useFimoLiveValue.js +4 -10
  54. package/dist/scripts/sync-schemas.js +67 -16
  55. package/package.json +14 -1
  56. package/release.json +2 -2
  57. package/templates/react-router/package.json +1 -1
@@ -0,0 +1,37 @@
1
+ import type { Fields, Populate, Query } from '../../primitives/lib/query.js';
2
+ import type { ContentRuntime } from './runtime.js';
3
+ import type { CollectionPage, ContentClientConfig, ContentModuleMeta, ContentReadOptions, ContentResult } from './types.js';
4
+ export interface CollectionContentCore<T extends {
5
+ id: string;
6
+ }, TInput extends object> {
7
+ readonly meta: ContentModuleMeta;
8
+ wrapWithSource(entry: Record<string, any>): T;
9
+ getById<TPopulate extends Populate<T> | undefined = undefined>(id: string, options?: ContentReadOptions<TPopulate>): Promise<ContentResult<T, undefined, TPopulate>>;
10
+ getBySlug<TPopulate extends Populate<T> | undefined = undefined>(slug: string, options?: ContentReadOptions<TPopulate>): Promise<ContentResult<T, undefined, TPopulate> | null>;
11
+ getByField<TPopulate extends Populate<T> | undefined = undefined>(fieldName: string, value: string, options?: ContentReadOptions<TPopulate>): Promise<ContentResult<T, undefined, TPopulate> | null>;
12
+ list<TFields extends Fields<T> | undefined = undefined, TPopulate extends Populate<T> | undefined = undefined>(params?: Query<T, TFields, TPopulate>): Promise<CollectionPage<ContentResult<T, TFields, TPopulate>>>;
13
+ get<TFields extends Fields<T> | undefined = undefined, TPopulate extends Populate<T> | undefined = undefined>(params?: Query<T, TFields, TPopulate>): Promise<ContentResult<T, TFields, TPopulate>[]>;
14
+ create(payload: TInput): Promise<T>;
15
+ update(id: string, payload: Partial<TInput>): Promise<T>;
16
+ remove(id: string, options?: {
17
+ locale?: string;
18
+ }): Promise<void>;
19
+ }
20
+ export interface CollectionCoreState<T extends {
21
+ id: string;
22
+ }, TInput extends object> {
23
+ client: CollectionContentCore<T, TInput>;
24
+ defaultLocale: string;
25
+ queryKeys: {
26
+ root: readonly [string];
27
+ byId(id: string, options?: ContentReadOptions<Populate<T> | undefined>): readonly unknown[];
28
+ bySlug(slug: string, options?: ContentReadOptions<Populate<T> | undefined>): readonly unknown[];
29
+ byField(field: string, value: string, options?: ContentReadOptions<Populate<T> | undefined>): readonly unknown[];
30
+ list(params: Query<T, Fields<T> | undefined, Populate<T> | undefined>): readonly unknown[];
31
+ infiniteList(params: Omit<Query<T, Fields<T> | undefined, Populate<T> | undefined>, 'offset'>): readonly unknown[];
32
+ };
33
+ }
34
+ export declare function createCollectionContentCore<T extends {
35
+ id: string;
36
+ }, TInput extends object>(config: ContentClientConfig, runtime: ContentRuntime): CollectionCoreState<T, TInput>;
37
+ //# sourceMappingURL=collection-core.d.ts.map
@@ -0,0 +1,149 @@
1
+ import { parseCollectionResponse } from '../parseCollectionResponse.js';
2
+ import { buildReadPath, interpretSchema, readCollectionLocale, resolveClientHandle, splitWritePayload, validateReferences, withMutationLocale, wrapEntry, } from './internal.js';
3
+ export function createCollectionContentCore(config, runtime) {
4
+ const references = config.references ?? {};
5
+ const info = interpretSchema(config.schema);
6
+ if (info.isSingleton) {
7
+ throw new Error(`Schema "${info.uid}" is a Single — use createSingletonClient instead.`);
8
+ }
9
+ validateReferences(info, references);
10
+ const defaultLocale = runtime.defaultLocale();
11
+ const meta = { uid: info.uid, pascal: info.pascal, localized: info.localized };
12
+ const self = { wrapWithSource, meta };
13
+ const resolve = (uid) => uid === info.uid ? self : resolveClientHandle(references[uid]);
14
+ function wrapWithSource(entry) {
15
+ return wrapEntry(info, entry, { alwaysIncludeLocale: true, defaultLocale, resolve });
16
+ }
17
+ function readPath(url, options) {
18
+ return buildReadPath(url, readCollectionLocale(options, defaultLocale), options.populate, options.localeFallbacks);
19
+ }
20
+ async function getById(id, options = {}) {
21
+ const response = await runtime.fetch(`${runtime.apiBase()}${readPath(`/entries/${info.uid}/${id}`, options)}`, {
22
+ headers: runtime.headers(),
23
+ });
24
+ if (!response.ok) {
25
+ throw new Error(await response.text());
26
+ }
27
+ const body = await response.json();
28
+ return wrapWithSource(body.data);
29
+ }
30
+ async function getBySlug(slug, options = {}) {
31
+ const response = await runtime.fetch(`${runtime.apiBase()}${readPath(`/entries/${info.uid}/slug/${encodeURIComponent(slug)}`, options)}`, { headers: runtime.headers() });
32
+ if (response.status === 404) {
33
+ return null;
34
+ }
35
+ if (!response.ok) {
36
+ throw new Error(await response.text());
37
+ }
38
+ const body = await response.json();
39
+ return wrapWithSource(body.data);
40
+ }
41
+ async function getByField(fieldName, value, options = {}) {
42
+ const response = await runtime.fetch(`${runtime.apiBase()}${readPath(`/entries/${info.uid}/field/${encodeURIComponent(fieldName)}/${encodeURIComponent(value)}`, options)}`, { headers: runtime.headers() });
43
+ if (response.status === 404) {
44
+ return null;
45
+ }
46
+ if (!response.ok) {
47
+ throw new Error(await response.text());
48
+ }
49
+ const body = await response.json();
50
+ return wrapWithSource(body.data);
51
+ }
52
+ function toQueryString(params) {
53
+ const { where, sort, fields, populate, locale: requestedLocale, ...flat } = params;
54
+ const query = new URLSearchParams();
55
+ for (const [key, value] of Object.entries(flat)) {
56
+ if (value != null) {
57
+ query.set(key, String(value));
58
+ }
59
+ }
60
+ if (sort != null) {
61
+ query.set('sort', Array.isArray(sort) ? sort.join(',') : String(sort));
62
+ }
63
+ if (fields != null) {
64
+ query.set('fields', typeof fields === 'string' ? fields : fields.join(','));
65
+ }
66
+ if (populate != null) {
67
+ query.set('populate', typeof populate === 'string' ? populate : populate.join(','));
68
+ }
69
+ if (where && Object.keys(where).length > 0) {
70
+ query.set('where', JSON.stringify(where));
71
+ }
72
+ query.set('locale', readCollectionLocale({ locale: requestedLocale }, defaultLocale));
73
+ return query.toString();
74
+ }
75
+ async function list(params = {}) {
76
+ const query = toQueryString(params);
77
+ const response = await runtime.fetch(`${runtime.apiBase()}/entries/${info.uid}${query ? `?${query}` : ''}`, {
78
+ headers: runtime.headers(),
79
+ });
80
+ if (!response.ok) {
81
+ throw new Error(await response.text());
82
+ }
83
+ const body = parseCollectionResponse(await response.json());
84
+ const items = body.data.map(wrapWithSource);
85
+ const pageInfo = {
86
+ limit: body.meta.limit,
87
+ offset: body.meta.offset,
88
+ total: body.meta.total,
89
+ hasMore: body.meta.hasMore,
90
+ nextOffset: body.meta.hasMore ? body.meta.offset + body.meta.limit : null,
91
+ };
92
+ return { items, pageInfo };
93
+ }
94
+ async function get(params = {}) {
95
+ return (await list(params)).items;
96
+ }
97
+ async function create(payload) {
98
+ const localizedPayload = info.localized
99
+ ? withMutationLocale(payload, readCollectionLocale({}, defaultLocale))
100
+ : payload;
101
+ const response = await runtime.fetch(`${runtime.apiBase()}/entries/${info.uid}`, {
102
+ method: 'POST',
103
+ headers: runtime.headers({ 'Content-Type': 'application/json' }),
104
+ body: JSON.stringify(splitWritePayload(localizedPayload)),
105
+ });
106
+ if (!response.ok) {
107
+ throw new Error(await response.text());
108
+ }
109
+ const body = await response.json();
110
+ return wrapWithSource(body.data);
111
+ }
112
+ async function update(id, payload) {
113
+ const write = splitWritePayload(payload);
114
+ const url = buildReadPath(`/entries/${info.uid}/${id}`, readCollectionLocale({ locale: write.locale }, defaultLocale), undefined);
115
+ const response = await runtime.fetch(`${runtime.apiBase()}${url}`, {
116
+ method: 'PUT',
117
+ headers: runtime.headers({ 'Content-Type': 'application/json' }),
118
+ body: JSON.stringify({ data: write.data }),
119
+ });
120
+ if (!response.ok) {
121
+ throw new Error(await response.text());
122
+ }
123
+ const body = await response.json();
124
+ return wrapWithSource(body.data);
125
+ }
126
+ async function remove(id, options = {}) {
127
+ const url = buildReadPath(`/entries/${info.uid}/${id}`, readCollectionLocale(options, defaultLocale), undefined);
128
+ const response = await runtime.fetch(`${runtime.apiBase()}${url}`, {
129
+ method: 'DELETE',
130
+ headers: runtime.headers(),
131
+ });
132
+ if (!response.ok) {
133
+ throw new Error(await response.text());
134
+ }
135
+ }
136
+ const queryKeys = {
137
+ root: [info.pascal],
138
+ byId: (id, options = {}) => [info.pascal, 'id', id, options.locale, options.populate],
139
+ bySlug: (slug, options = {}) => [info.pascal, 'slug', slug, options.locale, options.populate],
140
+ byField: (field, value, options = {}) => [info.pascal, field, value, options.locale, options.populate],
141
+ list: (params) => [info.pascal, params],
142
+ infiniteList: (params) => [info.pascal, 'infinite', params],
143
+ };
144
+ return {
145
+ client: { meta, wrapWithSource, getById, getBySlug, getByField, list, get, create, update, remove },
146
+ defaultLocale,
147
+ queryKeys,
148
+ };
149
+ }
@@ -1,19 +1,6 @@
1
- import type { CollectionContentClient, ContentClientConfig } from './types.js';
2
- /**
3
- * React Query's structural sharing (`replaceEqualDeep`) rebuilds plain objects
4
- * and arrays key-by-key from `Object.keys`, which silently drops the
5
- * non-enumerable, symbol-keyed source tag `wrapField` puts on `json` values.
6
- * A `json` field would then lose its `data-fimo-source` on the first refetch
7
- * that changes it, making it unselectable in every editing surface. Every other
8
- * wrapped type is a class instance, which structural sharing returns untouched.
9
- *
10
- * Turning it off costs nothing here: `wrapEntry` allocates fresh wrappers on
11
- * every fetch, so a wrapped entry never compared equal to its predecessor and
12
- * sharing was already returning the new object.
13
- */
14
- export declare const SOURCE_TRACKED_QUERY_OPTIONS: {
15
- readonly structuralSharing: false;
16
- };
1
+ import { SOURCE_TRACKED_QUERY_OPTIONS } from './react-content-client.js';
2
+ import type { ContentClientConfig, CollectionContentClient } from './types.js';
3
+ export { SOURCE_TRACKED_QUERY_OPTIONS };
17
4
  export declare function createContentClient<T extends {
18
5
  id: string;
19
6
  }, TInput extends object>(config: ContentClientConfig): CollectionContentClient<T, TInput>;
@@ -1,324 +1,7 @@
1
- import { useInfiniteQuery, useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
2
- import { parseCollectionResponse } from '../parseCollectionResponse.js';
3
- import { apiBase, buildReadPath, fimoHeaders, interpretSchema, readCollectionLocale, resolveClientHandle, splitWritePayload, validateReferences, withMutationLocale, wrapEntry, } from './internal.js';
1
+ import { createReactContentClient, SOURCE_TRACKED_QUERY_OPTIONS } from './react-content-client.js';
4
2
  import { useQueryLocale } from './useQueryLocale.js';
5
- /**
6
- * React Query's structural sharing (`replaceEqualDeep`) rebuilds plain objects
7
- * and arrays key-by-key from `Object.keys`, which silently drops the
8
- * non-enumerable, symbol-keyed source tag `wrapField` puts on `json` values.
9
- * A `json` field would then lose its `data-fimo-source` on the first refetch
10
- * that changes it, making it unselectable in every editing surface. Every other
11
- * wrapped type is a class instance, which structural sharing returns untouched.
12
- *
13
- * Turning it off costs nothing here: `wrapEntry` allocates fresh wrappers on
14
- * every fetch, so a wrapped entry never compared equal to its predecessor and
15
- * sharing was already returning the new object.
16
- */
17
- export const SOURCE_TRACKED_QUERY_OPTIONS = { structuralSharing: false };
3
+ import { viteContentRuntime } from './vite-runtime.js';
4
+ export { SOURCE_TRACKED_QUERY_OPTIONS };
18
5
  export function createContentClient(config) {
19
- const references = config.references ?? {};
20
- const info = interpretSchema(config.schema);
21
- if (info.isSingleton) {
22
- throw new Error(`Schema "${info.uid}" is a Single — use createSingletonClient instead.`);
23
- }
24
- validateReferences(info, references);
25
- const meta = { uid: info.uid, pascal: info.pascal, localized: info.localized };
26
- const self = { wrapWithSource, meta };
27
- const resolve = (uid) => uid === info.uid ? self : resolveClientHandle(references[uid]);
28
- function wrapWithSource(entry) {
29
- return wrapEntry(info, entry, { alwaysIncludeLocale: true, resolve });
30
- }
31
- function readPath(url, options) {
32
- return buildReadPath(url, readCollectionLocale(options), options.populate, options.localeFallbacks);
33
- }
34
- async function getById(id, options = {}) {
35
- const res = await fetch(`${apiBase()}${readPath(`/entries/${info.uid}/${id}`, options)}`, {
36
- headers: fimoHeaders(),
37
- });
38
- if (!res.ok) {
39
- throw new Error(await res.text());
40
- }
41
- const data = await res.json();
42
- return wrapWithSource(data.data);
43
- }
44
- async function getBySlug(slug, options = {}) {
45
- const res = await fetch(`${apiBase()}${readPath(`/entries/${info.uid}/slug/${encodeURIComponent(slug)}`, options)}`, { headers: fimoHeaders() });
46
- if (res.status === 404) {
47
- return null;
48
- }
49
- if (!res.ok) {
50
- throw new Error(await res.text());
51
- }
52
- const data = await res.json();
53
- return wrapWithSource(data.data);
54
- }
55
- async function getByField(fieldName, value, options = {}) {
56
- const res = await fetch(`${apiBase()}${readPath(`/entries/${info.uid}/field/${encodeURIComponent(fieldName)}/${encodeURIComponent(value)}`, options)}`, { headers: fimoHeaders() });
57
- if (res.status === 404) {
58
- return null;
59
- }
60
- if (!res.ok) {
61
- throw new Error(await res.text());
62
- }
63
- const data = await res.json();
64
- return wrapWithSource(data.data);
65
- }
66
- function toQueryString(params) {
67
- const { where, sort, fields, populate, locale: requestedLocale, ...flat } = params;
68
- const query = new URLSearchParams();
69
- for (const [key, value] of Object.entries(flat)) {
70
- if (value == null) {
71
- continue;
72
- }
73
- query.set(key, String(value));
74
- }
75
- if (sort != null) {
76
- query.set('sort', Array.isArray(sort) ? sort.join(',') : String(sort));
77
- }
78
- if (fields != null) {
79
- query.set('fields', typeof fields === 'string' ? fields : fields.join(','));
80
- }
81
- if (populate != null) {
82
- query.set('populate', typeof populate === 'string' ? populate : populate.join(','));
83
- }
84
- if (where && Object.keys(where).length > 0) {
85
- query.set('where', JSON.stringify(where));
86
- }
87
- const locale = readCollectionLocale({ locale: requestedLocale });
88
- if (locale) {
89
- query.set('locale', locale);
90
- }
91
- return query.toString();
92
- }
93
- async function list(params = {}) {
94
- const qs = toQueryString(params);
95
- const res = await fetch(`${apiBase()}/entries/${info.uid}${qs ? '?' + qs : ''}`, {
96
- headers: fimoHeaders(),
97
- });
98
- if (!res.ok) {
99
- throw new Error(await res.text());
100
- }
101
- const data = parseCollectionResponse(await res.json());
102
- const items = data.data.map(wrapWithSource);
103
- const pageInfo = {
104
- limit: data.meta.limit,
105
- offset: data.meta.offset,
106
- total: data.meta.total,
107
- hasMore: data.meta.hasMore,
108
- nextOffset: data.meta.hasMore ? data.meta.offset + data.meta.limit : null,
109
- };
110
- return { items, pageInfo };
111
- }
112
- async function get(params = {}) {
113
- return (await list(params)).items;
114
- }
115
- async function create(payload) {
116
- const localizedPayload = info.localized ? withMutationLocale(payload, readCollectionLocale({})) : payload;
117
- const res = await fetch(`${apiBase()}/entries/${info.uid}`, {
118
- method: 'POST',
119
- headers: fimoHeaders({ 'Content-Type': 'application/json' }),
120
- body: JSON.stringify(splitWritePayload(localizedPayload)),
121
- });
122
- if (!res.ok) {
123
- throw new Error(await res.text());
124
- }
125
- const data = await res.json();
126
- return wrapWithSource(data.data);
127
- }
128
- async function update(id, payload) {
129
- const write = splitWritePayload(payload);
130
- const url = buildReadPath(`/entries/${info.uid}/${id}`, readCollectionLocale({ locale: write.locale }), undefined);
131
- const res = await fetch(`${apiBase()}${url}`, {
132
- method: 'PUT',
133
- headers: fimoHeaders({ 'Content-Type': 'application/json' }),
134
- body: JSON.stringify({ data: write.data }),
135
- });
136
- if (!res.ok) {
137
- throw new Error(await res.text());
138
- }
139
- const data = await res.json();
140
- return wrapWithSource(data.data);
141
- }
142
- async function remove(id, options = {}) {
143
- const url = buildReadPath(`/entries/${info.uid}/${id}`, readCollectionLocale(options), undefined);
144
- const res = await fetch(`${apiBase()}${url}`, { method: 'DELETE', headers: fimoHeaders() });
145
- if (!res.ok) {
146
- throw new Error(await res.text());
147
- }
148
- }
149
- const qk = {
150
- root: [info.pascal],
151
- byId: (id, options = {}) => [info.pascal, 'id', id, options.locale, options.populate],
152
- bySlug: (slug, options = {}) => [info.pascal, 'slug', slug, options.locale, options.populate],
153
- byField: (field, value, options = {}) => [info.pascal, field, value, options.locale, options.populate],
154
- list: (params) => [info.pascal, params],
155
- infiniteList: (params) => [info.pascal, 'infinite', params],
156
- };
157
- function useGetById(id, options = {}) {
158
- const locale = useQueryLocale(options.locale);
159
- const localizedOptions = { ...options, locale };
160
- return useQuery({
161
- queryKey: qk.byId(id, localizedOptions),
162
- queryFn: () => getById(id, localizedOptions),
163
- ...SOURCE_TRACKED_QUERY_OPTIONS,
164
- });
165
- }
166
- function useGetBySlug(slug, options = {}) {
167
- const locale = readCollectionLocale(options, useQueryLocale(options.locale));
168
- const localizedOptions = { ...options, locale };
169
- return useQuery({
170
- queryKey: qk.bySlug(slug, localizedOptions),
171
- queryFn: () => getBySlug(slug, localizedOptions),
172
- ...SOURCE_TRACKED_QUERY_OPTIONS,
173
- enabled: !!slug,
174
- });
175
- }
176
- function useGetByField(fieldName, value, options = {}) {
177
- const locale = readCollectionLocale(options, useQueryLocale(options.locale));
178
- const localizedOptions = { ...options, locale };
179
- return useQuery({
180
- queryKey: qk.byField(fieldName, value, localizedOptions),
181
- queryFn: () => getByField(fieldName, value, localizedOptions),
182
- ...SOURCE_TRACKED_QUERY_OPTIONS,
183
- enabled: !!value,
184
- });
185
- }
186
- function useGet(params = {}) {
187
- const locale = readCollectionLocale(params, useQueryLocale(params.locale));
188
- const localizedParams = { ...params, locale };
189
- return useQuery({
190
- queryKey: qk.list(localizedParams),
191
- queryFn: () => list(localizedParams),
192
- ...SOURCE_TRACKED_QUERY_OPTIONS,
193
- select: (page) => page.items,
194
- });
195
- }
196
- function useList(params = {}) {
197
- const locale = readCollectionLocale(params, useQueryLocale(params.locale));
198
- const localizedParams = { ...params, locale };
199
- return useQuery({
200
- queryKey: qk.list(localizedParams),
201
- queryFn: () => list(localizedParams),
202
- ...SOURCE_TRACKED_QUERY_OPTIONS,
203
- });
204
- }
205
- function useInfiniteList(params = {}) {
206
- const locale = readCollectionLocale(params, useQueryLocale(params.locale));
207
- const localizedParams = { ...params, locale };
208
- const query = useInfiniteQuery({
209
- queryKey: qk.infiniteList(localizedParams),
210
- queryFn: ({ pageParam }) => list({ ...localizedParams, offset: pageParam }),
211
- ...SOURCE_TRACKED_QUERY_OPTIONS,
212
- initialPageParam: 0,
213
- getNextPageParam: (lastPage) => lastPage.pageInfo.nextOffset ?? undefined,
214
- });
215
- return {
216
- ...query,
217
- items: query.data?.pages.flatMap((page) => page.items) ?? [],
218
- total: query.data?.pages[0]?.pageInfo.total ?? 0,
219
- hasMore: query.hasNextPage,
220
- loadMore: () => query.fetchNextPage(),
221
- isLoadingMore: query.isFetchingNextPage,
222
- };
223
- }
224
- function useSuspenseGetById(id, options = {}) {
225
- const locale = useQueryLocale(options.locale);
226
- const localizedOptions = { ...options, locale };
227
- return useSuspenseQuery({
228
- queryKey: qk.byId(id, localizedOptions),
229
- queryFn: () => getById(id, localizedOptions),
230
- ...SOURCE_TRACKED_QUERY_OPTIONS,
231
- });
232
- }
233
- function useSuspenseGetBySlug(slug, options = {}) {
234
- const locale = readCollectionLocale(options, useQueryLocale(options.locale));
235
- const localizedOptions = { ...options, locale };
236
- return useSuspenseQuery({
237
- queryKey: qk.bySlug(slug, localizedOptions),
238
- queryFn: () => getBySlug(slug, localizedOptions),
239
- ...SOURCE_TRACKED_QUERY_OPTIONS,
240
- });
241
- }
242
- function useSuspenseGetByField(fieldName, value, options = {}) {
243
- const locale = readCollectionLocale(options, useQueryLocale(options.locale));
244
- const localizedOptions = { ...options, locale };
245
- return useSuspenseQuery({
246
- queryKey: qk.byField(fieldName, value, localizedOptions),
247
- queryFn: () => getByField(fieldName, value, localizedOptions),
248
- ...SOURCE_TRACKED_QUERY_OPTIONS,
249
- });
250
- }
251
- function useSuspenseGet(params = {}) {
252
- const locale = readCollectionLocale(params, useQueryLocale(params.locale));
253
- const localizedParams = { ...params, locale };
254
- return useSuspenseQuery({
255
- queryKey: qk.list(localizedParams),
256
- queryFn: () => list(localizedParams),
257
- ...SOURCE_TRACKED_QUERY_OPTIONS,
258
- select: (page) => page.items,
259
- });
260
- }
261
- function useCreate() {
262
- const qc = useQueryClient();
263
- const activeLocale = useQueryLocale();
264
- return useMutation({
265
- mutationFn: (payload) => create(withMutationLocale(payload, activeLocale)),
266
- onSuccess: () => qc.invalidateQueries({ queryKey: qk.root }),
267
- });
268
- }
269
- function useUpdate() {
270
- const qc = useQueryClient();
271
- const activeLocale = useQueryLocale();
272
- return useMutation({
273
- mutationFn: ({ id, ...patch }) => update(id, withMutationLocale(patch, activeLocale)),
274
- onSuccess: (_data, vars) => {
275
- qc.invalidateQueries({ queryKey: qk.root });
276
- qc.invalidateQueries({ queryKey: qk.byId(vars.id, { locale: vars.locale ?? activeLocale }) });
277
- },
278
- });
279
- }
280
- function useRemove() {
281
- const qc = useQueryClient();
282
- const activeLocale = useQueryLocale();
283
- return useMutation({
284
- mutationFn: (value) => {
285
- const { id, locale } = typeof value === 'string'
286
- ? { id: value, locale: activeLocale }
287
- : { ...value, locale: value.locale ?? activeLocale };
288
- return remove(id, { locale });
289
- },
290
- onSuccess: (_data, value) => {
291
- const { id, locale } = typeof value === 'string'
292
- ? { id: value, locale: activeLocale }
293
- : { ...value, locale: value.locale ?? activeLocale };
294
- qc.invalidateQueries({ queryKey: qk.root });
295
- qc.removeQueries({ queryKey: qk.byId(id, { locale }), exact: false });
296
- },
297
- });
298
- }
299
- return {
300
- meta,
301
- wrapWithSource,
302
- getById,
303
- getBySlug,
304
- getByField,
305
- list,
306
- get,
307
- create,
308
- update,
309
- remove,
310
- useGetById,
311
- useGetBySlug,
312
- useGetByField,
313
- useGet,
314
- useList,
315
- useInfiniteList,
316
- useSuspenseGetById,
317
- useSuspenseGetBySlug,
318
- useSuspenseGetByField,
319
- useSuspenseGet,
320
- useCreate,
321
- useUpdate,
322
- useRemove,
323
- };
6
+ return createReactContentClient(config, viteContentRuntime, (requested, fallback) => useQueryLocale(requested, fallback));
324
7
  }
@@ -1,111 +1,6 @@
1
- import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
2
- import { SOURCE_TRACKED_QUERY_OPTIONS } from './createContentClient.js';
3
- import { apiBase, buildReadPath, fimoHeaders, interpretSchema, readSingletonLocale, referenceTargetUids, resolveClientHandle, splitWritePayload, validateReferences, withMutationLocale, wrapEntry, } from './internal.js';
1
+ import { createReactSingletonClient } from './react-singleton-client.js';
4
2
  import { useQueryLocale } from './useQueryLocale.js';
3
+ import { viteContentRuntime } from './vite-runtime.js';
5
4
  export function createSingletonClient(config) {
6
- const references = config.references ?? {};
7
- const info = interpretSchema(config.schema);
8
- if (!info.isSingleton) {
9
- throw new Error(`Schema "${info.uid}" is a collection — use createContentClient instead.`);
10
- }
11
- validateReferences(info, references);
12
- const meta = { uid: info.uid, pascal: info.pascal, localized: info.localized };
13
- const self = { wrapWithSource, meta };
14
- const resolve = (uid) => uid === info.uid ? self : resolveClientHandle(references[uid]);
15
- // Which reference fields point at a localized schema decides whether a
16
- // non-localized singleton read must still send a locale. Target metas live
17
- // on other modules in a possibly-cyclic graph, so this is derived lazily on
18
- // first request, never at module init.
19
- let localizedReferenceFields = null;
20
- function getLocalizedReferenceFields() {
21
- if (localizedReferenceFields) {
22
- return localizedReferenceFields;
23
- }
24
- const fields = new Set();
25
- for (const [key, field] of info.fields) {
26
- if (field.type !== 'reference') {
27
- continue;
28
- }
29
- const localizedTarget = referenceTargetUids(field).some((uid) => resolve(uid)?.meta?.localized !== false);
30
- if (localizedTarget) {
31
- fields.add(key);
32
- }
33
- }
34
- localizedReferenceFields = fields;
35
- return fields;
36
- }
37
- function wrapWithSource(entry) {
38
- return wrapEntry(info, entry, { alwaysIncludeLocale: info.localized, resolve });
39
- }
40
- function readLocale(options, fallbackLocale) {
41
- return fallbackLocale === undefined
42
- ? readSingletonLocale(options, info.localized, getLocalizedReferenceFields())
43
- : readSingletonLocale(options, info.localized, getLocalizedReferenceFields(), fallbackLocale);
44
- }
45
- function readPath(url, options) {
46
- return buildReadPath(url, readLocale(options), options.populate, options.localeFallbacks);
47
- }
48
- async function get(options = {}) {
49
- const res = await fetch(`${apiBase()}${readPath(`/documents/${info.uid}`, options)}`, {
50
- headers: fimoHeaders(),
51
- });
52
- if (!res.ok) {
53
- throw new Error(await res.text());
54
- }
55
- const data = await res.json();
56
- return wrapWithSource(data.data);
57
- }
58
- async function update(payload) {
59
- const write = splitWritePayload(payload);
60
- const url = buildReadPath(`/documents/${info.uid}`, readLocale({ locale: write.locale }), undefined);
61
- const res = await fetch(`${apiBase()}${url}`, {
62
- method: 'PUT',
63
- headers: fimoHeaders({ 'Content-Type': 'application/json' }),
64
- body: JSON.stringify({ data: write.data }),
65
- });
66
- if (!res.ok) {
67
- throw new Error(await res.text());
68
- }
69
- const data = await res.json();
70
- return wrapWithSource(data.data);
71
- }
72
- const qk = {
73
- root: [info.pascal],
74
- document: (options = {}) => [info.pascal, 'document', options.locale, options.populate],
75
- };
76
- function useGet(options = {}) {
77
- const locale = readLocale(options, useQueryLocale(options.locale));
78
- const localizedOptions = { ...options, locale };
79
- return useQuery({
80
- queryKey: qk.document(localizedOptions),
81
- queryFn: () => get(localizedOptions),
82
- ...SOURCE_TRACKED_QUERY_OPTIONS,
83
- });
84
- }
85
- function useSuspenseGet(options = {}) {
86
- const locale = readLocale(options, useQueryLocale(options.locale));
87
- const localizedOptions = { ...options, locale };
88
- return useSuspenseQuery({
89
- queryKey: qk.document(localizedOptions),
90
- queryFn: () => get(localizedOptions),
91
- ...SOURCE_TRACKED_QUERY_OPTIONS,
92
- });
93
- }
94
- function useUpdate() {
95
- const qc = useQueryClient();
96
- const activeLocale = useQueryLocale();
97
- return useMutation({
98
- mutationFn: (payload) => update(withMutationLocale(payload, activeLocale)),
99
- onSuccess: () => qc.invalidateQueries({ queryKey: qk.root }),
100
- });
101
- }
102
- return {
103
- meta,
104
- wrapWithSource,
105
- get,
106
- update,
107
- useGet,
108
- useSuspenseGet,
109
- useUpdate,
110
- };
5
+ return createReactSingletonClient(config, viteContentRuntime, (requested, fallback) => useQueryLocale(requested, fallback));
111
6
  }
@@ -9,9 +9,6 @@ export interface InterpretedSchema {
9
9
  export declare function interpretSchema(schema: ContentSchema): InterpretedSchema;
10
10
  export declare function referenceTargetUids(field: ContentSchemaField): readonly string[];
11
11
  export declare function validateReferences(info: InterpretedSchema, references: Readonly<Record<string, ContentModuleRef>>): void;
12
- export declare function apiBase(): string;
13
- export declare function defaultLocale(): string;
14
- export declare function fimoHeaders(extra?: Record<string, string>): Record<string, string>;
15
12
  interface LocaleReadOptions {
16
13
  locale?: string;
17
14
  populate?: string | readonly string[];
@@ -38,6 +35,7 @@ type ReferenceResolver = (uid: string) => ContentClientHandle | undefined;
38
35
  export declare function resolveClientHandle(ref: ContentModuleRef | undefined): ContentClientHandle | undefined;
39
36
  export declare function wrapEntry(info: InterpretedSchema, entry: Record<string, any>, options: {
40
37
  alwaysIncludeLocale: boolean;
38
+ defaultLocale: string;
41
39
  resolve: ReferenceResolver;
42
40
  }): Record<string, unknown>;
43
41
  export {};