nothing-browser 0.0.12 → 0.0.14

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/README.md CHANGED
@@ -1,3 +1,4 @@
1
+ # `nothing-browser`
1
2
 
2
3
  <p align="center">
3
4
  <img src="nothing_browser_pig_pink.svg" width="160" alt="Nothing Browser logo"/>
@@ -12,51 +13,65 @@
12
13
  <a href="https://github.com/BunElysiaReact/nothing-browser/releases"><img src="https://img.shields.io/github/v/release/BunElysiaReact/nothing-browser" alt="releases"/></a>
13
14
  </p>
14
15
 
15
- ---
16
+ **A scraper-first headless browser library** powered by the Nothing Browser Qt6/Chromium engine. Control real browser tabs, intercept network traffic, spoof fingerprints, capture WebSockets — all from Bun + TypeScript.
16
17
 
17
- A scraper-first headless browser library powered by the Nothing Browser Qt6/Chromium engine. Control real browser tabs, intercept network traffic, spoof fingerprints, capture WebSockets — all from Bun + TypeScript.
18
+ ```ts
19
+ import piggy from "nothing-browser";
18
20
 
19
- ---
21
+ await piggy.launch();
22
+ await piggy.register("books", "https://books.toscrape.com");
23
+ await piggy.books.navigate();
24
+
25
+ const books = await piggy.books.evaluate(() =>
26
+ Array.from(document.querySelectorAll(".product_pod")).map(el => ({
27
+ title: el.querySelector("h3 a")?.getAttribute("title") ?? "",
28
+ price: el.querySelector(".price_color")?.textContent?.trim() ?? "",
29
+ }))
30
+ );
31
+
32
+ console.log(books);
33
+ await piggy.close();
34
+ ```
20
35
 
