grab-url 1.0.7 → 1.0.8

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/src/grab-api.ts CHANGED
@@ -1,29 +1,15 @@
1
1
  import {
2
2
  printJSONStructure,
3
3
  log,
4
- showAlert,
5
- setupDevTools,
6
4
  type LogOptions,
7
- } from "./log";
8
-
9
- /**
10
- * TODO
11
- * - react tests
12
- * - grab error popup and dev tool
13
- * - show net log in alert
14
- * - progress
15
- * - pagination working
16
- * - tests in stackblitz
17
- * - loading icons
18
- * - cache revalidation
19
- */
5
+ } from "./log-json";
20
6
 
21
7
  /**
22
8
  * ### GRAB: Generate Request to API from Browser
23
- * ![GrabAPILogo](https://i.imgur.com/qrQWkeb.png)
9
+ * ![GrabAPILogo](https://i.imgur.com/xWD7gyV.png)
24
10
  *
25
11
  * 1. **GRAB is the FBEST Request Manager: Functionally Brilliant, Elegantly Simple Tool**: One Function, no dependencies,
26
- * minimalist syntax, [more features than alternatives](https://grab.js.org/guide/Comparisons)
12
+ * minimalist syntax, [more features than alternatives](https://grab.js.org/docs/Comparisons)
27
13
  * 2. **Auto-JSON Convert**: Pass parameters and get response or error in JSON, handling other data types as is.
28
14
  * 3. **isLoading Status**: Sets `.isLoading=true` on the pre-initialized response object so you can show a "Loading..." in any framework
29
15
  * 4. **Debug Logging**: Adds global `log()` and prints colored JSON structure, response, timing for requests in test.
@@ -39,7 +25,7 @@ import {
39
25
  * 14. **Framework Agnostic**: Alternatives like TanStack work only in component initialization and depend on React & others.
40
26
  * 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`
41
27
  * 16. **TypeScript Tooltips**: Developers can hover over option names and autocomplete TypeScript.
42
- * 17. **Request Stategies**: [🎯 Examples](https://grab.js.org/guide/Examples) show common stategies like debounce, repeat, proxy, unit tests, interceptors, file upload, etc
28
+ * 17. **Request Stategies**: [🎯 Examples](https://grab.js.org/docs/Examples) show common stategies like debounce, repeat, proxy, unit tests, interceptors, file upload, etc
43
29
  * 18. **Rate Limiting**: Built-in rate limiting to prevent multi-click cascading responses, require to wait seconds between requests.
44
30
  * 19. **Repeat**: Repeat request this many times, or repeat every X seconds to poll for updates.
45
31
  * 20. **Loading Icons**: Import from `grab-url/icons` to get enhanced animated loading icons.
@@ -77,33 +63,27 @@ import {
77
63
  * @param {any} [...params] All other params become GET params, POST body, and other methods.
78
64
  * @returns {Promise<Object>} The response object with resulting data or .error if error.
79
65
  * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)
80
- * @see [🎯 Examples](https://grab.js.org/guide/Examples) [📑 Docs](https://grab.js.org)
81
- * @example import grab from 'grab-url';
82
- * let res = {};
83
- * await grab('search', {
84
- * response: res,
85
- * query: "search words"
86
- * })
66
+ * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org)
87
67
  */
