@cmssy/react 0.1.6 → 0.1.8

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/dist/client.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { B as BlockSchema, a as BlockMeta, C as CmssyPageData, b as BlockDefinition, c as CmssyFormDefinition, d as CmssyLayoutGroup } from './registry-BaFQ2k7r.cjs';
2
+ import { B as BlockSchema, a as BlockMeta, C as CmssyPageData, b as BlockDefinition, c as CmssyFormDefinition, d as CmssyLayoutGroup } from './registry-e3O4s_bN.cjs';
3
3
  import 'react';
4
4
 
5
5
  interface EditBridgeConfig {
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { B as BlockSchema, a as BlockMeta, C as CmssyPageData, b as BlockDefinition, c as CmssyFormDefinition, d as CmssyLayoutGroup } from './registry-BaFQ2k7r.js';
2
+ import { B as BlockSchema, a as BlockMeta, C as CmssyPageData, b as BlockDefinition, c as CmssyFormDefinition, d as CmssyLayoutGroup } from './registry-e3O4s_bN.js';
3
3
  import 'react';
4
4
 
5
5
  interface EditBridgeConfig {
package/dist/index.cjs CHANGED
@@ -35,6 +35,13 @@ function buildBlockMap(blocks) {
35
35
  for (const block of blocks) map[block.type] = block.component;
36
36
  return map;
37
37
  }
38
+ function buildLoaderMap(blocks) {
39
+ const map = /* @__PURE__ */ Object.create(null);
40
+ for (const block of blocks) {
41
+ if (block.loader) map[block.type] = block.loader;
42
+ }
43
+ return map;
44
+ }
38
45
  function blocksToSchemas(blocks) {
39
46
  const out = /* @__PURE__ */ Object.create(null);
40
47
  for (const block of blocks) {
@@ -60,19 +67,6 @@ function blocksToMeta(blocks, defaults = {}) {
60
67
  return out;
61
68
  }
62
69
 
63
- // src/components/block-context.ts
64
- function buildBlockContext(locale, defaultLocale, enabledLocales, isPreview, forms) {
65
- return {
66
- locale: {
67
- current: locale,
68
- default: defaultLocale,
69
- enabled: enabledLocales && enabledLocales.length > 0 ? enabledLocales : Array.from(/* @__PURE__ */ new Set([defaultLocale, locale]))
70
- },
71
- isPreview: isPreview ?? false,
72
- forms
73
- };
74
- }
75
-
76
70
  // src/content/get-block-content.ts
77
71
  function isPlainObject(value) {
78
72
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -96,6 +90,19 @@ function getBlockContentForLanguage(content, locale, defaultLocale = "en", avail
96
90
  const chosen = localeMap[locale] ?? localeMap[defaultLocale] ?? localeMap[fallbackKey];
97
91
  return { ...nonTranslatable, ...chosen };
98
92
  }
93
+
94
+ // src/components/block-context.ts
95
+ function buildBlockContext(locale, defaultLocale, enabledLocales, isPreview, forms) {
96
+ return {
97
+ locale: {
98
+ current: locale,
99
+ default: defaultLocale,
100
+ enabled: enabledLocales && enabledLocales.length > 0 ? enabledLocales : Array.from(/* @__PURE__ */ new Set([defaultLocale, locale]))
101
+ },
102
+ isPreview: isPreview ?? false,
103
+ forms
104
+ };
105
+ }
99
106
  var WARN_CAP = 256;
100
107
  var warned = /* @__PURE__ */ new Set();
101
108
  function UnknownBlock({ type }) {
@@ -106,12 +113,14 @@ function UnknownBlock({ type }) {
106
113
  }
107
114
  return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-cmssy-unknown-block": type });
108
115
  }
109
- function renderResolvedBlock(block, map, locale, defaultLocale, context) {
116
+ function renderResolvedBlock(block, map, locale, defaultLocale, options = {}) {
117
+ const { context, data, resolvedContent, enabledLocales } = options;
110
118
  const Component = Object.hasOwn(map, block.type) ? map[block.type] : void 0;
111
- const content = getBlockContentForLanguage(
119
+ const content = resolvedContent ?? getBlockContentForLanguage(
112
120
  block.content,
113
121
  locale,
114
- defaultLocale
122
+ defaultLocale,
123
+ enabledLocales?.length ? enabledLocales : void 0
115
124
  );
116
125
  return /* @__PURE__ */ jsxRuntime.jsx(
117
126
  "div",
@@ -119,12 +128,12 @@ function renderResolvedBlock(block, map, locale, defaultLocale, context) {
119
128
  "data-block-id": block.id,
120
129
  "data-block-type": block.type,
121
130
  style: Component ? void 0 : { display: "none" },
122
- children: Component ? react.createElement(Component, { content, context }) : /* @__PURE__ */ jsxRuntime.jsx(UnknownBlock, { type: block.type })
131
+ children: Component ? react.createElement(Component, { content, context, data }) : /* @__PURE__ */ jsxRuntime.jsx(UnknownBlock, { type: block.type })
123
132
  },
124
133
  block.id
125
134
  );
126
135
  }
127
- function CmssyServerPage({
136
+ async function CmssyServerPage({
128
137
  page,
129
138
  blocks,
130
139
  locale = "en",
@@ -134,6 +143,7 @@ function CmssyServerPage({
134
143
  }) {
135
144
  if (!page) return null;
136
145
  const map = buildBlockMap(blocks);
146
+ const loaderMap = buildLoaderMap(blocks);
137
147
  const context = buildBlockContext(
138
148
  locale,
139
149
  defaultLocale,
@@ -141,8 +151,38 @@ function CmssyServerPage({
141
151
  false,
142
152
  forms
143
153
  );
154
+ const resolved = await Promise.all(
155
+ page.blocks.map(async (block) => {
156
+ const content = getBlockContentForLanguage(
157
+ block.content,
158
+ locale,
159
+ defaultLocale,
160
+ enabledLocales?.length ? enabledLocales : void 0
161
+ );
162
+ const loader = loaderMap[block.type];
163
+ let data;
164
+ if (loader) {
165
+ try {
166
+ data = await loader({ content, context });
167
+ } catch (err) {
168
+ if (typeof console !== "undefined") {
169
+ console.warn(
170
+ `[cmssy] loader for block "${block.type}" (${block.id}) failed`,
171
+ err
172
+ );
173
+ }
174
+ }
175
+ }
176
+ return { content, data };
177
+ })
178
+ );
144
179
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: page.blocks.map(
145
- (block) => renderResolvedBlock(block, map, locale, defaultLocale, context)
180
+ (block, i) => renderResolvedBlock(block, map, locale, defaultLocale, {
181
+ context,
182
+ data: resolved[i]?.data,
183
+ resolvedContent: resolved[i]?.content,
184
+ enabledLocales
185
+ })
146
186
  ) });
147
187
  }
148
188
  function CmssyServerLayout({
@@ -159,7 +199,10 @@ function CmssyServerLayout({
159
199
  const map = buildBlockMap(blocks);
160
200
  const context = buildBlockContext(locale, defaultLocale, enabledLocales);
161
201
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: layoutBlocks.map(
162
- (block) => renderResolvedBlock(block, map, locale, defaultLocale, context)
202
+ (block) => renderResolvedBlock(block, map, locale, defaultLocale, {
203
+ context,
204
+ enabledLocales
205
+ })
163
206
  ) });
164
207
  }
165
208
 
@@ -547,6 +590,46 @@ async function resolveForms(config, blocks, locale, defaultLocale, options) {
547
590
  }
548
591
  return forms;
549
592
  }
593
+
594
+ // src/data/site-locales.ts
595
+ var TTL_MS = 6e4;
596
+ var MAX_ENTRIES = 64;
597
+ var cache = /* @__PURE__ */ new Map();
598
+ async function resolveSiteLocales(config, options) {
599
+ const key = `${config.apiUrl}::${config.workspaceSlug}`;
600
+ const cached = cache.get(key);
601
+ if (cached && cached.expires > Date.now()) return cached.value;
602
+ cache.delete(key);
603
+ let value;
604
+ try {
605
+ const data = await graphqlRequest(
606
+ config,
607
+ SITE_CONFIG_QUERY,
608
+ { workspaceSlug: config.workspaceSlug },
609
+ options,
610
+ "site config"
611
+ );
612
+ const siteConfig = data.publicSiteConfig;
613
+ const defaultLocale = siteConfig?.defaultLanguage || "en";
614
+ const enabled = siteConfig?.enabledLanguages ?? [];
615
+ value = {
616
+ defaultLocale,
617
+ locales: enabled.length > 0 ? enabled : [defaultLocale]
618
+ };
619
+ } catch {
620
+ value = { defaultLocale: "en", locales: ["en"] };
621
+ }
622
+ if (cache.size >= MAX_ENTRIES) cache.clear();
623
+ cache.set(key, { value, expires: Date.now() + TTL_MS });
624
+ return value;
625
+ }
626
+ function splitLocaleFromPath(path, siteLocales) {
627
+ const first = path?.[0];
628
+ if (first && first !== siteLocales.defaultLocale && siteLocales.locales.includes(first)) {
629
+ return { locale: first, path: path.slice(1) };
630
+ }
631
+ return { locale: siteLocales.defaultLocale, path };
632
+ }
550
633
  function CmssyBlock({
551
634
  block,
552
635
  locale,
@@ -601,3 +684,5 @@ exports.normalizeSlug = normalizeSlug;
601
684
  exports.parseEditorMessage = parseEditorMessage;
602
685
  exports.postToEditor = postToEditor;
603
686
  exports.resolveForms = resolveForms;
687
+ exports.resolveSiteLocales = resolveSiteLocales;
688
+ exports.splitLocaleFromPath = splitLocaleFromPath;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { F as FieldDefinition, C as CmssyPageData, b as BlockDefinition, c as CmssyFormDefinition, d as CmssyLayoutGroup, E as EditorToAppMessage, A as AppToEditorMessage, e as FetchLike, f as CmssyClientConfig, R as RawBlock, g as BlockMap, h as CmssyBlockContext } from './registry-BaFQ2k7r.cjs';
2
- export { a as BlockMeta, i as BlockRect, B as BlockSchema, j as BoundsMessage, k as ClickMessage, l as CmssyBranding, m as CmssyFormField, n as CmssyFormSettings, o as CmssyFormSubmitResponse, p as CmssyLocaleContext, q as CmssyModelDefinition, r as CmssyModelRecord, s as CmssyRecordList, t as CmssySiteConfig, u as FORM_QUERY, v as FetchLikeResponse, w as FetchPageOptions, x as FieldType, M as MODEL_DEFINITIONS_QUERY, y as MODEL_RECORDS_QUERY, P as PROTOCOL_VERSION, z as ParentReadyMessage, D as PatchMessage, G as RawLayoutBlock, H as ReadyMessage, S as SITE_CONFIG_QUERY, I as SUBMIT_FORM_MUTATION, J as SelectMessage, K as SubmitFormInput, L as blocksToMeta, N as blocksToSchemas, O as buildBlockContext, Q as buildBlockMap, T as defineBlock, U as fetchLayouts, V as fetchPage, W as isProtocolCompatible, X as normalizeSlug } from './registry-BaFQ2k7r.cjs';
1
+ import { F as FieldDefinition, C as CmssyPageData, b as BlockDefinition, c as CmssyFormDefinition, d as CmssyLayoutGroup, E as EditorToAppMessage, A as AppToEditorMessage, e as FetchLike, f as CmssyClientConfig, R as RawBlock, g as BlockMap, h as CmssyBlockContext } from './registry-e3O4s_bN.cjs';
2
+ export { a as BlockMeta, i as BlockRect, B as BlockSchema, j as BoundsMessage, k as ClickMessage, l as CmssyBranding, m as CmssyFormField, n as CmssyFormSettings, o as CmssyFormSubmitResponse, p as CmssyLocaleContext, q as CmssyModelDefinition, r as CmssyModelRecord, s as CmssyRecordList, t as CmssySiteConfig, u as FORM_QUERY, v as FetchLikeResponse, w as FetchPageOptions, x as FieldType, M as MODEL_DEFINITIONS_QUERY, y as MODEL_RECORDS_QUERY, P as PROTOCOL_VERSION, z as ParentReadyMessage, D as PatchMessage, G as RawLayoutBlock, H as ReadyMessage, S as SITE_CONFIG_QUERY, I as SUBMIT_FORM_MUTATION, J as SelectMessage, K as SubmitFormInput, L as blocksToMeta, N as blocksToSchemas, O as buildBlockContext, Q as buildBlockMap, T as defineBlock, U as fetchLayouts, V as fetchPage, W as isProtocolCompatible, X as normalizeSlug } from './registry-e3O4s_bN.cjs';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import 'react';
5
5
 
@@ -31,7 +31,12 @@ interface CmssyServerPageProps {
31
31
  /** Form definitions referenced by page blocks, exposed via context.forms. */
32
32
  forms?: Record<string, CmssyFormDefinition>;
33
33
  }
34
- declare function CmssyServerPage({ page, blocks, locale, defaultLocale, enabledLocales, forms, }: CmssyServerPageProps): react_jsx_runtime.JSX.Element | null;
34
+ /**
35
+ * Async React Server Component (Next.js App Router / RSC). It runs each block's
36
+ * loader server-side before rendering, so it must be rendered in a server
37
+ * component tree (as `createCmssyPage` does) - not in a client component.
38
+ */
39
+ declare function CmssyServerPage({ page, blocks, locale, defaultLocale, enabledLocales, forms, }: CmssyServerPageProps): Promise<react_jsx_runtime.JSX.Element | null>;
35
40
 
36
41
  interface CmssyServerLayoutProps {
37
42
  groups: CmssyLayoutGroup[];
@@ -74,6 +79,16 @@ declare function createCmssyClient(config: CmssyClientConfig): CmssyClient;
74
79
  declare function collectFormIds(blocks: RawBlock[], locale: string, defaultLocale: string): string[];
75
80
  declare function resolveForms(config: CmssyClientConfig, blocks: RawBlock[], locale: string, defaultLocale: string, options?: QueryScopedOptions): Promise<Record<string, CmssyFormDefinition>>;
76
81
 
82
+ interface CmssySiteLocales {
83
+ defaultLocale: string;
84
+ locales: string[];
85
+ }
86
+ declare function resolveSiteLocales(config: CmssyClientConfig, options?: GraphqlRequestOptions): Promise<CmssySiteLocales>;
87
+ declare function splitLocaleFromPath(path: string[] | undefined, siteLocales: CmssySiteLocales): {
88
+ locale: string;
89
+ path: string[] | undefined;
90
+ };
91
+
77
92
  interface CmssyBlockProps {
78
93
  block: RawBlock;
79
94
  locale: string;
@@ -91,4 +106,4 @@ interface UnknownBlockProps {
91
106
  }
92
107
  declare function UnknownBlock({ type }: UnknownBlockProps): react_jsx_runtime.JSX.Element;
93
108
 
94
- export { AppToEditorMessage, BlockDefinition, BlockMap, CmssyBlock, CmssyBlockContext, type CmssyBlockProps, type CmssyClient, CmssyClientConfig, CmssyFormDefinition, CmssyLayoutGroup, CmssyPageData, CmssyServerLayout, type CmssyServerLayoutProps, CmssyServerPage, type CmssyServerPageProps, EditorToAppMessage, FetchLike, type FieldControl, FieldDefinition, type GraphqlRequestOptions, type PostTarget, type QueryScopedOptions, RawBlock, UnknownBlock, type UnknownBlockProps, collectFormIds, createCmssyClient, fields, getBlockContentForLanguage, graphqlRequest, normalizeOrigin, parseEditorMessage, postToEditor, resolveForms };
109
+ export { AppToEditorMessage, BlockDefinition, BlockMap, CmssyBlock, CmssyBlockContext, type CmssyBlockProps, type CmssyClient, CmssyClientConfig, CmssyFormDefinition, CmssyLayoutGroup, CmssyPageData, CmssyServerLayout, type CmssyServerLayoutProps, CmssyServerPage, type CmssyServerPageProps, type CmssySiteLocales, EditorToAppMessage, FetchLike, type FieldControl, FieldDefinition, type GraphqlRequestOptions, type PostTarget, type QueryScopedOptions, RawBlock, UnknownBlock, type UnknownBlockProps, collectFormIds, createCmssyClient, fields, getBlockContentForLanguage, graphqlRequest, normalizeOrigin, parseEditorMessage, postToEditor, resolveForms, resolveSiteLocales, splitLocaleFromPath };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { F as FieldDefinition, C as CmssyPageData, b as BlockDefinition, c as CmssyFormDefinition, d as CmssyLayoutGroup, E as EditorToAppMessage, A as AppToEditorMessage, e as FetchLike, f as CmssyClientConfig, R as RawBlock, g as BlockMap, h as CmssyBlockContext } from './registry-BaFQ2k7r.js';
2
- export { a as BlockMeta, i as BlockRect, B as BlockSchema, j as BoundsMessage, k as ClickMessage, l as CmssyBranding, m as CmssyFormField, n as CmssyFormSettings, o as CmssyFormSubmitResponse, p as CmssyLocaleContext, q as CmssyModelDefinition, r as CmssyModelRecord, s as CmssyRecordList, t as CmssySiteConfig, u as FORM_QUERY, v as FetchLikeResponse, w as FetchPageOptions, x as FieldType, M as MODEL_DEFINITIONS_QUERY, y as MODEL_RECORDS_QUERY, P as PROTOCOL_VERSION, z as ParentReadyMessage, D as PatchMessage, G as RawLayoutBlock, H as ReadyMessage, S as SITE_CONFIG_QUERY, I as SUBMIT_FORM_MUTATION, J as SelectMessage, K as SubmitFormInput, L as blocksToMeta, N as blocksToSchemas, O as buildBlockContext, Q as buildBlockMap, T as defineBlock, U as fetchLayouts, V as fetchPage, W as isProtocolCompatible, X as normalizeSlug } from './registry-BaFQ2k7r.js';
1
+ import { F as FieldDefinition, C as CmssyPageData, b as BlockDefinition, c as CmssyFormDefinition, d as CmssyLayoutGroup, E as EditorToAppMessage, A as AppToEditorMessage, e as FetchLike, f as CmssyClientConfig, R as RawBlock, g as BlockMap, h as CmssyBlockContext } from './registry-e3O4s_bN.js';
2
+ export { a as BlockMeta, i as BlockRect, B as BlockSchema, j as BoundsMessage, k as ClickMessage, l as CmssyBranding, m as CmssyFormField, n as CmssyFormSettings, o as CmssyFormSubmitResponse, p as CmssyLocaleContext, q as CmssyModelDefinition, r as CmssyModelRecord, s as CmssyRecordList, t as CmssySiteConfig, u as FORM_QUERY, v as FetchLikeResponse, w as FetchPageOptions, x as FieldType, M as MODEL_DEFINITIONS_QUERY, y as MODEL_RECORDS_QUERY, P as PROTOCOL_VERSION, z as ParentReadyMessage, D as PatchMessage, G as RawLayoutBlock, H as ReadyMessage, S as SITE_CONFIG_QUERY, I as SUBMIT_FORM_MUTATION, J as SelectMessage, K as SubmitFormInput, L as blocksToMeta, N as blocksToSchemas, O as buildBlockContext, Q as buildBlockMap, T as defineBlock, U as fetchLayouts, V as fetchPage, W as isProtocolCompatible, X as normalizeSlug } from './registry-e3O4s_bN.js';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import 'react';
5
5
 
@@ -31,7 +31,12 @@ interface CmssyServerPageProps {
31
31
  /** Form definitions referenced by page blocks, exposed via context.forms. */
32
32
  forms?: Record<string, CmssyFormDefinition>;
33
33
  }
34
- declare function CmssyServerPage({ page, blocks, locale, defaultLocale, enabledLocales, forms, }: CmssyServerPageProps): react_jsx_runtime.JSX.Element | null;
34
+ /**
35
+ * Async React Server Component (Next.js App Router / RSC). It runs each block's
36
+ * loader server-side before rendering, so it must be rendered in a server
37
+ * component tree (as `createCmssyPage` does) - not in a client component.
38
+ */
39
+ declare function CmssyServerPage({ page, blocks, locale, defaultLocale, enabledLocales, forms, }: CmssyServerPageProps): Promise<react_jsx_runtime.JSX.Element | null>;
35
40
 
36
41
  interface CmssyServerLayoutProps {
37
42
  groups: CmssyLayoutGroup[];
@@ -74,6 +79,16 @@ declare function createCmssyClient(config: CmssyClientConfig): CmssyClient;
74
79
  declare function collectFormIds(blocks: RawBlock[], locale: string, defaultLocale: string): string[];
75
80
  declare function resolveForms(config: CmssyClientConfig, blocks: RawBlock[], locale: string, defaultLocale: string, options?: QueryScopedOptions): Promise<Record<string, CmssyFormDefinition>>;
76
81
 
82
+ interface CmssySiteLocales {
83
+ defaultLocale: string;
84
+ locales: string[];
85
+ }
86
+ declare function resolveSiteLocales(config: CmssyClientConfig, options?: GraphqlRequestOptions): Promise<CmssySiteLocales>;
87
+ declare function splitLocaleFromPath(path: string[] | undefined, siteLocales: CmssySiteLocales): {
88
+ locale: string;
89
+ path: string[] | undefined;
90
+ };
91
+
77
92
  interface CmssyBlockProps {
78
93
  block: RawBlock;
79
94
  locale: string;
@@ -91,4 +106,4 @@ interface UnknownBlockProps {
91
106
  }
92
107
  declare function UnknownBlock({ type }: UnknownBlockProps): react_jsx_runtime.JSX.Element;
93
108
 
94
- export { AppToEditorMessage, BlockDefinition, BlockMap, CmssyBlock, CmssyBlockContext, type CmssyBlockProps, type CmssyClient, CmssyClientConfig, CmssyFormDefinition, CmssyLayoutGroup, CmssyPageData, CmssyServerLayout, type CmssyServerLayoutProps, CmssyServerPage, type CmssyServerPageProps, EditorToAppMessage, FetchLike, type FieldControl, FieldDefinition, type GraphqlRequestOptions, type PostTarget, type QueryScopedOptions, RawBlock, UnknownBlock, type UnknownBlockProps, collectFormIds, createCmssyClient, fields, getBlockContentForLanguage, graphqlRequest, normalizeOrigin, parseEditorMessage, postToEditor, resolveForms };
109
+ export { AppToEditorMessage, BlockDefinition, BlockMap, CmssyBlock, CmssyBlockContext, type CmssyBlockProps, type CmssyClient, CmssyClientConfig, CmssyFormDefinition, CmssyLayoutGroup, CmssyPageData, CmssyServerLayout, type CmssyServerLayoutProps, CmssyServerPage, type CmssyServerPageProps, type CmssySiteLocales, EditorToAppMessage, FetchLike, type FieldControl, FieldDefinition, type GraphqlRequestOptions, type PostTarget, type QueryScopedOptions, RawBlock, UnknownBlock, type UnknownBlockProps, collectFormIds, createCmssyClient, fields, getBlockContentForLanguage, graphqlRequest, normalizeOrigin, parseEditorMessage, postToEditor, resolveForms, resolveSiteLocales, splitLocaleFromPath };
package/dist/index.js CHANGED
@@ -33,6 +33,13 @@ function buildBlockMap(blocks) {
33
33
  for (const block of blocks) map[block.type] = block.component;
34
34
  return map;
35
35
  }
36
+ function buildLoaderMap(blocks) {
37
+ const map = /* @__PURE__ */ Object.create(null);
38
+ for (const block of blocks) {
39
+ if (block.loader) map[block.type] = block.loader;
40
+ }
41
+ return map;
42
+ }
36
43
  function blocksToSchemas(blocks) {
37
44
  const out = /* @__PURE__ */ Object.create(null);
38
45
  for (const block of blocks) {
@@ -58,19 +65,6 @@ function blocksToMeta(blocks, defaults = {}) {
58
65
  return out;
59
66
  }
60
67
 
61
- // src/components/block-context.ts
62
- function buildBlockContext(locale, defaultLocale, enabledLocales, isPreview, forms) {
63
- return {
64
- locale: {
65
- current: locale,
66
- default: defaultLocale,
67
- enabled: enabledLocales && enabledLocales.length > 0 ? enabledLocales : Array.from(/* @__PURE__ */ new Set([defaultLocale, locale]))
68
- },
69
- isPreview: isPreview ?? false,
70
- forms
71
- };
72
- }
73
-
74
68
  // src/content/get-block-content.ts
75
69
  function isPlainObject(value) {
76
70
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -94,6 +88,19 @@ function getBlockContentForLanguage(content, locale, defaultLocale = "en", avail
94
88
  const chosen = localeMap[locale] ?? localeMap[defaultLocale] ?? localeMap[fallbackKey];
95
89
  return { ...nonTranslatable, ...chosen };
96
90
  }
91
+
92
+ // src/components/block-context.ts
93
+ function buildBlockContext(locale, defaultLocale, enabledLocales, isPreview, forms) {
94
+ return {
95
+ locale: {
96
+ current: locale,
97
+ default: defaultLocale,
98
+ enabled: enabledLocales && enabledLocales.length > 0 ? enabledLocales : Array.from(/* @__PURE__ */ new Set([defaultLocale, locale]))
99
+ },
100
+ isPreview: isPreview ?? false,
101
+ forms
102
+ };
103
+ }
97
104
  var WARN_CAP = 256;
98
105
  var warned = /* @__PURE__ */ new Set();
99
106
  function UnknownBlock({ type }) {
@@ -104,12 +111,14 @@ function UnknownBlock({ type }) {
104
111
  }
105
112
  return /* @__PURE__ */ jsx("div", { "data-cmssy-unknown-block": type });
106
113
  }
107
- function renderResolvedBlock(block, map, locale, defaultLocale, context) {
114
+ function renderResolvedBlock(block, map, locale, defaultLocale, options = {}) {
115
+ const { context, data, resolvedContent, enabledLocales } = options;
108
116
  const Component = Object.hasOwn(map, block.type) ? map[block.type] : void 0;
109
- const content = getBlockContentForLanguage(
117
+ const content = resolvedContent ?? getBlockContentForLanguage(
110
118
  block.content,
111
119
  locale,
112
- defaultLocale
120
+ defaultLocale,
121
+ enabledLocales?.length ? enabledLocales : void 0
113
122
  );
114
123
  return /* @__PURE__ */ jsx(
115
124
  "div",
@@ -117,12 +126,12 @@ function renderResolvedBlock(block, map, locale, defaultLocale, context) {
117
126
  "data-block-id": block.id,
118
127
  "data-block-type": block.type,
119
128
  style: Component ? void 0 : { display: "none" },
120
- children: Component ? createElement(Component, { content, context }) : /* @__PURE__ */ jsx(UnknownBlock, { type: block.type })
129
+ children: Component ? createElement(Component, { content, context, data }) : /* @__PURE__ */ jsx(UnknownBlock, { type: block.type })
121
130
  },
122
131
  block.id
123
132
  );
124
133
  }
125
- function CmssyServerPage({
134
+ async function CmssyServerPage({
126
135
  page,
127
136
  blocks,
128
137
  locale = "en",
@@ -132,6 +141,7 @@ function CmssyServerPage({
132
141
  }) {
133
142
  if (!page) return null;
134
143
  const map = buildBlockMap(blocks);
144
+ const loaderMap = buildLoaderMap(blocks);
135
145
  const context = buildBlockContext(
136
146
  locale,
137
147
  defaultLocale,
@@ -139,8 +149,38 @@ function CmssyServerPage({
139
149
  false,
140
150
  forms
141
151
  );
152
+ const resolved = await Promise.all(
153
+ page.blocks.map(async (block) => {
154
+ const content = getBlockContentForLanguage(
155
+ block.content,
156
+ locale,
157
+ defaultLocale,
158
+ enabledLocales?.length ? enabledLocales : void 0
159
+ );
160
+ const loader = loaderMap[block.type];
161
+ let data;
162
+ if (loader) {
163
+ try {
164
+ data = await loader({ content, context });
165
+ } catch (err) {
166
+ if (typeof console !== "undefined") {
167
+ console.warn(
168
+ `[cmssy] loader for block "${block.type}" (${block.id}) failed`,
169
+ err
170
+ );
171
+ }
172
+ }
173
+ }
174
+ return { content, data };
175
+ })
176
+ );
142
177
  return /* @__PURE__ */ jsx(Fragment, { children: page.blocks.map(
143
- (block) => renderResolvedBlock(block, map, locale, defaultLocale, context)
178
+ (block, i) => renderResolvedBlock(block, map, locale, defaultLocale, {
179
+ context,
180
+ data: resolved[i]?.data,
181
+ resolvedContent: resolved[i]?.content,
182
+ enabledLocales
183
+ })
144
184
  ) });
145
185
  }
146
186
  function CmssyServerLayout({
@@ -157,7 +197,10 @@ function CmssyServerLayout({
157
197
  const map = buildBlockMap(blocks);
158
198
  const context = buildBlockContext(locale, defaultLocale, enabledLocales);
159
199
  return /* @__PURE__ */ jsx(Fragment, { children: layoutBlocks.map(
160
- (block) => renderResolvedBlock(block, map, locale, defaultLocale, context)
200
+ (block) => renderResolvedBlock(block, map, locale, defaultLocale, {
201
+ context,
202
+ enabledLocales
203
+ })
161
204
  ) });
162
205
  }
163
206
 
@@ -545,6 +588,46 @@ async function resolveForms(config, blocks, locale, defaultLocale, options) {
545
588
  }
546
589
  return forms;
547
590
  }
591
+
592
+ // src/data/site-locales.ts
593
+ var TTL_MS = 6e4;
594
+ var MAX_ENTRIES = 64;
595
+ var cache = /* @__PURE__ */ new Map();
596
+ async function resolveSiteLocales(config, options) {
597
+ const key = `${config.apiUrl}::${config.workspaceSlug}`;
598
+ const cached = cache.get(key);
599
+ if (cached && cached.expires > Date.now()) return cached.value;
600
+ cache.delete(key);
601
+ let value;
602
+ try {
603
+ const data = await graphqlRequest(
604
+ config,
605
+ SITE_CONFIG_QUERY,
606
+ { workspaceSlug: config.workspaceSlug },
607
+ options,
608
+ "site config"
609
+ );
610
+ const siteConfig = data.publicSiteConfig;
611
+ const defaultLocale = siteConfig?.defaultLanguage || "en";
612
+ const enabled = siteConfig?.enabledLanguages ?? [];
613
+ value = {
614
+ defaultLocale,
615
+ locales: enabled.length > 0 ? enabled : [defaultLocale]
616
+ };
617
+ } catch {
618
+ value = { defaultLocale: "en", locales: ["en"] };
619
+ }
620
+ if (cache.size >= MAX_ENTRIES) cache.clear();
621
+ cache.set(key, { value, expires: Date.now() + TTL_MS });
622
+ return value;
623
+ }
624
+ function splitLocaleFromPath(path, siteLocales) {
625
+ const first = path?.[0];
626
+ if (first && first !== siteLocales.defaultLocale && siteLocales.locales.includes(first)) {
627
+ return { locale: first, path: path.slice(1) };
628
+ }
629
+ return { locale: siteLocales.defaultLocale, path };
630
+ }
548
631
  function CmssyBlock({
549
632
  block,
550
633
  locale,
@@ -571,4 +654,4 @@ function CmssyBlock({
571
654
  );
572
655
  }
573
656
 
574
- export { CmssyBlock, CmssyServerLayout, CmssyServerPage, FORM_QUERY, MODEL_DEFINITIONS_QUERY, MODEL_RECORDS_QUERY, PROTOCOL_VERSION, SITE_CONFIG_QUERY, SUBMIT_FORM_MUTATION, UnknownBlock, blocksToMeta, blocksToSchemas, buildBlockContext, buildBlockMap, collectFormIds, createCmssyClient, defineBlock, fetchLayouts, fetchPage, fields, getBlockContentForLanguage, graphqlRequest, isProtocolCompatible, normalizeOrigin, normalizeSlug, parseEditorMessage, postToEditor, resolveForms };
657
+ export { CmssyBlock, CmssyServerLayout, CmssyServerPage, FORM_QUERY, MODEL_DEFINITIONS_QUERY, MODEL_RECORDS_QUERY, PROTOCOL_VERSION, SITE_CONFIG_QUERY, SUBMIT_FORM_MUTATION, UnknownBlock, blocksToMeta, blocksToSchemas, buildBlockContext, buildBlockMap, collectFormIds, createCmssyClient, defineBlock, fetchLayouts, fetchPage, fields, getBlockContentForLanguage, graphqlRequest, isProtocolCompatible, normalizeOrigin, normalizeSlug, parseEditorMessage, postToEditor, resolveForms, resolveSiteLocales, splitLocaleFromPath };
@@ -256,6 +256,11 @@ interface DragEndMessage {
256
256
  type EditorToAppMessage = SelectMessage | PatchMessage | ParentReadyMessage | InsertMessage | ReorderMessage | RemoveMessage | DragOverMessage | DragEndMessage;
257
257
  declare function isProtocolCompatible(version: number): boolean;
258
258
 
259
+ interface BlockLoaderArgs {
260
+ content: Record<string, unknown>;
261
+ context?: CmssyBlockContext;
262
+ }
263
+ type BlockLoader = (args: BlockLoaderArgs) => Promise<unknown> | unknown;
259
264
  interface BlockDefinition {
260
265
  type: string;
261
266
  label?: string;
@@ -263,26 +268,43 @@ interface BlockDefinition {
263
268
  icon?: string;
264
269
  layoutPositions?: string[];
265
270
  props: Record<string, FieldDefinition>;
271
+ /**
272
+ * Optional server-side data loader. Run by CmssyServerPage during SSR; its
273
+ * result is passed to the component as the `data` prop. Not run in the
274
+ * editor (the component receives `data: undefined` there).
275
+ *
276
+ * The result crosses the server→client boundary when the block component is a
277
+ * Client Component, so it must be RSC-serializable (plain objects, arrays and
278
+ * primitives - no functions, class instances, etc.).
279
+ */
280
+ loader?: BlockLoader;
266
281
  component: ComponentType<{
267
282
  content: Record<string, unknown>;
268
283
  context?: CmssyBlockContext;
284
+ data?: unknown;
269
285
  }>;
270
286
  }
271
- declare function defineBlock<C extends Record<string, unknown>>(def: {
287
+ declare function defineBlock<C extends Record<string, unknown>, D = unknown>(def: {
272
288
  type: string;
273
289
  label?: string;
274
290
  category?: string;
275
291
  icon?: string;
276
292
  layoutPositions?: string[];
277
293
  props: Record<string, FieldDefinition>;
294
+ loader?: (args: {
295
+ content: C;
296
+ context?: CmssyBlockContext;
297
+ }) => Promise<D> | D;
278
298
  component: ComponentType<{
279
299
  content: C;
280
300
  context?: CmssyBlockContext;
301
+ data?: D;
281
302
  }>;
282
303
  }): BlockDefinition;
283
304
  type BlockMap = Record<string, ComponentType<{
284
305
  content: Record<string, unknown>;
285
306
  context?: CmssyBlockContext;
307
+ data?: unknown;
286
308
  }>>;
287
309
  declare function buildBlockMap(blocks: BlockDefinition[]): BlockMap;
288
310
  declare function blocksToSchemas(blocks: BlockDefinition[]): Record<string, BlockSchema>;
@@ -256,6 +256,11 @@ interface DragEndMessage {
256
256
  type EditorToAppMessage = SelectMessage | PatchMessage | ParentReadyMessage | InsertMessage | ReorderMessage | RemoveMessage | DragOverMessage | DragEndMessage;
257
257
  declare function isProtocolCompatible(version: number): boolean;
258
258
 
259
+ interface BlockLoaderArgs {
260
+ content: Record<string, unknown>;
261
+ context?: CmssyBlockContext;
262
+ }
263
+ type BlockLoader = (args: BlockLoaderArgs) => Promise<unknown> | unknown;
259
264
  interface BlockDefinition {
260
265
  type: string;
261
266
  label?: string;
@@ -263,26 +268,43 @@ interface BlockDefinition {
263
268
  icon?: string;
264
269
  layoutPositions?: string[];
265
270
  props: Record<string, FieldDefinition>;
271
+ /**
272
+ * Optional server-side data loader. Run by CmssyServerPage during SSR; its
273
+ * result is passed to the component as the `data` prop. Not run in the
274
+ * editor (the component receives `data: undefined` there).
275
+ *
276
+ * The result crosses the server→client boundary when the block component is a
277
+ * Client Component, so it must be RSC-serializable (plain objects, arrays and
278
+ * primitives - no functions, class instances, etc.).
279
+ */
280
+ loader?: BlockLoader;
266
281
  component: ComponentType<{
267
282
  content: Record<string, unknown>;
268
283
  context?: CmssyBlockContext;
284
+ data?: unknown;
269
285
  }>;
270
286
  }
271
- declare function defineBlock<C extends Record<string, unknown>>(def: {
287
+ declare function defineBlock<C extends Record<string, unknown>, D = unknown>(def: {
272
288
  type: string;
273
289
  label?: string;
274
290
  category?: string;
275
291
  icon?: string;
276
292
  layoutPositions?: string[];
277
293
  props: Record<string, FieldDefinition>;
294
+ loader?: (args: {
295
+ content: C;
296
+ context?: CmssyBlockContext;
297
+ }) => Promise<D> | D;
278
298
  component: ComponentType<{
279
299
  content: C;
280
300
  context?: CmssyBlockContext;
301
+ data?: D;
281
302
  }>;
282
303
  }): BlockDefinition;
283
304
  type BlockMap = Record<string, ComponentType<{
284
305
  content: Record<string, unknown>;
285
306
  context?: CmssyBlockContext;
307
+ data?: unknown;
286
308
  }>>;
287
309
  declare function buildBlockMap(blocks: BlockDefinition[]): BlockMap;
288
310
  declare function blocksToSchemas(blocks: BlockDefinition[]): Record<string, BlockSchema>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/react",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "React blocks, renderers, data client and editor bridge for cmssy headless sites",
5
5
  "keywords": [
6
6
  "cmssy",