21
- ## Why nothing-browser
36
+ > **📚 Full documentation is available here:**
37
+ > [https://nothing-browser-docs.pages.dev/guide/piggy/quickstart](https://nothing-browser-docs.pages.dev/guide/piggy/quickstart)
38
+
39
+ ---
22
40
 
23
- Yes, we are bragging. Here's why.
41
+ ## Why nothing-browser?
24
42
 
25
- | | nothing-browser | Puppeteer | Playwright |
26
- |---|---|---|---|
27
- | Imports | **1** | 5–10 | 5–10 |
28
- | Lines to scrape a site | **~20** | 80–200 | 80–200 |
29
- | Fingerprint spoofing | ✅ built in | ❌ plugin needed | ❌ plugin needed |
30
- | Network capture | ✅ built in | ❌ manual | ❌ manual |
31
- | Built-in API server | ✅ | ❌ | ❌ |
32
- | Cloudflare bypass | ✅ passes | ⚠️ often blocked | ⚠️ often blocked |
33
- | Headless detection bypass | ✅ built in | manual | manual |
34
- | Session persistence | ✅ built in | ❌ manual | ❌ manual |
35
- | Human mode | ✅ built in | ❌ manual | ❌ manual |
36
- | **Browser → Node.js RPC** | ✅ **exposeFunction** | ✅ page.exposeFunction | ✅ page.exposeFunction |
43
+ | | nothing-browser | Puppeteer | Playwright |
44
+ |------------------------|----------------|-----------|------------|
45
+ | Imports | **1** | 5–10 | 5–10 |
46
+ | Lines to scrape a site | **~20** | 80–200 | 80–200 |
47
+ | Fingerprint spoofing | ✅ built in | ❌ plugin | ❌ plugin |
48
+ | Network capture | ✅ built in | ❌ manual | ❌ manual |
49
+ | Built-in API server | ✅ | ❌ | ❌ |
50
+ | Cloudflare bypass | ✅ passes | ⚠️ often blocked | ⚠️ often blocked |
51
+ | **Browser Node.js RPC** | ✅ **`exposeFunction`** | `page.exposeFunction` | `page.exposeFunction` |
37
52
 
38
- One import. No middleware soup. No 47 plugins to avoid detection. No CAPTCHAs on sites that Puppeteer chokes on. Just write your scraper and go.
53
+ One import. No 47 plugins to avoid detection. Just write your scraper and go.
39
54
 
40
55
  ---
41
56
 
42
57
  ## Requirements
43
58
 
44
- - [Bun](https://bun.sh) ≥ 1.0
45
- - A Nothing Browser binary placed in your **project root** (see below)
59
+ - **[Bun](https://bun.sh) ≥ 1.0**
60
+ - A **Nothing Browser binary** placed in your **project root** (see [Binaries](#binaries))
46
61
 
47
62
  ---
48
63
 
49
64
  ## Binaries
50
65
 
51
- There are three binaries. All downloaded from the same place — [GitHub Releases](https://github.com/BunElysiaReact/nothing-browser/releases).
66
+ Download the correct binary from **[GitHub Releases](https://github.com/BunElysiaReact/nothing-browser/releases)**.
52
67
 
53
68
  | Binary | What it is | Where it goes |
54
69
  |--------|-----------|---------------|
55
- | `nothing-browser` | Full UI browser app DevTools, YouTube tab, Plugins, etc. | Install system-wide |
56
- | `nothing-browser-headless` | No window, no GPU. Runs as a background daemon for the scraping lib. | **Your project root** |
57
- | `nothing-browser-headful` | Visible browser window, script-controlled. Useful when a site needs a real display. | **Your project root** |
70
+ | `nothing-browser` | Full UI browser app (DevTools, YouTube, Plugins) | Install system-wide |
71
+ | `nothing-browser-headless` | No window, no GPU for automated scraping | **Your project root** |
72
+ | `nothing-browser-headful` | Visible window, script-controlled for debugging | **Your project root** |
58
73
 
59
- The lib talks to whichever binary is in your project root over a local socket. You pick headless or headful depending on your use case.
74
+ The library communicates with the binary in your project root over a local socket.
60
75
 
61
76
  ---
62
77
 
@@ -66,944 +81,151 @@ The lib talks to whichever binary is in your project root over a local socket. Y
66
81
  bun add nothing-browser
67
82
  ```
68
83
 
69
- Then download the binary for your platform from [GitHub Releases](https://github.com/BunElysiaReact/nothing-browser/releases) and place it in your project root.
84
+ Then download the binary and place it in your project root.
70
85
 
71
- ### Linux
86
+ <details>
87
+ <summary><strong>Linux</strong></summary>
72
88
 
73
- **Headless** (no visible window — most common for scraping)
74
89
  ```bash
90
+ # Headless (most common for scraping)
75
91
  tar -xzf nothing-browser-headless-*-linux-x86_64.tar.gz
76
92
  chmod +x nothing-browser-headless
77
- ```
78
93
 
79
- **Headful** (visible window, script-controlled)
80
- ```bash
94
+ # Headful (visible window)
81
95
  tar -xzf nothing-browser-headful-*-linux-x86_64.tar.gz
82
96
  chmod +x nothing-browser-headful
83
- ```
84
97
 
85
- **Full browser** (system-wide install, for using the UI)
86
- ```bash
98
+ # Full browser (system-wide)
87
99
  sudo dpkg -i nothing-browser_*_amd64.deb
88
100
  ```
101
+ </details>
89
102
 
90
- ### Windows
91
-
92
- Download the `.zip` → extract → place the exe in your project root.
93
-
94
- ### macOS
95
-
96
- Download the `.tar.gz` → extract → place the binary in your project root.
97
-
98
- ---
99
-
100
- ## Quick Start
101
-
102
- ```ts
103
- import piggy from "nothing-browser";
104
-
105
- await piggy.launch({ mode: "tab" });
106
- await piggy.register("books", "https://books.toscrape.com");
103
+ <details>
104
+ <summary><strong>Windows</strong></summary>
107
105
 
108
- await piggy.books.navigate();
109
- await piggy.books.waitForSelector(".product_pod");
106
+ Download the `.zip` → extract → place `.exe` in your project root.
107
+ </details>
110
108
 
111
- const books = await piggy.books.evaluate(() =>
112
- Array.from(document.querySelectorAll(".product_pod")).map(el => ({
113
- title: el.querySelector("h3 a")?.getAttribute("title") ?? "",
114
- price: el.querySelector(".price_color")?.textContent?.trim() ?? "",
115
- }))
116
- );
117
-
118
- console.log(books);
119
- await piggy.close();
120
- ```
109
+ <details>
110
+ <summary><strong>macOS</strong></summary>
121
111
 
122
- That's it. One import, one register, scrape, done.
112
+ Download the `.tar.gz` extract place binary in your project root.
113
+ </details>
123
114
 
124
115
  ---
125
116
 
126
117
  ## Headless vs Headful
127
118
 
128
- **Headless** — no display needed, runs anywhere including CI.
129
-
130
119
  ```ts
131
- await piggy.launch({ mode: "tab", binary: "headless" }); // default
132
- ```
120
+ // Headless no display, runs anywhere (default)
121
+ await piggy.launch({ mode: "tab", binary: "headless" });
133
122
 
134
- **Headful** opens a real visible Chromium window your script drives. Use this when a site detects headless or requires a real display.
135
-
136
- ```ts
123
+ // Headful visible window for debugging
137
124
  await piggy.launch({ mode: "tab", binary: "headful" });
138
125
  ```
139
126
 
140
- Same API either way. Switching is just changing one word.
127
+ Switching is just changing one word.
141
128
 
142
129
  ---
143
130
 
144
- ## Examples
131
+ ## Key Features (with Examples)
145
132
 
146
- ### 🔥 NEW: Browser → Node.js RPC with `exposeFunction`
133
+ ### 🔥 Browser → Node.js RPC (`exposeFunction`)
147
134
 
148
- Call Node.js functions directly from browser JavaScript. Perfect for:
149
- - Processing data in real-time as the user navigates
150
- - Handling authentication callbacks
151
- - Streaming WebSocket messages to your backend
152
- - Building browser extensions with Node.js power
135
+ Call Node.js functions directly from browser JavaScript.
153
136
 
154
137
  ```ts
155
- import piggy from "nothing-browser";
156
-
157
- await piggy.launch({ mode: "tab" });
158
- await piggy.register("whatsapp", "https://web.whatsapp.com");
159
-
160
- // Expose a function that WhatsApp Web can call
161
138
  await piggy.whatsapp.exposeFunction("onNewMessage", async (message) => {
162
- console.log("📱 New message:", message);
163
-
164
- // Save to database
165
- await db.messages.insert({
166
- text: message.text,
167
- sender: message.sender,
168
- timestamp: message.timestamp,
169
- });
170
-
171
- // Return value goes back to the browser
139
+ await db.messages.insert(message);
172
140
  return { saved: true, id: crypto.randomUUID() };
173
141
  });
174
-
175
- // Inject the listener that calls our exposed function
176
- await piggy.whatsapp.evaluate(() => {
177
- const observer = new MutationObserver(() => {
178
- document.querySelectorAll('.message-in:not([data-seen])').forEach(el => {
179
- el.dataset.seen = '1';
180
-
181
- // Call the exposed function - returns a Promise!
182
- window.onNewMessage({
183
- text: el.innerText,
184
- timestamp: Date.now(),
185
- sender: el.querySelector('.sender')?.innerText,
186
- }).then(result => {
187
- console.log('Message saved with ID:', result.id);
188
- el.style.borderLeft = '3px solid green';
189
- });
190
- });
191
- });
192
-
193
- observer.observe(document.body, { childList: true, subtree: true });
194
- });
195
-
196
- console.log("Listening for WhatsApp messages...");
197
- ```
198
- You're absolutely right. Let me add those two critical features to the README documentation. Here's the updated section to add:
199
-
200
- ## Add this new section after the "Expose Function" section:
201
-
202
- ```markdown
203
- ---
204
-
205
- ### Request Interception with Custom Response
206
-
207
- Block, redirect, or **serve custom responses** to network requests. Perfect for:
208
- - Caching API responses locally
209
- - Mocking endpoints during development
210
- - Serving a local web version cache
211
- - Modifying response bodies on the fly
212
-
213
- ```ts
214
- import piggy from "nothing-browser";
215
-
216
- await piggy.launch({ mode: "tab" });
217
- await piggy.register("app", "https://your-app.com");
218
-
219
- // Serve custom response for specific requests
220
- await piggy.app.intercept.respond(
221
- "*/api/users*",
222
- async (request) => {
223
- // Return custom response
224
- return {
225
- status: 200,
226
- contentType: "application/json",
227
- headers: { "X-Cache": "HIT" },
228
- body: JSON.stringify({
229
- users: [
230
- { id: 1, name: "Cached User 1" },
231
- { id: 2, name: "Cached User 2" },
232
- ]
233
- })
234
- };
235
- }
236
- );
237
-
238
- // Serve static file from disk
239
- await piggy.app.intercept.respond(
240
- "*/assets/bundle.js",
241
- async () => {
242
- const cached = await Bun.file("./cache/bundle.js").text();
243
- return {
244
- status: 200,
245
- contentType: "application/javascript",
246
- body: cached,
247
- headers: { "X-Served-From": "local-cache" }
248
- };
249
- }
250
- );
251
-
252
- // Dynamic response based on request
253
- await piggy.app.intercept.respond(
254
- "*/api/product/*",
255
- async (request) => {
256
- const productId = request.url.match(/\/product\/(\d+)/)?.[1];
257
-
258
- // Check local cache
259
- const cached = await db.products.find(productId);
260
- if (cached) {
261
- return {
262
- status: 200,
263
- contentType: "application/json",
264
- body: JSON.stringify(cached),
265
- headers: { "X-Cache": "HIT" }
266
- };
267
- }
268
-
269
- // Let the request through to the server
270
- return null;
271
- }
272
- );
273
-
274
- // Modify response on the fly
275
- await piggy.app.intercept.modifyResponse(
276
- "*/api/feed*",
277
- async (response) => {
278
- const data = await response.json();
279
-
280
- // Add custom field to every item
281
- data.items = data.items.map(item => ({
282
- ...item,
283
- _cached_at: Date.now(),
284
- _source: 'modified-by-interceptor'
285
- }));
286
-
287
- return {
288
- body: JSON.stringify(data),
289
- headers: { "X-Modified": "true" }
290
- };
291
- }
292
- );
293
-
294
- await piggy.app.navigate();
295
- ```
296
-
297
- ### Response Interceptor API
298
-
299
- ```ts
300
- // Full response replacement
301
- site.intercept.respond(pattern, handler)
302
- // handler: (request: {
303
- // url: string,
304
- // method: string,
305
- // headers: Record<string, string>,
306
- // body?: string
307
- // }) => Promise<{
308
- // status?: number, // default: 200
309
- // contentType?: string, // default: auto-detect
310
- // headers?: Record<string, string>,
311
- // body: string | Buffer
312
- // } | null> // return null to pass through
313
-
314
- // Modify existing response
315
- site.intercept.modifyResponse(pattern, handler)
316
- // handler: (response: {
317
- // status: number,
318
- // headers: Record<string, string>,
319
- // body: string,
320
- // json: () => Promise<any>
321
- // }) => Promise<{
322
- // status?: number,
323
- // headers?: Record<string, string>,
324
- // body?: string
325
- // }>
326
-
327
- // Block requests (existing)
328
- site.intercept.block(pattern)
329
-
330
- // Redirect requests (existing)
331
- site.intercept.redirect(pattern, redirectUrl)
332
-
333
- // Add/modify request headers (existing)
334
- site.intercept.headers(pattern, headers)
335
-
336
- // Clear all rules for this site
337
- site.intercept.clear()
338
- ```
339
-
340
- ### Web Version Cache Example
341
-
342
- ```ts
343
- import piggy from "nothing-browser";
344
-
345
- // Build a complete offline cache of your web app
346
- const cache = new Map();
347
-
348
- await piggy.launch({ mode: "tab" });
349
- await piggy.register("spa", "https://your-spa.com");
350
-
351
- // Cache all static assets
352
- await piggy.spa.intercept.respond("*.js", async (req) => {
353
- const key = req.url;
354
- if (!cache.has(key)) {
355
- const response = await fetch(req.url);
356
- cache.set(key, await response.text());
357
- console.log(`Cached: ${key}`);
358
- }
359
- return {
360
- status: 200,
361
- contentType: "application/javascript",
362
- body: cache.get(key),
363
- headers: { "X-Cache": "HIT" }
364
- };
365
- });
366
-
367
- await piggy.spa.intercept.respond("*.css", async (req) => {
368
- const key = req.url;
369
- if (!cache.has(key)) {
370
- const response = await fetch(req.url);
371
- cache.set(key, await response.text());
372
- }
373
- return {
374
- status: 200,
375
- contentType: "text/css",
376
- body: cache.get(key)
377
- };
378
- });
379
-
380
- // Cache API responses with TTL
381
- const apiCache = new Map();
382
- await piggy.spa.intercept.respond("*/api/*", async (req) => {
383
- const key = `${req.method}:${req.url}`;
384
- const cached = apiCache.get(key);
385
-
386
- if (cached && Date.now() < cached.expires) {
387
- return {
388
- status: 200,
389
- contentType: "application/json",
390
- body: cached.data,
391
- headers: { "X-Cache": "HIT", "X-Cache-Age": String(Date.now() - cached.timestamp) }
392
- };
393
- }
394
-
395
- // Pass through - will be cached by modifyResponse
396
- return null;
397
- });
398
-
399
- await piggy.spa.intercept.modifyResponse("*/api/*", async (res) => {
400
- const key = `${res.url}`;
401
- const data = await res.json();
402
-
403
- apiCache.set(key, {
404
- data: JSON.stringify(data),
405
- timestamp: Date.now(),
406
- expires: Date.now() + 5 * 60 * 1000 // 5 minutes
407
- });
408
-
409
- return {
410
- body: JSON.stringify(data),
411
- headers: { ...res.headers, "X-Cache": "MISS" }
412
- };
413
- });
414
-
415
- await piggy.spa.navigate();
416
- // App now runs mostly from local cache!
417
142
  ```
418
143
 
419
- ---
420
-
421
- ### Evaluate on New Document (Script Injection)
144
+ ### 📡 Request Interception
422
145
 
423
- Inject JavaScript before any page JavaScript runs. Equivalent to Puppeteer's `page.evaluateOnNewDocument()`. Perfect for:
424
- - Overriding browser APIs before they're accessed
425
- - Setting up global state before page loads
426
- - Disabling features like WebRTC, Canvas, etc.
427
- - Installing persistent event listeners
146
+ Block, redirect, or serve custom responses.
428
147
 
429
148
  ```ts
430
- import piggy from "nothing-browser";
431
-
432
- await piggy.launch({ mode: "tab" });
433
- await piggy.register("site", "https://example.com");
434
-
435
- // Inject before ANY page script runs
436
- await piggy.site.addInitScript(`
437
- // Override navigator properties
438
- Object.defineProperty(navigator, 'webdriver', {
439
- get: () => undefined
440
- });
441
-
442
- // Disable WebRTC
443
- Object.defineProperty(navigator, 'mediaDevices', {
444
- get: () => undefined
445
- });
446
-
447
- // Mock geolocation
448
- navigator.geolocation.getCurrentPosition = (success) => {
449
- success({
450
- coords: {
451
- latitude: 40.7128,
452
- longitude: -74.0060,
453
- accuracy: 10
454
- },
455
- timestamp: Date.now()
456
- });
457
- };
458
-
459
- // Set up global state
460
- window.__MY_APP_CONFIG__ = {
461
- apiUrl: 'https://my-api.com',
462
- debug: true,
463
- version: '1.0.0'
464
- };
465
-
466
- console.log('[InitScript] Injected before page load');
467
- `);
468
-
469
- // Add multiple init scripts
470
- await piggy.site.addInitScript(`
471
- // Second script - runs in order
472
- window.__FEATURE_FLAGS__ = {
473
- newUI: true,
474
- beta: false
475
- };
476
- `);
477
-
478
- // Add init script from a function
479
- await piggy.site.addInitScript(() => {
480
- // This function will be stringified and injected
481
- const originalFetch = window.fetch;
482
- window.fetch = function(...args) {
483
- console.log('[Fetch]', args[0]);
484
- return originalFetch.apply(this, args);
485
- };
486
-
487
- // Disable battery API
488
- if (navigator.getBattery) {
489
- navigator.getBattery = undefined;
490
- }
491
- });
492
-
493
- // Add init script that runs in all frames
494
- await piggy.site.addInitScript(`
495
- // This runs in iframes too
496
- if (window.self !== window.top) {
497
- console.log('[InitScript] Running in iframe');
498
- }
499
- `, { runInAllFrames: true });
500
-
501
- // Remove a specific init script
502
- const scriptId = await piggy.site.addInitScript(`...`);
503
- await piggy.site.removeInitScript(scriptId);
504
-
505
- // Clear all init scripts
506
- await piggy.site.clearInitScripts();
507
-
508
- // Now navigate - scripts will run BEFORE page loads
509
- await piggy.site.navigate();
149
+ await piggy.app.intercept.respond("*/api/users*", async () => ({
150
+ status: 200,
151
+ body: JSON.stringify([{ id: 1, name: "Cached User" }])
152
+ }));
510
153
  ```
511
154
 
512
- ### Init Script API
513
-
514
- ```ts
515
- // Add script that runs before every page load
516
- site.addInitScript(script, options?)
517
- // script: string | (() => void)
518
- // options: {
519
- // runInAllFrames?: boolean, // default: false
520
- // world?: "main" | "isolated", // default: "main"
521
- // name?: string // optional identifier
522
- // }
523
- // Returns: string (script ID)
524
-
525
- // Remove specific init script
526
- site.removeInitScript(scriptId)
527
-
528
- // Remove all init scripts
529
- site.clearInitScripts()
530
-
531
- // Get all registered init scripts
532
- site.getInitScripts()
533
- // Returns: Array<{ id: string, name?: string, runInAllFrames: boolean }>
534
- ```
535
-
536
- ### Advanced: Persistent Init Scripts Across Navigations
537
-
538
- ```ts
539
- // Scripts survive navigation automatically!
540
- await piggy.site.addInitScript(`
541
- window.__SESSION_ID__ = '${crypto.randomUUID()}';
542
- window.__START_TIME__ = Date.now();
543
- `);
544
-
545
- await piggy.site.navigate("https://example.com/page1");
546
- // Script runs here ✓
547
-
548
- await piggy.site.click("a[href='/page2']");
549
- await piggy.site.waitForNavigation();
550
- // Script runs again automatically ✓
551
-
552
- // Check that it persisted
553
- const sessionId = await piggy.site.evaluate(() => window.__SESSION_ID__);
554
- console.log(sessionId); // Same UUID across pages!
555
- ```
155
+ ### 🧠 Human Mode
556
156
 
557
- ### Complete Anti-Detection Setup
157
+ Add random delays, typos, and natural scrolling.
558
158
 
559
159
  ```ts
560
- import piggy from "nothing-browser";
561
-
562
- await piggy.launch({ mode: "tab", binary: "headful" });
563
- await piggy.register("stealth", "https://example.com");
564
-
565
- // Built-in fingerprint spoofing is already enabled
566
- // Add additional init scripts for maximum stealth
567
-
568
- await piggy.stealth.addInitScript(`
569
- // Remove automation traces
570
- delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
571
- delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
572
- delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
573
-
574
- // Override permissions
575
- const originalQuery = window.navigator.permissions.query;
576
- window.navigator.permissions.query = (parameters) => (
577
- parameters.name === 'notifications' ||
578
- parameters.name === 'geolocation' ||
579
- parameters.name === 'camera' ||
580
- parameters.name === 'microphone'
581
- ) ? Promise.resolve({ state: 'prompt', onchange: null })
582
- : originalQuery(parameters);
583
-
584
- // Fake plugins
585
- Object.defineProperty(navigator, 'plugins', {
586
- get: () => [
587
- { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' },
588
- { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
589
- { name: 'Native Client', filename: 'internal-nacl-plugin' }
590
- ]
591
- });
592
-
593
- // Fake languages
594
- Object.defineProperty(navigator, 'languages', {
595
- get: () => ['en-US', 'en']
596
- });
597
-
598
- // WebGL vendor spoof
599
- const getParameter = WebGLRenderingContext.prototype.getParameter;
600
- WebGLRenderingContext.prototype.getParameter = function(parameter) {
601
- if (parameter === 37445) return 'Intel Inc.';
602
- if (parameter === 37446) return 'Intel Iris OpenGL Engine';
603
- return getParameter.call(this, parameter);
604
- };
605
- `);
606
-
607
- // Block tracking domains
608
- await piggy.stealth.intercept.block("*google-analytics.com*");
609
- await piggy.stealth.intercept.block("*doubleclick.net*");
610
- await piggy.stealth.intercept.block("*facebook.com/tr*");
611
-
612
- // Add human-like behavior
613
160
  piggy.actHuman(true);
614
-
615
- await piggy.stealth.navigate();
616
- // You're now virtually undetectable
617
- ```
618
-
619
- ---
620
-
621
- ## Updated API Reference Section
622
- ```ts
623
- // Block requests (existing)
624
- site.intercept.block(pattern)
625
-
626
- // Redirect requests (existing)
627
- site.intercept.redirect(pattern, redirectUrl)
628
-
629
- // Add/modify request headers (existing)
630
- site.intercept.headers(pattern, headers)
631
-
632
- // 🔥 NEW: Serve custom response
633
- site.intercept.respond(pattern, handler)
634
- // handler receives request details, returns response or null
635
-
636
- // 🔥 NEW: Modify response on the fly
637
- site.intercept.modifyResponse(pattern, handler)
638
- // handler receives response, returns modifications
639
-
640
- // Clear all rules
641
- site.intercept.clear()
161
+ await piggy.books.click(".product_pod h3 a");
642
162
  ```
643
163
 
644
- #### Script Injection
645
- ```ts
646
- // 🔥 NEW: Inject before page loads (evaluateOnNewDocument)
647
- site.addInitScript(script, options?)
648
- // script: string | function
649
- // options: { runInAllFrames?: boolean, world?: "main" | "isolated", name?: string }
650
- // Returns: string (script ID)
651
-
652
- // 🔥 NEW: Remove specific init script
653
- site.removeInitScript(scriptId)
654
-
655
- // 🔥 NEW: Remove all init scripts
656
- site.clearInitScripts()
657
-
658
- // 🔥 NEW: List all init scripts
659
- site.getInitScripts()
164
+ ### 💾 Session Persistence
660
165
 
166
+ Save and restore cookies, storage, and state.
661
167
 
662
168
  ```ts
663
- await piggy.whatsapp.exposeAndInject(
664
- "onNewMessage",
665
- async (message) => {
666
- await saveToDatabase(message);
667
- return { ok: true };
668
- },
669
- (fnName) => `
670
- // This runs in the browser
671
- setInterval(() => {
672
- const msgs = document.querySelectorAll('.new-message');
673
- msgs.forEach(msg => {
674
- window.${fnName}({ text: msg.innerText });
675
- });
676
- }, 2000);
677
- `
678
- );
169
+ await piggy.site.session.export(); // save
170
+ await piggy.site.session.import(data); // restore
679
171
  ```
680
172
 
681
- ### Structured API with multiple methods
682
-
683
- ```ts
684
- import { createExposedAPI } from "nothing-browser/register";
685
-
686
- await createExposedAPI(piggy.whatsapp, "whatsappAPI", {
687
- onMessage: async (msg) => {
688
- await db.messages.insert(msg);
689
- return { saved: true };
690
- },
691
-
692
- getContacts: async () => {
693
- return await db.contacts.findAll();
694
- },
695
-
696
- sendReply: async ({ to, text }) => {
697
- // Your sending logic here
698
- return { sent: true };
699
- }
700
- });
701
-
702
- // In browser JS:
703
- const result = await window.whatsappAPI({
704
- method: 'sendReply',
705
- args: { to: '+1234567890', text: 'Hello!' }
706
- });
707
- ```
708
-
709
- ### Global expose (available to all sites)
710
-
711
- ```ts
712
- await piggy.expose("logToServer", async (data) => {
713
- console.log("[Browser]", data);
714
- await analytics.track(data.event, data.properties);
715
- return { logged: true };
716
- });
717
-
718
- // Any page can call: window.logToServer({ event: 'pageview' })
719
- ```
173
+ ### 🚀 Built‑in API Server
720
174
 
721
- ### Scrape a site and expose it as an API
175
+ Turn your scraper into a REST API.
722
176
 
723
177
  ```ts
724
- import piggy from "nothing-browser";
725
-
726
- await piggy.launch({ mode: "tab" });
727
- await piggy.register("books", "https://books.toscrape.com");
728
-
729
- await piggy.books.intercept.block("*google-analytics*");
730
- await piggy.books.intercept.block("*doubleclick*");
731
-
732
- piggy.books.api("/list", async (_params, query) => {
733
- const page = query.page ? parseInt(query.page) : 1;
734
- const url = page === 1
735
- ? "https://books.toscrape.com"
736
- : `https://books.toscrape.com/catalogue/page-${page}.html`;
737
-
738
- await piggy.books.navigate(url);
739
- await piggy.books.waitForSelector(".product_pod", 10000);
740
-
741
- const books = await piggy.books.evaluate(() => {
742
- const ratingMap: Record<string, number> = {
743
- One: 1, Two: 2, Three: 3, Four: 4, Five: 5,
744
- };
745
- return Array.from(document.querySelectorAll(".product_pod")).map(el => ({
746
- title: el.querySelector("h3 a")?.getAttribute("title") ?? "",
747
- price: el.querySelector(".price_color")?.textContent?.trim() ?? "",
748
- rating: ratingMap[el.querySelector(".star-rating")?.className.replace("star-rating","").trim() ?? ""] ?? 0,
749
- available: el.querySelector(".availability")?.textContent?.trim() ?? "",
750
- }));
751
- });
752
-
753
- return { page, count: books.length, books };
754
- }, { ttl: 300_000 });
755
-
756
- piggy.books.noclose();
178
+ piggy.books.api("/list", async () => ({ books }));
757
179
  await piggy.serve(3000);
758
180
  // GET http://localhost:3000/books/list
759
- // GET http://localhost:3000/books/list?page=2
760
- ```
761
-
762
- ---
763
-
764
- ### Middleware — auth + logging
765
-
766
- ```ts
767
- const authMiddleware = async ({ headers, set }: any) => {
768
- if (headers["x-api-key"] !== "secret") {
769
- set.status = 401;
770
- throw new Error("Unauthorized");
771
- }
772
- };
773
-
774
- piggy.books.api("/search", async (_params, query) => {
775
- // handler
776
- }, { ttl: 120_000, before: [authMiddleware] });
777
- ```
778
-
779
- ---
780
-
781
- ### Network capture
782
-
783
- ```ts
784
- await piggy.books.capture.clear();
785
- await piggy.books.capture.start();
786
- await piggy.books.wait(300);
787
-
788
- await piggy.books.navigate("https://books.toscrape.com");
789
- await piggy.books.waitForSelector("body", 10000);
790
- await piggy.books.wait(2000);
791
-
792
- await piggy.books.capture.stop();
793
-
794
- const requests = await piggy.books.capture.requests();
795
- const ws = await piggy.books.capture.ws();
796
- const cookies = await piggy.books.capture.cookies();
797
- const storage = await piggy.books.capture.storage();
798
-
799
- console.log(`${requests.length} requests, ${ws.length} WS frames`);
800
181
  ```
801
182
 
802
- ---
803
-
804
- ### Session persistence
805
-
806
- ```ts
807
- import { existsSync, readFileSync, writeFileSync } from "fs";
808
-
809
- const SESSION_FILE = "./session.json";
810
-
811
- if (existsSync(SESSION_FILE)) {
812
- const saved = JSON.parse(readFileSync(SESSION_FILE, "utf8"));
813
- await piggy.books.session.import(saved);
814
- }
815
-
816
- process.on("SIGINT", async () => {
817
- const session = await piggy.books.session.export();
818
- writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2));
819
- await piggy.close({ force: true });
820
- process.exit(0);
821
- });
822
- ```
183
+ > **For many more examples** (WebSocket capture, multi‑site scraping, PDF/screenshot, middleware, etc.), see the **[full documentation](https://nothing-browser-docs.pages.dev/guide/piggy/quickstart)**.
823
184
 
824
185
  ---
825
186
 
826
- ### Human mode
187
+ ## API Reference (Quick)
827
188
 
828
- ```ts
829
- piggy.actHuman(true);
189
+ ### Core
830
190
 
831
- await piggy.books.click(".product_pod h3 a");
832
- await piggy.books.type("#search", "mystery novels");
833
- await piggy.books.scroll.by(400);
834
- ```
191
+ | Method | Description |
192
+ |--------|-------------|
193
+ | `piggy.launch(opts?)` | Start browser (`mode`, `binary`) |
194
+ | `piggy.register(name, url)` | Register a site → `piggy.<name>` |
195
+ | `piggy.actHuman(enable)` | Enable human‑like timing |
196
+ | `piggy.expose(name, handler)` | Global RPC function |
197
+ | `piggy.serve(port)` | Start API server |
198
+ | `piggy.close(opts?)` | Close gracefully or force |
835
199
 
836
- Affects `click`, `type`, `hover`, `scroll.by`, `wait` — random delays, simulated typos, self-correction.
200
+ ### Site Methods
837
201
 
838
- ---
202
+ | Category | Methods |
203
+ |----------|---------|
204
+ | **Navigation** | `navigate()`, `reload()`, `goBack()`, `goForward()`, `waitForSelector()` |
205
+ | **Interactions** | `click()`, `type()`, `hover()`, `select()`, `keyboard.press()`, `scroll.to()` |
206
+ | **Data** | `evaluate()`, `fetchText()`, `fetchLinks()`, `fetchImages()` |
207
+ | **RPC** | `exposeFunction()`, `unexposeFunction()`, `exposeAndInject()` |
208
+ | **Network** | `capture.start()`, `intercept.respond()`, `intercept.modifyResponse()`, `blockImages()` |
209
+ | **Session** | `cookies.set()`, `session.export()`, `session.import()` |
210
+ | **Output** | `screenshot()`, `pdf()` |
839
211
 
840
- ### Screenshot / PDF
841
-
842
- ```ts
843
- await piggy.books.screenshot("./out/page.png");
844
- await piggy.books.pdf("./out/page.pdf");
845
-
846
- const b64 = await piggy.books.screenshot(); // base64
847
- ```
848
-
849
- ---
850
-
851
- ### Multi-site parallel scraping
852
-
853
- ```ts
854
- await piggy.register("site1", "https://example.com");
855
- await piggy.register("site2", "https://example.org");
856
-
857
- const titles = await piggy.all([piggy.site1, piggy.site2]).title();
858
- const h1s = await piggy.diff([piggy.site1, piggy.site2]).fetchText("h1");
859
- // → { site1: "...", site2: "..." }
860
- ```
861
-
862
- ---
863
-
864
- ## API Reference
865
-
866
- ### `piggy.launch(opts?)`
867
-
868
- | Option | Type | Default |
869
- |--------|------|---------|
870
- | `mode` | `"tab" \| "process"` | `"tab"` |
871
- | `binary` | `"headless" \| "headful"` | `"headless"` |
872
-
873
- ### `piggy.register(name, url)`
874
- Registers a site. Accessible as `piggy.<name>` after registration.
875
-
876
- ### `piggy.actHuman(enable)`
877
- Toggles human-like interaction timing globally.
878
-
879
- ### `piggy.expose(name, handler, tabId?)`
880
- Exposes a function globally (available to all tabs). The handler receives data from the browser and can return a value that resolves the Promise in the browser.
881
-
882
- ### `piggy.unexpose(name, tabId?)`
883
- Removes a globally exposed function.
884
-
885
- ### `piggy.serve(port, opts?)`
886
- Starts the Elysia HTTP server. Built-in routes: `GET /health`, `GET /cache/keys`, `DELETE /cache`.
887
-
888
- ### `piggy.routes()`
889
- Returns all registered API routes with method, path, TTL, and middleware count.
890
-
891
- ### `piggy.close(opts?)`
892
-
893
- ```ts
894
- await piggy.close(); // graceful
895
- await piggy.close({ force: true }); // kills everything immediately
896
- ```
897
-
898
- ### Site methods
899
-
900
- #### Navigation
901
- ```ts
902
- site.navigate(url?)
903
- site.reload() / site.goBack() / site.goForward()
904
- site.waitForNavigation()
905
- site.waitForSelector(selector, timeout?)
906
- site.waitForResponse(urlPattern, timeout?)
907
- site.title() / site.url() / site.content()
908
- site.wait(ms)
909
- ```
910
-
911
- #### Interactions
912
- ```ts
913
- site.click(selector, opts?)
914
- site.doubleClick(selector) / site.hover(selector)
915
- site.type(selector, text, opts?)
916
- site.select(selector, value)
917
- site.keyboard.press(key)
918
- site.keyboard.combo(combo)
919
- site.mouse.move(x, y)
920
- site.mouse.drag(from, to)
921
- site.scroll.to(selector) / site.scroll.by(px)
922
- ```
923
-
924
- #### Data
925
- ```ts
926
- site.fetchText(selector)
927
- site.fetchLinks(selector)
928
- site.fetchImages(selector)
929
- site.search.css(query) / site.search.id(query)
930
- site.evaluate(js | fn, ...args)
931
- ```
932
-
933
- #### 🔥 Expose Function (RPC)
934
- ```ts
935
- // Expose a function that browser JS can call
936
- site.exposeFunction(name, handler)
937
- // handler: (data: any) => Promise<any> | any
938
-
939
- // Remove an exposed function
940
- site.unexposeFunction(name)
941
-
942
- // Remove all exposed functions for this site
943
- site.clearExposedFunctions()
944
-
945
- // Expose and inject in one call
946
- site.exposeAndInject(name, handler, injectionJs)
947
- // injectionJs: string | ((fnName: string) => string)
948
- ```
949
-
950
- #### Network
951
- ```ts
952
- site.capture.start() / .stop() / .clear()
953
- site.capture.requests() / .ws() / .cookies() / .storage()
954
- site.intercept.block(pattern)
955
- site.intercept.redirect(pattern, redirectUrl)
956
- site.intercept.headers(pattern, headers)
957
- site.intercept.clear()
958
- site.blockImages() / site.unblockImages()
959
- ```
960
-
961
- #### Cookies & Session
962
- ```ts
963
- site.cookies.set(name, value, domain, path?)
964
- site.cookies.get(name) / .delete(name) / .list()
965
- site.session.export() / site.session.import(data)
966
- ```
967
-
968
- #### API
969
- ```ts
970
- site.api(path, handler, opts?)
971
- // opts: { ttl?, method?, before?: middleware[] }
972
-
973
- site.noclose()
974
- site.screenshot(filePath?) / site.pdf(filePath?)
975
- ```
976
-
977
- ### Helper Functions
978
-
979
- ```ts
980
- import { createExposedAPI } from "nothing-browser/register";
981
-
982
- // Create a structured API with multiple methods
983
- await createExposedAPI(site, apiName, {
984
- method1: async (args) => { /* ... */ },
985
- method2: async (args) => { /* ... */ },
986
- });
987
-
988
- // Browser calls: window[apiName]({ method: 'method1', args: {...} })
989
- ```
212
+ > **Full API reference:** [https://nothing-browser-docs.pages.dev/guide/piggy/api-reference](https://nothing-browser-docs.pages.dev/guide/piggy/api-reference)
990
213
 
991
214
  ---
992
215
 
993
216
  ## How `exposeFunction` Works
994
217
 
995
- 1. **Browser injects stub**: `window.fnName` becomes a Promise-returning function
996
- 2. **Browser queues calls**: Arguments are pushed to `__NOTHING_QUEUE__`
997
- 3. **C++ picks up queue**: 250ms poll timer reads the queue
998
- 4. **Signal to Node.js**: Server broadcasts event to all connected clients
999
- 5. **Your handler runs**: TypeScript handler processes the data
1000
- 6. **Result returns**: Promise in browser resolves with your return value
218
+ 1. Browser injects a Promise‑returning stub into `window.fnName`.
219
+ 2. Calls are queued to `__NOTHING_QUEUE__`.
220
+ 3. C++ polls the queue (every 250ms) and sends the call via socket.
221
+ 4. Your Node.js handler runs.
222
+ 5. The result is sent back and the browser’s Promise resolves.
1001
223
 
1002
- The function survives page navigations (injected at `DocumentCreation`) and works with both tab and process modes.
224
+ The function survives page navigations (injected at `DocumentCreation`) and works in both tab and process modes.
1003
225
 
1004
226
  ---
1005
227
 
1006
- ## Binary download
228
+ ## Binary Download Links
1007
229
 
1008
230
  | Platform | Headless | Headful | Full Browser |
1009
231
  |----------|----------|---------|--------------|
@@ -1012,10 +234,21 @@ The function survives page navigations (injected at `DocumentCreation`) and work
1012
234
  | Windows x64 | `nothing-browser-headless-*-windows-x64.zip` | `nothing-browser-headful-*-windows-x64.zip` | `nothing-browser-*-windows-x64.zip` |
1013
235
  | macOS | `nothing-browser-headless-*-macos.tar.gz` | `nothing-browser-headful-*-macos.tar.gz` | `nothing-browser-*-macos.dmg` |
1014
236
 
1015
- [All releases](https://github.com/BunElysiaReact/nothing-browser/releases)
237
+ ➡️ **[All releases on GitHub](https://github.com/BunElysiaReact/nothing-browser/releases)**
238
+
239
+ ---
240
+
241
+ ## Contributing & Security
242
+
243
+ - **Contributing:** See the [Contributing Guide](https://nothing-browser-docs.pages.dev/guide/community/contributing)
244
+ - **Security issues:** Email `ernesttechhouse@gmail.com` (not a public issue)
1016
245
 
1017
246
  ---
1018
247
 
1019
248
  ## License
1020
249
 
1021
250
  MIT © [Ernest Tech House](https://github.com/BunElysiaReact/nothing-browser)
251
+
252
+ ---
253
+
254
+ *Part of the [Nothing Ecosystem](https://nothing-browser-docs.pages.dev). Built in Kenya 🇰🇪*