@base44/vite-plugin 1.0.9 → 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.
- package/dist/bridge.d.ts +1 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +1 -0
- package/dist/bridge.js.map +1 -1
- package/dist/html-injections-plugin.d.ts.map +1 -1
- package/dist/html-injections-plugin.js +4 -1
- package/dist/html-injections-plugin.js.map +1 -1
- package/dist/injections/page-height-bridge.d.ts +8 -0
- package/dist/injections/page-height-bridge.d.ts.map +1 -0
- package/dist/injections/page-height-bridge.js +242 -0
- package/dist/injections/page-height-bridge.js.map +1 -0
- package/dist/processors/collection-item-id-processor.d.ts +8 -0
- package/dist/processors/collection-item-id-processor.d.ts.map +1 -1
- package/dist/processors/collection-item-id-processor.js +23 -2
- package/dist/processors/collection-item-id-processor.js.map +1 -1
- package/dist/statics/index.mjs +8 -8
- package/dist/statics/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/bridge.ts +1 -0
- package/src/html-injections-plugin.ts +4 -1
- package/src/injections/page-height-bridge.ts +264 -0
- package/src/processors/collection-item-id-processor.ts +26 -3
|
@@ -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
|
+
}
|
|
@@ -71,6 +71,10 @@ export class DataItemIdProcessor {
|
|
|
71
71
|
const fn = this.pathUtils.findEnclosingFunction(path);
|
|
72
72
|
if (!fn) return;
|
|
73
73
|
|
|
74
|
+
// Skip if this function IS a .map()/.flatMap() callback — it's an iteration
|
|
75
|
+
// function, not a component (e.g. [...Array(3)].map((_, i) => <div />))
|
|
76
|
+
if (this.isMapCallbackFunction(fn)) return;
|
|
77
|
+
|
|
74
78
|
// Skip if this function maps over a collection — it's a container, not an item
|
|
75
79
|
if (this.functionContainsMapCall(fn)) return;
|
|
76
80
|
|
|
@@ -86,6 +90,24 @@ export class DataItemIdProcessor {
|
|
|
86
90
|
);
|
|
87
91
|
}
|
|
88
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Returns true when `fn` is a direct callback argument to a `.map()` or
|
|
95
|
+
* `.flatMap()` call — i.e. the function is an iteration callback, not a
|
|
96
|
+
* component definition. This prevents injecting prop-forwarding into
|
|
97
|
+
* patterns like `[...Array(3)].map((_, i) => <div />)` where the first
|
|
98
|
+
* param may be `undefined`.
|
|
99
|
+
*/
|
|
100
|
+
private isMapCallbackFunction(fn: NodePath<t.Function>): boolean {
|
|
101
|
+
const parent = fn.parentPath;
|
|
102
|
+
if (!parent?.isCallExpression()) return false;
|
|
103
|
+
const callee = parent.node.callee;
|
|
104
|
+
return (
|
|
105
|
+
this.types.isMemberExpression(callee) &&
|
|
106
|
+
this.types.isIdentifier(callee.property) &&
|
|
107
|
+
(callee.property.name === "map" || callee.property.name === "flatMap")
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
89
111
|
private functionContainsMapCall(fn: NodePath<t.Function>): boolean {
|
|
90
112
|
let found = false;
|
|
91
113
|
const types = this.types;
|
|
@@ -159,11 +181,12 @@ export class DataItemIdProcessor {
|
|
|
159
181
|
}
|
|
160
182
|
|
|
161
183
|
if (firstParam.isIdentifier()) {
|
|
162
|
-
// props["data-collection-item-id"] —
|
|
163
|
-
return this.types.
|
|
184
|
+
// props?.["data-collection-item-id"] — optional access in case param is nullish
|
|
185
|
+
return this.types.optionalMemberExpression(
|
|
164
186
|
this.types.identifier(firstParam.node.name),
|
|
165
187
|
this.types.stringLiteral(DATA_COLLECTION_ITEM_ID),
|
|
166
|
-
true // computed
|
|
188
|
+
true, // computed
|
|
189
|
+
true // optional
|
|
167
190
|
);
|
|
168
191
|
}
|
|
169
192
|
|