88
68
  export default async function grab<TResponse = any, TParams = any>(
89
69
  path: string,
90
70
  options: GrabOptions<TResponse, TParams>
91
71
  ): Promise<GrabResponse<TResponse>> {
92
- let {
72
+ var {
93
73
  headers,
94
74
  response = {} as any, // Pre-initialized object to set the response in. isLoading and error are also set on this object.
95
75
  method = options.post // set post: true for POST, omit for GET
96
76
  ? "POST"
97
77
  : options.put
98
- ? "PUT"
99
- : options.patch
100
- ? "PATCH"
101
- : "GET",
78
+ ? "PUT"
79
+ : options.patch
80
+ ? "PATCH"
81
+ : "GET",
102
82
  cache = false, // Enable/disable frontend caching
103
83
  cacheForTime = 60, // Seconds to consider data stale and invalidate cache
104
84
  timeout = 30, // Request timeout in seconds
105
85
  baseURL = (typeof process !== "undefined" && process.env.SERVER_API_URL) ||
106
- "/api/", // Use env var or default to /api/
86
+ "/api/", // Use env var or default to /api/
107
87
  cancelOngoingIfNew = false, // Cancel previous request for same path
108
88
  cancelNewIfOngoing = false, // Don't make new request if one is ongoing
109
89
  rateLimit = 0, // Minimum seconds between requests
@@ -127,7 +107,7 @@ export default async function grab<TResponse = any, TParams = any>(
127
107
  put = false,
128
108
  patch = false,
129
109
  body = null,
130
- ...params // All other params become request params/query
110
+ ...params // All other params become request params/query
131
111
  } = {
132
112
  // Destructure options with defaults, merging with any globally set defaults
133
113
  ...(typeof window !== "undefined"
@@ -143,7 +123,7 @@ export default async function grab<TResponse = any, TParams = any>(
143
123
  else if (!s("/") && !baseURL.endsWith("/")) path = "/" + path;
144
124
  else if (s("/") && baseURL.endsWith("/")) path = path.slice(1);
145
125
 
146
- // try {
126
+ try {
147
127
  //handle debounce
148
128
  if (debounce > 0)
149
129
  return (await debouncer(async () => {
@@ -181,7 +161,7 @@ export default async function grab<TResponse = any, TParams = any>(
181
161
  }
182
162
 
183
163
  // regrab on stale, on window refocus, on network
184
- if (typeof window !== undefined) {
164
+ if (typeof window !== "undefined") {
185
165
  const regrab = async () => await grab(path, { ...options, cache: false });
186
166
  if (regrabOnStale && cache) setTimeout(regrab, 1000 * cacheForTime);
187
167
  if (regrabOnNetwork) window.addEventListener("online", regrab);
@@ -203,21 +183,22 @@ export default async function grab<TResponse = any, TParams = any>(
203
183
  // Configure infinite scroll behavior if enabled
204
184
  // Attaches scroll listener to specified element that triggers next page load
205
185
  if (infiniteScroll?.length && typeof paginateElement !== "undefined"
206
- && typeof window !== "undefined") {
186
+ && typeof window !== "undefined") {
207
187
  let paginateDOM =
208
188
  typeof paginateElement === "string"
209
189
  ? document.querySelector(paginateElement)
210
190
  : paginateElement;
211
191
 
212
192
  if (!paginateDOM) log("paginateDOM not found", { color: "red" });
213
-
214
- if (window.scrollListener)
193
+ else if (window.scrollListener
194
+ && typeof paginateDOM !== "undefined"
195
+ && typeof paginateDOM.removeEventListener === "function")
215
196
  paginateDOM.removeEventListener("scroll", window.scrollListener);
216
197
 
217
198
  // Your modified scroll listener with position saving
218
199
  window.scrollListener = async (event) => {
219
200
  const t = event.target as HTMLElement;
220
-
201
+
221
202
  // Save scroll position whenever user scrolls
222
203
  localStorage.setItem(
223
204
  "scroll",
@@ -233,8 +214,8 @@ export default async function grab<TResponse = any, TParams = any>(
233
214
  }
234
215
  };
235
216
 
236
- if (paginateDOM)
237
- paginateDOM.addEventListener("scroll", window.scrollListener);
217
+ if (paginateDOM)
218
+ paginateDOM.addEventListener("scroll", window.scrollListener);
238
219
  }
239
220
 
240
221
  // Check request history for a previous request with same path/params
@@ -280,8 +261,7 @@ export default async function grab<TResponse = any, TParams = any>(
280
261
 
281
262
  // Update page tracking
282
263
  if (priorRequest) priorRequest.currentPage = pageNumber;
283
- // @ts-ignore
284
- params[paginateKey] = pageNumber;
264
+ params = { ...params, [paginateKey]: pageNumber };
285
265
  }
286
266
 
287
267
  // Set loading state on response object
@@ -377,7 +357,7 @@ export default async function grab<TResponse = any, TParams = any>(
377
357
  // Make actual API request and handle response based on content type
378
358
  res = await fetch(baseURL + path + paramsGETRequest, fetchParams).catch(
379
359
  (e) => {
380
- throw new Error(e);
360
+ throw new Error(e.message);
381
361
  }
382
362
  );
383
363
 
@@ -394,8 +374,8 @@ export default async function grab<TResponse = any, TParams = any>(
394
374
  ? res && res.json()
395
375
  : type.includes("application/pdf") ||
396
376
  type.includes("application/octet-stream")
397
- ? res.blob()
398
- : res.text()
377
+ ? res.blob()
378
+ : res.text()
399
379
  : res.json()
400
380
  ).catch((e) => {
401
381
  throw new Error("Error parsing response: " + e);
@@ -427,15 +407,15 @@ export default async function grab<TResponse = any, TParams = any>(
427
407
  if (debug) {
428
408
  logger(
429
409
  "Path:" +
430
- baseURL +
431
- path +
432
- paramsGETRequest +
433
- "\n" +
434
- JSON.stringify(options, null, 2) +
435
- "\nTime: " +
436
- elapsedTime +
437
- "s\nResponse: " +
438
- printJSONStructure(res)
410
+ baseURL +
411
+ path +
412
+ paramsGETRequest +
413
+ "\n" +
414
+ JSON.stringify(options, null, 2) +
415
+ "\nTime: " +
416
+ elapsedTime +
417
+ "s\nResponse: " +
418
+ printJSONStructure(res)
439
419
  );
440
420
  // console.log(res);
441
421
  }
@@ -467,46 +447,47 @@ export default async function grab<TResponse = any, TParams = any>(
467
447
  if (resFunction) response = resFunction(response);
468
448
 
469
449
  return response;
470
- // } catch (error) {
471
- // // Handle any errors that occurred during request processing
472
- // let errorMessage =
473
- // "Error: " + error.message + "\nPath:" + baseURL + path + "\n";
474
- // JSON.stringify(params);
475
-
476
- // if (typeof onError === "function")
477
- // onError(error.message, baseURL + path, params);
478
-
479
- // // Retry request if retries are configured and attempts remain
480
- // if (options.retryAttempts > 0)
481
- // return await grab(path, {
482
- // ...options,
483
- // retryAttempts: --options.retryAttempts,
484
- // });
485
-
486
- // // Update error state in response object
487
- // // Do not show errors for duplicate aborted requests
488
- // if (!error.message.includes("signal") && options.debug) {
489
- // logger(errorMessage, { color: "red" });
490
- // if (debug && typeof document !== undefined) showAlert(errorMessage);
491
- // }
492
- // response.error = error.message;
493
- // if (typeof response === "function") {
494
- // response.data = response({ isLoading: undefined, error: error.message });
495
- // response = response.data;
496
- // } else delete response?.isLoading;
497
-
498
- // // Log error in request history
499
- // if (typeof grab.log != "undefined")
500
- // grab.log?.unshift({
501
- // path,
502
- // request: JSON.stringify(params),
503
- // error: error.message,
504
- // });
505
-
506
- // // if (typeof options.response === "function")
507
- // // response = options.response(response);
508
- // return response;
509
- // }
450
+ } catch (error) {
451
+ // Handle any errors that occurred during request processing
452
+ let errorMessage =
453
+ "Error: " + error.message + "\nPath:" + baseURL + path + "\n";
454
+ // JSON.stringify(params);
455
+
456
+ // if onError hook is passed
457
+ if (typeof onError === "function")
458
+ onError(error.message, baseURL + path, params);
459
+
460
+ // Retry request if retries are configured and attempts remain
461
+ if (options.retryAttempts > 0)
462
+ return await grab(path, {
463
+ ...options,
464
+ retryAttempts: --options.retryAttempts,
465
+ });
466
+
467
+ // Update error state in response object
468
+ // Do not show errors for duplicate aborted requests
469
+ if (!error.message.includes("signal") && options.debug) {
470
+ logger(errorMessage, { color: "red" });
471
+ if (debug && typeof document !== "undefined") showAlert(errorMessage);
472
+ }
473
+ response.error = error.message;
474
+ if (typeof response === "function") {
475
+ response.data = response({ isLoading: undefined, error: error.message });
476
+ response = response.data;
477
+ } else delete response?.isLoading;
478
+
479
+ // Log error in request history
480
+ if (typeof grab.log != "undefined")
481
+ grab.log?.unshift({
482
+ path,
483
+ request: JSON.stringify(params),
484
+ error: error.message,
485
+ });
486
+
487
+ // if (typeof options.response === "function")
488
+ // response = options.response(response);
489
+ return response;
490
+ }
510
491
  }
511
492
 
512
493
  /**
@@ -517,8 +498,8 @@ export default async function grab<TResponse = any, TParams = any>(
517
498
  */
518
499
  grab.instance =
519
500
  (defaults = {}) =>
520
- (path, options = {}) =>
521
- grab(path, { ...defaults, ...options });
501
+ (path, options = {}) =>
502
+ grab(path, { ...defaults, ...options });
522
503
 
523
504
  // delays execution so that future calls may override and only executes last one
524
505
  const debouncer = async (func, wait) => {
@@ -536,7 +517,6 @@ const debouncer = async (func, wait) => {
536
517
  // Add globals to window in browser, or global in Node.js
537
518
  if (typeof window !== "undefined") {
538
519
  window.log = log;
539
- // @ts-ignore
540
520
  window.grab = grab;
541
521
 
542
522
  window.grab.log = [];
@@ -568,6 +548,73 @@ if (typeof window !== "undefined") {
568
548
  globalThis.grab = grab.instance();
569
549
  }
570
550
 
551
+
552
+
553
+ /**
554
+ * Shows message in a modal overlay with scrollable message stack
555
+ * and is easier to dismiss unlike alert() which blocks window.
556
+ * Creates a semi-transparent overlay with a white box containing the message.
557
+ * @param {string} msg - The message to display
558
+ */
559
+ export function showAlert(msg) {
560
+ if (typeof document === "undefined") return;
561
+ let o = document.getElementById("alert-overlay"),
562
+ list;
563
+
564
+ // Create overlay and alert box if they don't exist
565
+ if (!o) {
566
+ o = document.body.appendChild(document.createElement("div"));
567
+ o.id = "alert-overlay";
568
+ o.setAttribute(
569
+ "style",
570
+ "position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center"
571
+ );
572
+ o.innerHTML = `<div id="alert-box" style="background:#fff;padding:1.5em 2em;border-radius:8px;box-shadow:0 2px 16px #0003;min-width:220px;max-height:80vh;position:relative;display:flex;flex-direction:column;">
573
+ <button id="close-alert" style="position:absolute;top:12px;right:20px;font-size:1.5em;background:none;border:none;cursor:pointer;color:black;">&times;</button>
574
+ <div id="alert-list" style="overflow:auto;flex:1;"></div>
575
+ </div>`;
576
+
577
+ // Add click handlers to close overlay
578
+ o.addEventListener("click", (e) => e.target == o && o.remove());
579
+ document.getElementById("close-alert").onclick = () => o.remove();
580
+ }
581
+
582
+ list = o.querySelector("#alert-list");
583
+
584
+ // Add new message to list
585
+ list.innerHTML += `<div style="border-bottom:1px solid #333; font-size:1.2em;margin:0.5em 0;">${msg}</div>`;
586
+ }
587
+
588
+ /**
589
+ * Sets up development tools for debugging API requests
590
+ * Adds a keyboard shortcut (Ctrl+Alt+I) that shows a modal with request history
591
+ * Each request entry shows:
592
+ * - Request path
593
+ * - Request details
594
+ * - Response data
595
+ * - Timestamp
596
+ */
597
+ export function setupDevTools() {
598
+ // Keyboard shortcut (Ctrl+Alt+I) to toggle debug view
599
+ document.addEventListener("keydown", (e) => {
600
+ if (e.key === "i" && e.ctrlKey && e.altKey) {
601
+ // Create HTML of the grab.log requests
602
+ let html = " ";
603
+ for (let request of grab.log) {
604
+ html += `<div style="margin-bottom:1em; border-bottom:1px solid #ccc; padding-bottom:1em;">
605
+ <b>Path:</b> ${request.path}<br>
606
+ <b>Request:</b> ${printJSONStructure(request.request, 0, 'html')}<br>
607
+ <b>Response:</b> ${printJSONStructure(request.response, 0, 'html')}<br>
608
+ <b>Time:</b> ${new Date(request.lastFetchTime).toLocaleString()}
609
+ </div>`;
610
+ }
611
+ showAlert(html);
612
+ }
613
+ });
614
+ }
615
+
616
+
617
+
571
618
  /***************** TYPESCRIPT INTERFACES *****************/
572
619
 
573
620
  // Core response object that gets populated with API response data
@@ -693,23 +740,23 @@ export interface GrabGlobal {
693
740
  export interface GrabFunction {
694
741
  /**
695
742
  * ### GRAB: Generate Request to API from Browser
696
- * ![grabAPILogo](https://i.imgur.com/qrQWkeb.png)
743
+ * ![grabAPILogo](https://i.imgur.com/xWD7gyV.png)
697
744
  * Make API request with path
698
745
  * @returns {Promise<Object>} The response object with resulting data or .error if error.
699
746
  * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)
700
- * @see [🎯 Examples](https://grab.js.org/guide/Examples) [📑 Docs](https://grab.js.org/lib)
747
+ * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org/lib)
701
748
  */
702
- <TResponse = any, TParams = Record<string, any>>(path: string): Promise<
749
+ <TResponse = any, TParams = Record<string, any>>(path: string, options?: GrabOptions<TResponse, TParams>): Promise<
703
750
  GrabResponse<TResponse>
704
751
  >;
705
752
 
706
753
  /**
707
754
  * ### GRAB: Generate Request to API from Browser
708
- * ![grabAPILogo](https://i.imgur.com/qrQWkeb.png)
755
+ * ![grabAPILogo](https://i.imgur.com/xWD7gyV.png)
709
756
  * Make API request with path and options/parameters
710
757
  * @returns {Promise<Object>} The response object with resulting data or .error if error.
711
758
  * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)
712
- * @see [🎯 Examples](https://grab.js.org/guide/Examples) [📑 Docs](https://grab.js.org/lib)
759
+ * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org/lib)
713
760
  */
714
761
  <TResponse = any, TParams = Record<string, any>>(
715
762
  path: string,
@@ -748,15 +795,6 @@ export interface printJSONStructureFunction {
748
795
  (obj: any): string;
749
796
  }
750
797
 
751
- // Helper type for creating typed API clients
752
- // export type TypedGrabFunction = <
753
- // TResponse = any,
754
- // TParams = Record<string, any>
755
- // >(
756
- // path: string,
757
- // config?: GrabOptions<TResponse, TParams>
758
- // ) => Promise<GrabResponse<TResponse>>;
759
-
760
798
  declare global {
761
799
  // Browser globals
762
800
  interface Window {
@@ -778,4 +816,4 @@ declare global {
778
816
  var grab: GrabFunction;
779
817
  }
780
818
 
781
- export { grab, log, showAlert, printJSONStructure };
819
+ export { grab, log, printJSONStructure };
package/src/grab-url.js CHANGED
@@ -4,48 +4,17 @@ import fs from 'fs';
4
4
  import path from 'path';
5
5
  import { pipeline } from 'stream/promises';
6
6
  import { Readable } from 'stream';
7
- import cliProgress from 'cli-progress';
8
- import chalk from 'chalk';
9
- import ora from 'ora';
10
- import Table from 'cli-table3';
11
- import grab, { log } from '../dist/grab-api.es.js';
12
7
  import readline from 'readline';
13
8
  import { readFileSync } from 'fs';
14
- import { fileURLToPath, pathToFileURL } from 'url';
15
- import { dirname, join } from 'path';
16
-
17
- // Try multiple possible paths for spinners.json
18
- const __filename = fileURLToPath(import.meta.url);
19
- const __dirname = dirname(__filename);
20
-
21
- let spinners;
22
- try {
23
- // Try the icons/cli path first (where it actually exists)
24
- spinners = JSON.parse(readFileSync(join(__dirname, 'icons', 'cli', 'spinners.json'), 'utf8'));
25
- } catch (error) {
26
- try {
27
- // Try the local path
28
- spinners = JSON.parse(readFileSync(join(__dirname, 'spinners.json'), 'utf8'));
29
- } catch (error2) {
30
- try {
31
- // Try the parent directory (src)
32
- spinners = JSON.parse(readFileSync(join(__dirname, '..', 'spinners.json'), 'utf8'));
33
- } catch (error3) {
34
- try {
35
- // Try the current working directory
36
- spinners = JSON.parse(readFileSync(join(process.cwd(), 'src', 'spinners.json'), 'utf8'));
37
- } catch (error4) {
38
- // Fallback to default spinners if file not found
39
- console.warn('Could not load spinners.json, using defaults');
40
- spinners = {
41
- dots: { frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] },
42
- line: { frames: ['-', '\\', '|', '/'] },
43
- arrow: { frames: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'] }
44
- };
45
- }
46
- }
47
- }
48
- }
9
+ import { join } from 'path';
10
+ import spinners from './icons/cli/spinners';
11
+ import grab, { log } from './grab-api';
12
+ import { pathToFileURL } from 'url';
13
+
14
+ import cliProgress from 'cli-progress';
15
+ import chalk from 'chalk';
16
+ // Use cli-spinners for spinner animations
17
+ const __dirname = dirname(import.meta.url);
49
18
 
50
19
  // --- ArgParser from grab-cli.js ---
51
20
  class ArgParser {