nothing-browser 0.0.13 → 0.0.15

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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +120 -887
  3. package/dist/piggy/cache/memory.d.ts +7 -0
  4. package/dist/piggy/cache/memory.d.ts.map +1 -0
  5. package/dist/piggy/client/index.d.ts +79 -0
  6. package/dist/piggy/client/index.d.ts.map +1 -0
  7. package/dist/piggy/human/index.d.ts +7 -0
  8. package/dist/piggy/human/index.d.ts.map +1 -0
  9. package/dist/piggy/intercept/scripts.d.ts +13 -0
  10. package/dist/piggy/intercept/scripts.d.ts.map +1 -0
  11. package/dist/piggy/launch/detect.d.ts +3 -0
  12. package/dist/piggy/launch/detect.d.ts.map +1 -0
  13. package/dist/piggy/launch/spawn.d.ts +6 -0
  14. package/dist/piggy/launch/spawn.d.ts.map +1 -0
  15. package/dist/piggy/logger/index.d.ts +3 -0
  16. package/dist/piggy/logger/index.d.ts.map +1 -0
  17. package/dist/piggy/open/index.d.ts +4 -0
  18. package/dist/piggy/open/index.d.ts.map +1 -0
  19. package/dist/piggy/register/index.d.ts +7 -0
  20. package/dist/piggy/register/index.d.ts.map +1 -0
  21. package/dist/piggy/server/index.d.ts +21 -0
  22. package/dist/piggy/server/index.d.ts.map +1 -0
  23. package/dist/piggy.d.ts +222 -0
  24. package/dist/piggy.d.ts.map +1 -0
  25. package/dist/piggy.js +3 -3
  26. package/dist/register/index.js +1 -1
  27. package/package.json +30 -16
  28. package/piggy/cache/memory.d.ts +7 -0
  29. package/piggy/cache/memory.d.ts.map +1 -0
  30. package/piggy/client/index.d.ts +79 -0
  31. package/piggy/client/index.d.ts.map +1 -0
  32. package/piggy/human/index.d.ts +7 -0
  33. package/piggy/human/index.d.ts.map +1 -0
  34. package/piggy/intercept/scripts.d.ts +13 -0
  35. package/piggy/intercept/scripts.d.ts.map +1 -0
  36. package/piggy/launch/detect.d.ts +3 -0
  37. package/piggy/launch/detect.d.ts.map +1 -0
  38. package/piggy/launch/spawn.d.ts +6 -0
  39. package/piggy/launch/spawn.d.ts.map +1 -0
  40. package/piggy/logger/index.d.ts +3 -0
  41. package/piggy/logger/index.d.ts.map +1 -0
  42. package/piggy/open/index.d.ts +4 -0
  43. package/piggy/open/index.d.ts.map +1 -0
  44. package/piggy/register/index.d.ts +7 -0
  45. package/piggy/register/index.d.ts.map +1 -0
  46. package/piggy/register/index.ts +1 -1
  47. package/piggy/server/index.d.ts +21 -0
  48. package/piggy/server/index.d.ts.map +1 -0
  49. package/piggy.ts +344 -33
package/piggy.ts CHANGED
@@ -5,24 +5,321 @@ import { PiggyClient } from "./piggy/client";
5
5
  import { setClient, setHumanMode, createSiteObject } from "./piggy/register";
6
6
  import { routeRegistry, keepAliveSites, startServer, stopServer } from "./piggy/server";
7
7
  import logger from "./piggy/logger";
8
+ import type { Logger } from "ernest-logger";
9
+ import type { Elysia } from "elysia";
10
+ import type { RouteHandler, BeforeMiddleware, RouteConfig } from "./piggy/server";
8
11
 
