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/LICENSE +21 -0
- package/README.md +120 -887
- package/dist/piggy/cache/memory.d.ts +6 -0
- package/dist/piggy/client/index.d.ts +78 -0
- package/dist/piggy/human/index.d.ts +6 -0
- package/dist/piggy/intercept/scripts.d.ts +12 -0
- package/dist/piggy/launch/detect.d.ts +2 -0
- package/dist/piggy/launch/spawn.d.ts +5 -0
- package/dist/piggy/logger/index.d.ts +2 -0
- package/dist/piggy/open/index.d.ts +3 -0
- package/dist/piggy/register/index.d.ts +6 -0
- package/dist/piggy/server/index.d.ts +20 -0
- package/dist/piggy.d.ts +3 -0
- package/dist/register/index.js +1 -1
- package/package.json +16 -12
- package/piggy/cache/memory.d.ts +7 -0
- package/piggy/cache/memory.d.ts.map +1 -0
- package/piggy/client/index.d.ts +79 -0
- package/piggy/client/index.d.ts.map +1 -0
- package/piggy/human/index.d.ts +7 -0
- package/piggy/human/index.d.ts.map +1 -0
- package/piggy/intercept/scripts.d.ts +13 -0
- package/piggy/intercept/scripts.d.ts.map +1 -0
- package/piggy/launch/detect.d.ts +3 -0
- package/piggy/launch/detect.d.ts.map +1 -0
- package/piggy/launch/spawn.d.ts +6 -0
- package/piggy/launch/spawn.d.ts.map +1 -0
- package/piggy/logger/index.d.ts +3 -0
- package/piggy/logger/index.d.ts.map +1 -0
- package/piggy/open/index.d.ts +4 -0
- package/piggy/open/index.d.ts.map +1 -0
- package/piggy/register/index.d.ts +7 -0
- package/piggy/register/index.d.ts.map +1 -0
- package/piggy/register/index.ts +1 -1
- package/piggy/server/index.d.ts +21 -0
- package/piggy/server/index.d.ts.map +1 -0
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
## Why nothing-browser?
|
|
24
42
|
|
|
25
|
-
|
|
|
26
|
-
|
|
27
|
-
| Imports
|
|
28
|
-
| Lines to scrape a site | **~20**
|
|
29
|
-
| Fingerprint spoofing
|
|
30
|
-
| Network capture
|
|
31
|
-
| Built-in API server
|
|
32
|
-
| Cloudflare bypass
|
|
33
|
-
|
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
56
|
-
| `nothing-browser-headless` | No window, no GPU
|
|
57
|
-
| `nothing-browser-headful` | Visible
|
|
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
|
|
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
|
|
84
|
+
Then download the binary and place it in your project root.
|
|
70
85
|
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
```bash
|
|
98
|
+
# Full browser (system-wide)
|
|
87
99
|
sudo dpkg -i nothing-browser_*_amd64.deb
|
|
88
100
|
```
|
|
101
|
+
</details>
|
|
89
102
|
|
|
90
|
-
|
|
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
|
-
|
|
109
|
-
|
|
106
|
+
Download the `.zip` → extract → place `.exe` in your project root.
|
|
107
|
+
</details>
|
|
110
108
|
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
120
|
+
// Headless – no display, runs anywhere (default)
|
|
121
|
+
await piggy.launch({ mode: "tab", binary: "headless" });
|
|
133
122
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
```ts
|
|
123
|
+
// Headful – visible window for debugging
|
|
137
124
|
await piggy.launch({ mode: "tab", binary: "headful" });
|
|
138
125
|
```
|
|
139
126
|
|
|
140
|
-
|
|
127
|
+
Switching is just changing one word.
|
|
141
128
|
|
|
142
129
|
---
|
|
143
130
|
|
|
144
|
-
## Examples
|
|
131
|
+
## Key Features (with Examples)
|
|
145
132
|
|
|
146
|
-
### 🔥
|
|
133
|
+
### 🔥 Browser → Node.js RPC (`exposeFunction`)
|
|
147
134
|
|
|
148
|
-
Call Node.js functions directly from browser JavaScript.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
664
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
175
|
+
Turn your scraper into a REST API.
|
|
722
176
|
|
|
723
177
|
```ts
|
|
724
|
-
|
|
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
|
-
|
|
187
|
+
## API Reference (Quick)
|
|
827
188
|
|
|
828
|
-
|
|
829
|
-
piggy.actHuman(true);
|
|
189
|
+
### Core
|
|
830
190
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
996
|
-
2.
|
|
997
|
-
3.
|
|
998
|
-
4.
|
|
999
|
-
5.
|
|
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
|
|
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
|
|
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
|
-
|
|
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 🇰🇪*
|