@zucker-framework/ext-kit 1.0.2 → 1.0.6

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,309 @@
1
+ "use strict";
2
+ (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, { get: all[name], enumerable: true });
7
+ };
8
+
9
+ // src/scroll-capture.js
10
+ var scroll_capture_exports = {};
11
+ __export(scroll_capture_exports, {
12
+ prepare: () => prepare,
13
+ restore: () => restore
14
+ });
15
+ var sessions = /* @__PURE__ */ new WeakMap();
16
+ function pageTask(action, id, index, leaseMs) {
17
+ const key = "__zuckerScrollCapture";
18
+ let state = window[key];
19
+ if (action === "restore") {
20
+ if (state?.id === id) state.restore();
21
+ return {};
22
+ }
23
+ if (action === "init") {
24
+ if (state) throw new Error("Another scrolling-content capture is active");
25
+ const elements = [...document.querySelectorAll("*")];
26
+ if (elements.length > 3e4)
27
+ throw new Error(
28
+ "This page is too complex to expand its scrolling content safely."
29
+ );
30
+ const panels = elements.filter((el) => {
31
+ if (el === document.body || el === document.documentElement || el === document.scrollingElement || el.closest(
32
+ 'nav,aside,[role="navigation"],[role="menu"],textarea,select'
33
+ ))
34
+ return false;
35
+ const rect = el.getBoundingClientRect(), css = getComputedStyle(el);
36
+ if (rect.width < 120 || rect.height < 80 || el.scrollHeight <= el.clientHeight + 2 || !/^(auto|scroll|overlay)$/.test(css.overflowY) || css.visibility !== "visible")
37
+ return false;
38
+ for (let parent = el; parent; parent = parent.parentElement) {
39
+ const style = getComputedStyle(parent);
40
+ if (style.display === "none" || Number(style.opacity) === 0)
41
+ return false;
42
+ }
43
+ return true;
44
+ });
45
+ if (!panels.length) return { count: 0 };
46
+ if (panels.length > 24)
47
+ throw new Error(
48
+ "This page has too many independent scrolling areas. Use Visible window or export smaller sections."
49
+ );
50
+ const virtual = '.rc-virtual-list-holder,.ant-table-tbody-virtual-holder,[data-virtuoso-scroller],.ReactVirtualized__Grid,.ReactVirtualized__List,[data-virtualized="true"]';
51
+ if (panels.some(
52
+ (el) => el.matches(virtual) || [...el.querySelectorAll(virtual)].some(
53
+ (list) => list.getClientRects().length && list.checkVisibility({
54
+ checkOpacity: true,
55
+ checkVisibilityCSS: true
56
+ })
57
+ )
58
+ ))
59
+ throw new Error(
60
+ "This page uses a virtual scrolling list. A complete full-page capture is not available; use the table's export or Visible window."
61
+ );
62
+ const changes = /* @__PURE__ */ new Map(), scrolls = /* @__PURE__ */ new Map();
63
+ state = {
64
+ id,
65
+ panels,
66
+ changes,
67
+ scrolls,
68
+ boxes: /* @__PURE__ */ new Map(),
69
+ watchdog: null,
70
+ leaseMs
71
+ };
72
+ state.set = (el, property, value) => {
73
+ if (!changes.has(el))
74
+ changes.set(el, {
75
+ hadStyle: el.hasAttribute("style"),
76
+ properties: /* @__PURE__ */ new Map()
77
+ });
78
+ const saved = changes.get(el).properties;
79
+ if (!saved.has(property))
80
+ saved.set(property, {
81
+ value: el.style.getPropertyValue(property),
82
+ priority: el.style.getPropertyPriority(property)
83
+ });
84
+ el.style.setProperty(property, value, "important");
85
+ };
86
+ state.restore = () => {
87
+ clearTimeout(state.watchdog);
88
+ const scrollingProperties = /* @__PURE__ */ new Set([
89
+ "scroll-behavior",
90
+ "scroll-snap-type",
91
+ "overflow-anchor"
92
+ ]);
93
+ const restoreProperty = (el, property, old) => old.value ? el.style.setProperty(property, old.value, old.priority) : el.style.removeProperty(property);
94
+ for (const [el, saved] of [...changes].reverse())
95
+ for (const [property, old] of saved.properties)
96
+ if (!scrollingProperties.has(property))
97
+ restoreProperty(el, property, old);
98
+ for (const [el, saved] of scrolls) {
99
+ el.scrollLeft = saved.x;
100
+ el.scrollTop = saved.y;
101
+ }
102
+ for (const [el, saved] of changes) {
103
+ for (const [property, old] of saved.properties)
104
+ if (scrollingProperties.has(property))
105
+ restoreProperty(el, property, old);
106
+ if (!saved.hadStyle && !el.style.length) {
107
+ el.getAttribute("style");
108
+ el.removeAttribute("style");
109
+ }
110
+ }
111
+ if (window[key]?.id === id) delete window[key];
112
+ };
113
+ window[key] = state;
114
+ state.watchdog = setTimeout(state.restore, state.leaseMs);
115
+ for (const el of elements) {
116
+ if (!el.scrollTop && !el.scrollLeft) continue;
117
+ scrolls.set(el, { x: el.scrollLeft, y: el.scrollTop });
118
+ state.set(el, "scroll-behavior", "auto");
119
+ state.set(el, "scroll-snap-type", "none");
120
+ state.set(el, "overflow-anchor", "none");
121
+ }
122
+ for (const panel of panels) {
123
+ const rect = panel.getBoundingClientRect();
124
+ state.boxes.set(panel, { width: rect.width, x: rect.x });
125
+ scrolls.set(panel, { x: panel.scrollLeft, y: panel.scrollTop });
126
+ state.set(panel, "scroll-behavior", "auto");
127
+ state.set(panel, "scroll-snap-type", "none");
128
+ state.set(panel, "overflow-anchor", "none");
129
+ panel.scrollTop = 0;
130
+ }
131
+ return { count: panels.length };
132
+ }
133
+ if (!state || state.id !== id)
134
+ throw new Error(
135
+ "The page changed while preparing its scrolling content."
136
+ );
137
+ clearTimeout(state.watchdog);
138
+ state.watchdog = setTimeout(state.restore, state.leaseMs);
139
+ if (action === "scroll" || action === "position") {
140
+ const panel = state.panels[index];
141
+ if (!panel.isConnected)
142
+ throw new Error(
143
+ "A scrolling area changed during capture. Please retry."
144
+ );
145
+ if (action === "scroll")
146
+ panel.scrollTop += Math.max(80, panel.clientHeight * 0.8);
147
+ return {
148
+ bottom: panel.scrollTop + panel.clientHeight >= panel.scrollHeight - 1,
149
+ height: panel.scrollHeight
150
+ };
151
+ }
152
+ if (action === "expand") {
153
+ for (const panel of [...state.panels].reverse()) {
154
+ if (!panel.isConnected)
155
+ throw new Error(
156
+ "A scrolling area changed during capture. Please retry."
157
+ );
158
+ let el = panel;
159
+ while (el) {
160
+ const rect = el.getBoundingClientRect(), css = getComputedStyle(el);
161
+ if (!state.scrolls.has(el))
162
+ state.scrolls.set(el, { x: el.scrollLeft, y: el.scrollTop });
163
+ if (!state.boxes.has(el))
164
+ state.boxes.set(el, { width: rect.width, x: rect.x });
165
+ state.set(el, "scroll-behavior", "auto");
166
+ state.set(el, "overflow-anchor", "none");
167
+ state.set(el, "transition", "none");
168
+ state.set(el, "box-sizing", "border-box");
169
+ state.set(el, "width", `${state.boxes.get(el).width}px`);
170
+ state.set(el, "max-height", "none");
171
+ state.set(el, "flex-shrink", "0");
172
+ if (css.position === "fixed") {
173
+ state.set(el, "position", "absolute");
174
+ state.set(el, "top", `${rect.top + scrollY}px`);
175
+ state.set(el, "left", `${rect.left + scrollX}px`);
176
+ state.set(el, "right", "auto");
177
+ }
178
+ if (css.position === "fixed" || css.position === "absolute")
179
+ state.set(el, "bottom", "auto");
180
+ if (state.panels.includes(el)) {
181
+ const height = el.scrollHeight + el.offsetHeight - el.clientHeight;
182
+ state.set(el, "height", `${height}px`);
183
+ state.set(el, "min-height", `${height}px`);
184
+ state.set(el, "scrollbar-gutter", "stable");
185
+ state.set(el, "overflow-y", "hidden");
186
+ el.scrollTop = 0;
187
+ } else {
188
+ state.set(el, "height", "auto");
189
+ state.set(
190
+ el,
191
+ "min-height",
192
+ `${Math.max(rect.height, el.clientHeight)}px`
193
+ );
194
+ if (/hidden|clip|auto|scroll/.test(css.overflowY)) {
195
+ state.set(el, "overflow-x", "clip");
196
+ state.set(el, "overflow-y", "visible");
197
+ }
198
+ }
199
+ if (css.contain !== "none") state.set(el, "contain", "none");
200
+ el = el.parentElement;
201
+ }
202
+ }
203
+ return { height: document.documentElement.scrollHeight };
204
+ }
205
+ if (action === "measure") {
206
+ for (const panel of state.panels) {
207
+ const rect = panel.getBoundingClientRect(), original = state.boxes.get(panel);
208
+ if (Math.abs(rect.width - original.width) > 1 || Math.abs(rect.x - original.x) > 1)
209
+ throw new Error(
210
+ "This scrolling layout cannot be expanded without changing its width. Use Visible window or the website's export."
211
+ );
212
+ }
213
+ return {
214
+ height: Math.max(
215
+ document.documentElement.scrollHeight,
216
+ document.body?.scrollHeight || 0
217
+ ),
218
+ panels: state.panels.map((el) => [el.scrollHeight, el.clientHeight]),
219
+ ready: document.fonts.status === "loaded" && [...document.images].every(
220
+ (image) => image.complete || !state.panels.some((panel) => panel.contains(image)) || !image.getClientRects().length || image.checkVisibility?.({
221
+ checkOpacity: true,
222
+ checkVisibilityCSS: true
223
+ }) === false
224
+ )
225
+ };
226
+ }
227
+ return {};
228
+ }
229
+ async function command(ctx, id, action, index, leaseMs) {
230
+ const response = await ctx[action === "restore" ? "cleanup" : "send"](
231
+ "Runtime.evaluate",
232
+ {
233
+ expression: `(${pageTask.toString()})(${JSON.stringify(action)},${JSON.stringify(id)},${JSON.stringify(index)},${JSON.stringify(leaseMs)})`,
234
+ returnByValue: true
235
+ }
236
+ );
237
+ if (response?.exceptionDetails)
238
+ throw new Error(
239
+ response.exceptionDetails.exception?.description?.split("\n")[0] || "Could not prepare scrolling content"
240
+ );
241
+ return response?.result?.value || {};
242
+ }
243
+ async function prepare(ctx, waitForContent = async () => {
244
+ }) {
245
+ const id = crypto.randomUUID();
246
+ sessions.set(ctx, id);
247
+ const readinessMs = Math.min(
248
+ 6e4,
249
+ Math.max(0, Number(ctx.options?.readyTimeoutMs) || 25e3)
250
+ );
251
+ const initial = await command(
252
+ ctx,
253
+ id,
254
+ "init",
255
+ null,
256
+ Math.max(6e4, readinessMs + 15e3)
257
+ );
258
+ if (!initial.count) {
259
+ sessions.delete(ctx);
260
+ return;
261
+ }
262
+ let deadline = Date.now() + readinessMs + 15e3;
263
+ for (let index = 0; index < initial.count; index++) {
264
+ let bottom = false;
265
+ for (let step = 0; step < 60 && Date.now() < deadline; step++) {
266
+ const position = await command(ctx, id, "scroll", index);
267
+ await new Promise((resolve) => setTimeout(resolve, 120));
268
+ if (position.bottom) {
269
+ await waitForContent();
270
+ const loaded = await command(ctx, id, "position", index);
271
+ if (loaded.bottom && loaded.height === position.height) {
272
+ bottom = true;
273
+ break;
274
+ }
275
+ }
276
+ }
277
+ if (!bottom)
278
+ throw new Error(
279
+ "Scrolling content keeps growing or exceeds the capture limit. Use Visible window or a smaller section."
280
+ );
281
+ }
282
+ await waitForContent();
283
+ await command(ctx, id, "expand");
284
+ await waitForContent();
285
+ deadline = Date.now() + 15e3;
286
+ let previous = "", stable = 0;
287
+ while (Date.now() < deadline) {
288
+ await command(ctx, id, "expand");
289
+ await new Promise((resolve) => setTimeout(resolve, 250));
290
+ const measured = await command(ctx, id, "measure");
291
+ const signature = JSON.stringify([measured.height, measured.panels]);
292
+ stable = measured.ready && signature === previous && measured.panels.every(([full, view]) => full <= view + 2) ? stable + 1 : 0;
293
+ if (stable >= 2) return;
294
+ previous = signature;
295
+ }
296
+ throw new Error(
297
+ "Scrolling content did not settle in time. Wait for it to finish loading and retry."
298
+ );
299
+ }
300
+ async function restore(ctx) {
301
+ const id = sessions.get(ctx);
302
+ if (!id) return;
303
+ sessions.delete(ctx);
304
+ await command(ctx, id, "restore");
305
+ }
306
+
307
+ // src/scroll-capture.entry.ts
308
+ globalThis.ZuckerScrollCapture = scroll_capture_exports;
309
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zucker-framework/ext-kit",
3
- "version": "1.0.2",
3
+ "version": "1.0.6",
4
4
  "description": "Browser extension infrastructure kit — batch engine, capture session, licensing, popup driver. Vendored into extensions as ext-kit.js / ext-kit-popup.js.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -47,6 +47,8 @@
47
47
  "scripts": {
48
48
  "build": "tsup && node build-vendored.mjs",
49
49
  "clean": "rm -rf dist",
50
- "lint": "eslint src/"
50
+ "lint": "eslint src/",
51
+ "test:unit": "node --test test/engine.test.cjs test/licensing.test.cjs test/lifecycle.test.cjs && vitest run --root ../.. packages/ext-kit/src/naming.spec.ts",
52
+ "test:browser": "node --test test/browser.test.cjs"
51
53
  }
52
54
  }