@akinon/pz-theme 2.0.71-beta.0 → 2.0.71
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 +7 -4
- package/package.json +4 -4
- package/src/__tests__/custom-catalog-announce.test.ts +276 -0
- package/src/blocks/group-block.tsx +9 -57
- package/src/blocks/render-composed-children.tsx +104 -0
- package/src/custom/README.md +249 -0
- package/src/custom/__tests__/define.test.tsx +619 -0
- package/src/custom/__tests__/module-boundaries.test.ts +156 -0
- package/src/custom/__tests__/public-api.test.tsx +427 -0
- package/src/custom/__tests__/slot-node-wire.test.tsx +309 -0
- package/src/custom/__tests__/slot-nodes.test.ts +144 -0
- package/src/custom/__tests__/theme-editable.test.tsx +486 -0
- package/src/custom/__tests__/theme-slot.test.tsx +387 -0
- package/src/custom/__tests__/use-theme-setting.test.tsx +260 -0
- package/src/custom/block-context.ts +64 -0
- package/src/custom/block-host.tsx +162 -0
- package/src/custom/catalog.ts +97 -0
- package/src/custom/collect.ts +342 -0
- package/src/custom/define.tsx +172 -0
- package/src/custom/editable-tags.ts +120 -0
- package/src/custom/index.ts +105 -0
- package/src/custom/parts.ts +108 -0
- package/src/custom/property-types.ts +70 -0
- package/src/custom/register.ts +545 -0
- package/src/custom/slot-nodes.ts +119 -0
- package/src/custom/theme-editable.tsx +342 -0
- package/src/custom/theme-slot.tsx +210 -0
- package/src/custom/types.ts +251 -0
- package/src/custom/use-theme-setting.ts +225 -0
- package/src/index.ts +66 -0
- package/src/placeholder-registry.ts +168 -16
|
@@ -1,29 +1,181 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getCustomCatalog } from './custom/catalog';
|
|
2
|
+
import type { CustomCatalog } from './custom/types';
|
|
3
|
+
import { isTrustedEditorEvent, postToEditor } from './editor-bridge';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The single announce the storefront makes to the editor: which placeholder
|
|
7
|
+
* slugs this document mounted, and — only for a proven designer — which
|
|
8
|
+
* developer-authored blocks this build can render.
|
|
9
|
+
*
|
|
10
|
+
* One debounced flush carries both. Placeholders register as their components
|
|
11
|
+
* mount, while custom blocks register at module evaluation (before any
|
|
12
|
+
* placeholder exists), so the catalog re-arms the SAME timer instead of owning
|
|
13
|
+
* one: two timers would mean two frames, and the editor treats every frame as
|
|
14
|
+
* a complete answer.
|
|
15
|
+
*/
|
|
2
16
|
const placeholderRegistry = new Set<string>();
|
|
3
17
|
let registryTimeout: NodeJS.Timeout | null = null;
|
|
18
|
+
let listeningForDesigner = false;
|
|
19
|
+
/**
|
|
20
|
+
* The designer re-announce is a ONE-SHOT. See `handleEditorMessage` — the
|
|
21
|
+
* editor answers every announce with the same cookie it answered the last one
|
|
22
|
+
* with, so an unlatched re-arm is an endless round trip.
|
|
23
|
+
*/
|
|
24
|
+
let designerReannounceSpent = false;
|
|
4
25
|
|
|
5
|
-
|
|
6
|
-
|
|
26
|
+
interface PlaceholdersAvailablePayload {
|
|
27
|
+
placeholders: string[];
|
|
28
|
+
/**
|
|
29
|
+
* ALWAYS THE WHOLE CATALOG, never a delta. An in-iframe route change
|
|
30
|
+
* remounts every placeholder and produces a second announce; the receiver
|
|
31
|
+
* must replace what it holds rather than merge, or a block the build no
|
|
32
|
+
* longer registers would live on in the picker and be offered for a type
|
|
33
|
+
* nothing can render.
|
|
34
|
+
*/
|
|
35
|
+
customBlocks?: CustomCatalog;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isFramed = (): boolean =>
|
|
39
|
+
typeof window !== 'undefined' &&
|
|
40
|
+
Boolean(window.parent) &&
|
|
41
|
+
window.self !== window.top;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The editor-proven designer flag — the same signal `chrome/checkout-pieces.ts`
|
|
45
|
+
* exports as `isThemeEditorDesigner`, set only after a trusted
|
|
46
|
+
* SET_THEME_EDITOR_COOKIE arrives through the origin-guarded bridge.
|
|
47
|
+
*
|
|
48
|
+
* Read inline rather than imported so the module every placeholder pulls in
|
|
49
|
+
* does not drag the checkout outlet store into its graph.
|
|
50
|
+
*/
|
|
51
|
+
const isDesignerAttached = (): boolean =>
|
|
52
|
+
typeof window !== 'undefined' &&
|
|
53
|
+
(window as { __themeEditorDesigner?: boolean }).__themeEditorDesigner ===
|
|
54
|
+
true;
|
|
55
|
+
|
|
56
|
+
const flush = () => {
|
|
57
|
+
registryTimeout = null;
|
|
58
|
+
|
|
59
|
+
// An empty announce is not a harmless no-op. The editor's only
|
|
60
|
+
// PLACEHOLDERS_AVAILABLE subscriber (loadThemeForPlaceholders) runs
|
|
61
|
+
// setIsLoadingTheme, clears its cache and swaps the active placeholder set
|
|
62
|
+
// before its first await — so a frame carrying no slugs makes it reload the
|
|
63
|
+
// theme against nothing. The custom-block re-arm below fires before any
|
|
64
|
+
// placeholder has mounted, which is exactly when that would happen.
|
|
65
|
+
if (placeholderRegistry.size === 0) return;
|
|
66
|
+
if (!isFramed()) return;
|
|
7
67
|
|
|
68
|
+
const data: PlaceholdersAvailablePayload = {
|
|
69
|
+
placeholders: Array.from(placeholderRegistry)
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Being framed is NOT enough to hand the catalog over. A storefront is
|
|
73
|
+
// legitimately framed in production — payment providers, marketplace
|
|
74
|
+
// previews — and the catalog is brand IP: every block name, every setting
|
|
75
|
+
// key and every option list this build ships. The placeholder slugs are
|
|
76
|
+
// route names and keep their existing behaviour; the catalog waits for the
|
|
77
|
+
// editor to have proven itself.
|
|
78
|
+
if (isDesignerAttached()) {
|
|
79
|
+
const customBlocks = getCustomCatalog();
|
|
80
|
+
// Omitted entirely rather than sent empty: a build with no custom blocks
|
|
81
|
+
// must be indistinguishable from one whose catalog simply is not this
|
|
82
|
+
// frame's business.
|
|
83
|
+
if (customBlocks) data.customBlocks = customBlocks;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
postToEditor({
|
|
87
|
+
type: 'PLACEHOLDERS_AVAILABLE',
|
|
88
|
+
data
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const scheduleAnnounce = () => {
|
|
8
93
|
if (registryTimeout) {
|
|
9
94
|
clearTimeout(registryTimeout);
|
|
10
95
|
}
|
|
11
96
|
|
|
12
|
-
registryTimeout = setTimeout(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
97
|
+
registryTimeout = setTimeout(flush, 100);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Re-announce once the editor proves itself.
|
|
102
|
+
*
|
|
103
|
+
* The designer flag arrives in the editor's reply to IFRAME_READY, which is a
|
|
104
|
+
* postMessage round trip — nothing guarantees it lands inside the 100ms
|
|
105
|
+
* debounce. Without this the first flush would go out gated shut and the
|
|
106
|
+
* catalog would never be offered again for the life of the document, leaving
|
|
107
|
+
* the picker with no custom blocks on exactly the loads where the handshake
|
|
108
|
+
* was slow.
|
|
109
|
+
*
|
|
110
|
+
* The listener is attached on first placeholder registration (never at module
|
|
111
|
+
* scope, which would give the barrel an import side effect) and then kept for
|
|
112
|
+
* the life of the document, like the one in browse-mode.ts.
|
|
113
|
+
*
|
|
114
|
+
* AT MOST ONCE, and only when there is something a frame could not already
|
|
115
|
+
* carry. The editor does not send this cookie once — it answers EVERY
|
|
116
|
+
* PLACEHOLDERS_AVAILABLE with it (`theme-iframe-service.ts`, the same handler
|
|
117
|
+
* that ingests the catalog), so an unlatched re-arm is a ping-pong:
|
|
118
|
+
* announce -> cookie -> announce, every 100ms for the life of the document.
|
|
119
|
+
* Each lap costs the editor a full theme reload, because that announce is what
|
|
120
|
+
* `loadThemeForPlaceholders` subscribes to — it clears the widget cache,
|
|
121
|
+
* rewrites the active placeholder set and refetches every widget.
|
|
122
|
+
*/
|
|
123
|
+
const handleEditorMessage = (event: MessageEvent) => {
|
|
124
|
+
if (!isTrustedEditorEvent(event)) return;
|
|
125
|
+
if (event.data?.type !== 'SET_THEME_EDITOR_COOKIE') return;
|
|
126
|
+
if (designerReannounceSpent) return;
|
|
127
|
+
designerReannounceSpent = true;
|
|
128
|
+
|
|
129
|
+
// A build that registers no custom block has nothing to add to a frame it
|
|
130
|
+
// has already sent, so it stays at exactly one announce — the behaviour
|
|
131
|
+
// every brand shipping no custom block had before any of this existed.
|
|
132
|
+
if (!getCustomCatalog()) return;
|
|
133
|
+
|
|
134
|
+
// Re-arm rather than flush: the flag itself is set by the placeholder
|
|
135
|
+
// client's own listener, and listener order across modules is not ours to
|
|
136
|
+
// depend on. By the time the timer fires both have run.
|
|
137
|
+
scheduleAnnounce();
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const listenForDesigner = () => {
|
|
141
|
+
if (listeningForDesigner || typeof window === 'undefined') return;
|
|
142
|
+
listeningForDesigner = true;
|
|
143
|
+
window.addEventListener('message', handleEditorMessage);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const registerPlaceholder = (slug: string) => {
|
|
147
|
+
placeholderRegistry.add(slug);
|
|
148
|
+
listenForDesigner();
|
|
149
|
+
scheduleAnnounce();
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Re-arms the placeholder announce so a registered barrel rides the next
|
|
154
|
+
* flush. Called by `registerCustomBlocks` at module evaluation; posts nothing
|
|
155
|
+
* on its own, because a document with no placeholders has nothing to say.
|
|
156
|
+
*/
|
|
157
|
+
export const announceCustomBlocks = () => {
|
|
158
|
+
scheduleAnnounce();
|
|
25
159
|
};
|
|
26
160
|
|
|
27
161
|
export const unregisterPlaceholder = (slug: string) => {
|
|
28
162
|
placeholderRegistry.delete(slug);
|
|
29
163
|
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Test-only. Module state outlives a test file, and the window listener
|
|
167
|
+
* outlives even a re-imported copy of this module — one jsdom window collects
|
|
168
|
+
* every copy's listener, so without this each test would announce for all of
|
|
169
|
+
* its predecessors as well.
|
|
170
|
+
*/
|
|
171
|
+
export const __resetPlaceholderRegistryForTests = () => {
|
|
172
|
+
if (registryTimeout) clearTimeout(registryTimeout);
|
|
173
|
+
registryTimeout = null;
|
|
174
|
+
placeholderRegistry.clear();
|
|
175
|
+
designerReannounceSpent = false;
|
|
176
|
+
|
|
177
|
+
if (typeof window !== 'undefined') {
|
|
178
|
+
window.removeEventListener('message', handleEditorMessage);
|
|
179
|
+
}
|
|
180
|
+
listeningForDesigner = false;
|
|
181
|
+
};
|