@akinon/pz-theme 2.0.79-beta.1 → 2.0.79-rc.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/CHANGELOG.md +33 -15
- package/package.json +4 -4
- package/src/chrome/__tests__/composition-source.test.ts +1 -79
- package/src/chrome/__tests__/sticky-slot.test.tsx +0 -40
- package/src/chrome/chrome-slot.tsx +2 -10
- package/src/chrome/composition-source.ts +0 -11
- package/src/load-placeholder-sections.ts +54 -144
- package/src/chrome/preview-snapshot-source.ts +0 -51
package/CHANGELOG.md
CHANGED
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.79-
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- Updated dependencies [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- Updated dependencies [
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
## 2.0.79-rc.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [86187b1c]
|
|
8
|
+
- Updated dependencies [0cf9ea239]
|
|
9
|
+
- Updated dependencies [324f97d55]
|
|
10
|
+
- Updated dependencies [51ea06888]
|
|
11
|
+
- Updated dependencies
|
|
12
|
+
- Updated dependencies [2fea4143]
|
|
13
|
+
- Updated dependencies [b55acb768]
|
|
14
|
+
- Updated dependencies [760258c1c]
|
|
15
|
+
- Updated dependencies [143be2b9d]
|
|
16
|
+
- Updated dependencies [7889b08fe]
|
|
17
|
+
- Updated dependencies [9f8cd3bc5]
|
|
18
|
+
- Updated dependencies [bfafa3f49]
|
|
19
|
+
- Updated dependencies [57d7eb305]
|
|
20
|
+
- Updated dependencies [d99a6a7d5]
|
|
21
|
+
- Updated dependencies [9db81a714]
|
|
22
|
+
- Updated dependencies [591e345e1]
|
|
23
|
+
- Updated dependencies [4de5303c5]
|
|
24
|
+
- Updated dependencies [95b139dc1]
|
|
25
|
+
- Updated dependencies [1d00f2d06]
|
|
26
|
+
- Updated dependencies [4ac7b2a1e]
|
|
27
|
+
- Updated dependencies [e9598c71]
|
|
28
|
+
- Updated dependencies [4998a9631]
|
|
29
|
+
- Updated dependencies [804d2bd6c]
|
|
30
|
+
- Updated dependencies [3909d3224]
|
|
31
|
+
- Updated dependencies [6a3d8a631]
|
|
32
|
+
- Updated dependencies [e18836b20]
|
|
33
|
+
- Updated dependencies [b1111d7b]
|
|
34
|
+
- @akinon/next@2.0.79-rc.0
|
|
35
|
+
- @akinon/theme-contract@2.0.79-rc.0
|
|
18
36
|
|
|
19
37
|
## 2.0.78
|
|
20
38
|
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.79-
|
|
4
|
+
"version": "2.0.79-rc.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@akinon/next": "2.0.79-
|
|
11
|
+
"@akinon/next": "2.0.79-rc.0",
|
|
12
12
|
"react": "^18.0.0 || ^19.0.0",
|
|
13
13
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@akinon/theme-contract": "2.0.79-
|
|
16
|
+
"@akinon/theme-contract": "2.0.79-rc.0",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"tailwind-merge": "^2.5.4",
|
|
20
20
|
"zod": "4.4.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@akinon/next": "2.0.79-
|
|
23
|
+
"@akinon/next": "2.0.79-rc.0",
|
|
24
24
|
"@testing-library/jest-dom": "5.16.4",
|
|
25
25
|
"@testing-library/react": "16.3.2",
|
|
26
26
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -27,17 +27,12 @@ jest.mock('react', () => {
|
|
|
27
27
|
const getWidgetData = jest.fn();
|
|
28
28
|
const getWidgetSchemaData = jest.fn();
|
|
29
29
|
const getCollectionWidgetData = jest.fn();
|
|
30
|
-
// Named preview snapshot reader (draft mode only). Resolves null by default:
|
|
31
|
-
// "not a preview", the live path.
|
|
32
|
-
const getPreviewSnapshot = jest.fn(async () => null as unknown);
|
|
33
30
|
|
|
34
31
|
jest.mock('@akinon/next/data/server', () => ({
|
|
35
32
|
getWidgetData: (...args: unknown[]) => getWidgetData(...args),
|
|
36
33
|
getWidgetSchemaData: (...args: unknown[]) => getWidgetSchemaData(...args),
|
|
37
34
|
getCollectionWidgetData: (...args: unknown[]) =>
|
|
38
|
-
getCollectionWidgetData(...args)
|
|
39
|
-
getPreviewSnapshot: (...args: unknown[]) =>
|
|
40
|
-
(getPreviewSnapshot as (...a: unknown[]) => unknown)(...args)
|
|
35
|
+
getCollectionWidgetData(...args)
|
|
41
36
|
}));
|
|
42
37
|
|
|
43
38
|
jest.mock(
|
|
@@ -128,76 +123,3 @@ describe('shared placeholder widget fetch', () => {
|
|
|
128
123
|
expect(attempts).toBe(3);
|
|
129
124
|
});
|
|
130
125
|
});
|
|
131
|
-
|
|
132
|
-
describe('named preview snapshots', () => {
|
|
133
|
-
beforeEach(() => {
|
|
134
|
-
getWidgetData.mockReset();
|
|
135
|
-
getWidgetSchemaData.mockReset();
|
|
136
|
-
getCollectionWidgetData.mockReset();
|
|
137
|
-
getPreviewSnapshot.mockReset();
|
|
138
|
-
getPreviewSnapshot.mockResolvedValue(null);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const blob = {
|
|
142
|
-
version: 1,
|
|
143
|
-
themeSettings: { mainMenuSticky: true },
|
|
144
|
-
dataSources: [],
|
|
145
|
-
sections: [
|
|
146
|
-
{
|
|
147
|
-
id: 'sec-preview',
|
|
148
|
-
type: 'custom',
|
|
149
|
-
label: 'Preview hero',
|
|
150
|
-
properties: {},
|
|
151
|
-
styles: {},
|
|
152
|
-
blocks: [],
|
|
153
|
-
order: 0
|
|
154
|
-
}
|
|
155
|
-
]
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
it('composes the slot and loads the sections from the blob, never the live widget', async () => {
|
|
159
|
-
getPreviewSnapshot.mockResolvedValue(blob);
|
|
160
|
-
getWidgetData.mockResolvedValue(placeholderPayload('preview-slot-a'));
|
|
161
|
-
|
|
162
|
-
const state = await getSlugCompositionState('preview-slot-a');
|
|
163
|
-
const loaded = await loadPlaceholderSections('preview-slot-a');
|
|
164
|
-
|
|
165
|
-
expect(state).toEqual({ kind: 'composed', sectionCount: 1 });
|
|
166
|
-
expect(loaded?.sections.map((s) => s.id)).toEqual(['sec-preview']);
|
|
167
|
-
expect(loaded?.placeholderId).toBe('preview-slot-a');
|
|
168
|
-
expect(loaded?.themeSettings).toEqual({ mainMenuSticky: true });
|
|
169
|
-
// The blob owns the render: no per-section or theme-config reads.
|
|
170
|
-
expect(getWidgetData).not.toHaveBeenCalled();
|
|
171
|
-
expect(getWidgetSchemaData).not.toHaveBeenCalled();
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('an empty blob withdraws the slot even when live is composed', async () => {
|
|
175
|
-
getPreviewSnapshot.mockResolvedValue({ ...blob, sections: [] });
|
|
176
|
-
getWidgetData.mockResolvedValue(placeholderPayload('preview-slot-b'));
|
|
177
|
-
|
|
178
|
-
expect(await getSlugCompositionState('preview-slot-b')).toEqual({
|
|
179
|
-
kind: 'empty'
|
|
180
|
-
});
|
|
181
|
-
expect((await loadPlaceholderSections('preview-slot-b'))?.sections).toEqual(
|
|
182
|
-
[]
|
|
183
|
-
);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('falls through to the live path without a blob or when the reader throws', async () => {
|
|
187
|
-
getPreviewSnapshot.mockRejectedValue(new Error('backend down'));
|
|
188
|
-
getWidgetData.mockResolvedValue(placeholderPayload('preview-slot-c'));
|
|
189
|
-
getWidgetSchemaData.mockResolvedValue({
|
|
190
|
-
schema: { 'sec-a': { metadata: { blocks: [] } } }
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
expect(await getSlugCompositionState('preview-slot-c')).toEqual({
|
|
194
|
-
kind: 'composed',
|
|
195
|
-
sectionCount: 1
|
|
196
|
-
});
|
|
197
|
-
expect(
|
|
198
|
-
(await loadPlaceholderSections('preview-slot-c'))?.sections.map(
|
|
199
|
-
(s) => s.id
|
|
200
|
-
)
|
|
201
|
-
).toEqual(['sec-a']);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
@@ -27,14 +27,6 @@ jest.mock('../theme-config-source', () => ({
|
|
|
27
27
|
getThemeConfig: () => themeConfig()
|
|
28
28
|
}));
|
|
29
29
|
|
|
30
|
-
// A named preview's snapshot owns the theme settings; null = not a preview.
|
|
31
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
-
const previewSnapshot = jest.fn<any, [string]>(() => null);
|
|
33
|
-
|
|
34
|
-
jest.mock('../preview-snapshot-source', () => ({
|
|
35
|
-
getPlaceholderPreviewSnapshot: (slug: string) => previewSnapshot(slug)
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
30
|
// The placeholder is an async server component that reaches for the data
|
|
39
31
|
// layer; what the slot decides about itself is what is under test.
|
|
40
32
|
jest.mock('../../theme-placeholder', () => ({
|
|
@@ -229,35 +221,3 @@ describe('ChromeSlot sticky', () => {
|
|
|
229
221
|
expect(themeConfig).not.toHaveBeenCalled();
|
|
230
222
|
});
|
|
231
223
|
});
|
|
232
|
-
|
|
233
|
-
describe('ChromeSlot sticky in a named preview', () => {
|
|
234
|
-
afterEach(() => {
|
|
235
|
-
cleanup();
|
|
236
|
-
previewSnapshot.mockReset();
|
|
237
|
-
previewSnapshot.mockReturnValue(null);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('follows the preview snapshot, not the live theme-config', async () => {
|
|
241
|
-
compositionState.mockResolvedValue(composed);
|
|
242
|
-
themeConfig.mockResolvedValue({ settings: { mainMenuSticky: false } });
|
|
243
|
-
previewSnapshot.mockResolvedValue({
|
|
244
|
-
version: 1,
|
|
245
|
-
sections: [{ id: 'sec' }],
|
|
246
|
-
themeSettings: { mainMenuSticky: true }
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
await renderSlot({ slug: 'header', fallback: <div>brand</div> });
|
|
250
|
-
|
|
251
|
-
expect(slotElement('header').dataset.sticky).toBe('true');
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('falls back to the live theme-config when the preview has no blob', async () => {
|
|
255
|
-
compositionState.mockResolvedValue(composed);
|
|
256
|
-
themeConfig.mockResolvedValue({ settings: { mainMenuSticky: true } });
|
|
257
|
-
previewSnapshot.mockResolvedValue(null);
|
|
258
|
-
|
|
259
|
-
await renderSlot({ slug: 'header', fallback: <div>brand</div> });
|
|
260
|
-
|
|
261
|
-
expect(slotElement('header').dataset.sticky).toBe('true');
|
|
262
|
-
});
|
|
263
|
-
});
|
|
@@ -2,7 +2,6 @@ import React from 'react';
|
|
|
2
2
|
import ThemePlaceholder from '../theme-placeholder';
|
|
3
3
|
import { warnAboutUncomposedSlot } from './composition-state';
|
|
4
4
|
import { getSlugCompositionState } from './composition-source';
|
|
5
|
-
import { getPlaceholderPreviewSnapshot } from './preview-snapshot-source';
|
|
6
5
|
import { getThemeConfig } from './theme-config-source';
|
|
7
6
|
import { ChromeFallback } from './chrome-fallback';
|
|
8
7
|
import { ChromeScrollState } from './scroll-state';
|
|
@@ -80,16 +79,9 @@ export default async function ChromeSlot({
|
|
|
80
79
|
warnAboutUncomposedSlot(slug, state);
|
|
81
80
|
|
|
82
81
|
// Only read when the mounting code said nothing, and memoized per request, so
|
|
83
|
-
// a page full of slots costs at most one theme-config read.
|
|
84
|
-
// preview the snapshot blob owns the theme settings (theme-config is never
|
|
85
|
-
// preview-transformed), so the pin must follow the blob the placeholder
|
|
86
|
-
// renders from — otherwise "sticky header" toggled in the preview would
|
|
87
|
-
// only show once published.
|
|
82
|
+
// a page full of slots costs at most one theme-config read.
|
|
88
83
|
const themeSettings =
|
|
89
|
-
sticky === undefined
|
|
90
|
-
? (await getPlaceholderPreviewSnapshot(slug))?.themeSettings ??
|
|
91
|
-
(await getThemeConfig()).settings
|
|
92
|
-
: null;
|
|
84
|
+
sticky === undefined ? (await getThemeConfig()).settings : null;
|
|
93
85
|
const pinned = resolveSlotSticky({
|
|
94
86
|
slug,
|
|
95
87
|
sticky,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
3
|
import { CompositionState, readCompositionState } from './composition-state';
|
|
4
|
-
import { getPlaceholderPreviewSnapshot } from './preview-snapshot-source';
|
|
5
4
|
import { getWidgetDataWithRetry } from './retrying-widget-data';
|
|
6
5
|
|
|
7
6
|
// `React.cache` exists on the RSC-capable React this package renders under;
|
|
@@ -50,16 +49,6 @@ export const getPlaceholderWidget = cache
|
|
|
50
49
|
*/
|
|
51
50
|
const load = async (slug: string): Promise<CompositionState> => {
|
|
52
51
|
try {
|
|
53
|
-
// A named preview (draft mode) composes from its snapshot, not from the
|
|
54
|
-
// live placeholder: a slug composed only in the preview must still take
|
|
55
|
-
// the slot over there, and one emptied in the preview must give it back.
|
|
56
|
-
const preview = await getPlaceholderPreviewSnapshot(slug);
|
|
57
|
-
if (preview) {
|
|
58
|
-
return preview.sections.length > 0
|
|
59
|
-
? { kind: 'composed', sectionCount: preview.sections.length }
|
|
60
|
-
: { kind: 'empty' };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
52
|
const data = await getPlaceholderWidget(slug);
|
|
64
53
|
|
|
65
54
|
return readCompositionState(data);
|
|
@@ -8,10 +8,6 @@ import { Section } from './theme-section';
|
|
|
8
8
|
import { devWarn } from './utils/dev-warn';
|
|
9
9
|
import { validateThemeSectionsInDev } from './schemas/theme-schema';
|
|
10
10
|
import { getPlaceholderWidget } from './chrome/composition-source';
|
|
11
|
-
import {
|
|
12
|
-
getPlaceholderPreviewSnapshot,
|
|
13
|
-
PreviewSnapshot
|
|
14
|
-
} from './chrome/preview-snapshot-source';
|
|
15
11
|
import { getThemeConfig, ThemeConfig } from './chrome/theme-config-source';
|
|
16
12
|
|
|
17
13
|
const { getWidgetData } = serverData;
|
|
@@ -62,135 +58,6 @@ const getWidgetSchemaData = async <T,>(args: {
|
|
|
62
58
|
return directFetch<T>(`/widgets/${args.widgetSlug}/schema/`);
|
|
63
59
|
};
|
|
64
60
|
|
|
65
|
-
/**
|
|
66
|
-
* Resolves a section's data source reference into the object the renderer
|
|
67
|
-
* expects, fetching collection data for collection-backed sources. Shared by
|
|
68
|
-
* the live render path and the preview-snapshot path so the two cannot drift.
|
|
69
|
-
*/
|
|
70
|
-
async function resolveSectionDataSource(
|
|
71
|
-
dataSourceId: string | undefined,
|
|
72
|
-
dataSources: ThemeConfig['dataSources'],
|
|
73
|
-
sectionSlug: string
|
|
74
|
-
): Promise<any | undefined> {
|
|
75
|
-
if (!dataSourceId) {
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const dataSource = dataSources.find((ds) => ds.id === dataSourceId);
|
|
80
|
-
|
|
81
|
-
if (!dataSource) {
|
|
82
|
-
devWarn(
|
|
83
|
-
'ThemePlaceholder',
|
|
84
|
-
`Section "${sectionSlug}" references data source "${dataSourceId}" which was not found; rendering placeholder items`
|
|
85
|
-
);
|
|
86
|
-
return undefined;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (!dataSource.details) {
|
|
90
|
-
return dataSource;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Only fetch collection data if we have a valid widget slug
|
|
94
|
-
// (not the section's own slug which contains config, not products)
|
|
95
|
-
const collectionSlug = dataSource.details.collection?.slug;
|
|
96
|
-
if (!collectionSlug) {
|
|
97
|
-
// The saved products from the editor (details.collection.products)
|
|
98
|
-
// will be used as fallback
|
|
99
|
-
return dataSource;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
let collectionData = await getWidgetData<{
|
|
104
|
-
[key: string]: unknown;
|
|
105
|
-
}>({
|
|
106
|
-
slug: collectionSlug
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
if (!collectionData) {
|
|
110
|
-
collectionData = await getCollectionWidgetData<{
|
|
111
|
-
[key: string]: unknown;
|
|
112
|
-
}>({
|
|
113
|
-
slug: collectionSlug
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (collectionData) {
|
|
118
|
-
return {
|
|
119
|
-
...dataSource,
|
|
120
|
-
details: {
|
|
121
|
-
...dataSource.details,
|
|
122
|
-
collection: {
|
|
123
|
-
...dataSource.details.collection,
|
|
124
|
-
data: collectionData
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
} catch (error) {
|
|
130
|
-
console.error(
|
|
131
|
-
`Error fetching widget data for section ${sectionSlug}:`,
|
|
132
|
-
error
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return dataSource;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Preview (draft mode): the theme editor writes the whole placeholder as one
|
|
141
|
-
* snapshot blob — sections arrive in render shape, so none of the per-widget
|
|
142
|
-
* fetches of the live path happen. The blob also owns the theme settings and
|
|
143
|
-
* data sources it was saved with.
|
|
144
|
-
*/
|
|
145
|
-
async function loadFromPreviewSnapshot(
|
|
146
|
-
slug: string,
|
|
147
|
-
snapshot: PreviewSnapshot
|
|
148
|
-
): Promise<LoadedPlaceholder> {
|
|
149
|
-
const dataSources: ThemeConfig['dataSources'] = Array.isArray(
|
|
150
|
-
snapshot.dataSources
|
|
151
|
-
)
|
|
152
|
-
? (snapshot.dataSources as any[])
|
|
153
|
-
: [];
|
|
154
|
-
|
|
155
|
-
const resolved = await Promise.all(
|
|
156
|
-
snapshot.sections.map(async (raw: any): Promise<Section | null> => {
|
|
157
|
-
if (!raw?.id) {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
id: raw.id,
|
|
163
|
-
type: raw.type || 'default',
|
|
164
|
-
name: raw.name || raw.id,
|
|
165
|
-
label: raw.label || raw.id,
|
|
166
|
-
properties: raw.properties || {},
|
|
167
|
-
styles: raw.styles || {},
|
|
168
|
-
blocks: Array.isArray(raw.blocks) ? raw.blocks : [],
|
|
169
|
-
order: raw.order ?? 0,
|
|
170
|
-
hidden: raw.hidden || false,
|
|
171
|
-
dataSourceId: raw.dataSourceId,
|
|
172
|
-
dataSource: await resolveSectionDataSource(
|
|
173
|
-
raw.dataSourceId,
|
|
174
|
-
dataSources,
|
|
175
|
-
raw.id
|
|
176
|
-
)
|
|
177
|
-
};
|
|
178
|
-
})
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
const sections = resolved.filter((s): s is Section => s !== null);
|
|
182
|
-
|
|
183
|
-
validateThemeSectionsInDev(sections);
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
sections,
|
|
187
|
-
placeholderId: slug,
|
|
188
|
-
dataSources,
|
|
189
|
-
themeSettings:
|
|
190
|
-
(snapshot.themeSettings as Record<string, unknown> | null) ?? null
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
61
|
export interface LoadedPlaceholder {
|
|
195
62
|
sections: Section[];
|
|
196
63
|
placeholderId: string;
|
|
@@ -214,12 +81,6 @@ export async function loadPlaceholderSections(
|
|
|
214
81
|
slug: string
|
|
215
82
|
): Promise<LoadedPlaceholder | null> {
|
|
216
83
|
try {
|
|
217
|
-
// Absent/malformed blob (or no draft mode) falls through to the live path.
|
|
218
|
-
const previewSnapshot = await getPlaceholderPreviewSnapshot(slug);
|
|
219
|
-
if (previewSnapshot) {
|
|
220
|
-
return loadFromPreviewSnapshot(slug, previewSnapshot);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
84
|
// theme-config is optional for placeholder rendering, and the reader is
|
|
224
85
|
// memoized per request — a page with several placeholders, or a ChromeSlot
|
|
225
86
|
// asking the same question to decide whether it is pinned, reads it once.
|
|
@@ -363,11 +224,60 @@ export async function loadPlaceholderSections(
|
|
|
363
224
|
(getBlockValue('dataSourceId') as string | undefined) ||
|
|
364
225
|
(getBlockValue('selectedDataSourceId') as string | undefined);
|
|
365
226
|
|
|
366
|
-
const
|
|
367
|
-
dataSourceId
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
)
|
|
227
|
+
const dataSource = dataSourceId
|
|
228
|
+
? dataSources.find((ds) => ds.id === dataSourceId)
|
|
229
|
+
: undefined;
|
|
230
|
+
|
|
231
|
+
if (dataSourceId && !dataSource) {
|
|
232
|
+
devWarn(
|
|
233
|
+
'ThemePlaceholder',
|
|
234
|
+
`Section "${sectionSlug}" references data source "${dataSourceId}" which was not found; rendering placeholder items`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let dataSourceWithData = dataSource;
|
|
239
|
+
if (dataSource && dataSource.details) {
|
|
240
|
+
// Only fetch collection data if we have a valid widget slug
|
|
241
|
+
// (not the section's own slug which contains config, not products)
|
|
242
|
+
const collectionSlug = dataSource.details.collection?.slug;
|
|
243
|
+
if (collectionSlug) {
|
|
244
|
+
try {
|
|
245
|
+
let collectionData = await getWidgetData<{
|
|
246
|
+
[key: string]: unknown;
|
|
247
|
+
}>({
|
|
248
|
+
slug: collectionSlug
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (!collectionData) {
|
|
252
|
+
collectionData = await getCollectionWidgetData<{
|
|
253
|
+
[key: string]: unknown;
|
|
254
|
+
}>({
|
|
255
|
+
slug: collectionSlug
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (collectionData) {
|
|
260
|
+
dataSourceWithData = {
|
|
261
|
+
...dataSource,
|
|
262
|
+
details: {
|
|
263
|
+
...dataSource.details,
|
|
264
|
+
collection: {
|
|
265
|
+
...dataSource.details.collection,
|
|
266
|
+
data: collectionData
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error(
|
|
273
|
+
`Error fetching widget data for section ${sectionSlug}:`,
|
|
274
|
+
error
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// If no collection slug, the saved products from editor
|
|
279
|
+
// (details.collection.products) will be used as fallback
|
|
280
|
+
}
|
|
371
281
|
|
|
372
282
|
const section: Section = {
|
|
373
283
|
id: sectionSlug,
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import * as serverData from '@akinon/next/data/server';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Whole-placeholder snapshot the theme editor writes for a named preview
|
|
6
|
-
* (`<slug>-preview-<id>`, `preview_snapshot` attribute). Sections arrive in
|
|
7
|
-
* render shape. See akinon-next's `getPreviewSnapshot` for the contract.
|
|
8
|
-
*/
|
|
9
|
-
export interface PreviewSnapshot {
|
|
10
|
-
version: number;
|
|
11
|
-
savedAt?: string;
|
|
12
|
-
themeSettings?: Record<string, unknown> | null;
|
|
13
|
-
dataSources?: unknown[];
|
|
14
|
-
sections: unknown[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type GetPreviewSnapshot = (args: {
|
|
18
|
-
slug: string;
|
|
19
|
-
}) => Promise<PreviewSnapshot | null>;
|
|
20
|
-
|
|
21
|
-
const fetchPreviewSnapshot = async (
|
|
22
|
-
slug: string
|
|
23
|
-
): Promise<PreviewSnapshot | null> => {
|
|
24
|
-
// v1 cores predate widget preview: no reader means never a preview. The
|
|
25
|
-
// reader itself answers null outside draft mode or without a preview id.
|
|
26
|
-
const fn = (
|
|
27
|
-
serverData as unknown as { getPreviewSnapshot?: GetPreviewSnapshot }
|
|
28
|
-
).getPreviewSnapshot;
|
|
29
|
-
if (!fn) return null;
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
return (await fn({ slug })) ?? null;
|
|
33
|
-
} catch {
|
|
34
|
-
// A failed preview read must fall back to the live render, never blank it.
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// `React.cache` exists on the RSC-capable React this package renders under;
|
|
40
|
-
// fall back to the raw function on older cores, where correctness is identical
|
|
41
|
-
// and the only cost is a second fetch.
|
|
42
|
-
const cache = (React as { cache?: <T>(fn: T) => T }).cache;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* The one preview-snapshot read per placeholder per request — shared by the
|
|
46
|
-
* slot's composed/uncomposed decision and the section loader so the two can
|
|
47
|
-
* never disagree about which content a preview shows.
|
|
48
|
-
*/
|
|
49
|
-
export const getPlaceholderPreviewSnapshot = cache
|
|
50
|
-
? cache(fetchPreviewSnapshot)
|
|
51
|
-
: fetchPreviewSnapshot;
|