@base44/vite-plugin 1.0.10 → 1.0.11

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.
@@ -0,0 +1,264 @@
1
+ // page-height-bridge — postMessage protocol the iframe exposes to its parent.
2
+ // Inert until the parent posts a message; no observers, rewrites, or timers
3
+ // fire on their own.
4
+ //
5
+ // parent → child { type: "freeze-vh-units", referenceVhBase?: number }
6
+ // Rewrites every `vh` in <style> + CSSOM to fixed `px` (kills the
7
+ // vh ↔ auto-resize feedback loop). Idempotent; covers HMR-added
8
+ // styles too. Fire-and-forget.
9
+ //
10
+ // parent → child { type: "measure-page-height", settleMs?: number }
11
+ // After `settleMs` (default 2000) posts the page's scrollHeight back
12
+ // as `{ type: "page-height-measured", height }` to the requester.
13
+
14
+ type IncomingMessage = {
15
+ type?: string;
16
+ settleMs?: number;
17
+ referenceVhBase?: number;
18
+ };
19
+
20
+ type IndexableCssRule = CSSRule & {
21
+ cssRules?: CSSRuleList;
22
+ style?: CSSStyleDeclaration;
23
+ };
24
+
25
+ type Debouncer = { trigger: () => void; cancel: () => void };
26
+
27
+ const FALLBACK_VHBASE: number = 900;
28
+ const MIN_VHBASE: number = 400;
29
+ const DEFAULT_SETTLE_MS: number = 2000;
30
+ const NEUTRALIZE_DEBOUNCE_MS: number = 16;
31
+
32
+ const noop: () => void = (): void => {};
33
+
34
+ let started: boolean = false;
35
+
36
+ /**
37
+ * Installs the parent-driven message listener. Returns a teardown that
38
+ * removes the listener, disconnects any observers attached as a result of
39
+ * `freeze-vh-units`, and clears any pending response timer. Returns a
40
+ * no-op when bailed (already started, SSR, top window).
41
+ */
42
+ export function setupPageHeightBridge(): () => void {
43
+ if (started) return noop;
44
+ if (typeof window === "undefined") return noop;
45
+ if (window.self === window.top) return noop;
46
+ started = true;
47
+
48
+ let vhCleanups: Array<() => void> | null = null;
49
+ let vhForceRun: (() => void) | null = null;
50
+ let pendingResponse: number | undefined;
51
+ let pendingOrigin: string = "*";
52
+
53
+ const freezeVhUnits = (override: number | undefined): void => {
54
+ if (vhCleanups) {
55
+ vhForceRun?.();
56
+ return;
57
+ }
58
+ const referenceVhBase: number = resolveReferenceVhBase(override);
59
+ vhCleanups = [];
60
+ vhForceRun = startVhNeutralizer(referenceVhBase, vhCleanups);
61
+ };
62
+
63
+ // Target the requester's origin so the height isn't broadcast to anyone
64
+ // who happens to embed us. Falls back to "*" when origin is unavailable
65
+ // (jsdom default, sandboxed iframes with `null` origin).
66
+ const measurePageHeight = (origin: string, settleMs: number): void => {
67
+ pendingOrigin = origin;
68
+ if (pendingResponse !== undefined) window.clearTimeout(pendingResponse);
69
+ pendingResponse = window.setTimeout((): void => {
70
+ requestAnimationFrame((): void => {
71
+ const height: number = measureContentHeight();
72
+ window.parent.postMessage({ type: "page-height-measured", height }, pendingOrigin);
73
+ });
74
+ }, settleMs);
75
+ };
76
+
77
+ const onMessage = (event: MessageEvent): void => {
78
+ const data: IncomingMessage | null = (event.data ?? null) as IncomingMessage | null;
79
+ if (!data || typeof data !== "object") return;
80
+ switch (data.type) {
81
+ case "freeze-vh-units": {
82
+ const override: number | undefined =
83
+ typeof data.referenceVhBase === "number" ? data.referenceVhBase : undefined;
84
+ freezeVhUnits(override);
85
+ return;
86
+ }
87
+ case "measure-page-height": {
88
+ const settleMs: number =
89
+ typeof data.settleMs === "number" ? data.settleMs : DEFAULT_SETTLE_MS;
90
+ const origin: string =
91
+ event.origin && event.origin !== "null" ? event.origin : "*";
92
+ measurePageHeight(origin, settleMs);
93
+ return;
94
+ }
95
+ }
96
+ };
97
+
98
+ window.addEventListener("message", onMessage);
99
+
100
+ let torn: boolean = false;
101
+ return (): void => {
102
+ if (torn) return;
103
+ torn = true;
104
+ started = false;
105
+ window.removeEventListener("message", onMessage);
106
+ if (vhCleanups) {
107
+ for (const c of vhCleanups) c();
108
+ vhCleanups = null;
109
+ vhForceRun = null;
110
+ }
111
+ if (pendingResponse !== undefined) window.clearTimeout(pendingResponse);
112
+ };
113
+ }
114
+
115
+ function resolveReferenceVhBase(override: number | undefined): number {
116
+ if (override !== undefined) return override;
117
+ const detected: number = window.innerHeight || 0;
118
+ return detected >= MIN_VHBASE ? detected : FALLBACK_VHBASE;
119
+ }
120
+
121
+ // Caches keep work proportional to *new* CSS, not total CSS. <head> observer
122
+ // catches `<style>`/`<link>` adds; per-element observers catch HMR text edits.
123
+ // Returns a `forceRun` so the caller can re-trigger neutralization on later
124
+ // parent requests (idempotent — already-rewritten text is skipped via the
125
+ // processed sets).
126
+ function startVhNeutralizer(
127
+ referenceVhBase: number,
128
+ cleanups: Array<() => void>,
129
+ ): () => void {
130
+ const VH_RE: RegExp = /(\d+(?:\.\d+)?)vh\b/g;
131
+ const processedStyles: WeakSet<HTMLStyleElement> = new WeakSet();
132
+ const processedSheets: WeakMap<CSSStyleSheet, number> = new WeakMap();
133
+ const watchedStyles: WeakSet<HTMLStyleElement> = new WeakSet();
134
+ const styleObservers: Set<MutationObserver> = new Set();
135
+
136
+ const neutralize = (): void => {
137
+ const rewrite = (input: string): string =>
138
+ input.replace(VH_RE, (_match: string, n: string): string =>
139
+ `${((parseFloat(n) / 100) * referenceVhBase).toFixed(2)}px`,
140
+ );
141
+
142
+ document.querySelectorAll<HTMLStyleElement>("style").forEach((el: HTMLStyleElement): void => {
143
+ watchStyleEl(el);
144
+ if (processedStyles.has(el)) return;
145
+ processedStyles.add(el);
146
+ const text: string | null = el.textContent;
147
+ if (!text || text.indexOf("vh") === -1) return;
148
+ const next: string = rewrite(text);
149
+ if (next !== text) el.textContent = next;
150
+ });
151
+
152
+ for (let i: number = 0; i < document.styleSheets.length; i++) {
153
+ const sheet: CSSStyleSheet | undefined = document.styleSheets[i];
154
+ if (!sheet) continue;
155
+ let rules: CSSRuleList;
156
+ try {
157
+ rules = sheet.cssRules;
158
+ } catch {
159
+ continue; // CORS-protected
160
+ }
161
+ if (processedSheets.get(sheet) === rules.length) continue;
162
+ rewriteVhInRules(rules, rewrite);
163
+ processedSheets.set(sheet, rules.length);
164
+ }
165
+ };
166
+
167
+ const debouncer: Debouncer = createDebouncer(neutralize, NEUTRALIZE_DEBOUNCE_MS);
168
+ const debouncedNeutralize: () => void = debouncer.trigger;
169
+ cleanups.push(debouncer.cancel);
170
+
171
+ const watchStyleEl = (el: HTMLStyleElement): void => {
172
+ if (watchedStyles.has(el)) return;
173
+ watchedStyles.add(el);
174
+ const obs: MutationObserver = new MutationObserver((): void => {
175
+ processedStyles.delete(el);
176
+ debouncedNeutralize();
177
+ });
178
+ obs.observe(el, { characterData: true, childList: true, subtree: true });
179
+ styleObservers.add(obs);
180
+ };
181
+ cleanups.push((): void => {
182
+ for (const obs of styleObservers) obs.disconnect();
183
+ styleObservers.clear();
184
+ });
185
+
186
+ debouncedNeutralize();
187
+ if (document.readyState === "loading") {
188
+ document.addEventListener("DOMContentLoaded", debouncedNeutralize);
189
+ cleanups.push((): void => document.removeEventListener("DOMContentLoaded", debouncedNeutralize));
190
+ }
191
+ window.addEventListener("load", debouncedNeutralize);
192
+ cleanups.push((): void => window.removeEventListener("load", debouncedNeutralize));
193
+
194
+ const headObserver: MutationObserver = new MutationObserver((mutations: MutationRecord[]): void => {
195
+ for (const m of mutations) {
196
+ if (containsStylesheetNode(m.addedNodes) || containsStylesheetNode(m.removedNodes)) {
197
+ debouncedNeutralize();
198
+ return;
199
+ }
200
+ }
201
+ });
202
+ cleanups.push((): void => headObserver.disconnect());
203
+
204
+ const attachHeadObserver = (): void => {
205
+ if (document.head) headObserver.observe(document.head, { childList: true, subtree: false });
206
+ };
207
+ if (document.head) {
208
+ attachHeadObserver();
209
+ } else {
210
+ document.addEventListener("DOMContentLoaded", attachHeadObserver);
211
+ cleanups.push((): void => document.removeEventListener("DOMContentLoaded", attachHeadObserver));
212
+ }
213
+
214
+ return debouncedNeutralize;
215
+ }
216
+
217
+ function rewriteVhInRules(rules: CSSRuleList, rewrite: (value: string) => string): void {
218
+ for (let i: number = 0; i < rules.length; i++) {
219
+ const rule: IndexableCssRule | undefined = rules[i] as IndexableCssRule | undefined;
220
+ if (!rule) continue;
221
+ if (rule.cssRules) rewriteVhInRules(rule.cssRules, rewrite);
222
+ const style: CSSStyleDeclaration | undefined = rule.style;
223
+ if (!style) continue;
224
+ for (let j: number = 0; j < style.length; j++) {
225
+ const prop: string | undefined = style[j];
226
+ if (!prop) continue;
227
+ const value: string = style.getPropertyValue(prop);
228
+ if (!value || value.indexOf("vh") === -1) continue;
229
+ const next: string = rewrite(value);
230
+ if (next !== value) style.setProperty(prop, next, style.getPropertyPriority(prop));
231
+ }
232
+ }
233
+ }
234
+
235
+ function containsStylesheetNode(nodes: NodeList): boolean {
236
+ for (let i: number = 0; i < nodes.length; i++) {
237
+ const node: Node | undefined = nodes[i];
238
+ if (node instanceof HTMLStyleElement || node instanceof HTMLLinkElement) return true;
239
+ }
240
+ return false;
241
+ }
242
+
243
+ function measureContentHeight(): number {
244
+ return Math.max(
245
+ document.documentElement.scrollHeight,
246
+ document.body?.scrollHeight ?? 0,
247
+ );
248
+ }
249
+
250
+ function createDebouncer(fn: () => void, delayMs: number): Debouncer {
251
+ let timer: number | undefined;
252
+ return {
253
+ trigger: (): void => {
254
+ if (timer !== undefined) window.clearTimeout(timer);
255
+ timer = window.setTimeout(fn, delayMs);
256
+ },
257
+ cancel: (): void => {
258
+ if (timer !== undefined) {
259
+ window.clearTimeout(timer);
260
+ timer = undefined;
261
+ }
262
+ },
263
+ };
264
+ }