9
- type TabMode = "tab" | "process";
10
- type SiteObject = ReturnType<typeof createSiteObject>;
12
+ // ── Core types ───────────────────────────────────────────────────────────────
13
+
14
+ export type TabMode = "tab" | "process";
15
+
16
+ /** Returned by createSiteObject — full API surface of a registered site. */
17
+ export interface SiteObject {
18
+ /** Internal name used when registering. */
19
+ _name: string;
20
+ /** Internal CDP / socket tab ID. */
21
+ _tabId: string;
22
+
23
+ // ── Navigation ─────────────────────────────────────────────────────────────
24
+ navigate(url?: string, opts?: { retries?: number }): Promise<void>;
25
+ reload(): Promise<void>;
26
+ goBack(): Promise<void>;
27
+ goForward(): Promise<void>;
28
+ waitForNavigation(): Promise<void>;
29
+
30
+ title(): Promise<string>;
31
+ /** Returns the last known URL (synchronous — does NOT hit the browser). */
32
+ url(): string;
33
+ content(): Promise<string>;
34
+
35
+ // ── Timing ────────────────────────────────────────────────────────────────
36
+ wait(ms: number): Promise<void>;
37
+ waitForSelector(selector: string, timeout?: number): Promise<void>;
38
+ waitForVisible(selector: string, timeout?: number): Promise<void>;
39
+ waitForResponse(pattern: string, timeout?: number): Promise<void>;
40
+
41
+ // ── Init script ───────────────────────────────────────────────────────────
42
+ addInitScript(js: string | (() => void)): Promise<SiteObject>;
43
+
44
+ // ── Event emitter ─────────────────────────────────────────────────────────
45
+ on(event: "navigate", handler: (url: string) => void): () => void;
46
+ on(event: string, handler: (data: unknown) => void): () => void;
47
+ off(event: string, handler: (data: unknown) => void): void;
48
+
49
+ // ── Interactions ──────────────────────────────────────────────────────────
50
+ click(selector: string, opts?: { retries?: number; timeout?: number }): Promise<boolean>;
51
+ doubleClick(selector: string): Promise<boolean>;
52
+ hover(selector: string): Promise<boolean>;
53
+ type(
54
+ selector: string,
55
+ text: string,
56
+ opts?: { delay?: number; retries?: number; fact?: boolean; wpm?: number }
57
+ ): Promise<boolean>;
58
+ select(selector: string, value: string): Promise<boolean>;
59
+ evaluate<T = unknown>(js: string | ((...args: unknown[]) => T), ...args: unknown[]): Promise<T>;
60
+
61
+ keyboard: {
62
+ press(key: string): Promise<boolean>;
63
+ combo(combo: string): Promise<boolean>;
64
+ };
65
+
66
+ mouse: {
67
+ move(x: number, y: number): Promise<boolean>;
68
+ drag(from: { x: number; y: number }, to: { x: number; y: number }): Promise<boolean>;
69
+ };
70
+
71
+ scroll: {
72
+ to(selector: string): Promise<boolean>;
73
+ by(px: number): Promise<boolean>;
74
+ };
75
+
76
+ // ── Fetch ─────────────────────────────────────────────────────────────────
77
+ fetchText(selector: string): Promise<string | null>;
78
+ fetchLinks(selector: string): Promise<string[]>;
79
+ fetchImages(selector: string): Promise<string[]>;
80
+
81
+ search: {
82
+ css(query: string): Promise<unknown>;
83
+ id(query: string): Promise<unknown>;
84
+ };
85
+
86
+ // ── Screenshot / PDF ──────────────────────────────────────────────────────
87
+ screenshot(filePath?: string): Promise<string>;
88
+ pdf(filePath?: string): Promise<string>;
89
+ blockImages(): Promise<void>;
90
+ unblockImages(): Promise<void>;
91
+
92
+ // ── Cookies ───────────────────────────────────────────────────────────────
93
+ cookies: {
94
+ set(name: string, value: string, domain: string, path?: string): Promise<void>;
95
+ get(name: string): Promise<unknown>;
96
+ delete(name: string): Promise<void>;
97
+ list(): Promise<unknown[]>;
98
+ };
99
+
100
+ // ── Interception ──────────────────────────────────────────────────────────
101
+ intercept: {
102
+ block(pattern: string): Promise<void>;
103
+ redirect(pattern: string, redirectUrl: string): Promise<void>;
104
+ headers(pattern: string, headers: Record<string, string>): Promise<void>;
105
+ respond(
106
+ pattern: string,
107
+ handlerOrResponse:
108
+ | { status?: number; contentType?: string; body: string }
109
+ | ((req: { url: string; method: string }) => {
110
+ status?: number;
111
+ contentType?: string;
112
+ body: string;
113
+ })
114
+ ): Promise<SiteObject>;
115
+ modifyResponse(
116
+ pattern: string,
117
+ handler: (response: {
118
+ body: string;
119
+ status: number;
120
+ headers: Record<string, string>;
121
+ }) => Promise<{ body?: string; status?: number; headers?: Record<string, string> } | void> | void
122
+ ): Promise<SiteObject>;
123
+ clear(): Promise<void>;
124
+ };
125
+
126
+ // ── Network capture ───────────────────────────────────────────────────────
127
+ capture: {
128
+ start(): Promise<void>;
129
+ stop(): Promise<void>;
130
+ requests(): Promise<unknown[]>;
131
+ ws(): Promise<unknown[]>;
132
+ cookies(): Promise<unknown[]>;
133
+ storage(): Promise<unknown>;
134
+ clear(): Promise<void>;
135
+ };
136
+
137
+ // ── Session ───────────────────────────────────────────────────────────────
138
+ session: {
139
+ export(): Promise<unknown>;
140
+ import(data: unknown): Promise<void>;
141
+ };
142
+
143
+ // ── Expose / unexpose functions ───────────────────────────────────────────
144
+ exposeFunction(
145
+ fnName: string,
146
+ handler: (data: unknown) => Promise<unknown> | unknown
147
+ ): Promise<SiteObject>;
148
+ unexposeFunction(fnName: string): Promise<SiteObject>;
149
+ clearExposedFunctions(): Promise<SiteObject>;
150
+ exposeAndInject(
151
+ fnName: string,
152
+ handler: (data: unknown) => Promise<unknown> | unknown,
153
+ injectionJs: string | ((fnName: string) => string)
154
+ ): Promise<SiteObject>;
155
+
156
+ // ── Elysia route registration ─────────────────────────────────────────────
157
+ api(
158
+ path: string,
159
+ handler: RouteHandler,
160
+ opts?: {
161
+ ttl?: number;
162
+ before?: BeforeMiddleware[];
163
+ method?: "GET" | "POST" | "PUT" | "DELETE";
164
+ }
165
+ ): SiteObject;
166
+
167
+ noclose(): SiteObject;
168
+ close(): Promise<void>;
169
+ }
170
+
171
+ // ── Route summary returned by piggy.routes() ─────────────────────────────────
172
+
173
+ export interface RouteSummary {
174
+ site: string;
175
+ method: RouteConfig["method"];
176
+ path: string;
177
+ ttl: number;
178
+ middlewareCount: number;
179
+ }
180
+
181
+ // ── Multi-site proxy helpers ──────────────────────────────────────────────────
182
+
183
+ /**
184
+ * Proxy that runs the same method on every site in parallel (Promise.all).
185
+ * The return type mirrors the SiteObject API but every method returns
186
+ * `Promise<ReturnType[]>` instead of a single value.
187
+ */
188
+ export type AllProxy = {
189
+ [K in keyof SiteObject]: SiteObject[K] extends (...args: infer A) => Promise<infer R>
190
+ ? (...args: A) => Promise<R[]>
191
+ : SiteObject[K] extends (...args: infer A) => infer R
192
+ ? (...args: A) => Promise<R[]>
193
+ : never;
194
+ };
195
+
196
+ /**
197
+ * Proxy that runs the same method on every site in parallel and returns a
198
+ * Record keyed by site `_name`.
199
+ */
200
+ export type DiffProxy = {
201
+ [K in keyof SiteObject]: SiteObject[K] extends (...args: infer A) => Promise<infer R>
202
+ ? (...args: A) => Promise<Record<string, R>>
203
+ : SiteObject[K] extends (...args: infer A) => infer R
204
+ ? (...args: A) => Promise<Record<string, R>>
205
+ : never;
206
+ };
207
+
208
+ // ── Launch / register option bags ────────────────────────────────────────────
209
+
210
+ export interface LaunchOptions {
211
+ /** Whether to open a separate browser process per registered site or share a single one via tabs. Default: `"tab"`. */
212
+ mode?: TabMode;
213
+ /** Whether to run the browser binary in headed or headless mode. Default: `"headless"`. */
214
+ binary?: BinaryMode;
215
+ }
216
+
217
+ export interface RegisterOptions {
218
+ /** Override the binary mode for this site's dedicated process (only used when `mode === "process"`). */
219
+ binary?: BinaryMode;
220
+ }
221
+
222
+ export interface ServeOptions {
223
+ hostname?: string;
224
+ }
225
+
226
+ // ── The Piggy object type ─────────────────────────────────────────────────────
227
+
228
+ export interface Piggy {
229
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
230
+ /**
231
+ * Spawns the Nothing Browser binary and connects the internal socket client.
232
+ * Must be called before any other method.
233
+ */
234
+ launch(opts?: LaunchOptions): Promise<Piggy>;
235
+
236
+ /**
237
+ * Registers a named site at the given URL.
238
+ * After registration `piggy[name]` is available as a `SiteObject`.
239
+ */
240
+ register(name: string, url: string, opts?: RegisterOptions): Promise<Piggy>;
241
+
242
+ // ── Global controls ────────────────────────────────────────────────────────
243
+ /** Enables or disables human-simulation mode (random delays, typos, smooth scrolling). */
244
+ actHuman(enable: boolean): Piggy;
245
+
246
+ /** Changes the tab/process mode *before* the next `register()` call. */
247
+ mode(m: TabMode): Piggy;
248
+
249
+ // ── Global function exposure ───────────────────────────────────────────────
250
+ /** Exposes a Node.js function to the browser's global scope on the default (or specified) tab. */
251
+ expose(
252
+ name: string,
253
+ handler: (data: unknown) => Promise<unknown> | unknown,
254
+ tabId?: string
255
+ ): Promise<Piggy>;
256
+
257
+ /** Removes a previously exposed function from the browser. */
258
+ unexpose(name: string, tabId?: string): Promise<Piggy>;
259
+
260
+ // ── Elysia server ─────────────────────────────────────────────────────────
261
+ /** Starts an Elysia HTTP server that exposes all registered `.api()` routes. */
262
+ serve(port: number, opts?: ServeOptions): Promise<Elysia>;
263
+
264
+ /** Stops the running Elysia server. */
265
+ stopServer(): void;
266
+
267
+ // ── Introspection ─────────────────────────────────────────────────────────
268
+ /** Returns a summary of every mounted HTTP route. */
269
+ routes(): RouteSummary[];
270
+
271
+ // ── Multi-site helpers ────────────────────────────────────────────────────
272
+ /**
273
+ * Returns a proxy that calls the same method on **all** given sites in
274
+ * parallel (via `Promise.all`) and returns an array of results.
275
+ */
276
+ all(sites: SiteObject[]): AllProxy;
277
+
278
+ /**
279
+ * Returns a proxy that calls the same method on **all** given sites in
280
+ * parallel and returns a `Record<siteName, result>` object.
281
+ */
282
+ diff(sites: SiteObject[]): DiffProxy;
283
+
284
+ // ── Shutdown ──────────────────────────────────────────────────────────────
285
+ /**
286
+ * Gracefully shuts down all tabs and the browser process.
287
+ * Pass `{ force: true }` to skip graceful close and kill immediately.
288
+ */
289
+ close(opts?: { force?: boolean }): Promise<void>;
290
+
291
+ // ── Utilities ─────────────────────────────────────────────────────────────
292
+ /** Detects the Nothing Browser binary path for the given mode. */
293
+ detect(mode?: BinaryMode): string | null;
294
+
295
+ /** The ernest-logger instance used internally. */
296
+ logger: Logger;
297
+
298
+ // ── Dynamic site keys ─────────────────────────────────────────────────────
299
+ /**
300
+ * After `piggy.register("mysite", url)` you can access the site as
301
+ * `piggy.mysite`. The index signature covers those dynamic properties.
302
+ */
303
+ [site: string]: SiteObject | unknown;
304
+ }
305
+
306
+ // ── Module-private state ──────────────────────────────────────────────────────
11
307
 
