grab-url 0.9.132
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/grab-api.cjs.js +2 -0
- package/dist/grab-api.cjs.js.map +1 -0
- package/dist/grab-api.d.ts +321 -0
- package/dist/grab-api.es.js +495 -0
- package/dist/grab-api.es.js.map +1 -0
- package/dist/icons.cjs.js +2 -0
- package/dist/icons.cjs.js.map +1 -0
- package/dist/icons.d.ts +235 -0
- package/dist/icons.es.js +114 -0
- package/dist/icons.es.js.map +1 -0
- package/package.json +78 -0
- package/readme.md +156 -0
- package/src/grab-cli.js +316 -0
- package/src/grab-url-cli.js +2013 -0
package/readme.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
|
|
2
|
+
<p align="center">
|
|
3
|
+
<img width="400px" src="https://i.imgur.com/qrQWkeb.png" />
|
|
4
|
+
</p>
|
|
5
|
+
<p align="center">
|
|
6
|
+
<br />
|
|
7
|
+
<a href="https://npmjs.org/package/grab-api.js">
|
|
8
|
+
<img src="https://i.imgur.com/ifE8SbX.png"
|
|
9
|
+
alt="NPM badge" />
|
|
10
|
+
</a>
|
|
11
|
+
</p>
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://discord.gg/SJdBqBz3tV"><img src="https://img.shields.io/discord/1110227955554209923.svg?label=Chat&logo=Discord&colorB=7289da&style=flat" alt="Join Discord" /></a>
|
|
14
|
+
<a href="https://github.com/vtempest/grab-api/discussions">
|
|
15
|
+
<img alt="GitHub Stars" src="https://img.shields.io/github/stars/vtempest/grab-api" /></a>
|
|
16
|
+
<a href="https://npmjs.org/package/grab-api.js"><img alt="NPM Version" src="https://img.shields.io/npm/v/grab-api.js" /></a>
|
|
17
|
+
<a href="https://bundlephobia.com/package/grab-api.js"><img src="https://img.shields.io/bundlephobia/minzip/grab-api.js" /></a>
|
|
18
|
+
<a href="https://github.com/vtempest/grab-API/discussions"><img alt="GitHub Discussions"
|
|
19
|
+
src="https://img.shields.io/github/discussions/vtempest/grab-API" /></a>
|
|
20
|
+
<a href="https://github.blog/developer-skills/github/beginners-guide-to-github-creating-a-pull-request/"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"/></a>
|
|
21
|
+
<a href="https://codespaces.new/vtempest/grab-API"><img src="https://github.com/codespaces/badge.svg" width="150" height="20"/></a>
|
|
22
|
+
</p>
|
|
23
|
+
<h3 align="center">
|
|
24
|
+
<a href="https://grab.js.org"> 📑 Docs (grab.js.org)</a>
|
|
25
|
+
<a href="https://grab.js.org/guide/Examples"> 🎯 Example Strategies </a>
|
|
26
|
+
</h3>
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
npm i grab-api.js
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### GRAB: Generate Request to API from Browser
|
|
33
|
+
|
|
34
|
+
1. **GRAB is the FBEST Request Manager: Functionally Brilliant, Elegantly Simple Tool**: One Function, no dependencies, minimalist syntax, [more features than alternatives](https://grab.js.org/guide/Comparisons)
|
|
35
|
+
2. **Auto-JSON Convert**: Pass parameters and get response or error in JSON, handling other data types as is.
|
|
36
|
+
3. **isLoading Status**: Sets `.isLoading=true` on the pre-initialized response object so you can show a "Loading..." in any framework
|
|
37
|
+
4. **Debug Logging**: Adds global `log()` and prints colored JSON structure, response, timing for requests in test.
|
|
38
|
+
5. **Mock Server Support**: Configure `window.grab.mock` for development and testing environments
|
|
39
|
+
6. **Cancel Duplicates**: Prevent this request if one is ongoing to same path & params, or cancel the ongoing request.
|
|
40
|
+
7. **Timeout & Retry**: Customizable request timeout, default 30s, and auto-retry on error
|
|
41
|
+
8. **DevTools**: `Ctrl+I` overlays webpage with devtools showing all requests and responses, timing, and JSON structure.
|
|
42
|
+
9. **Request History**: Stores all request and response data in global `grab.log` object
|
|
43
|
+
10. **Pagination Infinite Scroll**: Built-in pagination for infinite scroll to auto-load and merge next result page, with scroll position recovery.
|
|
44
|
+
11. **Base URL Based on Environment**: Configure `grab.defaults.baseURL` once at the top, overide with `SERVER_API_URL` in `.env`.
|
|
45
|
+
12. **Frontend Cache**: Set cache headers and retrieve from frontend memory for repeat requests to static data.
|
|
46
|
+
13. **Regrab On Error**: Regrab on timeout error, or on window refocus, or on network change, or on stale data.
|
|
47
|
+
14. **Framework Agnostic**: Alternatives like TanStack work only in component initialization and depend on React & others.
|
|
48
|
+
15. **Globals**: Adds to window in browser or global in Node.js so you only import once: `grab()`, `log()`, `grab.log`, `grab.mock`, `grab.defaults`
|
|
49
|
+
16. **TypeScript Tooltips**: Developers can hover over option names and autocomplete TypeScript.
|
|
50
|
+
17. **Request Stategies**: [🎯 Examples](https://grab.js.org/guide/Examples) show common stategies like debounce, repeat, proxy, unit tests, interceptors, file upload, etc
|
|
51
|
+
18. **Rate Limiting**: Built-in rate limiting to prevent multi-click cascading responses, require to wait seconds between requests.
|
|
52
|
+
19. **Repeat**: Repeat request this many times, or repeat every X seconds to poll for updates.
|
|
53
|
+
20. **Loading Icons**: Import from `grab-api.js/icons` to get enhanced animated loading icons.
|
|
54
|
+
|
|
55
|
+
### Examples
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import grab from 'grab-api.js';
|
|
59
|
+
|
|
60
|
+
let res = $state({}) as {
|
|
61
|
+
results: Array<{title:string}>,
|
|
62
|
+
isLoading: boolean,
|
|
63
|
+
error: string,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
await grab('search', {
|
|
67
|
+
response: res,
|
|
68
|
+
query: "search words",
|
|
69
|
+
post: true
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
grab('user').then(log)
|
|
73
|
+
|
|
74
|
+
//in svelte component
|
|
75
|
+
{#if res.results}
|
|
76
|
+
{res.results}
|
|
77
|
+
{:else if res.isLoading}
|
|
78
|
+
...
|
|
79
|
+
{:else if res.error}
|
|
80
|
+
{res.error}
|
|
81
|
+
{/if}
|
|
82
|
+
|
|
83
|
+
//Setup Mock testing server, response is object or function
|
|
84
|
+
window.grab.mock["search"] = {
|
|
85
|
+
response: (params) => {
|
|
86
|
+
return { results: [{title:`Result about ${params.query}`}] };
|
|
87
|
+
},
|
|
88
|
+
method: "POST"
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
//set defaults for all requests
|
|
92
|
+
grab("", {
|
|
93
|
+
setDefaults: true,
|
|
94
|
+
baseURL: "http://localhost:8080",
|
|
95
|
+
timeout: 30,
|
|
96
|
+
debug: true,
|
|
97
|
+
rateLimit: 1,
|
|
98
|
+
cache: true,
|
|
99
|
+
cancelOngoingIfNew: true,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
grab.defaults.baseURL = "http://localhost:8080/api/";
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Screenshots
|
|
106
|
+
|
|
107
|
+
**Animated SVG Loading Icons with Customizable Colors**
|
|
108
|
+
|
|
109
|
+

|
|
110
|
+
|
|
111
|
+
**Set Types for Tooltips on Request & Response**
|
|
112
|
+
|
|
113
|
+

|
|
114
|
+
|
|
115
|
+
**Debug Colorized log(JSON)**
|
|
116
|
+
|
|
117
|
+

|
|
118
|
+
|
|
119
|
+
**Autocomplete option names**
|
|
120
|
+
|
|
121
|
+

|
|
122
|
+
|
|
123
|
+
**Hover over options for info**
|
|
124
|
+
|
|
125
|
+

|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
### Comparison of HTTP Request Libraries
|
|
129
|
+
|
|
130
|
+
| Feature | [GRAB](https://github.com/vtempest/grab-api) | [Axios](https://github.com/axios/axios) | [TanStack Query](https://github.com/TanStack/query) | [SWR](https://github.com/vercel/swr) | [Alova](https://github.com/alovajs/alova) | [SuperAgent](https://github.com/ladjs/superagent) | [Apisauce](https://github.com/infinitered/apisauce) | [Ky](https://github.com/sindresorhus/ky) |
|
|
131
|
+
| :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- |
|
|
132
|
+
| Size | ✅ 3KB | ❌ 13KB | ❌ 39KB | ❌ 4.2KB | ⚠️ 4KB | ❌ 19KB | ❌ 15KB (with axios) | ⚠️ 4KB |
|
|
133
|
+
| Zero Dependencies | ✅ Yes | ❌ No | ❌ No | ❌ No | ✅ Yes | ❌ No | ❌ Needs Axios | ✅ Yes |
|
|
134
|
+
| isLoading State Handling | ✅ Auto-managed | ❌ Manual | ✅ Yes | ✅ Yes | ✅ Yes | ❌ Manual | ❌ Manual | ❌ Manual |
|
|
135
|
+
| Auto JSON Handling | ✅ Automatic | ✅ Configurable | ❌ Manual | ❌ Manual | ✅ Automatic | ✅ Automatic | ✅ Automatic | ✅ Automatic |
|
|
136
|
+
| Request Deduplication | ✅ Built-in | ❌ No | ✅ Yes | ✅ Yes | ✅ Yes | ❌ No | ❌ No | ❌ No |
|
|
137
|
+
| Caching | ✅ Multi-level | ❌ No | ✅ Advanced | ✅ Advanced | ✅ Multi-level | ❌ No | ❌ No | ❌ No |
|
|
138
|
+
| Mock Testing | ✅ Easy setup | ❌ Needs MSW/etc | ❌ Needs MSW/etc | ❌ Needs MSW/etc | ⚠️ Basic | ❌ Needs separate lib | ❌ Needs separate lib | ❌ Needs MSW/etc |
|
|
139
|
+
| Rate Limiting | ✅ Built-in | ❌ Manual | ❌ Manual | ❌ Manual | ⚠️ Basic | ❌ Manual | ❌ Manual | ❌ Manual |
|
|
140
|
+
| Automatic Retry | ✅ Configurable | ⚠️ Via interceptors | ✅ Built-in | ✅ Built-in | ✅ Built-in | ✅ Built-in | ❌ Manual | ✅ Built-in |
|
|
141
|
+
| Request Cancellation | ✅ Auto + manual | ✅ Manual | ✅ Automatic | ✅ Automatic | ✅ Manual | ✅ Manual | ✅ Manual | ✅ Manual |
|
|
142
|
+
| Pagination Support | ✅ Infinite scroll | ❌ Manual | ✅ Advanced | ⚠️ Basic | ✅ Built-in | ❌ Manual | ❌ Manual | ❌ Manual |
|
|
143
|
+
| Interceptors | ✅ Advanced | ✅ Advanced | ⚠️ Limited | ⚠️ Limited | ✅ Advanced | ✅ Plugins | ✅ Transforms | ✅ Hooks system |
|
|
144
|
+
| Debug Logging | ✅ Colored output | ⚠️ Basic | ✅ DevTools | ✅ DevTools | ⚠️ Basic | ⚠️ Basic | ⚠️ Basic | ⚠️ Basic |
|
|
145
|
+
| Request History | ✅ Built-in | ❌ Manual | ✅ DevTools | ✅ DevTools | ❌ Manual | ❌ Manual | ❌ Manual | ❌ Manual |
|
|
146
|
+
| Easy Syntax | ✅ Minimal | ⚠️ Medium | ❌ High | ❌ High | ⚠️ Medium | ⚠️ Medium | ✅ Low | ✅ Minimal |
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
**Stop trying to make fetch happen!** [*](https://knowyourmeme.com/memes/stop-trying-to-make-fetch-happen)
|
|
150
|
+
|
|
151
|
+
**Why fetch things when you can just GRAB?**
|
|
152
|
+
|
|
153
|
+
**Debugging requests is a bitch. [Make the switch!](https://grab.js.org/guide/Comparisons)**
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
🌟 Star this repo so it will grow and get updates!
|
package/src/grab-cli.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { grab, log } from './grab-api';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
// Lightweight argument parser polyfill
|
|
7
|
+
class ArgParser {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.commands = {};
|
|
10
|
+
this.options = {};
|
|
11
|
+
this.examples = [];
|
|
12
|
+
this.helpText = '';
|
|
13
|
+
this.versionText = '1.0.0';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
usage(text) {
|
|
17
|
+
this.helpText = text;
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
command(pattern, desc, handler) {
|
|
22
|
+
const match = pattern.match(/\$0 <(\w+)>/);
|
|
23
|
+
if (match) {
|
|
24
|
+
this.commands[match[1]] = { desc, handler, required: true };
|
|
25
|
+
}
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
option(name, opts = {}) {
|
|
30
|
+
this.options[name] = opts;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
example(cmd, desc) {
|
|
35
|
+
this.examples.push({ cmd, desc });
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
help() {
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
alias(short, long) {
|
|
44
|
+
if (this.options[long]) {
|
|
45
|
+
this.options[long].alias = short;
|
|
46
|
+
}
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
version(v) {
|
|
51
|
+
if (v) this.versionText = v;
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
strict() {
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
parseSync() {
|
|
60
|
+
const args = process.argv.slice(2);
|
|
61
|
+
const result = {};
|
|
62
|
+
const positional = [];
|
|
63
|
+
|
|
64
|
+
// Check for help
|
|
65
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
66
|
+
this.showHelp();
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check for version
|
|
71
|
+
if (args.includes('--version')) {
|
|
72
|
+
console.log(this.versionText);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < args.length; i++) {
|
|
77
|
+
const arg = args[i];
|
|
78
|
+
|
|
79
|
+
if (arg.startsWith('--')) {
|
|
80
|
+
const [key, value] = arg.split('=');
|
|
81
|
+
const optName = key.slice(2);
|
|
82
|
+
|
|
83
|
+
if (value !== undefined) {
|
|
84
|
+
result[optName] = this.coerceValue(optName, value);
|
|
85
|
+
} else if (this.options[optName]?.type === 'boolean') {
|
|
86
|
+
result[optName] = true;
|
|
87
|
+
} else {
|
|
88
|
+
const nextArg = args[i + 1];
|
|
89
|
+
if (nextArg && !nextArg.startsWith('-')) {
|
|
90
|
+
result[optName] = this.coerceValue(optName, nextArg);
|
|
91
|
+
i++; // Skip next arg
|
|
92
|
+
} else {
|
|
93
|
+
result[optName] = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} else if (arg.startsWith('-') && arg.length === 2) {
|
|
97
|
+
const shortFlag = arg[1];
|
|
98
|
+
const longName = this.findLongName(shortFlag);
|
|
99
|
+
|
|
100
|
+
if (longName) {
|
|
101
|
+
if (this.options[longName]?.type === 'boolean') {
|
|
102
|
+
result[longName] = true;
|
|
103
|
+
} else {
|
|
104
|
+
const nextArg = args[i + 1];
|
|
105
|
+
if (nextArg && !nextArg.startsWith('-')) {
|
|
106
|
+
result[longName] = this.coerceValue(longName, nextArg);
|
|
107
|
+
i++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
positional.push(arg);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Handle positional arguments
|
|
117
|
+
if (positional.length > 0) {
|
|
118
|
+
result.url = positional[0];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Set defaults
|
|
122
|
+
Object.keys(this.options).forEach(key => {
|
|
123
|
+
if (result[key] === undefined && this.options[key].default !== undefined) {
|
|
124
|
+
result[key] = this.options[key].default;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Validate required positional args
|
|
129
|
+
if (!result.url && this.commands.url?.required) {
|
|
130
|
+
console.error('Error: Missing required argument: url');
|
|
131
|
+
this.showHelp();
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
coerceValue(optName, value) {
|
|
139
|
+
const opt = this.options[optName];
|
|
140
|
+
if (!opt) return value;
|
|
141
|
+
|
|
142
|
+
if (opt.coerce) {
|
|
143
|
+
return opt.coerce(value);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
switch (opt.type) {
|
|
147
|
+
case 'number':
|
|
148
|
+
return Number(value);
|
|
149
|
+
case 'boolean':
|
|
150
|
+
return value === 'true' || value === '1';
|
|
151
|
+
default:
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
findLongName(shortFlag) {
|
|
157
|
+
return Object.keys(this.options).find(key =>
|
|
158
|
+
this.options[key].alias === shortFlag
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
showHelp() {
|
|
163
|
+
console.log(this.helpText || 'Usage: grab <url> [options]');
|
|
164
|
+
console.log('\nPositional arguments:');
|
|
165
|
+
Object.keys(this.commands).forEach(cmd => {
|
|
166
|
+
console.log(` ${cmd.padEnd(20)} ${this.commands[cmd].desc}`);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
console.log('\nOptions:');
|
|
170
|
+
Object.keys(this.options).forEach(key => {
|
|
171
|
+
const opt = this.options[key];
|
|
172
|
+
const flags = opt.alias ? `-${opt.alias}, --${key}` : `--${key}`;
|
|
173
|
+
console.log(` ${flags.padEnd(20)} ${opt.describe || ''}`);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (this.examples.length > 0) {
|
|
177
|
+
console.log('\nExamples:');
|
|
178
|
+
this.examples.forEach(ex => {
|
|
179
|
+
console.log(` ${ex.cmd}`);
|
|
180
|
+
console.log(` ${ex.desc}`);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Create parser instance
|
|
187
|
+
const createParser = () => new ArgParser();
|
|
188
|
+
|
|
189
|
+
// Parse arguments with custom parser
|
|
190
|
+
const argv = createParser()
|
|
191
|
+
.usage('Usage: grab <url> [options]')
|
|
192
|
+
.command('$0 <url>', 'Fetch data from API endpoint')
|
|
193
|
+
.option('x', {
|
|
194
|
+
alias: 'exec',
|
|
195
|
+
type: 'boolean',
|
|
196
|
+
default: false,
|
|
197
|
+
describe: 'Execute flag (functionality TBD)'
|
|
198
|
+
})
|
|
199
|
+
.option('no-save', {
|
|
200
|
+
type: 'boolean',
|
|
201
|
+
default: false,
|
|
202
|
+
describe: 'Don\'t save output to file, just print to console'
|
|
203
|
+
})
|
|
204
|
+
.option('output', {
|
|
205
|
+
alias: 'o',
|
|
206
|
+
type: 'string',
|
|
207
|
+
describe: 'Output filename (default: output.json)',
|
|
208
|
+
default: null
|
|
209
|
+
})
|
|
210
|
+
.option('params', {
|
|
211
|
+
alias: 'p',
|
|
212
|
+
type: 'string',
|
|
213
|
+
describe: 'JSON string of query parameters (e.g., \'{"key":"value"}\')',
|
|
214
|
+
coerce: (arg) => {
|
|
215
|
+
if (!arg) return {};
|
|
216
|
+
try {
|
|
217
|
+
return JSON.parse(arg);
|
|
218
|
+
} catch (e) {
|
|
219
|
+
throw new Error(`Invalid JSON in params: ${arg}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
.example('grab https://api.example.com/data', 'Fetch data and save to output.json')
|
|
224
|
+
.example('grab https://api.example.com/data --no-save', 'Fetch data and print to console')
|
|
225
|
+
.example('grab https://api.example.com/data -o result.json', 'Save output to result.json')
|
|
226
|
+
.example('grab https://api.example.com/data -p \'{"limit":10,"page":1}\'', 'Pass query parameters')
|
|
227
|
+
.help()
|
|
228
|
+
.alias('h', 'help')
|
|
229
|
+
.version()
|
|
230
|
+
.strict()
|
|
231
|
+
.parseSync();
|
|
232
|
+
|
|
233
|
+
// Extract values from parsed arguments
|
|
234
|
+
const { url, x: execFlag, 'no-save': noSave, output: outputFile, params = {} } = argv;
|
|
235
|
+
// params.debug = true;
|
|
236
|
+
|
|
237
|
+
// Validate URL
|
|
238
|
+
if (!url || (!url.startsWith('http://') && !url.startsWith('https://'))) {
|
|
239
|
+
log('Error: URL must start with http:// or https://');
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
(async () => {
|
|
244
|
+
const startTime = process.hrtime(); // High-res timer
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const res = await grab(url, params);
|
|
248
|
+
|
|
249
|
+
if (res.error)
|
|
250
|
+
log(`\n\nStatus: ❌ ${res.error}`);
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
let filePath = null;
|
|
254
|
+
let outputData;
|
|
255
|
+
let isTextData = false;
|
|
256
|
+
|
|
257
|
+
// Determine data type and prepare output
|
|
258
|
+
if (typeof res.data === 'string') {
|
|
259
|
+
outputData = res.data;
|
|
260
|
+
isTextData = true;
|
|
261
|
+
} else if (Buffer.isBuffer(res.data) || res.data instanceof Uint8Array) {
|
|
262
|
+
// Binary data (like video files)
|
|
263
|
+
outputData = res.data;
|
|
264
|
+
isTextData = false;
|
|
265
|
+
} else if (res.data instanceof Blob) {
|
|
266
|
+
// Convert Blob to Buffer for file writing
|
|
267
|
+
const arrayBuffer = await res.data.arrayBuffer();
|
|
268
|
+
outputData = Buffer.from(arrayBuffer);
|
|
269
|
+
isTextData = false;
|
|
270
|
+
} else if (res.data && typeof res.data === 'object') {
|
|
271
|
+
// JSON or other objects
|
|
272
|
+
outputData = JSON.stringify(res.data, null, 2);
|
|
273
|
+
isTextData = true;
|
|
274
|
+
} else {
|
|
275
|
+
// Fallback - try to stringify
|
|
276
|
+
outputData = String(res.data);
|
|
277
|
+
isTextData = true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!noSave) {
|
|
281
|
+
// Determine file extension based on URL or data type
|
|
282
|
+
const urlPath = new URL(url).pathname;
|
|
283
|
+
const urlExt = path.extname(urlPath);
|
|
284
|
+
const defaultExt = isTextData ? '.json' : (urlExt || '.bin');
|
|
285
|
+
|
|
286
|
+
filePath = outputFile
|
|
287
|
+
? path.resolve(outputFile)
|
|
288
|
+
: path.resolve(process.cwd(), `output${defaultExt}`);
|
|
289
|
+
|
|
290
|
+
// Write file with appropriate encoding
|
|
291
|
+
if (isTextData) {
|
|
292
|
+
fs.writeFileSync(filePath, outputData, 'utf8');
|
|
293
|
+
} else {
|
|
294
|
+
fs.writeFileSync(filePath, outputData);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Calculate elapsed time
|
|
298
|
+
const [seconds, nanoseconds] = process.hrtime(startTime);
|
|
299
|
+
const elapsedMs = (seconds + nanoseconds / 1e9).toFixed(2);
|
|
300
|
+
const stats = fs.statSync(filePath);
|
|
301
|
+
const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(1);
|
|
302
|
+
|
|
303
|
+
log(`⏱️ ${elapsedMs}s 📦 ${fileSizeMB}MB ✅ Saved to: ${filePath}`);
|
|
304
|
+
} else {
|
|
305
|
+
// For console output, only show text data
|
|
306
|
+
if (isTextData) {
|
|
307
|
+
// log(outputData);
|
|
308
|
+
} else {
|
|
309
|
+
log(`Binary data received (${outputData.length} bytes). Use --output to save to file.`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
log(`Error: ${error.message}`, {color: 'red'});
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
})();
|