@cmssy/react 16.9.1 → 16.10.0

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/spa.js ADDED
@@ -0,0 +1,636 @@
1
+ "use client";
2
+ import { useMemo, useState, useRef, useEffect, useCallback, createElement } from 'react';
3
+ import { layoutRegionIds, fetchPage, buildBlockContext, fetchLayouts, normalizeBlockContent, asBucket, getBlockContentForLanguage, resolveRelationContent } from '@cmssy/core/internal';
4
+ import { resolveEditorOrigin, blocksToSchemas } from '@cmssy/core';
5
+ import { resolveSiteLocales, splitLocaleFromPath } from '@cmssy/core/internal/locale';
6
+ import { BlockErrorBoundary } from '@cmssy/react/block-error-boundary';
7
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
8
+
9
+ function buildBlockMap(blocks) {
10
+ const map = /* @__PURE__ */ Object.create(null);
11
+ for (const block of blocks) map[block.type] = block.component;
12
+ return map;
13
+ }
14
+ function buildLoaderMap(blocks) {
15
+ const map = /* @__PURE__ */ Object.create(null);
16
+ for (const block of blocks) {
17
+ if (block.loader) map[block.type] = block.loader;
18
+ }
19
+ return map;
20
+ }
21
+
22
+ // src/components/block-error.ts
23
+ var BLOCK_ERROR_KEY = "__cmssyBlockError";
24
+ var SOURCES = [
25
+ "loader",
26
+ "render",
27
+ "unregistered"
28
+ ];
29
+ function blockErrorMessage(err) {
30
+ return err instanceof Error ? err.message : String(err);
31
+ }
32
+ function unregisteredBlockError(type) {
33
+ return {
34
+ source: "unregistered",
35
+ message: `Block type "${type}" is not registered in this site's blocks array.`
36
+ };
37
+ }
38
+ function markBlockError(error) {
39
+ return { [BLOCK_ERROR_KEY]: error };
40
+ }
41
+ function readBlockError(value) {
42
+ if (typeof value !== "object" || value === null) return void 0;
43
+ const marked = value[BLOCK_ERROR_KEY];
44
+ if (typeof marked !== "object" || marked === null) return void 0;
45
+ const { message, source } = marked;
46
+ if (typeof message !== "string") return void 0;
47
+ if (!SOURCES.includes(source)) return void 0;
48
+ return { message, source };
49
+ }
50
+ async function resolveFoldedBlocks(blocks, contents, loaderMap, locale, context, options) {
51
+ const schemas = options?.schemas;
52
+ if (schemas) {
53
+ if (options?.config) {
54
+ await resolveRelationContent(
55
+ options.config,
56
+ blocks.map((block, i) => ({ type: block.type, content: contents[i] })),
57
+ schemas,
58
+ locale,
59
+ options.workspaceId ? { workspaceId: options.workspaceId } : {}
60
+ );
61
+ }
62
+ blocks.forEach((block, i) => {
63
+ const schema = schemas[block.type];
64
+ if (schema) normalizeBlockContent(contents[i], schema);
65
+ });
66
+ }
67
+ return Promise.all(
68
+ blocks.map(async (block, i) => {
69
+ const content = contents[i];
70
+ const loader = loaderMap[block.type];
71
+ let data;
72
+ let error;
73
+ if (loader) {
74
+ try {
75
+ data = await loader({ content, context });
76
+ } catch (err) {
77
+ if (typeof console !== "undefined") {
78
+ console.error(
79
+ `[cmssy] loader for block "${block.type}" (${block.id}) failed`,
80
+ err
81
+ );
82
+ }
83
+ error = { message: blockErrorMessage(err), source: "loader" };
84
+ }
85
+ }
86
+ return { content, data, error };
87
+ })
88
+ );
89
+ }
90
+ async function resolveBlocks(blocks, loaderMap, locale, defaultLocale, context, enabledLocales, options) {
91
+ const contents = blocks.map(
92
+ (block) => getBlockContentForLanguage(
93
+ block.content,
94
+ locale,
95
+ defaultLocale,
96
+ enabledLocales?.length ? enabledLocales : void 0
97
+ )
98
+ );
99
+ return resolveFoldedBlocks(
100
+ blocks,
101
+ contents,
102
+ loaderMap,
103
+ locale,
104
+ context,
105
+ options
106
+ );
107
+ }
108
+
109
+ // src/components/resolve-block-data.ts
110
+ function collectBlockData(blocks, resolved, isPreview) {
111
+ const data = {};
112
+ const content = {};
113
+ blocks.forEach((block, index) => {
114
+ const entry = resolved[index];
115
+ if (!entry) return;
116
+ content[block.id] = entry.content;
117
+ if (entry.error && isPreview) {
118
+ data[block.id] = markBlockError(entry.error);
119
+ } else if (entry.data !== void 0) {
120
+ data[block.id] = entry.data;
121
+ }
122
+ });
123
+ return { data, content };
124
+ }
125
+ async function resolveEditorBlockData({
126
+ page,
127
+ blocks,
128
+ locale,
129
+ defaultLocale,
130
+ enabledLocales,
131
+ forms,
132
+ isPreview = false,
133
+ config,
134
+ appContext
135
+ }) {
136
+ if (!page) return { data: {}, content: {} };
137
+ const loaderMap = buildLoaderMap(blocks);
138
+ const context = buildBlockContext(
139
+ locale,
140
+ defaultLocale,
141
+ enabledLocales,
142
+ isPreview,
143
+ forms,
144
+ { page, app: appContext }
145
+ );
146
+ const resolved = await resolveBlocks(
147
+ page.blocks,
148
+ loaderMap,
149
+ locale,
150
+ defaultLocale,
151
+ context,
152
+ enabledLocales,
153
+ { schemas: blocksToSchemas(blocks), config }
154
+ );
155
+ return collectBlockData(page.blocks, resolved, isPreview);
156
+ }
157
+ async function resolveEditorLayoutBlockData({
158
+ groups,
159
+ blocks,
160
+ region,
161
+ page,
162
+ locale,
163
+ defaultLocale,
164
+ enabledLocales,
165
+ forms,
166
+ isPreview = false,
167
+ config,
168
+ appContext
169
+ }) {
170
+ const group = groups.find((g) => g.region === region);
171
+ const layoutBlocks = group ? group.blocks.filter((b) => b.isActive !== false).slice().sort((a, b) => a.order - b.order) : [];
172
+ if (layoutBlocks.length === 0) return { data: {}, content: {} };
173
+ const loaderMap = buildLoaderMap(blocks);
174
+ const context = buildBlockContext(
175
+ locale,
176
+ defaultLocale,
177
+ enabledLocales,
178
+ isPreview,
179
+ forms,
180
+ { page, app: appContext }
181
+ );
182
+ const resolved = await resolveBlocks(
183
+ layoutBlocks,
184
+ loaderMap,
185
+ locale,
186
+ defaultLocale,
187
+ context,
188
+ enabledLocales,
189
+ { schemas: blocksToSchemas(blocks), config }
190
+ );
191
+ return collectBlockData(layoutBlocks, resolved, isPreview);
192
+ }
193
+ function layoutSlotPage(slug) {
194
+ return { slug, path: slug.split("/").filter(Boolean) };
195
+ }
196
+ async function resolveCmssyLayoutSlot(config, options) {
197
+ const {
198
+ region,
199
+ blocks,
200
+ editMode,
201
+ preview = false,
202
+ page: explicitPage,
203
+ forms,
204
+ appContext,
205
+ path,
206
+ locale: explicitLocale,
207
+ retry,
208
+ fetch: fetchImpl
209
+ } = options;
210
+ const requestOptions = {
211
+ retry: retry ?? "build",
212
+ ...fetchImpl ? { fetch: fetchImpl } : {}
213
+ };
214
+ const siteLocales = await resolveSiteLocales(config, requestOptions);
215
+ const fromPath = path ? splitLocaleFromPath(path, siteLocales) : { locale: siteLocales.defaultLocale, path: [] };
216
+ const locale = explicitLocale ?? fromPath.locale;
217
+ const slugSegments = fromPath.path ?? [];
218
+ const page = layoutSlotPage(explicitPage ?? "/" + slugSegments.join("/"));
219
+ const groups = await fetchLayouts(config, page.slug, {
220
+ previewSecret: editMode || preview ? config.draftSecret : void 0,
221
+ ...requestOptions
222
+ });
223
+ const base = {
224
+ groups,
225
+ settings: groups.find((g) => g.region === region)?.settings ?? null,
226
+ page,
227
+ locale,
228
+ defaultLocale: siteLocales.defaultLocale,
229
+ enabledLocales: siteLocales.locales,
230
+ path: slugSegments
231
+ };
232
+ if (!editMode) return base;
233
+ const editorData = await resolveEditorLayoutBlockData({
234
+ groups,
235
+ blocks,
236
+ region,
237
+ page,
238
+ locale,
239
+ defaultLocale: siteLocales.defaultLocale,
240
+ enabledLocales: siteLocales.locales,
241
+ forms,
242
+ isPreview: true,
243
+ config,
244
+ appContext
245
+ });
246
+ return {
247
+ ...base,
248
+ data: editorData.data,
249
+ resolvedContent: editorData.content,
250
+ editorOrigin: resolveEditorOrigin(config.editorOrigin)
251
+ };
252
+ }
253
+
254
+ // src/components/load-cmssy-route.ts
255
+ async function loadCmssyRoute(config, options) {
256
+ const {
257
+ blocks,
258
+ path,
259
+ locale,
260
+ forms,
261
+ appContext,
262
+ previewSecret,
263
+ isPreview = false,
264
+ retry,
265
+ fetch: fetchImpl
266
+ } = options;
267
+ const regions = options.regions ?? layoutRegionIds(config.layout);
268
+ const firstRegion = regions[0] ?? "header";
269
+ const slot = await resolveCmssyLayoutSlot(config, {
270
+ region: firstRegion,
271
+ blocks,
272
+ editMode: false,
273
+ preview: isPreview,
274
+ forms,
275
+ appContext,
276
+ retry,
277
+ ...fetchImpl ? { fetch: fetchImpl } : {},
278
+ path: path ?? [],
279
+ ...locale === void 0 ? {} : { locale }
280
+ });
281
+ const secret = previewSecret ?? (isPreview ? config.draftSecret : void 0);
282
+ const page = await fetchPage(config, slot.path, {
283
+ ...secret ? { previewSecret: secret } : {},
284
+ ...retry === void 0 ? {} : { retry },
285
+ ...fetchImpl ? { fetch: fetchImpl } : {}
286
+ });
287
+ const shared = {
288
+ blocks,
289
+ locale: slot.locale,
290
+ defaultLocale: slot.defaultLocale,
291
+ enabledLocales: slot.enabledLocales,
292
+ forms,
293
+ isPreview,
294
+ config,
295
+ appContext
296
+ };
297
+ const [pageBlocks, ...layoutBlocks] = await Promise.all([
298
+ resolveEditorBlockData({ ...shared, page }),
299
+ ...regions.map(
300
+ (region) => resolveEditorLayoutBlockData({
301
+ ...shared,
302
+ groups: slot.groups,
303
+ page: slot.page,
304
+ region
305
+ })
306
+ )
307
+ ]);
308
+ return {
309
+ page,
310
+ layouts: slot.groups,
311
+ pageContext: slot.page,
312
+ locale: slot.locale,
313
+ defaultLocale: slot.defaultLocale,
314
+ enabledLocales: slot.enabledLocales,
315
+ path: slot.path,
316
+ blockData: pageBlocks.data,
317
+ blockContent: pageBlocks.content,
318
+ layoutData: Object.fromEntries(
319
+ regions.map((region, index) => [region, layoutBlocks[index]])
320
+ ),
321
+ regions
322
+ };
323
+ }
324
+ var SOURCE_LABELS = {
325
+ loader: "loader failed",
326
+ render: "render failed",
327
+ unregistered: "type not registered"
328
+ };
329
+ var cardStyle = {
330
+ boxSizing: "border-box",
331
+ margin: "8px 0",
332
+ padding: "12px 16px",
333
+ border: "1px dashed #ef4444",
334
+ borderRadius: "8px",
335
+ background: "rgba(239, 68, 68, 0.08)",
336
+ color: "#ef4444",
337
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace",
338
+ fontSize: "13px",
339
+ lineHeight: 1.5,
340
+ textAlign: "left"
341
+ };
342
+ var headingStyle = {
343
+ fontWeight: 700,
344
+ marginBottom: "4px"
345
+ };
346
+ var metaStyle = {
347
+ opacity: 0.8,
348
+ marginBottom: "4px"
349
+ };
350
+ var messageStyle = {
351
+ whiteSpace: "pre-wrap",
352
+ overflowWrap: "anywhere"
353
+ };
354
+ function BlockErrorCard({
355
+ blockType,
356
+ blockId,
357
+ error
358
+ }) {
359
+ return /* @__PURE__ */ jsxs("div", { role: "alert", "data-cmssy-block-error": error.source, style: cardStyle, children: [
360
+ /* @__PURE__ */ jsxs("div", { style: headingStyle, children: [
361
+ 'Block "',
362
+ blockType,
363
+ '" - ',
364
+ SOURCE_LABELS[error.source]
365
+ ] }),
366
+ /* @__PURE__ */ jsxs("div", { style: metaStyle, children: [
367
+ "id: ",
368
+ blockId
369
+ ] }),
370
+ /* @__PURE__ */ jsx("div", { style: messageStyle, children: error.message })
371
+ ] });
372
+ }
373
+ function foldBlockContent(block, locale, defaultLocale, patchedContent, resolvedContent) {
374
+ const base = resolvedContent ? { ...resolvedContent } : getBlockContentForLanguage(block.content, locale, defaultLocale);
375
+ return patchedContent ? { ...base, ...patchedContent } : base;
376
+ }
377
+ var WARN_CAP = 256;
378
+ var warned = /* @__PURE__ */ new Set();
379
+ function UnknownBlock({ type }) {
380
+ if (typeof window !== "undefined" && !warned.has(type)) {
381
+ if (warned.size >= WARN_CAP) warned.clear();
382
+ warned.add(type);
383
+ console.error(`[cmssy] no component registered for block type "${type}"`);
384
+ }
385
+ return /* @__PURE__ */ jsx("div", { "data-cmssy-unknown-block": type });
386
+ }
387
+ function CmssyBlock({
388
+ block,
389
+ locale,
390
+ defaultLocale,
391
+ blockMap,
392
+ patchedContent,
393
+ patchedStyle,
394
+ patchedAdvanced,
395
+ resolvedContent,
396
+ schema,
397
+ editable,
398
+ editMode,
399
+ layoutRegion,
400
+ context,
401
+ data
402
+ }) {
403
+ const inEditMode = editMode ?? editable ?? false;
404
+ const Component = Object.hasOwn(blockMap, block.type) ? blockMap[block.type] : void 0;
405
+ const blockError = readBlockError(data);
406
+ const wrap = (children, hidden = false) => /* @__PURE__ */ jsx(
407
+ "div",
408
+ {
409
+ "data-block-id": block.id,
410
+ "data-block-type": block.type,
411
+ "data-layout-region": layoutRegion,
412
+ draggable: editable || void 0,
413
+ style: hidden ? { display: "none" } : void 0,
414
+ children
415
+ }
416
+ );
417
+ if (!Component) {
418
+ return inEditMode ? wrap(
419
+ /* @__PURE__ */ jsx(
420
+ BlockErrorCard,
421
+ {
422
+ blockType: block.type,
423
+ blockId: block.id,
424
+ error: unregisteredBlockError(block.type)
425
+ }
426
+ )
427
+ ) : wrap(/* @__PURE__ */ jsx(UnknownBlock, { type: block.type }), true);
428
+ }
429
+ if (blockError) {
430
+ if (!inEditMode) return null;
431
+ return wrap(
432
+ /* @__PURE__ */ jsx(
433
+ BlockErrorCard,
434
+ {
435
+ blockType: block.type,
436
+ blockId: block.id,
437
+ error: blockError
438
+ }
439
+ )
440
+ );
441
+ }
442
+ const content = foldBlockContent(
443
+ block,
444
+ locale,
445
+ defaultLocale,
446
+ patchedContent,
447
+ resolvedContent
448
+ );
449
+ if (schema) normalizeBlockContent(content, schema, resolvedContent);
450
+ const style = patchedStyle ? { ...asBucket(block.style), ...patchedStyle } : asBucket(block.style);
451
+ const advanced = patchedAdvanced ? { ...asBucket(block.advanced), ...patchedAdvanced } : asBucket(block.advanced);
452
+ return wrap(
453
+ /* @__PURE__ */ jsx(
454
+ BlockErrorBoundary,
455
+ {
456
+ blockType: block.type,
457
+ blockId: block.id,
458
+ editMode: inEditMode,
459
+ children: createElement(Component, {
460
+ content,
461
+ style,
462
+ advanced,
463
+ context,
464
+ data
465
+ })
466
+ }
467
+ )
468
+ );
469
+ }
470
+ function CmssyBlocks({
471
+ page,
472
+ blocks,
473
+ locale,
474
+ defaultLocale,
475
+ enabledLocales,
476
+ blockData,
477
+ blockContent,
478
+ pageContext,
479
+ forms,
480
+ auth,
481
+ workspace,
482
+ appContext,
483
+ isPreview = false,
484
+ editMode = false
485
+ }) {
486
+ if (!page) return null;
487
+ const blockMap = buildBlockMap(blocks);
488
+ const context = buildBlockContext(
489
+ locale,
490
+ defaultLocale,
491
+ enabledLocales,
492
+ isPreview,
493
+ forms,
494
+ { page: pageContext, auth, workspace, app: appContext }
495
+ );
496
+ return /* @__PURE__ */ jsx(Fragment, { children: (page.blocks ?? []).map((block) => /* @__PURE__ */ jsx(
497
+ CmssyBlock,
498
+ {
499
+ block,
500
+ blockMap,
501
+ locale,
502
+ defaultLocale,
503
+ context,
504
+ editMode,
505
+ resolvedContent: blockContent?.[block.id],
506
+ data: blockData?.[block.id]
507
+ },
508
+ block.id
509
+ )) });
510
+ }
511
+ function CmssyLayoutRegion({
512
+ groups,
513
+ region,
514
+ blocks,
515
+ locale,
516
+ defaultLocale,
517
+ enabledLocales,
518
+ page,
519
+ blockData,
520
+ blockContent,
521
+ forms,
522
+ appContext,
523
+ isPreview = false,
524
+ editMode = false
525
+ }) {
526
+ const group = groups.find((candidate) => candidate.region === region);
527
+ const regionBlocks = group ? group.blocks.filter((block) => block.isActive !== false).slice().sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) : [];
528
+ if (regionBlocks.length === 0) return null;
529
+ const blockMap = buildBlockMap(blocks);
530
+ const context = buildBlockContext(
531
+ locale,
532
+ defaultLocale,
533
+ enabledLocales,
534
+ isPreview,
535
+ forms,
536
+ { page, app: appContext }
537
+ );
538
+ return /* @__PURE__ */ jsx(Fragment, { children: regionBlocks.map((block) => /* @__PURE__ */ jsx(
539
+ CmssyBlock,
540
+ {
541
+ block,
542
+ blockMap,
543
+ locale,
544
+ defaultLocale,
545
+ context,
546
+ layoutRegion: region,
547
+ editMode,
548
+ resolvedContent: blockContent?.[block.id],
549
+ data: blockData?.[block.id]
550
+ },
551
+ block.id
552
+ )) });
553
+ }
554
+ function toSegments(path) {
555
+ if (Array.isArray(path)) return path;
556
+ const value = path ?? (typeof window === "undefined" ? "/" : window.location.pathname);
557
+ return value.split("/").filter(Boolean);
558
+ }
559
+ function useCmssyRoute(config, options) {
560
+ const segments = useMemo(() => toSegments(options.path), [options.path]);
561
+ const [state, setState] = useState({ data: null, error: null, loading: true });
562
+ const [attempt, setAttempt] = useState(0);
563
+ const latest = useRef(0);
564
+ const optionsRef = useRef(options);
565
+ optionsRef.current = options;
566
+ useEffect(() => {
567
+ const request = ++latest.current;
568
+ setState((current) => ({ ...current, error: null, loading: true }));
569
+ loadCmssyRoute(config, { ...optionsRef.current, path: segments }).then((data) => {
570
+ if (request !== latest.current) return;
571
+ setState({ data, error: null, loading: false });
572
+ }).catch((cause) => {
573
+ if (request !== latest.current) return;
574
+ setState({
575
+ data: null,
576
+ error: cause instanceof Error ? cause : new Error(String(cause)),
577
+ loading: false
578
+ });
579
+ });
580
+ }, [
581
+ config,
582
+ segments.join("/"),
583
+ options.locale,
584
+ options.previewSecret,
585
+ options.isPreview,
586
+ options.regions?.join(","),
587
+ attempt
588
+ ]);
589
+ const reload = useCallback(() => setAttempt((value) => value + 1), []);
590
+ return { ...state, reload };
591
+ }
592
+ function CmssyRoute({
593
+ config,
594
+ children,
595
+ fallback = null,
596
+ notFound = null,
597
+ renderError,
598
+ ...options
599
+ }) {
600
+ const { data, error, loading } = useCmssyRoute(config, options);
601
+ if (error) return /* @__PURE__ */ jsx(Fragment, { children: renderError ? renderError(error) : null });
602
+ if (!data) return /* @__PURE__ */ jsx(Fragment, { children: loading ? fallback : null });
603
+ const shared = {
604
+ blocks: options.blocks,
605
+ locale: data.locale,
606
+ defaultLocale: data.defaultLocale,
607
+ enabledLocales: data.enabledLocales,
608
+ forms: options.forms,
609
+ appContext: options.appContext,
610
+ isPreview: options.isPreview ?? false
611
+ };
612
+ const Blocks = () => data.page ? /* @__PURE__ */ jsx(
613
+ CmssyBlocks,
614
+ {
615
+ ...shared,
616
+ page: data.page,
617
+ pageContext: data.pageContext,
618
+ blockData: data.blockData,
619
+ blockContent: data.blockContent
620
+ }
621
+ ) : /* @__PURE__ */ jsx(Fragment, { children: notFound });
622
+ const Region = ({ id }) => /* @__PURE__ */ jsx(
623
+ CmssyLayoutRegion,
624
+ {
625
+ ...shared,
626
+ groups: data.layouts,
627
+ region: id,
628
+ page: data.pageContext,
629
+ blockData: data.layoutData[id]?.data,
630
+ blockContent: data.layoutData[id]?.content
631
+ }
632
+ );
633
+ return /* @__PURE__ */ jsx(Fragment, { children: children({ data, Blocks, Region }) });
634
+ }
635
+
636
+ export { CmssyRoute, useCmssyRoute };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/react",
3
- "version": "16.9.1",
3
+ "version": "16.10.0",
4
4
  "description": "React blocks, renderers, data client and editor bridge for cmssy headless sites",
5
5
  "keywords": [
6
6
  "cmssy",
@@ -72,6 +72,16 @@
72
72
  "types": "./dist/block-error-boundary.d.cts",
73
73
  "default": "./dist/block-error-boundary.cjs"
74
74
  }
75
+ },
76
+ "./spa": {
77
+ "import": {
78
+ "types": "./dist/spa.d.ts",
79
+ "default": "./dist/spa.js"
80
+ },
81
+ "require": {
82
+ "types": "./dist/spa.d.cts",
83
+ "default": "./dist/spa.cjs"
84
+ }
75
85
  }
76
86
  },
77
87
  "main": "./dist/index.cjs",
@@ -97,7 +107,7 @@
97
107
  },
98
108
  "dependencies": {
99
109
  "@cmssy/types": "0.49.0",
100
- "@cmssy/core": "16.9.1"
110
+ "@cmssy/core": "16.10.0"
101
111
  },
102
112
  "scripts": {
103
113
  "build": "tsup",