12
308
  let _client: PiggyClient | null = null;
13
309
  let _tabMode: TabMode = "tab";
14
310
  const _extraProcs: { socket: string; client: PiggyClient }[] = [];
15
311
  const _sites: Record<string, SiteObject> = {};
16
312
 
17
- // CREATE THE PIGGY OBJECT AS A PLAIN OBJECT - NOT A PROXY
18
- const piggy: any = {
313
+ // ── The piggy singleton ───────────────────────────────────────────────────────
314
+
315
+ const piggy: Piggy = {
19
316
  // ── Lifecycle ───────────────────────────────────────────────────────────────
20
317
 
21
- launch: async (opts?: { mode?: TabMode; binary?: BinaryMode }) => {
318
+ launch: async (opts?: LaunchOptions): Promise<Piggy> => {
22
319
  _tabMode = opts?.mode ?? "tab";
23
320
  const binaryMode: BinaryMode = opts?.binary ?? "headless";
24
321
  await spawnBrowser(binaryMode);
25
- await new Promise(r => setTimeout(r, 500));
322
+ await new Promise<void>((r) => setTimeout(r, 500));
26
323
  _client = new PiggyClient();
27
324
  await _client.connect();
28
325
  setClient(_client);
@@ -30,7 +327,7 @@ const piggy: any = {
30
327
  return piggy;
31
328
  },
32
329
 
33
- register: async (name: string, url: string, opts?: { binary?: BinaryMode }) => {
330
+ register: async (name: string, url: string, opts?: RegisterOptions): Promise<Piggy> => {
34
331
  if (!url?.trim()) throw new Error(`No URL for site "${name}"`);
35
332
  const binaryMode: BinaryMode = opts?.binary ?? "headless";
36
333
 
@@ -38,21 +335,20 @@ const piggy: any = {
38
335
  if (_tabMode === "tab") {
39
336
  if (!_client) throw new Error("No client. Call piggy.launch() first.");
40
337
  tabId = await _client.newTab();
41
- // HERE IT IS - CREATE SITE OBJECT AND ASSIGN DIRECTLY
42
- const siteObj = createSiteObject(name, url, _client, tabId);
338
+ const siteObj: SiteObject = createSiteObject(name, url, _client, tabId);
43
339
  _sites[name] = siteObj;
44
- piggy[name] = siteObj; // DIRECT ASSIGNMENT - NO PROXY
340
+ (piggy as Record<string, unknown>)[name] = siteObj;
45
341
  logger.success(`[${name}] registered as tab ${tabId}`);
46
342
  } else {
47
343
  const socketName = `piggy_${name}`;
48
344
  await spawnBrowserOnSocket(socketName, binaryMode);
49
- await new Promise(r => setTimeout(r, 500));
345
+ await new Promise<void>((r) => setTimeout(r, 500));
50
346
  const c = new PiggyClient(socketName);
51
347
  await c.connect();
52
348
  _extraProcs.push({ socket: socketName, client: c });
53
- const siteObj = createSiteObject(name, url, c, "default");
349
+ const siteObj: SiteObject = createSiteObject(name, url, c, "default");
54
350
  _sites[name] = siteObj;
55
- piggy[name] = siteObj; // DIRECT ASSIGNMENT - NO PROXY
351
+ (piggy as Record<string, unknown>)[name] = siteObj;
56
352
  logger.success(`[${name}] registered as process on "${socketName}"`);
57
353
  }
58
354
 
@@ -61,24 +357,31 @@ const piggy: any = {
61
357
 
62
358
  // ── Global controls ─────────────────────────────────────────────────────────
63
359
 
64
- actHuman: (enable: boolean) => {
360
+ actHuman: (enable: boolean): Piggy => {
65
361
  setHumanMode(enable);
66
362
  logger.info(`[piggy] actHuman: ${enable}`);
67
363
  return piggy;
68
364
  },
69
365
 
70
- mode: (m: TabMode) => { _tabMode = m; return piggy; },
366
+ mode: (m: TabMode): Piggy => {
367
+ _tabMode = m;
368
+ return piggy;
369
+ },
71
370
 
72
371
  // ── Expose Function (global) ─────────────────────────────────────────────────
73
372
 
74
- expose: async (name: string, handler: (data: any) => Promise<any> | any, tabId = "default") => {
373
+ expose: async (
374
+ name: string,
375
+ handler: (data: unknown) => Promise<unknown> | unknown,
376
+ tabId = "default"
377
+ ): Promise<Piggy> => {
75
378
  if (!_client) throw new Error("No client. Call piggy.launch() first.");
76
379
  await _client.exposeFunction(name, handler, tabId);
77
380
  logger.success(`[piggy] exposed global function: ${name}`);
78
381
  return piggy;
79
382
  },
80
383
 
81
- unexpose: async (name: string, tabId = "default") => {
384
+ unexpose: async (name: string, tabId = "default"): Promise<Piggy> => {
82
385
  if (!_client) throw new Error("No client. Call piggy.launch() first.");
83
386
  await _client.unexposeFunction(name, tabId);
84
387
  logger.info(`[piggy] unexposed function: ${name}`);
@@ -87,18 +390,18 @@ const piggy: any = {
87
390
 
88
391
  // ── Elysia server ────────────────────────────────────────────────────────────
89
392
 
90
- serve: (port: number, opts?: { hostname?: string }) =>
393
+ serve: (port: number, opts?: ServeOptions): Promise<Elysia> =>
91
394
  startServer(port, opts?.hostname),
92
395
 
93
396
  stopServer,
94
397
 
95
398
  // ── Route listing ────────────────────────────────────────────────────────────
96
399
 
97
- routes: () =>
98
- Array.from(routeRegistry.entries()).map(([key, cfg]) => {
400
+ routes: (): RouteSummary[] =>
401
+ Array.from(routeRegistry.entries()).map(([key, cfg]): RouteSummary => {
99
402
  const [site] = key.split(":");
100
403
  return {
101
- site,
404
+ site: site ?? key,
102
405
  method: cfg.method,
103
406
  path: `/${site}${cfg.path}`,
104
407
  ttl: cfg.ttl,
@@ -108,24 +411,33 @@ const piggy: any = {
108
411
 
109
412
  // ── Multi-site ───────────────────────────────────────────────────────────────
110
413
 
111
- all: (sites: SiteObject[]) =>
112
- new Proxy({} as any, {
113
- get: (_, method: string) =>
114
- (...args: any[]) => Promise.all(sites.map((s: any) => s[method]?.(...args))),
414
+ all: (sites: SiteObject[]): AllProxy =>
415
+ new Proxy({} as AllProxy, {
416
+ get: (_target, method: string) =>
417
+ (...args: unknown[]) =>
418
+ Promise.all(
419
+ sites.map((s) =>
420
+ (s as unknown as Record<string, (...a: unknown[]) => unknown>)[method]?.(...args)
421
+ )
422
+ ),
115
423
  }),
116
424
 
117
- diff: (sites: SiteObject[]) =>
118
- new Proxy({} as any, {
119
- get: (_, method: string) =>
120
- async (...args: any[]) => {
121
- const results = await Promise.all(sites.map((s: any) => s[method]?.(...args)));
122
- return Object.fromEntries(sites.map((s: any, i) => [s._name ?? i, results[i]]));
425
+ diff: (sites: SiteObject[]): DiffProxy =>
426
+ new Proxy({} as DiffProxy, {
427
+ get: (_target, method: string) =>
428
+ async (...args: unknown[]) => {
429
+ const results = await Promise.all(
430
+ sites.map((s) =>
431
+ (s as unknown as Record<string, (...a: unknown[]) => unknown>)[method]?.(...args)
432
+ )
433
+ );
434
+ return Object.fromEntries(sites.map((s, i) => [s._name ?? i, results[i]]));
123
435
  },
124
436
  }),
125
437
 
126
438
  // ── Shutdown ─────────────────────────────────────────────────────────────────
127
439
 
128
- close: async (opts?: { force?: boolean }) => {
440
+ close: async (opts?: { force?: boolean }): Promise<void> => {
129
441
  stopServer();
130
442
  if (opts?.force) {
131
443
  for (const { client: c } of _extraProcs) c.disconnect();
@@ -153,6 +465,5 @@ const piggy: any = {
153
465
  logger,
154
466
  };
155
467
 
156
- // NO PROXY WRAPPER - EXPORT THE PLAIN OBJECT DIRECTLY
157
468
  export default piggy;
158
469
  export { piggy };